Skip to content

Commit 37f5a17

Browse files
Merge pull request #889 from linode/dev
Release v5.68.0
2 parents aedc371 + f61690e commit 37f5a17

13 files changed

Lines changed: 259 additions & 32 deletions

File tree

.github/workflows/e2e-suite.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ jobs:
302302
steps:
303303
- name: Notify Slack
304304
id: main_message
305-
uses: slackapi/slack-github-action@v2
305+
uses: slackapi/slack-github-action@v3
306306
with:
307307
method: chat.postMessage
308308
token: ${{ secrets.SLACK_BOT_TOKEN }}
@@ -334,7 +334,7 @@ jobs:
334334
335335
- name: Test summary thread
336336
if: success()
337-
uses: slackapi/slack-github-action@v2
337+
uses: slackapi/slack-github-action@v3
338338
with:
339339
method: chat.postMessage
340340
token: ${{ secrets.SLACK_BOT_TOKEN }}

.github/workflows/labeler.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
uses: actions/checkout@v6
2222
-
2323
name: Run Labeler
24-
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916
24+
uses: crazy-max/ghaction-github-labeler@548a7c3603594ec17c819e1239f281a3b801ab4d
2525
with:
2626
github-token: ${{ secrets.GITHUB_TOKEN }}
2727
yaml-file: .github/labels.yml

.github/workflows/nightly-smoke-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646

4747
- name: Notify Slack
4848
if: always() && github.repository == 'linode/linode-cli' # Run even if integration tests fail and only on main repository
49-
uses: slackapi/slack-github-action@v2
49+
uses: slackapi/slack-github-action@v3
5050
with:
5151
method: chat.postMessage
5252
token: ${{ secrets.SLACK_BOT_TOKEN }}

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
steps:
1212
- name: Notify Slack - Main Message
1313
id: main_message
14-
uses: slackapi/slack-github-action@v2
14+
uses: slackapi/slack-github-action@v3
1515
with:
1616
method: chat.postMessage
1717
token: ${{ secrets.SLACK_BOT_TOKEN }}
@@ -67,7 +67,7 @@ jobs:
6767
result-encoding: string
6868

6969
- name: Build and push to DockerHub
70-
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # pin@v7.0.0
70+
uses: docker/build-push-action@v7
7171
with:
7272
context: .
7373
file: Dockerfile

linodecli/cli.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,8 +305,56 @@ def _load_openapi_spec(spec_location: str) -> OpenAPI:
305305
with CLI._get_spec_file_reader(spec_location) as f:
306306
parsed = CLI._parse_spec_file(f)
307307

308+
CLI._normalize_content_parameters(parsed)
309+
308310
return OpenAPI(parsed)
309311

312+
@staticmethod
313+
def _normalize_content_parameters(parsed: Dict[str, Any]):
314+
"""
315+
The openapi3 library does not support the OpenAPI 3.0 ``content``
316+
form for Parameter objects. This method converts any such
317+
parameters (in components and inline on paths/operations) to use
318+
a top-level ``schema`` field so they can be parsed normally.
319+
320+
:param parsed: The raw spec dict to mutate in-place.
321+
"""
322+
323+
def _fix_param(param):
324+
if not isinstance(param, dict):
325+
return
326+
if "content" in param and "schema" not in param:
327+
content = param.pop("content")
328+
for media_obj in content.values():
329+
if isinstance(media_obj, dict) and "schema" in media_obj:
330+
param["schema"] = media_obj["schema"]
331+
break
332+
333+
for param in (
334+
parsed.get("components", {}).get("parameters", {}).values()
335+
):
336+
_fix_param(param)
337+
338+
for path_item in parsed.get("paths", {}).values():
339+
if not isinstance(path_item, dict):
340+
continue
341+
for p in path_item.get("parameters", []):
342+
_fix_param(p)
343+
for method in (
344+
"get",
345+
"put",
346+
"post",
347+
"delete",
348+
"options",
349+
"head",
350+
"patch",
351+
"trace",
352+
):
353+
operation = path_item.get(method)
354+
if isinstance(operation, dict):
355+
for p in operation.get("parameters", []):
356+
_fix_param(p)
357+
310358
@staticmethod
311359
@contextlib.contextmanager
312360
def _get_spec_file_reader(

tests/integration/linodes/fixtures.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,3 +662,41 @@ def linode_with_label(linode_cloud_firewall):
662662
res_arr = result.split(",")
663663
linode_id = res_arr[4]
664664
delete_target_id(target="linodes", id=linode_id)
665+
666+
667+
@pytest.fixture(scope="module")
668+
def linode_with_authorization_key(linode_cloud_firewall):
669+
label = "cli" + get_random_text(5)
670+
test_region = get_random_region_with_caps(
671+
required_capabilities=["Linodes", "Disk Encryption"]
672+
)
673+
result = exec_test_command(
674+
BASE_CMDS["linodes"]
675+
+ [
676+
"create",
677+
"--type",
678+
"g6-nanode-1",
679+
"--region",
680+
test_region,
681+
"--image",
682+
DEFAULT_TEST_IMAGE,
683+
"--label",
684+
label,
685+
"--authorized_keys",
686+
"ssh-rsa",
687+
"--kernel",
688+
"linode/latest-64bit",
689+
"--boot_size",
690+
"9000",
691+
"--text",
692+
"--delimiter",
693+
",",
694+
"--no-headers",
695+
"--no-defaults",
696+
"--format",
697+
"id,type",
698+
]
699+
).split(",")
700+
701+
yield result
702+
delete_target_id(target="linodes", id=result[0])

tests/integration/linodes/helpers.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,33 @@ def get_disk_id(test_linode_instance):
304304
).splitlines()
305305
first_id = disk_id[0].split(",")[0]
306306
return first_id
307+
308+
309+
def wait_for_disk_status(
310+
linode_id: "str", disk_id: "str", timeout, status: "str", period=10
311+
):
312+
must_end = time.time() + timeout
313+
while time.time() < must_end:
314+
time.sleep(period)
315+
try:
316+
result = exec_test_command(
317+
[
318+
"linode-cli",
319+
"linodes",
320+
"disk-view",
321+
linode_id,
322+
disk_id,
323+
"--format",
324+
"status",
325+
"--text",
326+
"--no-headers",
327+
]
328+
)
329+
except RuntimeError as response_error:
330+
if "Not found" in str(response_error):
331+
continue
332+
else:
333+
raise RuntimeError(response_error)
334+
if status == result:
335+
return True
336+
return False

tests/integration/linodes/test_linodes.py

Lines changed: 108 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
exec_failing_test_command,
1212
exec_test_command,
1313
get_random_region_with_caps,
14+
get_random_text,
15+
retry_exec_test_command_with_delay,
1416
)
1517
from tests.integration.linodes.fixtures import ( # noqa: F401
1618
linode_min_req,
19+
linode_with_authorization_key,
1720
linode_with_label,
1821
linode_wo_image,
1922
test_linode_instance,
@@ -23,6 +26,7 @@
2326
DEFAULT_TEST_IMAGE,
2427
create_linode,
2528
get_disk_id,
29+
wait_for_disk_status,
2630
wait_until,
2731
)
2832

@@ -42,6 +46,110 @@ def test_create_linodes_with_a_label(linode_with_label):
4246
)
4347

4448

49+
def test_expected_error_if_fields_authorized_users_authorized_keys_root_pass_are_not_set():
50+
test_region = get_random_region_with_caps(
51+
required_capabilities=["Linodes", "Disk Encryption"]
52+
)
53+
result = exec_failing_test_command(
54+
BASE_CMDS["linodes"]
55+
+ [
56+
"create",
57+
"--type",
58+
"g6-nanode-1",
59+
"--region",
60+
test_region,
61+
"--image",
62+
DEFAULT_TEST_IMAGE,
63+
"--label",
64+
"cli-negative-test-case",
65+
"--kernel",
66+
"linode/latest-64bit",
67+
"--boot_size",
68+
"9000",
69+
"--text",
70+
"--delimiter",
71+
",",
72+
"--no-headers",
73+
"--format",
74+
"label,region,type,image,id",
75+
"--no-defaults",
76+
],
77+
expected_code=ExitCodes.REQUEST_FAILED,
78+
)
79+
assert "Request failed: 400" in result
80+
assert (
81+
"Must provide valid root_pass, authorized_keys, or authorized_users"
82+
in result
83+
)
84+
85+
86+
def test_create_linode_with_kernel_and_boot_size_then_add_disk_and_rebuild(
87+
linode_with_authorization_key,
88+
):
89+
result_create = linode_with_authorization_key
90+
assert result_create[1] == "g6-nanode-1"
91+
assert wait_until(
92+
linode_id=result_create[0], timeout=180, status="running"
93+
), "linode failed to change status to running from creating.."
94+
95+
response_create_disk = (
96+
retry_exec_test_command_with_delay(
97+
BASE_CMDS["linodes"]
98+
+ [
99+
"disk-create",
100+
result_create[0],
101+
"--size",
102+
"2000",
103+
"--label",
104+
"cli" + get_random_text(5),
105+
"--image",
106+
"linode/debian12",
107+
"--root_pass",
108+
"aComplex@Password123",
109+
"--text",
110+
"--no-headers",
111+
"--delimiter",
112+
",",
113+
],
114+
retries=3,
115+
delay=10,
116+
)
117+
.splitlines()[0]
118+
.split(",")
119+
)
120+
assert "not ready" in response_create_disk
121+
assert wait_for_disk_status(
122+
linode_id=result_create[0],
123+
disk_id=response_create_disk[0],
124+
timeout=90,
125+
status="ready",
126+
), "linode failed to change disk status to ready after disk creation.."
127+
128+
result_rebuild = (
129+
exec_test_command(
130+
BASE_CMDS["linodes"]
131+
+ [
132+
"rebuild",
133+
"--image",
134+
DEFAULT_TEST_IMAGE,
135+
"--authorized_keys",
136+
"ssh-rsa-sha2-512",
137+
"--text",
138+
"--no-headers",
139+
"--delimiter",
140+
",",
141+
result_create[0],
142+
]
143+
)
144+
.splitlines()[0]
145+
.split(",")
146+
)
147+
assert DEFAULT_TEST_IMAGE in result_rebuild
148+
assert wait_until(
149+
linode_id=result_create[0], timeout=180, status="running"
150+
), "linode failed to change status to running from rebuilding.."
151+
152+
45153
@pytest.mark.smoke
46154
def test_view_linode_configuration(test_linode_instance):
47155
linode_id = test_linode_instance
@@ -75,26 +183,6 @@ def test_create_linode_with_min_required_props(linode_min_req):
75183
assert re.search("[0-9]+,us-ord,g6-nanode-1", result)
76184

77185

78-
def test_create_linodes_fails_without_a_root_pass():
79-
result = exec_failing_test_command(
80-
BASE_CMDS["linodes"]
81-
+ [
82-
"create",
83-
"--type",
84-
"g6-nanode-1",
85-
"--region",
86-
"us-ord",
87-
"--image",
88-
DEFAULT_TEST_IMAGE,
89-
"--text",
90-
"--no-headers",
91-
],
92-
ExitCodes.REQUEST_FAILED,
93-
)
94-
assert "Request failed: 400" in result
95-
assert "root_pass root_pass is required" in result
96-
97-
98186
def test_create_linode_without_image_and_not_boot(linode_wo_image):
99187
linode_id = linode_wo_image
100188

tests/integration/lke/helpers.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,22 @@ def get_lke_enterprise_id():
7676
return enterprise_ti.get("id")
7777

7878

79+
def get_lke_standard_id():
80+
standard_versions_list = exec_test_command(
81+
BASE_CMDS["lke"]
82+
+ [
83+
"versions-list",
84+
"--json",
85+
]
86+
)
87+
88+
parsed = json.loads(standard_versions_list)
89+
90+
standard_ti = parsed[0]
91+
92+
return standard_ti.get("id")
93+
94+
7995
def get_cluster_id(label: str):
8096
cluster_id = exec_test_command(
8197
[

tests/integration/lke/test_lke_enterprise.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
get_random_region_with_caps,
1212
get_random_text,
1313
)
14-
from tests.integration.lke.helpers import get_cluster_id, get_lke_enterprise_id
14+
from tests.integration.lke.helpers import (
15+
get_cluster_id,
16+
get_lke_enterprise_id,
17+
get_lke_standard_id,
18+
)
1519

1620

1721
def test_enterprise_tier_available_in_types(monkeypatch: MonkeyPatch):
@@ -116,6 +120,7 @@ def test_lke_tiered_versions_list():
116120

117121
def test_lke_tiered_versions_view():
118122
enterprise_id = get_lke_enterprise_id()
123+
standard_id = get_lke_standard_id()
119124
enterprise_tier_info = exec_test_command(
120125
BASE_CMDS["lke"]
121126
+ [
@@ -138,7 +143,7 @@ def test_lke_tiered_versions_view():
138143
+ [
139144
"tiered-version-view",
140145
"standard",
141-
"1.33",
146+
standard_id,
142147
"--json",
143148
]
144149
)

0 commit comments

Comments
 (0)