Skip to content

Commit 592ddb3

Browse files
jriddle-linodelgarber-akamaizliang-akamai
authored
v5.49.0 (#584)
Co-authored-by: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> Co-authored-by: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com>
1 parent 72c723f commit 592ddb3

17 files changed

+298
-72
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ install: check-prerequisites requirements build
2020

2121
.PHONY: bake
2222
bake: clean
23+
ifeq ($(SKIP_BAKE), 1)
24+
@echo Skipping bake stage
25+
else
2326
python3 -m linodecli bake ${SPEC} --skip-config
2427
cp data-3 linodecli/
28+
endif
2529

2630
.PHONY: build
2731
build: clean bake

linodecli/api_request.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import json
77
import sys
88
import time
9-
from sys import version_info
109
from typing import Any, Iterable, List, Optional
1110

1211
import requests
@@ -69,10 +68,7 @@ def do_request(
6968
headers = {
7069
"Authorization": f"Bearer {ctx.config.get_token()}",
7170
"Content-Type": "application/json",
72-
"User-Agent": (
73-
f"linode-cli:{ctx.version} "
74-
f"python/{version_info[0]}.{version_info[1]}.{version_info[2]}"
75-
),
71+
"User-Agent": ctx.user_agent,
7672
}
7773

7874
parsed_args = operation.parse_args(args)
@@ -257,11 +253,15 @@ def _build_request_body(ctx, operation, parsed_args) -> Optional[str]:
257253

258254
# expand paths
259255
for k, v in vars(parsed_args).items():
256+
if v is None:
257+
continue
258+
260259
cur = expanded_json
261260
for part in k.split(".")[:-1]:
262261
if part not in cur:
263262
cur[part] = {}
264263
cur = cur[part]
264+
265265
cur[k.split(".")[-1]] = v
266266

267267
return json.dumps(_traverse_request_body(expanded_json))

linodecli/arg_helpers.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ def help_with_ops(ops, config):
341341
)
342342

343343

344-
def action_help(cli, command, action):
344+
def action_help(cli, command, action): # pylint: disable=too-many-branches
345345
"""
346346
Prints help relevant to the command and action
347347
"""
@@ -398,11 +398,24 @@ def action_help(cli, command, action):
398398
if op.method in {"post", "put"} and arg.required
399399
else ""
400400
)
401-
nullable_fmt = " (nullable)" if arg.nullable else ""
402-
print(
403-
f" --{arg.path}: {is_required}{arg.description}{nullable_fmt}"
401+
402+
extensions = []
403+
404+
if arg.format == "json":
405+
extensions.append("JSON")
406+
407+
if arg.nullable:
408+
extensions.append("nullable")
409+
410+
if arg.is_parent:
411+
extensions.append("conflicts with children")
412+
413+
suffix = (
414+
f" ({', '.join(extensions)})" if len(extensions) > 0 else ""
404415
)
405416

417+
print(f" --{arg.path}: {is_required}{arg.description}{suffix}")
418+
406419

407420
def bake_command(cli, spec_loc):
408421
"""

linodecli/baked/operation.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
import platform
99
import re
1010
import sys
11+
from collections import defaultdict
1112
from getpass import getpass
1213
from os import environ, path
13-
from typing import List, Tuple
14+
from typing import Any, List, Tuple
1415

1516
from openapi3.paths import Operation
1617

@@ -427,14 +428,14 @@ def _add_args_post_put(self, parser) -> List[Tuple[str, str]]:
427428
action=ArrayAction,
428429
type=arg_type_handler,
429430
)
430-
elif arg.list_item:
431+
elif arg.is_child:
431432
parser.add_argument(
432433
"--" + arg.path,
433434
metavar=arg.name,
434435
action=ListArgumentAction,
435436
type=arg_type_handler,
436437
)
437-
list_items.append((arg.path, arg.list_parent))
438+
list_items.append((arg.path, arg.parent))
438439
else:
439440
if arg.datatype == "string" and arg.format == "password":
440441
# special case - password input
@@ -463,10 +464,51 @@ def _add_args_post_put(self, parser) -> List[Tuple[str, str]]:
463464

464465
return list_items
465466

467+
def _validate_parent_child_conflicts(self, parsed: argparse.Namespace):
468+
"""
469+
This method validates that no child arguments (e.g. --interfaces.purpose) are
470+
specified alongside their parent (e.g. --interfaces).
471+
"""
472+
conflicts = defaultdict(list)
473+
474+
for arg in self.args:
475+
parent = arg.parent
476+
arg_value = getattr(parsed, arg.path, None)
477+
478+
if parent is None or arg_value is None:
479+
continue
480+
481+
# Special case to ignore child arguments that are not specified
482+
# but are implicitly populated by ListArgumentAction.
483+
if isinstance(arg_value, list) and arg_value.count(None) == len(
484+
arg_value
485+
):
486+
continue
487+
488+
# If the parent isn't defined, we can
489+
# skip this one
490+
if getattr(parsed, parent) is None:
491+
continue
492+
493+
# We found a conflict
494+
conflicts[parent].append(arg)
495+
496+
# No conflicts found
497+
if len(conflicts) < 1:
498+
return
499+
500+
for parent, args in conflicts.items():
501+
arg_format = ", ".join([f"--{v.path}" for v in args])
502+
print(
503+
f"Argument(s) {arg_format} cannot be specified when --{parent} is specified.",
504+
file=sys.stderr,
505+
)
506+
507+
sys.exit(2)
508+
466509
@staticmethod
467510
def _handle_list_items(
468-
list_items,
469-
parsed,
511+
list_items: List[Tuple[str, str]], parsed: Any
470512
): # pylint: disable=too-many-locals,too-many-branches,too-many-statements
471513
lists = {}
472514

@@ -563,4 +605,9 @@ def parse_args(self, args):
563605
elif self.method in ("post", "put"):
564606
list_items = self._add_args_post_put(parser)
565607

566-
return self._handle_list_items(list_items, parser.parse_args(args))
608+
parsed = parser.parse_args(args)
609+
610+
if self.method in ("post", "put"):
611+
self._validate_parent_child_conflicts(parsed)
612+
613+
return self._handle_list_items(list_items, parsed)

linodecli/baked/request.py

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ class OpenAPIRequestArg:
99
"""
1010

1111
def __init__(
12-
self, name, schema, required, prefix=None, list_parent=None
12+
self,
13+
name,
14+
schema,
15+
required,
16+
prefix=None,
17+
is_parent=False,
18+
parent=None,
1319
): # pylint: disable=too-many-arguments
1420
"""
1521
Parses a single Schema node into a argument the CLI can use when making
@@ -23,6 +29,10 @@ def __init__(
2329
:param prefix: The prefix for this arg's path, used in the actual argument
2430
to the CLI to ensure unique arg names
2531
:type prefix: str
32+
:param is_parent: Whether this argument is a parent to child fields.
33+
:type is_parent: bool
34+
:param parent: If applicable, the path to the parent list for this argument.
35+
:type parent: Optional[str]
2636
"""
2737
#: The name of this argument, mostly used for display and docs
2838
self.name = name
@@ -50,6 +60,11 @@ def __init__(
5060
schema.extensions.get("linode-cli-format") or schema.format or None
5161
)
5262

63+
# If this is a deeply nested array we should treat it as JSON.
64+
# This allows users to specify fields like --interfaces.ip_ranges.
65+
if is_parent or (schema.type == "array" and parent is not None):
66+
self.format = "json"
67+
5368
#: The type accepted for this argument. This will ultimately determine what
5469
#: we accept in the ArgumentParser
5570
self.datatype = (
@@ -59,13 +74,16 @@ def __init__(
5974
#: The type of item accepted in this list; if None, this is not a list
6075
self.item_type = None
6176

62-
#: Whether the argument is a field in a nested list.
63-
self.list_item = list_parent is not None
77+
#: Whether the argument is a parent to child fields.
78+
self.is_parent = is_parent
79+
80+
#: Whether the argument is a nested field.
81+
self.is_child = parent is not None
6482

6583
#: The name of the list this argument falls under.
6684
#: This allows nested dictionaries to be specified in lists of objects.
6785
#: e.g. --interfaces.ipv4.nat_1_1
68-
self.list_parent = list_parent
86+
self.parent = parent
6987

7088
#: The path of the path element in the schema.
7189
self.prefix = prefix
@@ -85,7 +103,7 @@ def __init__(
85103
)
86104

87105

88-
def _parse_request_model(schema, prefix=None, list_parent=None):
106+
def _parse_request_model(schema, prefix=None, parent=None):
89107
"""
90108
Parses a schema into a list of OpenAPIRequest objects
91109
:param schema: The schema to parse as a request model
@@ -107,8 +125,11 @@ def _parse_request_model(schema, prefix=None, list_parent=None):
107125
if v.type == "object" and not v.readOnly and v.properties:
108126
# nested objects receive a prefix and are otherwise parsed normally
109127
pref = prefix + "." + k if prefix else k
128+
110129
args += _parse_request_model(
111-
v, prefix=pref, list_parent=list_parent
130+
v,
131+
prefix=pref,
132+
parent=parent,
112133
)
113134
elif (
114135
v.type == "array"
@@ -119,9 +140,20 @@ def _parse_request_model(schema, prefix=None, list_parent=None):
119140
# handle lists of objects as a special case, where each property
120141
# of the object in the list is its own argument
121142
pref = prefix + "." + k if prefix else k
122-
args += _parse_request_model(
123-
v.items, prefix=pref, list_parent=pref
143+
144+
# Support specifying this list as JSON
145+
args.append(
146+
OpenAPIRequestArg(
147+
k,
148+
v.items,
149+
False,
150+
prefix=prefix,
151+
is_parent=True,
152+
parent=parent,
153+
)
124154
)
155+
156+
args += _parse_request_model(v.items, prefix=pref, parent=pref)
125157
else:
126158
# required fields are defined in the schema above the property, so
127159
# we have to check here if required fields are defined/if this key
@@ -131,7 +163,7 @@ def _parse_request_model(schema, prefix=None, list_parent=None):
131163
required = k in schema.required
132164
args.append(
133165
OpenAPIRequestArg(
134-
k, v, required, prefix=prefix, list_parent=list_parent
166+
k, v, required, prefix=prefix, parent=parent
135167
)
136168
)
137169

linodecli/cli.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,14 @@ def find_operation(self, command, action):
193193

194194
# Fail if no matching alias was found
195195
raise ValueError(f"No action {action} for command {command}")
196+
197+
@property
198+
def user_agent(self) -> str:
199+
"""
200+
Returns the User-Agent to use when making API requests.
201+
"""
202+
return (
203+
f"linode-cli/{self.version} "
204+
f"linode-api-docs/{self.spec_version} "
205+
f"python/{version_info[0]}.{version_info[1]}.{version_info[2]}"
206+
)

linodecli/plugins/metadata.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,16 @@ def print_ssh_keys_table(data):
5757
"""
5858
table = Table(show_lines=True)
5959

60-
table.add_column("ssh keys")
60+
table.add_column("user")
61+
table.add_column("ssh key")
6162

62-
if data.users.root is not None:
63-
for key in data.users.root:
64-
table.add_row(key)
63+
for name, keys in data.users.items():
64+
# Keys will be None if no keys are configured for the user
65+
if keys is None:
66+
continue
67+
68+
for key in keys:
69+
table.add_row(name, key)
6570

6671
rprint(table)
6772

@@ -189,7 +194,7 @@ def get_metadata_parser():
189194
return parser
190195

191196

192-
def call(args, _):
197+
def call(args, context):
193198
"""
194199
The entrypoint for this plugin
195200
"""
@@ -204,7 +209,7 @@ def call(args, _):
204209
# make a client, but only if we weren't printing help and endpoint is valid
205210
if "--help" not in args:
206211
try:
207-
client = MetadataClient()
212+
client = MetadataClient(user_agent=context.client.user_agent)
208213
except ConnectTimeout as exc:
209214
raise ConnectionError(
210215
"Can't access Metadata service. Please verify that you are inside a Linode."

tests/fixtures/api_request_test_foobar_post.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ components:
114114
nested_int:
115115
type: number
116116
description: A deeply nested integer.
117+
field_array:
118+
type: array
119+
description: An arbitrary deeply nested array.
120+
items:
121+
type: string
117122
field_string:
118123
type: string
119124
description: An arbitrary field.

0 commit comments

Comments
 (0)