oks-cli 1.19__tar.gz → 1.21__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.19 → oks_cli-1.21}/PKG-INFO +2 -2
- {oks_cli-1.19 → oks_cli-1.21}/oks_cli/cache.py +1 -1
- {oks_cli-1.19 → oks_cli-1.21}/oks_cli/cluster.py +67 -45
- {oks_cli-1.19 → oks_cli-1.21}/oks_cli/main.py +3 -1
- {oks_cli-1.19 → oks_cli-1.21}/oks_cli/profile.py +1 -1
- {oks_cli-1.19 → oks_cli-1.21}/oks_cli/project.py +17 -3
- oks_cli-1.21/oks_cli/user.py +169 -0
- {oks_cli-1.19 → oks_cli-1.21}/oks_cli/utils.py +29 -26
- {oks_cli-1.19 → oks_cli-1.21}/oks_cli.egg-info/PKG-INFO +2 -2
- {oks_cli-1.19 → oks_cli-1.21}/oks_cli.egg-info/SOURCES.txt +3 -1
- {oks_cli-1.19 → oks_cli-1.21}/oks_cli.egg-info/requires.txt +1 -1
- {oks_cli-1.19 → oks_cli-1.21}/setup.py +3 -3
- {oks_cli-1.19 → oks_cli-1.21}/tests/test_shell_completion.py +2 -2
- oks_cli-1.21/tests/test_user.py +56 -0
- {oks_cli-1.19 → oks_cli-1.21}/LICENSE +0 -0
- {oks_cli-1.19 → oks_cli-1.21}/README.md +0 -0
- {oks_cli-1.19 → oks_cli-1.21}/oks_cli/__init__.py +0 -0
- {oks_cli-1.19 → oks_cli-1.21}/oks_cli/netpeering.py +0 -0
- {oks_cli-1.19 → oks_cli-1.21}/oks_cli/quotas.py +0 -0
- {oks_cli-1.19 → oks_cli-1.21}/oks_cli.egg-info/dependency_links.txt +0 -0
- {oks_cli-1.19 → oks_cli-1.21}/oks_cli.egg-info/entry_points.txt +0 -0
- {oks_cli-1.19 → oks_cli-1.21}/oks_cli.egg-info/top_level.txt +0 -0
- {oks_cli-1.19 → oks_cli-1.21}/setup.cfg +0 -0
- {oks_cli-1.19 → oks_cli-1.21}/tests/test_cache.py +0 -0
- {oks_cli-1.19 → oks_cli-1.21}/tests/test_cluster.py +0 -0
- {oks_cli-1.19 → oks_cli-1.21}/tests/test_netpeering.py +0 -0
- {oks_cli-1.19 → oks_cli-1.21}/tests/test_nodepool.py +0 -0
- {oks_cli-1.19 → oks_cli-1.21}/tests/test_profile.py +0 -0
- {oks_cli-1.19 → oks_cli-1.21}/tests/test_project.py +0 -0
- {oks_cli-1.19 → oks_cli-1.21}/tests/test_quota.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oks-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.21
|
|
4
4
|
Author: Outscale SAS
|
|
5
5
|
Author-email: opensource@outscale.com
|
|
6
6
|
License: BSD
|
|
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: certifi>=2024.8.30
|
|
19
19
|
Requires-Dist: charset-normalizer>=3.3.2
|
|
20
|
-
Requires-Dist: click<8.3.0,>=8.
|
|
20
|
+
Requires-Dist: click<8.3.0,>=8.2.0
|
|
21
21
|
Requires-Dist: colorama>=0.4.6
|
|
22
22
|
Requires-Dist: idna>=3.10
|
|
23
23
|
Requires-Dist: pyyaml>=6.0.2
|
|
@@ -38,7 +38,7 @@ def list_kubeconfigs(ctx, project_name, cluster_name, plain, msword, profile):
|
|
|
38
38
|
|
|
39
39
|
result = get_all_cache(project_id, cluster_id, "kubeconfig")
|
|
40
40
|
|
|
41
|
-
data =
|
|
41
|
+
data = []
|
|
42
42
|
fields = [["user", "user"],["group", "group"], ["expiration date", "expires_at"]]
|
|
43
43
|
|
|
44
44
|
for element in result:
|
|
@@ -74,6 +74,13 @@ def cluster_logout(ctx, profile):
|
|
|
74
74
|
"""Clear the current default cluster selection."""
|
|
75
75
|
_, _, profile = ctx_update(ctx, None, None, profile)
|
|
76
76
|
login_profile(profile)
|
|
77
|
+
|
|
78
|
+
current_cluster = get_cluster_id()
|
|
79
|
+
|
|
80
|
+
if not current_cluster:
|
|
81
|
+
click.echo("You are not connected to any cluster.")
|
|
82
|
+
return
|
|
83
|
+
|
|
77
84
|
set_cluster_id("")
|
|
78
85
|
click.echo("Logged out from the current cluster")
|
|
79
86
|
|
|
@@ -114,20 +121,20 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
|
|
|
114
121
|
if all:
|
|
115
122
|
field_names.insert(0, "PROJECT")
|
|
116
123
|
|
|
117
|
-
projects = {project["id"]: project for project in do_request("GET", "projects")}
|
|
118
|
-
data = do_request("GET", "clusters/all", params=params)
|
|
119
|
-
|
|
120
|
-
for cluster in data:
|
|
121
|
-
project = projects.get(cluster.get("project_id"))
|
|
122
|
-
cluster["project_name"] = project.get("name")
|
|
123
|
-
else:
|
|
124
|
-
data = do_request("GET", "clusters", params=params)
|
|
125
|
-
|
|
126
124
|
if output == "wide":
|
|
127
125
|
field_names.insert(0, "ID")
|
|
128
126
|
field_names.append("VERSION")
|
|
129
127
|
field_names.append("CONTROL PLANE")
|
|
130
128
|
elif output:
|
|
129
|
+
if all:
|
|
130
|
+
projects = {p["id"]: p for p in do_request("GET", "projects")}
|
|
131
|
+
data = do_request("GET", "clusters/all", params=params)
|
|
132
|
+
for cluster in data:
|
|
133
|
+
project = projects.get(cluster.get("project_id"))
|
|
134
|
+
cluster["project_name"] = project.get("name")
|
|
135
|
+
else:
|
|
136
|
+
data = do_request("GET", "clusters", params=params)
|
|
137
|
+
|
|
131
138
|
print_output(data, output)
|
|
132
139
|
return
|
|
133
140
|
|
|
@@ -142,10 +149,14 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
|
|
|
142
149
|
if msword:
|
|
143
150
|
table.set_style(TableStyle.MSWORD_FRIENDLY)
|
|
144
151
|
|
|
145
|
-
initial_clusters = {}
|
|
146
152
|
|
|
147
|
-
|
|
148
|
-
row,
|
|
153
|
+
def build_row(cluster):
|
|
154
|
+
row, current_status, _ = format_row(
|
|
155
|
+
cluster.get('statuses'),
|
|
156
|
+
cluster.get('name'),
|
|
157
|
+
cluster_id == cluster.get('id')
|
|
158
|
+
)
|
|
159
|
+
|
|
149
160
|
row.insert(1, profile_name)
|
|
150
161
|
row.insert(2, region_name)
|
|
151
162
|
if all:
|
|
@@ -156,6 +167,21 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
|
|
|
156
167
|
row.append(cluster.get('version'))
|
|
157
168
|
row.append(cluster.get('control_planes'))
|
|
158
169
|
|
|
170
|
+
return row, current_status
|
|
171
|
+
|
|
172
|
+
if all:
|
|
173
|
+
projects = {p["id"]: p for p in do_request("GET", "projects")}
|
|
174
|
+
data = do_request("GET", "clusters/all", params=params)
|
|
175
|
+
for cluster in data:
|
|
176
|
+
project = projects.get(cluster.get("project_id"))
|
|
177
|
+
cluster["project_name"] = project.get("name")
|
|
178
|
+
else:
|
|
179
|
+
data = do_request("GET", "clusters", params=params)
|
|
180
|
+
|
|
181
|
+
initial_clusters = {}
|
|
182
|
+
|
|
183
|
+
for cluster in data:
|
|
184
|
+
row, _ = build_row(cluster)
|
|
159
185
|
table.add_row(row)
|
|
160
186
|
initial_clusters[cluster.get("id")] = cluster
|
|
161
187
|
|
|
@@ -170,46 +196,32 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
|
|
|
170
196
|
|
|
171
197
|
try:
|
|
172
198
|
if all:
|
|
173
|
-
projects = {
|
|
199
|
+
projects = {p["id"]: p for p in do_request("GET", "projects")}
|
|
174
200
|
data = do_request("GET", "clusters/all", params=params)
|
|
175
|
-
|
|
176
201
|
for cluster in data:
|
|
177
202
|
project = projects.get(cluster.get("project_id"))
|
|
178
203
|
cluster["project_name"] = project.get("name")
|
|
179
204
|
else:
|
|
180
|
-
data = do_request("GET",
|
|
205
|
+
data = do_request("GET", "clusters", params=params)
|
|
181
206
|
except click.ClickException as err:
|
|
182
207
|
click.echo(f"Error during watch: {err}")
|
|
183
208
|
continue
|
|
184
209
|
|
|
185
|
-
|
|
210
|
+
current_ids = {c.get('id') for c in data}
|
|
186
211
|
|
|
187
|
-
for
|
|
188
|
-
if
|
|
189
|
-
deleted_cluster =
|
|
212
|
+
for cl_id, stored_cluster in list(initial_clusters.items()):
|
|
213
|
+
if cl_id not in current_ids:
|
|
214
|
+
deleted_cluster = stored_cluster.copy()
|
|
190
215
|
deleted_cluster['statuses']['status'] = 'deleted'
|
|
191
216
|
|
|
192
|
-
row,
|
|
193
|
-
row.insert(1, profile_name)
|
|
194
|
-
row.insert(2, region_name)
|
|
195
|
-
if all:
|
|
196
|
-
project_name = click.style(cluster.get("project_name"), bold=True)
|
|
197
|
-
row.insert(0, project_name)
|
|
198
|
-
|
|
217
|
+
row, _ = build_row(deleted_cluster)
|
|
199
218
|
new_table = format_changed_row(table, row)
|
|
200
219
|
click.echo(new_table)
|
|
201
|
-
|
|
202
|
-
del initial_clusters[id]
|
|
220
|
+
del initial_clusters[cl_id]
|
|
203
221
|
|
|
204
222
|
for cluster in data:
|
|
205
|
-
row, current_status, name = format_row(cluster.get('statuses'), cluster.get('name'), cluster_id == cluster.get('id'))
|
|
206
|
-
row.insert(1, profile_name)
|
|
207
|
-
row.insert(2, region_name)
|
|
208
|
-
if all:
|
|
209
|
-
project_name = click.style(cluster.get("project_name"), bold=True)
|
|
210
|
-
row.insert(0, project_name)
|
|
211
|
-
|
|
212
223
|
cl_id = cluster.get('id')
|
|
224
|
+
row, current_status = build_row(cluster)
|
|
213
225
|
|
|
214
226
|
if cl_id not in initial_clusters:
|
|
215
227
|
new_table = format_changed_row(table, row)
|
|
@@ -218,8 +230,8 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
|
|
|
218
230
|
continue
|
|
219
231
|
|
|
220
232
|
stored_cluster = initial_clusters[cl_id]
|
|
221
|
-
|
|
222
|
-
if
|
|
233
|
+
stored_status = stored_cluster.get('statuses').get('status')
|
|
234
|
+
if stored_status != current_status:
|
|
223
235
|
new_table = format_changed_row(table, row)
|
|
224
236
|
click.echo(new_table)
|
|
225
237
|
initial_clusters[cl_id] = cluster
|
|
@@ -516,7 +528,7 @@ def cluster_update_command(ctx, project_name, cluster_name, description, admin,
|
|
|
516
528
|
if tags is not None:
|
|
517
529
|
parsed_tags = {}
|
|
518
530
|
|
|
519
|
-
if
|
|
531
|
+
if len(tags) != 0:
|
|
520
532
|
pairs = tags.split(',')
|
|
521
533
|
for pair in pairs:
|
|
522
534
|
if '=' not in pair:
|
|
@@ -729,19 +741,29 @@ def _run_kubectl(project_id, cluster_id, user, group, args, input=None, capture=
|
|
|
729
741
|
|
|
730
742
|
if not kubeconfig_raw:
|
|
731
743
|
click.echo("Cannot get kubeconfig")
|
|
732
|
-
raise SystemExit()
|
|
744
|
+
raise SystemExit(1)
|
|
733
745
|
|
|
734
746
|
kubeconfig_path = save_cache(project_id, cluster_id, 'kubeconfig', kubeconfig_raw, user, group)
|
|
735
747
|
|
|
736
748
|
env = dict(os.environ)
|
|
737
749
|
env['KUBECONFIG'] = str(kubeconfig_path)
|
|
738
|
-
|
|
739
|
-
cmd
|
|
750
|
+
|
|
751
|
+
cmd = ['kubectl'] + list(args)
|
|
740
752
|
logging.info("running %s", cmd)
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
753
|
+
|
|
754
|
+
try:
|
|
755
|
+
if not input:
|
|
756
|
+
return subprocess.run(cmd, env=env, capture_output=capture, check=True)
|
|
757
|
+
else:
|
|
758
|
+
return subprocess.run(cmd, input=input, text=True, env=env, capture_output=capture, check=True)
|
|
759
|
+
|
|
760
|
+
except subprocess.CalledProcessError as e:
|
|
761
|
+
if e.stderr:
|
|
762
|
+
click.echo(e.stderr, err=True)
|
|
763
|
+
elif e.stdout:
|
|
764
|
+
click.echo(e.stdout, err=True)
|
|
765
|
+
|
|
766
|
+
raise SystemExit(e.returncode)
|
|
745
767
|
|
|
746
768
|
|
|
747
769
|
@cluster.command('kubectl', help='Fetch the kubeconfig for a cluster and run kubectl against it', context_settings={"ignore_unknown_options": True})
|
|
@@ -9,6 +9,7 @@ from .profile import profile
|
|
|
9
9
|
from .cache import cache
|
|
10
10
|
from .quotas import quotas
|
|
11
11
|
from .netpeering import netpeering
|
|
12
|
+
from .user import user
|
|
12
13
|
|
|
13
14
|
from .utils import ctx_update, install_completions, profile_completer, cluster_completer, project_completer
|
|
14
15
|
|
|
@@ -47,7 +48,7 @@ def cli(ctx, project_name, cluster_name, profile, verbose):
|
|
|
47
48
|
click.echo(ctx.get_help())
|
|
48
49
|
|
|
49
50
|
if not hasattr(ctx, 'obj') or not ctx.obj:
|
|
50
|
-
ctx.obj =
|
|
51
|
+
ctx.obj = {}
|
|
51
52
|
|
|
52
53
|
if project_name != None:
|
|
53
54
|
ctx.obj['project_name'] = project_name
|
|
@@ -62,6 +63,7 @@ cli.add_command(profile)
|
|
|
62
63
|
cli.add_command(cache)
|
|
63
64
|
cli.add_command(quotas)
|
|
64
65
|
cli.add_command(netpeering)
|
|
66
|
+
cli.add_command(user)
|
|
65
67
|
|
|
66
68
|
def recursive_help(cmd, parent=None):
|
|
67
69
|
"""Recursively prints help for all commands and subcommands."""
|
|
@@ -56,6 +56,13 @@ def project_logout(ctx, profile):
|
|
|
56
56
|
"""Unset the current default project and log out."""
|
|
57
57
|
_, _, profile = ctx_update(ctx, None, None, profile)
|
|
58
58
|
login_profile(profile)
|
|
59
|
+
|
|
60
|
+
current_project = get_project_id()
|
|
61
|
+
|
|
62
|
+
if not current_project:
|
|
63
|
+
click.echo("You are not connected to any project.")
|
|
64
|
+
return
|
|
65
|
+
|
|
59
66
|
set_project_id("")
|
|
60
67
|
set_cluster_id("")
|
|
61
68
|
click.echo("Logged out from the current project")
|
|
@@ -321,7 +328,7 @@ def project_update_command(ctx, project_name, description, quirk, tags, disable_
|
|
|
321
328
|
if tags is not None:
|
|
322
329
|
parsed_tags = {}
|
|
323
330
|
|
|
324
|
-
if
|
|
331
|
+
if len(tags) != 0:
|
|
325
332
|
pairs = tags.split(',')
|
|
326
333
|
for pair in pairs:
|
|
327
334
|
if '=' not in pair:
|
|
@@ -404,7 +411,7 @@ def project_get_public_ips(ctx, project_name, output, profile):
|
|
|
404
411
|
# GET NETS BY PROJECT NAME
|
|
405
412
|
@project.command('nets', help="Get project nets")
|
|
406
413
|
@click.option('--project-name', '-p', help="Name of the project", shell_complete=project_completer)
|
|
407
|
-
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
414
|
+
@click.option('--output', '-o', type=click.Choice(["json", "yaml", "table"]), help="Specify output format, by default is json")
|
|
408
415
|
@click.option('--profile',help="Configuration profile to use")
|
|
409
416
|
@click.pass_context
|
|
410
417
|
def project_get_public_ips(ctx, project_name, output, profile):
|
|
@@ -415,4 +422,11 @@ def project_get_public_ips(ctx, project_name, output, profile):
|
|
|
415
422
|
project_id = find_project_id_by_name(project_name)
|
|
416
423
|
|
|
417
424
|
data = do_request("GET", f'projects/{project_id}/nets')
|
|
418
|
-
|
|
425
|
+
if output == "table":
|
|
426
|
+
print_table(data, [["DHCP options set id", "DhcpOptionsSetId"],
|
|
427
|
+
["Ip range", "IpRange"],
|
|
428
|
+
["Net id", "NetId"],
|
|
429
|
+
["State", "State"],
|
|
430
|
+
["Tenancy", "Tenancy"]])
|
|
431
|
+
else:
|
|
432
|
+
print_output(data, output)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import dateutil.parser
|
|
4
|
+
import human_readable
|
|
5
|
+
import prettytable
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from nacl.public import PrivateKey, SealedBox
|
|
9
|
+
from nacl.encoding import Base64Encoder
|
|
10
|
+
|
|
11
|
+
from .utils import do_request, print_output, find_project_id_by_name, ctx_update, login_profile, profile_completer, project_completer, JSONClickException
|
|
12
|
+
|
|
13
|
+
# DEIFNE THE USER COMMAND GROUP
|
|
14
|
+
@click.group(help="EIM users related commands.")
|
|
15
|
+
@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
|
|
16
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
17
|
+
@click.pass_context
|
|
18
|
+
def user(ctx, project_name, profile):
|
|
19
|
+
"""Group of commands related to project management."""
|
|
20
|
+
ctx_update(ctx, project_name, None, profile)
|
|
21
|
+
|
|
22
|
+
# LIST USERS
|
|
23
|
+
@user.command('list', help="List EIM users")
|
|
24
|
+
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
25
|
+
@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
|
|
26
|
+
@click.option('--profile', help="Configuration profile to use")
|
|
27
|
+
@click.pass_context
|
|
28
|
+
def user_list(ctx, output, project_name, profile):
|
|
29
|
+
"""List users"""
|
|
30
|
+
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
|
|
31
|
+
login_profile(profile)
|
|
32
|
+
|
|
33
|
+
project_id = find_project_id_by_name(project_name)
|
|
34
|
+
|
|
35
|
+
data = do_request("GET", f'projects/{project_id}/eim_users')
|
|
36
|
+
|
|
37
|
+
if output:
|
|
38
|
+
print_output(data, output)
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
field_names = ["USER", "ACCESS KEY", "STATE", "CREATED", "EXPIRATION DATE"]
|
|
42
|
+
table = prettytable.PrettyTable()
|
|
43
|
+
table.field_names = field_names
|
|
44
|
+
|
|
45
|
+
for user in data:
|
|
46
|
+
access_keys = user.get("AccessKeys", [])
|
|
47
|
+
access_key = access_keys[0] if access_keys else {}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
state = access_key.get("State", "N/A")
|
|
51
|
+
if state == 'ACTIVE':
|
|
52
|
+
state = click.style(state, fg='green')
|
|
53
|
+
elif state == "INACTIVE":
|
|
54
|
+
state = click.style(state, fg='red')
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
row = [
|
|
58
|
+
user.get("UserName"),
|
|
59
|
+
access_key.get("AccessKeyId", "N/A"),
|
|
60
|
+
state
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
if "CreationDate" in access_key:
|
|
64
|
+
created_at = dateutil.parser.parse(access_key.get("CreationDate"))
|
|
65
|
+
now = datetime.now(tz=created_at.tzinfo)
|
|
66
|
+
row.append(human_readable.date_time(now - created_at))
|
|
67
|
+
else:
|
|
68
|
+
row.append("N/A")
|
|
69
|
+
|
|
70
|
+
if "ExpirationDate" in access_key:
|
|
71
|
+
exp_at = dateutil.parser.parse(access_key.get("ExpirationDate"))
|
|
72
|
+
now = datetime.now(tz=exp_at.tzinfo)
|
|
73
|
+
row.append(human_readable.date_time(now - exp_at))
|
|
74
|
+
else:
|
|
75
|
+
row.append("N/A")
|
|
76
|
+
|
|
77
|
+
table.add_row(row)
|
|
78
|
+
|
|
79
|
+
click.echo(table)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@user.command('create', help="Create a new EIM user")
|
|
83
|
+
@click.option('--project-name', '-p', help="Name of project", type=click.STRING, shell_complete=project_completer)
|
|
84
|
+
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
85
|
+
@click.option('--profile', help="Configuration profile to use")
|
|
86
|
+
@click.option('--user', '-u', required=True, help="OKS User type")
|
|
87
|
+
@click.option('--ttl', type=click.STRING, help="TTL in human readable format (5h, 1d, 1w), by default is 1w")
|
|
88
|
+
@click.option('--nacl', is_flag=True, help="Use public key encryption on wire")
|
|
89
|
+
@click.pass_context
|
|
90
|
+
def user_create(ctx, project_name, output, profile, user, ttl, nacl):
|
|
91
|
+
"""Create a new EIM user."""
|
|
92
|
+
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
|
|
93
|
+
login_profile(profile)
|
|
94
|
+
|
|
95
|
+
project_id = find_project_id_by_name(project_name)
|
|
96
|
+
|
|
97
|
+
params = {
|
|
98
|
+
"user": user
|
|
99
|
+
}
|
|
100
|
+
if ttl:
|
|
101
|
+
params["ttl"] = ttl
|
|
102
|
+
|
|
103
|
+
if nacl:
|
|
104
|
+
ephemeral = PrivateKey.generate()
|
|
105
|
+
unsealbox = SealedBox(ephemeral)
|
|
106
|
+
|
|
107
|
+
headers = {
|
|
108
|
+
'x-encrypt-nacl': ephemeral.public_key.encode(Base64Encoder).decode('ascii')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
raw_data = do_request(
|
|
112
|
+
"POST",
|
|
113
|
+
f'projects/{project_id}/eim_users',
|
|
114
|
+
params=params,
|
|
115
|
+
headers=headers
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
decrypted = unsealbox.decrypt(
|
|
119
|
+
raw_data.get("Data").encode('ascii'),
|
|
120
|
+
encoder=Base64Encoder
|
|
121
|
+
).decode('ascii')
|
|
122
|
+
|
|
123
|
+
data = json.loads(decrypted)
|
|
124
|
+
|
|
125
|
+
# format decrypted errors the same way as the api errors.
|
|
126
|
+
if "Errors" in data:
|
|
127
|
+
response_context = raw_data.get("ResponseContext")
|
|
128
|
+
errors = []
|
|
129
|
+
for error in data.get("Errors", []):
|
|
130
|
+
error["Code"] = str(data.get("Code"))
|
|
131
|
+
errors.append(error)
|
|
132
|
+
|
|
133
|
+
raise JSONClickException(json.dumps({"Errors": errors,"ResponseContext": response_context}, separators=(",", ":")))
|
|
134
|
+
|
|
135
|
+
else:
|
|
136
|
+
data = do_request(
|
|
137
|
+
"POST",
|
|
138
|
+
f'projects/{project_id}/eim_users',
|
|
139
|
+
params=params
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
print_output(data, output)
|
|
143
|
+
|
|
144
|
+
# DELETE USER
|
|
145
|
+
@user.command('delete', help="Delete an EIM user")
|
|
146
|
+
@click.option('--project-name', '-p', required=False, help="Project name", shell_complete=project_completer)
|
|
147
|
+
@click.option('--user', '-u', required=True, help="User name")
|
|
148
|
+
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format")
|
|
149
|
+
@click.option('--dry-run', is_flag=True, help="Run without any action")
|
|
150
|
+
@click.option('--force', is_flag=True, help="Force deletion without confirmation")
|
|
151
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
152
|
+
@click.pass_context
|
|
153
|
+
def user_delete(ctx, project_name, user, output, dry_run, force, profile):
|
|
154
|
+
"""CLI command to delete an EIM user."""
|
|
155
|
+
|
|
156
|
+
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
|
|
157
|
+
login_profile(profile)
|
|
158
|
+
|
|
159
|
+
project_id = find_project_id_by_name(project_name)
|
|
160
|
+
|
|
161
|
+
if dry_run:
|
|
162
|
+
message = {"message": f"Dry run: The user '{user}' would be deleted."}
|
|
163
|
+
print_output(message, output)
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
if force or click.confirm(f"Are you sure you want to delete the user '{user}'?", abort=True):
|
|
167
|
+
data = do_request("DELETE", f"projects/{project_id}/eim_users/{user}")
|
|
168
|
+
print_output(data, output)
|
|
169
|
+
|
|
@@ -79,6 +79,12 @@ def find_response_object(data):
|
|
|
79
79
|
return response["IP"]
|
|
80
80
|
elif key == "Nets":
|
|
81
81
|
return response["Nets"]
|
|
82
|
+
elif key == "EimUsers":
|
|
83
|
+
return response["EimUsers"]
|
|
84
|
+
elif key == "EimUser":
|
|
85
|
+
return response["EimUser"]
|
|
86
|
+
elif key == "Data":
|
|
87
|
+
return response
|
|
82
88
|
|
|
83
89
|
raise click.ClickException("The API response format is incorrect.")
|
|
84
90
|
|
|
@@ -202,8 +208,8 @@ def print_table(data, table_fields, align="l", style=None):
|
|
|
202
208
|
table.align = align
|
|
203
209
|
if style and isinstance(style, prettytable.TableStyle):
|
|
204
210
|
table.set_style(style)
|
|
205
|
-
fields =
|
|
206
|
-
values =
|
|
211
|
+
fields = []
|
|
212
|
+
values = []
|
|
207
213
|
|
|
208
214
|
for d in table_fields:
|
|
209
215
|
fields.append(d[0])
|
|
@@ -417,7 +423,7 @@ def format_row(data: dict, name: str, is_default: bool):
|
|
|
417
423
|
"""Parse status and dates from a cluster of project object and returns elements"""
|
|
418
424
|
|
|
419
425
|
if not data.get('status'):
|
|
420
|
-
raise click.ClickException(
|
|
426
|
+
raise click.ClickException("Can't find 'status' in project/cluster data")
|
|
421
427
|
|
|
422
428
|
status = data.get('status')
|
|
423
429
|
if status == 'ready':
|
|
@@ -805,14 +811,14 @@ def kubeconfig_parse_fields(kubeconfig, cluster_name, user, group):
|
|
|
805
811
|
group: user group name of this kubeconfig (if set)
|
|
806
812
|
"""
|
|
807
813
|
kubeconfig_str = yaml.safe_load(kubeconfig)
|
|
808
|
-
kubedata =
|
|
814
|
+
kubedata = []
|
|
809
815
|
|
|
810
816
|
# Ensure loaded YAML returnes a valid dict object
|
|
811
817
|
if not isinstance(kubeconfig_str, dict):
|
|
812
818
|
return kubedata
|
|
813
819
|
|
|
814
820
|
for context in kubeconfig_str.get('contexts', []):
|
|
815
|
-
data =
|
|
821
|
+
data = {}
|
|
816
822
|
ctx_cluster = context.get('context').get('cluster', None)
|
|
817
823
|
ctx_user = context.get('context').get('user', None)
|
|
818
824
|
ctx_name = context.get('name')
|
|
@@ -917,7 +923,11 @@ def find_shell_profile(home, shell_type):
|
|
|
917
923
|
if os.path.exists(bash_profile) and os.path.exists(bashrc):
|
|
918
924
|
return None
|
|
919
925
|
|
|
920
|
-
|
|
926
|
+
if os.path.exists(bash_profile):
|
|
927
|
+
shell_profile = bash_profile
|
|
928
|
+
|
|
929
|
+
if os.path.exists(bashrc):
|
|
930
|
+
shell_profile = bashrc
|
|
921
931
|
|
|
922
932
|
elif shell_type == "zsh":
|
|
923
933
|
|
|
@@ -927,7 +937,11 @@ def find_shell_profile(home, shell_type):
|
|
|
927
937
|
if os.path.exists(zshrc) and os.path.exists(profile):
|
|
928
938
|
return None
|
|
929
939
|
|
|
930
|
-
|
|
940
|
+
if os.path.exists(zshrc):
|
|
941
|
+
shell_profile = zshrc
|
|
942
|
+
|
|
943
|
+
if os.path.exists(profile):
|
|
944
|
+
shell_profile = profile
|
|
931
945
|
|
|
932
946
|
return shell_profile
|
|
933
947
|
|
|
@@ -942,8 +956,8 @@ def install_completions(shell_type):
|
|
|
942
956
|
shell_name = result.stdout.strip()
|
|
943
957
|
|
|
944
958
|
shell_type = os.path.basename(shell_name).lstrip('-')
|
|
945
|
-
except subprocess.
|
|
946
|
-
click.
|
|
959
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
960
|
+
raise click.ClickException("Failed to determine shell type, please specify it by --type")
|
|
947
961
|
|
|
948
962
|
completion_dir = os.path.join(home, ".oks_cli", "completions")
|
|
949
963
|
os.makedirs(completion_dir, exist_ok=True)
|
|
@@ -1029,7 +1043,7 @@ def get_template(type):
|
|
|
1029
1043
|
def ctx_update(ctx, project_name=None, cluster_name=None, profile=None, overwrite=True):
|
|
1030
1044
|
"""Update context with project, cluster, and profile; optionally prevent overwrites."""
|
|
1031
1045
|
if not hasattr(ctx, 'obj') or not ctx.obj:
|
|
1032
|
-
ctx.obj =
|
|
1046
|
+
ctx.obj = {}
|
|
1033
1047
|
|
|
1034
1048
|
if project_name is not None:
|
|
1035
1049
|
if ctx.obj.get('project_name') and not overwrite:
|
|
@@ -1097,30 +1111,19 @@ def is_interesting_status(status):
|
|
|
1097
1111
|
def normalize_key_path(key_path: str) -> str:
|
|
1098
1112
|
return re.sub(r'\[(\d+)\]', r'.\1', key_path)
|
|
1099
1113
|
|
|
1100
|
-
def parse_value(value):
|
|
1114
|
+
def parse_value(value: str):
|
|
1101
1115
|
value = value.strip()
|
|
1102
1116
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
return []
|
|
1108
|
-
return [parse_value(v.strip()) for v in inner.split(',')]
|
|
1117
|
+
try:
|
|
1118
|
+
return json.loads(value)
|
|
1119
|
+
except Exception:
|
|
1120
|
+
pass
|
|
1109
1121
|
|
|
1110
1122
|
if value.lower() == "true":
|
|
1111
1123
|
return True
|
|
1112
1124
|
if value.lower() == "false":
|
|
1113
1125
|
return False
|
|
1114
1126
|
|
|
1115
|
-
try:
|
|
1116
|
-
return int(value)
|
|
1117
|
-
except ValueError:
|
|
1118
|
-
pass
|
|
1119
|
-
|
|
1120
|
-
try:
|
|
1121
|
-
return float(value)
|
|
1122
|
-
except ValueError:
|
|
1123
|
-
pass
|
|
1124
1127
|
|
|
1125
1128
|
if ',' in value:
|
|
1126
1129
|
return [parse_value(v) for v in value.split(',')]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oks-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.21
|
|
4
4
|
Author: Outscale SAS
|
|
5
5
|
Author-email: opensource@outscale.com
|
|
6
6
|
License: BSD
|
|
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: certifi>=2024.8.30
|
|
19
19
|
Requires-Dist: charset-normalizer>=3.3.2
|
|
20
|
-
Requires-Dist: click<8.3.0,>=8.
|
|
20
|
+
Requires-Dist: click<8.3.0,>=8.2.0
|
|
21
21
|
Requires-Dist: colorama>=0.4.6
|
|
22
22
|
Requires-Dist: idna>=3.10
|
|
23
23
|
Requires-Dist: pyyaml>=6.0.2
|
|
@@ -9,6 +9,7 @@ oks_cli/netpeering.py
|
|
|
9
9
|
oks_cli/profile.py
|
|
10
10
|
oks_cli/project.py
|
|
11
11
|
oks_cli/quotas.py
|
|
12
|
+
oks_cli/user.py
|
|
12
13
|
oks_cli/utils.py
|
|
13
14
|
oks_cli.egg-info/PKG-INFO
|
|
14
15
|
oks_cli.egg-info/SOURCES.txt
|
|
@@ -23,4 +24,5 @@ tests/test_nodepool.py
|
|
|
23
24
|
tests/test_profile.py
|
|
24
25
|
tests/test_project.py
|
|
25
26
|
tests/test_quota.py
|
|
26
|
-
tests/test_shell_completion.py
|
|
27
|
+
tests/test_shell_completion.py
|
|
28
|
+
tests/test_user.py
|
|
@@ -2,11 +2,11 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="oks-cli",
|
|
5
|
-
version="1.
|
|
5
|
+
version="1.21",
|
|
6
6
|
packages=['oks_cli'],
|
|
7
7
|
author="Outscale SAS",
|
|
8
8
|
author_email="opensource@outscale.com",
|
|
9
|
-
long_description=open("README.md").read(),
|
|
9
|
+
long_description=open("README.md", encoding="utf-8").read(),
|
|
10
10
|
long_description_content_type="text/markdown",
|
|
11
11
|
include_package_data=True,
|
|
12
12
|
license="BSD",
|
|
@@ -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.
|
|
29
|
+
"click>=8.2.0,<8.3.0",
|
|
30
30
|
"colorama>=0.4.6",
|
|
31
31
|
"idna>=3.10",
|
|
32
32
|
"pyyaml>=6.0.2",
|
|
@@ -9,7 +9,7 @@ def test_install_completion_zsh():
|
|
|
9
9
|
|
|
10
10
|
result = runner.invoke(cli, ["install-completion", "--type", "zsh"])
|
|
11
11
|
assert result.exit_code == 0
|
|
12
|
-
assert "
|
|
12
|
+
assert "To activate autocompletion on login please add following lines into your .profile or .zshrc file" in result.output
|
|
13
13
|
|
|
14
14
|
completion_file = Path("~/.oks_cli/completions/oks-cli.sh").expanduser()
|
|
15
15
|
assert completion_file.exists()
|
|
@@ -22,7 +22,7 @@ def test_install_completion_bash():
|
|
|
22
22
|
|
|
23
23
|
result = runner.invoke(cli, ["install-completion", "--type", "bash"])
|
|
24
24
|
assert result.exit_code == 0
|
|
25
|
-
assert "
|
|
25
|
+
assert "To activate autocompletion on login please add following lines into your .bash_profile or .bashrc file" in result.output
|
|
26
26
|
|
|
27
27
|
completion_file = Path("~/.oks_cli/completions/oks-cli.sh").expanduser()
|
|
28
28
|
assert completion_file.exists()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from click.testing import CliRunner
|
|
2
|
+
from oks_cli.main import cli
|
|
3
|
+
from unittest.mock import patch, MagicMock
|
|
4
|
+
|
|
5
|
+
@patch("oks_cli.utils.requests.request")
|
|
6
|
+
def test_user_list_command(mock_request, add_default_profile):
|
|
7
|
+
mock_request.side_effect = [
|
|
8
|
+
MagicMock(status_code=200, headers = {}, json=lambda: {"ResponseContext": {}, "Projects": [{"id": "12345"}]}),
|
|
9
|
+
MagicMock(status_code=200, headers = {}, json=lambda: {"ResponseContext": {}, "EimUsers": []})
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
runner = CliRunner()
|
|
13
|
+
result = runner.invoke(cli, ["user", "list", "-p", "test"])
|
|
14
|
+
assert result.exit_code == 0
|
|
15
|
+
assert 'USER | ACCESS KEY' in result.output
|
|
16
|
+
|
|
17
|
+
@patch("oks_cli.utils.requests.request")
|
|
18
|
+
def test_user_create_command(mock_request, add_default_profile):
|
|
19
|
+
mock_request.side_effect = [
|
|
20
|
+
MagicMock(status_code=200, headers = {}, json=lambda: {"ResponseContext": {}, "Projects": [{"id": "12345"}]}),
|
|
21
|
+
MagicMock(status_code=200, headers = {}, json=lambda: {"ResponseContext": {}, "EimUser": {
|
|
22
|
+
"UserName": "OKSAuditor",
|
|
23
|
+
"CreationDate": "2026-03-04T12:31:58.000+0000",
|
|
24
|
+
"UserId": "BlaBla",
|
|
25
|
+
"UserEmail": "bla@email.local",
|
|
26
|
+
"LastModificationDate": "2026-03-04T12:31:58.000+0000",
|
|
27
|
+
"Path": "/",
|
|
28
|
+
"AccessKeys": [
|
|
29
|
+
{
|
|
30
|
+
"State": "ACTIVE",
|
|
31
|
+
"AccessKeyId": "AK",
|
|
32
|
+
"CreationDate": "2026-03-04T12:31:59.841+0000",
|
|
33
|
+
"ExpirationDate": "2026-03-11T12:31:59.297+0000",
|
|
34
|
+
"SecretKey": "SK",
|
|
35
|
+
"LastModificationDate": "2026-03-04T12:31:59.841+0000"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}})
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
runner = CliRunner()
|
|
42
|
+
result = runner.invoke(cli, ["user", "create", "-p", "test", "-u", "OKSAuditor", "--ttl", "1w"])
|
|
43
|
+
assert result.exit_code == 0
|
|
44
|
+
assert 'bla@email.local' in result.output
|
|
45
|
+
|
|
46
|
+
@patch("oks_cli.utils.requests.request")
|
|
47
|
+
def test_user_delete_command(mock_request, add_default_profile):
|
|
48
|
+
mock_request.side_effect = [
|
|
49
|
+
MagicMock(status_code=200, headers = {}, json=lambda: {"ResponseContext": {}, "Projects": [{"id": "12345"}]}),
|
|
50
|
+
MagicMock(status_code=200, headers = {}, json=lambda: {"ResponseContext": {}, "Details": "User has been deleted." })
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
runner = CliRunner()
|
|
54
|
+
result = runner.invoke(cli, ["user", "delete", "-p", "test", "-u", "OKSAuditor", "--force"])
|
|
55
|
+
assert result.exit_code == 0
|
|
56
|
+
assert 'User has been deleted.' in result.output
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|