Skip to content

Commit c5f2d30

Browse files
authored
Merge pull request #770 from linode/dev
Release v5.57.1
2 parents 10d24ef + c04d864 commit c5f2d30

File tree

8 files changed

+116
-23
lines changed

8 files changed

+116
-23
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ jobs:
6767
result-encoding: string
6868

6969
- name: Build and push to DockerHub
70-
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # pin@v6.15.0
70+
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # pin@v6.16.0
7171
with:
7272
context: .
7373
file: Dockerfile

.github/workflows/remote-release-trigger.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jobs:
6666
commit_sha: ${{ steps.calculate_head_sha.outputs.commit_sha }}
6767

6868
- name: Release
69-
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # pin@v2.2.1
69+
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # pin@v2.2.2
7070
with:
7171
target_commitish: 'main'
7272
token: ${{ steps.generate_token.outputs.token }}

linodecli/api_request.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from linodecli.helpers import API_CA_PATH, API_VERSION_OVERRIDE
1919

2020
from .baked.operation import (
21+
ExplicitEmptyDictValue,
2122
ExplicitEmptyListValue,
2223
ExplicitNullValue,
2324
OpenAPIOperation,
@@ -303,15 +304,19 @@ def _traverse_request_body(o: Any) -> Any:
303304
if v is None:
304305
continue
305306

306-
# Values that are expected to be serialized as empty lists
307-
# and explicit None values are converted here.
307+
# Values that are expected to be serialized as empty
308+
# dicts, lists, and explicit None values are converted here.
308309
# See: operation.py
309310
# NOTE: These aren't handled at the top-level of this function
310311
# because we don't want them filtered out in the step below.
311312
if isinstance(v, ExplicitEmptyListValue):
312313
result[k] = []
313314
continue
314315

316+
if isinstance(v, ExplicitEmptyDictValue):
317+
result[k] = {}
318+
continue
319+
315320
if isinstance(v, ExplicitNullValue):
316321
result[k] = None
317322
continue

linodecli/baked/operation.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from collections import defaultdict
1313
from getpass import getpass
1414
from os import environ, path
15-
from typing import Any, Dict, List, Optional, Tuple
15+
from typing import Any, Dict, List, Optional, Tuple, Union
1616
from urllib.parse import urlparse
1717

1818
import openapi3.paths
@@ -48,7 +48,9 @@ def parse_boolean(value: str) -> bool:
4848
raise argparse.ArgumentTypeError("Expected a boolean value")
4949

5050

51-
def parse_dict(value: str) -> dict:
51+
def parse_dict(
52+
value: str,
53+
) -> Union[Dict[str, Any], "ExplicitEmptyDictValue", "ExplicitEmptyListValue"]:
5254
"""
5355
A helper function to decode incoming JSON data as python dicts. This is
5456
intended to be passed to the `type=` kwarg for ArgumentParaser.add_argument.
@@ -57,15 +59,28 @@ def parse_dict(value: str) -> dict:
5759
:type value: str
5860
5961
:returns: The dict value of the input.
60-
:rtype: dict
62+
:rtype: dict, ExplicitEmptyDictValue, or ExplicitEmptyListValue
6163
"""
6264
if not isinstance(value, str):
6365
raise argparse.ArgumentTypeError("Expected a JSON string")
66+
6467
try:
65-
return json.loads(value)
68+
result = json.loads(value)
6669
except Exception as e:
6770
raise argparse.ArgumentTypeError("Expected a JSON string") from e
6871

72+
# This is necessary because empty dicts and lists are excluded from requests
73+
# by default, but we still want to support user-defined empty dict
74+
# strings. This is particularly helpful when updating LKE node pool
75+
# labels and taints.
76+
if isinstance(result, dict) and result == {}:
77+
return ExplicitEmptyDictValue()
78+
79+
if isinstance(result, list) and result == []:
80+
return ExplicitEmptyListValue()
81+
82+
return result
83+
6984

7085
TYPES = {
7186
"string": str,
@@ -90,6 +105,12 @@ class ExplicitEmptyListValue:
90105
"""
91106

92107

108+
class ExplicitEmptyDictValue:
109+
"""
110+
A special type used to explicitly pass empty dictionaries to the API.
111+
"""
112+
113+
93114
def wrap_parse_nullable_value(arg_type: str) -> TYPES:
94115
"""
95116
A helper function to parse `null` as None for nullable CLI args.

tests/integration/lke/test_clusters.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ def test_update_node_pool(test_lke_cluster):
330330
node_pool_id = get_node_pool_id(cluster_id)
331331
new_value = get_random_text(8) + "updated_pool"
332332

333-
result = (
333+
result = json.loads(
334334
exec_test_command(
335335
BASE_CMD
336336
+ [
@@ -341,16 +341,42 @@ def test_update_node_pool(test_lke_cluster):
341341
"5",
342342
"--labels",
343343
json.dumps({"label-key": new_value}),
344-
"--text",
345-
"--no-headers",
346-
"--format=label",
344+
"--taints",
345+
'[{"key": "test-key", "value": "test-value", "effect": "NoSchedule"}]',
346+
"--json",
347347
]
348-
)
349-
.stdout.decode()
350-
.rstrip()
348+
).stdout.decode()
349+
)
350+
351+
assert result[0]["labels"] == {"label-key": new_value}
352+
353+
assert result[0]["taints"] == [
354+
{
355+
"key": "test-key",
356+
"value": "test-value",
357+
"effect": "NoSchedule",
358+
}
359+
]
360+
361+
# Reset the values for labels and taints (TPT-3665)
362+
result = json.loads(
363+
exec_test_command(
364+
BASE_CMD
365+
+ [
366+
"pool-update",
367+
cluster_id,
368+
node_pool_id,
369+
"--labels",
370+
"{}",
371+
"--taints",
372+
"[]",
373+
"--json",
374+
]
375+
).stdout.decode()
351376
)
352377

353-
assert new_value in result
378+
assert result[0]["labels"] == {}
379+
assert result[0]["taints"] == []
354380

355381

356382
def test_view_node(test_lke_cluster):
@@ -407,7 +433,7 @@ def test_list_lke_types():
407433
assert "LKE High Availability" in types
408434

409435

410-
def test_create_node_pool_default_to_disk_encryption_disabled(test_lke_cluster):
436+
def test_create_node_pool_has_disk_encryption_field_set(test_lke_cluster):
411437
cluster_id = test_lke_cluster
412438

413439
result = (
@@ -437,7 +463,7 @@ def test_create_node_pool_default_to_disk_encryption_disabled(test_lke_cluster):
437463

438464
disk_encryption_status = pool_info.get("disk_encryption")
439465

440-
assert "disabled" in result
466+
assert disk_encryption_status in ("enabled", "disabled")
441467
assert "g6-standard-4" in result
442468

443469

tests/integration/lke/test_lke_enterprise.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import json
2+
import re
23

3-
import pytest
44
from pytest import MonkeyPatch
55

66
from tests.integration.helpers import (
@@ -87,7 +87,6 @@ def test_create_lke_enterprise(monkeypatch: MonkeyPatch):
8787
)
8888

8989

90-
@pytest.mark.skip(reason="Commnad not available in spec < v4.198.0")
9190
def test_lke_tiered_versions_list():
9291
enterprise_tier_info_list = (
9392
exec_test_command(
@@ -106,7 +105,7 @@ def test_lke_tiered_versions_list():
106105

107106
enterprise_ti = parsed[0]
108107

109-
assert enterprise_ti.get("id") == "v1.31.1+lke4"
108+
assert re.match(r"^v\d+\.\d+\.\d+\+lke\d+$", enterprise_ti.get("id"))
110109
assert enterprise_ti.get("tier") == "enterprise"
111110

112111
standard_tier_info_list = (
@@ -130,7 +129,6 @@ def test_lke_tiered_versions_list():
130129
assert s_ti_list[1].get("tier") == "standard"
131130

132131

133-
@pytest.mark.skip(reason="Commnad not available in spec < v4.198.0")
134132
def test_lke_tiered_versions_view():
135133
enterprise_tier_info = (
136134
exec_test_command(

tests/unit/test_api_request.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
import requests
1313

1414
from linodecli import api_request
15-
from linodecli.baked.operation import ExplicitEmptyListValue, ExplicitNullValue
15+
from linodecli.baked.operation import (
16+
ExplicitEmptyDictValue,
17+
ExplicitEmptyListValue,
18+
ExplicitNullValue,
19+
)
1620

1721

1822
class TestAPIRequest:
@@ -597,6 +601,7 @@ def test_traverse_request_body(self):
597601
"baz": ExplicitNullValue(),
598602
},
599603
"cool": [],
604+
"pretty_cool": ExplicitEmptyDictValue(),
600605
"cooler": ExplicitEmptyListValue(),
601606
"coolest": ExplicitNullValue(),
602607
}
@@ -614,6 +619,7 @@ def test_traverse_request_body(self):
614619
"foo": "bar",
615620
"baz": None,
616621
},
622+
"pretty_cool": {},
617623
"cooler": [],
618624
"coolest": None,
619625
}

tests/unit/test_operation.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from linodecli.baked import operation
77
from linodecli.baked.operation import (
8+
TYPES,
9+
ExplicitEmptyDictValue,
810
ExplicitEmptyListValue,
911
ExplicitNullValue,
1012
OpenAPIOperation,
@@ -277,6 +279,41 @@ def test_array_arg_action_basic(self):
277279
result = parser.parse_args(["--foo", "foo", "--foo", "[]"])
278280
assert getattr(result, "foo") == ["foo", "[]"]
279281

282+
def test_object_arg_action_basic(self):
283+
"""
284+
Tests a basic array argument condition..
285+
"""
286+
287+
parser = argparse.ArgumentParser(
288+
prog=f"foo",
289+
)
290+
291+
parser.add_argument(
292+
"--foo",
293+
metavar="foo",
294+
type=TYPES["object"],
295+
)
296+
297+
# User specifies a normal object (dict)
298+
result = parser.parse_args(["--foo", '{"test-key": "test-value"}'])
299+
assert getattr(result, "foo") == {"test-key": "test-value"}
300+
301+
# User specifies a normal object (list)
302+
result = parser.parse_args(["--foo", '[{"test-key": "test-value"}]'])
303+
assert getattr(result, "foo") == [{"test-key": "test-value"}]
304+
305+
# User wants an explicitly empty object (dict)
306+
result = parser.parse_args(["--foo", "{}"])
307+
assert isinstance(getattr(result, "foo"), ExplicitEmptyDictValue)
308+
309+
# User wants an explicitly empty object (list)
310+
result = parser.parse_args(["--foo", "[]"])
311+
assert isinstance(getattr(result, "foo"), ExplicitEmptyListValue)
312+
313+
# User doesn't specify the list
314+
result = parser.parse_args([])
315+
assert getattr(result, "foo") is None
316+
280317
def test_resolve_api_components(self, get_openapi_for_api_components_tests):
281318
root = get_openapi_for_api_components_tests
282319

0 commit comments

Comments
 (0)