Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/e2e-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ jobs:
steps:
- name: Notify Slack
id: main_message
uses: slackapi/slack-github-action@v2
uses: slackapi/slack-github-action@v3
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
Expand Down Expand Up @@ -334,7 +334,7 @@ jobs:

- name: Test summary thread
if: success()
uses: slackapi/slack-github-action@v2
uses: slackapi/slack-github-action@v3
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
uses: actions/checkout@v6
-
name: Run Labeler
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916
uses: crazy-max/ghaction-github-labeler@548a7c3603594ec17c819e1239f281a3b801ab4d
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
yaml-file: .github/labels.yml
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/nightly-smoke-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:

- name: Notify Slack
if: always() && github.repository == 'linode/linode-cli' # Run even if integration tests fail and only on main repository
uses: slackapi/slack-github-action@v2
uses: slackapi/slack-github-action@v3
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
steps:
- name: Notify Slack - Main Message
id: main_message
uses: slackapi/slack-github-action@v2
uses: slackapi/slack-github-action@v3
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
Expand Down Expand Up @@ -67,7 +67,7 @@ jobs:
result-encoding: string

- name: Build and push to DockerHub
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # pin@v7.0.0
uses: docker/build-push-action@v7
with:
context: .
file: Dockerfile
Expand Down
48 changes: 48 additions & 0 deletions linodecli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,56 @@ def _load_openapi_spec(spec_location: str) -> OpenAPI:
with CLI._get_spec_file_reader(spec_location) as f:
parsed = CLI._parse_spec_file(f)

CLI._normalize_content_parameters(parsed)

return OpenAPI(parsed)

@staticmethod
def _normalize_content_parameters(parsed: Dict[str, Any]):
"""
The openapi3 library does not support the OpenAPI 3.0 ``content``
form for Parameter objects. This method converts any such
parameters (in components and inline on paths/operations) to use
a top-level ``schema`` field so they can be parsed normally.

:param parsed: The raw spec dict to mutate in-place.
"""

def _fix_param(param):
if not isinstance(param, dict):
return
if "content" in param and "schema" not in param:
content = param.pop("content")
for media_obj in content.values():
if isinstance(media_obj, dict) and "schema" in media_obj:
param["schema"] = media_obj["schema"]
break

for param in (
parsed.get("components", {}).get("parameters", {}).values()
):
_fix_param(param)

for path_item in parsed.get("paths", {}).values():
if not isinstance(path_item, dict):
continue
for p in path_item.get("parameters", []):
_fix_param(p)
for method in (
"get",
"put",
"post",
"delete",
"options",
"head",
"patch",
"trace",
):
operation = path_item.get(method)
if isinstance(operation, dict):
for p in operation.get("parameters", []):
_fix_param(p)

@staticmethod
@contextlib.contextmanager
def _get_spec_file_reader(
Expand Down
38 changes: 38 additions & 0 deletions tests/integration/linodes/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,3 +662,41 @@ def linode_with_label(linode_cloud_firewall):
res_arr = result.split(",")
linode_id = res_arr[4]
delete_target_id(target="linodes", id=linode_id)


@pytest.fixture(scope="module")
def linode_with_authorization_key(linode_cloud_firewall):
label = "cli" + get_random_text(5)
test_region = get_random_region_with_caps(
required_capabilities=["Linodes", "Disk Encryption"]
)
result = exec_test_command(
BASE_CMDS["linodes"]
+ [
"create",
"--type",
"g6-nanode-1",
"--region",
test_region,
"--image",
DEFAULT_TEST_IMAGE,
"--label",
label,
"--authorized_keys",
"ssh-rsa",
"--kernel",
"linode/latest-64bit",
"--boot_size",
"9000",
"--text",
"--delimiter",
",",
"--no-headers",
"--no-defaults",
"--format",
"id,type",
]
).split(",")

yield result
delete_target_id(target="linodes", id=result[0])
30 changes: 30 additions & 0 deletions tests/integration/linodes/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,33 @@ def get_disk_id(test_linode_instance):
).splitlines()
first_id = disk_id[0].split(",")[0]
return first_id


def wait_for_disk_status(
linode_id: "str", disk_id: "str", timeout, status: "str", period=10
):
must_end = time.time() + timeout
while time.time() < must_end:
time.sleep(period)
try:
result = exec_test_command(
[
"linode-cli",
"linodes",
"disk-view",
linode_id,
disk_id,
"--format",
"status",
"--text",
"--no-headers",
]
)
except RuntimeError as response_error:
if "Not found" in str(response_error):
continue
else:
raise RuntimeError(response_error)
if status == result:
return True
return False
128 changes: 108 additions & 20 deletions tests/integration/linodes/test_linodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
exec_failing_test_command,
exec_test_command,
get_random_region_with_caps,
get_random_text,
retry_exec_test_command_with_delay,
)
from tests.integration.linodes.fixtures import ( # noqa: F401
linode_min_req,
linode_with_authorization_key,
linode_with_label,
linode_wo_image,
test_linode_instance,
Expand All @@ -23,6 +26,7 @@
DEFAULT_TEST_IMAGE,
create_linode,
get_disk_id,
wait_for_disk_status,
wait_until,
)

Expand All @@ -42,6 +46,110 @@ def test_create_linodes_with_a_label(linode_with_label):
)


def test_expected_error_if_fields_authorized_users_authorized_keys_root_pass_are_not_set():
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 tests require API SPEC release. Could you let them know about that?

test_region = get_random_region_with_caps(
required_capabilities=["Linodes", "Disk Encryption"]
)
result = exec_failing_test_command(
BASE_CMDS["linodes"]
+ [
"create",
"--type",
"g6-nanode-1",
"--region",
test_region,
"--image",
DEFAULT_TEST_IMAGE,
"--label",
"cli-negative-test-case",
"--kernel",
"linode/latest-64bit",
"--boot_size",
"9000",
"--text",
"--delimiter",
",",
"--no-headers",
"--format",
"label,region,type,image,id",
"--no-defaults",
],
expected_code=ExitCodes.REQUEST_FAILED,
)
assert "Request failed: 400" in result
assert (
"Must provide valid root_pass, authorized_keys, or authorized_users"
in result
)


def test_create_linode_with_kernel_and_boot_size_then_add_disk_and_rebuild(
linode_with_authorization_key,
):
result_create = linode_with_authorization_key
assert result_create[1] == "g6-nanode-1"
assert wait_until(
linode_id=result_create[0], timeout=180, status="running"
), "linode failed to change status to running from creating.."

response_create_disk = (
retry_exec_test_command_with_delay(
BASE_CMDS["linodes"]
+ [
"disk-create",
result_create[0],
"--size",
"2000",
"--label",
"cli" + get_random_text(5),
"--image",
"linode/debian12",
"--root_pass",
"aComplex@Password123",
"--text",
"--no-headers",
"--delimiter",
",",
],
retries=3,
delay=10,
)
.splitlines()[0]
.split(",")
)
assert "not ready" in response_create_disk
assert wait_for_disk_status(
linode_id=result_create[0],
disk_id=response_create_disk[0],
timeout=90,
status="ready",
), "linode failed to change disk status to ready after disk creation.."

result_rebuild = (
exec_test_command(
BASE_CMDS["linodes"]
+ [
"rebuild",
"--image",
DEFAULT_TEST_IMAGE,
"--authorized_keys",
"ssh-rsa-sha2-512",
"--text",
"--no-headers",
"--delimiter",
",",
result_create[0],
]
)
.splitlines()[0]
.split(",")
)
assert DEFAULT_TEST_IMAGE in result_rebuild
assert wait_until(
linode_id=result_create[0], timeout=180, status="running"
), "linode failed to change status to running from rebuilding.."


@pytest.mark.smoke
def test_view_linode_configuration(test_linode_instance):
linode_id = test_linode_instance
Expand Down Expand Up @@ -75,26 +183,6 @@ def test_create_linode_with_min_required_props(linode_min_req):
assert re.search("[0-9]+,us-ord,g6-nanode-1", result)


def test_create_linodes_fails_without_a_root_pass():
result = exec_failing_test_command(
BASE_CMDS["linodes"]
+ [
"create",
"--type",
"g6-nanode-1",
"--region",
"us-ord",
"--image",
DEFAULT_TEST_IMAGE,
"--text",
"--no-headers",
],
ExitCodes.REQUEST_FAILED,
)
assert "Request failed: 400" in result
assert "root_pass root_pass is required" in result


def test_create_linode_without_image_and_not_boot(linode_wo_image):
linode_id = linode_wo_image

Expand Down
2 changes: 1 addition & 1 deletion tests/integration/ssh/test_plugin_ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
)

TEST_REGION = get_random_region_with_caps(required_capabilities=["Linodes"])
TEST_IMAGE = "linode/ubuntu24.10"
TEST_IMAGE = "linode/arch"
TEST_TYPE = "g6-nanode-1"
TEST_ROOT_PASS = "r00tp@ss!long-long-and-longer"

Expand Down
Loading