Skip to content

Commit acabfd7

Browse files
Merge pull request #90 from baloise/feat/deploy-array-path
feat(deploy): add support for updating values in YAML arrays
2 parents 7f89c30 + 9c0546d commit acabfd7

File tree

3 files changed

+87
-68
lines changed

3 files changed

+87
-68
lines changed

docs/commands/deploy.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ frontend:
1313
backend:
1414
repository: my-app/backend
1515
tag: 1.0.0 # <- and this one
16+
env:
17+
- name: TEST
18+
value: foo # <- and even one in a list
1619
```
1720
1821
With the following command GitOps CLI will update both values to `1.1.0` on the `master` branch.
@@ -27,14 +30,20 @@ gitopscli deploy \
2730
--organisation "deployment" \
2831
--repository-name "myapp-non-prod" \
2932
--file "example/values.yaml" \
30-
--values "{frontend.tag: 1.1.0, backend.tag: 1.1.0}"
33+
--values "{frontend.tag: 1.1.0, backend.tag: 1.1.0, backend.env.[0].value: bar}"
3134
```
3235

3336
### Number Of Commits
3437

3538
Note that by default GitOps CLI will create a separate commit for every value change:
3639

3740
```
41+
commit 0dcaa136b4c5249576bb1f40b942bff6ac718144
42+
Author: GitOpsCLI <[email protected]>
43+
Date: Thu Mar 12 15:30:32 2020 +0100
44+
45+
changed 'backend.env.[0].value' to 'bar' in example/values.yaml
46+
3847
commit d98913ad8fecf571d5f8c3635f8070b05c43a9ca
3948
Author: GitOpsCLI <[email protected]>
4049
Date: Thu Mar 12 15:30:32 2020 +0100
@@ -59,6 +68,7 @@ Date: Thu Mar 12 15:30:00 2020 +0100
5968
6069
frontend.tag: '1.1.0'
6170
backend.tag: '1.1.0'
71+
backend.env.[0].value: 'bar'
6272
```
6373

6474
### Create Pull Request
@@ -75,7 +85,7 @@ gitopscli deploy \
7585
--organisation "deployment" \
7686
--repository-name "myapp-non-prod" \
7787
--file "example/values.yaml" \
78-
--values "{frontend.tag: 1.1.0, backend.tag: 1.1.0}" \
88+
--values "{frontend.tag: 1.1.0, backend.tag: 1.1.0, backend.env.[0].value: bar}" \
7989
--create-pr \
8090
--auto-merge
8191
```

gitopscli/yaml/yaml_util.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import re
12
from ruamel.yaml import YAML
23

4+
ARRAY_KEY_SEGMENT_PATTERN = re.compile(r"\[(\d+)\]")
5+
36

47
def yaml_load(doc):
58
return YAML().load(doc)
@@ -20,27 +23,34 @@ def get_string(self):
2023
return stream.get_string()
2124

2225

23-
def update_yaml_file(file_path, key, value, create_new=False):
26+
def update_yaml_file(file_path, key, value):
2427
yaml = YAML()
2528
with open(file_path, "r") as stream:
2629
content = yaml.load(stream)
2730

28-
keys, obj = key.split("."), content
29-
for k in keys[:-1]:
30-
if k not in obj or not isinstance(obj[k], dict):
31-
if not create_new:
32-
raise KeyError(f"Key '{key}' not found in YAML!")
33-
obj[k] = dict()
34-
obj = obj[k]
35-
if keys[-1] in obj and obj[keys[-1]] == value:
36-
return False # nothing to update
37-
if not create_new and keys[-1] not in obj:
38-
raise KeyError(f"Key '{key}' not found in YAML!")
39-
obj[keys[-1]] = value
40-
41-
with open(file_path, "w+") as stream:
42-
yaml.dump(content, stream)
43-
return True
31+
key_segments = key.split(".")
32+
current_key_segments = []
33+
parent_item = content
34+
for current_key_segment in key_segments:
35+
current_key_segments.append(current_key_segment)
36+
current_key = ".".join(current_key_segments)
37+
is_array = ARRAY_KEY_SEGMENT_PATTERN.match(current_key_segment)
38+
if is_array:
39+
current_key_segment = int(is_array.group(1))
40+
if not isinstance(parent_item, list) or current_key_segment >= len(parent_item):
41+
raise KeyError(f"Key '{current_key}' not found in YAML!")
42+
else:
43+
if not isinstance(parent_item, dict) or current_key_segment not in parent_item:
44+
raise KeyError(f"Key '{current_key}' not found in YAML!")
45+
if current_key == key:
46+
if parent_item[current_key_segment] == value:
47+
return False # nothing to update
48+
parent_item[current_key_segment] = value
49+
with open(file_path, "w+") as stream:
50+
yaml.dump(content, stream)
51+
return True
52+
parent_item = parent_item[current_key_segment]
53+
raise KeyError(f"Empty key!")
4454

4555

4656
def merge_yaml_element(file_path, element_path, desired_value, delete_missing_key=False):

tests/yaml/test_yaml_util.py

Lines changed: 48 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -47,71 +47,70 @@ def test_yaml_dump(self):
4747
def test_update_yaml_file(self):
4848
test_file = self._create_file(
4949
"""\
50-
a: # comment
51-
# comment
50+
a: # comment 1
51+
# comment 2
5252
b:
53-
d: 1 # comment
54-
c: 2 # comment"""
53+
d: 1 # comment 3
54+
c: 2 # comment 4
55+
e:
56+
- f: 3 # comment 5
57+
g: 4 # comment 6
58+
- [hello, world] # comment 7
59+
- foo: # comment 8
60+
bar # comment 9"""
5561
)
5662

57-
updated = update_yaml_file(test_file, "a.b.c", "2")
58-
self.assertTrue(updated)
63+
self.assertTrue(update_yaml_file(test_file, "a.b.c", "2"))
64+
self.assertFalse(update_yaml_file(test_file, "a.b.c", "2")) # already updated
5965

60-
updated = update_yaml_file(test_file, "a.b.c", "2")
61-
self.assertFalse(updated) # already updated
66+
self.assertTrue(update_yaml_file(test_file, "a.e.[0].g", 42))
67+
self.assertFalse(update_yaml_file(test_file, "a.e.[0].g", 42)) # already updated
6268

63-
expected = """\
64-
a: # comment
65-
# comment
66-
b:
67-
d: 1 # comment
68-
c: '2' # comment
69-
"""
70-
actual = self._read_file(test_file)
71-
self.assertEqual(expected, actual)
69+
self.assertTrue(update_yaml_file(test_file, "a.e.[1].[1]", "tester"))
70+
self.assertFalse(update_yaml_file(test_file, "a.e.[1].[1]", "tester")) # already updated
7271

73-
with pytest.raises(KeyError):
74-
updated = update_yaml_file(test_file, "a.x", "foo")
75-
updated = update_yaml_file(test_file, "a.x", "foo", create_new=True)
76-
self.assertTrue(updated)
72+
self.assertTrue(update_yaml_file(test_file, "a.e.[2]", "replaced object"))
73+
self.assertFalse(update_yaml_file(test_file, "a.e.[2]", "replaced object")) # already updated
7774

7875
expected = """\
79-
a: # comment
80-
# comment
76+
a: # comment 1
77+
# comment 2
8178
b:
82-
d: 1 # comment
83-
c: '2' # comment
84-
x: foo
79+
d: 1 # comment 3
80+
c: '2' # comment 4
81+
e:
82+
- f: 3 # comment 5
83+
g: 42 # comment 6
84+
- [hello, tester] # comment 7
85+
- replaced object
8586
"""
8687
actual = self._read_file(test_file)
8788
self.assertEqual(expected, actual)
8889

89-
with pytest.raises(KeyError):
90-
updated = update_yaml_file(test_file, "a.x.z", "foo_z")
91-
updated = update_yaml_file(test_file, "a.x.z", "foo_z", create_new=True)
92-
self.assertTrue(updated)
90+
with pytest.raises(KeyError) as ex:
91+
update_yaml_file(test_file, "x.y", "foo")
92+
self.assertEqual("\"Key 'x' not found in YAML!\"", str(ex.value))
9393

94-
with pytest.raises(KeyError):
95-
updated = update_yaml_file(test_file, "a.x.y", "foo_y")
96-
updated = update_yaml_file(test_file, "a.x.y", "foo_y", create_new=True)
97-
self.assertTrue(updated)
94+
with pytest.raises(KeyError) as ex:
95+
update_yaml_file(test_file, "[42].y", "foo")
96+
self.assertEqual("\"Key '[42]' not found in YAML!\"", str(ex.value))
9897

99-
with pytest.raises(KeyError):
100-
updated = update_yaml_file(test_file, "a.x.y.z", "foo_y_z")
101-
updated = update_yaml_file(test_file, "a.x.y.z", "foo_y_z", create_new=True)
102-
self.assertTrue(updated)
98+
with pytest.raises(KeyError) as ex:
99+
update_yaml_file(test_file, "a.x", "foo")
100+
self.assertEqual("\"Key 'a.x' not found in YAML!\"", str(ex.value))
101+
102+
with pytest.raises(KeyError) as ex:
103+
update_yaml_file(test_file, "a.[42]", "foo")
104+
self.assertEqual("\"Key 'a.[42]' not found in YAML!\"", str(ex.value))
105+
106+
with pytest.raises(KeyError) as ex:
107+
update_yaml_file(test_file, "a.e.[3]", "foo")
108+
self.assertEqual("\"Key 'a.e.[3]' not found in YAML!\"", str(ex.value))
109+
110+
with pytest.raises(KeyError) as ex:
111+
update_yaml_file(test_file, "a.e.[2].[2]", "foo")
112+
self.assertEqual("\"Key 'a.e.[2].[2]' not found in YAML!\"", str(ex.value))
103113

104-
expected = """\
105-
a: # comment
106-
# comment
107-
b:
108-
d: 1 # comment
109-
c: '2' # comment
110-
x:
111-
z: foo_z
112-
y:
113-
z: foo_y_z
114-
"""
115114
actual = self._read_file(test_file)
116115
self.assertEqual(expected, actual)
117116

0 commit comments

Comments
 (0)