From 543b8a2e05c81ab8a06106b76185f530649d3400 Mon Sep 17 00:00:00 2001 From: Luis Tomas Bolivar Date: Thu, 22 Nov 2018 19:01:59 +0100 Subject: [PATCH] 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 --- doc/source/installation/network_policy.rst | 23 +++ .../controller/drivers/network_policy.py | 103 ++++++++++-- .../controller/drivers/test_network_policy.py | 148 ++++++++++++++++-- 3 files changed, 247 insertions(+), 27 deletions(-) diff --git a/doc/source/installation/network_policy.rst b/doc/source/installation/network_policy.rst index 98170eb06..5386104cb 100644 --- a/doc/source/installation/network_policy.rst +++ b/doc/source/installation/network_policy.rst @@ -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 diff --git a/kuryr_kubernetes/controller/drivers/network_policy.py b/kuryr_kubernetes/controller/drivers/network_policy.py index d39a314c7..205d12995 100644 --- a/kuryr_kubernetes/controller/drivers/network_policy.py +++ b/kuryr_kubernetes/controller/drivers/network_policy.py @@ -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 diff --git a/kuryr_kubernetes/tests/unit/controller/drivers/test_network_policy.py b/kuryr_kubernetes/tests/unit/controller/drivers/test_network_policy.py index 96a1eca8d..d012a33a4 100644 --- a/kuryr_kubernetes/tests/unit/controller/drivers/test_network_policy.py +++ b/kuryr_kubernetes/tests/unit/controller/drivers/test_network_policy.py @@ -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']}