# Copyright [2017] [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 __future__ import absolute_import, division, print_function __metaclass__ = type from ansible_collections.kubernetes.core.plugins.module_utils.apply import merge, apply_patch tests = [ dict( last_applied=dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", two="2") ), desired=dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", two="2") ), expected={} ), dict( last_applied=dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", two="2") ), desired=dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", two="2", three="3") ), expected=dict(data=dict(three="3")) ), dict( last_applied=dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", two="2") ), desired=dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", three="3") ), expected=dict(data=dict(two=None, three="3")) ), dict( last_applied=dict( kind="ConfigMap", metadata=dict(name="foo", annotations=dict(this="one", hello="world")), data=dict(one="1", two="2") ), desired=dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", three="3") ), expected=dict(metadata=dict(annotations=None), data=dict(two=None, three="3")) ), dict( last_applied=dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, name="http")]) ), actual=dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, protocol='TCP', name="http")]) ), desired=dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, name="http")]) ), expected=dict(spec=dict(ports=[dict(port=8080, protocol='TCP', name="http")])) ), dict( last_applied=dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, name="http")]) ), actual=dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, protocol='TCP', name="http")]) ), desired=dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8081, name="http")]) ), expected=dict(spec=dict(ports=[dict(port=8081, name="http")])) ), dict( last_applied=dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, name="http")]) ), actual=dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, protocol='TCP', name="http")]) ), desired=dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8443, name="https"), dict(port=8080, name="http")]) ), expected=dict(spec=dict(ports=[dict(port=8443, name="https"), dict(port=8080, name="http", protocol='TCP')])) ), dict( last_applied=dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8443, name="https"), dict(port=8080, name="http")]) ), actual=dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8443, protocol='TCP', name="https"), dict(port=8080, protocol='TCP', name='http')]) ), desired=dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8080, name="http")]) ), expected=dict(spec=dict(ports=[dict(port=8080, name="http", protocol='TCP')])) ), dict( last_applied=dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8443, name="https", madeup="xyz"), dict(port=8080, name="http")]) ), actual=dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8443, protocol='TCP', name="https", madeup="xyz"), dict(port=8080, protocol='TCP', name='http')]) ), desired=dict( kind="Service", metadata=dict(name="foo"), spec=dict(ports=[dict(port=8443, name="https")]) ), expected=dict(spec=dict(ports=[dict(madeup=None, port=8443, name="https", protocol='TCP')])) ), dict( last_applied=dict( kind="Pod", metadata=dict(name="foo"), spec=dict(containers=[dict(name="busybox", image="busybox", resources=dict(requests=dict(cpu="100m", memory="100Mi"), limits=dict(cpu="100m", memory="100Mi")))]) ), actual=dict( kind="Pod", metadata=dict(name="foo"), spec=dict(containers=[dict(name="busybox", image="busybox", resources=dict(requests=dict(cpu="100m", memory="100Mi"), limits=dict(cpu="100m", memory="100Mi")))]) ), desired=dict( kind="Pod", metadata=dict(name="foo"), spec=dict(containers=[dict(name="busybox", image="busybox", resources=dict(requests=dict(cpu="50m", memory="50Mi"), limits=dict(memory="50Mi")))]) ), expected=dict(spec=dict(containers=[dict(name="busybox", image="busybox", resources=dict(requests=dict(cpu="50m", memory="50Mi"), limits=dict(cpu=None, memory="50Mi")))])) ), dict( desired=dict(kind='Pod', spec=dict(containers=[ dict(name='hello', volumeMounts=[dict(name="test", mountPath="/test")]) ], volumes=[ dict(name="test", configMap=dict(name="test")), ])), last_applied=dict(kind='Pod', spec=dict(containers=[ dict(name='hello', volumeMounts=[dict(name="test", mountPath="/test")]) ], volumes=[ dict(name="test", configMap=dict(name="test"))])), actual=dict(kind='Pod', spec=dict(containers=[ dict(name='hello', volumeMounts=[ dict(name="test", mountPath="/test"), dict(mountPath="/var/run/secrets/kubernetes.io/serviceaccount", name="default-token-xyz")]) ], volumes=[ dict(name="test", configMap=dict(name="test")), dict(name="default-token-xyz", secret=dict(secretName="default-token-xyz")), ])), expected=dict(spec=dict(containers=[dict(name='hello', volumeMounts=[dict(name="test", mountPath="/test"), dict(mountPath="/var/run/secrets/kubernetes.io/serviceaccount", name="default-token-xyz")])], volumes=[dict(name="test", configMap=dict(name="test")), dict(name="default-token-xyz", secret=dict(secretName="default-token-xyz"))])), ), # This next one is based on a real world case where definition was mostly # str type and everything else was mostly unicode type (don't ask me how) dict( last_applied={ u'kind': u'ConfigMap', u'data': {u'one': '1', 'three': '3', 'two': '2'}, u'apiVersion': u'v1', u'metadata': {u'namespace': u'apply', u'name': u'apply-configmap'} }, actual={ u'kind': u'ConfigMap', u'data': {u'one': '1', 'three': '3', 'two': '2'}, u'apiVersion': u'v1', u'metadata': {u'namespace': u'apply', u'name': u'apply-configmap', u'resourceVersion': '1714994', u'creationTimestamp': u'2019-08-17T05:08:05Z', u'annotations': {}, u'selfLink': u'/api/v1/namespaces/apply/configmaps/apply-configmap', u'uid': u'fed45fb0-c0ac-11e9-9d95-025000000001'} }, desired={ 'kind': u'ConfigMap', 'data': {'one': '1', 'three': '3', 'two': '2'}, 'apiVersion': 'v1', 'metadata': {'namespace': 'apply', 'name': 'apply-configmap'} }, expected=dict() ), # apply a Deployment, then scale the Deployment (which doesn't affect last-applied) # then apply the Deployment again. Should un-scale the Deployment dict( last_applied={ 'kind': u'Deployment', 'spec': { 'replicas': 1, 'template': { 'spec': { 'containers': [ { 'name': 'this_must_exist', 'envFrom': [ { 'configMapRef': { 'name': 'config-xyz' } }, { 'secretRef': { 'name': 'config-wxy' } } ] } ] } } }, 'metadata': { 'namespace': 'apply', 'name': u'apply-deployment' } }, actual={ 'kind': u'Deployment', 'spec': { 'replicas': 0, 'template': { 'spec': { 'containers': [ { 'name': 'this_must_exist', 'envFrom': [ { 'configMapRef': { 'name': 'config-xyz' } }, { 'secretRef': { 'name': 'config-wxy' } } ] } ] } } }, 'metadata': { 'namespace': 'apply', 'name': u'apply-deployment' } }, desired={ 'kind': u'Deployment', 'spec': { 'replicas': 1, 'template': { 'spec': { 'containers': [ { 'name': 'this_must_exist', 'envFrom': [ { 'configMapRef': { 'name': 'config-abc' } } ] } ] } } }, 'metadata': { 'namespace': 'apply', 'name': u'apply-deployment' } }, expected={ 'spec': { 'replicas': 1, 'template': { 'spec': { 'containers': [ { 'name': 'this_must_exist', 'envFrom': [ { 'configMapRef': { 'name': 'config-abc' } } ] } ] } } } } ), dict( last_applied={ 'kind': 'MadeUp', 'toplevel': { 'original': 'entry' } }, actual={ 'kind': 'MadeUp', 'toplevel': { 'original': 'entry', 'another': { 'nested': { 'entry': 'value' } } } }, desired={ 'kind': 'MadeUp', 'toplevel': { 'original': 'entry', 'another': { 'nested': { 'entry': 'value' } } } }, expected={} ) ] def test_merges(): for test in tests: assert(merge(test['last_applied'], test['desired'], test.get('actual', test['last_applied'])) == test['expected']) def test_apply_patch(): actual = dict( kind="ConfigMap", metadata=dict(name="foo", annotations={'kubectl.kubernetes.io/last-applied-configuration': '{"data":{"one":"1","two":"2"},"kind":"ConfigMap",' '"metadata":{"annotations":{"hello":"world","this":"one"},"name":"foo"}}', 'this': 'one', 'hello': 'world'}), data=dict(one="1", two="2") ) desired = dict( kind="ConfigMap", metadata=dict(name="foo"), data=dict(one="1", three="3") ) expected = dict( metadata=dict( annotations={'kubectl.kubernetes.io/last-applied-configuration': '{"data":{"one":"1","three":"3"},"kind":"ConfigMap","metadata":{"name":"foo"}}', 'this': None, 'hello': None}), data=dict(two=None, three="3") ) assert(apply_patch(actual, desired) == (actual, expected))