oks-cli 1.15__tar.gz → 1.17__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.17}/PKG-INFO +2 -2
  2. {oks_cli-1.15 → oks_cli-1.17}/oks_cli/cache.py +15 -14
  3. {oks_cli-1.15 → oks_cli-1.17}/oks_cli/cluster.py +194 -118
  4. {oks_cli-1.15 → oks_cli-1.17}/oks_cli/main.py +7 -8
  5. {oks_cli-1.15 → oks_cli-1.17}/oks_cli/profile.py +5 -5
  6. {oks_cli-1.15 → oks_cli-1.17}/oks_cli/project.py +45 -60
  7. {oks_cli-1.15 → oks_cli-1.17}/oks_cli/quotas.py +1 -1
  8. {oks_cli-1.15 → oks_cli-1.17}/oks_cli/utils.py +107 -11
  9. {oks_cli-1.15 → oks_cli-1.17}/oks_cli.egg-info/PKG-INFO +2 -2
  10. {oks_cli-1.15 → oks_cli-1.17}/oks_cli.egg-info/requires.txt +1 -1
  11. {oks_cli-1.15 → oks_cli-1.17}/setup.py +2 -2
  12. {oks_cli-1.15 → oks_cli-1.17}/tests/test_cache.py +1 -1
  13. {oks_cli-1.15 → oks_cli-1.17}/tests/test_cluster.py +159 -3
  14. {oks_cli-1.15 → oks_cli-1.17}/tests/test_project.py +130 -3
  15. {oks_cli-1.15 → oks_cli-1.17}/LICENSE +0 -0
  16. {oks_cli-1.15 → oks_cli-1.17}/README.md +0 -0
  17. {oks_cli-1.15 → oks_cli-1.17}/oks_cli/__init__.py +0 -0
  18. {oks_cli-1.15 → oks_cli-1.17}/oks_cli.egg-info/SOURCES.txt +0 -0
  19. {oks_cli-1.15 → oks_cli-1.17}/oks_cli.egg-info/dependency_links.txt +0 -0
  20. {oks_cli-1.15 → oks_cli-1.17}/oks_cli.egg-info/entry_points.txt +0 -0
  21. {oks_cli-1.15 → oks_cli-1.17}/oks_cli.egg-info/top_level.txt +0 -0
  22. {oks_cli-1.15 → oks_cli-1.17}/setup.cfg +0 -0
  23. {oks_cli-1.15 → oks_cli-1.17}/tests/test_nodepool.py +0 -0
  24. {oks_cli-1.15 → oks_cli-1.17}/tests/test_profile.py +0 -0
  25. {oks_cli-1.15 → oks_cli-1.17}/tests/test_quota.py +0 -0
  26. {oks_cli-1.15 → oks_cli-1.17}/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.17
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
3
- import prettytable
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
+
5
+ from prettytable import TableStyle
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 = TableStyle.PLAIN_COLUMNS
61
+ if msword:
62
+ style = TableStyle.MSWORD_FRIENDLY
62
63
 
63
- click.echo(table)
64
+ print_table(data, fields, style=style)
@@ -6,23 +6,32 @@ 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 prettytable import TableStyle
18
+ from .utils import cluster_completer, do_request, print_output, \
19
+ find_project_id_by_name, find_cluster_id_by_name, \
20
+ get_cache, save_cache, detect_and_parse_input, \
21
+ verify_certificate, shell_completions, transform_tuple, \
22
+ profile_list, login_profile, cluster_create_in_background, \
23
+ ctx_update, set_cluster_id, get_cluster_id, get_project_id, \
24
+ get_template, get_cluster_name, format_changed_row, \
25
+ is_interesting_status, profile_completer, project_completer, \
26
+ kubeconfig_parse_fields, print_table, format_row
16
27
 
17
28
  from .profile import add_profile
18
29
  from .project import project_create, project_login
19
30
 
20
31
  # DEFINE THE CLUSTER GROUP
21
32
  @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)
33
+ @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
34
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
26
35
  @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
27
36
  @click.pass_context
28
37
  def cluster(ctx, project_name, cluster_name, profile):
@@ -31,7 +40,7 @@ def cluster(ctx, project_name, cluster_name, profile):
31
40
 
32
41
  # LOGIN ON CLUSTER
33
42
  @cluster.command('login', help="Set a default cluster")
34
- @click.option('--cluster-name', '-c', required=False, help="Name of cluster", shell_complete=cluster_completer)
43
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Name of cluster", shell_complete=cluster_completer)
35
44
  @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
36
45
  @click.pass_context
37
46
  def cluster_login(ctx, cluster_name, profile):
@@ -71,34 +80,48 @@ def cluster_logout(ctx, profile):
71
80
  # LIST CLUSTERS
72
81
  @cluster.command('list', help="List all clusters")
73
82
  @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")
83
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
84
+ @click.option('--deleted', '-x', is_flag=True, help="List deleted clusters") # x pour "deleted" / "removed"
77
85
  @click.option('--plain', is_flag=True, help="Plain table format")
78
86
  @click.option('--msword', is_flag=True, help="Microsoft Word table format")
79
87
  @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")
88
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml", "wide"]), help="Specify output format")
81
89
  @click.option('--profile', help="Configuration profile to use")
90
+ @click.option('--all', '-A', is_flag=True, help="List clusters from all projects")
82
91
  @click.pass_context
83
- def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch, output, profile):
92
+ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch, output, profile, all):
84
93
  """Display clusters with optional filtering and real-time monitoring."""
85
94
  project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
86
95
  login_profile(profile)
87
96
 
88
- project_id = find_project_id_by_name(project_name)
89
- cluster_id = get_cluster_id()
90
-
97
+ profile_name = os.getenv('OKS_PROFILE')
98
+ region_name = os.getenv('OKS_REGION')
91
99
  params = {}
92
- params['project_id'] = project_id
100
+
101
+ if not all:
102
+ project_id = find_project_id_by_name(project_name)
103
+ params['project_id'] = project_id
104
+
105
+ cluster_id = get_cluster_id()
93
106
 
94
107
  if cluster_name:
95
108
  params['name'] = cluster_name
96
109
  if deleted:
97
110
  params['deleted'] = True
98
111
 
99
- field_names = ["NAME", "CREATED", "UPDATED", "STATUS", "DEFAULT"]
112
+ field_names = ["CLUSTER", "PROFILE", "REGION", "CREATED", "UPDATED", "STATUS", "DEFAULT"]
113
+
114
+ if all:
115
+ field_names.insert(0, "PROJECT")
100
116
 
101
- data = do_request("GET", 'clusters', params=params)
117
+ projects = {project["id"]: project for project in do_request("GET", "projects")}
118
+ data = do_request("GET", "clusters/all", params=params)
119
+
120
+ for cluster in data:
121
+ project = projects.get(cluster.get("project_id"))
122
+ cluster["project_name"] = project.get("name")
123
+ else:
124
+ data = do_request("GET", "clusters", params=params)
102
125
 
103
126
  if output == "wide":
104
127
  field_names.insert(0, "ID")
@@ -114,49 +137,27 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
114
137
  table._min_width = {"CREATED": 13, "UPDATED": 13, "STATUS": 10}
115
138
 
116
139
  if plain or watch:
117
- table.set_style(prettytable.PLAIN_COLUMNS)
140
+ table.set_style(TableStyle.PLAIN_COLUMNS)
118
141
 
119
142
  if msword:
120
143
  table.set_style(prettytable.MSWORD_FRIENDLY)
121
144
 
122
- def format_row(cluster):
123
- status = cluster['statuses']['status']
124
-
125
- is_default = True if cluster.get('id') == cluster_id else False
126
-
127
- if status == 'ready':
128
- msg = click.style(status, fg='green')
129
- elif status == 'failed' or status == 'deleted':
130
- msg = click.style(status, fg='red')
131
- elif status == 'deploying':
132
- msg = click.style(status, fg='yellow')
133
- else:
134
- msg = status
135
-
136
- name = click.style(cluster['name'], bold=True)
137
- if is_default:
138
- default = "*"
139
- else:
140
- default = ""
141
-
142
- created_at = dateutil.parser.parse(cluster['statuses']['created_at'])
143
- updated_at = dateutil.parser.parse(cluster['statuses']['updated_at'])
144
- now = datetime.datetime.now(tz = created_at.tzinfo)
145
-
146
- row = [name, human_readable.date_time(now - created_at), human_readable.date_time(now - updated_at), msg, default]
145
+ initial_clusters = {}
147
146
 
147
+ for cluster in data:
148
+ row, _, name = format_row(cluster.get('statuses'), cluster.get('name'), cluster_id == cluster.get('id'))
149
+ row.insert(1, profile_name)
150
+ row.insert(2, region_name)
151
+ if all:
152
+ project_name = click.style(cluster.get("project_name"), bold=True)
153
+ row.insert(0, project_name)
148
154
  if output == "wide":
149
- row.insert(0, cluster['id'])
150
- row.append(cluster['version'])
151
- row.append(cluster['control_planes'])
155
+ row.insert(0, cluster.get('id'))
156
+ row.append(cluster.get('version'))
157
+ row.append(cluster.get('control_planes'))
152
158
 
153
- return row, status, cluster['name']
154
-
155
- initial_clusters = {}
156
- for cluster in data:
157
- row, _, name = format_row(cluster)
158
159
  table.add_row(row)
159
- initial_clusters[name] = cluster
160
+ initial_clusters[cluster.get("id")] = cluster
160
161
 
161
162
  click.echo(table)
162
163
 
@@ -168,46 +169,66 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
168
169
  total_sleep += 2
169
170
 
170
171
  try:
171
- data = do_request("GET", 'clusters', params=params)
172
+ if all:
173
+ projects = {project["id"]: project for project in do_request("GET", "projects")}
174
+ data = do_request("GET", "clusters/all", params=params)
175
+
176
+ for cluster in data:
177
+ project = projects.get(cluster.get("project_id"))
178
+ cluster["project_name"] = project.get("name")
179
+ else:
180
+ data = do_request("GET", 'clusters', params=params)
172
181
  except click.ClickException as err:
173
182
  click.echo(f"Error during watch: {err}")
174
183
  continue
175
184
 
176
- current_cluster_names = {cluster['name'] for cluster in data}
185
+ current_cluster_ids = {cluster.get('id') for cluster in data}
177
186
 
178
- for name, cluster in list(initial_clusters.items()):
179
- if name not in current_cluster_names:
187
+ for id, cluster in list(initial_clusters.items()):
188
+ if id not in current_cluster_ids:
180
189
  deleted_cluster = cluster.copy()
181
190
  deleted_cluster['statuses']['status'] = 'deleted'
182
191
 
183
- row, current_status, _ = format_row(deleted_cluster)
192
+ row, current_status, _ = format_row(deleted_cluster.get('statuses'), deleted_cluster.get('name'), cluster_id == deleted_cluster.get('id'))
193
+ row.insert(1, profile_name)
194
+ row.insert(2, region_name)
195
+ if all:
196
+ project_name = click.style(cluster.get("project_name"), bold=True)
197
+ row.insert(0, project_name)
184
198
 
185
199
  new_table = format_changed_row(table, row)
186
200
  click.echo(new_table)
187
201
 
188
- del initial_clusters[name]
202
+ del initial_clusters[id]
189
203
 
190
204
  for cluster in data:
191
- row, current_status, name = format_row(cluster)
205
+ row, current_status, name = format_row(cluster.get('statuses'), cluster.get('name'), cluster_id == cluster.get('id'))
206
+ row.insert(1, profile_name)
207
+ row.insert(2, region_name)
208
+ if all:
209
+ project_name = click.style(cluster.get("project_name"), bold=True)
210
+ row.insert(0, project_name)
192
211
 
193
- if name not in initial_clusters:
212
+ cl_id = cluster.get('id')
213
+
214
+ if cl_id not in initial_clusters:
194
215
  new_table = format_changed_row(table, row)
195
216
  click.echo(new_table)
196
- initial_clusters[name] = cluster
217
+ initial_clusters[cl_id] = cluster
197
218
  continue
198
219
 
199
- stored_cluster = initial_clusters[name]
220
+ stored_cluster = initial_clusters[cl_id]
200
221
  cluster_status = stored_cluster.get('statuses').get('status')
201
222
  if cluster_status != current_status:
202
223
  new_table = format_changed_row(table, row)
203
224
  click.echo(new_table)
204
- initial_clusters[name] = cluster
225
+ initial_clusters[cl_id] = cluster
205
226
  continue
206
227
 
207
228
  if total_sleep % 10 == 0 and is_interesting_status(current_status):
208
229
  new_table = format_changed_row(table, row)
209
230
  click.echo(new_table)
210
- initial_clusters[name] = cluster
231
+ initial_clusters[cl_id] = cluster
211
232
 
212
233
  except KeyboardInterrupt:
213
234
  click.echo("\nWatch stopped.")
@@ -215,10 +236,9 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
215
236
 
216
237
  # GET CLUSTER BY NAME
217
238
  @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")
239
+ @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
240
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
241
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
222
242
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
223
243
  @click.pass_context
224
244
  def cluster_get_command(ctx, project_name, cluster_name, output, profile):
@@ -234,6 +254,26 @@ def cluster_get_command(ctx, project_name, cluster_name, output, profile):
234
254
  print_output(data, output)
235
255
 
236
256
 
257
+ def prepare_cluster_template(cluster_config):
258
+ cluster_template = get_template("cluster")
259
+
260
+ admin_whitelist = cluster_config.get("admin_whitelist") or []
261
+ if isinstance(admin_whitelist, str):
262
+ admin_whitelist = [admin_whitelist]
263
+
264
+ final_whitelist = []
265
+
266
+ for entry in admin_whitelist:
267
+ if entry == "my-ip":
268
+ final_whitelist.extend(cluster_template.get("admin_whitelist", []))
269
+ else:
270
+ final_whitelist.append(entry)
271
+
272
+ cluster_config["admin_whitelist"] = list(dict.fromkeys(final_whitelist))
273
+
274
+ cluster_template.update(cluster_config)
275
+ return cluster_template
276
+
237
277
  def _create_cluster(project_name, cluster_config, output):
238
278
  """Create a new cluster with interactive setup for missing profiles/projects."""
239
279
  profiles = profile_list()
@@ -274,8 +314,8 @@ def _create_cluster(project_name, cluster_config, output):
274
314
  project_name = project_name or "default"
275
315
  projects = do_request("GET", 'projects', params={"name": project_name})
276
316
 
277
- cluster_template = get_template('cluster')
278
- cluster_template.update(cluster_config)
317
+ cluster_template = prepare_cluster_template(cluster_config)
318
+ print_output(cluster_template, output)
279
319
 
280
320
  project_name_styled = click.style(project_name, bold=True)
281
321
  cluster_name_styled = click.style(cluster_template.get("name"), bold=True)
@@ -299,9 +339,7 @@ def _create_cluster(project_name, cluster_config, output):
299
339
  else:
300
340
  project_id = find_project_id_by_name(project_name)
301
341
 
302
- cluster_template = get_template('cluster')
303
- cluster_template.update(cluster_config)
304
-
342
+ cluster_template = prepare_cluster_template(cluster_config)
305
343
  do_request("GET", f'projects/{project_id}')
306
344
  cluster_template['project_id'] = project_id
307
345
 
@@ -313,22 +351,22 @@ def _create_cluster(project_name, cluster_config, output):
313
351
  @cluster.command('create', help="Create a new cluster")
314
352
  @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
315
353
  @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")
354
+ @click.option('--description', '-d', help="Description of the cluster")
355
+ @click.option('--admin', '-a', help="Admin Whitelist ips. you can use 'my-ip' to automatically use your current IP.")
356
+ @click.option('--version', '-v', shell_complete=shell_completions, help="Kubernetes version")
319
357
  @click.option('--cidr-pods', help="CIDR of pods")
320
358
  @click.option('--cidr-service', help='CIDR of services')
321
359
  @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")
360
+ @click.option('--zone', '-z', multiple=True, shell_complete=shell_completions, help="List of Control Plane availability zones")
323
361
  @click.option('--enable-admission-plugins', help="List of admission plugins, separated by commas")
324
362
  @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'")
363
+ @click.option('--quirk', '-q', multiple=True, help="Quirk")
364
+ @click.option('--tags', '-t', help="Comma-separated list of tags, example: 'key1=value1,key2=value2'")
327
365
  @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")
366
+ @click.option('--cp-multi-az', '-m', is_flag=True, help="Enable control plane multi AZ")
329
367
  @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 ")
368
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
369
+ @click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to create the cluster ")
332
370
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
333
371
  @click.pass_context
334
372
  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 +443,25 @@ def cluster_create_command(ctx, project_name, cluster_name, description, admin,
405
443
  if not dry_run:
406
444
  _create_cluster(project_name, cluster_config, output)
407
445
  else:
408
- cluster_template = get_template("cluster")
409
- cluster_template.update(cluster_config)
446
+ cluster_template = prepare_cluster_template(cluster_config)
410
447
  print_output(cluster_template, output)
411
448
 
412
449
  # UPDATE CLUSTER
413
450
  @cluster.command('update', help="Update a cluster by name")
414
451
  @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'")
452
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster name", shell_complete=cluster_completer)
453
+ @click.option('--description', '-d', help="Description of the cluster")
454
+ @click.option('--admin', '-a', help="Admin Whitelist ips. you can use 'my-ip' to automatically use your current IP.")
455
+ @click.option('--version', '-v', shell_complete=shell_completions, help="Kubernetes version")
456
+ @click.option('--tags', '-t', help="Comma-separated list of tags, example: 'key1=value1,key2=value2'")
421
457
  @click.option('--enable-admission-plugins', help="List of admission plugins, separated by commas")
422
458
  @click.option('--disable-admission-plugins', help="List of admission plugins, separated by commas")
423
- @click.option('--quirk', multiple=True, help="Quirk")
459
+ @click.option('--quirk', '-q', multiple=True, help="Quirk")
424
460
  @click.option('--disable-api-termination', type=click.BOOL, help="Disable delete action by API")
425
461
  @click.option('--control-plane', shell_complete=shell_completions, help="Controlplane plan")
426
462
  @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 ")
463
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
464
+ @click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to update the cluster ")
429
465
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
430
466
  @click.pass_context
431
467
  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):
@@ -449,7 +485,23 @@ def cluster_update_command(ctx, project_name, cluster_name, description, admin,
449
485
  if len(admin) == 0:
450
486
  cluster_config['admin_whitelist'] = []
451
487
  else:
452
- cluster_config['admin_whitelist'] = admin.split(',')
488
+ admin_list = admin.split(',')
489
+ resolved_ips = []
490
+ for ip in admin_list:
491
+ ip = ip.strip()
492
+ if ip == "my-ip":
493
+ try:
494
+ data = do_request("GET", "myip")
495
+ if isinstance(data, dict) and "x_real_ip" in data:
496
+ resolved_ip = data["x_real_ip"]
497
+ resolved_ips.append(f"{resolved_ip}/32")
498
+ else:
499
+ raise click.ClickException(f"Unexpected response format from 'myip': {data}")
500
+ except Exception as e:
501
+ raise click.ClickException(f"Unable to resolve 'my-ip': {e}")
502
+ else:
503
+ resolved_ips.append(ip)
504
+ cluster_config['admin_whitelist'] = resolved_ips
453
505
 
454
506
  if version is not None:
455
507
  cluster_config['version'] = version
@@ -500,9 +552,8 @@ def cluster_update_command(ctx, project_name, cluster_name, description, admin,
500
552
  # UPGRADE CLUSTER
501
553
  @cluster.command('upgrade', help="Upgrade a cluster by name")
502
554
  @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")
555
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster name", shell_complete=cluster_completer)
556
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
506
557
  @click.option('--force', is_flag=True, help="Force upgrade")
507
558
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
508
559
  @click.pass_context
@@ -522,9 +573,8 @@ def cluster_update_command(ctx, project_name, cluster_name, output, force, profi
522
573
  # DELETE CLUSTER BY NAME
523
574
  @cluster.command('delete', help="Delete a cluster by name")
524
575
  @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")
576
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster name", shell_complete=cluster_completer)
577
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
528
578
  @click.option('--dry-run', is_flag=True, help="Run without any action")
529
579
  @click.option('--force', is_flag=True, help="Force deletion without confirmation")
530
580
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
@@ -554,9 +604,10 @@ def cluster_delete_command(ctx, project_name, cluster_name, output, dry_run, for
554
604
  # GET KUBECONFIG
555
605
  @cluster.command('kubeconfig', help="Fetch the kubeconfig for a cluster")
556
606
  @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)
607
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
559
608
  @click.option('--print-path', is_flag=True, help="Print path to saved kubeconfig")
609
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml", "table"]), default="yaml", help="Specify output format, default is yaml")
610
+ @click.option('--wide', is_flag=True, help="Prints additional info, only supported for table output")
560
611
  @click.option('--refresh', '--force', is_flag=True, help="Force refresh saved kubeconfig")
561
612
  @click.option('--nacl', is_flag=True, help="Use public key encryption on wire (require api support)")
562
613
  @click.option('--user', type=click.STRING, help="User")
@@ -564,7 +615,7 @@ def cluster_delete_command(ctx, project_name, cluster_name, output, dry_run, for
564
615
  @click.option('--ttl', type=click.STRING, help="TTL in human readable format (5h, 1d, 1w)")
565
616
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
566
617
  @click.pass_context
567
- def cluster_kubeconfig_command(ctx, project_name, cluster_name, print_path, refresh, nacl, user, group, ttl, profile):
618
+ def cluster_kubeconfig_command(ctx, project_name, cluster_name, print_path, output, wide, refresh, nacl, user, group, ttl, profile):
568
619
  """CLI command to fetch and optionally print the kubeconfig for a specified cluster."""
569
620
  project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
570
621
  login_profile(profile)
@@ -619,9 +670,31 @@ def cluster_kubeconfig_command(ctx, project_name, cluster_name, print_path, refr
619
670
  kubeconfig_path = save_cache(project_id, cluster_id, 'kubeconfig', kubeconfig, user, group)
620
671
 
621
672
  if print_path:
622
- print(kubeconfig_path)
673
+ click.echo(kubeconfig_path)
623
674
  else:
624
- print(kubeconfig)
675
+ if output == 'table':
676
+ kubeconfig_path = pathlib.Path(kubeconfig_path).absolute()
677
+ if not user:
678
+ user = kubeconfig_path.parts[-3]
679
+ if not group:
680
+ group = kubeconfig_path.parts[-2]
681
+ if kubeconfig_path.is_file():
682
+ with kubeconfig_path.open() as f:
683
+ kubeconfig_str = f.read()
684
+ kubedata = kubeconfig_parse_fields(kubeconfig_str, cluster_name, user, group)
685
+ if not len(kubedata):
686
+ raise SystemExit("Something went wrong, could not parse kubeconfig")
687
+ fields = [["user", "user"], ["group", "group"], ["expiration date", "expires_at"]]
688
+ if wide:
689
+ fields.extend([["Cert subject", "cn"], ["context:name", "context_name"], ["context:user", "ctx_user"],
690
+ ["context:cluster", "cluster_name"], ["cluster endpoint", "server_name"]])
691
+ print_table(kubedata, fields)
692
+ else:
693
+ raise SystemExit(f"Could not find {kubeconfig_path}")
694
+ elif output == 'json':
695
+ click.echo(json.dumps(yaml.safe_load(kubeconfig)))
696
+ else:
697
+ click.echo(kubeconfig)
625
698
 
626
699
 
627
700
  def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
@@ -646,7 +719,7 @@ def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
646
719
  "GET", f'clusters/{cluster_id}/kubeconfig')['data']['kubeconfig']
647
720
 
648
721
  if not kubeconfig_raw:
649
- print("Cannot get kubeconfig")
722
+ click.echo("Cannot get kubeconfig")
650
723
  raise SystemExit()
651
724
 
652
725
  kubeconfig_path = save_cache(project_id, cluster_id, 'kubeconfig', kubeconfig_raw, user, group)
@@ -664,11 +737,11 @@ def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
664
737
 
665
738
  @cluster.command('kubectl', help='Fetch the kubeconfig for a cluster and run kubectl against it', context_settings={"ignore_unknown_options": True})
666
739
  @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)
740
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
668
741
  @click.option('--user', type=click.STRING, help="User")
669
742
  @click.option('--group', type=click.STRING, help="Group")
670
- @click.argument("args", nargs=-1, type=click.UNPROCESSED)
671
743
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
744
+ @click.argument("args", nargs=-1, type=click.UNPROCESSED)
672
745
  @click.pass_context
673
746
  def cluster_kubectl_command(ctx, project_name, cluster_name, user, group, args, profile):
674
747
  """CLI command to run kubectl against a specified cluster using its kubeconfig."""
@@ -681,9 +754,9 @@ def cluster_kubectl_command(ctx, project_name, cluster_name, user, group, args,
681
754
  _run_kubectl(project_id, cluster_id, user, group, args)
682
755
 
683
756
 
684
- @click.group(help="nodepool related commands.")
757
+ @click.group(help="Nodepool related commands.")
685
758
  @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)
759
+ @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
687
760
  @click.option('--user', type=click.STRING, help="User")
688
761
  @click.option('--group', type=click.STRING, help="Group")
689
762
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
@@ -711,14 +784,14 @@ def nodepool_list(ctx):
711
784
  'get', 'nodepool', '-o', 'wide'])
712
785
 
713
786
 
714
- @nodepool.command('create')
787
+ @nodepool.command('create', help="Create a new nodepool")
715
788
  @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")
789
+ @click.option('--count', '-c', default=2, help="Count of nodes")
790
+ @click.option('--type', 'vmtype', '-t', default="tinav6.c2r4p3", help="Type of VMs")
791
+ @click.option('--zone', '-z', multiple=True, help="Provide zone(s)")
792
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
720
793
  @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 ")
794
+ @click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to create the Nodepool")
722
795
  @click.pass_context
723
796
  def setup_worker_pool(ctx, nodepool_name, count, vmtype, zone, output, dry_run, filename):
724
797
  """Create a new nodepool in the cluster, optionally from a file or parameters."""
@@ -734,6 +807,9 @@ def setup_worker_pool(ctx, nodepool_name, count, vmtype, zone, output, dry_run,
734
807
  if zone:
735
808
  nodepool['spec']["zones"] = list(zone)
736
809
 
810
+ if not nodepool['spec']["zones"]:
811
+ raise click.BadArgumentUsage("Missing option '--zone' / '-z'.")
812
+
737
813
  if dry_run:
738
814
  print_output(nodepool, output)
739
815
  else: