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.
Files changed (30) hide show
  1. {oks_cli-1.20 → oks_cli-1.21}/PKG-INFO +1 -1
  2. {oks_cli-1.20 → oks_cli-1.21}/oks_cli/cache.py +1 -1
  3. {oks_cli-1.20 → oks_cli-1.21}/oks_cli/cluster.py +60 -45
  4. {oks_cli-1.20 → oks_cli-1.21}/oks_cli/main.py +3 -1
  5. {oks_cli-1.20 → oks_cli-1.21}/oks_cli/profile.py +1 -1
  6. {oks_cli-1.20 → oks_cli-1.21}/oks_cli/project.py +1 -1
  7. oks_cli-1.21/oks_cli/user.py +169 -0
  8. {oks_cli-1.20 → oks_cli-1.21}/oks_cli/utils.py +14 -8
  9. {oks_cli-1.20 → oks_cli-1.21}/oks_cli.egg-info/PKG-INFO +1 -1
  10. {oks_cli-1.20 → oks_cli-1.21}/oks_cli.egg-info/SOURCES.txt +3 -1
  11. {oks_cli-1.20 → oks_cli-1.21}/setup.py +2 -2
  12. oks_cli-1.21/tests/test_user.py +56 -0
  13. {oks_cli-1.20 → oks_cli-1.21}/LICENSE +0 -0
  14. {oks_cli-1.20 → oks_cli-1.21}/README.md +0 -0
  15. {oks_cli-1.20 → oks_cli-1.21}/oks_cli/__init__.py +0 -0
  16. {oks_cli-1.20 → oks_cli-1.21}/oks_cli/netpeering.py +0 -0
  17. {oks_cli-1.20 → oks_cli-1.21}/oks_cli/quotas.py +0 -0
  18. {oks_cli-1.20 → oks_cli-1.21}/oks_cli.egg-info/dependency_links.txt +0 -0
  19. {oks_cli-1.20 → oks_cli-1.21}/oks_cli.egg-info/entry_points.txt +0 -0
  20. {oks_cli-1.20 → oks_cli-1.21}/oks_cli.egg-info/requires.txt +0 -0
  21. {oks_cli-1.20 → oks_cli-1.21}/oks_cli.egg-info/top_level.txt +0 -0
  22. {oks_cli-1.20 → oks_cli-1.21}/setup.cfg +0 -0
  23. {oks_cli-1.20 → oks_cli-1.21}/tests/test_cache.py +0 -0
  24. {oks_cli-1.20 → oks_cli-1.21}/tests/test_cluster.py +0 -0
  25. {oks_cli-1.20 → oks_cli-1.21}/tests/test_netpeering.py +0 -0
  26. {oks_cli-1.20 → oks_cli-1.21}/tests/test_nodepool.py +0 -0
  27. {oks_cli-1.20 → oks_cli-1.21}/tests/test_profile.py +0 -0
  28. {oks_cli-1.20 → oks_cli-1.21}/tests/test_project.py +0 -0
  29. {oks_cli-1.20 → oks_cli-1.21}/tests/test_quota.py +0 -0
  30. {oks_cli-1.20 → oks_cli-1.21}/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.20
3
+ Version: 1.21
4
4
  Author: Outscale SAS
5
5
  Author-email: opensource@outscale.com
6
6
  License: BSD
@@ -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 = list()
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
- for cluster in data:
155
- row, _, name = format_row(cluster.get('statuses'), cluster.get('name'), cluster_id == cluster.get('id'))
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 = {project["id"]: project for project in do_request("GET", "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", 'clusters', params=params)
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
- current_cluster_ids = {cluster.get('id') for cluster in data}
210
+ current_ids = {c.get('id') for c in data}
193
211
 
194
- for id, cluster in list(initial_clusters.items()):
195
- if id not in current_cluster_ids:
196
- deleted_cluster = cluster.copy()
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, current_status, _ = format_row(deleted_cluster.get('statuses'), deleted_cluster.get('name'), cluster_id == deleted_cluster.get('id'))
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
- cluster_status = stored_cluster.get('statuses').get('status')
229
- if cluster_status != current_status:
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 not len(tags) == 0:
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
- cmd = ['kubectl']
746
- cmd += list(args)
750
+
751
+ cmd = ['kubectl'] + list(args)
747
752
  logging.info("running %s", cmd)
748
- if not input:
749
- return subprocess.run(cmd, env=env, capture_output=capture)
750
- else:
751
- return subprocess.run(cmd, input=input, text=True, env=env, capture_output=capture)
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 = dict()
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."""
@@ -122,7 +122,7 @@ def list_profiles(output):
122
122
  return click.echo("There are no profiles")
123
123
 
124
124
  profiles_keys = list(profiles.keys())
125
- lines = list()
125
+ lines = []
126
126
 
127
127
  for key in profiles_keys:
128
128
  if 'endpoint' not in profiles[key]:
@@ -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 not len(tags) == 0:
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 = list()
206
- values = list()
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(f"Can't find 'status' in project/cluster data")
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 = list()
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 = dict()
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.SubProcessError:
954
- click.echo("Failed to determine shell type, please specify it by --type")
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 = dict()
1046
+ ctx.obj = {}
1041
1047
 
1042
1048
  if project_name is not None:
1043
1049
  if ctx.obj.get('project_name') and not overwrite:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oks-cli
3
- Version: 1.20
3
+ Version: 1.21
4
4
  Author: Outscale SAS
5
5
  Author-email: opensource@outscale.com
6
6
  License: BSD
@@ -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.20",
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