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 - Egress
ingress: ingress:
- from: - from:
- namespaceSelector:
matchLabels:
project: default
ports: ports:
- protocol: TCP - protocol: TCP
port: 6379 port: 6379
egress: egress:
- to: - to:
- namespaceSelector:
matchLabels:
project: default
ports: ports:
- protocol: TCP - protocol: TCP
port: 5978 port: 5978
@ -121,11 +127,17 @@ Testing the network policy support functionality
networkpolicy_spec: networkpolicy_spec:
egress: egress:
- to: - to:
- namespaceSelector:
matchLabels:
project: default
ports: ports:
- port: 5978 - port: 5978
protocol: TCP protocol: TCP
ingress: ingress:
- from: - from:
- namespaceSelector:
matchLabels:
project: default
ports: ports:
- port: 6379 - port: 6379
protocol: TCP protocol: TCP
@ -223,10 +235,17 @@ Testing the network policy support functionality
- port: 5978 - port: 5978
protocol: TCP protocol: TCP
to: to:
- namespaceSelector:
matchLabels:
project: default
ingress: ingress:
- ports: - ports:
- port: 8080 - port: 8080
protocol: TCP protocol: TCP
from:
- namespaceSelector:
matchLabels:
project: default
policyTypes: policyTypes:
- Ingress - Ingress
- Egress - Egress
@ -244,6 +263,10 @@ Testing the network policy support functionality
$ curl 10.0.0.68:8080 $ curl 10.0.0.68:8080
demo-5558c7865d-fdkdv: HELLO! I AM ALIVE!!! 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:: 10. Confirm the teardown of the resources once the network policy is removed::
$ kubectl delete -f network_policy.yml $ kubectl delete -f network_policy.yml

View File

@ -185,6 +185,33 @@ class NetworkPolicyDriver(base.NetworkPolicyDriver):
LOG.exception('Error annotating network policy') LOG.exception('Error annotating network policy')
raise 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): def parse_network_policy_rules(self, policy, sg_id):
"""Create security group rule bodies out of network policies. """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) ingress_sg_rule_body_list.append(i_rule)
for ingress_rule in ingress_rule_list: for ingress_rule in ingress_rule_list:
LOG.debug('Parsing Ingress Rule %s', ingress_rule) 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: if 'ports' in ingress_rule:
for port in ingress_rule['ports']: 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( i_rule = self._create_security_group_rule_body(
sg_id, 'ingress', port['port'], sg_id, 'ingress',
protocol=port['protocol'].lower()) port_range_min=1,
port_range_max=65535,
cidr=cidr)
ingress_sg_rule_body_list.append(i_rule) ingress_sg_rule_body_list.append(i_rule)
else: else:
LOG.debug('This network policy specifies no ingress ' LOG.debug('This network policy specifies no ingress from '
'ports: %s', policy['metadata']['selfLink']) 'and no ports: %s',
policy['metadata']['selfLink'])
if egress_rule_list: if egress_rule_list:
if egress_rule_list[0] == {}: if egress_rule_list[0] == {}:
@ -226,35 +276,66 @@ class NetworkPolicyDriver(base.NetworkPolicyDriver):
egress_sg_rule_body_list.append(e_rule) egress_sg_rule_body_list.append(e_rule)
for egress_rule in egress_rule_list: for egress_rule in egress_rule_list:
LOG.debug('Parsing Egress Rule %s', egress_rule) 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: if 'ports' in egress_rule:
for port in egress_rule['ports']: 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( e_rule = self._create_security_group_rule_body(
sg_id, 'egress', port['port'], sg_id, 'egress',
protocol=port['protocol'].lower()) port_range_min=1,
port_range_max=65535,
cidr=cidr)
egress_sg_rule_body_list.append(e_rule) egress_sg_rule_body_list.append(e_rule)
else: else:
LOG.debug('This network policy specifies no egress ' LOG.debug('This network policy specifies no egrees to '
'ports: %s', policy['metadata']['selfLink']) 'and no ports: %s',
policy['metadata']['selfLink'])
return ingress_sg_rule_body_list, egress_sg_rule_body_list return ingress_sg_rule_body_list, egress_sg_rule_body_list
def _create_security_group_rule_body( def _create_security_group_rule_body(
self, security_group_id, direction, port_range_min, 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"): 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 port_range_max = port_range_min
if not protocol:
protocol = 'TCP'
security_group_rule_body = { security_group_rule_body = {
u'security_group_rule': { u'security_group_rule': {
u'ethertype': ethertype, u'ethertype': ethertype,
u'security_group_id': security_group_id, u'security_group_id': security_group_id,
u'description': description, u'description': description,
u'direction': direction, u'direction': direction,
u'protocol': protocol, u'protocol': protocol.lower(),
u'port_range_min': port_range_min, u'port_range_min': port_range_min,
u'port_range_max': port_range_max 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) LOG.debug("Creating sg rule body %s", security_group_rule_body)
return security_group_rule_body return security_group_rule_body

View File

@ -14,6 +14,7 @@
import mock import mock
from kuryr_kubernetes import constants
from kuryr_kubernetes.controller.drivers import network_policy from kuryr_kubernetes.controller.drivers import network_policy
from kuryr_kubernetes import exceptions from kuryr_kubernetes import exceptions
from kuryr_kubernetes.tests import base as test_base 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 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): class TestNetworkPolicyDriver(test_base.TestCase):
def setUp(self): def setUp(self):
@ -49,9 +78,17 @@ class TestNetworkPolicyDriver(test_base.TestCase):
}, },
'spec': { 'spec': {
'egress': [{'ports': 'egress': [{'ports':
[{'port': 5978, 'protocol': 'TCP'}]}], [{'port': 5978, 'protocol': 'TCP'}],
'to':
[{'namespaceSelector': {
'matchLabels': {
'project': 'myproject'}}}]}],
'ingress': [{'ports': 'ingress': [{'ports':
[{'port': 6379, 'protocol': 'TCP'}]}], [{'port': 6379, 'protocol': 'TCP'}],
'from':
[{'namespaceSelector': {
'matchLabels': {
'project': 'myproject'}}}]}],
'policyTypes': ['Ingress', 'Egress'] 'policyTypes': ['Ingress', 'Egress']
} }
} }
@ -242,28 +279,107 @@ class TestNetworkPolicyDriver(test_base.TestCase):
self._policy) self._policy)
m_parse.assert_called_with(self._policy, self._sg_id) m_parse.assert_called_with(self._policy, self._sg_id)
def test_parse_network_policy_rules(self): def test_get_namespaces_cidr(self):
i_rule, e_rule = ( namespace_selector = {'matchLabels': {'test': 'test'}}
self._driver.parse_network_policy_rules(self._policy, self._sg_id)) pod = get_pod_obj()
self.assertEqual( annotation = mock.sentinel.annotation
self._policy['spec']['ingress'][0]['ports'][0]['port'], subnet_cidr = mock.sentinel.subnet_cidr
i_rule[0]['security_group_rule']['port_range_min']) net_crd = {'spec': {'subnetCIDR': subnet_cidr}}
self.assertEqual( pod['metadata']['annotations'][constants.K8S_ANNOTATION_NET_CRD] = (
self._policy['spec']['egress'][0]['ports'][0]['port'], annotation)
e_rule[0]['security_group_rule']['port_range_min']) 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, @mock.patch.object(network_policy.NetworkPolicyDriver,
'_create_security_group_rule_body') '_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) self._driver.parse_network_policy_rules(self._policy, self._sg_id)
m_create.assert_called() 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, @mock.patch.object(network_policy.NetworkPolicyDriver,
'_create_security_group_rule_body') '_create_security_group_rule_body')
def test_parse_network_policy_rules_with_no_rules(self, m_create): def test_parse_network_policy_rules_with_no_rules(self, m_create,
self._policy['spec'] = {} m_get_ns_cidr):
self._driver.parse_network_policy_rules(self._policy, self._sg_id) policy = self._policy.copy()
m_create.assert_not_called() 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): def test_knps_on_namespace(self):
self.kubernetes.get.return_value = {'items': ['not-empty']} self.kubernetes.get.return_value = {'items': ['not-empty']}