diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5bea77b2c..d97f93452 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 diff --git a/.github/workflows/e2e-suite.yml b/.github/workflows/e2e-suite.yml index fa5581798..bf2ff0154 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: @@ -127,7 +135,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 @@ -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 }} @@ -243,7 +252,7 @@ jobs: submodules: 'recursive' - name: Download test report - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: test-report-file @@ -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: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 77e26ca66..009472d7b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,10 +39,10 @@ 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 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # pin@v3.12.0 - name: Login to Docker Hub uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # pin@v3.6.0 diff --git a/.github/workflows/remote-release-trigger.yml b/.github/workflows/remote-release-trigger.yml index 2dfb75c2c..75a5da592 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 }} diff --git a/README.md b/README.md index 0db85368b..ef5e786c0 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 2f5f1b70c..2a6d7c9a0 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 718b619a4..419b5e06e 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 6a402eecf..e95acd961 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 a7e23b39d..1b633c7bb 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) @@ -348,7 +344,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, ) diff --git a/linodecli/configuration/helpers.py b/linodecli/configuration/helpers.py index a0398e2b3..35be04047 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 2d5761340..6952487f9 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/linodecli/plugins/obj/config.py b/linodecli/plugins/obj/config.py index f53806df5..0e287f56c 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" diff --git a/resolve_spec_url b/resolve_spec_url index 60ae216fb..7487e2ef3 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/integration/account/test_account.py b/tests/integration/account/test_account.py index e3d99c3c5..55c53513f 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/conftest.py b/tests/integration/conftest.py index 253f1498e..2f1e5ed76 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 476e38042..c2071f80d 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 59e307918..90306fd03 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 0c9b76478..b4886e470 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/firewalls/test_firewalls.py b/tests/integration/firewalls/test_firewalls.py index 1cd5b9c44..27baf3e2e 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/helpers.py b/tests/integration/helpers.py index f74d81957..03a0fc30b 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/linodes/fixtures.py b/tests/integration/linodes/fixtures.py index d2cb419fd..16b43a9f3 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 d923baa12..b07f0794a 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 3b09f1f0a..0949fbd5b 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 fcde7f385..993ce9b1d 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 a0cef3a71..3b92e83e2 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 17bb92bb5..2864af96b 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): diff --git a/tests/integration/lke/test_clusters.py b/tests/integration/lke/test_clusters.py index 4b11f5cb8..f9fae8e7f 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/nodebalancers/fixtures.py b/tests/integration/nodebalancers/fixtures.py index ff07d0e57..a1a973e66 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 8a69c34df..1dde58b7c 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 513806221..e8b765735 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 diff --git a/tests/integration/ssh/test_ssh.py b/tests/integration/ssh/test_ssh.py index a1f1f8e91..1c944cd46 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}" diff --git a/tests/unit/test_api_request.py b/tests/unit/test_api_request.py index a222d60db..1afa699ce 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 3ce0f8788..961a62121 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 14b704d0c..409250330 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 a354103f7..555b668aa 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 bf667fcbe..7293620ce 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