oks-cli 1.14__py3-none-any.whl

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.
oks_cli/cluster.py ADDED
@@ -0,0 +1,746 @@
1
+ import click
2
+ import subprocess
3
+ import json
4
+ from nacl.public import PrivateKey, SealedBox
5
+ from nacl.encoding import Base64Encoder
6
+
7
+ import time
8
+ import os
9
+ import datetime
10
+ import dateutil.parser
11
+ import human_readable
12
+ import prettytable
13
+ 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
16
+
17
+ from .profile import add_profile
18
+ from .project import project_create, project_login
19
+
20
+ # DEFINE THE CLUSTER GROUP
21
+ @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")
26
+ @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
27
+ @click.pass_context
28
+ def cluster(ctx, project_name, cluster_name, profile):
29
+ """Group of commands related to cluster management."""
30
+ ctx_update(ctx, project_name, cluster_name, profile)
31
+
32
+ # LOGIN ON CLUSTER
33
+ @cluster.command('login', help="Set a default cluster")
34
+ @click.option('--cluster-name', '-c', required=False, help="Name of cluster")
35
+ @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
36
+ @click.pass_context
37
+ def cluster_login(ctx, cluster_name, profile):
38
+ """Set the specified cluster as the default active cluster."""
39
+ _, cluster_name, profile = ctx_update(ctx, None, cluster_name, profile)
40
+ login_profile(profile)
41
+
42
+ project_id = get_project_id()
43
+ data = do_request("GET", 'clusters', params={"name": cluster_name, "project_id": project_id})
44
+
45
+
46
+ if len(data) != 1:
47
+ raise click.BadParameter(
48
+ f"{len(data)} clusters found by name: {cluster_name}")
49
+ cluster = data.pop()
50
+
51
+ cluster_id = cluster['id']
52
+ cluster_name = cluster['name']
53
+
54
+ set_cluster_id(cluster_id)
55
+
56
+ cluster_name = click.style(cluster_name, bold=True)
57
+
58
+ click.echo(f"Logged into cluster: {cluster_name}")
59
+
60
+ # LOGOUT ON CLUSTER
61
+ @cluster.command('logout', help="Unset default cluster")
62
+ @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
63
+ @click.pass_context
64
+ def cluster_logout(ctx, profile):
65
+ """Clear the current default cluster selection."""
66
+ _, _, profile = ctx_update(ctx, None, None, profile)
67
+ login_profile(profile)
68
+ set_cluster_id("")
69
+ click.echo("Logged out from the current cluster")
70
+
71
+ # LIST CLUSTERS
72
+ @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")
77
+ @click.option('--plain', is_flag=True, help="Plain table format")
78
+ @click.option('--msword', is_flag=True, help="Microsoft Word table format")
79
+ @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")
81
+ @click.option('--profile', help="Configuration profile to use")
82
+ @click.pass_context
83
+ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch, output, profile):
84
+ """Display clusters with optional filtering and real-time monitoring."""
85
+ project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
86
+ login_profile(profile)
87
+
88
+ project_id = find_project_id_by_name(project_name)
89
+ cluster_id = get_cluster_id()
90
+
91
+ params = {}
92
+ params['project_id'] = project_id
93
+
94
+ if cluster_name:
95
+ params['name'] = cluster_name
96
+ if deleted:
97
+ params['deleted'] = True
98
+
99
+ field_names = ["NAME", "CREATED", "UPDATED", "STATUS", "DEFAULT"]
100
+
101
+ data = do_request("GET", 'clusters', params=params)
102
+
103
+ if output == "wide":
104
+ field_names.insert(0, "ID")
105
+ field_names.append("VERSION")
106
+ field_names.append("CONTROL PLANE")
107
+ elif output:
108
+ print_output(data, output)
109
+ return
110
+
111
+ table = prettytable.PrettyTable()
112
+ table.field_names = field_names
113
+
114
+ table._min_width = {"CREATED": 13, "UPDATED": 13, "STATUS": 10}
115
+
116
+ if plain or watch:
117
+ table.set_style(prettytable.PLAIN_COLUMNS)
118
+
119
+ if msword:
120
+ table.set_style(prettytable.MSWORD_FRIENDLY)
121
+
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]
147
+
148
+ if output == "wide":
149
+ row.insert(0, cluster['id'])
150
+ row.append(cluster['version'])
151
+ row.append(cluster['control_planes'])
152
+
153
+ return row, status, cluster['name']
154
+
155
+ initial_clusters = {}
156
+ for cluster in data:
157
+ row, _, name = format_row(cluster)
158
+ table.add_row(row)
159
+ initial_clusters[name] = cluster
160
+
161
+ click.echo(table)
162
+
163
+ if watch:
164
+ total_sleep = 0
165
+ try:
166
+ while True:
167
+ time.sleep(2)
168
+ total_sleep += 2
169
+
170
+ try:
171
+ data = do_request("GET", 'clusters', params=params)
172
+ except click.ClickException as err:
173
+ click.echo(f"Error during watch: {err}")
174
+ continue
175
+
176
+ current_cluster_names = {cluster['name'] for cluster in data}
177
+
178
+ for name, cluster in list(initial_clusters.items()):
179
+ if name not in current_cluster_names:
180
+ deleted_cluster = cluster.copy()
181
+ deleted_cluster['statuses']['status'] = 'deleted'
182
+
183
+ row, current_status, _ = format_row(deleted_cluster)
184
+
185
+ new_table = format_changed_row(table, row)
186
+ click.echo(new_table)
187
+
188
+ del initial_clusters[name]
189
+
190
+ for cluster in data:
191
+ row, current_status, name = format_row(cluster)
192
+
193
+ if name not in initial_clusters:
194
+ new_table = format_changed_row(table, row)
195
+ click.echo(new_table)
196
+ initial_clusters[name] = cluster
197
+ continue
198
+
199
+ stored_cluster = initial_clusters[name]
200
+ cluster_status = stored_cluster.get('statuses').get('status')
201
+ if cluster_status != current_status:
202
+ new_table = format_changed_row(table, row)
203
+ click.echo(new_table)
204
+ initial_clusters[name] = cluster
205
+ continue
206
+
207
+ if total_sleep % 10 == 0 and is_interesting_status(current_status):
208
+ new_table = format_changed_row(table, row)
209
+ click.echo(new_table)
210
+ initial_clusters[name] = cluster
211
+
212
+ except KeyboardInterrupt:
213
+ click.echo("\nWatch stopped.")
214
+
215
+
216
+ # GET CLUSTER BY NAME
217
+ @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")
222
+ @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
223
+ @click.pass_context
224
+ def cluster_get_command(ctx, project_name, cluster_name, output, profile):
225
+ """Retrieve and display detailed information about a specific cluster."""
226
+ project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
227
+ login_profile(profile)
228
+
229
+ project_id = find_project_id_by_name(project_name)
230
+ cluster_id = find_cluster_id_by_name(project_id, cluster_name)
231
+
232
+ data = do_request("GET", f'clusters/{cluster_id}')
233
+
234
+ print_output(data, output)
235
+
236
+
237
+ def _create_cluster(project_name, cluster_config, output):
238
+ """Create a new cluster with interactive setup for missing profiles/projects."""
239
+ profiles = profile_list()
240
+ ctx = click.get_current_context()
241
+
242
+ if profiles == {} and click.confirm("Looks like there is no profile set.\nWould you like to add a profile to proceed with cluster creation?"):
243
+
244
+ profile_name = "default"
245
+ region = click.prompt("Choose the region", type=click.Choice(['eu-west-2', 'cloudgouv-eu-west-1'], case_sensitive=False))
246
+ endpoint = None
247
+
248
+ if click.confirm("Do you want to use a custom endpoint?"):
249
+ endpoint = click.prompt("Endpoint")
250
+
251
+ profile_type = click.prompt("Choose the profile type", type=click.Choice(['ak/sk', 'username'], case_sensitive=False))
252
+
253
+ if profile_type == "ak/sk":
254
+ access_key = click.prompt("AccessKey")
255
+ secret_key = click.prompt("SecretKey")
256
+ ctx.invoke(add_profile,
257
+ profile_name=profile_name,
258
+ access_key=access_key,
259
+ secret_key=secret_key,
260
+ region=region,
261
+ endpoint=endpoint)
262
+ else:
263
+ username = click.prompt("Username")
264
+ password = click.prompt("Password")
265
+ ctx.invoke(add_profile,
266
+ profile_name=profile_name,
267
+ username=username,
268
+ password=password,
269
+ region=region,
270
+ endpoint=endpoint)
271
+
272
+ login_profile(profile_name)
273
+
274
+ project_name = project_name or "default"
275
+ projects = do_request("GET", 'projects', params={"name": project_name})
276
+
277
+ cluster_template = get_template('cluster')
278
+ cluster_template.update(cluster_config)
279
+
280
+ project_name_styled = click.style(project_name, bold=True)
281
+ cluster_name_styled = click.style(cluster_template.get("name"), bold=True)
282
+
283
+ msg = f"\nYour cluster {cluster_name_styled} is currently being created.\n"
284
+
285
+ if len(projects) != 1 and click.confirm(f"Unable to find project: {project_name_styled}\nDo you want create a new project for your cluster?"):
286
+ ctx.invoke(project_create, project_name = project_name, output="silent")
287
+ msg = f"\nYour cluster {cluster_name_styled} and project {project_name_styled} are currently being created.\n"
288
+
289
+
290
+ project_id = find_project_id_by_name(project_name)
291
+ ctx.invoke(project_login, project_name = project_name)
292
+
293
+ cluster_template['project_id'] = project_id
294
+
295
+ text = f"{msg}To monitor the progress, please use the following commands.\n\nTo check the progress of project provisioning:\n$ oks-cli project list\n\nAnd to check the progress after cluster provisioning:\n$ oks-cli cluster list\n"
296
+
297
+ cluster_create_in_background(cluster_template, text)
298
+
299
+ else:
300
+ project_id = find_project_id_by_name(project_name)
301
+
302
+ cluster_template = get_template('cluster')
303
+ cluster_template.update(cluster_config)
304
+
305
+ do_request("GET", f'projects/{project_id}')
306
+ cluster_template['project_id'] = project_id
307
+
308
+ data = do_request("POST", 'clusters', json=cluster_template)
309
+ print_output(data, output)
310
+
311
+
312
+ # CLUSTER CREATE BY NAME
313
+ @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")
319
+ @click.option('--cidr-pods', help="CIDR of pods")
320
+ @click.option('--cidr-service', help='CIDR of services')
321
+ @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")
323
+ @click.option('--enable-admission-plugins', help="List of admission plugins, separated by commas")
324
+ @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'")
327
+ @click.option('--disable-api-termination', type=click.BOOL, help="Disable delete action by API")
328
+ @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 ")
331
+ @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
332
+ @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):
334
+ """CLI command to create a new Kubernetes cluster with optional configuration parameters."""
335
+ project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
336
+ login_profile(profile)
337
+
338
+ cluster_config = {}
339
+
340
+ if filename:
341
+ input_data = filename.read()
342
+ cluster_config = detect_and_parse_input(input_data)
343
+
344
+ if not cluster_name and "name" not in cluster_config:
345
+ raise click.BadArgumentUsage("Missing option '--cluster-name' / '-c'.")
346
+
347
+ if cluster_name:
348
+ cluster_config['name'] = cluster_name
349
+
350
+ if description:
351
+ cluster_config['description'] = description
352
+
353
+ if admin:
354
+ cluster_config['admin_whitelist'] = admin.split(',')
355
+
356
+ if version:
357
+ cluster_config['version'] = version
358
+
359
+ if cidr_pods:
360
+ cluster_config['cidr_pods'] = cidr_pods
361
+
362
+ if cidr_service:
363
+ cluster_config['cidr_service'] = cidr_service
364
+
365
+ if control_plane:
366
+ cluster_config['control_planes'] = control_plane
367
+
368
+ if zone:
369
+ if len(zone) > 1:
370
+ cluster_config['cp_multi_az'] = True
371
+ cluster_config['cp_subregions'] = list(zone) # see kube_quirks section
372
+
373
+ if enable_admission_plugins is not None or disable_admission_plugins is not None:
374
+ cluster_config['admission_flags'] = {}
375
+
376
+ if enable_admission_plugins is not None:
377
+ cluster_config['admission_flags']['enable_admission_plugins'] = enable_admission_plugins.split(',')
378
+
379
+ if disable_admission_plugins is not None:
380
+ cluster_config['admission_flags']['disable_admission_plugins'] = disable_admission_plugins.split(',')
381
+
382
+ if tags:
383
+ parsed_tags = {}
384
+
385
+ pairs = tags.split(',')
386
+ for pair in pairs:
387
+ if '=' not in pair:
388
+ raise click.ClickException(f"Malformed tags: '{pair}' (expected key=value)")
389
+ key, value = pair.split('=', 1)
390
+ parsed_tags[key.strip()] = value.strip()
391
+
392
+ cluster_config['tags'] = parsed_tags
393
+
394
+ if quirk:
395
+ # Convert the tuple to a list because multiple=True in the decorator returns a tuple
396
+ cluster_config['quirks'] = transform_tuple(quirk)
397
+
398
+ if disable_api_termination is not None:
399
+ cluster_config["disable_api_termination"] = disable_api_termination
400
+
401
+ if not dry_run:
402
+ _create_cluster(project_name, cluster_config, output)
403
+ else:
404
+ cluster_template = get_template("cluster")
405
+ cluster_template.update(cluster_config)
406
+ print_output(cluster_template, output)
407
+
408
+ # UPDATE CLUSTER
409
+ @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'")
417
+ @click.option('--enable-admission-plugins', help="List of admission plugins, separated by commas")
418
+ @click.option('--disable-admission-plugins', help="List of admission plugins, separated by commas")
419
+ @click.option('--quirk', multiple=True, help="Quirk")
420
+ @click.option('--disable-api-termination', type=click.BOOL, help="Disable delete action by API")
421
+ @click.option('--control-plane', shell_complete=shell_completions, help="Controlplane plan")
422
+ @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 ")
425
+ @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
426
+ @click.pass_context
427
+ 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):
428
+ """CLI command to update an existing Kubernetes cluster with new configuration options."""
429
+ project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
430
+ login_profile(profile)
431
+
432
+ project_id = find_project_id_by_name(project_name)
433
+ cluster_id = find_cluster_id_by_name(project_id, cluster_name)
434
+
435
+ cluster_config = {}
436
+
437
+ if filename:
438
+ input_data = filename.read()
439
+ cluster_config = detect_and_parse_input(input_data)
440
+
441
+ if description:
442
+ cluster_config['description'] = description
443
+
444
+ if admin is not None:
445
+ if len(admin) == 0:
446
+ cluster_config['admin_whitelist'] = []
447
+ else:
448
+ cluster_config['admin_whitelist'] = admin.split(',')
449
+
450
+ if version is not None:
451
+ cluster_config['version'] = version
452
+
453
+ if tags is not None:
454
+ parsed_tags = {}
455
+
456
+ if not len(tags) == 0:
457
+ pairs = tags.split(',')
458
+ for pair in pairs:
459
+ if '=' not in pair:
460
+ raise click.ClickException(f"Malformed tags: '{pair}' (expected key=value)")
461
+ key, value = pair.split('=', 1)
462
+ parsed_tags[key.strip()] = value.strip()
463
+
464
+ cluster_config['tags'] = parsed_tags
465
+
466
+ if enable_admission_plugins is not None or disable_admission_plugins is not None:
467
+ cluster_config['admission_flags'] = {}
468
+
469
+ if enable_admission_plugins is not None:
470
+ if len(enable_admission_plugins) == 0:
471
+ cluster_config['admission_flags']['enable_admission_plugins'] = []
472
+ else:
473
+ cluster_config['admission_flags']['enable_admission_plugins'] = enable_admission_plugins.split(',')
474
+
475
+ if disable_admission_plugins is not None:
476
+ if len(disable_admission_plugins) == 0:
477
+ cluster_config['admission_flags']['disable_admission_plugins'] = []
478
+ else:
479
+ cluster_config['admission_flags']['disable_admission_plugins'] = disable_admission_plugins.split(',')
480
+
481
+ if quirk:
482
+ cluster_config['quirks'] = transform_tuple(quirk)
483
+
484
+ if disable_api_termination is not None:
485
+ cluster_config["disable_api_termination"] = disable_api_termination
486
+
487
+ if control_plane:
488
+ cluster_config['control_planes'] = control_plane
489
+
490
+ if dry_run:
491
+ print_output(cluster_config, output)
492
+ else:
493
+ data = do_request("PATCH", f'clusters/{cluster_id}', json=cluster_config)
494
+ print_output(data, output)
495
+
496
+ # UPGRADE CLUSTER
497
+ @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")
502
+ @click.option('--force', is_flag=True, help="Force upgrade")
503
+ @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
504
+ @click.pass_context
505
+ def cluster_update_command(ctx, project_name, cluster_name, output, force, profile):
506
+ """CLI command to upgrade an existing Kubernetes cluster to the latest supported version."""
507
+ project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
508
+ login_profile(profile)
509
+
510
+ project_id = find_project_id_by_name(project_name)
511
+ cluster_id = find_cluster_id_by_name(project_id, cluster_name)
512
+ cluster_name = get_cluster_name(cluster_name)
513
+
514
+ if force or click.confirm(f"Are you sure you want to upgrade the cluster with name {cluster_name}?", abort=True):
515
+ data = do_request("PATCH", f'clusters/{cluster_id}/upgrade')
516
+ print_output(data, output)
517
+
518
+ # DELETE CLUSTER BY NAME
519
+ @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")
524
+ @click.option('--dry-run', is_flag=True, help="Run without any action")
525
+ @click.option('--force', is_flag=True, help="Force deletion without confirmation")
526
+ @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
527
+ @click.pass_context
528
+ def cluster_delete_command(ctx, project_name, cluster_name, output, dry_run, force, profile):
529
+ """CLI command to delete an existing Kubernetes cluster by name."""
530
+ project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
531
+ login_profile(profile)
532
+
533
+ project_id = find_project_id_by_name(project_name)
534
+ cluster_id = find_cluster_id_by_name(project_id, cluster_name)
535
+ cluster_name = get_cluster_name(cluster_name)
536
+
537
+ cluster_login = get_cluster_id()
538
+
539
+ if dry_run:
540
+ message = {"message": "Dry run: The cluster would be deleted."}
541
+ print_output(message, output)
542
+ return
543
+
544
+ if force or click.confirm(f"Are you sure you want to delete the cluster with name {cluster_name}?", abort=True):
545
+ data = do_request("DELETE", f'clusters/{cluster_id}')
546
+ if cluster_id == cluster_login:
547
+ set_cluster_id("")
548
+ print_output(data, output)
549
+
550
+ # GET KUBECONFIG
551
+ @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")
555
+ @click.option('--print-path', is_flag=True, help="Print path to saved kubeconfig")
556
+ @click.option('--refresh', '--force', is_flag=True, help="Force refresh saved kubeconfig")
557
+ @click.option('--nacl', is_flag=True, help="Use public key encryption on wire (require api support)")
558
+ @click.option('--user', type=click.STRING, help="User")
559
+ @click.option('--group', type=click.STRING, help="Group")
560
+ @click.option('--ttl', type=click.STRING, help="TTL in human readable format (5h, 1d, 1w)")
561
+ @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
562
+ @click.pass_context
563
+ def cluster_kubeconfig_command(ctx, project_name, cluster_name, print_path, refresh, nacl, user, group, ttl, profile):
564
+ """CLI command to fetch and optionally print the kubeconfig for a specified cluster."""
565
+ project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
566
+ login_profile(profile)
567
+
568
+ project_id = find_project_id_by_name(project_name)
569
+ cluster_id = find_cluster_id_by_name(project_id, cluster_name)
570
+
571
+ # @TODO: check expiration in get_cache() code, etc
572
+ kubeconfig_path = get_cache(project_id, cluster_id, 'kubeconfig', user, group)
573
+
574
+ kubeconfig = None
575
+ is_cert_valid = False
576
+
577
+ if kubeconfig_path:
578
+ with open(kubeconfig_path) as f:
579
+ kubeconfig = f.read()
580
+
581
+ if kubeconfig:
582
+ is_cert_valid = verify_certificate(kubeconfig)
583
+
584
+ if not kubeconfig_path or refresh or not is_cert_valid:
585
+ logging.info("extracting kubeconfig by api")
586
+
587
+ params = {}
588
+ if user:
589
+ params["user"] = user
590
+ if group:
591
+ params["group"] = group
592
+ if ttl:
593
+ params["ttl"] = ttl
594
+
595
+ if nacl:
596
+ ephemeral = PrivateKey.generate()
597
+ unsealbox = SealedBox(ephemeral)
598
+
599
+ headers = {
600
+ 'x-encrypt-nacl': ephemeral.public_key.encode(Base64Encoder).decode('ascii')
601
+ }
602
+ kubeconfig_raw = do_request("POST", f'clusters/{cluster_id}/kubeconfig', params = params, headers = headers)['data']['kubeconfig']
603
+ else:
604
+ kubeconfig_raw = do_request("GET", f'clusters/{cluster_id}/kubeconfig', params = params)['data']['kubeconfig']
605
+
606
+ if not kubeconfig_raw:
607
+ logging.error("empty response")
608
+ raise SystemExit()
609
+ elif nacl:
610
+ logging.info("decrypting received kubeconfig")
611
+ kubeconfig = unsealbox.decrypt(kubeconfig_raw.encode('ascii'), encoder = Base64Encoder).decode('ascii')
612
+ else:
613
+ kubeconfig = kubeconfig_raw
614
+
615
+ kubeconfig_path = save_cache(project_id, cluster_id, 'kubeconfig', kubeconfig, user, group)
616
+
617
+ if print_path:
618
+ print(kubeconfig_path)
619
+ else:
620
+ print(kubeconfig)
621
+
622
+
623
+ def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
624
+ """Run a kubectl command using the cached kubeconfig for the specified cluster, refreshing it if needed."""
625
+ # @TODO: check expiration in get_cache() code, etc
626
+ kubeconfig_path = get_cache(project_id, cluster_id, 'kubeconfig', user, group)
627
+
628
+ kubeconfig = None
629
+ is_cert_valid = False
630
+
631
+ if kubeconfig_path :
632
+ with open(kubeconfig_path) as f:
633
+ kubeconfig = f.read()
634
+
635
+ if kubeconfig:
636
+ is_cert_valid = verify_certificate(kubeconfig)
637
+
638
+ if not kubeconfig_path or not is_cert_valid:
639
+ logging.info("extracting kubeconfig by api")
640
+
641
+ kubeconfig_raw = do_request(
642
+ "GET", f'clusters/{cluster_id}/kubeconfig')['data']['kubeconfig']
643
+
644
+ if not kubeconfig_raw:
645
+ print("Cannot get kubeconfig")
646
+ raise SystemExit()
647
+
648
+ kubeconfig_path = save_cache(project_id, cluster_id, 'kubeconfig', kubeconfig_raw, user, group)
649
+
650
+ env = dict(os.environ)
651
+ env['KUBECONFIG'] = str(kubeconfig_path)
652
+ cmd = ['kubectl']
653
+ cmd += list(args)
654
+ logging.info("running %s", cmd)
655
+ if not input:
656
+ return subprocess.run(cmd, env = env)
657
+ else:
658
+ return subprocess.run(cmd, input=input, text=True, env = env)
659
+
660
+
661
+ @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")
664
+ @click.option('--user', type=click.STRING, help="User")
665
+ @click.option('--group', type=click.STRING, help="Group")
666
+ @click.argument("args", nargs=-1, type=click.UNPROCESSED)
667
+ @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
668
+ @click.pass_context
669
+ def cluster_kubectl_command(ctx, project_name, cluster_name, user, group, args, profile):
670
+ """CLI command to run kubectl against a specified cluster using its kubeconfig."""
671
+ project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
672
+ login_profile(profile)
673
+
674
+ project_id = find_project_id_by_name(project_name)
675
+ cluster_id = find_cluster_id_by_name(project_id, cluster_name)
676
+
677
+ _run_kubectl(project_id, cluster_id, user, group, args)
678
+
679
+
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")
683
+ @click.option('--user', type=click.STRING, help="User")
684
+ @click.option('--group', type=click.STRING, help="Group")
685
+ @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
686
+ @click.pass_context
687
+ def nodepool(ctx, project_name, cluster_name, user, group, profile):
688
+ """CLI group for nodepool-related commands, managing project, cluster, user, and profile context."""
689
+ project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
690
+ login_profile(profile)
691
+
692
+ ctx.obj['project_id'] = find_project_id_by_name(project_name)
693
+ ctx.obj['cluster_id'] = find_cluster_id_by_name(
694
+ ctx.obj['project_id'], cluster_name)
695
+ ctx.obj['user'] = user
696
+ ctx.obj['group'] = group
697
+
698
+
699
+ cluster.add_command(nodepool)
700
+
701
+
702
+ @nodepool.command('list')
703
+ @click.pass_context
704
+ def nodepool_list(ctx):
705
+ """List nodepools in the specified cluster using kubectl."""
706
+ _run_kubectl(ctx.obj['project_id'], ctx.obj['cluster_id'], ctx.obj['user'], ctx.obj['group'], [
707
+ 'get', 'nodepool', '-o', 'wide'])
708
+
709
+
710
+ @nodepool.command('create')
711
+ @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")
716
+ @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 ")
718
+ @click.pass_context
719
+ def setup_worker_pool(ctx, nodepool_name, count, vmtype, zone, output, dry_run, filename):
720
+ """Create a new nodepool in the cluster, optionally from a file or parameters."""
721
+ nodepool = get_template("nodepool")
722
+
723
+ if filename:
724
+ input_data = filename.read()
725
+ nodepool = detect_and_parse_input(input_data)
726
+ else:
727
+ nodepool['metadata']["name"] = nodepool_name
728
+ nodepool['spec']["desiredNodes"] = count
729
+ nodepool['spec']["nodeType"] = vmtype
730
+ if zone:
731
+ nodepool['spec']["zones"] = list(zone)
732
+
733
+ if dry_run:
734
+ print_output(nodepool, output)
735
+ else:
736
+ _run_kubectl(ctx.obj['project_id'], ctx.obj['cluster_id'], ctx.obj['user'], ctx.obj['group'], [
737
+ 'create', '-f', '-'], input=json.dumps(nodepool))
738
+
739
+
740
+ @nodepool.command('delete')
741
+ @click.option('--nodepool-name', '-n', required=True, help="Nodepool Name")
742
+ @click.pass_context
743
+ def delete_worker_pool(ctx, nodepool_name):
744
+ """Delete a nodepool by name from the cluster."""
745
+ _run_kubectl(ctx.obj['project_id'], ctx.obj['cluster_id'], ctx.obj['user'], ctx.obj['group'], [
746
+ 'delete', 'nodepool', nodepool_name])