Skip to content

Commit e7ed7a4

Browse files
authored
fix: get-kubeconfig merge into empty lists (#648)
1 parent 601dc54 commit e7ed7a4

File tree

2 files changed

+75
-20
lines changed

2 files changed

+75
-20
lines changed

linodecli/plugins/get-kubeconfig.py

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def call(args, context):
9090
else cluster_config
9191
)
9292
if parsed.dry_run:
93-
print(cluster_config)
93+
print(yaml.dump(cluster_config))
9494
else:
9595
_dump_config(kubeconfig_path, cluster_config)
9696

@@ -146,27 +146,35 @@ def _dump_config(filepath, data):
146146
yaml.dump(data, file_descriptor)
147147

148148

149-
# Merges the lists in the provided dicts. If non-list properties of the two
150-
# dicts differ, uses the value from the first dict.
151149
def _merge_dict(dict_1, dict_2):
150+
"""
151+
Merges two dicts:
152+
* Lists that are present in both dicts are merged together by their "name" key
153+
(preferring duplicate values in the first dict)
154+
* `None` or missing keys in the first dict are set to the second dict's value
155+
* Other values are preferred from the first dict
156+
"""
152157
# Return a new dict to prevent any accidental mutations
153158
result = {}
154159

155-
for key in dict_1:
156-
if not isinstance(dict_1[key], list):
157-
result[key] = dict_1[key]
158-
continue
159-
160-
merge_map = {sub["name"]: sub for sub in dict_1[key]}
161-
162-
for sub in dict_2[key]:
163-
# If the name is already in the merge map, skip
164-
if sub["name"] in merge_map:
165-
continue
166-
167-
merge_map[sub["name"]] = sub
168-
169-
# Convert back to a list
170-
result[key] = list(merge_map.values())
160+
for key, dict_1_value in dict_1.items():
161+
if dict_1_value is None and (dict_2_value := dict_2.get(key)):
162+
# Replace null value in previous config
163+
result[key] = dict_2_value
164+
elif isinstance(dict_1_value, list) and (
165+
dict_2_value := dict_2.get(key)
166+
):
167+
merge_map = {sub["name"]: sub for sub in dict_1_value}
168+
for list_2_item in dict_2_value:
169+
if (list_2_name := list_2_item["name"]) not in merge_map:
170+
merge_map[list_2_name] = list_2_item
171+
# Convert back to a list
172+
result[key] = list(merge_map.values())
173+
else:
174+
result[key] = dict_1_value
175+
176+
# Process keys missing in dict_1
177+
for key in set(dict_2.keys()).difference(dict_1.keys()):
178+
result[key] = dict_2[key]
171179

172180
return result

tests/unit/test_plugin_kubeconfig.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
- item:
4343
property-1: a
4444
property-2: b
45-
property-3: c
4645
name: item-1
4746
- item:
4847
property-1: d
@@ -51,6 +50,12 @@
5150
dictionary: {"foo": "bar"}
5251
"""
5352

53+
TEST_YAML_EMPTY_CONFIG = """
54+
name: testing-kubeconfig
55+
things: null
56+
items: null
57+
"""
58+
5459

5560
# Test the output of --help
5661
def test_print_help():
@@ -199,6 +204,37 @@ def test_merge(mock_cli, fake_kubeconfig_file):
199204
assert result["dictionary"] == yaml_a["dictionary"]
200205

201206

207+
def test_merge_to_empty_config(mock_cli, fake_kubeconfig_file_without_entries):
208+
stdout_buf = io.StringIO()
209+
mock_cli.call_operation = mock_call_operation
210+
211+
file_path = fake_kubeconfig_file_without_entries
212+
213+
try:
214+
with contextlib.redirect_stdout(stdout_buf):
215+
plugin.call(
216+
[
217+
"--label",
218+
"nonempty_data",
219+
"--kubeconfig",
220+
file_path,
221+
"--dry-run",
222+
],
223+
PluginContext("REALTOKEN", mock_cli),
224+
)
225+
except SystemExit as err:
226+
assert err.code == 0
227+
228+
result = yaml.safe_load(stdout_buf.getvalue())
229+
yaml_a = yaml.safe_load(TEST_YAML_EMPTY_CONFIG)
230+
yaml_b = yaml.safe_load(TEST_YAML_CONTENT_B)
231+
232+
assert result["name"] == yaml_a["name"]
233+
assert result["things"] is None
234+
assert result["items"] == yaml_b["items"]
235+
assert result["dictionary"] == yaml_b["dictionary"]
236+
237+
202238
@pytest.fixture(scope="session", autouse=True)
203239
def fake_kubeconfig_file():
204240
with tempfile.NamedTemporaryFile(delete=False) as fp:
@@ -220,6 +256,17 @@ def fake_empty_file():
220256
os.remove(file_path)
221257

222258

259+
@pytest.fixture(scope="session", autouse=True)
260+
def fake_kubeconfig_file_without_entries():
261+
with tempfile.NamedTemporaryFile("wt", delete=False) as fp:
262+
fp.write(TEST_YAML_EMPTY_CONFIG)
263+
file_path = fp.name
264+
265+
yield file_path
266+
267+
os.remove(file_path)
268+
269+
223270
def mock_call_operation(command, action, **kwargs):
224271
if (
225272
command == "lke"

0 commit comments

Comments
 (0)