Ensure pod relabeling is supported by the Network Policy

This patch adds a new handler in charge of reacting to pod relabeling
actions. It main purpose is to use it together with the network policy
handler and drivers to ensure the right policy is applied upon pod
label changes.

Partially Implements: blueprint k8s-network-policies

Change-Id: If026cefce847f77c54af09a0160eb35343f89f37
This commit is contained in:
Luis Tomas Bolivar 2018-11-16 14:08:47 +01:00
parent 4f71019c94
commit 96e314b0a9
5 changed files with 256 additions and 7 deletions

View File

@ -7,7 +7,7 @@ handlers at kuryr.conf (further info on how to do this can be found at
:doc:`./devstack/containerized`)::
[kubernetes]
enabled_handlers=vif,lb,lbaasspec,policy
enabled_handlers=vif,lb,lbaasspec,policy,pod_label
After that, enable also the security group drivers for policies::
@ -28,7 +28,7 @@ Same for containerized deployments::
For directly enabling the driver when deploying with devstack, you just need
to add the policy handler and drivers with::
KURYR_ENABLED_HANDLERS=vif,lb,lbaasspec,policy
KURYR_ENABLED_HANDLERS=vif,lb,lbaasspec,policy,pod_label
KURYR_SG_DRIVER=policy
Testing the network policy support functionality
@ -44,7 +44,7 @@ Testing the network policy support functionality
spec:
podSelector:
matchLabels:
role: db
project: default
policyTypes:
- Ingress
- Egress
@ -143,7 +143,7 @@ Testing the network policy support functionality
protocol: TCP
podSelector:
matchLabels:
role: db
project: default
policyTypes:
- Ingress
- Egress
@ -246,6 +246,9 @@ Testing the network policy support functionality
- namespaceSelector:
matchLabels:
project: default
podSelector:
matchLabels:
project: default
policyTypes:
- Ingress
- Egress
@ -264,10 +267,40 @@ Testing the network policy support functionality
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.
Note the curl only works 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. We can also create a single pod, without a label and check that there is
no connectivity to it, as it does not match the network policy
podSelector::
$ cat sample-pod.yml
apiVersion: v1
kind: Pod
metadata:
name: demo-pod
spec:
containers:
- image: kuryr/demo
imagePullPolicy: Always
name: demo-pod
$ kubectl apply -f sample-pod.yml
$ curl demo-pod-IP:8080
NO REPLY
11. If we add to the pod a label that match a network policy podSelector, in
this case 'project: default', the network policy will get applied on the
pod, and the traffic will be allowed::
$ kubectl label pod demo-pod project=default
$ curl demo-pod-IP:8080
demo-pod-XXX: HELLO! I AM ALIVE!!!
12. Confirm the teardown of the resources once the network policy is removed::
$ kubectl delete -f network_policy.yml
$ kubectl get kuryrnetpolicies

View File

@ -36,6 +36,7 @@ K8S_POD_STATUS_PENDING = 'Pending'
K8S_ANNOTATION_PREFIX = 'openstack.org/kuryr'
K8S_ANNOTATION_VIF = K8S_ANNOTATION_PREFIX + '-vif'
K8S_ANNOTATION_LABEL = K8S_ANNOTATION_PREFIX + '-pod-label'
K8S_ANNOTATION_LBAAS_SPEC = K8S_ANNOTATION_PREFIX + '-lbaas-spec'
K8S_ANNOTATION_LBAAS_STATE = K8S_ANNOTATION_PREFIX + '-lbaas-state'
K8S_ANNOTATION_NET_CRD = K8S_ANNOTATION_PREFIX + '-net-crd'

View File

@ -0,0 +1,94 @@
# Copyright 2018 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_log import log as logging
from oslo_serialization import jsonutils
from kuryr_kubernetes import clients
from kuryr_kubernetes import constants
from kuryr_kubernetes.controller.drivers import base as drivers
from kuryr_kubernetes import exceptions
from kuryr_kubernetes.handlers import k8s_base
LOG = logging.getLogger(__name__)
class PodLabelHandler(k8s_base.ResourceEventHandler):
"""Controller side of Pod Label process for Kubernetes pods.
`PodLabelHandler` runs on the Kuryr-Kubernetes controller and is
responsible for triggering the vif port updates upon pod labels changes.
"""
OBJECT_KIND = constants.K8S_OBJ_POD
OBJECT_WATCH_PATH = "%s/%s" % (constants.K8S_API_BASE, "pods")
def __init__(self):
super(PodLabelHandler, self).__init__()
self._drv_project = drivers.PodProjectDriver.get_instance()
self._drv_sg = drivers.PodSecurityGroupsDriver.get_instance()
self._drv_vif_pool = drivers.VIFPoolDriver.get_instance(
specific_driver='multi_pool')
self._drv_vif_pool.set_vif_driver()
def on_modified(self, pod):
if not self._has_pod_state(pod):
# NOTE(ltomasbo): Ensuring the event is retried and the right
# pod label annotation is added to the pod
raise exceptions.ResourceNotReady(pod)
current_pod_labels = pod['metadata'].get('labels')
previous_pod_labels = self._get_pod_labels(pod)
LOG.debug("Got previous pod labels from annotation: %r",
previous_pod_labels)
if current_pod_labels == previous_pod_labels:
return
project_id = self._drv_project.get_project(pod)
security_groups = self._drv_sg.get_security_groups(pod, project_id)
self._drv_vif_pool.update_vif_sgs(pod, security_groups)
self._set_pod_labels(pod, current_pod_labels)
def _get_pod_labels(self, pod):
try:
annotations = pod['metadata']['annotations']
pod_labels_annotation = annotations[constants.K8S_ANNOTATION_LABEL]
except KeyError:
return None
pod_labels = jsonutils.loads(pod_labels_annotation)
return pod_labels
def _set_pod_labels(self, pod, labels):
if not labels:
LOG.debug("Removing Label annotation: %r", labels)
annotation = None
else:
annotation = jsonutils.dumps(labels, sort_keys=True)
LOG.debug("Setting Labels annotation: %r", annotation)
k8s = clients.get_kubernetes_client()
k8s.annotate(pod['metadata']['selfLink'],
{constants.K8S_ANNOTATION_LABEL: annotation},
resource_version=pod['metadata']['resourceVersion'])
def _has_pod_state(self, pod):
try:
pod_state = pod['metadata']['annotations'][
constants.K8S_ANNOTATION_VIF]
LOG.debug("Pod state is: %s", pod_state)
except KeyError:
return False
return True

View File

@ -0,0 +1,120 @@
# Copyright 2018 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import mock
from kuryr_kubernetes import constants as k_const
from kuryr_kubernetes.controller.drivers import base as drivers
from kuryr_kubernetes.controller.handlers import pod_label as p_label
from kuryr_kubernetes import exceptions
from kuryr_kubernetes.tests import base as test_base
class TestPodLabelHandler(test_base.TestCase):
def setUp(self):
super(TestPodLabelHandler, self).setUp()
self._project_id = mock.sentinel.project_id
self._sg_id = mock.sentinel.sg_id
self._pod_version = mock.sentinel.pod_version
self._pod_link = mock.sentinel.pod_link
self._pod = {
'metadata': {'resourceVersion': self._pod_version,
'selfLink': self._pod_link},
'status': {'phase': k_const.K8S_POD_STATUS_PENDING},
'spec': {'hostNetwork': False,
'nodeName': 'hostname'}
}
self._handler = mock.MagicMock(spec=p_label.PodLabelHandler)
self._handler._drv_project = mock.Mock(spec=drivers.PodProjectDriver)
self._handler._drv_sg = mock.Mock(spec=drivers.PodSecurityGroupsDriver)
self._handler._drv_vif_pool = mock.MagicMock(
spec=drivers.VIFPoolDriver)
self._get_project = self._handler._drv_project.get_project
self._get_security_groups = self._handler._drv_sg.get_security_groups
self._set_vif_driver = self._handler._drv_vif_pool.set_vif_driver
self._get_pod_labels = self._handler._get_pod_labels
self._set_pod_labels = self._handler._set_pod_labels
self._has_pod_state = self._handler._has_pod_state
self._update_vif_sgs = self._handler._drv_vif_pool.update_vif_sgs
self._get_project.return_value = self._project_id
self._get_security_groups.return_value = [self._sg_id]
@mock.patch.object(drivers.VIFPoolDriver, 'get_instance')
@mock.patch.object(drivers.PodSecurityGroupsDriver, 'get_instance')
@mock.patch.object(drivers.PodProjectDriver, 'get_instance')
def test_init(self, m_get_project_driver, m_get_sg_driver,
m_get_vif_pool_driver):
project_driver = mock.sentinel.project_driver
sg_driver = mock.sentinel.sg_driver
vif_pool_driver = mock.Mock(spec=drivers.VIFPoolDriver)
m_get_project_driver.return_value = project_driver
m_get_sg_driver.return_value = sg_driver
m_get_vif_pool_driver.return_value = vif_pool_driver
handler = p_label.PodLabelHandler()
self.assertEqual(project_driver, handler._drv_project)
self.assertEqual(sg_driver, handler._drv_sg)
self.assertEqual(vif_pool_driver, handler._drv_vif_pool)
def test_on_modified(self):
self._has_pod_state.return_value = True
self._get_pod_labels.return_value = {'test1': 'test'}
p_label.PodLabelHandler.on_modified(self._handler, self._pod)
self._has_pod_state.assert_called_once_with(self._pod)
self._get_pod_labels.assert_called_once_with(self._pod)
self._get_project.assert_called_once()
self._get_security_groups.assert_called_once()
self._update_vif_sgs.assert_called_once_with(self._pod, [self._sg_id])
self._set_pod_labels.assert_called_once_with(self._pod, None)
def test_on_modified_no_state(self):
self._has_pod_state.return_value = False
self.assertRaises(exceptions.ResourceNotReady,
p_label.PodLabelHandler.on_modified, self._handler,
self._pod)
self._has_pod_state.assert_called_once_with(self._pod)
self._get_pod_labels.assert_not_called()
self._set_pod_labels.assert_not_called()
def test_on_modified_no_labels(self):
self._has_pod_state.return_value = True
self._get_pod_labels.return_value = None
p_label.PodLabelHandler.on_modified(self._handler, self._pod)
self._has_pod_state.assert_called_once_with(self._pod)
self._get_pod_labels.assert_called_once_with(self._pod)
self._set_pod_labels.assert_not_called()
def test_on_modified_no_changes(self):
self._has_pod_state.return_value = True
pod_with_label = self._pod.copy()
pod_with_label['metadata']['labels'] = {'test1': 'test'}
self._get_pod_labels.return_value = {'test1': 'test'}
p_label.PodLabelHandler.on_modified(self._handler, pod_with_label)
self._has_pod_state.assert_called_once_with(pod_with_label)
self._get_pod_labels.assert_called_once_with(pod_with_label)
self._set_pod_labels.assert_not_called()

View File

@ -102,6 +102,7 @@ kuryr_kubernetes.controller.handlers =
ingresslb = kuryr_kubernetes.controller.handlers.ingress_lbaas:IngressLoadBalancerHandler
ocproute = kuryr_kubernetes.platform.ocp.controller.handlers.route:OcpRouteHandler
policy = kuryr_kubernetes.controller.handlers.policy:NetworkPolicyHandler
pod_label = kuryr_kubernetes.controller.handlers.pod_label:PodLabelHandler
test_handler = kuryr_kubernetes.tests.unit.controller.handlers.test_fake_handler:TestHandler
kuryr_kubernetes.controller.drivers.multi_vif =