linode-cli 5.68.0__tar.gz → 5.69.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {linode_cli-5.68.0/linode_cli.egg-info → linode_cli-5.69.0}/PKG-INFO +13 -4
  2. {linode_cli-5.68.0 → linode_cli-5.69.0}/README.md +12 -3
  3. {linode_cli-5.68.0 → linode_cli-5.69.0/linode_cli.egg-info}/PKG-INFO +13 -4
  4. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/baked/request.py +9 -1
  5. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/baked/response.py +11 -2
  6. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/baked/util.py +75 -1
  7. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/configuration/auth.py +6 -5
  8. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/configuration/helpers.py +4 -3
  9. linode_cli-5.69.0/linodecli/data-3 +0 -0
  10. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/plugins/get-kubeconfig.py +27 -2
  11. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/version.py +1 -1
  12. linode_cli-5.68.0/linodecli/data-3 +0 -0
  13. {linode_cli-5.68.0 → linode_cli-5.69.0}/LICENSE +0 -0
  14. {linode_cli-5.68.0 → linode_cli-5.69.0}/MANIFEST.in +0 -0
  15. {linode_cli-5.68.0 → linode_cli-5.69.0}/linode_cli.egg-info/SOURCES.txt +0 -0
  16. {linode_cli-5.68.0 → linode_cli-5.69.0}/linode_cli.egg-info/dependency_links.txt +0 -0
  17. {linode_cli-5.68.0 → linode_cli-5.69.0}/linode_cli.egg-info/entry_points.txt +0 -0
  18. {linode_cli-5.68.0 → linode_cli-5.69.0}/linode_cli.egg-info/requires.txt +0 -0
  19. {linode_cli-5.68.0 → linode_cli-5.69.0}/linode_cli.egg-info/top_level.txt +0 -0
  20. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/__init__.py +0 -0
  21. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/__main__.py +0 -0
  22. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/api_request.py +0 -0
  23. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/arg_helpers.py +0 -0
  24. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/baked/__init__.py +0 -0
  25. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/baked/operation.py +0 -0
  26. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/baked/parsing.py +0 -0
  27. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/cli.py +0 -0
  28. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/completion.py +0 -0
  29. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/configuration/__init__.py +0 -0
  30. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/configuration/config.py +0 -0
  31. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/exit_codes.py +0 -0
  32. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/help_formatter.py +0 -0
  33. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/help_pages.py +0 -0
  34. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/helpers.py +0 -0
  35. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/oauth-landing-page.html +0 -0
  36. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/output/__init__.py +0 -0
  37. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/output/helpers.py +0 -0
  38. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/output/output_handler.py +0 -0
  39. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/overrides.py +0 -0
  40. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/plugins/__init__.py +0 -0
  41. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/plugins/firewall-editor.py +0 -0
  42. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/plugins/image-upload.py +0 -0
  43. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/plugins/metadata.py +0 -0
  44. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/plugins/monitor-api.py +0 -0
  45. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/plugins/obj/__init__.py +0 -0
  46. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/plugins/obj/buckets.py +0 -0
  47. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/plugins/obj/config.py +0 -0
  48. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/plugins/obj/helpers.py +0 -0
  49. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/plugins/obj/list.py +0 -0
  50. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/plugins/obj/objects.py +0 -0
  51. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/plugins/obj/website.py +0 -0
  52. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/plugins/plugins.py +0 -0
  53. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/plugins/region-table.py +0 -0
  54. {linode_cli-5.68.0 → linode_cli-5.69.0}/linodecli/plugins/ssh.py +0 -0
  55. {linode_cli-5.68.0 → linode_cli-5.69.0}/pyproject.toml +0 -0
  56. {linode_cli-5.68.0 → linode_cli-5.69.0}/setup.cfg +0 -0
  57. {linode_cli-5.68.0 → linode_cli-5.69.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linode-cli
3
- Version: 5.68.0
3
+ Version: 5.69.0
4
4
  Summary: The official command-line interface for interacting with the Linode API.
5
5
  Author-email: "Akamai Technologies Inc." <developers@linode.com>
6
6
  License-Expression: BSD-3-Clause
@@ -45,12 +45,21 @@ Visit the [Wiki](../../wiki) for more information.
45
45
 
46
46
  ## Install
47
47
 
48
- Install via PyPI:
48
+ We recommend installing `linode-cli` with `pipx`, which installs each Python CLI tool into its own isolated environment and works on distributions where `pip install` fails because the system Python is marked as externally managed ([PEP 668](https://peps.python.org/pep-0668/)). If pipx isn't installed yet, follow the [pipx installation guide](https://pipx.pypa.io/latest/how-to/install-pipx.html).
49
+
50
+ To install:
51
+
52
+ ```bash
53
+ pipx install linode-cli
54
+ ```
55
+
56
+ To upgrade:
57
+
49
58
  ```bash
50
- pip3 install linode-cli
59
+ pipx upgrade linode-cli
51
60
  ```
52
61
 
53
- Visit the [Wiki](../../wiki/Installation) for more information.
62
+ The [Wiki](https://github.com/linode/linode-cli/wiki/Installation) covers other installation methods, including the Docker image, the GitHub Action, and building from source.
54
63
 
55
64
  ## Contributing
56
65
 
@@ -12,12 +12,21 @@ Visit the [Wiki](../../wiki) for more information.
12
12
 
13
13
  ## Install
14
14
 
15
- Install via PyPI:
15
+ We recommend installing `linode-cli` with `pipx`, which installs each Python CLI tool into its own isolated environment and works on distributions where `pip install` fails because the system Python is marked as externally managed ([PEP 668](https://peps.python.org/pep-0668/)). If pipx isn't installed yet, follow the [pipx installation guide](https://pipx.pypa.io/latest/how-to/install-pipx.html).
16
+
17
+ To install:
18
+
19
+ ```bash
20
+ pipx install linode-cli
21
+ ```
22
+
23
+ To upgrade:
24
+
16
25
  ```bash
17
- pip3 install linode-cli
26
+ pipx upgrade linode-cli
18
27
  ```
19
28
 
20
- Visit the [Wiki](../../wiki/Installation) for more information.
29
+ The [Wiki](https://github.com/linode/linode-cli/wiki/Installation) covers other installation methods, including the Docker image, the GitHub Action, and building from source.
21
30
 
22
31
  ## Contributing
23
32
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linode-cli
3
- Version: 5.68.0
3
+ Version: 5.69.0
4
4
  Summary: The official command-line interface for interacting with the Linode API.
5
5
  Author-email: "Akamai Technologies Inc." <developers@linode.com>
6
6
  License-Expression: BSD-3-Clause
@@ -45,12 +45,21 @@ Visit the [Wiki](../../wiki) for more information.
45
45
 
46
46
  ## Install
47
47
 
48
- Install via PyPI:
48
+ We recommend installing `linode-cli` with `pipx`, which installs each Python CLI tool into its own isolated environment and works on distributions where `pip install` fails because the system Python is marked as externally managed ([PEP 668](https://peps.python.org/pep-0668/)). If pipx isn't installed yet, follow the [pipx installation guide](https://pipx.pypa.io/latest/how-to/install-pipx.html).
49
+
50
+ To install:
51
+
52
+ ```bash
53
+ pipx install linode-cli
54
+ ```
55
+
56
+ To upgrade:
57
+
49
58
  ```bash
50
- pip3 install linode-cli
59
+ pipx upgrade linode-cli
51
60
  ```
52
61
 
53
- Visit the [Wiki](../../wiki/Installation) for more information.
62
+ The [Wiki](https://github.com/linode/linode-cli/wiki/Installation) covers other installation methods, including the Docker image, the GitHub Action, and building from source.
54
63
 
55
64
  ## Contributing
56
65
 
@@ -182,12 +182,20 @@ def _parse_request_model(
182
182
  depth=depth,
183
183
  )
184
184
 
185
- # Handle arrays of objects that not marked as JSON
185
+ # Handle arrays of objects that are not marked as JSON.
186
+ # NOTE: We only expand an array of objects into individual child
187
+ # arguments when it is not already nested under another list
188
+ # (i.e. parent is None). The CLI can only associate one level of
189
+ # nested list objects, so a list of objects nested within another
190
+ # list must be treated as JSON instead.
191
+ # Otherwise, we would generate child arguments that can never
192
+ # be used because they always conflict with their implicit JSON parent.
186
193
  elif (
187
194
  v.type == "array"
188
195
  and v.items
189
196
  and v.items.type == "object"
190
197
  and v.extensions.get("linode-cli-format") != "json"
198
+ and parent is None
191
199
  ):
192
200
  # handle lists of objects as a special case, where each property
193
201
  # of the object in the list is its own argument
@@ -210,8 +210,10 @@ def _parse_response_model(schema, prefix=None, nested_list_depth=0):
210
210
  )
211
211
  elif v.type == "object":
212
212
  attrs += _parse_response_model(v, prefix=pref)
213
- elif v.type == "array" and v.items.type == "object":
214
- # Parse arrays for objects recursively and increase the nesting depth
213
+ elif v.type == "array" and (
214
+ v.items.type == "object"
215
+ or bool(_aggregate_schema_properties(v.items)[0])
216
+ ):
215
217
  attrs += _parse_response_model(
216
218
  v.items,
217
219
  prefix=pref,
@@ -316,8 +318,15 @@ class OpenAPIResponse:
316
318
  for cur in json:
317
319
  # Get the nested list using the path
318
320
  nlist_path = cur
321
+
319
322
  for p in path_parts:
320
323
  nlist_path = nlist_path.get(p)
324
+
325
+ # To avoid errors when nested list is not documented in
326
+ # the OpenAPI spec, but it is present in the API response
327
+ if nlist_path is None:
328
+ continue
329
+
321
330
  nlist = nlist_path
322
331
 
323
332
  # For each item in the nested list,
@@ -8,6 +8,67 @@ from typing import Any, Dict, List, Set, Tuple
8
8
 
9
9
  from openapi3.schemas import Schema
10
10
 
11
+ # The maximum schema nesting depth `_schema_richness` will traverse before
12
+ # bailing out. This is purely a safety valve to guarantee termination on
13
+ # self-referential or pathologically deep schemas (which the recursion would
14
+ # otherwise follow forever); it is far deeper than any real Linode API response
15
+ # model, so it never affects scoring in practice.
16
+ _MAX_RICHNESS_DEPTH = 32
17
+
18
+
19
+ def _schema_richness(schema: Any, _depth: int = 0) -> int:
20
+ """
21
+ Estimates how complete a schema definition is, used to decide which
22
+ definition to keep when the same property appears in multiple composition
23
+ (oneOf/allOf/anyOf) branches.
24
+
25
+ A branch that nulls a property out (e.g. ``{"type": "object", "nullable":
26
+ true}`` with no properties) should never overwrite a branch that fully
27
+ defines that property's nested structure. The score is a recursive measure
28
+ of how much structure a schema actually contains, so a fuller definition always outscores a
29
+ sparser one regardless of the
30
+ order the branches appear in.
31
+
32
+ :param schema: The schema (or raw schema dict) to score.
33
+ :return: A non-negative integer; higher means more complete.
34
+ """
35
+
36
+ # Guard against pathologically deep or self-referential schemas.
37
+ if _depth > _MAX_RICHNESS_DEPTH:
38
+ return 0
39
+
40
+ def get(source: Any, attr: str) -> Any:
41
+ if isinstance(source, dict):
42
+ return source.get(attr)
43
+ return getattr(source, attr, None)
44
+
45
+ score = 0
46
+
47
+ # Count each defined property, plus the richness of its own definition so
48
+ # that deeply-nested structure contributes to the total.
49
+ properties = get(schema, "properties")
50
+ if properties:
51
+ for _, prop in properties.items():
52
+ score += 1 + _schema_richness(prop, _depth + 1)
53
+
54
+ # Account for composite (oneOf/allOf/anyOf) definitions by summing the
55
+ # richness of each branch.
56
+ for composition_field in ("oneOf", "allOf", "anyOf"):
57
+ for branch in get(schema, composition_field) or []:
58
+ score += 1 + _schema_richness(branch, _depth + 1)
59
+
60
+ # Account for array item schemas so arrays of objects are scored by their
61
+ # element structure.
62
+ array_items = get(schema, "items")
63
+ if array_items is not None:
64
+ score += _schema_richness(array_items, _depth + 1)
65
+
66
+ additional_properties = get(schema, "additionalProperties")
67
+ if additional_properties is not None:
68
+ score += 1 + _schema_richness(additional_properties, _depth + 1)
69
+
70
+ return score
71
+
11
72
 
12
73
  def _aggregate_schema_properties(
13
74
  schema: Schema,
@@ -48,7 +109,20 @@ def _aggregate_schema_properties(
48
109
  return
49
110
 
50
111
  # This is a valid option
51
- properties.update(entry.properties)
112
+ for key, value in entry.properties.items():
113
+ # When the same property is defined in multiple composition
114
+ # branches (e.g. a oneOf of interface variants that each define
115
+ # `public`, `vpc`, `vlan`, etc.), keep the most complete
116
+ # definition instead of letting a later, emptier branch overwrite
117
+ # it. Otherwise nested fields like `public.ipv6.ranges.range`
118
+ # would be silently dropped when a subsequent branch nulls the
119
+ # property out.
120
+ if key in properties and _schema_richness(
121
+ value
122
+ ) <= _schema_richness(properties[key]):
123
+ continue
124
+
125
+ properties[key] = value
52
126
 
53
127
  nonlocal schema_count
54
128
  schema_count += 1
@@ -214,11 +214,12 @@ def _get_token_terminal(base_url: str) -> Tuple[str, str]:
214
214
  :returns: A tuple containing the user's username and token.
215
215
  :rtype: Tuple[str, str]
216
216
  """
217
- print(f"""
218
- First, we need a Personal Access Token. To get one, please visit
219
- {TOKEN_GENERATION_URL} and click
220
- "Create a Personal Access Token". The CLI needs access to everything
221
- on your account to work correctly.""")
217
+ print(
218
+ "First, we need a Personal Access Token. To get one, please visit\n"
219
+ f"{TOKEN_GENERATION_URL} and click\n"
220
+ '"Create a Personal Access Token". The CLI needs access to everything\n'
221
+ "on your account to work correctly."
222
+ )
222
223
 
223
224
  while True:
224
225
  token = input("Personal Access Token: ")
@@ -97,9 +97,10 @@ def _check_browsers() -> bool:
97
97
 
98
98
  # pylint: disable-next=protected-access
99
99
  if not KNOWN_GOOD_BROWSERS.intersection(webbrowser._tryorder):
100
- print("""
101
- This tool defaults to web-based authentication,
102
- however no known-working browsers were found.""")
100
+ print(
101
+ "This tool defaults to web-based authentication,\n"
102
+ "however no known-working browsers were found."
103
+ )
103
104
  while True:
104
105
  r = input("Try it anyway? [y/N]: ")
105
106
  if r.lower() in "yn ":
Binary file
@@ -8,6 +8,7 @@ Usage:
8
8
 
9
9
  import argparse
10
10
  import base64
11
+ import os
11
12
  import sys
12
13
  from pathlib import Path
13
14
 
@@ -19,6 +20,11 @@ from linodecli.plugins import inherit_plugin_args
19
20
 
20
21
  PLUGIN_BASE = "linode-cli get-kubeconfig"
21
22
 
23
+ # Kubeconfigs contain credentials, so they should only be
24
+ # accessible by the user that created them.
25
+ KUBECONFIG_FILE_MODE = 0o600
26
+ KUBECONFIG_DIR_MODE = 0o700
27
+
22
28
 
23
29
  def call(args, context):
24
30
  """
@@ -147,8 +153,27 @@ def _load_config(filepath):
147
153
 
148
154
  # Dumps data to a yaml file
149
155
  def _dump_config(filepath, data):
150
- Path.mkdir(filepath.parent, exist_ok=True)
151
- with open(filepath, "w", encoding="utf-8") as file_descriptor:
156
+ filepath.parent.mkdir(mode=KUBECONFIG_DIR_MODE, parents=True, exist_ok=True)
157
+
158
+ # Create the file with restrictive permissions rather than chmod-ing it
159
+ # afterwards, so its contents are never briefly readable by other users.
160
+ # NOTE: The mode is only applied when the file is created.
161
+ def opener(path, flags):
162
+ return os.open(path, flags, mode=KUBECONFIG_FILE_MODE)
163
+
164
+ with open(
165
+ filepath, "w", encoding="utf-8", opener=opener
166
+ ) as file_descriptor:
167
+ # Tighten the permissions of pre-existing files that are readable or
168
+ # writable by users other than the owner.
169
+ # NOTE: os.fchmod is not available on Windows, where POSIX file modes
170
+ # are not meaningful anyway.
171
+ if (
172
+ hasattr(os, "fchmod")
173
+ and os.fstat(file_descriptor.fileno()).st_mode & 0o077
174
+ ):
175
+ os.fchmod(file_descriptor.fileno(), KUBECONFIG_FILE_MODE)
176
+
152
177
  yaml.dump(data, file_descriptor)
153
178
 
154
179
 
@@ -2,4 +2,4 @@
2
2
  The version of the Linode CLI.
3
3
  """
4
4
 
5
- __version__ = "v5.68.0"
5
+ __version__ = "v5.69.0"
Binary file
File without changes
File without changes
File without changes
File without changes
File without changes