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