oks-cli 1.15__tar.gz → 1.16__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 (26) hide show
  1. {oks_cli-1.15 → oks_cli-1.16}/PKG-INFO +2 -2
  2. {oks_cli-1.15 → oks_cli-1.16}/oks_cli/cache.py +14 -13
  3. {oks_cli-1.15 → oks_cli-1.16}/oks_cli/cluster.py +114 -66
  4. {oks_cli-1.15 → oks_cli-1.16}/oks_cli/main.py +7 -8
  5. {oks_cli-1.15 → oks_cli-1.16}/oks_cli/profile.py +5 -5
  6. {oks_cli-1.15 → oks_cli-1.16}/oks_cli/project.py +28 -25
  7. {oks_cli-1.15 → oks_cli-1.16}/oks_cli/quotas.py +1 -1
  8. {oks_cli-1.15 → oks_cli-1.16}/oks_cli/utils.py +61 -9
  9. {oks_cli-1.15 → oks_cli-1.16}/oks_cli.egg-info/PKG-INFO +2 -2
  10. {oks_cli-1.15 → oks_cli-1.16}/oks_cli.egg-info/requires.txt +1 -1
  11. {oks_cli-1.15 → oks_cli-1.16}/setup.py +2 -2
  12. {oks_cli-1.15 → oks_cli-1.16}/tests/test_cache.py +1 -1
  13. {oks_cli-1.15 → oks_cli-1.16}/tests/test_cluster.py +39 -3
  14. {oks_cli-1.15 → oks_cli-1.16}/tests/test_project.py +21 -2
  15. {oks_cli-1.15 → oks_cli-1.16}/LICENSE +0 -0
  16. {oks_cli-1.15 → oks_cli-1.16}/README.md +0 -0
  17. {oks_cli-1.15 → oks_cli-1.16}/oks_cli/__init__.py +0 -0
  18. {oks_cli-1.15 → oks_cli-1.16}/oks_cli.egg-info/SOURCES.txt +0 -0
  19. {oks_cli-1.15 → oks_cli-1.16}/oks_cli.egg-info/dependency_links.txt +0 -0
  20. {oks_cli-1.15 → oks_cli-1.16}/oks_cli.egg-info/entry_points.txt +0 -0
  21. {oks_cli-1.15 → oks_cli-1.16}/oks_cli.egg-info/top_level.txt +0 -0
  22. {oks_cli-1.15 → oks_cli-1.16}/setup.cfg +0 -0
  23. {oks_cli-1.15 → oks_cli-1.16}/tests/test_nodepool.py +0 -0
  24. {oks_cli-1.15 → oks_cli-1.16}/tests/test_profile.py +0 -0
  25. {oks_cli-1.15 → oks_cli-1.16}/tests/test_quota.py +0 -0
  26. {oks_cli-1.15 → oks_cli-1.16}/tests/test_shell_completion.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oks-cli
3
- Version: 1.15
3
+ Version: 1.16
4
4
  Author: Outscale SAS
5
5
  Author-email: opensource@outscale.com
6
6
  License: BSD
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: certifi>=2024.8.30
19
19
  Requires-Dist: charset-normalizer>=3.3.2
20
- Requires-Dist: click>=8.1.7
20
+ Requires-Dist: click<8.3.0,>=8.1.7
21
21
  Requires-Dist: colorama>=0.4.6
22
22
  Requires-Dist: idna>=3.10
23
23
  Requires-Dist: pyyaml>=6.0.2
@@ -1,11 +1,13 @@
1
1
  import click
2
- from .utils import clear_cache, find_project_id_by_name, find_cluster_id_by_name, get_all_cache, get_expiration_date, ctx_update, login_profile, profile_completer, cluster_completer, project_completer
2
+ from .utils import clear_cache, find_project_id_by_name, find_cluster_id_by_name, get_all_cache, get_expiration_date, \
3
+ ctx_update, login_profile, profile_completer, cluster_completer, project_completer, print_table
4
+
3
5
  import prettytable
4
6
 
5
7
  # DEFINE THE CACHE COMMAND GROUP
6
8
  @click.group(help="Cache related commands.")
7
9
  @click.option('--project-name', '-p', required = False, help="Project Name", shell_complete=project_completer)
8
- @click.option('--cluster-name', '-c', required = False, help="Cluster Name", shell_complete=cluster_completer)
10
+ @click.option('--cluster-name', '--name', '-c', required = False, help="Cluster Name", shell_complete=cluster_completer)
9
11
  @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
10
12
  @click.pass_context
11
13
  def cache(ctx, project_name, cluster_name, profile):
@@ -21,7 +23,7 @@ def delete_cache(force):
21
23
 
22
24
  @cache.command('kubeconfigs', help="List cached kubeconfigs")
23
25
  @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
24
- @click.option('--cluster-name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
26
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
25
27
  @click.option('--plain', is_flag=True, help="Plain table format")
26
28
  @click.option('--msword', is_flag=True, help="Microsoft Word table format")
27
29
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
@@ -36,14 +38,8 @@ def list_kubeconfigs(ctx, project_name, cluster_name, plain, msword, profile):
36
38
 
37
39
  result = get_all_cache(project_id, cluster_id, "kubeconfig")
38
40
 
39
- table = prettytable.PrettyTable()
40
- table.field_names = ["user", "group", "expiration date"]
41
-
42
- if plain:
43
- table.set_style(prettytable.PLAIN_COLUMNS)
44
-
45
- if msword:
46
- table.set_style(prettytable.MSWORD_FRIENDLY)
41
+ data = list()
42
+ fields = [["user", "user"],["group", "group"], ["expiration date", "expires_at"]]
47
43
 
48
44
  for element in result:
49
45
  kubeconfig = None
@@ -57,7 +53,12 @@ def list_kubeconfigs(ctx, project_name, cluster_name, plain, msword, profile):
57
53
  if kubeconfig:
58
54
  exp = get_expiration_date(kubeconfig)
59
55
  row = user, group, exp
56
+ data.append({"user": user, "group": group, "expires_at": exp})
60
57
 
61
- table.add_row(row)
58
+ style = None
59
+ if plain:
60
+ style = prettytable.PLAIN_COLUMNS
61
+ if msword:
62
+ style = prettytable.MSWORD_FRIENDLY
62
63
 
63
- click.echo(table)
64
+ print_table(data, fields, style=style)
@@ -6,23 +6,31 @@ from nacl.encoding import Base64Encoder
6
6
 
7
7
  import time
8
8
  import os
9
- import datetime
9
+ from datetime import datetime
10
10
  import dateutil.parser
11
11
  import human_readable
12
+ import pathlib
12
13
  import prettytable
13
14
  import logging
14
-
15
- from .utils import cluster_completer, do_request, print_output, find_project_id_by_name, find_cluster_id_by_name, get_cache, save_cache, detect_and_parse_input, verify_certificate, shell_completions, transform_tuple, profile_list, login_profile, cluster_create_in_background, ctx_update, set_cluster_id, get_cluster_id, get_project_id, get_template, get_cluster_name, format_changed_row, is_interesting_status, profile_completer, project_completer
15
+ import yaml
16
+
17
+ from .utils import cluster_completer, do_request, print_output, \
18
+ find_project_id_by_name, find_cluster_id_by_name, \
19
+ get_cache, save_cache, detect_and_parse_input, \
20
+ verify_certificate, shell_completions, transform_tuple, \
21
+ profile_list, login_profile, cluster_create_in_background, \
22
+ ctx_update, set_cluster_id, get_cluster_id, get_project_id, \
23
+ get_template, get_cluster_name, format_changed_row, \
24
+ is_interesting_status, profile_completer, project_completer, \
25
+ kubeconfig_parse_fields, print_table, get_expiration_date
16
26
 
17
27
  from .profile import add_profile
18
28
  from .project import project_create, project_login
19
29
 
20
30
  # DEFINE THE CLUSTER GROUP
21
31
  @click.group(help="Cluster related commands.")
22
- @click.option('--project', 'project_name', required = False, help="Project Name")
23
- @click.option('--project-name', '-p', required = False, help="Project Name", shell_complete=project_completer)
24
- @click.option('--name', 'cluster_name', required = False, help="Cluster Name")
25
- @click.option('--cluster-name', '-c', required = False, help="Cluster Name", shell_complete=cluster_completer)
32
+ @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
33
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
26
34
  @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
27
35
  @click.pass_context
28
36
  def cluster(ctx, project_name, cluster_name, profile):
@@ -31,7 +39,7 @@ def cluster(ctx, project_name, cluster_name, profile):
31
39
 
32
40
  # LOGIN ON CLUSTER
33
41
  @cluster.command('login', help="Set a default cluster")
34
- @click.option('--cluster-name', '-c', required=False, help="Name of cluster", shell_complete=cluster_completer)
42
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Name of cluster", shell_complete=cluster_completer)
35
43
  @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
36
44
  @click.pass_context
37
45
  def cluster_login(ctx, cluster_name, profile):
@@ -71,13 +79,12 @@ def cluster_logout(ctx, profile):
71
79
  # LIST CLUSTERS
72
80
  @cluster.command('list', help="List all clusters")
73
81
  @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
74
- @click.option('--name', 'cluster_name', required = False, help="Cluster Name")
75
- @click.option('--cluster-name', '-c', required = False, help="Cluster Name", shell_complete=cluster_completer)
76
- @click.option('--deleted', is_flag=True, help="List deleted clusters")
82
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
83
+ @click.option('--deleted', '-x', is_flag=True, help="List deleted clusters") # x pour "deleted" / "removed"
77
84
  @click.option('--plain', is_flag=True, help="Plain table format")
78
85
  @click.option('--msword', is_flag=True, help="Microsoft Word table format")
79
86
  @click.option('--watch', '-w', is_flag=True, help="Watch the changes")
80
- @click.option('-o', '--output', type=click.Choice(["json", "yaml", "wide"]), help="Specify output format")
87
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml", "wide"]), help="Specify output format")
81
88
  @click.option('--profile', help="Configuration profile to use")
82
89
  @click.pass_context
83
90
  def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch, output, profile):
@@ -85,6 +92,8 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
85
92
  project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
86
93
  login_profile(profile)
87
94
 
95
+ profile_name = os.getenv('OKS_PROFILE')
96
+ region_name = os.getenv('OKS_REGION')
88
97
  project_id = find_project_id_by_name(project_name)
89
98
  cluster_id = get_cluster_id()
90
99
 
@@ -96,7 +105,7 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
96
105
  if deleted:
97
106
  params['deleted'] = True
98
107
 
99
- field_names = ["NAME", "CREATED", "UPDATED", "STATUS", "DEFAULT"]
108
+ field_names = ["CLUSTER", "PROFILE", "REGION", "CREATED", "UPDATED", "STATUS", "DEFAULT"]
100
109
 
101
110
  data = do_request("GET", 'clusters', params=params)
102
111
 
@@ -141,9 +150,9 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
141
150
 
142
151
  created_at = dateutil.parser.parse(cluster['statuses']['created_at'])
143
152
  updated_at = dateutil.parser.parse(cluster['statuses']['updated_at'])
144
- now = datetime.datetime.now(tz = created_at.tzinfo)
153
+ now = datetime.now(tz = created_at.tzinfo)
145
154
 
146
- row = [name, human_readable.date_time(now - created_at), human_readable.date_time(now - updated_at), msg, default]
155
+ row = [name, profile_name, region_name, human_readable.date_time(now - created_at), human_readable.date_time(now - updated_at), msg, default]
147
156
 
148
157
  if output == "wide":
149
158
  row.insert(0, cluster['id'])
@@ -215,10 +224,9 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
215
224
 
216
225
  # GET CLUSTER BY NAME
217
226
  @cluster.command('get', help="Get a cluster by name")
218
- @click.option('--project-name', '-p', required = False, help="Project Name", shell_complete=project_completer)
219
- @click.option('--name', 'cluster_name', required=False, help="Cluster Name", shell_complete=cluster_completer)
220
- @click.option('--cluster-name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
221
- @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
227
+ @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
228
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
229
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
222
230
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
223
231
  @click.pass_context
224
232
  def cluster_get_command(ctx, project_name, cluster_name, output, profile):
@@ -234,6 +242,26 @@ def cluster_get_command(ctx, project_name, cluster_name, output, profile):
234
242
  print_output(data, output)
235
243
 
236
244
 
245
+ def prepare_cluster_template(cluster_config):
246
+ cluster_template = get_template("cluster")
247
+
248
+ admin_whitelist = cluster_config.get("admin_whitelist") or []
249
+ if isinstance(admin_whitelist, str):
250
+ admin_whitelist = [admin_whitelist]
251
+
252
+ final_whitelist = []
253
+
254
+ for entry in admin_whitelist:
255
+ if entry == "my-ip":
256
+ final_whitelist.extend(cluster_template.get("admin_whitelist", []))
257
+ else:
258
+ final_whitelist.append(entry)
259
+
260
+ cluster_config["admin_whitelist"] = list(dict.fromkeys(final_whitelist))
261
+
262
+ cluster_template.update(cluster_config)
263
+ return cluster_template
264
+
237
265
  def _create_cluster(project_name, cluster_config, output):
238
266
  """Create a new cluster with interactive setup for missing profiles/projects."""
239
267
  profiles = profile_list()
@@ -274,8 +302,8 @@ def _create_cluster(project_name, cluster_config, output):
274
302
  project_name = project_name or "default"
275
303
  projects = do_request("GET", 'projects', params={"name": project_name})
276
304
 
277
- cluster_template = get_template('cluster')
278
- cluster_template.update(cluster_config)
305
+ cluster_template = prepare_cluster_template(cluster_config)
306
+ print_output(cluster_template, output)
279
307
 
280
308
  project_name_styled = click.style(project_name, bold=True)
281
309
  cluster_name_styled = click.style(cluster_template.get("name"), bold=True)
@@ -299,9 +327,7 @@ def _create_cluster(project_name, cluster_config, output):
299
327
  else:
300
328
  project_id = find_project_id_by_name(project_name)
301
329
 
302
- cluster_template = get_template('cluster')
303
- cluster_template.update(cluster_config)
304
-
330
+ cluster_template = prepare_cluster_template(cluster_config)
305
331
  do_request("GET", f'projects/{project_id}')
306
332
  cluster_template['project_id'] = project_id
307
333
 
@@ -313,22 +339,22 @@ def _create_cluster(project_name, cluster_config, output):
313
339
  @cluster.command('create', help="Create a new cluster")
314
340
  @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
315
341
  @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
316
- @click.option('--description', help="Description of the cluster")
317
- @click.option('--admin', help="Admin Whitelist")
318
- @click.option('--version', shell_complete=shell_completions, help="Kubernetes version")
342
+ @click.option('--description', '-d', help="Description of the cluster")
343
+ @click.option('--admin', '-a', help="Admin Whitelist ips. you can use 'my-ip' to automatically use your current IP.")
344
+ @click.option('--version', '-v', shell_complete=shell_completions, help="Kubernetes version")
319
345
  @click.option('--cidr-pods', help="CIDR of pods")
320
346
  @click.option('--cidr-service', help='CIDR of services')
321
347
  @click.option('--control-plane', shell_complete=shell_completions, help="Controlplane plan")
322
- @click.option('--zone', multiple=True, shell_complete=shell_completions, help="List of Control Plane availability zones")
348
+ @click.option('--zone', '-z', multiple=True, shell_complete=shell_completions, help="List of Control Plane availability zones")
323
349
  @click.option('--enable-admission-plugins', help="List of admission plugins, separated by commas")
324
350
  @click.option('--disable-admission-plugins', help="List of admission plugins, separated by commas")
325
- @click.option('--quirk', multiple=True, help="Quirk")
326
- @click.option('--tags', help="Comma-separated list of tags, example: 'key1=value1,key2=value2'")
351
+ @click.option('--quirk', '-q', multiple=True, help="Quirk")
352
+ @click.option('--tags', '-t', help="Comma-separated list of tags, example: 'key1=value1,key2=value2'")
327
353
  @click.option('--disable-api-termination', type=click.BOOL, help="Disable delete action by API")
328
- @click.option('--cp-multi-az', is_flag=True, help="enable control plane multi az")
354
+ @click.option('--cp-multi-az', '-m', is_flag=True, help="Enable control plane multi AZ")
329
355
  @click.option('--dry-run', is_flag=True, help="Client dry-run, only print the object that would be sent, without sending it")
330
- @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
331
- @click.option('-f', '--filename', type=click.File("r"), help="Path to file to use to create the cluster ")
356
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
357
+ @click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to create the cluster ")
332
358
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
333
359
  @click.pass_context
334
360
  def cluster_create_command(ctx, project_name, cluster_name, description, admin, version, cidr_pods, cidr_service, control_plane, zone, enable_admission_plugins, disable_admission_plugins, quirk, tags, disable_api_termination, cp_multi_az, dry_run, output, filename, profile):
@@ -405,27 +431,25 @@ def cluster_create_command(ctx, project_name, cluster_name, description, admin,
405
431
  if not dry_run:
406
432
  _create_cluster(project_name, cluster_config, output)
407
433
  else:
408
- cluster_template = get_template("cluster")
409
- cluster_template.update(cluster_config)
434
+ cluster_template = prepare_cluster_template(cluster_config)
410
435
  print_output(cluster_template, output)
411
436
 
412
437
  # UPDATE CLUSTER
413
438
  @cluster.command('update', help="Update a cluster by name")
414
439
  @click.option('--project-name', '-p', required=False, help="Project name", shell_complete=project_completer)
415
- @click.option('--name', 'cluster_name', required=False, help="Cluster name")
416
- @click.option('--cluster-name', '-c', required=False, help="Cluster name", shell_complete=cluster_completer)
417
- @click.option('--description', help="Description of the cluster")
418
- @click.option('--admin', help="Admin Whitelist")
419
- @click.option('--version', shell_complete=shell_completions, help="Kubernetes version")
420
- @click.option('--tags', help="Comma-separated list of tags, example: 'key1=value1,key2=value2'")
440
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster name", shell_complete=cluster_completer)
441
+ @click.option('--description', '-d', help="Description of the cluster")
442
+ @click.option('--admin', '-a', help="Admin Whitelist ips. you can use 'my-ip' to automatically use your current IP.")
443
+ @click.option('--version', '-v', shell_complete=shell_completions, help="Kubernetes version")
444
+ @click.option('--tags', '-t', help="Comma-separated list of tags, example: 'key1=value1,key2=value2'")
421
445
  @click.option('--enable-admission-plugins', help="List of admission plugins, separated by commas")
422
446
  @click.option('--disable-admission-plugins', help="List of admission plugins, separated by commas")
423
- @click.option('--quirk', multiple=True, help="Quirk")
447
+ @click.option('--quirk', '-q', multiple=True, help="Quirk")
424
448
  @click.option('--disable-api-termination', type=click.BOOL, help="Disable delete action by API")
425
449
  @click.option('--control-plane', shell_complete=shell_completions, help="Controlplane plan")
426
450
  @click.option('--dry-run', is_flag=True, help="Client dry-run, only print the object that would be sent, without sending it")
427
- @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
428
- @click.option('-f', '--filename', type=click.File("r"), help="Path to file to use to update the cluster ")
451
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
452
+ @click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to update the cluster ")
429
453
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
430
454
  @click.pass_context
431
455
  def cluster_update_command(ctx, project_name, cluster_name, description, admin, version, tags, enable_admission_plugins, disable_admission_plugins, quirk, disable_api_termination, control_plane, dry_run, output, filename, profile):
@@ -500,9 +524,8 @@ def cluster_update_command(ctx, project_name, cluster_name, description, admin,
500
524
  # UPGRADE CLUSTER
501
525
  @cluster.command('upgrade', help="Upgrade a cluster by name")
502
526
  @click.option('--project-name', '-p', required=False, help="Project name", shell_complete=project_completer)
503
- @click.option('--name', 'cluster_name', required=False, help="Cluster name")
504
- @click.option('--cluster-name', '-c', required=False, help="Cluster name", shell_complete=cluster_completer)
505
- @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
527
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster name", shell_complete=cluster_completer)
528
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
506
529
  @click.option('--force', is_flag=True, help="Force upgrade")
507
530
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
508
531
  @click.pass_context
@@ -522,9 +545,8 @@ def cluster_update_command(ctx, project_name, cluster_name, output, force, profi
522
545
  # DELETE CLUSTER BY NAME
523
546
  @cluster.command('delete', help="Delete a cluster by name")
524
547
  @click.option('--project-name', '-p', required=False, help="Project name", shell_complete=project_completer)
525
- @click.option('--name', 'cluster_name', required=False, help="Cluster name")
526
- @click.option('--cluster-name', '-c', required=False, help="Cluster name", shell_complete=cluster_completer)
527
- @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
548
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster name", shell_complete=cluster_completer)
549
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
528
550
  @click.option('--dry-run', is_flag=True, help="Run without any action")
529
551
  @click.option('--force', is_flag=True, help="Force deletion without confirmation")
530
552
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
@@ -554,9 +576,10 @@ def cluster_delete_command(ctx, project_name, cluster_name, output, dry_run, for
554
576
  # GET KUBECONFIG
555
577
  @cluster.command('kubeconfig', help="Fetch the kubeconfig for a cluster")
556
578
  @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
557
- @click.option('--name', 'cluster_name', required=False, help="Cluster name")
558
- @click.option('--cluster-name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
579
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
559
580
  @click.option('--print-path', is_flag=True, help="Print path to saved kubeconfig")
581
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml", "table"]), default="yaml", help="Specify output format, default is yaml")
582
+ @click.option('--wide', is_flag=True, help="Prints additional info, only supported for table output")
560
583
  @click.option('--refresh', '--force', is_flag=True, help="Force refresh saved kubeconfig")
561
584
  @click.option('--nacl', is_flag=True, help="Use public key encryption on wire (require api support)")
562
585
  @click.option('--user', type=click.STRING, help="User")
@@ -564,7 +587,7 @@ def cluster_delete_command(ctx, project_name, cluster_name, output, dry_run, for
564
587
  @click.option('--ttl', type=click.STRING, help="TTL in human readable format (5h, 1d, 1w)")
565
588
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
566
589
  @click.pass_context
567
- def cluster_kubeconfig_command(ctx, project_name, cluster_name, print_path, refresh, nacl, user, group, ttl, profile):
590
+ def cluster_kubeconfig_command(ctx, project_name, cluster_name, print_path, output, wide, refresh, nacl, user, group, ttl, profile):
568
591
  """CLI command to fetch and optionally print the kubeconfig for a specified cluster."""
569
592
  project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
570
593
  login_profile(profile)
@@ -619,9 +642,31 @@ def cluster_kubeconfig_command(ctx, project_name, cluster_name, print_path, refr
619
642
  kubeconfig_path = save_cache(project_id, cluster_id, 'kubeconfig', kubeconfig, user, group)
620
643
 
621
644
  if print_path:
622
- print(kubeconfig_path)
645
+ click.echo(kubeconfig_path)
623
646
  else:
624
- print(kubeconfig)
647
+ if output == 'table':
648
+ kubeconfig_path = pathlib.Path(kubeconfig_path).absolute()
649
+ if not user:
650
+ user = kubeconfig_path.parts[-3]
651
+ if not group:
652
+ group = kubeconfig_path.parts[-2]
653
+ if kubeconfig_path.is_file():
654
+ with kubeconfig_path.open() as f:
655
+ kubeconfig_str = f.read()
656
+ kubedata = kubeconfig_parse_fields(kubeconfig_str, cluster_name, user, group)
657
+ if not len(kubedata):
658
+ raise SystemExit("Something went wrong, could not parse kubeconfig")
659
+ fields = [["user", "user"], ["group", "group"], ["expiration date", "expires_at"]]
660
+ if wide:
661
+ fields.extend([["Cert subject", "cn"], ["context:name", "context_name"], ["context:user", "ctx_user"],
662
+ ["context:cluster", "cluster_name"], ["cluster endpoint", "server_name"]])
663
+ print_table(kubedata, fields)
664
+ else:
665
+ raise SystemExit(f"Could not find {kubeconfig_path}")
666
+ elif output == 'json':
667
+ click.echo(json.dumps(yaml.safe_load(kubeconfig)))
668
+ else:
669
+ click.echo(kubeconfig)
625
670
 
626
671
 
627
672
  def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
@@ -646,7 +691,7 @@ def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
646
691
  "GET", f'clusters/{cluster_id}/kubeconfig')['data']['kubeconfig']
647
692
 
648
693
  if not kubeconfig_raw:
649
- print("Cannot get kubeconfig")
694
+ click.echo("Cannot get kubeconfig")
650
695
  raise SystemExit()
651
696
 
652
697
  kubeconfig_path = save_cache(project_id, cluster_id, 'kubeconfig', kubeconfig_raw, user, group)
@@ -664,11 +709,11 @@ def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
664
709
 
665
710
  @cluster.command('kubectl', help='Fetch the kubeconfig for a cluster and run kubectl against it', context_settings={"ignore_unknown_options": True})
666
711
  @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
667
- @click.option('--cluster-name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
712
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
668
713
  @click.option('--user', type=click.STRING, help="User")
669
714
  @click.option('--group', type=click.STRING, help="Group")
670
- @click.argument("args", nargs=-1, type=click.UNPROCESSED)
671
715
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
716
+ @click.argument("args", nargs=-1, type=click.UNPROCESSED)
672
717
  @click.pass_context
673
718
  def cluster_kubectl_command(ctx, project_name, cluster_name, user, group, args, profile):
674
719
  """CLI command to run kubectl against a specified cluster using its kubeconfig."""
@@ -681,9 +726,9 @@ def cluster_kubectl_command(ctx, project_name, cluster_name, user, group, args,
681
726
  _run_kubectl(project_id, cluster_id, user, group, args)
682
727
 
683
728
 
684
- @click.group(help="nodepool related commands.")
729
+ @click.group(help="Nodepool related commands.")
685
730
  @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
686
- @click.option('--cluster-name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
731
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
687
732
  @click.option('--user', type=click.STRING, help="User")
688
733
  @click.option('--group', type=click.STRING, help="Group")
689
734
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
@@ -711,14 +756,14 @@ def nodepool_list(ctx):
711
756
  'get', 'nodepool', '-o', 'wide'])
712
757
 
713
758
 
714
- @nodepool.command('create')
759
+ @nodepool.command('create', help="Create a new nodepool")
715
760
  @click.option('--nodepool-name', '-n', default="nodepool01", help="Nodepool Name")
716
- @click.option('--count', default=2, help="Count of nodes")
717
- @click.option('--type', 'vmtype', default="tinav6.c2r4p3", help="Type of VMs")
718
- @click.option('--zone', multiple=True, required=True, help="Provide zone")
719
- @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
761
+ @click.option('--count', '-c', default=2, help="Count of nodes")
762
+ @click.option('--type', 'vmtype', '-t', default="tinav6.c2r4p3", help="Type of VMs")
763
+ @click.option('--zone', '-z', multiple=True, help="Provide zone(s)")
764
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
720
765
  @click.option('--dry-run', is_flag=True, help="Run without any action")
721
- @click.option('-f', '--filename', type=click.File("r"), help="Path to file to use to create the Nodepool ")
766
+ @click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to create the Nodepool")
722
767
  @click.pass_context
723
768
  def setup_worker_pool(ctx, nodepool_name, count, vmtype, zone, output, dry_run, filename):
724
769
  """Create a new nodepool in the cluster, optionally from a file or parameters."""
@@ -734,6 +779,9 @@ def setup_worker_pool(ctx, nodepool_name, count, vmtype, zone, output, dry_run,
734
779
  if zone:
735
780
  nodepool['spec']["zones"] = list(zone)
736
781
 
782
+ if not nodepool['spec']["zones"]:
783
+ raise click.BadArgumentUsage("Missing option '--zone' / '-z'.")
784
+
737
785
  if dry_run:
738
786
  print_output(nodepool, output)
739
787
  else:
@@ -9,14 +9,14 @@ from .profile import profile
9
9
  from .cache import cache
10
10
  from .quotas import quotas
11
11
 
12
- from .utils import ctx_update, login_profile, install_completions, profile_completer, cluster_completer, project_completer
12
+ from .utils import ctx_update, install_completions, profile_completer, cluster_completer, project_completer
13
13
 
14
14
  # Main CLI entry point
15
15
  @click.group(invoke_without_command=True)
16
- @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
17
- @click.option('--project-name', '-p', required = False, help="Project Name", shell_complete=project_completer)
18
- @click.option('--cluster-name', '-c', required = False, help="Cluster Name", shell_complete=cluster_completer)
19
- @click.option('-v', '--verbose', count=True)
16
+ @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
17
+ @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
18
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
19
+ @click.option('--verbose', '-v', count=True, help="Increase verbosity")
20
20
  @click.pass_context
21
21
  def cli(ctx, project_name, cluster_name, profile, verbose):
22
22
  """
@@ -64,8 +64,7 @@ cli.add_command(quotas)
64
64
  def recursive_help(cmd, parent=None):
65
65
  """Recursively prints help for all commands and subcommands."""
66
66
  ctx = click.core.Context(cmd, info_name=cmd.name, parent=parent)
67
- print(cmd.get_help(ctx))
68
- print()
67
+ click.echo(cmd.get_help(ctx))
69
68
  commands = getattr(cmd, 'commands', {})
70
69
  for sub in commands.values():
71
70
  recursive_help(sub, ctx)
@@ -79,7 +78,7 @@ def fullhelp():
79
78
  def version():
80
79
  """Display the current CLI version."""
81
80
  import importlib.metadata
82
- print(importlib.metadata.version(__package__))
81
+ click.echo(importlib.metadata.version(__package__))
83
82
 
84
83
  @cli.command("install-completion", help="Install shell completion scripts.")
85
84
  @click.option('--type', help="Shell, supported [bash,zsh]")
@@ -11,14 +11,14 @@ def profile():
11
11
 
12
12
 
13
13
  @profile.command('add', help="Add AK/SK or username/password new profile")
14
- @click.option('--profile-name', required=False, help="Name of profile, optional", type=click.STRING)
14
+ @click.option('--profile-name', '--profile', required=False, help="Name of profile, optional", type=click.STRING)
15
15
  @click.option('--access-key', required=False, help="AK of profile", type=click.STRING)
16
16
  @click.option('--secret-key', required=False, help="SK of profile", type=click.STRING)
17
17
  @click.option('--username', required=False, help="Username", type=click.STRING)
18
18
  @click.option('--password', required=False, help="Password", type=click.STRING)
19
19
  @click.option('--region', required=True, help="Region name", type=click.Choice(['eu-west-2', 'cloudgouv-eu-west-1']))
20
20
  @click.option('--endpoint', required=False, help="API endpoint", type=click.STRING)
21
- @click.option('--jwt', required=False, help="Enable jwt, by default is false", type=click.BOOL)
21
+ @click.option('--jwt', help="Enable JWT, by default is false")
22
22
  def add_profile(profile_name, access_key, secret_key, username, password, region, endpoint, jwt):
23
23
  """Add a new profile with AK/SK or username/password authentication."""
24
24
  if not profile_name:
@@ -98,8 +98,8 @@ def update_profile(profile_name, region, endpoint, jwt, new_name, force):
98
98
  click.echo(msg)
99
99
 
100
100
  @profile.command('delete', help="Delete a profile by name")
101
- @click.option('--profile-name', required=True, help="Name of profile", type=click.STRING)
102
- @click.option('--force', is_flag=True, help="Force deletion without confirmation")
101
+ @click.option('--profile-name', '--profile', required=True, help="Name of profile", type=click.STRING)
102
+ @click.option('--force', '-f', is_flag=True, help="Force deletion without confirmation")
103
103
  def delete_profile(profile_name, force):
104
104
  """Delete a profile with confirmation."""
105
105
  profiles = profile_list()
@@ -113,7 +113,7 @@ def delete_profile(profile_name, force):
113
113
  click.echo(f"Profile {profile_name_bold} has been successfully deleted")
114
114
 
115
115
  @profile.command('list', help="List existing profiles")
116
- @click.option('-o', '--output', type=click.Choice(["json", "yaml", "table", "wide"]), help="Specify output format, by default is wide")
116
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml", "table", "wide"]), help="Specify output format, by default is wide")
117
117
  def list_profiles(output):
118
118
  """Display all configured profiles with their settings."""
119
119
  profiles = profile_list()
@@ -11,8 +11,8 @@ from .utils import do_request, print_output, print_table, find_project_id_by_nam
11
11
  # DEIFNE THE PROJECT COMMAND GROUP
12
12
  @click.group(help="Project related commands.")
13
13
  @click.option('--project', 'project_name', required = False, help="Project Name")
14
- @click.option('--project-name', '-p', required = False, help="Project Name", shell_complete=project_completer)
15
- @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
14
+ @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
15
+ @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
16
16
  @click.pass_context
17
17
  def project(ctx, project_name, profile):
18
18
  """Group of commands related to project management."""
@@ -59,12 +59,12 @@ def project_logout(ctx, profile):
59
59
  # LIST PROJECTS
60
60
  @project.command('list', help="List all projects")
61
61
  @click.option('--project-name', '-p', help="Name of project", type=click.STRING, shell_complete=project_completer)
62
- @click.option('--deleted', is_flag=True, help="List deleted projects")
62
+ @click.option('--deleted', '-x', is_flag=True, help="List deleted projects")
63
63
  @click.option('--plain', is_flag=True, help="Plain table format")
64
64
  @click.option('--msword', is_flag=True, help="Microsoft Word table format")
65
- @click.option('--uuid', is_flag=True, help="show uuid")
65
+ @click.option('--uuid', is_flag=True, help="Show UUID")
66
66
  @click.option('--watch', '-w', is_flag=True, help="Watch the changes")
67
- @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
67
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
68
68
  @click.option('--profile', help="Configuration profile to use")
69
69
  @click.pass_context
70
70
  def project_list(ctx, project_name, deleted, plain, msword, uuid, watch, output, profile):
@@ -72,7 +72,9 @@ def project_list(ctx, project_name, deleted, plain, msword, uuid, watch, output,
72
72
  project_name, _, profile = ctx_update(ctx, project_name, None, profile)
73
73
  login_profile(profile)
74
74
 
75
+ profile_name = os.getenv('OKS_PROFILE')
75
76
  project_id = get_project_id()
77
+
76
78
  params = {}
77
79
  if project_name:
78
80
  params['name'] = project_name
@@ -86,7 +88,7 @@ def project_list(ctx, project_name, deleted, plain, msword, uuid, watch, output,
86
88
  print_output(data, output)
87
89
  return
88
90
 
89
- field_names = ["NAME", "CREATED", "UPDATED", "STATUS", "DEFAULT"]
91
+ field_names = ["PROJECT", "PROFILE", "REGION", "CREATED", "UPDATED", "STATUS", "DEFAULT"]
90
92
  if uuid:
91
93
  field_names.append('UUID')
92
94
 
@@ -120,11 +122,12 @@ def project_list(ctx, project_name, deleted, plain, msword, uuid, watch, output,
120
122
  else:
121
123
  default = ""
122
124
 
125
+ region_name = project.get('region')
123
126
  created_at = dateutil.parser.parse(project['created_at'])
124
127
  updated_at = dateutil.parser.parse(project['updated_at'])
125
128
  now = datetime.datetime.now(tz=created_at.tzinfo)
126
129
 
127
- row = [name, human_readable.date_time(now - created_at), human_readable.date_time(now - updated_at), msg, default]
130
+ row = [name, profile_name, region_name, human_readable.date_time(now - created_at), human_readable.date_time(now - updated_at), msg, default]
128
131
  if uuid:
129
132
  row.append(project['id'])
130
133
 
@@ -193,15 +196,15 @@ def project_list(ctx, project_name, deleted, plain, msword, uuid, watch, output,
193
196
 
194
197
  # CREATE PROJECT BY NAME
195
198
  @project.command('create', help="Create a new project")
196
- @click.option('--project-name', '-p', help="Name of the project", shell_complete=project_completer)
197
- @click.option('--description', help="Description of the project")
199
+ @click.option('--project-name', '-p', help="Name of the project", type=click.STRING, shell_complete=project_completer)
200
+ @click.option('--description', '-d', help="Description of the project")
198
201
  @click.option('--cidr', help='CIDR for the project')
199
- @click.option('--quirk', multiple=True, help="Quirk")
200
- @click.option('--tags', help="Comma-separated list of tags, example: 'key1=value1,key2=value2'")
202
+ @click.option('--quirk', '-q', multiple=True, help="Quirk")
203
+ @click.option('--tags', '-t', help="Comma-separated list of tags, example: 'key1=value1,key2=value2'")
201
204
  @click.option('--disable-api-termination', type=click.BOOL, help="Disable delete action by API")
202
205
  @click.option('--dry-run', is_flag=True, help="Client dry-run, only print the object that would be sent, without sending it")
203
- @click.option('-o', '--output', type=click.Choice(["json", "yaml", "silent"]), help="Specify output format, by default is json")
204
- @click.option('-f', '--filename', type=click.File("r"), help="Path to file to use to create the project ")
206
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml", "silent"]), help="Specify output format, by default is json")
207
+ @click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to create the project")
205
208
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
206
209
  @click.pass_context
207
210
  def project_create(ctx, project_name, description, cidr, quirk, tags, disable_api_termination, dry_run, output, filename, profile):
@@ -255,7 +258,7 @@ def project_create(ctx, project_name, description, cidr, quirk, tags, disable_ap
255
258
  # GET PROJECT BY NAME
256
259
  @project.command('get', help="Get default project or the project by name")
257
260
  @click.option('--project-name', '-p', help="Name of the project", shell_complete=project_completer)
258
- @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
261
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
259
262
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
260
263
  @click.pass_context
261
264
  def project_get(ctx, project_name, output, profile):
@@ -270,8 +273,8 @@ def project_get(ctx, project_name, output, profile):
270
273
 
271
274
  # DELETE PROJECT BY NAME
272
275
  @project.command('delete', help="Delete a project by name")
273
- @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
274
- @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
276
+ @click.option('--project-name', '-p', required=False, help="Project Name", type=click.STRING, shell_complete=project_completer)
277
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
275
278
  @click.option('--dry-run', is_flag=True, help="Run without any action")
276
279
  @click.option('--force', is_flag=True, help="Force deletion without confirmation")
277
280
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
@@ -302,12 +305,12 @@ def project_delete_command(ctx, project_name, output, dry_run, force, profile):
302
305
 
303
306
  # UPDATE PROJECT BY NAME
304
307
  @project.command('update', help="Update a project by name")
305
- @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
306
- @click.option('--description', help="Description of the project")
307
- @click.option('--quirk', multiple=True, help="Quirk")
308
- @click.option('--tags', help="Comma-separated list of tags, example: 'key1=value1,key2=value2'")
308
+ @click.option('--project-name', '-p', required=False, help="Project Name", type=click.STRING, shell_complete=project_completer)
309
+ @click.option('--description', '-d', help="Description of the project")
310
+ @click.option('--quirk', '-q', multiple=True, help="Quirk")
311
+ @click.option('--tags', '-t', help="Comma-separated list of tags, example: 'key1=value1,key2=value2'")
309
312
  @click.option('--disable-api-termination', type=click.BOOL, help="Disable delete action by API")
310
- @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
313
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
311
314
  @click.option('--dry-run', is_flag=True, help="Run without any action")
312
315
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
313
316
  @click.pass_context
@@ -351,7 +354,7 @@ def project_update_command(ctx, project_name, description, quirk, tags, disable_
351
354
  # GET PROJECT QUOTAS BY PROJECT NAME
352
355
  @project.command('quotas', help="Get project quotas")
353
356
  @click.option('--project-name', '-p', help="Name of the project", shell_complete=project_completer)
354
- @click.option('-o', '--output', type=click.Choice(["json", "yaml", "table"]), help="Specify output format, by default is json")
357
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml", "table"]), help="Specify output format, by default is json")
355
358
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
356
359
  @click.pass_context
357
360
  def project_get_quotas(ctx, project_name, output, profile):
@@ -379,7 +382,7 @@ def project_get_quotas(ctx, project_name, output, profile):
379
382
  # GET PROJECT SNAPSHOTS BY PROJECT NAME
380
383
  @project.command('snapshots', help="Get project snapshots")
381
384
  @click.option('--project-name', '-p', help="Name of the project", shell_complete=project_completer)
382
- @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
385
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
383
386
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
384
387
  @click.pass_context
385
388
  def project_get(ctx, project_name, output, profile):
@@ -395,8 +398,8 @@ def project_get(ctx, project_name, output, profile):
395
398
  # GET PUBLIC IPS BY PROJECT NAME
396
399
  @project.command('publicips', help="Get project public ips")
397
400
  @click.option('--project-name', '-p', help="Name of the project", shell_complete=project_completer)
398
- @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
399
- @click.option('--profile', help="Configuration profile to use")
401
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
402
+ @click.option('--profile',help="Configuration profile to use")
400
403
  @click.pass_context
401
404
  def project_get_public_ips(ctx, project_name, output, profile):
402
405
  """Retrieve the list of public IPs associated with the specified project."""
@@ -4,7 +4,7 @@ from .utils import do_request, print_output, print_table, ctx_update, login_prof
4
4
 
5
5
  @click.command(help="Get Quotas")
6
6
  @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
7
- @click.option('-o', '--output', type=click.Choice(["json", "yaml", "table"]), help="Specify output format, by default is json")
7
+ @click.option('--output','-o', type=click.Choice(["json", "yaml", "table"]), help="Specify output format, by default is json")
8
8
  @click.pass_context
9
9
  def quotas(ctx, profile, output):
10
10
  """Retrieve global quotas across all projects for the given profile."""
@@ -170,14 +170,17 @@ def print_output(data, output_fromat):
170
170
 
171
171
  click.echo(output_data)
172
172
 
173
- def print_table(data, table_fields, align="l"):
173
+ def print_table(data, table_fields, align="l", style=None):
174
174
  """Print API returned data as table
175
175
  data: list of dict containing data
176
176
  table_fields: List of 2 elements list. First element is the table field name, second element is the corresponding dict key in data
177
177
  align: Columns alignment (l,r,c)
178
+ style: Table format other style
178
179
  """
179
180
  table = prettytable.PrettyTable()
180
181
  table.align = align
182
+ if style and isinstance(style, prettytable.TableStyle):
183
+ table.set_style(style)
181
184
  fields = list()
182
185
  values = list()
183
186
 
@@ -697,16 +700,65 @@ def get_expiration_date(kubeconfig_str):
697
700
  logging.info("No client certificate data found for user.")
698
701
  continue
699
702
 
700
- ca_cert = base64.b64decode(client_cert_data)
703
+ cert = decode_parse_certificate(client_cert_data)
704
+ not_after = cert.get_notAfter().decode('ascii')
705
+ not_after_date = datetime.strptime(not_after, '%Y%m%d%H%M%SZ')
701
706
 
702
- try:
703
- cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, ca_cert)
704
- not_after = cert.get_notAfter().decode('ascii')
705
- not_after_date = datetime.strptime(not_after, '%Y%m%d%H%M%SZ')
707
+ return not_after_date
708
+
709
+ def decode_parse_certificate(cert_str):
710
+ """Parse base64 encoded certificate data and returns cert (X509) object"""
711
+ try:
712
+ ca_cert = base64.b64decode(cert_str)
713
+ cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, ca_cert)
714
+ return cert
715
+ except OpenSSL.crypto.Error as e:
716
+ logging.info(f"ERROR: Can't parse base64 encoded certificate: {e}")
706
717
 
707
- return not_after_date
708
- except OpenSSL.crypto.Error as e:
709
- logging.info(f"ERROR: Invalid certificate data for cluster. Error: {e}")
718
+ def kubeconfig_parse_fields(kubeconfig, cluster_name, user, group):
719
+ """
720
+ Load YAML kubeconfig and extract fields
721
+ kubeconfig: Kubeconfig load file as string (YAML)
722
+ cluster_name: Name of the cluster kubeconfig is related to
723
+ user: user name of this kubeconfig (if set)
724
+ group: user group name of this kubeconfig (if set)
725
+ """
726
+ kubeconfig_str = yaml.safe_load(kubeconfig)
727
+ kubedata = list()
728
+
729
+ # Ensure loaded YAML returnes a valid dict object
730
+ if not isinstance(kubeconfig_str, dict):
731
+ return kubedata
732
+
733
+ for context in kubeconfig_str.get('contexts', []):
734
+ data = dict()
735
+ ctx_cluster = context.get('context').get('cluster', None)
736
+ ctx_user = context.get('context').get('user', None)
737
+ ctx_name = context.get('name')
738
+ data.update({"context_name": ctx_name})
739
+
740
+ for cluster in kubeconfig_str.get('clusters', []):
741
+ if ctx_cluster == cluster.get('name', None):
742
+ cls_server = cluster.get('cluster').get('server')
743
+ if not cluster_name:
744
+ cluster_name = ctx_cluster
745
+ data.update({"cluster_name": cluster_name, "server_name": cls_server})
746
+ break
747
+
748
+ for user_name in kubeconfig_str.get('users', []):
749
+ if ctx_user == user_name.get('name'):
750
+ cert_str = user_name.get('user').get('client-certificate-data')
751
+ cert_obj = decode_parse_certificate(cert_str)
752
+ expires_at = datetime.strptime(cert_obj.get_notAfter().decode('ascii'), '%Y%m%d%H%M%SZ')
753
+ cn = cert_obj.get_subject().get_components()
754
+ cn_user = f"CN={cn[0][1].decode('utf-8')}"
755
+ cn_group = f"/O={cn[1][1].decode('utf-8')}" if len(cn) > 1 else ""
756
+ data.update({"user": click.style(user, bold=True), "group": click.style(group, bold=True),
757
+ "expires_at": expires_at, "ctx_user": ctx_user, "cn": f"{cn_user}{cn_group}"})
758
+ break
759
+ kubedata.append(data)
760
+
761
+ return kubedata
710
762
 
711
763
  def retrieve_cp_sized(filepath, endpoint):
712
764
  """Fetch control plane sizes from API and save to file."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oks-cli
3
- Version: 1.15
3
+ Version: 1.16
4
4
  Author: Outscale SAS
5
5
  Author-email: opensource@outscale.com
6
6
  License: BSD
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: certifi>=2024.8.30
19
19
  Requires-Dist: charset-normalizer>=3.3.2
20
- Requires-Dist: click>=8.1.7
20
+ Requires-Dist: click<8.3.0,>=8.1.7
21
21
  Requires-Dist: colorama>=0.4.6
22
22
  Requires-Dist: idna>=3.10
23
23
  Requires-Dist: pyyaml>=6.0.2
@@ -1,6 +1,6 @@
1
1
  certifi>=2024.8.30
2
2
  charset-normalizer>=3.3.2
3
- click>=8.1.7
3
+ click<8.3.0,>=8.1.7
4
4
  colorama>=0.4.6
5
5
  idna>=3.10
6
6
  pyyaml>=6.0.2
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="oks-cli",
5
- version="1.15",
5
+ version="1.16",
6
6
  packages=['oks_cli'],
7
7
  author="Outscale SAS",
8
8
  author_email="opensource@outscale.com",
@@ -26,7 +26,7 @@ setup(
26
26
  install_requires=[
27
27
  "certifi>=2024.8.30",
28
28
  "charset-normalizer>=3.3.2",
29
- "click>=8.1.7",
29
+ "click>=8.1.7,<8.3.0",
30
30
  "colorama>=0.4.6",
31
31
  "idna>=3.10",
32
32
  "pyyaml>=6.0.2",
@@ -15,6 +15,6 @@ def test_cache_kubeconfigs_command(mock_request, add_default_profile):
15
15
  ]
16
16
 
17
17
  runner = CliRunner()
18
- result = runner.invoke(cli, ["cache", "kubeconfigs", "-p", "test", "-c", "test"])
18
+ result = runner.invoke(cli, ["--profile", "default", "cache", "kubeconfigs", "-p", "test", "-c", "test"])
19
19
  assert result.exit_code == 0
20
20
  assert '| user | group | expiration date |' in result.output
@@ -5,6 +5,29 @@ from unittest.mock import patch, MagicMock
5
5
  import json
6
6
  import yaml
7
7
 
8
+ # Test the "cluster list" command: verifies region and profile are shown
9
+ @patch("oks_cli.utils.requests.request")
10
+ def test_cluster_list_command_with_region_and_profile(mock_request, add_default_profile):
11
+ mock_request.side_effect = [
12
+ MagicMock(status_code=200, headers = {}, json=lambda: {"ResponseContext": {}, "Projects": [{"id": "12345"}]}),
13
+ MagicMock(status_code=200, headers = {}, json=lambda:
14
+ {"ResponseContext": {},
15
+ "Clusters": [
16
+ {"id": "12345", "name": "test",
17
+ "statuses": {"status": "ready",
18
+ "created_at": "2019-08-24T14:15:22Z",
19
+ "updated_at": "2019-08-24T14:15:22Z"}
20
+ }
21
+ ]
22
+ }),
23
+ ]
24
+
25
+ runner = CliRunner()
26
+ result = runner.invoke(cli, ["--profile", "default", "cluster", "list", "-p", "test", "-c", "test"])
27
+ assert result.exit_code == 0
28
+ assert 'eu-west-2' in result.output
29
+ assert 'default' in result.output
30
+
8
31
  # Test the "cluster list" command: verifies listing clusters in a project
9
32
  @patch("oks_cli.utils.requests.request")
10
33
  def test_cluster_list_command(mock_request, add_default_profile):
@@ -14,7 +37,7 @@ def test_cluster_list_command(mock_request, add_default_profile):
14
37
  ]
15
38
 
16
39
  runner = CliRunner()
17
- result = runner.invoke(cli, ["cluster", "list", "-p", "test", "-c", "test", '-o', 'json'])
40
+ result = runner.invoke(cli, ["--profile", "default", "cluster", "list", "-p", "test", "-c", "test", '-o', 'json'])
18
41
  assert result.exit_code == 0
19
42
  assert '"name": "test"' in result.output
20
43
 
@@ -296,6 +319,20 @@ def test_cluster_kubeconfig_command(mock_request, add_default_profile):
296
319
  assert result.exit_code == 0
297
320
  assert 'kubeconfig' in result.output
298
321
 
322
+ # Test the "cluster kubeconfig --output table" command: verifies retrieving the kubeconfig and output as table
323
+ @patch("oks_cli.utils.requests.request")
324
+ def test_cluster_kubeconfig_info_command(mock_request, add_default_profile):
325
+ mock_request.side_effect = [
326
+ MagicMock(status_code=200, headers = {}, json=lambda: {"ResponseContext": {}, "Projects": [{"id": "12345"}]}),
327
+ MagicMock(status_code=200, headers = {}, json=lambda: {"ResponseContext": {}, "Clusters": [{"id": "12345"}]}),
328
+ MagicMock(status_code=200, headers = {}, json=lambda: {"ResponseContext": {}, "Cluster": {"data": {"kubeconfig": "kubeconfig"}}})
329
+ ]
330
+
331
+ runner = CliRunner()
332
+ result = runner.invoke(cli, ["cluster", "kubeconfig", "-p", "test", "-c", "test", "--output", "table"])
333
+ assert result.exit_code != 0
334
+ assert 'Something went wrong, could not parse kubeconfig' in result.output
335
+
299
336
  # Test the "cluster delete" command with JSON output
300
337
  @patch("oks_cli.utils.requests.request")
301
338
  def test_cluster_delete_command_json(mock_request, add_default_profile):
@@ -360,14 +397,13 @@ def test_cluster_kubectl_command(mock_request, mock_run, add_default_profile):
360
397
  runner = CliRunner()
361
398
  result = runner.invoke(cli, ["cluster", "-p", "test", "-c", "test", "kubectl", "get", "pods"])
362
399
  mock_run.assert_called()
363
-
400
+
364
401
  args, kwargs = mock_run.call_args
365
402
 
366
403
  assert result.exit_code == 0
367
404
  assert ".oks_cli/cache/12345-12345/default/default/kubeconfig" in kwargs["env"]["KUBECONFIG"]
368
405
  assert args[0] == ["kubectl", "get", "pods"]
369
406
 
370
-
371
407
  # Test the "cluster create by one-click" command: verifies creating cluster interactively
372
408
  @patch("oks_cli.utils.os.fork")
373
409
  @patch("oks_cli.utils.time.sleep")
@@ -5,6 +5,27 @@ import yaml
5
5
  import json
6
6
 
7
7
  # START PROJECT LIST COMMAND
8
+ # Test the "project list" command: verifies region and profile are shown
9
+ @patch("oks_cli.utils.requests.request")
10
+ def test_project_list_command_with_region_and_profile(mock_request, add_default_profile):
11
+ mock_request.side_effect = [
12
+ MagicMock(status_code=200, headers = {}, json=lambda: {
13
+ "ResponseContext": {},
14
+ "Projects": [
15
+ {"id": "12345",
16
+ "name":"test",
17
+ "created_at": "2019-08-24T14:15:22Z",
18
+ "updated_at": "2019-08-24T14:15:22Z",
19
+ "status": "ready",
20
+ "region":"eu-west-2"}]})
21
+ ]
22
+
23
+ runner = CliRunner()
24
+ result = runner.invoke(cli, ["project", "list"])
25
+ assert result.exit_code == 0
26
+ assert 'eu-west-2' in result.output
27
+ assert 'default' in result.output
28
+
8
29
  # Test the "project list" command: verifies listing 1 projects with json
9
30
  @patch("oks_cli.utils.requests.request")
10
31
  def test_project_list_command_json(mock_request, add_default_profile):
@@ -247,8 +268,6 @@ def test_project_delete_json(mock_request, add_default_profile):
247
268
  "--profile", "default"
248
269
  ])
249
270
 
250
- print(result.output)
251
-
252
271
  assert result.exit_code == 0
253
272
 
254
273
  # La sortie doit être du JSON valide
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes