Add support for text ports on Network Policy Spec

This commit adds support for Network Policies that define ports
with text. In the case of ingress rule with named port, the pods
selected by NetworkPolicySpec's podSelector has its containers
verified to check for ports with same name. In case of egress rule
all the pods selected by the NetworkPolicyEgressRule's selector
have its containers verified to check if the containers ports
have the same name as the ones defined in policy rule ports.
If matched, a security Group rule with
a 'remote_ip_prefixes' field pointing to that pod is created.

Change-Id: If1eddc3e6cc4884ca53e81e4f87b5fae80fed70e
Closes-Bug: 1818983
This commit is contained in:
Maysa Macedo 2019-03-13 15:42:25 +00:00
parent 9b3182cfeb
commit b644083835
3 changed files with 299 additions and 111 deletions

View File

@ -214,9 +214,8 @@ class NetworkPolicyDriver(base.NetworkPolicyDriver):
LOG.exception('Error annotating network policy')
raise
def _get_pods_ips(self, pod_selector, namespace=None,
namespace_selector=None):
ips = []
def _get_pods(self, pod_selector, namespace=None,
namespace_selector=None):
matching_pods = {"items": []}
if namespace_selector:
matching_namespaces = driver_utils.get_namespaces(
@ -226,33 +225,21 @@ class NetworkPolicyDriver(base.NetworkPolicyDriver):
ns['metadata']['name'])
else:
matching_pods = driver_utils.get_pods(pod_selector, namespace)
for pod in matching_pods.get('items'):
if pod['status'].get('podIP'):
pod_ip = pod['status']['podIP']
ns = pod['metadata']['namespace']
ips.append({'cidr': pod_ip, 'namespace': ns})
return ips
return matching_pods.get('items')
def _get_namespaces_cidr(self, namespace_selector, namespace=None):
cidrs = []
def _get_namespaces(self, namespace_selector, namespace=None):
matching_namespaces = []
if not namespace_selector and namespace:
ns = self.kubernetes.get(
'{}/namespaces/{}'.format(constants.K8S_API_BASE, namespace))
ns_cidr = driver_utils.get_namespace_subnet_cidr(ns)
cidrs.append({'cidr': ns_cidr, 'namespace': namespace})
matching_namespaces.append(self.kubernetes.get(
'{}/namespaces/{}'.format(constants.K8S_API_BASE, namespace)))
else:
matching_namespaces = driver_utils.get_namespaces(
namespace_selector)
for ns in matching_namespaces.get('items'):
# NOTE(ltomasbo): This requires the namespace handler to be
# also enabled
ns_cidr = driver_utils.get_namespace_subnet_cidr(ns)
ns_name = ns['metadata']['name']
cidrs.append({'cidr': ns_cidr, 'namespace': ns_name})
return cidrs
matching_namespaces.extend(driver_utils.get_namespaces(
namespace_selector).get('items'))
return matching_namespaces
def _parse_selectors(self, rule_block, rule_direction, policy_namespace):
allowed_cidrs = []
allowed_resources = []
allow_all = False
selectors = False
for rule in rule_block.get(rule_direction, []):
@ -262,7 +249,7 @@ class NetworkPolicyDriver(base.NetworkPolicyDriver):
selectors = True
if pod_selector:
# allow matching pods in all namespaces
allowed_cidrs.extend(self._get_pods_ips(
allowed_resources.extend(self._get_pods(
pod_selector))
else:
# allow from all
@ -271,30 +258,144 @@ class NetworkPolicyDriver(base.NetworkPolicyDriver):
selectors = True
if pod_selector:
# allow matching pods on matching namespaces
allowed_cidrs.extend(self._get_pods_ips(
allowed_resources.extend(self._get_pods(
pod_selector,
namespace_selector=namespace_selector))
else:
# allow from/to all on the matching namespaces
allowed_cidrs.extend(self._get_namespaces_cidr(
allowed_resources.extend(self._get_namespaces(
namespace_selector))
else:
if pod_selector == {}:
# allow from/to all pods on the network policy
# namespace
selectors = True
allowed_cidrs.extend(self._get_namespaces_cidr(
allowed_resources.extend(self._get_namespaces(
None,
namespace=policy_namespace))
elif pod_selector:
# allow matching pods on the network policy
# namespace
selectors = True
allowed_cidrs.extend(self._get_pods_ips(
allowed_resources.extend(self._get_pods(
pod_selector,
namespace=policy_namespace))
return allow_all, selectors, allowed_cidrs
return allow_all, selectors, allowed_resources
def _create_sg_rules_with_container_ports(
self, container_ports, allow_all, resource, matched_pods,
crd_rules, sg_id, direction, port, pod_selector=None,
policy_namespace=None):
cidr, ns = self._get_resource_details(resource)
for pod, container_port in container_ports:
pod_label = pod['metadata'].get('labels')
pod_ip = pod['status'].get('podIP')
pod_namespace = pod['metadata']['namespace']
pod_info = {pod_ip: pod_namespace}
# NOTE(maysams) Avoid to take into account pods that are also
# matched by NetworkPolicySpec's podSelector. This way we do
# not allow egress traffic to the actual set of pods the NP
# is enforced on.
if (direction == 'egress' and
(driver_utils.match_selector(pod_selector, pod_label) and
policy_namespace == pod_namespace)):
continue
if container_port in matched_pods:
matched_pods[container_port].update(pod_info)
else:
matched_pods[container_port] = pod_info
if not allow_all and matched_pods:
for container_port, pods in matched_pods.items():
sg_rule = driver_utils.create_security_group_rule_body(
sg_id, direction, container_port,
protocol=port.get('protocol'),
cidr=cidr, pods=pods)
if sg_rule not in crd_rules:
crd_rules.append(sg_rule)
def _create_sg_rule_body_on_text_port(self, sg_id, direction, port,
resources, crd_rules, pod_selector,
policy_namespace, allow_all=False):
"""Create SG rules when named port is used in the NP rule
In case of ingress, the pods selected by NetworkPolicySpec's
podSelector have its containers checked for ports with same name as
the named port. If true, rules are created for the resource matched
in the NP rule selector with that port. In case of egress, all the pods
selected by the NetworkPolicyEgressRule's selector have its containers
checked for containers ports with same name as the ones defined in
NP rule, and if true the rule is created.
param sg_id: String with the Security Group ID
param direction: String with ingress or egress
param port: dict containing port and protocol
param resources: list of K8S resources(pod/namespace) or
a dict with cird
param crd_rules: list of parsed SG rules
param pod_selector: dict with NetworkPolicySpec's podSelector
param policy_namespace: string with policy namespace
param allow_all: True if should parse a allow from/to all rule,
False otherwise
"""
matched_pods = {}
if direction == "ingress":
selected_pods = driver_utils.get_pods(
pod_selector, policy_namespace).get('items')
for selected_pod in selected_pods:
container_ports = driver_utils.get_ports(selected_pod, port)
for resource in resources:
self._create_sg_rules_with_container_ports(
container_ports, allow_all, resource, matched_pods,
crd_rules, sg_id, direction, port)
elif direction == "egress":
for resource in resources:
# NOTE(maysams) Skipping objects that refers to ipblocks
# and consequently do not contains a spec field
if not resource.get('spec'):
LOG.warning("IPBlock for egress with named ports is "
"not supported.")
continue
container_ports = driver_utils.get_ports(resource, port)
self._create_sg_rules_with_container_ports(
container_ports, allow_all, resource, matched_pods,
crd_rules, sg_id, direction, port, pod_selector,
policy_namespace)
if allow_all:
for container_port, pods in matched_pods.items():
sg_rule = driver_utils.create_security_group_rule_body(
sg_id, direction, container_port,
protocol=port.get('protocol'),
pods=pods)
crd_rules.append(sg_rule)
def _create_sg_rule_on_number_port(self, allowed_resources, sg_id,
direction, port, sg_rule_body_list):
for resource in allowed_resources:
cidr, ns = self._get_resource_details(resource)
sg_rule = (
driver_utils.create_security_group_rule_body(
sg_id, direction, port.get('port'),
protocol=port.get('protocol'),
cidr=cidr,
namespace=ns))
sg_rule_body_list.append(sg_rule)
def _create_all_pods_sg_rules(self, port, sg_id, direction,
sg_rule_body_list, pod_selector,
policy_namespace):
if type(port.get('port')) is not int:
all_pods = driver_utils.get_namespaced_pods().get('items')
self._create_sg_rule_body_on_text_port(
sg_id, direction, port, all_pods,
sg_rule_body_list, pod_selector, policy_namespace,
allow_all=True)
else:
sg_rule = (
driver_utils.create_security_group_rule_body(
sg_id, direction, port.get('port'),
protocol=port.get('protocol')))
sg_rule_body_list.append(sg_rule)
def _parse_sg_rules(self, sg_rule_body_list, direction, policy, sg_id):
"""Parse policy into security group rules.
@ -336,6 +437,8 @@ class NetworkPolicyDriver(base.NetworkPolicyDriver):
return
policy_namespace = policy['metadata']['namespace']
pod_selector = policy['spec'].get('podSelector')
rule_direction = 'from'
if direction == 'egress':
rule_direction = 'to'
@ -350,7 +453,7 @@ class NetworkPolicyDriver(base.NetworkPolicyDriver):
for rule_block in rule_list:
LOG.debug('Parsing %(dir)s Rule %(rule)s', {'dir': direction,
'rule': rule_block})
allow_all, selectors, allowed_cidrs = self._parse_selectors(
allow_all, selectors, allowed_resources = self._parse_selectors(
rule_block, rule_direction, policy_namespace)
ipblock_list = []
@ -365,41 +468,41 @@ class NetworkPolicyDriver(base.NetworkPolicyDriver):
for cidr_except in ipblock.get('except'):
cidr_list = netaddr.cidr_exclude(
ipblock.get('cidr'), cidr_except)
cidr_list = [{'cidr': str(cidr)} for cidr in cidr_list]
allowed_cidrs.extend(cidr_list)
cidr_list = [{'cidr': str(cidr)}
for cidr in cidr_list]
allowed_resources.extend(cidr_list)
else:
allowed_cidrs.append(ipblock)
allowed_resources.append(ipblock)
if 'ports' in rule_block:
for port in rule_block['ports']:
if allowed_cidrs or allow_all or selectors:
for cidr in allowed_cidrs:
rule = (
driver_utils.create_security_group_rule_body(
sg_id, direction, port.get('port'),
protocol=port.get('protocol'),
cidr=cidr.get('cidr'),
namespace=cidr.get('namespace')))
sg_rule_body_list.append(rule)
if allowed_resources or allow_all or selectors:
if type(port.get('port')) is not int:
self._create_sg_rule_body_on_text_port(
sg_id, direction, port, allowed_resources,
sg_rule_body_list, pod_selector,
policy_namespace)
else:
self._create_sg_rule_on_number_port(
allowed_resources, sg_id, direction, port,
sg_rule_body_list)
if allow_all:
rule = (
driver_utils.create_security_group_rule_body(
sg_id, direction, port.get('port'),
protocol=port.get('protocol')))
sg_rule_body_list.append(rule)
self._create_all_pods_sg_rules(
port, sg_id, direction, sg_rule_body_list,
pod_selector, policy_namespace)
else:
rule = driver_utils.create_security_group_rule_body(
sg_id, direction, port.get('port'),
protocol=port.get('protocol'))
sg_rule_body_list.append(rule)
elif allowed_cidrs or allow_all or selectors:
for cidr in allowed_cidrs:
self._create_all_pods_sg_rules(
port, sg_id, direction, sg_rule_body_list,
pod_selector, policy_namespace)
elif allowed_resources or allow_all or selectors:
for resource in allowed_resources:
cidr, namespace = self._get_resource_details(resource)
rule = driver_utils.create_security_group_rule_body(
sg_id, direction,
port_range_min=1,
port_range_max=65535,
cidr=cidr.get('cidr'),
namespace=cidr.get('namespace'))
cidr=cidr,
namespace=namespace)
sg_rule_body_list.append(rule)
if allow_all:
rule = driver_utils.create_security_group_rule_body(
@ -556,3 +659,19 @@ class NetworkPolicyDriver(base.NetworkPolicyDriver):
pods = self.kubernetes.get('{}/namespaces/{}/pods'.format(
constants.K8S_API_BASE, pod_namespace))
return pods.get('items')
def _get_resource_details(self, resource):
namespace = None
if self._is_pod(resource):
cidr = resource['status'].get('podIP')
namespace = resource['metadata']['namespace']
elif resource.get('cidr'):
cidr = resource.get('cidr')
else:
cidr = driver_utils.get_namespace_subnet_cidr(resource)
namespace = resource['metadata']['name']
return cidr, namespace
def _is_pod(self, resource):
if resource.get('spec'):
return resource['spec'].get('containers')

View File

@ -238,7 +238,8 @@ def patch_kuryr_crd(crd, i_rules, e_rules, pod_selector, np_spec=None):
def create_security_group_rule_body(
security_group_id, direction, port_range_min=None,
port_range_max=None, protocol=None, ethertype='IPv4', cidr=None,
description="Kuryr-Kubernetes NetPolicy SG rule", namespace=None):
description="Kuryr-Kubernetes NetPolicy SG rule", namespace=None,
pods=None):
if not port_range_min:
port_range_min = 1
port_range_max = 65535
@ -262,6 +263,8 @@ def create_security_group_rule_body(
u'remote_ip_prefix'] = cidr
if namespace:
security_group_rule_body['namespace'] = namespace
if pods:
security_group_rule_body['remote_ip_prefixes'] = pods
LOG.debug("Creating sg rule body %s", security_group_rule_body)
return security_group_rule_body
@ -423,3 +426,62 @@ def service_matches_affected_pods(service, pod_selectors):
if match_selector(selector, svc_selector):
return True
return False
def get_namespaced_pods(namespace=None):
kubernetes = clients.get_kubernetes_client()
if namespace:
namespace = namespace['metadata']['name']
pods = kubernetes.get(
'{}/namespaces/{}/pods'.format(
constants.K8S_API_BASE, namespace))
else:
pods = kubernetes.get(
'{}/pods'.format(
constants.K8S_API_BASE))
return pods
def get_container_ports(containers, np_port_name, pod):
matched_ports = []
if is_host_network(pod):
return matched_ports
for container in containers:
for container_port in container.get('ports', []):
if container_port.get('name') == np_port_name:
container_port = container_port.get('containerPort')
if container_port not in matched_ports:
matched_ports.append((pod, container_port))
return matched_ports
def get_ports(resource, port):
"""Returns values of ports that have a given port name
Retrieves the values of ports, defined in the containers
associated to the resource, that has its name matching a
given port.
param resource: k8s Pod or Namespace
param port: a dict containing a port and protocol
return: A list of tuples of port values and associated pods
"""
containers = resource['spec'].get('containers')
ports = []
np_port = port.get('port')
if containers:
ports.extend(get_container_ports(containers, np_port, resource))
else:
pods = get_namespaced_pods(resource).get('items')
for pod in pods:
containers = pod['spec']['containers']
ports.extend(get_container_ports(
containers, np_port, pod))
return ports
def get_namespace(namespace_name):
kubernetes = clients.get_kubernetes_client()
return kubernetes.get(
'{}/namespaces/{}'.format(
constants.K8S_API_BASE, namespace_name))

View File

@ -14,7 +14,6 @@
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
@ -52,6 +51,21 @@ def get_pod_obj():
}}
def get_namespace_obj():
return {
"kind": "Namespace",
"metadata": {
"annotations": {
"openstack.org/kuryr-namespace-label":
"{\"projetc\": \"myproject\"}",
"openstack.org/kuryr-net-crd": "ns-myproject"
},
"labels": {
"project": "myproject"
},
"name": "myproject"}}
class TestNetworkPolicyDriver(test_base.TestCase):
def setUp(self):
@ -307,86 +321,75 @@ class TestNetworkPolicyDriver(test_base.TestCase):
self._policy)
m_parse.assert_called_with(self._policy, self._sg_id)
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]
def test_get_namespaces(self):
namespace_selector = {'namespaceSelector': {
'matchLabels': {'project': 'myproject'}}}
self.kubernetes.get.side_effect = [{'items': [get_namespace_obj()]}]
resp = self._driver._get_namespaces_cidr(namespace_selector)
self.assertEqual(subnet_cidr, resp[0].get('cidr'))
resp = self._driver._get_namespaces(namespace_selector)
self.assertEqual([get_namespace_obj()], resp)
self.kubernetes.get.assert_called()
def test_get_namespaces_cidr_no_matches(self):
def test_get_namespaces_no_matches(self):
namespace_selector = {'matchLabels': {'test': 'test'}}
self.kubernetes.get.return_value = {'items': []}
resp = self._driver._get_namespaces_cidr(namespace_selector)
resp = self._driver._get_namespaces(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(exceptions.ResourceNotReady,
self._driver._get_namespaces_cidr,
namespace_selector)
self.kubernetes.get.assert_called_once()
@mock.patch.object(network_policy.NetworkPolicyDriver,
'_get_namespaces_cidr')
'_get_resource_details')
@mock.patch.object(network_policy.NetworkPolicyDriver,
'_get_namespaces')
@mock.patch('kuryr_kubernetes.controller.drivers.utils.'
'create_security_group_rule_body')
def test_parse_network_policy_rules_with_rules(self, m_create,
m_get_ns_cidr):
def test_parse_network_policy_rules_with_rules(
self, m_create, m_get_namespaces,
m_get_resource_details):
subnet_cidr = '10.10.0.0/24'
m_get_ns_cidr.return_value = [{'cidr': subnet_cidr, 'namespace': ''}]
namespace = 'myproject'
m_get_namespaces.return_value = [get_namespace_obj()]
m_get_resource_details.return_value = subnet_cidr, namespace
self._driver.parse_network_policy_rules(self._policy, self._sg_id)
m_get_namespaces.assert_called()
m_get_resource_details.assert_called()
m_create.assert_called()
m_get_ns_cidr.assert_called()
@mock.patch.object(network_policy.NetworkPolicyDriver,
'_get_namespaces_cidr')
'_get_namespaces')
@mock.patch('kuryr_kubernetes.controller.drivers.utils.'
'create_security_group_rule_body')
def test_parse_network_policy_rules_with_no_rules(self, m_create,
m_get_ns_cidr):
m_get_ns):
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()
m_get_ns.assert_not_called()
calls = [mock.call(self._sg_id, 'ingress'),
mock.call(self._sg_id, 'egress')]
m_create.assert_has_calls(calls)
@mock.patch.object(network_policy.NetworkPolicyDriver,
'_get_namespaces_cidr')
@mock.patch('kuryr_kubernetes.controller.drivers.utils.'
'create_security_group_rule_body')
def test_parse_network_policy_rules_with_no_pod_selector(self, m_create,
m_get_ns_cidr):
'_create_all_pods_sg_rules')
def test_parse_network_policy_rules_with_no_pod_selector(
self, m_create_all_pods_sg_rules):
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()
m_create_all_pods_sg_rules.assert_called()
@mock.patch.object(network_policy.NetworkPolicyDriver,
'_get_namespaces_cidr')
@mock.patch('kuryr_kubernetes.controller.drivers.utils.'
'create_security_group_rule_body')
def test_parse_network_policy_rules_with_ipblock(self, m_create,
m_get_ns_cidr):
'_create_sg_rule_on_number_port')
@mock.patch.object(network_policy.NetworkPolicyDriver,
'_get_namespaces')
def test_parse_network_policy_rules_with_ipblock(self,
m_get_namespaces,
m_create_sg_rule):
policy = self._policy.copy()
policy['spec']['ingress'] = [{'from':
[{'ipBlock':
@ -399,17 +402,20 @@ class TestNetworkPolicyDriver(test_base.TestCase):
'to': [{'ipBlock':
{'cidr': '10.0.0.0/24'}}]}]
self._driver.parse_network_policy_rules(policy, self._sg_id)
m_create.assert_called()
m_get_ns_cidr.assert_not_called()
m_create_sg_rule.assert_called()
@mock.patch.object(network_policy.NetworkPolicyDriver,
'_get_namespaces_cidr')
'_get_resource_details')
@mock.patch.object(network_policy.NetworkPolicyDriver,
'_get_namespaces')
@mock.patch('kuryr_kubernetes.controller.drivers.utils.'
'create_security_group_rule_body')
def test_parse_network_policy_rules_with_no_ports(self, m_create,
m_get_ns_cidr):
def test_parse_network_policy_rules_with_no_ports(
self, m_create, m_get_namespaces, m_get_resource_details):
subnet_cidr = '10.10.0.0/24'
m_get_ns_cidr.return_value = [{'cidr': subnet_cidr, 'namespace': ''}]
namespace = 'myproject'
m_get_namespaces.return_value = [get_namespace_obj()]
m_get_resource_details.return_value = subnet_cidr, namespace
policy = self._policy.copy()
selectors = {'namespaceSelector': {
'matchLabels': {
@ -422,13 +428,14 @@ class TestNetworkPolicyDriver(test_base.TestCase):
[selectors]}]
selectors = {'namespace_selector': selectors['namespaceSelector']}
self._driver.parse_network_policy_rules(policy, self._sg_id)
m_get_ns_cidr.assert_called()
m_get_namespaces.assert_called()
m_get_resource_details.assert_called()
calls = [mock.call(self._sg_id, 'ingress', port_range_min=1,
port_range_max=65535, cidr=subnet_cidr,
namespace=''),
namespace=namespace),
mock.call(self._sg_id, 'egress', port_range_min=1,
port_range_max=65535, cidr=subnet_cidr,
namespace='')]
namespace=namespace)]
m_create.assert_has_calls(calls)
def test_knps_on_namespace(self):