oks-cli 1.16__tar.gz → 1.17__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 (26) hide show
  1. {oks_cli-1.16 → oks_cli-1.17}/PKG-INFO +1 -1
  2. {oks_cli-1.16 → oks_cli-1.17}/oks_cli/cache.py +3 -3
  3. {oks_cli-1.16 → oks_cli-1.17}/oks_cli/cluster.py +83 -55
  4. {oks_cli-1.16 → oks_cli-1.17}/oks_cli/project.py +19 -37
  5. {oks_cli-1.16 → oks_cli-1.17}/oks_cli/utils.py +46 -2
  6. {oks_cli-1.16 → oks_cli-1.17}/oks_cli.egg-info/PKG-INFO +1 -1
  7. {oks_cli-1.16 → oks_cli-1.17}/setup.py +1 -1
  8. {oks_cli-1.16 → oks_cli-1.17}/tests/test_cluster.py +120 -0
  9. {oks_cli-1.16 → oks_cli-1.17}/tests/test_project.py +109 -1
  10. {oks_cli-1.16 → oks_cli-1.17}/LICENSE +0 -0
  11. {oks_cli-1.16 → oks_cli-1.17}/README.md +0 -0
  12. {oks_cli-1.16 → oks_cli-1.17}/oks_cli/__init__.py +0 -0
  13. {oks_cli-1.16 → oks_cli-1.17}/oks_cli/main.py +0 -0
  14. {oks_cli-1.16 → oks_cli-1.17}/oks_cli/profile.py +0 -0
  15. {oks_cli-1.16 → oks_cli-1.17}/oks_cli/quotas.py +0 -0
  16. {oks_cli-1.16 → oks_cli-1.17}/oks_cli.egg-info/SOURCES.txt +0 -0
  17. {oks_cli-1.16 → oks_cli-1.17}/oks_cli.egg-info/dependency_links.txt +0 -0
  18. {oks_cli-1.16 → oks_cli-1.17}/oks_cli.egg-info/entry_points.txt +0 -0
  19. {oks_cli-1.16 → oks_cli-1.17}/oks_cli.egg-info/requires.txt +0 -0
  20. {oks_cli-1.16 → oks_cli-1.17}/oks_cli.egg-info/top_level.txt +0 -0
  21. {oks_cli-1.16 → oks_cli-1.17}/setup.cfg +0 -0
  22. {oks_cli-1.16 → oks_cli-1.17}/tests/test_cache.py +0 -0
  23. {oks_cli-1.16 → oks_cli-1.17}/tests/test_nodepool.py +0 -0
  24. {oks_cli-1.16 → oks_cli-1.17}/tests/test_profile.py +0 -0
  25. {oks_cli-1.16 → oks_cli-1.17}/tests/test_quota.py +0 -0
  26. {oks_cli-1.16 → oks_cli-1.17}/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.16
3
+ Version: 1.17
4
4
  Author: Outscale SAS
5
5
  Author-email: opensource@outscale.com
6
6
  License: BSD
@@ -2,7 +2,7 @@ import click
2
2
  from .utils import clear_cache, find_project_id_by_name, find_cluster_id_by_name, get_all_cache, get_expiration_date, \
3
3
  ctx_update, login_profile, profile_completer, cluster_completer, project_completer, print_table
4
4
 
5
- import prettytable
5
+ from prettytable import TableStyle
6
6
 
7
7
  # DEFINE THE CACHE COMMAND GROUP
8
8
  @click.group(help="Cache related commands.")
@@ -57,8 +57,8 @@ def list_kubeconfigs(ctx, project_name, cluster_name, plain, msword, profile):
57
57
 
58
58
  style = None
59
59
  if plain:
60
- style = prettytable.PLAIN_COLUMNS
60
+ style = TableStyle.PLAIN_COLUMNS
61
61
  if msword:
62
- style = prettytable.MSWORD_FRIENDLY
62
+ style = TableStyle.MSWORD_FRIENDLY
63
63
 
64
64
  print_table(data, fields, style=style)
@@ -14,6 +14,7 @@ import prettytable
14
14
  import logging
15
15
  import yaml
16
16
 
17
+ from prettytable import TableStyle
17
18
  from .utils import cluster_completer, do_request, print_output, \
18
19
  find_project_id_by_name, find_cluster_id_by_name, \
19
20
  get_cache, save_cache, detect_and_parse_input, \
@@ -22,7 +23,7 @@ from .utils import cluster_completer, do_request, print_output,
22
23
  ctx_update, set_cluster_id, get_cluster_id, get_project_id, \
23
24
  get_template, get_cluster_name, format_changed_row, \
24
25
  is_interesting_status, profile_completer, project_completer, \
25
- kubeconfig_parse_fields, print_table, get_expiration_date
26
+ kubeconfig_parse_fields, print_table, format_row
26
27
 
27
28
  from .profile import add_profile
28
29
  from .project import project_create, project_login
@@ -86,19 +87,22 @@ def cluster_logout(ctx, profile):
86
87
  @click.option('--watch', '-w', is_flag=True, help="Watch the changes")
87
88
  @click.option('--output', '-o', type=click.Choice(["json", "yaml", "wide"]), help="Specify output format")
88
89
  @click.option('--profile', help="Configuration profile to use")
90
+ @click.option('--all', '-A', is_flag=True, help="List clusters from all projects")
89
91
  @click.pass_context
90
- def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch, output, profile):
92
+ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch, output, profile, all):
91
93
  """Display clusters with optional filtering and real-time monitoring."""
92
94
  project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
93
95
  login_profile(profile)
94
96
 
95
97
  profile_name = os.getenv('OKS_PROFILE')
96
98
  region_name = os.getenv('OKS_REGION')
97
- project_id = find_project_id_by_name(project_name)
98
- cluster_id = get_cluster_id()
99
-
100
99
  params = {}
101
- params['project_id'] = project_id
100
+
101
+ if not all:
102
+ project_id = find_project_id_by_name(project_name)
103
+ params['project_id'] = project_id
104
+
105
+ cluster_id = get_cluster_id()
102
106
 
103
107
  if cluster_name:
104
108
  params['name'] = cluster_name
@@ -107,7 +111,17 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
107
111
 
108
112
  field_names = ["CLUSTER", "PROFILE", "REGION", "CREATED", "UPDATED", "STATUS", "DEFAULT"]
109
113
 
110
- data = do_request("GET", 'clusters', params=params)
114
+ if all:
115
+ field_names.insert(0, "PROJECT")
116
+
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)
111
125
 
112
126
  if output == "wide":
113
127
  field_names.insert(0, "ID")
@@ -123,49 +137,27 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
123
137
  table._min_width = {"CREATED": 13, "UPDATED": 13, "STATUS": 10}
124
138
 
125
139
  if plain or watch:
126
- table.set_style(prettytable.PLAIN_COLUMNS)
140
+ table.set_style(TableStyle.PLAIN_COLUMNS)
127
141
 
128
142
  if msword:
129
143
  table.set_style(prettytable.MSWORD_FRIENDLY)
130
144
 
131
- def format_row(cluster):
132
- status = cluster['statuses']['status']
133
-
134
- is_default = True if cluster.get('id') == cluster_id else False
135
-
136
- if status == 'ready':
137
- msg = click.style(status, fg='green')
138
- elif status == 'failed' or status == 'deleted':
139
- msg = click.style(status, fg='red')
140
- elif status == 'deploying':
141
- msg = click.style(status, fg='yellow')
142
- else:
143
- msg = status
144
-
145
- name = click.style(cluster['name'], bold=True)
146
- if is_default:
147
- default = "*"
148
- else:
149
- default = ""
150
-
151
- created_at = dateutil.parser.parse(cluster['statuses']['created_at'])
152
- updated_at = dateutil.parser.parse(cluster['statuses']['updated_at'])
153
- now = datetime.now(tz = created_at.tzinfo)
154
-
155
- row = [name, profile_name, region_name, human_readable.date_time(now - created_at), human_readable.date_time(now - updated_at), msg, default]
145
+ initial_clusters = {}
156
146
 
147
+ for cluster in data:
148
+ row, _, name = format_row(cluster.get('statuses'), cluster.get('name'), cluster_id == cluster.get('id'))
149
+ row.insert(1, profile_name)
150
+ row.insert(2, region_name)
151
+ if all:
152
+ project_name = click.style(cluster.get("project_name"), bold=True)
153
+ row.insert(0, project_name)
157
154
  if output == "wide":
158
- row.insert(0, cluster['id'])
159
- row.append(cluster['version'])
160
- row.append(cluster['control_planes'])
161
-
162
- return row, status, cluster['name']
155
+ row.insert(0, cluster.get('id'))
156
+ row.append(cluster.get('version'))
157
+ row.append(cluster.get('control_planes'))
163
158
 
164
- initial_clusters = {}
165
- for cluster in data:
166
- row, _, name = format_row(cluster)
167
159
  table.add_row(row)
168
- initial_clusters[name] = cluster
160
+ initial_clusters[cluster.get("id")] = cluster
169
161
 
170
162
  click.echo(table)
171
163
 
@@ -177,46 +169,66 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
177
169
  total_sleep += 2
178
170
 
179
171
  try:
180
- data = do_request("GET", 'clusters', params=params)
172
+ if all:
173
+ projects = {project["id"]: project for project in do_request("GET", "projects")}
174
+ data = do_request("GET", "clusters/all", params=params)
175
+
176
+ for cluster in data:
177
+ project = projects.get(cluster.get("project_id"))
178
+ cluster["project_name"] = project.get("name")
179
+ else:
180
+ data = do_request("GET", 'clusters', params=params)
181
181
  except click.ClickException as err:
182
182
  click.echo(f"Error during watch: {err}")
183
183
  continue
184
184
 
185
- current_cluster_names = {cluster['name'] for cluster in data}
185
+ current_cluster_ids = {cluster.get('id') for cluster in data}
186
186
 
187
- for name, cluster in list(initial_clusters.items()):
188
- if name not in current_cluster_names:
187
+ for id, cluster in list(initial_clusters.items()):
188
+ if id not in current_cluster_ids:
189
189
  deleted_cluster = cluster.copy()
190
190
  deleted_cluster['statuses']['status'] = 'deleted'
191
191
 
192
- row, current_status, _ = format_row(deleted_cluster)
192
+ row, current_status, _ = format_row(deleted_cluster.get('statuses'), deleted_cluster.get('name'), cluster_id == deleted_cluster.get('id'))
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)
193
198
 
194
199
  new_table = format_changed_row(table, row)
195
200
  click.echo(new_table)
196
201
 
197
- del initial_clusters[name]
202
+ del initial_clusters[id]
198
203
 
199
204
  for cluster in data:
200
- row, current_status, name = format_row(cluster)
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
+ cl_id = cluster.get('id')
201
213
 
202
- if name not in initial_clusters:
214
+ if cl_id not in initial_clusters:
203
215
  new_table = format_changed_row(table, row)
204
216
  click.echo(new_table)
205
- initial_clusters[name] = cluster
217
+ initial_clusters[cl_id] = cluster
206
218
  continue
207
219
 
208
- stored_cluster = initial_clusters[name]
220
+ stored_cluster = initial_clusters[cl_id]
209
221
  cluster_status = stored_cluster.get('statuses').get('status')
210
222
  if cluster_status != current_status:
211
223
  new_table = format_changed_row(table, row)
212
224
  click.echo(new_table)
213
- initial_clusters[name] = cluster
225
+ initial_clusters[cl_id] = cluster
214
226
  continue
215
227
 
216
228
  if total_sleep % 10 == 0 and is_interesting_status(current_status):
217
229
  new_table = format_changed_row(table, row)
218
230
  click.echo(new_table)
219
- initial_clusters[name] = cluster
231
+ initial_clusters[cl_id] = cluster
220
232
 
221
233
  except KeyboardInterrupt:
222
234
  click.echo("\nWatch stopped.")
@@ -473,7 +485,23 @@ def cluster_update_command(ctx, project_name, cluster_name, description, admin,
473
485
  if len(admin) == 0:
474
486
  cluster_config['admin_whitelist'] = []
475
487
  else:
476
- cluster_config['admin_whitelist'] = admin.split(',')
488
+ admin_list = admin.split(',')
489
+ resolved_ips = []
490
+ for ip in admin_list:
491
+ ip = ip.strip()
492
+ if ip == "my-ip":
493
+ try:
494
+ data = do_request("GET", "myip")
495
+ if isinstance(data, dict) and "x_real_ip" in data:
496
+ resolved_ip = data["x_real_ip"]
497
+ resolved_ips.append(f"{resolved_ip}/32")
498
+ else:
499
+ raise click.ClickException(f"Unexpected response format from 'myip': {data}")
500
+ except Exception as e:
501
+ raise click.ClickException(f"Unable to resolve 'my-ip': {e}")
502
+ else:
503
+ resolved_ips.append(ip)
504
+ cluster_config['admin_whitelist'] = resolved_ips
477
505
 
478
506
  if version is not None:
479
507
  cluster_config['version'] = version
@@ -5,8 +5,12 @@ import dateutil.parser
5
5
  import human_readable
6
6
  import prettytable
7
7
  import os
8
+ from prettytable import TableStyle
8
9
 
9
- from .utils import do_request, print_output, print_table, find_project_id_by_name, get_project_id, set_project_id, detect_and_parse_input, transform_tuple, ctx_update, set_cluster_id, get_template, get_project_name, format_changed_row, is_interesting_status, login_profile, profile_completer, project_completer
10
+ from .utils import do_request, print_output, print_table, find_project_id_by_name, get_project_id, set_project_id, \
11
+ detect_and_parse_input, transform_tuple, ctx_update, set_cluster_id, get_template, get_project_name, \
12
+ format_changed_row, is_interesting_status, login_profile, profile_completer, project_completer, \
13
+ format_row
10
14
 
11
15
  # DEIFNE THE PROJECT COMMAND GROUP
12
16
  @click.group(help="Project related commands.")
@@ -73,6 +77,7 @@ def project_list(ctx, project_name, deleted, plain, msword, uuid, watch, output,
73
77
  login_profile(profile)
74
78
 
75
79
  profile_name = os.getenv('OKS_PROFILE')
80
+ region_name = os.getenv('OKS_REGION')
76
81
  project_id = get_project_id()
77
82
 
78
83
  params = {}
@@ -98,51 +103,24 @@ def project_list(ctx, project_name, deleted, plain, msword, uuid, watch, output,
98
103
  table._min_width = {"CREATED": 13, "UPDATED": 13, "STATUS": 10}
99
104
 
100
105
  if plain or watch:
101
- table.set_style(prettytable.PLAIN_COLUMNS)
106
+ table.set_style(TableStyle.PLAIN_COLUMNS)
102
107
 
103
108
  if msword:
104
109
  table.set_style(prettytable.MSWORD_FRIENDLY)
105
110
 
106
- def format_row(project):
107
- status = project.get('status')
108
- is_default = True if project.get('id') == project_id else False
109
-
110
- if status == 'ready':
111
- msg = click.style(status, fg='green')
112
- elif status == 'failed' or status == 'deleted':
113
- msg = click.style(status, fg='red')
114
- elif status == 'deploying':
115
- msg = click.style(status, fg='yellow')
116
- else:
117
- msg = status
118
-
119
- name = click.style(project['name'], bold=True)
120
- if is_default:
121
- default = "*"
122
- else:
123
- default = ""
124
-
125
- region_name = project.get('region')
126
- created_at = dateutil.parser.parse(project['created_at'])
127
- updated_at = dateutil.parser.parse(project['updated_at'])
128
- now = datetime.datetime.now(tz=created_at.tzinfo)
129
-
130
- row = [name, profile_name, region_name, human_readable.date_time(now - created_at), human_readable.date_time(now - updated_at), msg, default]
131
- if uuid:
132
- row.append(project['id'])
133
-
134
- return row, status, project['name']
135
-
136
111
  initial_projects = {}
137
112
 
138
113
  for project in data:
139
- row, _, name = format_row(project)
114
+ row, _, name = format_row(project, project.get('name'), project_id == project.get('id'))
115
+ row.insert(1, profile_name)
116
+ row.insert(2, region_name)
117
+ if uuid:
118
+ row.append(project.get('id'))
140
119
  table.add_row(row)
141
120
  initial_projects[name] = project
142
121
 
143
122
  click.echo(table)
144
123
 
145
-
146
124
  if watch:
147
125
  total_sleep = 0
148
126
  try:
@@ -162,15 +140,19 @@ def project_list(ctx, project_name, deleted, plain, msword, uuid, watch, output,
162
140
  deleted_project = project.copy()
163
141
  deleted_project['status'] = 'deleted'
164
142
 
165
- row, current_status, _ = format_row(deleted_project)
166
-
143
+ row, current_status, _ = format_row(deleted_project, deleted_project.get('name'), project_id == deleted_project.get('id'))
144
+ row.insert(1, profile_name)
145
+ row.insert(2, region_name)
167
146
  new_table = format_changed_row(table, row)
147
+
168
148
  click.echo(new_table)
169
149
 
170
150
  del initial_projects[name]
171
151
 
172
152
  for project in data:
173
- row, current_status, name = format_row(project)
153
+ row, current_status, name = format_row(project, project.get('name'), project_id == project.get('id'))
154
+ row.insert(1, profile_name)
155
+ row.insert(2, region_name)
174
156
 
175
157
  if name not in initial_projects:
176
158
  new_table = format_changed_row(table, row)
@@ -14,7 +14,8 @@ from datetime import datetime
14
14
  import OpenSSL
15
15
  import shutil
16
16
  import prettytable
17
-
17
+ import dateutil.parser
18
+ import human_readable
18
19
  import base64
19
20
  import sys
20
21
 
@@ -35,6 +36,9 @@ class JSONClickException(click.ClickException):
35
36
  def show(self, file=None):
36
37
  click.echo(self.message, file=file)
37
38
 
39
+ class _LiteralStr(str): pass
40
+
41
+ yaml.add_representer(_LiteralStr, lambda dumper, data: dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|'))
38
42
 
39
43
  def find_response_object(data):
40
44
  """Extract the main object from the API response payload."""
@@ -71,6 +75,8 @@ def find_response_object(data):
71
75
  return response["Snapshots"]
72
76
  elif key == "PublicIps":
73
77
  return response["PublicIps"]
78
+ elif key == "IP":
79
+ return response["IP"]
74
80
 
75
81
  raise click.ClickException("The API response format is incorrect.")
76
82
 
@@ -158,12 +164,22 @@ def build_headers():
158
164
 
159
165
  return headers
160
166
 
167
+ def _convert_multiline(obj):
168
+ """Recursively convert multiline strings"""
169
+ if isinstance(obj, dict):
170
+ return {k: _convert_multiline(v) for k, v in obj.items()}
171
+ if isinstance(obj, list):
172
+ return [_convert_multiline(i) for i in obj]
173
+ if isinstance(obj, str) and '\n' in obj:
174
+ return _LiteralStr(obj)
175
+ return obj
176
+
161
177
  def print_output(data, output_fromat):
162
178
  """Print data in the specified format: JSON, YAML, or silent."""
163
179
  output_data = json.dumps(data, indent=4)
164
180
 
165
181
  if output_fromat == "yaml":
166
- output_data = yaml.dump(data, sort_keys=False)
182
+ output_data = yaml.dump(_convert_multiline(data), sort_keys=False)
167
183
 
168
184
  elif output_fromat == "silent":
169
185
  return
@@ -365,6 +381,34 @@ def login_profile(name):
365
381
 
366
382
  return {}
367
383
 
384
+ def format_row(data: dict, name: str, is_default: bool):
385
+ """Parse status and dates from a cluster of project object and returns elements"""
386
+
387
+ if not data.get('status'):
388
+ raise click.ClickException(f"Can't find 'status' in project/cluster data")
389
+
390
+ status = data.get('status')
391
+ if status == 'ready':
392
+ msg = click.style(status, fg='green')
393
+ elif status in ['failed', 'deleted']:
394
+ msg = click.style(status, fg='red')
395
+ elif status in ['deploying', 'deleting', 'pending']:
396
+ msg = click.style(status, fg='yellow')
397
+ else:
398
+ msg = status
399
+
400
+ if is_default:
401
+ default = "*"
402
+ else:
403
+ default = ""
404
+
405
+ created_at = dateutil.parser.parse(data['created_at'])
406
+ updated_at = dateutil.parser.parse(data['updated_at'])
407
+ now = datetime.now(tz=created_at.tzinfo)
408
+
409
+ row = [click.style(name, bold=True), human_readable.date_time(now - created_at), human_readable.date_time(now - updated_at), msg, default]
410
+ return row, status, name
411
+
368
412
  def profile_list():
369
413
  """Return all profiles as a dict, or empty if none."""
370
414
  _, PROFILE_FILE = get_config_path()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oks-cli
3
- Version: 1.16
3
+ Version: 1.17
4
4
  Author: Outscale SAS
5
5
  Author-email: opensource@outscale.com
6
6
  License: BSD
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="oks-cli",
5
- version="1.16",
5
+ version="1.17",
6
6
  packages=['oks_cli'],
7
7
  author="Outscale SAS",
8
8
  author_email="opensource@outscale.com",
@@ -66,6 +66,37 @@ def test_cluster_list_all_args(mock_request, add_default_profile):
66
66
  assert result.exit_code == 0
67
67
  assert "test-cluster" in result.output
68
68
 
69
+ # Test the "cluster list" command with --all(-A) flag
70
+ @patch("oks_cli.utils.requests.request")
71
+ def test_cluster_list_all(mock_request, add_default_profile):
72
+ mock_request.side_effect = [
73
+ MagicMock(status_code=200, headers={}, json=lambda: {"ResponseContext": {}, "Projects": [{"id": "12345", "name": "test-project"}]}),
74
+ MagicMock(status_code=200, headers={}, json=lambda: {
75
+ "ResponseContext": {},
76
+ "Clusters": [{
77
+ "id": "67890",
78
+ "project_id": "12345",
79
+ "name": "test-cluster",
80
+ "statuses": {
81
+ "status": "ready",
82
+ "created_at": "2019-08-24T14:15:22Z",
83
+ "updated_at": "2019-08-24T14:15:22Z"
84
+ }
85
+ }]
86
+ }),
87
+ ]
88
+
89
+ runner = CliRunner()
90
+ result = runner.invoke(cli, [
91
+ "cluster", "list",
92
+ "-A",
93
+ "--profile", "default"
94
+ ])
95
+
96
+ assert result.exit_code == 0
97
+ assert "test-project" in result.output
98
+ assert "test-cluster" in result.output
99
+
69
100
  # Test the "cluster get" command: verifies fetching details of a specific cluster
70
101
  @patch("oks_cli.utils.requests.request")
71
102
  def test_cluster_get_command(mock_request, add_default_profile):
@@ -438,3 +469,92 @@ def test_cluster_create_by_one_click_command(mock_request, mock_sleep, mock_for
438
469
  result = runner.invoke(cli, ["cluster", "create", "-p", "default", "-c", "test"], input=input_data)
439
470
  assert result.exit_code == 0
440
471
 
472
+ @patch("oks_cli.utils.requests.request")
473
+ @patch("time.sleep")
474
+ def test_cluster_list_watch_command(mock_sleep, mock_request, add_default_profile):
475
+ """Test the cluster list command with --watch option"""
476
+
477
+ # Simulate successive API responses
478
+ initial_response = MagicMock(
479
+ status_code=200,
480
+ headers={},
481
+ json=lambda: {
482
+ "ResponseContext": {},
483
+ "Projects": [{"id": "12345"}]
484
+ }
485
+ )
486
+
487
+ # First query for clusters (initial state)
488
+ first_cluster_response = MagicMock(
489
+ status_code=200,
490
+ headers={},
491
+ json=lambda: {
492
+ "ResponseContext": {},
493
+ "Clusters": [{
494
+ "id": "12345",
495
+ "name": "test-cluster",
496
+ "statuses": {
497
+ "status": "deploying",
498
+ "created_at": "2023-01-01T00:00:00Z",
499
+ "updated_at": "2023-01-01T00:00:00Z"
500
+ },
501
+ "version": "1.0",
502
+ "control_planes": 3
503
+ }]
504
+ }
505
+ )
506
+
507
+ # Second query for clusters (status change)
508
+ second_cluster_response = MagicMock(
509
+ status_code=200,
510
+ headers={},
511
+ json=lambda: {
512
+ "ResponseContext": {},
513
+ "Clusters": [{
514
+ "id": "12345",
515
+ "name": "test-cluster",
516
+ "statuses": {
517
+ "status": "ready",
518
+ "created_at": "2023-01-01T00:00:00Z",
519
+ "updated_at": "2023-01-01T00:01:00Z"
520
+ },
521
+ "version": "1.0",
522
+ "control_planes": 3
523
+ }]
524
+ }
525
+ )
526
+
527
+ # Mock call configuration
528
+ mock_request.side_effect = [
529
+ initial_response,
530
+ first_cluster_response,
531
+ second_cluster_response,
532
+ second_cluster_response,
533
+ ]
534
+
535
+ # Simulate KeyboardInterrupt after a few iterations
536
+ def side_effect_sleep(duration):
537
+ if mock_sleep.call_count >= 3:
538
+ raise KeyboardInterrupt()
539
+ return None
540
+
541
+ mock_sleep.side_effect = side_effect_sleep
542
+
543
+ runner = CliRunner()
544
+
545
+ result = runner.invoke(cli, [
546
+ "cluster", "list",
547
+ "-p", "test",
548
+ "-c", "test-cluster",
549
+ "--watch"
550
+ ])
551
+
552
+ # Checks
553
+ assert result.exit_code == 0
554
+ assert "test-cluster" in result.output
555
+ assert "Watch stopped." in result.output
556
+
557
+ assert mock_sleep.called
558
+
559
+ assert mock_request.call_count >= 3
560
+
@@ -473,4 +473,112 @@ def test_project_publicips_yaml(mock_request, add_default_profile):
473
473
  data = yaml.safe_load(result.output)
474
474
  assert isinstance(data, list)
475
475
  assert data == []
476
- # END PROJECT PUBLICIPS COMMAND
476
+ # END PROJECT PUBLICIPS COMMAND
477
+
478
+
479
+ # Test the "project list" command with --watch option
480
+ @patch("oks_cli.utils.requests.request")
481
+ @patch("time.sleep")
482
+ def test_project_list_watch_command(mock_sleep, mock_request, add_default_profile):
483
+ """Test the project list command with --watch option"""
484
+
485
+ # First query for projects (initial state)
486
+ first_project_response = MagicMock(
487
+ status_code=200,
488
+ headers={},
489
+ json=lambda: {
490
+ "ResponseContext": {},
491
+ "Projects": [{
492
+ "id": "12345",
493
+ "name": "test-project",
494
+ "created_at": "2023-01-01T00:00:00Z",
495
+ "updated_at": "2023-01-01T00:00:00Z",
496
+ "status": "active"
497
+ }]
498
+ }
499
+ )
500
+
501
+ # Second query for projects (new project appears)
502
+ second_project_response = MagicMock(
503
+ status_code=200,
504
+ headers={},
505
+ json=lambda: {
506
+ "ResponseContext": {},
507
+ "Projects": [
508
+ {
509
+ "id": "12345",
510
+ "name": "test-project",
511
+ "created_at": "2023-01-01T00:00:00Z",
512
+ "updated_at": "2023-01-01T00:00:00Z",
513
+ "status": "active"
514
+ },
515
+ {
516
+ "id": "67890",
517
+ "name": "new-project",
518
+ "created_at": "2023-01-01T00:01:00Z",
519
+ "updated_at": "2023-01-01T00:01:00Z",
520
+ "status": "creating"
521
+ }
522
+ ]
523
+ }
524
+ )
525
+
526
+ # Third query for projects (status change)
527
+ third_project_response = MagicMock(
528
+ status_code=200,
529
+ headers={},
530
+ json=lambda: {
531
+ "ResponseContext": {},
532
+ "Projects": [
533
+ {
534
+ "id": "12345",
535
+ "name": "test-project",
536
+ "created_at": "2023-01-01T00:00:00Z",
537
+ "updated_at": "2023-01-01T00:00:00Z",
538
+ "status": "active"
539
+ },
540
+ {
541
+ "id": "67890",
542
+ "name": "new-project",
543
+ "created_at": "2023-01-01T00:01:00Z",
544
+ "updated_at": "2023-01-01T00:02:00Z",
545
+ "status": "active"
546
+ }
547
+ ]
548
+ }
549
+ )
550
+
551
+ # Mock call configuration
552
+ mock_request.side_effect = [
553
+ first_project_response,
554
+ second_project_response,
555
+ third_project_response,
556
+ third_project_response,
557
+ ]
558
+
559
+ # Simulate KeyboardInterrupt after a few iterations
560
+ def side_effect_sleep(duration):
561
+ if mock_sleep.call_count >= 3:
562
+ raise KeyboardInterrupt()
563
+ return None
564
+
565
+ mock_sleep.side_effect = side_effect_sleep
566
+
567
+ runner = CliRunner()
568
+
569
+ # Launch command with --watch
570
+ result = runner.invoke(cli, [
571
+ "project", "list",
572
+ "--watch"
573
+ ])
574
+
575
+ # Checks
576
+ assert result.exit_code == 0
577
+ assert "test-project" in result.output
578
+ assert "Watch stopped." in result.output
579
+
580
+ # Verify that sleep was called (indicates watch is working)
581
+ assert mock_sleep.called
582
+
583
+ # Verify multiple API calls were made (at least 3 for watching)
584
+ assert mock_request.call_count >= 3
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