Add namespaceSelector support for NetworkPolicies

This patch adds namespaceSelector support for ingress and egress
Network Policies.

In addition it handles the case where either no from/to or not ports
section appears on the ingress or egress block

Partially Implements: blueprint k8s-network-policies
Change-Id: I7bfb1275221b76ad811ac6baff99e642d31f7e0a
This commit is contained in:
Luis Tomas Bolivar 2018-11-22 19:01:59 +01:00
parent 4705b69d1a
commit 543b8a2e05
3 changed files with 247 additions and 27 deletions

View File

@ -50,11 +50,17 @@ Testing the network policy support functionality
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
project: default
ports:
- protocol: TCP
port: 6379
egress:
- to:
- namespaceSelector:
matchLabels:
project: default
ports:
- protocol: TCP
port: 5978
@ -121,11 +127,17 @@ Testing the network policy support functionality
networkpolicy_spec:
egress:
- to:
- namespaceSelector:
matchLabels:
project: default
ports:
- port: 5978
protocol: TCP
ingress:
- from:
- namespaceSelector:
matchLabels:
project: default
ports:
- port: 6379
protocol: TCP
@ -223,10 +235,17 @@ Testing the network policy support functionality
- port: 5978
protocol: TCP
to:
- namespaceSelector:
matchLabels:
project: default
ingress:
- ports:
- port: 8080
protocol: TCP
from:
- namespaceSelector:
matchLabels:
project: default
policyTypes:
- Ingress
- Egress
@ -244,6 +263,10 @@ Testing the network policy support functionality
$ curl 10.0.0.68:8080
demo-5558c7865d-fdkdv: HELLO! I AM ALIVE!!!
Note the ping will only work from pods (neutron ports) on a namespace that has
the label 'project: default' as stated on the policy namespaceSelector.
10. Confirm the teardown of the resources once the network policy is removed::
$ kubectl delete -f network_policy.yml

View File

@ -185,6 +185,33 @@ class NetworkPolicyDriver(base.NetworkPolicyDriver):
LOG.exception('Error annotating network policy')
raise
def _get_namespaces_cidr(self, namespace_selector):
cidrs = []
namespace_label = urlencode(namespace_selector[
'matchLabels'])
matching_namespaces = self.kubernetes.get(
'{}/namespaces?labelSelector={}'.format(
constants.K8S_API_BASE, namespace_label)).get('items')
for ns in matching_namespaces:
# NOTE(ltomasbo): This requires the namespace handler to be
# also enabled
try:
ns_annotations = ns['metadata']['annotations']
ns_name = ns_annotations[constants.K8S_ANNOTATION_NET_CRD]
except KeyError:
LOG.exception('Namespace handler must be enabled to support '
'Network Policies with namespaceSelector')
raise
try:
net_crd = self.kubernetes.get('{}/kuryrnets/{}'.format(
constants.K8S_API_CRD, ns_name))
except exceptions.K8sClientException:
LOG.exception("Kubernetes Client Exception.")
raise
ns_cidr = net_crd['spec']['subnetCIDR']
cidrs.append(ns_cidr)
return cidrs
def parse_network_policy_rules(self, policy, sg_id):
"""Create security group rule bodies out of network policies.
@ -207,15 +234,38 @@ class NetworkPolicyDriver(base.NetworkPolicyDriver):
ingress_sg_rule_body_list.append(i_rule)
for ingress_rule in ingress_rule_list:
LOG.debug('Parsing Ingress Rule %s', ingress_rule)
allowed_cidrs = []
for from_rule in ingress_rule.get('from', []):
namespace_selector = from_rule.get('namespaceSelector')
if namespace_selector:
allowed_cidrs = self._get_namespaces_cidr(
namespace_selector)
if 'ports' in ingress_rule:
for port in ingress_rule['ports']:
if allowed_cidrs:
for cidr in allowed_cidrs:
i_rule = self._create_security_group_rule_body(
sg_id, 'ingress', port.get('port'),
protocol=port.get('protocol'),
cidr=cidr)
ingress_sg_rule_body_list.append(i_rule)
else:
i_rule = self._create_security_group_rule_body(
sg_id, 'ingress', port.get('port'),
protocol=port.get('protocol'))
ingress_sg_rule_body_list.append(i_rule)
elif allowed_cidrs:
for cidr in allowed_cidrs:
i_rule = self._create_security_group_rule_body(
sg_id, 'ingress', port['port'],
protocol=port['protocol'].lower())
sg_id, 'ingress',
port_range_min=1,
port_range_max=65535,
cidr=cidr)
ingress_sg_rule_body_list.append(i_rule)
else:
LOG.debug('This network policy specifies no ingress '
'ports: %s', policy['metadata']['selfLink'])
LOG.debug('This network policy specifies no ingress from '
'and no ports: %s',
policy['metadata']['selfLink'])
if egress_rule_list:
if egress_rule_list[0] == {}:
@ -226,35 +276,66 @@ class NetworkPolicyDriver(base.NetworkPolicyDriver):
egress_sg_rule_body_list.append(e_rule)
for egress_rule in egress_rule_list:
LOG.debug('Parsing Egress Rule %s', egress_rule)
allowed_cidrs = []
for from_rule in egress_rule.get('to', []):
namespace_selector = from_rule.get('namespaceSelector')
if namespace_selector:
allowed_cidrs = self._get_namespaces_cidr(
namespace_selector)
if 'ports' in egress_rule:
for port in egress_rule['ports']:
if allowed_cidrs:
for cidr in allowed_cidrs:
e_rule = self._create_security_group_rule_body(
sg_id, 'egress', port.get('port'),
protocol=port.get('protocol'),
cidr=cidr)
egress_sg_rule_body_list.append(e_rule)
else:
e_rule = self._create_security_group_rule_body(
sg_id, 'egress', port.get('port'),
protocol=port.get('protocol'))
egress_sg_rule_body_list.append(e_rule)
elif allowed_cidrs:
for cidr in allowed_cidrs:
e_rule = self._create_security_group_rule_body(
sg_id, 'egress', port['port'],
protocol=port['protocol'].lower())
sg_id, 'egress',
port_range_min=1,
port_range_max=65535,
cidr=cidr)
egress_sg_rule_body_list.append(e_rule)
else:
LOG.debug('This network policy specifies no egress '
'ports: %s', policy['metadata']['selfLink'])
LOG.debug('This network policy specifies no egrees to '
'and no ports: %s',
policy['metadata']['selfLink'])
return ingress_sg_rule_body_list, egress_sg_rule_body_list
def _create_security_group_rule_body(
self, security_group_id, direction, port_range_min,
port_range_max=None, protocol='TCP', ethertype='IPv4',
port_range_max=None, protocol=None, ethertype='IPv4', cidr=None,
description="Kuryr-Kubernetes NetPolicy SG rule"):
if not port_range_max:
if not port_range_min:
port_range_min = 1
port_range_max = 65535
elif not port_range_max:
port_range_max = port_range_min
if not protocol:
protocol = 'TCP'
security_group_rule_body = {
u'security_group_rule': {
u'ethertype': ethertype,
u'security_group_id': security_group_id,
u'description': description,
u'direction': direction,
u'protocol': protocol,
u'protocol': protocol.lower(),
u'port_range_min': port_range_min,
u'port_range_max': port_range_max
}
}
if cidr:
security_group_rule_body[u'security_group_rule'][
u'remote_ip_prefix'] = cidr
LOG.debug("Creating sg rule body %s", security_group_rule_body)
return security_group_rule_body

View File

@ -14,6 +14,7 @@
import mock
from kuryr_kubernetes import constants
from kuryr_kubernetes.controller.drivers import network_policy
from kuryr_kubernetes import exceptions
from kuryr_kubernetes.tests import base as test_base
@ -22,6 +23,34 @@ from kuryr_kubernetes.tests.unit import kuryr_fixtures as k_fix
from neutronclient.common import exceptions as n_exc
def get_pod_obj():
return {
'status': {
'qosClass': 'BestEffort',
'hostIP': '192.168.1.2',
},
'kind': 'Pod',
'spec': {
'schedulerName': 'default-scheduler',
'containers': [{
'name': 'busybox',
'image': 'busybox',
'resources': {}
}],
'nodeName': 'kuryr-devstack'
},
'metadata': {
'name': 'busybox-sleep1',
'namespace': 'default',
'resourceVersion': '53808',
'selfLink': '/api/v1/namespaces/default/pods/busybox-sleep1',
'uid': '452176db-4a85-11e7-80bd-fa163e29dbbb',
'annotations': {
'openstack.org/kuryr-vif': {}
}
}}
class TestNetworkPolicyDriver(test_base.TestCase):
def setUp(self):
@ -49,9 +78,17 @@ class TestNetworkPolicyDriver(test_base.TestCase):
},
'spec': {
'egress': [{'ports':
[{'port': 5978, 'protocol': 'TCP'}]}],
[{'port': 5978, 'protocol': 'TCP'}],
'to':
[{'namespaceSelector': {
'matchLabels': {
'project': 'myproject'}}}]}],
'ingress': [{'ports':
[{'port': 6379, 'protocol': 'TCP'}]}],
[{'port': 6379, 'protocol': 'TCP'}],
'from':
[{'namespaceSelector': {
'matchLabels': {
'project': 'myproject'}}}]}],
'policyTypes': ['Ingress', 'Egress']
}
}
@ -242,28 +279,107 @@ class TestNetworkPolicyDriver(test_base.TestCase):
self._policy)
m_parse.assert_called_with(self._policy, self._sg_id)
def test_parse_network_policy_rules(self):
i_rule, e_rule = (
self._driver.parse_network_policy_rules(self._policy, self._sg_id))
self.assertEqual(
self._policy['spec']['ingress'][0]['ports'][0]['port'],
i_rule[0]['security_group_rule']['port_range_min'])
self.assertEqual(
self._policy['spec']['egress'][0]['ports'][0]['port'],
e_rule[0]['security_group_rule']['port_range_min'])
def test_get_namespaces_cidr(self):
namespace_selector = {'matchLabels': {'test': 'test'}}
pod = get_pod_obj()
annotation = mock.sentinel.annotation
subnet_cidr = mock.sentinel.subnet_cidr
net_crd = {'spec': {'subnetCIDR': subnet_cidr}}
pod['metadata']['annotations'][constants.K8S_ANNOTATION_NET_CRD] = (
annotation)
self.kubernetes.get.side_effect = [{'items': [pod]}, net_crd]
resp = self._driver._get_namespaces_cidr(namespace_selector)
self.assertEqual([subnet_cidr], resp)
self.kubernetes.get.assert_called()
def test_get_namespaces_cidr_no_matches(self):
namespace_selector = {'matchLabels': {'test': 'test'}}
self.kubernetes.get.return_value = {'items': []}
resp = self._driver._get_namespaces_cidr(namespace_selector)
self.assertEqual([], resp)
self.kubernetes.get.assert_called_once()
def test_get_namespaces_cidr_no_annotations(self):
namespace_selector = {'matchLabels': {'test': 'test'}}
pod = get_pod_obj()
self.kubernetes.get.return_value = {'items': [pod]}
self.assertRaises(KeyError, self._driver._get_namespaces_cidr,
namespace_selector)
self.kubernetes.get.assert_called_once()
@mock.patch.object(network_policy.NetworkPolicyDriver,
'_get_namespaces_cidr')
@mock.patch.object(network_policy.NetworkPolicyDriver,
'_create_security_group_rule_body')
def test_parse_network_policy_rules_with_rules(self, m_create):
def test_parse_network_policy_rules_with_rules(self, m_create,
m_get_ns_cidr):
subnet_cidr = '10.10.0.0/24'
m_get_ns_cidr.return_value = [subnet_cidr]
self._driver.parse_network_policy_rules(self._policy, self._sg_id)
m_create.assert_called()
m_get_ns_cidr.assert_called()
@mock.patch.object(network_policy.NetworkPolicyDriver,
'_get_namespaces_cidr')
@mock.patch.object(network_policy.NetworkPolicyDriver,
'_create_security_group_rule_body')
def test_parse_network_policy_rules_with_no_rules(self, m_create):
self._policy['spec'] = {}
self._driver.parse_network_policy_rules(self._policy, self._sg_id)
m_create.assert_not_called()
def test_parse_network_policy_rules_with_no_rules(self, m_create,
m_get_ns_cidr):
policy = self._policy.copy()
policy['spec']['ingress'] = [{}]
policy['spec']['egress'] = [{}]
self._driver.parse_network_policy_rules(policy, self._sg_id)
m_get_ns_cidr.assert_not_called()
calls = [mock.call(self._sg_id, 'ingress', port_range_min=1,
port_range_max=65535),
mock.call(self._sg_id, 'egress', port_range_min=1,
port_range_max=65535)]
m_create.assert_has_calls(calls)
@mock.patch.object(network_policy.NetworkPolicyDriver,
'_get_namespaces_cidr')
@mock.patch.object(network_policy.NetworkPolicyDriver,
'_create_security_group_rule_body')
def test_parse_network_policy_rules_with_no_pod_selector(self, m_create,
m_get_ns_cidr):
policy = self._policy.copy()
policy['spec']['ingress'] = [{'ports':
[{'port': 6379, 'protocol': 'TCP'}]}]
policy['spec']['egress'] = [{'ports':
[{'port': 6379, 'protocol': 'TCP'}]}]
self._driver.parse_network_policy_rules(policy, self._sg_id)
m_create.assert_called()
m_get_ns_cidr.assert_not_called()
@mock.patch.object(network_policy.NetworkPolicyDriver,
'_get_namespaces_cidr')
@mock.patch.object(network_policy.NetworkPolicyDriver,
'_create_security_group_rule_body')
def test_parse_network_policy_rules_with_no_ports(self, m_create,
m_get_ns_cidr):
subnet_cidr = '10.10.0.0/24'
m_get_ns_cidr.return_value = [subnet_cidr]
policy = self._policy.copy()
policy['spec']['egress'] = [
{'to':
[{'namespaceSelector': {
'matchLabels': {
'project': 'myproject'}}}]}]
policy['spec']['ingress'] = [
{'from':
[{'namespaceSelector': {
'matchLabels': {
'project': 'myproject'}}}]}]
self._driver.parse_network_policy_rules(policy, self._sg_id)
m_get_ns_cidr.assert_called()
calls = [mock.call(self._sg_id, 'ingress', port_range_min=1,
port_range_max=65535, cidr=subnet_cidr),
mock.call(self._sg_id, 'egress', port_range_min=1,
port_range_max=65535, cidr=subnet_cidr)]
m_create.assert_has_calls(calls)
def test_knps_on_namespace(self):
self.kubernetes.get.return_value = {'items': ['not-empty']}