oks-cli 1.14__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 (31) hide show
  1. {oks_cli-1.14 → oks_cli-1.16}/PKG-INFO +15 -20
  2. {oks_cli-1.14 → oks_cli-1.16}/README.md +13 -18
  3. {oks_cli-1.14 → oks_cli-1.16}/oks_cli/cache.py +16 -15
  4. {oks_cli-1.14 → oks_cli-1.16}/oks_cli/cluster.py +127 -75
  5. {oks_cli-1.14 → oks_cli-1.16}/oks_cli/main.py +8 -9
  6. {oks_cli-1.14 → oks_cli-1.16}/oks_cli/profile.py +61 -19
  7. {oks_cli-1.14 → oks_cli-1.16}/oks_cli/project.py +47 -33
  8. oks_cli-1.16/oks_cli/quotas.py +21 -0
  9. {oks_cli-1.14 → oks_cli-1.16}/oks_cli/utils.py +196 -32
  10. {oks_cli-1.14 → oks_cli-1.16}/oks_cli.egg-info/PKG-INFO +15 -20
  11. {oks_cli-1.14 → oks_cli-1.16}/oks_cli.egg-info/SOURCES.txt +2 -1
  12. {oks_cli-1.14 → oks_cli-1.16}/oks_cli.egg-info/requires.txt +1 -1
  13. {oks_cli-1.14 → oks_cli-1.16}/setup.py +2 -2
  14. {oks_cli-1.14 → oks_cli-1.16}/tests/test_cache.py +1 -1
  15. oks_cli-1.16/tests/test_cluster.py +440 -0
  16. {oks_cli-1.14 → oks_cli-1.16}/tests/test_nodepool.py +1 -1
  17. oks_cli-1.16/tests/test_profile.py +58 -0
  18. oks_cli-1.16/tests/test_project.py +476 -0
  19. oks_cli-1.16/tests/test_quota.py +27 -0
  20. oks_cli-1.16/tests/test_shell_completion.py +104 -0
  21. oks_cli-1.14/oks_cli/quotas.py +0 -14
  22. oks_cli-1.14/tests/test_cluster.py +0 -158
  23. oks_cli-1.14/tests/test_profile.py +0 -30
  24. oks_cli-1.14/tests/test_project.py +0 -102
  25. oks_cli-1.14/tests/test_quota.py +0 -15
  26. {oks_cli-1.14 → oks_cli-1.16}/LICENSE +0 -0
  27. {oks_cli-1.14 → oks_cli-1.16}/oks_cli/__init__.py +0 -0
  28. {oks_cli-1.14 → oks_cli-1.16}/oks_cli.egg-info/dependency_links.txt +0 -0
  29. {oks_cli-1.14 → oks_cli-1.16}/oks_cli.egg-info/entry_points.txt +0 -0
  30. {oks_cli-1.14 → oks_cli-1.16}/oks_cli.egg-info/top_level.txt +0 -0
  31. {oks_cli-1.14 → oks_cli-1.16}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oks-cli
3
- Version: 1.14
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
@@ -102,31 +102,17 @@ Dynamic: requires-dist
102
102
  ### Standard Installation
103
103
 
104
104
  ```bash
105
- # Clone the repository
106
- git clone https://github.com/outscale/oks-cli.git
107
- cd oks-cli
108
-
109
105
  # Create and activate a virtual environment
110
106
  python -m venv venv
111
107
  source venv/bin/activate
112
108
 
113
- # Install dependencies
114
- pip install -r requirements.txt
115
-
116
- # Install the CLI in editable mode
117
- pip install -e .
118
- ```
119
-
120
- ### User Installation
109
+ # Install the CLI
110
+ pip install oks-cli
121
111
 
122
- Install globally without a virtual environment (Python 3.11):
123
-
124
- ```bash
125
- pip3.11 install -e --user .
112
+ # Check version of oks-cli
113
+ oks-cli version
126
114
  ```
127
115
 
128
- > **Note:** Ensure `~/Library/Python/3.11/bin` (macOS) or the equivalent path is in your `PATH`.
129
-
130
116
  ---
131
117
 
132
118
  ## 🚀 Usage
@@ -207,6 +193,15 @@ oks-cli project login --project-name my-project
207
193
  Install the CLI in editable mode with development dependencies
208
194
 
209
195
  ```bash
196
+ # Clone the repository
197
+ git clone https://github.com/outscale/oks-cli.git
198
+ cd oks-cli
199
+
200
+ # Create and activate a virtual environment
201
+ python -m venv venv
202
+ source venv/bin/activate
203
+
204
+ # CLI in editable mode
210
205
  pip install -e ".[dev]"
211
206
  ```
212
207
 
@@ -59,31 +59,17 @@
59
59
  ### Standard Installation
60
60
 
61
61
  ```bash
62
- # Clone the repository
63
- git clone https://github.com/outscale/oks-cli.git
64
- cd oks-cli
65
-
66
62
  # Create and activate a virtual environment
67
63
  python -m venv venv
68
64
  source venv/bin/activate
69
65
 
70
- # Install dependencies
71
- pip install -r requirements.txt
72
-
73
- # Install the CLI in editable mode
74
- pip install -e .
75
- ```
76
-
77
- ### User Installation
66
+ # Install the CLI
67
+ pip install oks-cli
78
68
 
79
- Install globally without a virtual environment (Python 3.11):
80
-
81
- ```bash
82
- pip3.11 install -e --user .
69
+ # Check version of oks-cli
70
+ oks-cli version
83
71
  ```
84
72
 
85
- > **Note:** Ensure `~/Library/Python/3.11/bin` (macOS) or the equivalent path is in your `PATH`.
86
-
87
73
  ---
88
74
 
89
75
  ## 🚀 Usage
@@ -164,6 +150,15 @@ oks-cli project login --project-name my-project
164
150
  Install the CLI in editable mode with development dependencies
165
151
 
166
152
  ```bash
153
+ # Clone the repository
154
+ git clone https://github.com/outscale/oks-cli.git
155
+ cd oks-cli
156
+
157
+ # Create and activate a virtual environment
158
+ python -m venv venv
159
+ source venv/bin/activate
160
+
161
+ # CLI in editable mode
167
162
  pip install -e ".[dev]"
168
163
  ```
169
164
 
@@ -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
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
- @click.option('--project-name', '-p', required = False, help="Project Name")
8
- @click.option('--cluster-name', '-c', required = False, help="Cluster Name")
9
+ @click.option('--project-name', '-p', required = False, help="Project Name", shell_complete=project_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):
@@ -20,8 +22,8 @@ def delete_cache(force):
20
22
  clear_cache()
21
23
 
22
24
  @cache.command('kubeconfigs', help="List cached kubeconfigs")
23
- @click.option('--project-name', '-p', required=False, help="Project Name")
24
- @click.option('--cluster-name', '-c', required=False, help="Cluster Name")
25
+ @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_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 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
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")
24
- @click.option('--name', 'cluster_name', required = False, help="Cluster Name")
25
- @click.option('--cluster-name', '-c', required = False, help="Cluster Name")
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")
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):
@@ -70,14 +78,13 @@ def cluster_logout(ctx, profile):
70
78
 
71
79
  # LIST CLUSTERS
72
80
  @cluster.command('list', help="List all clusters")
73
- @click.option('--project-name', '-p', required=False, help="Project Name")
74
- @click.option('--name', 'cluster_name', required = False, help="Cluster Name")
75
- @click.option('--cluster-name', '-c', required = False, help="Cluster Name")
76
- @click.option('--deleted', is_flag=True, help="List deleted clusters")
81
+ @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
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")
219
- @click.option('--name', 'cluster_name', required=False, help="Cluster Name")
220
- @click.option('--cluster-name', '-c', required=False, help="Cluster Name")
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
 
@@ -311,26 +337,27 @@ def _create_cluster(project_name, cluster_config, output):
311
337
 
312
338
  # CLUSTER CREATE BY NAME
313
339
  @cluster.command('create', help="Create a new cluster")
314
- @click.option('--project-name', '-p', required=False, help="Project Name")
315
- @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name")
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")
340
+ @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
341
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
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")
354
+ @click.option('--cp-multi-az', '-m', is_flag=True, help="Enable control plane multi AZ")
328
355
  @click.option('--dry-run', is_flag=True, help="Client dry-run, only print the object that would be sent, without sending it")
329
- @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
330
- @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 ")
331
358
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
332
359
  @click.pass_context
333
- 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, dry_run, output, filename, profile):
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):
334
361
  """CLI command to create a new Kubernetes cluster with optional configuration parameters."""
335
362
  project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
336
363
  login_profile(profile)
@@ -397,31 +424,32 @@ def cluster_create_command(ctx, project_name, cluster_name, description, admin,
397
424
 
398
425
  if disable_api_termination is not None:
399
426
  cluster_config["disable_api_termination"] = disable_api_termination
427
+
428
+ if cp_multi_az is not None:
429
+ cluster_config["cp_multi_az"] = cp_multi_az
400
430
 
401
431
  if not dry_run:
402
432
  _create_cluster(project_name, cluster_config, output)
403
433
  else:
404
- cluster_template = get_template("cluster")
405
- cluster_template.update(cluster_config)
434
+ cluster_template = prepare_cluster_template(cluster_config)
406
435
  print_output(cluster_template, output)
407
436
 
408
437
  # UPDATE CLUSTER
409
438
  @cluster.command('update', help="Update a cluster by name")
410
- @click.option('--project-name', '-p', required=False, help="Project name")
411
- @click.option('--name', 'cluster_name', required=False, help="Cluster name")
412
- @click.option('--cluster-name', '-c', required=False, help="Cluster name")
413
- @click.option('--description', help="Description of the cluster")
414
- @click.option('--admin', help="Admin Whitelist")
415
- @click.option('--version', shell_complete=shell_completions, help="Kubernetes version")
416
- @click.option('--tags', help="Comma-separated list of tags, example: 'key1=value1,key2=value2'")
439
+ @click.option('--project-name', '-p', required=False, help="Project name", shell_complete=project_completer)
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'")
417
445
  @click.option('--enable-admission-plugins', help="List of admission plugins, separated by commas")
418
446
  @click.option('--disable-admission-plugins', help="List of admission plugins, separated by commas")
419
- @click.option('--quirk', multiple=True, help="Quirk")
447
+ @click.option('--quirk', '-q', multiple=True, help="Quirk")
420
448
  @click.option('--disable-api-termination', type=click.BOOL, help="Disable delete action by API")
421
449
  @click.option('--control-plane', shell_complete=shell_completions, help="Controlplane plan")
422
450
  @click.option('--dry-run', is_flag=True, help="Client dry-run, only print the object that would be sent, without sending it")
423
- @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
424
- @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 ")
425
453
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
426
454
  @click.pass_context
427
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):
@@ -495,10 +523,9 @@ def cluster_update_command(ctx, project_name, cluster_name, description, admin,
495
523
 
496
524
  # UPGRADE CLUSTER
497
525
  @cluster.command('upgrade', help="Upgrade a cluster by name")
498
- @click.option('--project-name', '-p', required=False, help="Project name")
499
- @click.option('--name', 'cluster_name', required=False, help="Cluster name")
500
- @click.option('--cluster-name', '-c', required=False, help="Cluster name")
501
- @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
526
+ @click.option('--project-name', '-p', required=False, help="Project name", shell_complete=project_completer)
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")
502
529
  @click.option('--force', is_flag=True, help="Force upgrade")
503
530
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
504
531
  @click.pass_context
@@ -517,10 +544,9 @@ def cluster_update_command(ctx, project_name, cluster_name, output, force, profi
517
544
 
518
545
  # DELETE CLUSTER BY NAME
519
546
  @cluster.command('delete', help="Delete a cluster by name")
520
- @click.option('--project-name', '-p', required=False, help="Project name")
521
- @click.option('--name', 'cluster_name', required=False, help="Cluster name")
522
- @click.option('--cluster-name', '-c', required=False, help="Cluster name")
523
- @click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
547
+ @click.option('--project-name', '-p', required=False, help="Project name", shell_complete=project_completer)
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")
524
550
  @click.option('--dry-run', is_flag=True, help="Run without any action")
525
551
  @click.option('--force', is_flag=True, help="Force deletion without confirmation")
526
552
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
@@ -549,10 +575,11 @@ def cluster_delete_command(ctx, project_name, cluster_name, output, dry_run, for
549
575
 
550
576
  # GET KUBECONFIG
551
577
  @cluster.command('kubeconfig', help="Fetch the kubeconfig for a cluster")
552
- @click.option('--project-name', '-p', required=False, help="Project Name")
553
- @click.option('--name', 'cluster_name', required=False, help="Cluster name")
554
- @click.option('--cluster-name', '-c', required=False, help="Cluster Name")
578
+ @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
579
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
555
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")
556
583
  @click.option('--refresh', '--force', is_flag=True, help="Force refresh saved kubeconfig")
557
584
  @click.option('--nacl', is_flag=True, help="Use public key encryption on wire (require api support)")
558
585
  @click.option('--user', type=click.STRING, help="User")
@@ -560,7 +587,7 @@ def cluster_delete_command(ctx, project_name, cluster_name, output, dry_run, for
560
587
  @click.option('--ttl', type=click.STRING, help="TTL in human readable format (5h, 1d, 1w)")
561
588
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
562
589
  @click.pass_context
563
- 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):
564
591
  """CLI command to fetch and optionally print the kubeconfig for a specified cluster."""
565
592
  project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
566
593
  login_profile(profile)
@@ -615,9 +642,31 @@ def cluster_kubeconfig_command(ctx, project_name, cluster_name, print_path, refr
615
642
  kubeconfig_path = save_cache(project_id, cluster_id, 'kubeconfig', kubeconfig, user, group)
616
643
 
617
644
  if print_path:
618
- print(kubeconfig_path)
645
+ click.echo(kubeconfig_path)
619
646
  else:
620
- 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)
621
670
 
622
671
 
623
672
  def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
@@ -642,7 +691,7 @@ def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
642
691
  "GET", f'clusters/{cluster_id}/kubeconfig')['data']['kubeconfig']
643
692
 
644
693
  if not kubeconfig_raw:
645
- print("Cannot get kubeconfig")
694
+ click.echo("Cannot get kubeconfig")
646
695
  raise SystemExit()
647
696
 
648
697
  kubeconfig_path = save_cache(project_id, cluster_id, 'kubeconfig', kubeconfig_raw, user, group)
@@ -659,12 +708,12 @@ def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
659
708
 
660
709
 
661
710
  @cluster.command('kubectl', help='Fetch the kubeconfig for a cluster and run kubectl against it', context_settings={"ignore_unknown_options": True})
662
- @click.option('--project-name', '-p', required=False, help="Project Name")
663
- @click.option('--cluster-name', '-c', required=False, help="Cluster Name")
711
+ @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
712
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
664
713
  @click.option('--user', type=click.STRING, help="User")
665
714
  @click.option('--group', type=click.STRING, help="Group")
666
- @click.argument("args", nargs=-1, type=click.UNPROCESSED)
667
715
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
716
+ @click.argument("args", nargs=-1, type=click.UNPROCESSED)
668
717
  @click.pass_context
669
718
  def cluster_kubectl_command(ctx, project_name, cluster_name, user, group, args, profile):
670
719
  """CLI command to run kubectl against a specified cluster using its kubeconfig."""
@@ -677,9 +726,9 @@ def cluster_kubectl_command(ctx, project_name, cluster_name, user, group, args,
677
726
  _run_kubectl(project_id, cluster_id, user, group, args)
678
727
 
679
728
 
680
- @click.group(help="nodepool related commands.")
681
- @click.option('--project-name', '-p', required=False, help="Project Name")
682
- @click.option('--cluster-name', '-c', required=False, help="Cluster Name")
729
+ @click.group(help="Nodepool related commands.")
730
+ @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
731
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
683
732
  @click.option('--user', type=click.STRING, help="User")
684
733
  @click.option('--group', type=click.STRING, help="Group")
685
734
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
@@ -707,14 +756,14 @@ def nodepool_list(ctx):
707
756
  'get', 'nodepool', '-o', 'wide'])
708
757
 
709
758
 
710
- @nodepool.command('create')
759
+ @nodepool.command('create', help="Create a new nodepool")
711
760
  @click.option('--nodepool-name', '-n', default="nodepool01", help="Nodepool Name")
712
- @click.option('--count', default=2, help="Count of nodes")
713
- @click.option('--type', 'vmtype', default="tinav6.c2r4p3", help="Type of VMs")
714
- @click.option('--zone', default=["eu-west-2a"], multiple=True, help="Provide zone")
715
- @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")
716
765
  @click.option('--dry-run', is_flag=True, help="Run without any action")
717
- @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")
718
767
  @click.pass_context
719
768
  def setup_worker_pool(ctx, nodepool_name, count, vmtype, zone, output, dry_run, filename):
720
769
  """Create a new nodepool in the cluster, optionally from a file or parameters."""
@@ -730,6 +779,9 @@ def setup_worker_pool(ctx, nodepool_name, count, vmtype, zone, output, dry_run,
730
779
  if zone:
731
780
  nodepool['spec']["zones"] = list(zone)
732
781
 
782
+ if not nodepool['spec']["zones"]:
783
+ raise click.BadArgumentUsage("Missing option '--zone' / '-z'.")
784
+
733
785
  if dry_run:
734
786
  print_output(nodepool, output)
735
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
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")
18
- @click.option('--cluster-name', '-c', required = False, help="Cluster Name")
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,10 +78,10 @@ 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
- @click.option('--type', help="Shell")
84
+ @click.option('--type', help="Shell, supported [bash,zsh]")
86
85
  def install_completion(type):
87
86
  """Install shell completion scripts for the CLI."""
88
87
  install_completions(type)