Skip to content

Commit

Permalink
Merge pull request #657 from linode/dev
Browse files Browse the repository at this point in the history
Release v5.53.0
  • Loading branch information
ezilber-akamai authored Oct 16, 2024
2 parents 700b935 + 268d689 commit 7991606
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 31 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/e2e-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ jobs:
"fields": [
{
"type": "mrkdwn",
"text": "*Build Result:*\n${{ steps.integration_tests.outcome == 'success' && ':large_green_circle: Build Passed' || ':red_circle: Build Failed' }}"
"text": "*Build Result:*\n${{ needs.integration_tests.result == 'success' && ':large_green_circle: Build Passed' || ':red_circle: Build Failed' }}"
},
{
"type": "mrkdwn",
Expand Down
11 changes: 4 additions & 7 deletions linodecli/configuration/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,10 @@ def update(
):
print(f"User {username} is not configured.")
sys.exit(ExitCodes.USERNAME_ERROR)
if not self.config.has_section(username) or allowed_defaults is None:
if (
not self.config.has_section(username)
and self.config.default_section is None
) or allowed_defaults is None:
return namespace

warn_dict = {}
Expand Down Expand Up @@ -335,12 +338,6 @@ def write_config(self):
to save values they've set, and is used internally to update the config
on disk when a new user if configured.
"""

# Create the config path isf necessary
config_path = f"{os.path.expanduser('~')}/.config"
if not os.path.exists(config_path):
os.makedirs(config_path)

with open(_get_config_path(), "w", encoding="utf-8") as f:
self.config.write(f)

Expand Down
16 changes: 15 additions & 1 deletion linodecli/configuration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"XDG_CONFIG_HOME", f"{os.path.expanduser('~')}/.config"
)

ENV_CONFIG_FILE_PATH = "LINODE_CLI_CONFIG"

# this is a list of browser that _should_ work for web-based auth. This is mostly
# intended to exclude lynx and other terminal browsers which could be opened, but
# won't work.
Expand All @@ -38,11 +40,23 @@ def _get_config_path() -> str:
:returns: The path to the local config file.
:rtype: str
"""
custom_path = os.getenv(ENV_CONFIG_FILE_PATH, None)

if custom_path is not None:
custom_path = os.path.expanduser(custom_path)
if not os.path.exists(custom_path):
os.makedirs(os.path.dirname(custom_path), exist_ok=True)
return custom_path

path = f"{LEGACY_CONFIG_DIR}/{LEGACY_CONFIG_NAME}"
if os.path.exists(path):
return path

return f"{CONFIG_DIR}/{CONFIG_NAME}"
path = f"{CONFIG_DIR}/{CONFIG_NAME}"
if not os.path.exists(path):
os.makedirs(CONFIG_DIR, exist_ok=True)

return path


def _get_config(load: bool = True):
Expand Down
2 changes: 2 additions & 0 deletions linodecli/help_pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"(e.g. 'v4beta')",
"LINODE_CLI_API_SCHEME": "Overrides the target scheme used for API requests. "
"(e.g. 'https')",
"LINODE_CLI_CONFIG": "Overrides the default configuration file path. "
"(e.g '~/.linode/my-cli-config')",
}

HELP_TOPICS = {
Expand Down
53 changes: 52 additions & 1 deletion linodecli/overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
large changes to the OpenAPI spec.
"""

from typing import Dict
from typing import Dict, List

from rich import box
from rich import print as rprint
Expand Down Expand Up @@ -57,6 +57,15 @@ def handle_types_region_prices_list(
return linode_types_with_region_prices(operation, output_handler, json_data)


@output_override("images", "replicate", OutputMode.table)
def handle_image_replicate(operation, output_handler, json_data) -> bool:
# pylint: disable=unused-argument
"""
Override the output of 'linode-cli images replicate'.
"""
return image_replicate_output(json_data)


def linode_types_with_region_prices(
operation, output_handler, json_data
) -> bool:
Expand Down Expand Up @@ -137,3 +146,45 @@ def format_region_prices(data: Dict[str, any]) -> any:
sub_table.add_row(*region_price_row)

return sub_table


def build_replicas_output(replicas: List) -> Table:
"""
Format nested replicas list to a sub-table.
"""
replicas_output = Table(show_header=False, box=None)
replicas_headers = replicas[0].keys()
for replica in replicas:
row = []
for h in replicas_headers:
row.append(Align(str(replica[h]), align="left"))
replicas_output.add_row(*row)

return replicas_output


def image_replicate_output(json_data) -> bool:
"""
Parse and format the image replicate output table.
"""
output = Table(
header_style="bold",
show_lines=True,
)

row = []
for header in json_data.keys():
if header == "regions" and len(json_data[header]) > 0:
# leverage `replicas` in output for readability
output.add_column("replicas", justify="center")
row.append(build_replicas_output(json_data[header]))
elif json_data[header] is not None:
output.add_column(header, justify="center")
row.append(Align(str(json_data[header]), align="left"))

output.add_row(*row)

console = Console()
console.print(output)

return False
46 changes: 27 additions & 19 deletions linodecli/plugins/get-kubeconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def call(args, context):
else cluster_config
)
if parsed.dry_run:
print(cluster_config)
print(yaml.dump(cluster_config))
else:
_dump_config(kubeconfig_path, cluster_config)

Expand Down Expand Up @@ -146,27 +146,35 @@ def _dump_config(filepath, data):
yaml.dump(data, file_descriptor)


# Merges the lists in the provided dicts. If non-list properties of the two
# dicts differ, uses the value from the first dict.
def _merge_dict(dict_1, dict_2):
"""
Merges two dicts:
* Lists that are present in both dicts are merged together by their "name" key
(preferring duplicate values in the first dict)
* `None` or missing keys in the first dict are set to the second dict's value
* Other values are preferred from the first dict
"""
# Return a new dict to prevent any accidental mutations
result = {}

for key in dict_1:
if not isinstance(dict_1[key], list):
result[key] = dict_1[key]
continue

merge_map = {sub["name"]: sub for sub in dict_1[key]}

for sub in dict_2[key]:
# If the name is already in the merge map, skip
if sub["name"] in merge_map:
continue

merge_map[sub["name"]] = sub

# Convert back to a list
result[key] = list(merge_map.values())
for key, dict_1_value in dict_1.items():
if dict_1_value is None and (dict_2_value := dict_2.get(key)):
# Replace null value in previous config
result[key] = dict_2_value
elif isinstance(dict_1_value, list) and (
dict_2_value := dict_2.get(key)
):
merge_map = {sub["name"]: sub for sub in dict_1_value}
for list_2_item in dict_2_value:
if (list_2_name := list_2_item["name"]) not in merge_map:
merge_map[list_2_name] = list_2_item
# Convert back to a list
result[key] = list(merge_map.values())
else:
result[key] = dict_1_value

# Process keys missing in dict_1
for key in set(dict_2.keys()).difference(dict_1.keys()):
result[key] = dict_2[key]

return result
23 changes: 23 additions & 0 deletions tests/unit/test_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,3 +609,26 @@ def test_bool_input_default(self, monkeypatch):
output = stdout_buf.getvalue()
assert "foo [y/N]: " in output
assert result

def test_custom_config_path(self, monkeypatch, tmp_path):
"""
Test use a custom configuration path
"""
conf = self._build_test_config()
custom_path = tmp_path / "test-cli-config"

with (
patch.dict(
os.environ,
{"LINODE_CLI_CONFIG": custom_path.absolute().as_posix()},
),
):
conf.write_config()

configs = custom_path.read_text().splitlines()
expected_configs = self.mock_config_file.splitlines()

assert len(configs) == len(expected_configs) + 1

for i, _ in enumerate(expected_configs):
assert expected_configs[i] == configs[i]
49 changes: 48 additions & 1 deletion tests/unit/test_plugin_kubeconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
- item:
property-1: a
property-2: b
property-3: c
name: item-1
- item:
property-1: d
Expand All @@ -51,6 +50,12 @@
dictionary: {"foo": "bar"}
"""

TEST_YAML_EMPTY_CONFIG = """
name: testing-kubeconfig
things: null
items: null
"""


# Test the output of --help
def test_print_help():
Expand Down Expand Up @@ -199,6 +204,37 @@ def test_merge(mock_cli, fake_kubeconfig_file):
assert result["dictionary"] == yaml_a["dictionary"]


def test_merge_to_empty_config(mock_cli, fake_kubeconfig_file_without_entries):
stdout_buf = io.StringIO()
mock_cli.call_operation = mock_call_operation

file_path = fake_kubeconfig_file_without_entries

try:
with contextlib.redirect_stdout(stdout_buf):
plugin.call(
[
"--label",
"nonempty_data",
"--kubeconfig",
file_path,
"--dry-run",
],
PluginContext("REALTOKEN", mock_cli),
)
except SystemExit as err:
assert err.code == 0

result = yaml.safe_load(stdout_buf.getvalue())
yaml_a = yaml.safe_load(TEST_YAML_EMPTY_CONFIG)
yaml_b = yaml.safe_load(TEST_YAML_CONTENT_B)

assert result["name"] == yaml_a["name"]
assert result["things"] is None
assert result["items"] == yaml_b["items"]
assert result["dictionary"] == yaml_b["dictionary"]


@pytest.fixture(scope="session", autouse=True)
def fake_kubeconfig_file():
with tempfile.NamedTemporaryFile(delete=False) as fp:
Expand All @@ -220,6 +256,17 @@ def fake_empty_file():
os.remove(file_path)


@pytest.fixture(scope="session", autouse=True)
def fake_kubeconfig_file_without_entries():
with tempfile.NamedTemporaryFile("wt", delete=False) as fp:
fp.write(TEST_YAML_EMPTY_CONFIG)
file_path = fp.name

yield file_path

os.remove(file_path)


def mock_call_operation(command, action, **kwargs):
if (
command == "lke"
Expand Down
3 changes: 3 additions & 0 deletions wiki/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ environment variable.
If you wish to hide the API Version warning you can use the `LINODE_CLI_SUPPRESS_VERSION_WARNING`
environment variable.

You may also specify a custom configuration path using the `LINODE_CLI_CONFIG` environment variable
to replace the default path `~/.config/linode-cli`.

## Configurable API URL

In some cases you may want to run linode-cli against a non-default Linode API URL.
Expand Down
3 changes: 2 additions & 1 deletion wiki/development/Development - Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ configure the following:
- Overrides for the target API URL (hostname, version, scheme, etc.)

This command serves as an interactive prompt and outputs a configuration file to `~/.config/linode-cli`.
This file is in a simple INI format and can be easily modified manually by users.
This file is in a simple INI format and can be easily modified manually by users.
You may also specify a custom configuration file path using the `LINODE_CLI_CONFIG` environment variable.

Additionally, multiple users can be created for the CLI which can be designated when running commands using the `--as-user` argument
or using the `default-user` config variable.
Expand Down

0 comments on commit 7991606

Please sign in to comment.