oks-cli 1.20__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.20 → oks_cli-1.21}/PKG-INFO +1 -1
- {oks_cli-1.20 → oks_cli-1.21}/oks_cli/cache.py +1 -1
- {oks_cli-1.20 → oks_cli-1.21}/oks_cli/cluster.py +60 -45
- {oks_cli-1.20 → oks_cli-1.21}/oks_cli/main.py +3 -1
- {oks_cli-1.20 → oks_cli-1.21}/oks_cli/profile.py +1 -1
- {oks_cli-1.20 → oks_cli-1.21}/oks_cli/project.py +1 -1
- oks_cli-1.21/oks_cli/user.py +169 -0
- {oks_cli-1.20 → oks_cli-1.21}/oks_cli/utils.py +14 -8
- {oks_cli-1.20 → oks_cli-1.21}/oks_cli.egg-info/PKG-INFO +1 -1
- {oks_cli-1.20 → oks_cli-1.21}/oks_cli.egg-info/SOURCES.txt +3 -1
- {oks_cli-1.20 → oks_cli-1.21}/setup.py +2 -2
- oks_cli-1.21/tests/test_user.py +56 -0
- {oks_cli-1.20 → oks_cli-1.21}/LICENSE +0 -0
- {oks_cli-1.20 → oks_cli-1.21}/README.md +0 -0
- {oks_cli-1.20 → oks_cli-1.21}/oks_cli/__init__.py +0 -0
- {oks_cli-1.20 → oks_cli-1.21}/oks_cli/netpeering.py +0 -0
- {oks_cli-1.20 → oks_cli-1.21}/oks_cli/quotas.py +0 -0
- {oks_cli-1.20 → oks_cli-1.21}/oks_cli.egg-info/dependency_links.txt +0 -0
- {oks_cli-1.20 → oks_cli-1.21}/oks_cli.egg-info/entry_points.txt +0 -0
- {oks_cli-1.20 → oks_cli-1.21}/oks_cli.egg-info/requires.txt +0 -0
- {oks_cli-1.20 → oks_cli-1.21}/oks_cli.egg-info/top_level.txt +0 -0
- {oks_cli-1.20 → oks_cli-1.21}/setup.cfg +0 -0
- {oks_cli-1.20 → oks_cli-1.21}/tests/test_cache.py +0 -0
- {oks_cli-1.20 → oks_cli-1.21}/tests/test_cluster.py +0 -0
- {oks_cli-1.20 → oks_cli-1.21}/tests/test_netpeering.py +0 -0
- {oks_cli-1.20 → oks_cli-1.21}/tests/test_nodepool.py +0 -0
- {oks_cli-1.20 → oks_cli-1.21}/tests/test_profile.py +0 -0
- {oks_cli-1.20 → oks_cli-1.21}/tests/test_project.py +0 -0
- {oks_cli-1.20 → oks_cli-1.21}/tests/test_quota.py +0 -0
- {oks_cli-1.20 → oks_cli-1.21}/tests/test_shell_completion.py +0 -0
|
@@ -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:
|
|
@@ -121,20 +121,20 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
|
|
|
121
121
|
if all:
|
|
122
122
|
field_names.insert(0, "PROJECT")
|
|
123
123
|
|
|
124
|
-
projects = {project["id"]: project for project in do_request("GET", "projects")}
|
|
125
|
-
data = do_request("GET", "clusters/all", params=params)
|
|
126
|
-
|
|
127
|
-
for cluster in data:
|
|
128
|
-
project = projects.get(cluster.get("project_id"))
|
|
129
|
-
cluster["project_name"] = project.get("name")
|
|
130
|
-
else:
|
|
131
|
-
data = do_request("GET", "clusters", params=params)
|
|
132
|
-
|
|
133
124
|
if output == "wide":
|
|
134
125
|
field_names.insert(0, "ID")
|
|
135
126
|
field_names.append("VERSION")
|
|
136
127
|
field_names.append("CONTROL PLANE")
|
|
137
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
|
+
|
|
138
138
|
print_output(data, output)
|
|
139
139
|
return
|
|
140
140
|
|
|
@@ -149,10 +149,14 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
|
|
|
149
149
|
if msword:
|
|
150
150
|
table.set_style(TableStyle.MSWORD_FRIENDLY)
|
|
151
151
|
|
|
152
|
-
initial_clusters = {}
|
|
153
152
|
|
|
154
|
-
|
|
155
|
-
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
|
+
|
|
156
160
|
row.insert(1, profile_name)
|
|
157
161
|
row.insert(2, region_name)
|
|
158
162
|
if all:
|
|
@@ -163,6 +167,21 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
|
|
|
163
167
|
row.append(cluster.get('version'))
|
|
164
168
|
row.append(cluster.get('control_planes'))
|
|
165
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)
|
|
166
185
|
table.add_row(row)
|
|
167
186
|
initial_clusters[cluster.get("id")] = cluster
|
|
168
187
|
|
|
@@ -177,46 +196,32 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
|
|
|
177
196
|
|
|
178
197
|
try:
|
|
179
198
|
if all:
|
|
180
|
-
projects = {
|
|
199
|
+
projects = {p["id"]: p for p in do_request("GET", "projects")}
|
|
181
200
|
data = do_request("GET", "clusters/all", params=params)
|
|
182
|
-
|
|
183
201
|
for cluster in data:
|
|
184
202
|
project = projects.get(cluster.get("project_id"))
|
|
185
203
|
cluster["project_name"] = project.get("name")
|
|
186
204
|
else:
|
|
187
|
-
data = do_request("GET",
|
|
205
|
+
data = do_request("GET", "clusters", params=params)
|
|
188
206
|
except click.ClickException as err:
|
|
189
207
|
click.echo(f"Error during watch: {err}")
|
|
190
208
|
continue
|
|
191
209
|
|
|
192
|
-
|
|
210
|
+
current_ids = {c.get('id') for c in data}
|
|
193
211
|
|
|
194
|
-
for
|
|
195
|
-
if
|
|
196
|
-
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()
|
|
197
215
|
deleted_cluster['statuses']['status'] = 'deleted'
|
|
198
216
|
|
|
199
|
-
row,
|
|
200
|
-
row.insert(1, profile_name)
|
|
201
|
-
row.insert(2, region_name)
|
|
202
|
-
if all:
|
|
203
|
-
project_name = click.style(cluster.get("project_name"), bold=True)
|
|
204
|
-
row.insert(0, project_name)
|
|
205
|
-
|
|
217
|
+
row, _ = build_row(deleted_cluster)
|
|
206
218
|
new_table = format_changed_row(table, row)
|
|
207
219
|
click.echo(new_table)
|
|
208
|
-
|
|
209
|
-
del initial_clusters[id]
|
|
220
|
+
del initial_clusters[cl_id]
|
|
210
221
|
|
|
211
222
|
for cluster in data:
|
|
212
|
-
row, current_status, name = format_row(cluster.get('statuses'), cluster.get('name'), cluster_id == cluster.get('id'))
|
|
213
|
-
row.insert(1, profile_name)
|
|
214
|
-
row.insert(2, region_name)
|
|
215
|
-
if all:
|
|
216
|
-
project_name = click.style(cluster.get("project_name"), bold=True)
|
|
217
|
-
row.insert(0, project_name)
|
|
218
|
-
|
|
219
223
|
cl_id = cluster.get('id')
|
|
224
|
+
row, current_status = build_row(cluster)
|
|
220
225
|
|
|
221
226
|
if cl_id not in initial_clusters:
|
|
222
227
|
new_table = format_changed_row(table, row)
|
|
@@ -225,8 +230,8 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
|
|
|
225
230
|
continue
|
|
226
231
|
|
|
227
232
|
stored_cluster = initial_clusters[cl_id]
|
|
228
|
-
|
|
229
|
-
if
|
|
233
|
+
stored_status = stored_cluster.get('statuses').get('status')
|
|
234
|
+
if stored_status != current_status:
|
|
230
235
|
new_table = format_changed_row(table, row)
|
|
231
236
|
click.echo(new_table)
|
|
232
237
|
initial_clusters[cl_id] = cluster
|
|
@@ -523,7 +528,7 @@ def cluster_update_command(ctx, project_name, cluster_name, description, admin,
|
|
|
523
528
|
if tags is not None:
|
|
524
529
|
parsed_tags = {}
|
|
525
530
|
|
|
526
|
-
if
|
|
531
|
+
if len(tags) != 0:
|
|
527
532
|
pairs = tags.split(',')
|
|
528
533
|
for pair in pairs:
|
|
529
534
|
if '=' not in pair:
|
|
@@ -736,19 +741,29 @@ def _run_kubectl(project_id, cluster_id, user, group, args, input=None, capture=
|
|
|
736
741
|
|
|
737
742
|
if not kubeconfig_raw:
|
|
738
743
|
click.echo("Cannot get kubeconfig")
|
|
739
|
-
raise SystemExit()
|
|
744
|
+
raise SystemExit(1)
|
|
740
745
|
|
|
741
746
|
kubeconfig_path = save_cache(project_id, cluster_id, 'kubeconfig', kubeconfig_raw, user, group)
|
|
742
747
|
|
|
743
748
|
env = dict(os.environ)
|
|
744
749
|
env['KUBECONFIG'] = str(kubeconfig_path)
|
|
745
|
-
|
|
746
|
-
cmd
|
|
750
|
+
|
|
751
|
+
cmd = ['kubectl'] + list(args)
|
|
747
752
|
logging.info("running %s", cmd)
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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)
|
|
752
767
|
|
|
753
768
|
|
|
754
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."""
|
|
@@ -328,7 +328,7 @@ def project_update_command(ctx, project_name, description, quirk, tags, disable_
|
|
|
328
328
|
if tags is not None:
|
|
329
329
|
parsed_tags = {}
|
|
330
330
|
|
|
331
|
-
if
|
|
331
|
+
if len(tags) != 0:
|
|
332
332
|
pairs = tags.split(',')
|
|
333
333
|
for pair in pairs:
|
|
334
334
|
if '=' not in pair:
|
|
@@ -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')
|
|
@@ -950,8 +956,8 @@ def install_completions(shell_type):
|
|
|
950
956
|
shell_name = result.stdout.strip()
|
|
951
957
|
|
|
952
958
|
shell_type = os.path.basename(shell_name).lstrip('-')
|
|
953
|
-
except subprocess.
|
|
954
|
-
click.
|
|
959
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
960
|
+
raise click.ClickException("Failed to determine shell type, please specify it by --type")
|
|
955
961
|
|
|
956
962
|
completion_dir = os.path.join(home, ".oks_cli", "completions")
|
|
957
963
|
os.makedirs(completion_dir, exist_ok=True)
|
|
@@ -1037,7 +1043,7 @@ def get_template(type):
|
|
|
1037
1043
|
def ctx_update(ctx, project_name=None, cluster_name=None, profile=None, overwrite=True):
|
|
1038
1044
|
"""Update context with project, cluster, and profile; optionally prevent overwrites."""
|
|
1039
1045
|
if not hasattr(ctx, 'obj') or not ctx.obj:
|
|
1040
|
-
ctx.obj =
|
|
1046
|
+
ctx.obj = {}
|
|
1041
1047
|
|
|
1042
1048
|
if project_name is not None:
|
|
1043
1049
|
if ctx.obj.get('project_name') and not overwrite:
|
|
@@ -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",
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|