From 08e0014ec703daa9f29b66f9d583d3dfdf1e83e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 23:55:04 -0500 Subject: [PATCH 01/13] build(deps): bump docker/setup-qemu-action from 3.6.0 to 3.7.0 (#839) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 77e26ca6..c44c757f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,7 +39,7 @@ jobs: run: make requirements - name: Set up QEMU - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # pin@v3.6.0 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # pin@v3.7.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # pin@v3.11.1 From e20564282e2f95b52cd4561176bff19c8394993f Mon Sep 17 00:00:00 2001 From: Fred Mora Date: Wed, 14 Jan 2026 14:33:54 -0500 Subject: [PATCH 02/13] Typo: Auth failure message (#846) --- linodecli/configuration/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linodecli/configuration/auth.py b/linodecli/configuration/auth.py index a7e23b39..e10d785c 100644 --- a/linodecli/configuration/auth.py +++ b/linodecli/configuration/auth.py @@ -348,7 +348,7 @@ def log_message(self, form, *args): # pylint: disable=arguments-differ except KeyboardInterrupt: print( "\nGiving up. If you couldn't get web authentication to work, please " - "try token using a token by invoking with `linode-cli configure --token`, " + "try using a token by invoking with `linode-cli configure --token`, " "and open an issue at https://github.com/linode/linode-cli", file=sys.stderr, ) From 54fa649edeaa4b24bf22018763677831ae103e1e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 09:50:58 -0500 Subject: [PATCH 03/13] build(deps): bump docker/setup-buildx-action from 3.11.1 to 3.12.0 (#841) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c44c757f..009472d7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,7 +42,7 @@ jobs: uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # pin@v3.7.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # pin@v3.11.1 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # pin@v3.12.0 - name: Login to Docker Hub uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # pin@v3.6.0 From d152f55a2045a75c33814333afcbbfde5ace5210 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 09:51:20 -0500 Subject: [PATCH 04/13] build(deps): bump softprops/action-gh-release from 2.4.1 to 2.5.0 (#842) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/remote-release-trigger.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/remote-release-trigger.yml b/.github/workflows/remote-release-trigger.yml index 2dfb75c2..75a5da59 100644 --- a/.github/workflows/remote-release-trigger.yml +++ b/.github/workflows/remote-release-trigger.yml @@ -66,7 +66,7 @@ jobs: commit_sha: ${{ steps.calculate_head_sha.outputs.commit_sha }} - name: Release - uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # pin@v2.4.1 + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # pin@v2.5.0 with: target_commitish: 'main' token: ${{ steps.generate_token.outputs.token }} From ec9fbaf40f27b2ea4e26635914daec2094a344e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 09:51:38 -0500 Subject: [PATCH 05/13] build(deps): bump actions/download-artifact from 6 to 7 (#843) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/e2e-suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-suite.yml b/.github/workflows/e2e-suite.yml index fa558179..1bea5ec0 100644 --- a/.github/workflows/e2e-suite.yml +++ b/.github/workflows/e2e-suite.yml @@ -243,7 +243,7 @@ jobs: submodules: 'recursive' - name: Download test report - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: test-report-file From 5c877524855d67117d15dd34e73016c75c6f8df6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 09:51:56 -0500 Subject: [PATCH 06/13] build(deps): bump actions/upload-artifact from 5 to 6 (#844) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/e2e-suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-suite.yml b/.github/workflows/e2e-suite.yml index 1bea5ec0..7cae500d 100644 --- a/.github/workflows/e2e-suite.yml +++ b/.github/workflows/e2e-suite.yml @@ -127,7 +127,7 @@ jobs: - name: Upload Test Report as Artifact if: always() - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: test-report-file if-no-files-found: ignore From 96a05a4d36ebf4ec7770bb3eb30a6e760f16e1fd Mon Sep 17 00:00:00 2001 From: Erik Zilber Date: Mon, 26 Jan 2026 15:14:16 -0500 Subject: [PATCH 07/13] Point to linode-api-openapi repo instead of linode-api-docs (#848) --- README.md | 2 +- linodecli/arg_helpers.py | 1 + linodecli/cli.py | 2 +- linodecli/completion.py | 7 +++---- linodecli/configuration/auth.py | 12 ++++-------- linodecli/configuration/helpers.py | 6 ++---- linodecli/plugins/obj/__init__.py | 1 + resolve_spec_url | 2 +- tests/unit/test_api_request.py | 1 + tests/unit/test_cli.py | 2 +- tests/unit/test_configuration.py | 1 + wiki/development/Development - Overview.md | 2 +- wiki/development/Development - Setup.md | 2 +- 13 files changed, 19 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 0db85368..ef5e786c 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Visit the [Wiki](../../wiki/Installation) for more information. ## Contributing -This CLI is generated from the [OpenAPI specification for Linode's API](https://github.com/linode/linode-api-docs). As +This CLI is generated from the [OpenAPI specification for Linode's API](https://github.com/linode/linode-api-openapi). As such, many changes are made directly to the spec. Please follow the [Contributing Guidelines](https://github.com/linode/linode-cli/blob/main/CONTRIBUTING.md) when making a contribution. diff --git a/linodecli/arg_helpers.py b/linodecli/arg_helpers.py index 2f5f1b70..2a6d7c9a 100644 --- a/linodecli/arg_helpers.py +++ b/linodecli/arg_helpers.py @@ -4,6 +4,7 @@ This module defines argument parsing, plugin registration, and plugin removal functionalities for the Linode CLI. """ + import sys from argparse import ArgumentParser from configparser import ConfigParser diff --git a/linodecli/cli.py b/linodecli/cli.py index 718b619a..419b5e06 100644 --- a/linodecli/cli.py +++ b/linodecli/cli.py @@ -286,7 +286,7 @@ def user_agent(self) -> str: """ return ( f"linode-cli/{self.version} " - f"linode-api-docs/{self.spec_version} " + f"linode-api-openapi/{self.spec_version} " f"python/{version_info[0]}.{version_info[1]}.{version_info[2]}" ) diff --git a/linodecli/completion.py b/linodecli/completion.py index 6a402eec..e95acd96 100644 --- a/linodecli/completion.py +++ b/linodecli/completion.py @@ -2,6 +2,7 @@ """ Contains any code relevant to generating/updating shell completions for linode-cli """ + from string import Template from openapi3 import OpenAPI @@ -93,12 +94,10 @@ def get_bash_completions(ops): complete -F _linode_cli lin""" ) - command_template = Template( - """$command) + command_template = Template("""$command) COMPREPLY=( $(compgen -W "$actions --help" -- ${cur}) ) return 0 - ;;""" - ) + ;;""") command_blocks = [ command_template.safe_substitute( diff --git a/linodecli/configuration/auth.py b/linodecli/configuration/auth.py index e10d785c..1b633c7b 100644 --- a/linodecli/configuration/auth.py +++ b/linodecli/configuration/auth.py @@ -214,13 +214,11 @@ def _get_token_terminal(base_url: str) -> Tuple[str, str]: :returns: A tuple containing the user's username and token. :rtype: Tuple[str, str] """ - print( - f""" + print(f""" First, we need a Personal Access Token. To get one, please visit {TOKEN_GENERATION_URL} and click "Create a Personal Access Token". The CLI needs access to everything -on your account to work correctly.""" - ) +on your account to work correctly.""") while True: token = input("Personal Access Token: ") @@ -329,15 +327,13 @@ def log_message(self, form, *args): # pylint: disable=arguments-differ # figure out the URL to direct the user to and print out the prompt # pylint: disable-next=line-too-long url = f"https://login.linode.com/oauth/authorize?client_id={OAUTH_CLIENT_ID}&response_type=token&scopes=*&redirect_uri=http://localhost:{serv.server_address[1]}" - print( - f"""A browser should open directing you to this URL to authenticate: + print(f"""A browser should open directing you to this URL to authenticate: {url} If you are not automatically directed there, please copy/paste the link into your browser to continue.. -""" - ) +""") webbrowser.open(url) diff --git a/linodecli/configuration/helpers.py b/linodecli/configuration/helpers.py index a0398e2b..35be0404 100644 --- a/linodecli/configuration/helpers.py +++ b/linodecli/configuration/helpers.py @@ -97,11 +97,9 @@ def _check_browsers() -> bool: # pylint: disable-next=protected-access if not KNOWN_GOOD_BROWSERS.intersection(webbrowser._tryorder): - print( - """ + print(""" This tool defaults to web-based authentication, -however no known-working browsers were found.""" - ) +however no known-working browsers were found.""") while True: r = input("Try it anyway? [y/N]: ") if r.lower() in "yn ": diff --git a/linodecli/plugins/obj/__init__.py b/linodecli/plugins/obj/__init__.py index 2d576134..6952487f 100644 --- a/linodecli/plugins/obj/__init__.py +++ b/linodecli/plugins/obj/__init__.py @@ -2,6 +2,7 @@ """ CLI Plugin for handling OBJ """ + import getpass import os import re diff --git a/resolve_spec_url b/resolve_spec_url index 60ae216f..7487e2ef 100755 --- a/resolve_spec_url +++ b/resolve_spec_url @@ -7,7 +7,7 @@ import sys import requests -LINODE_DOCS_REPO = "linode/linode-api-docs" +LINODE_DOCS_REPO = "linode/linode-api-openapi" def get_latest_tag(): diff --git a/tests/unit/test_api_request.py b/tests/unit/test_api_request.py index a222d60d..1afa699c 100644 --- a/tests/unit/test_api_request.py +++ b/tests/unit/test_api_request.py @@ -2,6 +2,7 @@ """ Unit tests for linodecli.api_request """ + import contextlib import io import json diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 3ce0f878..961a6212 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -79,7 +79,7 @@ def test_find_operation( def test_user_agent(self, mock_cli: CLI): assert re.compile( - r"linode-cli/[0-9]+\.[0-9]+\.[0-9]+ linode-api-docs/[0-9]+\.[0-9]+\.[0-9]+ python/[0-9]+\.[0-9]+\.[0-9]+" + r"linode-cli/[0-9]+\.[0-9]+\.[0-9]+ linode-api-openapi/[0-9]+\.[0-9]+\.[0-9]+ python/[0-9]+\.[0-9]+\.[0-9]+" ).match(mock_cli.user_agent) def test_load_openapi_spec_json(self): diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py index 14b704d0..40925033 100644 --- a/tests/unit/test_configuration.py +++ b/tests/unit/test_configuration.py @@ -2,6 +2,7 @@ """ Unit tests for linodecli.configuration """ + import argparse import contextlib import io diff --git a/wiki/development/Development - Overview.md b/wiki/development/Development - Overview.md index a354103f..555b668a 100644 --- a/wiki/development/Development - Overview.md +++ b/wiki/development/Development - Overview.md @@ -3,7 +3,7 @@ The following section outlines the core functions of the Linode CLI. ## OpenAPI Specification Parsing Most Linode CLI commands (excluding [plugin commands](https://github.com/linode/linode-cli/tree/dev/linodecli/plugins)) -are generated dynamically at build-time from the [Linode OpenAPI Specification](https://github.com/linode/linode-api-docs), +are generated dynamically at build-time from the [Linode OpenAPI Specification](https://github.com/linode/linode-api-openapi), which is also used to generate the [official Linode API documentation](https://www.linode.com/docs/api/). Each OpenAPI spec endpoint method is parsed into an `OpenAPIOperation` object. diff --git a/wiki/development/Development - Setup.md b/wiki/development/Development - Setup.md index bf667fcb..7293620c 100644 --- a/wiki/development/Development - Setup.md +++ b/wiki/development/Development - Setup.md @@ -73,7 +73,7 @@ This can be achieved using the `SPEC` Makefile argument, for example: ```bash # Download the OpenAPI spec -curl -o openapi.yaml https://raw.githubusercontent.com/linode/linode-api-docs/development/openapi.yaml +curl -o openapi.json https://raw.githubusercontent.com/linode/linode-api-openapi/main/openapi.json # Many arbitrary changes to the spec From f37b059cbdad029b64984211c8d0992a43612e2a Mon Sep 17 00:00:00 2001 From: Erik Zilber Date: Fri, 6 Feb 2026 08:56:12 -0500 Subject: [PATCH 08/13] Parse OBJ endpoint domain dynamically (#851) --- linodecli/plugins/obj/config.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/linodecli/plugins/obj/config.py b/linodecli/plugins/obj/config.py index f53806df..0e287f56 100644 --- a/linodecli/plugins/obj/config.py +++ b/linodecli/plugins/obj/config.py @@ -2,13 +2,24 @@ The config of the object storage plugin. """ +import os +import re import shutil ENV_ACCESS_KEY_NAME = "LINODE_CLI_OBJ_ACCESS_KEY" ENV_SECRET_KEY_NAME = "LINODE_CLI_OBJ_SECRET_KEY" -# replace {} with the cluster name -BASE_URL_TEMPLATE = "https://{}.linodeobjects.com" -BASE_WEBSITE_TEMPLATE = "{bucket}.website-{cluster}.linodeobjects.com" + +API_HOST = os.getenv("LINODE_CLI_API_HOST", "") + +OBJ_DOMAIN = "linodeobjects.com" + +if API_HOST: + match = re.match(r"api\.([^.]+)\.linode\.com", API_HOST) + if match: + OBJ_DOMAIN = f"{match.group(1)}.linodeobjects.com" + +BASE_URL_TEMPLATE = f"https://{{}}.{OBJ_DOMAIN}" +BASE_WEBSITE_TEMPLATE = f"{{bucket}}.website-{{cluster}}.{OBJ_DOMAIN}" # for all date output DATE_FORMAT = "%Y-%m-%d %H:%M" From e6e7efc14fcaf574b2dd2a8047b7325c7b9719dd Mon Sep 17 00:00:00 2001 From: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:43:21 -0500 Subject: [PATCH 09/13] Remove preview section from PR template (#850) --- .github/pull_request_template.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5bea77b2..d97f9345 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,7 +7,3 @@ **What are the steps to reproduce the issue or verify the changes?** **How do I run the relevant unit/integration tests?** - -## 📷 Preview - -**If applicable, include a screenshot or code snippet of this change. Otherwise, please remove this section.** \ No newline at end of file From e943e1fbb6bac31c72ef87df6d929254e5c2eab9 Mon Sep 17 00:00:00 2001 From: Maciej Wilk Date: Wed, 11 Feb 2026 14:17:33 +0100 Subject: [PATCH 10/13] Align GHA workflows in the scope of report uploads (#853) --- .github/workflows/e2e-suite.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-suite.yml b/.github/workflows/e2e-suite.yml index 7cae500d..bf2ff015 100644 --- a/.github/workflows/e2e-suite.yml +++ b/.github/workflows/e2e-suite.yml @@ -40,6 +40,14 @@ on: options: - 'true' - 'false' + test_report_upload: + description: 'Indicates whether to upload the test report to object storage. Defaults to "false"' + required: false + default: 'false' + type: choice + options: + - 'true' + - 'false' push: branches: @@ -231,7 +239,8 @@ jobs: process-upload-report: runs-on: ubuntu-latest needs: [integration_tests] - if: always() && github.repository == 'linode/linode-cli' # Run even if integration tests fail and only on main repository + # Run even if integration tests fail on main repository AND (push event OR (manually triggered and test_report_upload is true)) + if: always() && github.repository == 'linode/linode-cli' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.test_report_upload == 'true')) outputs: summary: ${{ steps.set-test-summary.outputs.summary }} @@ -258,7 +267,6 @@ jobs: - name: Set release version env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - - name: Add variables and upload test results if: always() run: | From fc305b5c25ad157d0988e24f51ab495578afc7cd Mon Sep 17 00:00:00 2001 From: Vinay <143587840+vshanthe@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:28:33 +0530 Subject: [PATCH 11/13] Add test for configure and fix flaky tests (#855) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/integration/account/test_account.py | 6 +++ tests/integration/lke/test_clusters.py | 2 +- tests/integration/ssh/test_ssh.py | 45 ++++++++++++++++------- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/tests/integration/account/test_account.py b/tests/integration/account/test_account.py index e3d99c3c..55c53513 100644 --- a/tests/integration/account/test_account.py +++ b/tests/integration/account/test_account.py @@ -374,3 +374,9 @@ def test_clients_list(): headers = ["label", "status"] assert_headers_in_lines(headers, lines) + + +def test_configure_command_smoke(): + result = exec_test_command(["linode-cli", "configure", "--help"]) + + assert "Configured the Linode CLI" in result diff --git a/tests/integration/lke/test_clusters.py b/tests/integration/lke/test_clusters.py index 4b11f5cb..f9fae8e7 100644 --- a/tests/integration/lke/test_clusters.py +++ b/tests/integration/lke/test_clusters.py @@ -257,7 +257,7 @@ def test_view_node(lke_cluster): ) lines = res.splitlines() - headers = ["id", "id,instance_id,status"] + headers = ["id", "instance_id", "pool_id", "status"] assert_headers_in_lines(headers, lines) diff --git a/tests/integration/ssh/test_ssh.py b/tests/integration/ssh/test_ssh.py index a1f1f8e9..1c944cd4 100644 --- a/tests/integration/ssh/test_ssh.py +++ b/tests/integration/ssh/test_ssh.py @@ -1,5 +1,6 @@ import os import re +import subprocess import time from sys import platform @@ -96,7 +97,7 @@ def test_ssh_to_linode_and_get_kernel_version( "--text", "--no-headers", ] - ) + ).strip() time.sleep(SSH_SLEEP_PERIOD) @@ -129,16 +130,32 @@ def test_check_vm_for_ipv4_connectivity( "--text", "--no-headers", ] - ) - - time.sleep(SSH_SLEEP_PERIOD) - - output = os.popen( - "linode-cli ssh root@" - + linode_label - + " -i " - + privkey_file - + ' -o StrictHostKeyChecking=no -o IdentitiesOnly=yes "ping -4 -W60 -c3 google.com"' - ).read() - - assert "0% packet loss" in output + ).strip() + + ssh_cmd = [ + "linode-cli", + "ssh", + f"root@{linode_label}", + "-i", + privkey_file, + "-o", + "StrictHostKeyChecking=no", + "-o", + "IdentitiesOnly=yes", + "ping -4 -W60 -c3 google.com", + ] + + output = "" + for attempt in range(NUM_OF_RETRIES): + result = subprocess.run( + ssh_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + output = result.stdout + if "0% packet loss" in output: + break + if attempt < NUM_OF_RETRIES - 1: + time.sleep(10) + + assert ( + "0% packet loss" in output + ), f"Ping failed after {NUM_OF_RETRIES} retries: {output}" From 20af39602fc39e50edb4349bfce5f673ddc24cba Mon Sep 17 00:00:00 2001 From: Vinay <143587840+vshanthe@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:10:55 +0530 Subject: [PATCH 12/13] Integration test fix for flaky test (#854) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/integration/firewalls/test_firewalls.py | 2 +- tests/integration/linodes/fixtures.py | 9 +++- tests/integration/linodes/helpers.py | 5 +- tests/integration/linodes/test_backups.py | 4 +- tests/integration/linodes/test_configs.py | 30 ++++-------- tests/integration/linodes/test_disk.py | 6 +-- tests/integration/linodes/test_interfaces.py | 47 ++++++++++++++----- 7 files changed, 62 insertions(+), 41 deletions(-) diff --git a/tests/integration/firewalls/test_firewalls.py b/tests/integration/firewalls/test_firewalls.py index 1cd5b9c4..27baf3e2 100644 --- a/tests/integration/firewalls/test_firewalls.py +++ b/tests/integration/firewalls/test_firewalls.py @@ -45,7 +45,7 @@ def test_list_firewall(firewall_id): + ["list", "--no-headers", "--text", "--delimiter", ","] ) - assert re.search(firewall_id + "," + FIREWALL_LABEL + ",enabled", result) + assert re.search(rf"^{firewall_id},", result, re.MULTILINE) @pytest.mark.smoke diff --git a/tests/integration/linodes/fixtures.py b/tests/integration/linodes/fixtures.py index d2cb419f..16b43a9f 100644 --- a/tests/integration/linodes/fixtures.py +++ b/tests/integration/linodes/fixtures.py @@ -10,6 +10,7 @@ get_random_region_with_caps, get_random_text, retry_exec_test_command_with_delay, + wait_for_condition, ) from tests.integration.linodes.helpers import ( DEFAULT_LABEL, @@ -133,8 +134,14 @@ def linode_instance_config_tests(linode_cloud_firewall): def linode_disk_config(linode_instance_config_tests): linode_id = linode_instance_config_tests + def disks_ready(): + disks = get_disk_ids(linode_id=linode_id) + return len(disks) > 0 + + wait_for_condition(10, 120, disks_ready) + + disk_id = get_disk_ids(linode_id=linode_id)[0] label = get_random_text(5) + "_config" - disk_id = get_disk_ids(linode_id=linode_id)[1] config_id = exec_test_command( BASE_CMDS["linodes"] diff --git a/tests/integration/linodes/helpers.py b/tests/integration/linodes/helpers.py index d923baa1..b07f0794 100644 --- a/tests/integration/linodes/helpers.py +++ b/tests/integration/linodes/helpers.py @@ -1,7 +1,10 @@ import json import time -from tests.integration.helpers import BASE_CMDS, exec_test_command +from tests.integration.helpers import ( + BASE_CMDS, + exec_test_command, +) DEFAULT_RANDOM_PASS = exec_test_command(["openssl", "rand", "-base64", "32"]) DEFAULT_REGION = "us-ord" diff --git a/tests/integration/linodes/test_backups.py b/tests/integration/linodes/test_backups.py index 3b09f1f0..0949fbd5 100755 --- a/tests/integration/linodes/test_backups.py +++ b/tests/integration/linodes/test_backups.py @@ -91,9 +91,9 @@ def test_create_backup_with_backup_enabled(linode_backup_enabled): os.environ.get("RUN_LONG_TESTS", None) != "True", reason="Skipping long-running Test, to run set RUN_LONG_TESTS=True", ) -def test_take_snapshot_of_linode(): +def test_take_snapshot_of_linode(firewall_id): # get linode id after creation and wait for "running" status - linode_id = create_linode_and_wait() + linode_id = create_linode_and_wait(firewall_id) snapshot_label = "test_snapshot1" diff --git a/tests/integration/linodes/test_configs.py b/tests/integration/linodes/test_configs.py index fcde7f38..993ce9b1 100644 --- a/tests/integration/linodes/test_configs.py +++ b/tests/integration/linodes/test_configs.py @@ -18,36 +18,31 @@ ) -def test_config_create(linode_instance_config_tests): +def test_config_create(linode_instance_config_tests, linode_disk_config): linode_id = linode_instance_config_tests + config_id = linode_disk_config - label = get_random_text(5) + "_config" - disk_id = get_disk_ids(linode_id=linode_id)[1] - - result = exec_test_command( + res = exec_test_command( BASE_CMDS["linodes"] + [ - "config-create", + "config-view", linode_id, - "--label", - label, - "--devices.sda.disk_id", - disk_id, + config_id, "--text", ] ) headers = ["id", "label", "kernel"] - - assert_headers_in_lines(headers, result.splitlines()) - assert label in result + assert_headers_in_lines(headers, res.splitlines()) + assert config_id in res def test_config_delete(linode_instance_config_tests): linode_id = linode_instance_config_tests + disk_id = get_disk_ids(linode_id=linode_id)[0] + label = get_random_text(5) + "_config" - disk_id = get_disk_ids(linode_id=linode_id)[1] config_id = exec_test_command( BASE_CMDS["linodes"] @@ -65,12 +60,7 @@ def test_config_delete(linode_instance_config_tests): ) retry_exec_test_command_with_delay( - BASE_CMDS["linodes"] - + [ - "config-delete", - linode_id, - config_id, - ] + BASE_CMDS["linodes"] + ["config-delete", linode_id, config_id] ) diff --git a/tests/integration/linodes/test_disk.py b/tests/integration/linodes/test_disk.py index a0cef3a7..3b92e83e 100644 --- a/tests/integration/linodes/test_disk.py +++ b/tests/integration/linodes/test_disk.py @@ -17,7 +17,7 @@ def test_disk_resize_clone_and_create(linode_instance_disk_tests): linode_id = linode_instance_disk_tests - disk_id = get_disk_ids(linode_id=linode_id)[1] + disk_id = get_disk_ids(linode_id=linode_id)[0] # resize disk retry_exec_test_command_with_delay( @@ -99,7 +99,7 @@ def disk_poll_func(): def test_disk_reset_password(linode_instance_disk_tests): linode_id = linode_instance_disk_tests - disk_id = get_disk_ids(linode_id)[1] + disk_id = get_disk_ids(linode_id)[0] retry_exec_test_command_with_delay( BASE_CMDS["linodes"] @@ -118,7 +118,7 @@ def test_disk_reset_password(linode_instance_disk_tests): def test_disk_update(linode_instance_disk_tests): linode_id = linode_instance_disk_tests - disk_id = get_disk_ids(linode_id)[1] + disk_id = get_disk_ids(linode_id)[0] update_label = get_random_text(5) + "newdisk" diff --git a/tests/integration/linodes/test_interfaces.py b/tests/integration/linodes/test_interfaces.py index 17bb92bb..2864af96 100644 --- a/tests/integration/linodes/test_interfaces.py +++ b/tests/integration/linodes/test_interfaces.py @@ -4,6 +4,7 @@ from tests.integration.helpers import ( BASE_CMDS, exec_test_command, + wait_for_condition, ) from tests.integration.linodes.fixtures import ( # noqa: F401 linode_with_vpc_interface_as_args, @@ -14,21 +15,42 @@ def assert_interface_configuration( linode_json: Dict[str, Any], vpc_json: Dict[str, Any] ): - config_json = json.loads( - exec_test_command( - BASE_CMDS["linodes"] - + [ - "configs-list", - str(linode_json["id"]), - "--json", - "--suppress-warnings", - ] + linode_id = str(linode_json["id"]) + configs = [] + + def fetch_configs(): + nonlocal configs + configs = json.loads( + exec_test_command( + BASE_CMDS["linodes"] + + [ + "configs-list", + linode_id, + "--json", + "--suppress-warnings", + ] + ) ) - )[0] + return len(configs) > 0 + + wait_for_condition(5, 180, fetch_configs) + + assert configs, f"No configs found for Linode {linode_id}" + config_json = configs[0] + + interfaces = config_json["interfaces"] - vpc_interface = config_json["interfaces"][0] - public_interface = config_json["interfaces"][1] + vpc_interface = next((i for i in interfaces if i["purpose"] == "vpc"), None) + public_interface = next( + (i for i in interfaces if i["purpose"] == "public"), None + ) + assert ( + vpc_interface + ), "Expected interface with purpose 'vpc' in configuration" + assert ( + public_interface + ), "Expected interface with purpose 'public' in configuration" assert vpc_interface["primary"] assert vpc_interface["purpose"] == "vpc" assert vpc_interface["subnet_id"] == vpc_json["subnets"][0]["id"] @@ -38,7 +60,6 @@ def assert_interface_configuration( assert vpc_interface["ip_ranges"][0] == "10.0.0.6/32" assert not public_interface["primary"] - assert public_interface["purpose"] == "public" def test_with_vpc_interface_as_args(linode_with_vpc_interface_as_args): From 049d77242895062478f2114e2380bdc493ac648e Mon Sep 17 00:00:00 2001 From: Maciej Wilk Date: Tue, 24 Feb 2026 10:55:29 +0100 Subject: [PATCH 13/13] Regression fixes (#856) --- tests/integration/conftest.py | 13 +++++++ tests/integration/domains/fixtures.py | 38 +++++++++++++++++++ .../domains/test_domain_records.py | 4 ++ tests/integration/events/fixtures.py | 14 +++++++ tests/integration/helpers.py | 31 +++++++++++++-- tests/integration/nodebalancers/fixtures.py | 13 +++++++ tests/integration/obj/test_object_storage.py | 7 +++- tests/integration/ssh/test_plugin_ssh.py | 2 +- 8 files changed, 117 insertions(+), 5 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 253f1498..2f1e5ed7 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -18,10 +18,12 @@ from linodecli import ENV_TOKEN_NAME from tests.integration.helpers import ( + check_attribute_value, delete_target_id, exec_test_command, get_random_region_with_caps, get_random_text, + wait_for_condition, ) @@ -96,6 +98,17 @@ def create_inbound_rule(ipv4_address, ipv6_address): command.extend(["--rules.inbound", inbound_rule]) firewall_id = exec_test_command(command) + # Verify firewall status is reachable before proceeding with tests + wait_for_condition( + 5, + 60, + check_attribute_value, + "firewalls", + "view", + firewall_id, + "status", + "enabled", + ) yield firewall_id diff --git a/tests/integration/domains/fixtures.py b/tests/integration/domains/fixtures.py index 476e3804..c2071f80 100644 --- a/tests/integration/domains/fixtures.py +++ b/tests/integration/domains/fixtures.py @@ -2,9 +2,11 @@ from tests.integration.helpers import ( BASE_CMDS, + check_attribute_value, delete_target_id, exec_test_command, get_random_text, + wait_for_condition, ) @@ -27,6 +29,18 @@ def master_domain(): ] ) + # Verify domain status becomes active before proceeding with tests + wait_for_condition( + 5, + 60, + check_attribute_value, + "domains", + "view", + domain_id, + "status", + "active", + ) + yield domain_id delete_target_id("domains", id=domain_id) @@ -52,6 +66,18 @@ def slave_domain(): ] ) + # Verify domain status becomes active before proceeding with tests + wait_for_condition( + 5, + 60, + check_attribute_value, + "domains", + "view", + domain_id, + "status", + "active", + ) + yield domain_id delete_target_id("domains", domain_id) @@ -75,6 +101,18 @@ def domain_and_record(): ] ) + # Verify domain status becomes active before proceeding with tests + wait_for_condition( + 5, + 60, + check_attribute_value, + "domains", + "view", + domain_id, + "status", + "active", + ) + # Create record record_id = exec_test_command( BASE_CMDS["domains"] diff --git a/tests/integration/domains/test_domain_records.py b/tests/integration/domains/test_domain_records.py index 59e30791..90306fd0 100644 --- a/tests/integration/domains/test_domain_records.py +++ b/tests/integration/domains/test_domain_records.py @@ -11,6 +11,7 @@ from tests.integration.helpers import ( BASE_CMDS, contains_at_least_one_of, + delete_target_id, exec_test_command, ) @@ -53,6 +54,9 @@ def test_create_a_domain(master_domain): ) assert another_domain in domain_list_after + # clean-up + delete_target_id("domains", id=another_domain) + @pytest.mark.smoke def test_create_domain_srv_record(domain_and_record): diff --git a/tests/integration/events/fixtures.py b/tests/integration/events/fixtures.py index 0c9b7647..b4886e47 100644 --- a/tests/integration/events/fixtures.py +++ b/tests/integration/events/fixtures.py @@ -2,9 +2,11 @@ from tests.integration.helpers import ( BASE_CMDS, + check_attribute_value, delete_target_id, exec_test_command, get_random_text, + wait_for_condition, ) @@ -27,6 +29,18 @@ def events_create_domain(): ] ) + # Verify domain status becomes active before proceeding with tests + wait_for_condition( + 5, + 60, + check_attribute_value, + "domains", + "view", + domain_id, + "status", + "active", + ) + yield domain_id delete_target_id(target="domains", id=domain_id) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index f74d8195..03a0fc30 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -58,13 +58,15 @@ def get_random_text(length: int = 10): - return "".join(random.choice(ascii_lowercase) for i in range(length)) + return "".join(random.choice(ascii_lowercase) for _ in range(length)) -def wait_for_condition(interval: int, timeout: int, condition: Callable): +def wait_for_condition(interval: int, timeout: int, condition: Callable, *args): start_time = time.time() while True: - if condition(): + result = condition(*args) + + if result: break if time.time() - start_time > timeout: @@ -213,3 +215,26 @@ def assert_help_actions_list(expected_actions, help_output): output_actions = re.findall(r"│\s(\S+(?:,\s)?\S+)\s*│", help_output) for expected_action in expected_actions: assert expected_action in output_actions + + +def view_command_attribute( + command: str, action: str, item_id: str, attribute: str +) -> str: + return exec_test_command( + BASE_CMDS[command] + + [ + action, + item_id, + "--text", + "--no-header", + "--format", + attribute, + ] + ) + + +def check_attribute_value( + command: str, action: str, item_id: str, attribute: str, expected_val: str +) -> bool: + result = view_command_attribute(command, action, item_id, attribute) + return expected_val in result diff --git a/tests/integration/nodebalancers/fixtures.py b/tests/integration/nodebalancers/fixtures.py index ff07d0e5..a1a973e6 100644 --- a/tests/integration/nodebalancers/fixtures.py +++ b/tests/integration/nodebalancers/fixtures.py @@ -2,8 +2,10 @@ from tests.integration.helpers import ( BASE_CMDS, + check_attribute_value, delete_target_id, exec_test_command, + wait_for_condition, ) from tests.integration.linodes.helpers import DEFAULT_TEST_IMAGE @@ -216,6 +218,17 @@ def nodebalancer_with_udp_config_and_node(linode_cloud_firewall): "id", ] ) + # Verify configs-list contains just created confid id + wait_for_condition( + 5, + 60, + check_attribute_value, + "nodebalancers", + "configs-list", + nodebalancer_id, + "id", + config_id, + ) linode_create = exec_test_command( BASE_CMDS["linodes"] diff --git a/tests/integration/obj/test_object_storage.py b/tests/integration/obj/test_object_storage.py index 8a69c34d..1dde58b7 100644 --- a/tests/integration/obj/test_object_storage.py +++ b/tests/integration/obj/test_object_storage.py @@ -34,7 +34,12 @@ def test_clusters_list(): assert cluster["id"] assert cluster["region"] - assert cluster["status"] in {"available", "unavailable"} + assert cluster["status"] in { + "available", + "unavailable", + "hidden", + "limited", + } assert cluster["domain"].endswith(".linodeobjects.com") assert cluster["static_site_domain"].startswith("website-") diff --git a/tests/integration/ssh/test_plugin_ssh.py b/tests/integration/ssh/test_plugin_ssh.py index 51380622..e8b76573 100644 --- a/tests/integration/ssh/test_plugin_ssh.py +++ b/tests/integration/ssh/test_plugin_ssh.py @@ -24,7 +24,7 @@ INSTANCE_WAIT_TIMEOUT_SECONDS = 120 -SSH_WAIT_TIMEOUT_SECONDS = 80 +SSH_WAIT_TIMEOUT_SECONDS = 120 POLL_INTERVAL = 5