Skip to content

Commit 054932f

Browse files
Take last applied configuration into account when patching (openshift#348)
If, as previously, we ignore the last-applied annotation when comparing desired and actual, we end up with a result where, when deleting all annotations in a change, no annotations are deleted(!) Going from ``` metadata: name: my-pod annotations: x: y ``` to: ``` metadata: name: my-pod ``` used to result in a patch that updated the last applied configuration value only. Now it correctly sets the old annotations to None too. Break out apply_patch as a separate method to allow it be tested in isolation. Ensure that the last-applied annotation is sorted. Ensure that the last-applied annotation is loaded consistently (i.e. without unnecessary conversion to u'value's)
1 parent 3dab226 commit 054932f

File tree

2 files changed

+89
-17
lines changed

2 files changed

+89
-17
lines changed

openshift/dynamic/apply.py

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from collections import OrderedDict
22
from copy import deepcopy
33
import json
4+
import sys
45

56
from openshift.dynamic.exceptions import NotFoundError, ApplyException
67

@@ -49,31 +50,67 @@
4950
)
5051

5152

52-
def apply_object(resource, definition):
53-
desired_annotation = dict(
53+
if sys.version_info.major >= 3:
54+
json_loads_byteified = json.loads
55+
else:
56+
# https://stackoverflow.com/a/33571117
57+
def json_loads_byteified(json_text):
58+
return _byteify(
59+
json.loads(json_text, object_hook=_byteify),
60+
ignore_dicts=True
61+
)
62+
63+
def _byteify(data, ignore_dicts = False):
64+
# if this is a unicode string, return its string representation
65+
if isinstance(data, unicode): # noqa: F821
66+
return data.encode('utf-8')
67+
# if this is a list of values, return list of byteified values
68+
if isinstance(data, list):
69+
return [ _byteify(item, ignore_dicts=True) for item in data ]
70+
# if this is a dictionary, return dictionary of byteified keys and values
71+
# but only if we haven't already byteified it
72+
if isinstance(data, dict) and not ignore_dicts:
73+
return {
74+
_byteify(key, ignore_dicts=True): _byteify(value, ignore_dicts=True)
75+
for key, value in data.items()
76+
}
77+
# if it's anything else, return it in its original form
78+
return data
79+
80+
81+
def annotate(desired):
82+
return dict(
5483
metadata=dict(
5584
annotations={
56-
LAST_APPLIED_CONFIG_ANNOTATION: json.dumps(definition, separators=(',', ':'), indent=None)
85+
LAST_APPLIED_CONFIG_ANNOTATION: json.dumps(desired, separators=(',', ':'), indent=None, sort_keys=True)
5786
}
5887
)
5988
)
60-
try:
61-
actual = resource.get(name=definition['metadata']['name'], namespace=definition['metadata'].get('namespace'))
62-
except NotFoundError:
63-
return None, dict_merge(definition, desired_annotation)
64-
last_applied = actual.metadata.get('annotations',{}).get(LAST_APPLIED_CONFIG_ANNOTATION)
89+
90+
91+
def apply_patch(actual, desired):
92+
last_applied = actual['metadata'].get('annotations',{}).get(LAST_APPLIED_CONFIG_ANNOTATION)
6593

6694
if last_applied:
67-
last_applied = json.loads(last_applied)
68-
actual_dict = actual.to_dict()
69-
del actual_dict['metadata']['annotations'][LAST_APPLIED_CONFIG_ANNOTATION]
70-
patch = merge(last_applied, definition, actual_dict)
95+
# ensure that last_applied doesn't come back as a dict of unicode key/value pairs
96+
# json.loads can be used if we stop supporting python 2
97+
last_applied = json_loads_byteified(last_applied)
98+
patch = merge(dict_merge(last_applied, annotate(last_applied)),
99+
dict_merge(desired, annotate(desired)), actual)
71100
if patch:
72-
return actual.to_dict(), dict_merge(patch, desired_annotation)
101+
return actual, patch
73102
else:
74-
return actual.to_dict(), actual.to_dict()
103+
return actual, actual
75104
else:
76-
return actual.to_dict(), dict_merge(definition, desired_annotation)
105+
return actual, dict_merge(desired, annotate(desired))
106+
107+
108+
def apply_object(resource, definition):
109+
try:
110+
actual = resource.get(name=definition['metadata']['name'], namespace=definition['metadata'].get('namespace'))
111+
except NotFoundError:
112+
return None, dict_merge(definition, annotate(definition))
113+
return apply_patch(actual.to_dict(), definition)
77114

78115

79116
def apply(resource, definition):

test/unit/test_apply.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
from openshift.dynamic.apply import merge
1+
from openshift.dynamic.apply import merge, apply_patch
22

33
tests = [
4-
54
dict(
65
last_applied = dict(
76
kind="ConfigMap",
@@ -41,6 +40,19 @@
4140
),
4241
expected = dict(data=dict(two=None, three="3"))
4342
),
43+
dict(
44+
last_applied = dict(
45+
kind="ConfigMap",
46+
metadata=dict(name="foo", annotations=dict(this="one", hello="world")),
47+
data=dict(one="1", two="2")
48+
),
49+
desired = dict(
50+
kind="ConfigMap",
51+
metadata=dict(name="foo"),
52+
data=dict(one="1", three="3")
53+
),
54+
expected = dict(metadata=dict(annotations=None), data=dict(two=None, three="3"))
55+
),
4456
dict(
4557
last_applied = dict(
4658
kind="Service",
@@ -278,3 +290,26 @@
278290
def test_merges():
279291
for test in tests:
280292
assert(merge(test['last_applied'], test['desired'], test.get('actual', test['last_applied'])) == test['expected'])
293+
294+
295+
def test_apply_patch():
296+
actual = dict(
297+
kind="ConfigMap",
298+
metadata=dict(name="foo",
299+
annotations={'kubectl.kubernetes.io/last-applied-configuration':
300+
'{"data":{"one":"1","two":"2"},"kind":"ConfigMap",'
301+
'"metadata":{"annotations":{"hello":"world","this":"one"},"name":"foo"}}',
302+
'this': 'one', 'hello': 'world'}),
303+
data=dict(one="1", two="2")
304+
)
305+
desired = dict(
306+
kind="ConfigMap",
307+
metadata=dict(name="foo"),
308+
data=dict(one="1", three="3")
309+
)
310+
expected = dict(
311+
metadata=dict(
312+
annotations={'kubectl.kubernetes.io/last-applied-configuration': '{"data":{"one":"1","three":"3"},"kind":"ConfigMap","metadata":{"name":"foo"}}',
313+
'this': None, 'hello': None}),
314+
data=dict(two=None, three="3"))
315+
assert(apply_patch(actual, desired) == (actual, expected))

0 commit comments

Comments
 (0)