Merge "Add support to match expressions in Network Policies"

This commit is contained in:
Zuul 2018-12-13 12:22:46 +00:00 committed by Gerrit Code Review
commit fe583c3e6d
7 changed files with 314 additions and 119 deletions

View File

@ -72,3 +72,8 @@ DEFAULT_IFNAME = 'eth0'
ADDITIONAL_IFNAME_PREFIX = 'eth'
K8S_NPWG_SRIOV_PREFIX = "intel.com/sriov"
K8S_OPERATOR_IN = 'in'
K8S_OPERATOR_NOT_IN = 'notin'
K8S_OPERATOR_DOES_NOT_EXIST = 'doesnotexist'
K8S_OPERATOR_EXISTS = 'exists'

View File

@ -22,6 +22,7 @@ from neutronclient.common import exceptions as n_exc
from kuryr_kubernetes import clients
from kuryr_kubernetes import constants
from kuryr_kubernetes.controller.drivers import base
from kuryr_kubernetes.controller.drivers import utils
from kuryr_kubernetes import exceptions
LOG = logging.getLogger(__name__)
@ -120,10 +121,7 @@ class NetworkPolicyDriver(base.NetworkPolicyDriver):
'egressSgRules': e_rules,
'podSelector': pod_selector,
'networkpolicy_spec': policy['spec']})
# TODO(ltomasbo): allow patching both spec and metadata in the
# same call
self.kubernetes.patch('metadata', crd['metadata']['selfLink'],
{'labels': pod_selector.get('matchLabels')})
except exceptions.K8sClientException:
LOG.exception('Error updating kuryrnetpolicy CRD %s', crd_name)
raise
@ -443,13 +441,6 @@ class NetworkPolicyDriver(base.NetworkPolicyDriver):
'networkpolicy_spec': policy['spec']
},
}
if pod_selector:
try:
netpolicy_crd['metadata']['labels'] = pod_selector[
'matchLabels']
except KeyError:
# NOTE(ltomasbo): Only supporting matchLabels for now
LOG.info("Pod Selector only allowed with matchLabels")
try:
LOG.debug("Creating KuryrNetPolicy CRD %s" % netpolicy_crd)
@ -482,18 +473,8 @@ class NetworkPolicyDriver(base.NetworkPolicyDriver):
else:
pod_selector = policy['spec'].get('podSelector')
if pod_selector:
pod_label = pod_selector['matchLabels']
pod_namespace = policy['metadata']['namespace']
# Removing pod-template-hash as pods will not have it and
# otherwise there will be no match
pod_label.pop('pod-template-hash', None)
pod_label = urlencode(pod_label)
# NOTE(ltomasbo): K8s API does not accept &, so we need to AND
# the matchLabels with ',' or '%2C' instead
pod_label = pod_label.replace('&', ',')
pods = self.kubernetes.get(
'{}/namespaces/{}/pods?labelSelector={}'.format(
constants.K8S_API_BASE, pod_namespace, pod_label))
policy_namespace = policy['metadata']['namespace']
pods = utils.get_pods(pod_selector, policy_namespace)
return pods.get('items')
else:
# NOTE(ltomasbo): It affects all the pods on the namespace

View File

@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from six.moves.urllib.parse import urlencode
from kuryr_kubernetes import clients
from kuryr_kubernetes import config
@ -26,25 +25,19 @@ from oslo_log import log as logging
LOG = logging.getLogger(__name__)
def _get_kuryrnetpolicy_crds(labels=None, namespace='default'):
OPERATORS_WITH_VALUES = [constants.K8S_OPERATOR_IN,
constants.K8S_OPERATOR_NOT_IN]
def _get_kuryrnetpolicy_crds(namespace='default'):
kubernetes = clients.get_kubernetes_client()
try:
if labels:
LOG.debug("Using labels %s", labels)
labels.pop('pod-template-hash', None)
# removing pod-template-hash is necessary to fetch the proper list
labels = urlencode(labels)
# NOTE(maysams): K8s API does not accept &, so we need to replace
# it with ',' or '%2C' instead
labels = labels.replace('&', ',')
knp_path = '{}/{}/kuryrnetpolicies?labelSelector={}'.format(
constants.K8S_API_CRD_NAMESPACES, namespace, labels)
LOG.debug("K8s API Query %s", knp_path)
knps = kubernetes.get(knp_path)
LOG.debug("Return Kuryr Network Policies with label %s", knps)
else:
knps = kubernetes.get('{}/{}/kuryrnetpolicies'.format(
constants.K8S_API_CRD_NAMESPACES, namespace))
knp_path = '{}/{}/kuryrnetpolicies'.format(
constants.K8S_API_CRD_NAMESPACES, namespace)
LOG.debug("K8s API Query %s", knp_path)
knps = kubernetes.get(knp_path)
LOG.debug("Return Kuryr Network Policies with label %s", knps)
except exceptions.K8sResourceNotFound:
LOG.exception("KuryrNetPolicy CRD not found")
raise
@ -54,29 +47,76 @@ def _get_kuryrnetpolicy_crds(labels=None, namespace='default'):
return knps
def _match_expressions(expressions, pod_labels):
for exp in expressions:
exp_op = exp['operator'].lower()
if pod_labels:
if exp_op in OPERATORS_WITH_VALUES:
exp_values = exp['values']
pod_value = pod_labels.get(str(exp['key']), None)
if exp_op == constants.K8S_OPERATOR_IN:
if pod_value is None or pod_value not in exp_values:
return False
elif exp_op == constants.K8S_OPERATOR_NOT_IN:
if pod_value in exp_values:
return False
else:
if exp_op == constants.K8S_OPERATOR_EXISTS:
exists = pod_labels.get(str(exp['key']), None)
if exists is None:
return False
elif exp_op == constants.K8S_OPERATOR_DOES_NOT_EXIST:
exists = pod_labels.get(str(exp['key']), None)
if exists is not None:
return False
else:
if exp_op in (constants.K8S_OPERATOR_IN,
constants.K8S_OPERATOR_EXISTS):
return False
return True
def _match_labels(crd_labels, pod_labels):
for label_key, label_value in crd_labels.items():
pod_value = pod_labels.get(label_key, None)
if not pod_value or label_value != pod_value:
return False
return True
class NetworkPolicySecurityGroupsDriver(base.PodSecurityGroupsDriver):
"""Provides security groups for pods based on network policies"""
def get_security_groups(self, pod, project_id):
sg_list = []
pod_namespace = pod['metadata']['namespace']
pod_labels = pod['metadata'].get('labels')
LOG.debug("Using labels %s", pod_labels)
pod_namespace = pod['metadata']['namespace']
if pod_labels:
knp_crds = _get_kuryrnetpolicy_crds(pod_labels,
namespace=pod_namespace)
for crd in knp_crds.get('items'):
knp_crds = _get_kuryrnetpolicy_crds(namespace=pod_namespace)
for crd in knp_crds.get('items'):
pod_selector = crd['spec'].get('podSelector')
if pod_selector:
crd_labels = pod_selector.get('matchLabels', None)
crd_expressions = pod_selector.get('matchExpressions', None)
match_exp = match_lb = True
if crd_expressions:
match_exp = _match_expressions(crd_expressions,
pod_labels)
if crd_labels and pod_labels:
match_lb = _match_labels(crd_labels, pod_labels)
if match_exp and match_lb:
LOG.debug("Appending %s",
str(crd['spec']['securityGroupId']))
sg_list.append(str(crd['spec']['securityGroupId']))
else:
LOG.debug("Appending %s", str(crd['spec']['securityGroupId']))
sg_list.append(str(crd['spec']['securityGroupId']))
knp_namespace_crds = _get_kuryrnetpolicy_crds(namespace=pod_namespace)
for crd in knp_namespace_crds.get('items'):
if not crd['metadata'].get('labels'):
LOG.debug("Appending %s", str(crd['spec']['securityGroupId']))
sg_list.append(str(crd['spec']['securityGroupId']))
if not knp_namespace_crds.get('items') and not sg_list:
# NOTE(maysams) Pods that are not selected by any Networkpolicy
# are fully accessible. Thus, the default security group is associated.
if not sg_list:
sg_list = config.CONF.neutron_defaults.pod_security_groups
if not sg_list:
raise cfg.RequiredOptError('pod_security_groups',
@ -104,20 +144,30 @@ class NetworkPolicyServiceSecurityGroupsDriver(
svc_labels = service['metadata'].get('labels')
LOG.debug("Using labels %s", svc_labels)
if svc_labels:
knp_crds = _get_kuryrnetpolicy_crds(svc_labels,
namespace=svc_namespace)
for crd in knp_crds.get('items'):
knp_crds = _get_kuryrnetpolicy_crds(namespace=svc_namespace)
for crd in knp_crds.get('items'):
pod_selector = crd['spec'].get('podSelector')
if pod_selector:
crd_labels = pod_selector.get('matchLabels', None)
crd_expressions = pod_selector.get('matchExpressions', None)
match_exp = match_lb = True
if crd_expressions:
match_exp = _match_expressions(crd_expressions,
svc_labels)
if crd_labels and svc_labels:
match_lb = _match_labels(crd_labels, svc_labels)
if match_exp and match_lb:
LOG.debug("Appending %s",
str(crd['spec']['securityGroupId']))
sg_list.append(str(crd['spec']['securityGroupId']))
else:
LOG.debug("Appending %s", str(crd['spec']['securityGroupId']))
sg_list.append(str(crd['spec']['securityGroupId']))
knp_namespace_crds = _get_kuryrnetpolicy_crds(namespace=svc_namespace)
for crd in knp_namespace_crds.get('items'):
if not crd['metadata'].get('labels'):
LOG.debug("Appending %s", str(crd['spec']['securityGroupId']))
sg_list.append(str(crd['spec']['securityGroupId']))
if not knp_namespace_crds.get('items') and not sg_list:
# NOTE(maysams) Pods that are not selected by any Networkpolicy
# are fully accessible. Thus, the default security group is associated.
if not sg_list:
sg_list = config.CONF.neutron_defaults.pod_security_groups
if not sg_list:
raise cfg.RequiredOptError('pod_security_groups',

View File

@ -14,12 +14,17 @@
# under the License.
from oslo_serialization import jsonutils
from six.moves.urllib import parse
from kuryr_kubernetes import clients
from kuryr_kubernetes import constants
from kuryr_kubernetes import exceptions as k_exc
from kuryr_kubernetes import os_vif_util as ovu
from kuryr_kubernetes import utils
OPERATORS_WITH_VALUES = [constants.K8S_OPERATOR_IN,
constants.K8S_OPERATOR_NOT_IN]
def get_network_id(subnets):
ids = ovu.osvif_to_neutron_network_ids(subnets)
@ -58,3 +63,51 @@ def get_pod_state(pod):
def is_host_network(pod):
return pod['spec'].get('hostNetwork', False)
def get_pods(selector, namespace):
kubernetes = clients.get_kubernetes_client()
labels = selector.get('matchLabels', None)
if labels:
# Removing pod-template-hash as pods will not have it and
# otherwise there will be no match
labels.pop('pod-template-hash', None)
labels = replace_encoded_characters(labels)
exps = selector.get('matchExpressions', None)
if exps:
exps = ', '.join(format_expression(exp) for exp in exps)
if labels:
expressions = parse.quote("," + exps)
labels += expressions
else:
labels = parse.quote(exps)
pods = kubernetes.get(
'{}/namespaces/{}/pods?labelSelector={}'.format(
constants.K8S_API_BASE, namespace, labels))
return pods
def format_expression(expression):
key = expression['key']
operator = expression['operator'].lower()
if operator in OPERATORS_WITH_VALUES:
values = expression['values']
values = str(', '.join(values))
values = "(%s)" % values
return "%s %s %s" % (key, operator, values)
else:
if operator == constants.K8S_OPERATOR_DOES_NOT_EXIST:
return "!%s" % key
else:
return key
def replace_encoded_characters(labels):
labels = parse.urlencode(labels)
# NOTE(ltomasbo): K8s API does not accept &, so we need to AND
# the matchLabels with ',' or '%2C' instead
labels = labels.replace('&', ',')
return labels

View File

@ -61,12 +61,6 @@ class NetworkPolicyHandler(k8s_base.ResourceEventHandler):
project_id = self._drv_project.get_project(policy)
pods_to_update = []
knps_on_namespace = self._drv_policy.knps_on_namespace(
policy['metadata']['namespace'])
if not knps_on_namespace:
namespace_pods = self._drv_policy.namespaced_pods(policy)
pods_to_update.extend(namespace_pods)
modified_pods = self._drv_policy.ensure_network_policy(policy,
project_id)
if modified_pods:
@ -89,18 +83,15 @@ class NetworkPolicyHandler(k8s_base.ResourceEventHandler):
pod_sgs = self._drv_pod_sg.get_security_groups(pod, project_id)
if crd_sg in pod_sgs:
pod_sgs.remove(crd_sg)
if not pod_sgs:
pod_sgs = oslo_cfg.CONF.neutron_defaults.pod_security_groups
if not pod_sgs:
raise oslo_cfg.RequiredOptError('pod_security_groups',
oslo_cfg.OptGroup(
'neutron_defaults'))
self._drv_vif_pool.update_vif_sgs(pod, pod_sgs)
self._drv_policy.release_network_policy(netpolicy_crd)
# re-apply original security groups for the namespace
knps_on_namespace = self._drv_policy.knps_on_namespace(
policy['metadata']['namespace'])
if not knps_on_namespace:
namespace_pods = self._drv_policy.namespaced_pods(policy)
for pod in namespace_pods:
pod_sgs = self._drv_pod_sg.get_security_groups(pod,
project_id)
self._drv_vif_pool.update_vif_sgs(pod, pod_sgs)
@MEMOIZE
def is_ready(self, quota):

View File

@ -25,9 +25,9 @@ class TestNetworkPolicySecurityGroupsDriver(test_base.TestCase):
def setUp(self):
super(TestNetworkPolicySecurityGroupsDriver, self).setUp()
self._labels = mock.sentinel.labels
self._project_id = mock.sentinel.project_id
self._sg_id = mock.sentinel.sg_id
self._sg_id2 = mock.sentinel._sg_id2
self._namespace = 'default'
self._crd = {
'metadata': {'name': mock.sentinel.name,
@ -55,9 +55,38 @@ class TestNetworkPolicySecurityGroupsDriver(test_base.TestCase):
'security_group_id': self._sg_id,
'id': mock.sentinel.id
}}],
'podSelector': {
'matchExpressions': [
{
'key': 'environment',
'operator': 'In',
'values': [
'production']}],
'matchLabels': {
'run': 'demo'
}},
'securityGroupId': self._sg_id,
'securityGroupName': mock.sentinel.sg_name}}
self._crd2 = {
'metadata': {'name': mock.sentinel.name3,
'selfLink': mock.sentinel.selfLink},
'spec': {
'ingressSgRules': [
{'security_group_rule':
{'description': 'Kuryr-Kubernetes NetPolicy SG rule',
'direction': 'ingress',
'ethertype': 'IPv4',
'port_range_max': 8080,
'port_range_min': 8080,
'protocol': 'tcp',
'security_group_id': self._sg_id2,
'id': mock.sentinel.id
}}],
'podSelector': {},
'securityGroupId': self._sg_id2,
'securityGroupName': mock.sentinel.sg_name}}
self._crds = {
"apiVersion": "v1",
"items": [self._crd],
@ -66,6 +95,14 @@ class TestNetworkPolicySecurityGroupsDriver(test_base.TestCase):
"resourceVersion": "",
"selfLink": mock.sentinel.selfLink}}
self._multiple_crds = {
"apiVersion": "v1",
"items": [self._crd, self._crd2],
"kind": "List",
"metadata": {
"resourceVersion": "",
"selfLink": mock.sentinel.selfLink}}
self._empty_crds = {
"apiVersion": "v1",
"items": [],
@ -81,7 +118,39 @@ class TestNetworkPolicySecurityGroupsDriver(test_base.TestCase):
'name': mock.sentinel.pod_name,
'namespace': self._namespace,
'labels': {
'run': 'demo'}},
'run': 'demo',
'environment': 'production'}},
'spec': {
'containers': [{
'image': 'kuryr/demo',
'imagePullPolicy': 'Always',
'name': mock.sentinel.pod_name
}]
}}
self._pod2 = {
'apiVersion': 'v1',
'kind': 'Pod',
'metadata': {
'name': mock.sentinel.pod_name,
'namespace': self._namespace,
'labels': {
'run': 'demo',
'environment': 'development'}},
'spec': {
'containers': [{
'image': 'kuryr/demo',
'imagePullPolicy': 'Always',
'name': mock.sentinel.pod_name
}]
}}
self._pod_without_label = {
'apiVersion': 'v1',
'kind': 'Pod',
'metadata': {
'name': mock.sentinel.pod_name,
'namespace': self._namespace},
'spec': {
'containers': [{
'image': 'kuryr/demo',
@ -94,35 +163,92 @@ class TestNetworkPolicySecurityGroupsDriver(test_base.TestCase):
self._driver = (
network_policy_security_groups.NetworkPolicySecurityGroupsDriver())
@mock.patch('kuryr_kubernetes.config.CONF')
@mock.patch.object(network_policy_security_groups,
'_get_kuryrnetpolicy_crds')
def test_get_security_groups(self, m_get_crds):
def test_get_sgs_for_pod_without_label(self, m_get_crds, m_cfg):
m_get_crds.return_value = self._crds
self._driver.get_security_groups(self._pod, self._project_id)
calls = [mock.call(self._pod['metadata']['labels'],
namespace=self._namespace),
mock.call(namespace=self._namespace)]
m_get_crds.assert_has_calls(calls)
sg_list = [str(mock.sentinel.sg_id)]
m_cfg.neutron_defaults.pod_security_groups = sg_list
sgs = self._driver.get_security_groups(self._pod_without_label,
self._project_id)
@mock.patch.object(network_policy_security_groups,
'_get_kuryrnetpolicy_crds')
def test_get_security_groups_without_label(self, m_get_crds):
pod = self._pod.copy()
del pod['metadata']['labels']
labels = {'run': 'demo'}
self._crds['items'][0]['metadata']['labels'] = labels
m_get_crds.return_value = self._crds
self._driver.get_security_groups(pod, self._project_id)
m_get_crds.assert_called_once_with(namespace=self._namespace)
self.assertEqual(sg_list, sgs)
@mock.patch.object(network_policy_security_groups,
'_match_expressions')
@mock.patch.object(network_policy_security_groups,
'_match_labels')
@mock.patch.object(network_policy_security_groups,
'_get_kuryrnetpolicy_crds')
def test_get_sgs_for_pod_with_label(self, m_get_crds, m_match_labels,
m_match_expressions):
m_get_crds.return_value = self._crds
m_match_expressions.return_value = True
m_match_labels.return_value = True
pod_labels = self._pod['metadata']['labels']
resp = self._driver.get_security_groups(self._pod, self._project_id)
m_get_crds.assert_called_once_with(namespace=self._namespace)
m_match_expressions.assert_called_once_with(
self._crd['spec']['podSelector']['matchExpressions'], pod_labels)
m_match_labels.assert_called_once_with(
self._crd['spec']['podSelector']['matchLabels'], pod_labels)
self.assertEqual(resp, [str(self._sg_id)])
@mock.patch('kuryr_kubernetes.config.CONF')
@mock.patch.object(network_policy_security_groups,
'_match_expressions')
@mock.patch.object(network_policy_security_groups,
'_match_labels')
@mock.patch.object(network_policy_security_groups,
'_get_kuryrnetpolicy_crds')
def test_get_sgs_for_pod_with_label_no_match(self, m_get_crds,
m_match_labels,
m_match_expressions, m_cfg):
m_get_crds.return_value = self._crds
m_match_expressions.return_value = False
m_match_labels.return_value = True
sg_list = [mock.sentinel.sg_id]
m_cfg.neutron_defaults.pod_security_groups = sg_list
pod_labels = self._pod2['metadata']['labels']
sgs = self._driver.get_security_groups(self._pod2, self._project_id)
m_get_crds.assert_called_once_with(namespace=self._namespace)
m_match_expressions.assert_called_once_with(
self._crd['spec']['podSelector']['matchExpressions'], pod_labels)
m_match_labels.assert_called_once_with(
self._crd['spec']['podSelector']['matchLabels'], pod_labels)
self.assertEqual(sg_list, sgs)
@mock.patch.object(network_policy_security_groups,
'_get_kuryrnetpolicy_crds')
def test_get_security_groups_no_crds(self, m_get_crds):
def test_get_sgs_no_crds(self, m_get_crds):
m_get_crds.return_value = self._empty_crds
cfg.CONF.set_override('pod_security_groups', [],
group='neutron_defaults')
self.assertRaises(cfg.RequiredOptError,
self._driver.get_security_groups, self._pod,
self._project_id)
calls = [mock.call(self._pod['metadata']['labels'],
namespace=self._namespace),
mock.call(namespace=self._namespace)]
m_get_crds.assert_has_calls(calls)
m_get_crds.assert_called_with(namespace=self._namespace)
@mock.patch.object(network_policy_security_groups,
'_match_expressions')
@mock.patch.object(network_policy_security_groups,
'_match_labels')
@mock.patch.object(network_policy_security_groups,
'_get_kuryrnetpolicy_crds')
def test_get_sgs_multiple_crds(self, m_get_crds, m_match_labels,
m_match_expressions):
m_match_expressions.return_value = True
m_match_labels.return_value = True
m_get_crds.return_value = self._multiple_crds
resp = self._driver.get_security_groups(self._pod, self._project_id)
m_get_crds.assert_called_once_with(namespace=self._namespace)
self.assertEqual([str(self._sg_id), str(self._sg_id2)], resp)

View File

@ -139,34 +139,25 @@ class TestPolicyHandler(test_base.TestCase):
def test_on_present_without_knps_on_namespace(self):
modified_pod = mock.sentinel.modified_pod
match_pod = mock.sentinel.match_pod
namespace_pod = mock.sentinel.namespace_pod
knp_on_ns = self._handler._drv_policy.knps_on_namespace
knp_on_ns.return_value = False
namespaced_pods = self._handler._drv_policy.namespaced_pods
namespaced_pods.return_value = [namespace_pod]
ensure_nw_policy = self._handler._drv_policy.ensure_network_policy
ensure_nw_policy.return_value = [modified_pod]
affected_pods = self._handler._drv_policy.affected_pods
affected_pods.return_value = [match_pod]
sg1 = [mock.sentinel.sg1]
sg2 = [mock.sentinel.sg2]
sg3 = [mock.sentinel.sg3]
self._get_security_groups.side_effect = [sg1, sg2, sg3]
self._get_security_groups.side_effect = [sg2, sg3]
policy.NetworkPolicyHandler.on_present(self._handler, self._policy)
namespaced_pods.assert_called_once_with(self._policy)
ensure_nw_policy.assert_called_once_with(self._policy,
self._project_id)
affected_pods.assert_called_once_with(self._policy)
calls = [mock.call(namespace_pod, self._project_id),
mock.call(modified_pod, self._project_id),
calls = [mock.call(modified_pod, self._project_id),
mock.call(match_pod, self._project_id)]
self._get_security_groups.assert_has_calls(calls)
calls = [mock.call(namespace_pod, sg1),
mock.call(modified_pod, sg2),
calls = [mock.call(modified_pod, sg2),
mock.call(match_pod, sg3)]
self._update_vif_sgs.assert_has_calls(calls)
@ -189,8 +180,6 @@ class TestPolicyHandler(test_base.TestCase):
policy.NetworkPolicyHandler.on_deleted(self._handler, self._policy)
release_nw_policy.assert_called_once_with(knp_obj)
calls = [mock.call(match_pod, self._project_id),
mock.call(namespace_pod, self._project_id)]
self._get_security_groups.assert_has_calls(calls)
calls = [mock.call(match_pod, sg1), mock.call(namespace_pod, sg2)]
self._update_vif_sgs.assert_has_calls(calls)
self._get_security_groups.assert_called_once_with(match_pod,
self._project_id)
self._update_vif_sgs.assert_called_once_with(match_pod, sg1)