Skip to content
Merged
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
docker-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Build the Docker image
run: docker build . --file Dockerfile --tag linode/cli:$(date +%s) --build-arg="github_token=$GITHUB_TOKEN"
env:
Expand All @@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: checkout repo
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: setup python 3
uses: actions/setup-python@v5
Expand All @@ -37,7 +37,7 @@ jobs:
python-version: [ "3.9","3.10","3.11", "3.12", "3.13" ]
steps:
- name: Clone Repository
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Setup Python
uses: actions/setup-python@v5
Expand All @@ -59,7 +59,7 @@ jobs:
runs-on: windows-latest
steps:
- name: Clone Repository
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Setup Python
uses: actions/setup-python@v5
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout repository'
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4
with:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/e2e-suite-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
steps:
# Check out merge commit
- name: Checkout PR
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
ref: ${{ inputs.sha }}

Expand Down Expand Up @@ -109,7 +109,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: 'recursive'
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/e2e-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,15 @@ jobs:
steps:
- name: Checkout Repository with SHA
if: ${{ inputs.sha != '' }}
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: 'recursive'
ref: ${{ inputs.sha }}

- name: Checkout Repository without SHA
if: ${{ inputs.sha == '' }}
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: 'recursive'
Expand Down Expand Up @@ -170,7 +170,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: 'recursive'
Expand Down Expand Up @@ -237,13 +237,13 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: 'recursive'

- name: Download test report
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: test-report-file

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
-
name: Run Labeler
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916
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 @@ -18,7 +18,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: 'recursive'
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-wiki.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ jobs:
publish-wiki:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Andrew-Chen-Wang/github-wiki-action@50650fccf3a10f741995523cf9708c53cec8912a # pin@v4.4.0
- uses: actions/checkout@v5
- uses: Andrew-Chen-Wang/github-wiki-action@2c80c13ee98aa43683bd77973ef4916e2eedf817 # pin@v5.0.1
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone Repository
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: setup python 3
uses: actions/setup-python@v5
Expand All @@ -45,7 +45,7 @@ jobs:
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # pin@v3.11.1

- name: Login to Docker Hub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # pin@v3.4.0
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # pin@v3.5.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
Expand Down Expand Up @@ -86,7 +86,7 @@ jobs:
environment: pypi-release
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Setup Python
uses: actions/setup-python@v5
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/remote-release-trigger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
private_key: ${{ secrets.CLI_RELEASE_PRIVATE_KEY }}

- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
# We want to checkout the main branch
ref: 'main'
Expand Down
10 changes: 5 additions & 5 deletions linodecli/api_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
from linodecli.helpers import API_CA_PATH, API_VERSION_OVERRIDE

from .baked.operation import (
ExplicitEmptyDictValue,
ExplicitEmptyListValue,
ExplicitJsonValue,
ExplicitNullValue,
OpenAPIOperation,
)
Expand Down Expand Up @@ -314,14 +314,14 @@ def _traverse_request_body(o: Any) -> Any:
result[k] = []
continue

if isinstance(v, ExplicitEmptyDictValue):
result[k] = {}
continue

if isinstance(v, ExplicitNullValue):
result[k] = None
continue

if isinstance(v, ExplicitJsonValue):
result[k] = v.json_value
continue

value = _traverse_request_body(v)

# We should exclude implicit empty lists
Expand Down
49 changes: 10 additions & 39 deletions linodecli/baked/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import re
import sys
from collections import defaultdict
from collections.abc import Callable
from dataclasses import dataclass
from getpass import getpass
from os import environ, path
from typing import Any, Dict, List, Optional, Tuple, Union
from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import urlparse

import openapi3.paths
Expand Down Expand Up @@ -49,46 +51,12 @@ def parse_boolean(value: str) -> bool:
raise argparse.ArgumentTypeError("Expected a boolean value")


def parse_dict(
value: str,
) -> Union[Dict[str, Any], "ExplicitEmptyDictValue", "ExplicitEmptyListValue"]:
"""
A helper function to decode incoming JSON data as python dicts. This is
intended to be passed to the `type=` kwarg for ArgumentParaser.add_argument.

:param value: The json string to be parsed into dict.
:type value: str

:returns: The dict value of the input.
:rtype: dict, ExplicitEmptyDictValue, or ExplicitEmptyListValue
"""
if not isinstance(value, str):
raise argparse.ArgumentTypeError("Expected a JSON string")

try:
result = json.loads(value)
except Exception as e:
raise argparse.ArgumentTypeError("Expected a JSON string") from e

# This is necessary because empty dicts and lists are excluded from requests
# by default, but we still want to support user-defined empty dict
# strings. This is particularly helpful when updating LKE node pool
# labels and taints.
if isinstance(result, dict) and result == {}:
return ExplicitEmptyDictValue()

if isinstance(result, list) and result == []:
return ExplicitEmptyListValue()

return result


TYPES = {
"string": str,
"integer": int,
"boolean": parse_boolean,
"array": list,
"object": parse_dict,
"object": lambda value: ExplicitJsonValue(json_value=json.loads(value)),
"number": float,
}

Expand All @@ -106,13 +74,16 @@ class ExplicitEmptyListValue:
"""


class ExplicitEmptyDictValue:
@dataclass
class ExplicitJsonValue:
"""
A special type used to explicitly pass empty dictionaries to the API.
A special type used to explicitly pass raw JSON from user input as is.
"""

json_value: Any


def wrap_parse_nullable_value(arg_type: str) -> TYPES:
def wrap_parse_nullable_value(arg_type: str) -> Callable[[Any], Any]:
"""
A helper function to parse `null` as None for nullable CLI args.
This is intended to be called and passed to the `type=` kwarg for ArgumentParser.add_argument.
Expand Down
2 changes: 1 addition & 1 deletion linodecli/configuration/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def print_users(self):
default_user = self.default_username()

for sec in self.config.sections():
if sec != "DEFAULT":
if sec not in ("DEFAULT", "custom_aliases"):
print(f'{"*" if sec == default_user else " "} {sec}')

sys.exit(ExitCodes.SUCCESS)
Expand Down
11 changes: 10 additions & 1 deletion tests/integration/account/test_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ def test_account_setting_view():
"longview_subscription",
"network_helper",
"interfaces_for_new_linodes",
"maintenance_policy",
]

settings_text = exec_test_command(
Expand Down Expand Up @@ -343,7 +344,15 @@ def test_maintenance_list():
)
lines = res.splitlines()

headers = ["entity.type", "entity.label"]
headers = [
"complete_time",
"entity.type",
"entity.label",
"maintenance_policy_set",
"not_before",
"source",
"start_time",
]
assert_headers_in_lines(headers, lines)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ def test_mysql_engine_config_view():
binlog_retention = mysql_config[0]["binlog_retention_period"]
assert binlog_retention["type"] == "integer"
assert binlog_retention["minimum"] == 600
assert binlog_retention["maximum"] == 86400
assert binlog_retention["maximum"] == 604800
assert binlog_retention["requires_restart"] is False

mysql_settings = mysql_config[0]["mysql"]
Expand Down
41 changes: 38 additions & 3 deletions tests/integration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
# TypeVars for generic type hints below
T = TypeVar("T")


MODULES = [
"account",
"alerts",
"domains",
"linodes",
"nodebalancers",
Expand All @@ -33,7 +33,9 @@
"linodes",
"lke",
"longview",
"maintenance",
"managed",
"monitor",
"networking",
"obj",
"object-storage",
Expand Down Expand Up @@ -100,8 +102,41 @@ def exec_failing_test_command(


# Delete/Remove helper functions (mainly used in clean-ups after tests)
def delete_target_id(target: str, id: str, delete_command: str = "delete"):
command = ["linode-cli", target, delete_command, id]
def delete_target_id(
target: str,
id: str,
delete_command: str = "delete",
service_type: str = None,
use_retry: bool = False,
retries: int = 3,
delay: int = 80,
):
if service_type:
command = ["linode-cli", target, delete_command, service_type, id]
else:
command = ["linode-cli", target, delete_command, id]

if use_retry:
last_exc = None
for attempt in range(retries):
try:
subprocess.run(
command,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
return # success
except Exception as e:
last_exc = e
if attempt < retries - 1:
time.sleep(delay)
# If all retries fail, raise
raise RuntimeError(
f"Error executing command '{' '.join(command)}' after {retries} retries: {last_exc}"
)

try:
subprocess.run(
command,
Expand Down
Loading
Loading