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.
- {oks_cli-1.16 → oks_cli-1.17}/PKG-INFO +1 -1
- {oks_cli-1.16 → oks_cli-1.17}/oks_cli/cache.py +3 -3
- {oks_cli-1.16 → oks_cli-1.17}/oks_cli/cluster.py +83 -55
- {oks_cli-1.16 → oks_cli-1.17}/oks_cli/project.py +19 -37
- {oks_cli-1.16 → oks_cli-1.17}/oks_cli/utils.py +46 -2
- {oks_cli-1.16 → oks_cli-1.17}/oks_cli.egg-info/PKG-INFO +1 -1
- {oks_cli-1.16 → oks_cli-1.17}/setup.py +1 -1
- {oks_cli-1.16 → oks_cli-1.17}/tests/test_cluster.py +120 -0
- {oks_cli-1.16 → oks_cli-1.17}/tests/test_project.py +109 -1
- {oks_cli-1.16 → oks_cli-1.17}/LICENSE +0 -0
- {oks_cli-1.16 → oks_cli-1.17}/README.md +0 -0
- {oks_cli-1.16 → oks_cli-1.17}/oks_cli/__init__.py +0 -0
- {oks_cli-1.16 → oks_cli-1.17}/oks_cli/main.py +0 -0
- {oks_cli-1.16 → oks_cli-1.17}/oks_cli/profile.py +0 -0
- {oks_cli-1.16 → oks_cli-1.17}/oks_cli/quotas.py +0 -0
- {oks_cli-1.16 → oks_cli-1.17}/oks_cli.egg-info/SOURCES.txt +0 -0
- {oks_cli-1.16 → oks_cli-1.17}/oks_cli.egg-info/dependency_links.txt +0 -0
- {oks_cli-1.16 → oks_cli-1.17}/oks_cli.egg-info/entry_points.txt +0 -0
- {oks_cli-1.16 → oks_cli-1.17}/oks_cli.egg-info/requires.txt +0 -0
- {oks_cli-1.16 → oks_cli-1.17}/oks_cli.egg-info/top_level.txt +0 -0
- {oks_cli-1.16 → oks_cli-1.17}/setup.cfg +0 -0
- {oks_cli-1.16 → oks_cli-1.17}/tests/test_cache.py +0 -0
- {oks_cli-1.16 → oks_cli-1.17}/tests/test_nodepool.py +0 -0
- {oks_cli-1.16 → oks_cli-1.17}/tests/test_profile.py +0 -0
- {oks_cli-1.16 → oks_cli-1.17}/tests/test_quota.py +0 -0
- {oks_cli-1.16 → oks_cli-1.17}/tests/test_shell_completion.py +0 -0
|
@@ -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
|
|
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 =
|
|
60
|
+
style = TableStyle.PLAIN_COLUMNS
|
|
61
61
|
if msword:
|
|
62
|
-
style =
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
140
|
+
table.set_style(TableStyle.PLAIN_COLUMNS)
|
|
127
141
|
|
|
128
142
|
if msword:
|
|
129
143
|
table.set_style(prettytable.MSWORD_FRIENDLY)
|
|
130
144
|
|
|
131
|
-
|
|
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
|
|
159
|
-
row.append(cluster
|
|
160
|
-
row.append(cluster
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
185
|
+
current_cluster_ids = {cluster.get('id') for cluster in data}
|
|
186
186
|
|
|
187
|
-
for
|
|
188
|
-
if
|
|
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[
|
|
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
|
|
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[
|
|
217
|
+
initial_clusters[cl_id] = cluster
|
|
206
218
|
continue
|
|
207
219
|
|
|
208
|
-
stored_cluster = initial_clusters[
|
|
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[
|
|
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[
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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()
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|