Skip to content

Commit 9cb5bb2

Browse files
feat(deploy): add support for updating values in YAML arrays
Support YAML paths containing array indexes, e.g. `a.b.[1].c` for `{a: {b: [foo, {c: bar}]}}`
1 parent 0ff97cb commit 9cb5bb2

3 files changed

Lines changed: 91 additions & 26 deletions

File tree

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 <gitopscli@baloise.dev>
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 <gitopscli@baloise.dev>
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: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import re
12
from ruamel.yaml import YAML
23

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

47
def yaml_load(doc):
58
return YAML().load(doc)
@@ -25,16 +28,27 @@ def update_yaml_file(file_path, key, value):
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-
raise KeyError(f"Key '{key}' not found in YAML!")
32-
obj = obj[k]
33-
if keys[-1] in obj and obj[keys[-1]] == value:
34-
return False # nothing to update
35-
if keys[-1] not in obj:
36-
raise KeyError(f"Key '{key}' not found in YAML!")
37-
obj[keys[-1]] = value
31+
keys, item = key.split("."), content
32+
leaf_idx = len(keys) - 1
33+
current_key_segments = []
34+
current_key = ""
35+
for i, k in enumerate(keys):
36+
current_key_segments.append(k)
37+
current_key = ".".join(current_key_segments)
38+
is_array = INDEX_PATTERN.match(k)
39+
if is_array:
40+
k = int(is_array.group(1))
41+
if not isinstance(item, list) or k >= len(item):
42+
raise KeyError(f"Key '{current_key}' not found in YAML!")
43+
else:
44+
if not isinstance(item, dict) or k not in item:
45+
raise KeyError(f"Key '{current_key}' not found in YAML!")
46+
if i == leaf_idx:
47+
if item[k] == value:
48+
return False # nothing to update
49+
item[k] = value
50+
break
51+
item = item[k]
3852

3953
with open(file_path, "w+") as stream:
4054
yaml.dump(content, stream)

tests/yaml/test_yaml_util.py

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,31 +47,72 @@ 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
68+
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
71+
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
6274

6375
expected = """\
64-
a: # comment
65-
# comment
76+
a: # comment 1
77+
# comment 2
6678
b:
67-
d: 1 # comment
68-
c: '2' # comment
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
6986
"""
7087
actual = self._read_file(test_file)
7188
self.assertEqual(expected, actual)
7289

73-
with pytest.raises(KeyError):
74-
updated = update_yaml_file(test_file, "a.x", "foo")
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))
93+
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))
97+
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))
113+
114+
actual = self._read_file(test_file)
115+
self.assertEqual(expected, actual)
75116

76117
def test_merge_yaml_element(self):
77118
test_file = self._create_file(

0 commit comments

Comments
 (0)