linode-cli 5.63.0__tar.gz → 5.65.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 (55) hide show
  1. {linode_cli-5.63.0/linode_cli.egg-info → linode_cli-5.65.0}/PKG-INFO +2 -1
  2. {linode_cli-5.63.0 → linode_cli-5.65.0/linode_cli.egg-info}/PKG-INFO +2 -1
  3. {linode_cli-5.63.0 → linode_cli-5.65.0}/linode_cli.egg-info/requires.txt +1 -0
  4. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/configuration/config.py +57 -12
  5. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/data-3 +0 -0
  6. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/get-kubeconfig.py +5 -2
  7. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/obj/__init__.py +231 -13
  8. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/version.py +1 -1
  9. {linode_cli-5.63.0 → linode_cli-5.65.0}/pyproject.toml +2 -1
  10. {linode_cli-5.63.0 → linode_cli-5.65.0}/LICENSE +0 -0
  11. {linode_cli-5.63.0 → linode_cli-5.65.0}/MANIFEST.in +0 -0
  12. {linode_cli-5.63.0 → linode_cli-5.65.0}/README.md +0 -0
  13. {linode_cli-5.63.0 → linode_cli-5.65.0}/linode_cli.egg-info/SOURCES.txt +0 -0
  14. {linode_cli-5.63.0 → linode_cli-5.65.0}/linode_cli.egg-info/dependency_links.txt +0 -0
  15. {linode_cli-5.63.0 → linode_cli-5.65.0}/linode_cli.egg-info/entry_points.txt +0 -0
  16. {linode_cli-5.63.0 → linode_cli-5.65.0}/linode_cli.egg-info/top_level.txt +0 -0
  17. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/__init__.py +0 -0
  18. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/__main__.py +0 -0
  19. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/api_request.py +0 -0
  20. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/arg_helpers.py +0 -0
  21. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/baked/__init__.py +0 -0
  22. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/baked/operation.py +0 -0
  23. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/baked/parsing.py +0 -0
  24. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/baked/request.py +0 -0
  25. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/baked/response.py +0 -0
  26. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/baked/util.py +0 -0
  27. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/cli.py +0 -0
  28. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/completion.py +0 -0
  29. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/configuration/__init__.py +0 -0
  30. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/configuration/auth.py +0 -0
  31. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/configuration/helpers.py +0 -0
  32. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/exit_codes.py +0 -0
  33. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/help_formatter.py +0 -0
  34. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/help_pages.py +0 -0
  35. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/helpers.py +0 -0
  36. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/oauth-landing-page.html +0 -0
  37. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/output/__init__.py +0 -0
  38. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/output/helpers.py +0 -0
  39. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/output/output_handler.py +0 -0
  40. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/overrides.py +0 -0
  41. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/__init__.py +0 -0
  42. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/firewall-editor.py +0 -0
  43. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/image-upload.py +0 -0
  44. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/metadata.py +0 -0
  45. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/obj/buckets.py +0 -0
  46. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/obj/config.py +0 -0
  47. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/obj/helpers.py +0 -0
  48. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/obj/list.py +0 -0
  49. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/obj/objects.py +0 -0
  50. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/obj/website.py +0 -0
  51. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/plugins.py +0 -0
  52. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/region-table.py +0 -0
  53. {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/ssh.py +0 -0
  54. {linode_cli-5.63.0 → linode_cli-5.65.0}/setup.cfg +0 -0
  55. {linode_cli-5.63.0 → linode_cli-5.65.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linode-cli
3
- Version: 5.63.0
3
+ Version: 5.65.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
@@ -14,6 +14,7 @@ Requires-Dist: packaging
14
14
  Requires-Dist: rich
15
15
  Requires-Dist: urllib3<3
16
16
  Requires-Dist: linode-metadata>=0.3.0
17
+ Requires-Dist: pytimeparse
17
18
  Provides-Extra: obj
18
19
  Requires-Dist: boto3>=1.36.0; extra == "obj"
19
20
  Provides-Extra: dev
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linode-cli
3
- Version: 5.63.0
3
+ Version: 5.65.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
@@ -14,6 +14,7 @@ Requires-Dist: packaging
14
14
  Requires-Dist: rich
15
15
  Requires-Dist: urllib3<3
16
16
  Requires-Dist: linode-metadata>=0.3.0
17
+ Requires-Dist: pytimeparse
17
18
  Provides-Extra: obj
18
19
  Requires-Dist: boto3>=1.36.0; extra == "obj"
19
20
  Provides-Extra: dev
@@ -5,6 +5,7 @@ packaging
5
5
  rich
6
6
  urllib3<3
7
7
  linode-metadata>=0.3.0
8
+ pytimeparse
8
9
 
9
10
  [dev]
10
11
  pylint>=2.17.4
@@ -5,7 +5,7 @@ Contains logic for loading, updating, and saving Linode CLI configurations.
5
5
  import argparse
6
6
  import os
7
7
  import sys
8
- from typing import Any, Dict, List, Optional
8
+ from typing import Any, Dict, List, Optional, Type, TypeVar, cast
9
9
 
10
10
  from linodecli.exit_codes import ExitCodes
11
11
 
@@ -27,6 +27,8 @@ from .helpers import (
27
27
 
28
28
  ENV_TOKEN_NAME = "LINODE_CLI_TOKEN"
29
29
 
30
+ T = TypeVar("T")
31
+
30
32
 
31
33
  class CLIConfig:
32
34
  """
@@ -216,15 +218,12 @@ class CLIConfig:
216
218
  :param value: The value to set for this key
217
219
  :type value: any
218
220
  """
219
- if self.running_plugin is None:
220
- raise RuntimeError(
221
- "No running plugin to retrieve configuration for!"
222
- )
223
-
224
221
  username = self.username or self.default_username()
225
- self.config.set(username, f"plugin-{self.running_plugin}-{key}", value)
222
+ self.config.set(username, self._get_plugin_key(key), value)
226
223
 
227
- def plugin_get_value(self, key: str) -> Optional[Any]:
224
+ def plugin_get_value(
225
+ self, key: str, default: Optional[T] = None, value_type: Type[T] = str
226
+ ) -> Optional[T]:
228
227
  """
229
228
  Retrieves and returns a config value previously set for a plugin. Your
230
229
  plugin should have set this value in the past. If this value does not
@@ -235,18 +234,54 @@ class CLIConfig:
235
234
  :param key: The key of the value to return
236
235
  :type key: str
237
236
 
237
+ :param default: The default value to return if the key is not set
238
+ :type default: T
239
+
240
+ :param value_type: The type to which the value should be cast
241
+ :type value_type: Type[T]
242
+
238
243
  :returns: The value for this plugin for this key, or None if not set
239
244
  :rtype: any
240
245
  """
246
+ username = self.username or self.default_username() or "DEFAULT"
247
+ value = self.config.get(
248
+ username, self._get_plugin_key(key), fallback=None
249
+ )
250
+ if value is None:
251
+ return default
252
+
253
+ if value_type == str:
254
+ return value
255
+
256
+ if value_type == bool:
257
+ bool_value = self.parse_boolean(value)
258
+ return bool_value if bool_value is not None else default
259
+
260
+ try:
261
+ return cast(T, value_type(value))
262
+ except (ValueError, TypeError):
263
+ print(
264
+ f"Could not cast config value {value} to {value_type}.",
265
+ file=sys.stderr,
266
+ )
267
+ return default
268
+
269
+ def plugin_remove_option(self, key: str):
270
+ """
271
+ Removes a plugin configuration option.
272
+
273
+ :param key: The key of the option to remove
274
+ """
275
+ username = self.username or self.default_username()
276
+ self.config.remove_option(username, self._get_plugin_key(key))
277
+
278
+ def _get_plugin_key(self, key: str) -> str:
241
279
  if self.running_plugin is None:
242
280
  raise RuntimeError(
243
281
  "No running plugin to retrieve configuration for!"
244
282
  )
245
283
 
246
- username = self.username or self.default_username() or "DEFAULT"
247
- full_key = f"plugin-{self.running_plugin}-{key}"
248
-
249
- return self.config.get(username, full_key, fallback=None)
284
+ return f"plugin-{self.running_plugin}-{key}"
250
285
 
251
286
  # TODO: this is more of an argparsing function than it is a config function
252
287
  # might be better to move this to argparsing during refactor and just have
@@ -654,3 +689,13 @@ class CLIConfig:
654
689
  if (self.config.has_section("custom_aliases"))
655
690
  else {}
656
691
  )
692
+
693
+ def parse_boolean(self, value: str) -> Optional[bool]:
694
+ """
695
+ Parses a string config value into a boolean. Returns None if the value
696
+ cannot be parsed as a boolean.
697
+
698
+ :param value: The string value to parse.
699
+ :return: The parsed boolean value.
700
+ """
701
+ return self.config.BOOLEAN_STATES.get(value.lower(), None)
@@ -15,6 +15,7 @@ import yaml
15
15
 
16
16
  from linodecli.exit_codes import ExitCodes
17
17
  from linodecli.help_formatter import SortingHelpFormatter
18
+ from linodecli.plugins import inherit_plugin_args
18
19
 
19
20
  PLUGIN_BASE = "linode-cli get-kubeconfig"
20
21
 
@@ -23,8 +24,10 @@ def call(args, context):
23
24
  """
24
25
  The entrypoint for this plugin
25
26
  """
26
- parser = argparse.ArgumentParser(
27
- PLUGIN_BASE, add_help=True, formatter_class=SortingHelpFormatter
27
+ parser = inherit_plugin_args(
28
+ argparse.ArgumentParser(
29
+ PLUGIN_BASE, add_help=True, formatter_class=SortingHelpFormatter
30
+ )
28
31
  )
29
32
 
30
33
  group = parser.add_mutually_exclusive_group()
@@ -4,6 +4,7 @@ CLI Plugin for handling OBJ
4
4
  """
5
5
  import getpass
6
6
  import os
7
+ import re
7
8
  import socket
8
9
  import sys
9
10
  import time
@@ -11,13 +12,13 @@ from argparse import ArgumentParser
11
12
  from contextlib import suppress
12
13
  from datetime import datetime
13
14
  from math import ceil
14
- from typing import List
15
+ from typing import List, Optional
15
16
 
17
+ from pytimeparse import parse as parse_time
16
18
  from rich import print as rprint
17
19
  from rich.table import Table
18
20
 
19
21
  from linodecli.cli import CLI
20
- from linodecli.configuration import _do_get_request
21
22
  from linodecli.configuration.helpers import _default_text_input
22
23
  from linodecli.exit_codes import ExitCodes
23
24
  from linodecli.help_formatter import SortingHelpFormatter
@@ -65,6 +66,17 @@ except ImportError:
65
66
  HAS_BOTO = False
66
67
 
67
68
 
69
+ CLUSTER_KEY = "cluster"
70
+ KEY_CLEANUP_ENABLED_KEY = "key-cleanup-enabled"
71
+ KEY_LIFESPAN_KEY = "key-lifespan"
72
+ KEY_ROTATION_PERIOD_KEY = "key-rotation-period"
73
+ KEY_CLEANUP_BATCH_SIZE_KEY = "key-cleanup-batch-size"
74
+ LAST_KEY_CLEANUP_TIMESTAMP_KEY = "last-key-cleanup-timestamp"
75
+ ACCESS_KEY_KEY = "access-key"
76
+ SECRET_KEY_KEY = "secret-key"
77
+ TOKEN_KEY = "token"
78
+
79
+
68
80
  def generate_url(get_client, args, **kwargs): # pylint: disable=unused-argument
69
81
  """
70
82
  Generates a URL to an object
@@ -314,13 +326,44 @@ def get_obj_args_parser():
314
326
  metavar="COMMAND",
315
327
  nargs="?",
316
328
  type=str,
317
- help="The command to execute in object storage",
329
+ help="The command to execute in object storage.",
318
330
  )
319
331
  parser.add_argument(
320
332
  "--cluster",
321
333
  metavar="CLUSTER",
322
334
  type=str,
323
- help="The cluster to use for the operation",
335
+ help="The cluster to use for the operation.",
336
+ )
337
+ parser.add_argument(
338
+ "--force-key-cleanup",
339
+ action="store_true",
340
+ help="Performs cleanup of old linode-cli generated Object Storage keys"
341
+ " before executing the Object Storage command. It overrides"
342
+ " the --perform-key-cleanup option.",
343
+ )
344
+ parser.add_argument(
345
+ "--key-cleanup-enabled",
346
+ choices=["yes", "no"],
347
+ help="If set to 'yes', performs cleanup of old linode-cli generated Object Storage"
348
+ " keys before executing the Object Storage command. Cleanup occurs"
349
+ " at most once every 24 hours.",
350
+ )
351
+ parser.add_argument(
352
+ "--key-lifespan",
353
+ type=str,
354
+ help="Specifies the lifespan of linode-cli generated Object Storage keys"
355
+ " (e.g. 30d for 30 days). Used only during key cleanup.",
356
+ )
357
+ parser.add_argument(
358
+ "--key-rotation-period",
359
+ type=str,
360
+ help="Specifies the period after which the linode-cli generated Object Storage"
361
+ " key must be rotated (e.g. 10d for 10 days). Used only during key cleanup.",
362
+ )
363
+ parser.add_argument(
364
+ "--key-cleanup-batch-size",
365
+ type=int,
366
+ help="Number of old linode-cli generated Object Storage keys to clean up at once.",
324
367
  )
325
368
 
326
369
  return parser
@@ -400,8 +443,9 @@ def call(
400
443
  access_key = None
401
444
  secret_key = None
402
445
 
403
- # make a client, but only if we weren't printing help
446
+ # make a client and clean-up keys, but only if we weren't printing help
404
447
  if not is_help:
448
+ _cleanup_keys(context.client, parsed)
405
449
  access_key, secret_key = get_credentials(context.client)
406
450
 
407
451
  cluster = parsed.cluster
@@ -497,8 +541,8 @@ def _get_s3_creds(client: CLI, force: bool = False):
497
541
  :returns: The access key and secret key for this user
498
542
  :rtype: tuple(str, str)
499
543
  """
500
- access_key = client.config.plugin_get_value("access-key")
501
- secret_key = client.config.plugin_get_value("secret-key")
544
+ access_key = client.config.plugin_get_value(ACCESS_KEY_KEY)
545
+ secret_key = client.config.plugin_get_value(SECRET_KEY_KEY)
502
546
 
503
547
  if force or access_key is None:
504
548
  # this means there are no stored s3 creds for this user - set them up
@@ -507,7 +551,7 @@ def _get_s3_creds(client: CLI, force: bool = False):
507
551
  # being provided by the environment, but if the CLI is running without a
508
552
  # config, we shouldn't generate new keys (or we'd end up doing so with each
509
553
  # request) - instead ask for them to be set up.
510
- if client.config.get_value("token") is None:
554
+ if client.config.get_value(TOKEN_KEY) is None:
511
555
  print(
512
556
  "You are running the Linode CLI without a configuration file, but "
513
557
  "object storage keys were not configured. "
@@ -571,8 +615,8 @@ def _get_s3_creds(client: CLI, force: bool = False):
571
615
  access_key = resp["access_key"]
572
616
  secret_key = resp["secret_key"]
573
617
 
574
- client.config.plugin_set_value("access-key", access_key)
575
- client.config.plugin_set_value("secret-key", secret_key)
618
+ client.config.plugin_set_value(ACCESS_KEY_KEY, access_key)
619
+ client.config.plugin_set_value(SECRET_KEY_KEY, secret_key)
576
620
  client.config.write_config()
577
621
 
578
622
  return access_key, secret_key
@@ -580,14 +624,188 @@ def _get_s3_creds(client: CLI, force: bool = False):
580
624
 
581
625
  def _configure_plugin(client: CLI):
582
626
  """
583
- Configures a default cluster value.
627
+ Configures Object Storage plugin.
584
628
  """
585
-
586
629
  cluster = _default_text_input( # pylint: disable=protected-access
587
630
  "Default cluster for operations (e.g. `us-mia-1`)",
588
631
  optional=False,
589
632
  )
590
633
 
591
634
  if cluster:
592
- client.config.plugin_set_value("cluster", cluster)
635
+ client.config.plugin_set_value(CLUSTER_KEY, cluster)
636
+
593
637
  client.config.write_config()
638
+
639
+
640
+ def _cleanup_keys(client: CLI, options) -> None:
641
+ """
642
+ Cleans up stale linode-cli generated object storage keys.
643
+ """
644
+
645
+ try:
646
+ current_timestamp = int(time.time())
647
+ if not _should_perform_key_cleanup(client, options, current_timestamp):
648
+ return
649
+
650
+ cleanup_message = (
651
+ "Cleaning up old linode-cli generated Object Storage keys."
652
+ )
653
+ if not options.force_key_cleanup and not options.key_cleanup_enabled:
654
+ cleanup_message += (
655
+ " To disable this, use the '--key-cleanup-enabled no' option."
656
+ )
657
+ print(cleanup_message, file=sys.stderr)
658
+
659
+ status, keys = client.call_operation("object-storage", "keys-list")
660
+ if status != 200:
661
+ print(
662
+ "Failed to list object storage keys for cleanup",
663
+ file=sys.stderr,
664
+ )
665
+ return
666
+
667
+ key_lifespan = _get_key_lifespan(client, options)
668
+ key_rotation_period = _get_key_rotation_period(client, options)
669
+ cleanup_batch_size = _get_cleanup_batch_size(client, options)
670
+
671
+ linode_cli_keys = _get_linode_cli_keys(
672
+ keys["data"], key_lifespan, key_rotation_period, current_timestamp
673
+ )
674
+
675
+ _rotate_current_key_if_needed(client, linode_cli_keys)
676
+ _delete_stale_keys(client, linode_cli_keys, cleanup_batch_size)
677
+
678
+ client.config.plugin_set_value(
679
+ LAST_KEY_CLEANUP_TIMESTAMP_KEY, str(current_timestamp)
680
+ )
681
+ client.config.write_config()
682
+
683
+ except Exception as e:
684
+ print(
685
+ f"Unable to clean up stale linode-cli Object Storage keys: {e}",
686
+ file=sys.stderr,
687
+ )
688
+
689
+
690
+ def _should_perform_key_cleanup(
691
+ client: CLI, options, current_timestamp
692
+ ) -> bool:
693
+ if options.force_key_cleanup:
694
+ return True
695
+ if not _is_key_cleanup_enabled(client, options):
696
+ return False
697
+
698
+ last_cleanup = client.config.plugin_get_value(
699
+ LAST_KEY_CLEANUP_TIMESTAMP_KEY
700
+ )
701
+
702
+ # if we did a cleanup in the last 24 hours, skip it this time
703
+ return (
704
+ last_cleanup is None
705
+ or int(last_cleanup) <= current_timestamp - 24 * 60 * 60
706
+ )
707
+
708
+
709
+ def _is_key_cleanup_enabled(client, options) -> bool:
710
+ if options.key_cleanup_enabled in ["yes", "no"]:
711
+ return options.key_cleanup_enabled == "yes"
712
+ return client.config.plugin_get_value(KEY_CLEANUP_ENABLED_KEY, True, bool)
713
+
714
+
715
+ def _get_key_lifespan(client, options) -> str:
716
+ return options.key_lifespan or client.config.plugin_get_value(
717
+ KEY_LIFESPAN_KEY, "30d"
718
+ )
719
+
720
+
721
+ def _get_key_rotation_period(client, options) -> str:
722
+ return options.key_rotation_period or client.config.plugin_get_value(
723
+ KEY_ROTATION_PERIOD_KEY, "10d"
724
+ )
725
+
726
+
727
+ def _get_cleanup_batch_size(client, options) -> int:
728
+ return options.key_cleanup_batch_size or client.config.plugin_get_value(
729
+ KEY_CLEANUP_BATCH_SIZE_KEY, 10, int
730
+ )
731
+
732
+
733
+ def _get_linode_cli_keys(
734
+ keys_data: list,
735
+ key_lifespan: str,
736
+ key_rotation_period: str,
737
+ current_timestamp: int,
738
+ ) -> list:
739
+ stale_threshold = current_timestamp - parse_time(key_lifespan)
740
+ rotation_threshold = current_timestamp - parse_time(key_rotation_period)
741
+
742
+ def extract_key_info(key: dict) -> Optional[dict]:
743
+ match = re.match(r"^linode-cli-.+@.+-(\d{10,})$", key["label"])
744
+ if not match:
745
+ return None
746
+
747
+ created_timestamp = int(match.group(1))
748
+ is_stale = created_timestamp < stale_threshold
749
+ needs_rotation = is_stale or created_timestamp <= rotation_threshold
750
+
751
+ return {
752
+ "id": key["id"],
753
+ "label": key["label"],
754
+ "access_key": key["access_key"],
755
+ "created_timestamp": created_timestamp,
756
+ "is_stale": is_stale,
757
+ "needs_rotation": needs_rotation,
758
+ }
759
+
760
+ return sorted(
761
+ [info for key in keys_data if (info := extract_key_info(key))],
762
+ key=lambda k: k["created_timestamp"],
763
+ )
764
+
765
+
766
+ def _rotate_current_key_if_needed(client: CLI, linode_cli_keys: list) -> None:
767
+ current_access_key = client.config.plugin_get_value(ACCESS_KEY_KEY)
768
+
769
+ key_to_rotate = next(
770
+ (
771
+ key_info
772
+ for key_info in linode_cli_keys
773
+ if key_info["access_key"] == current_access_key
774
+ and key_info["needs_rotation"]
775
+ ),
776
+ None,
777
+ )
778
+ if key_to_rotate:
779
+ _delete_key(client, key_to_rotate["id"], key_to_rotate["label"])
780
+ linode_cli_keys.remove(key_to_rotate)
781
+ client.config.plugin_remove_option(ACCESS_KEY_KEY)
782
+ client.config.plugin_remove_option(SECRET_KEY_KEY)
783
+ client.config.write_config()
784
+
785
+
786
+ def _delete_stale_keys(
787
+ client: CLI, linode_cli_keys: list, batch_size: int
788
+ ) -> None:
789
+ stale_keys = [k for k in linode_cli_keys if k["is_stale"]]
790
+ for key_info in stale_keys[:batch_size]:
791
+ _delete_key(client, key_info["id"], key_info["label"])
792
+
793
+
794
+ def _delete_key(client: CLI, key_id: str, label: str) -> None:
795
+ try:
796
+ print(
797
+ f"Deleting linode-cli Object Storage key: {label}", file=sys.stderr
798
+ )
799
+ status, _ = client.call_operation(
800
+ "object-storage", "keys-delete", [str(key_id)]
801
+ )
802
+ if status != 200:
803
+ print(
804
+ f"Failed to delete key: {label}; status {status}",
805
+ file=sys.stderr,
806
+ )
807
+ except Exception as e:
808
+ print(
809
+ f"Exception occurred while deleting key: {label}; {e}",
810
+ file=sys.stderr,
811
+ )
@@ -2,4 +2,4 @@
2
2
  The version of the Linode CLI.
3
3
  """
4
4
 
5
- __version__ = "v5.63.0"
5
+ __version__ = "v5.65.0"
@@ -17,7 +17,8 @@ dependencies = [
17
17
  "packaging",
18
18
  "rich",
19
19
  "urllib3<3",
20
- "linode-metadata>=0.3.0"
20
+ "linode-metadata>=0.3.0",
21
+ "pytimeparse"
21
22
  ]
22
23
  dynamic = ["version"]
23
24
 
File without changes
File without changes
File without changes
File without changes
File without changes