oks-cli 1.14__py3-none-any.whl
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/__init__.py +0 -0
- oks_cli/cache.py +63 -0
- oks_cli/cluster.py +746 -0
- oks_cli/main.py +91 -0
- oks_cli/profile.py +128 -0
- oks_cli/project.py +398 -0
- oks_cli/quotas.py +14 -0
- oks_cli/utils.py +850 -0
- oks_cli-1.14.dist-info/METADATA +270 -0
- oks_cli-1.14.dist-info/RECORD +14 -0
- oks_cli-1.14.dist-info/WHEEL +5 -0
- oks_cli-1.14.dist-info/entry_points.txt +2 -0
- oks_cli-1.14.dist-info/licenses/LICENSE +29 -0
- oks_cli-1.14.dist-info/top_level.txt +1 -0
oks_cli/cluster.py
ADDED
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import subprocess
|
|
3
|
+
import json
|
|
4
|
+
from nacl.public import PrivateKey, SealedBox
|
|
5
|
+
from nacl.encoding import Base64Encoder
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
import os
|
|
9
|
+
import datetime
|
|
10
|
+
import dateutil.parser
|
|
11
|
+
import human_readable
|
|
12
|
+
import prettytable
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
from .utils import do_request, print_output, find_project_id_by_name, find_cluster_id_by_name, get_cache, save_cache, detect_and_parse_input, verify_certificate, shell_completions, transform_tuple, profile_list, login_profile, cluster_create_in_background, ctx_update, set_cluster_id, get_cluster_id, get_project_id, get_template, get_cluster_name, format_changed_row, is_interesting_status, profile_completer
|
|
16
|
+
|
|
17
|
+
from .profile import add_profile
|
|
18
|
+
from .project import project_create, project_login
|
|
19
|
+
|
|
20
|
+
# DEFINE THE CLUSTER GROUP
|
|
21
|
+
@click.group(help="Cluster related commands.")
|
|
22
|
+
@click.option('--project', 'project_name', required = False, help="Project Name")
|
|
23
|
+
@click.option('--project-name', '-p', required = False, help="Project Name")
|
|
24
|
+
@click.option('--name', 'cluster_name', required = False, help="Cluster Name")
|
|
25
|
+
@click.option('--cluster-name', '-c', required = False, help="Cluster Name")
|
|
26
|
+
@click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
|
|
27
|
+
@click.pass_context
|
|
28
|
+
def cluster(ctx, project_name, cluster_name, profile):
|
|
29
|
+
"""Group of commands related to cluster management."""
|
|
30
|
+
ctx_update(ctx, project_name, cluster_name, profile)
|
|
31
|
+
|
|
32
|
+
# LOGIN ON CLUSTER
|
|
33
|
+
@cluster.command('login', help="Set a default cluster")
|
|
34
|
+
@click.option('--cluster-name', '-c', required=False, help="Name of cluster")
|
|
35
|
+
@click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
|
|
36
|
+
@click.pass_context
|
|
37
|
+
def cluster_login(ctx, cluster_name, profile):
|
|
38
|
+
"""Set the specified cluster as the default active cluster."""
|
|
39
|
+
_, cluster_name, profile = ctx_update(ctx, None, cluster_name, profile)
|
|
40
|
+
login_profile(profile)
|
|
41
|
+
|
|
42
|
+
project_id = get_project_id()
|
|
43
|
+
data = do_request("GET", 'clusters', params={"name": cluster_name, "project_id": project_id})
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if len(data) != 1:
|
|
47
|
+
raise click.BadParameter(
|
|
48
|
+
f"{len(data)} clusters found by name: {cluster_name}")
|
|
49
|
+
cluster = data.pop()
|
|
50
|
+
|
|
51
|
+
cluster_id = cluster['id']
|
|
52
|
+
cluster_name = cluster['name']
|
|
53
|
+
|
|
54
|
+
set_cluster_id(cluster_id)
|
|
55
|
+
|
|
56
|
+
cluster_name = click.style(cluster_name, bold=True)
|
|
57
|
+
|
|
58
|
+
click.echo(f"Logged into cluster: {cluster_name}")
|
|
59
|
+
|
|
60
|
+
# LOGOUT ON CLUSTER
|
|
61
|
+
@cluster.command('logout', help="Unset default cluster")
|
|
62
|
+
@click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
|
|
63
|
+
@click.pass_context
|
|
64
|
+
def cluster_logout(ctx, profile):
|
|
65
|
+
"""Clear the current default cluster selection."""
|
|
66
|
+
_, _, profile = ctx_update(ctx, None, None, profile)
|
|
67
|
+
login_profile(profile)
|
|
68
|
+
set_cluster_id("")
|
|
69
|
+
click.echo("Logged out from the current cluster")
|
|
70
|
+
|
|
71
|
+
# LIST CLUSTERS
|
|
72
|
+
@cluster.command('list', help="List all clusters")
|
|
73
|
+
@click.option('--project-name', '-p', required=False, help="Project Name")
|
|
74
|
+
@click.option('--name', 'cluster_name', required = False, help="Cluster Name")
|
|
75
|
+
@click.option('--cluster-name', '-c', required = False, help="Cluster Name")
|
|
76
|
+
@click.option('--deleted', is_flag=True, help="List deleted clusters")
|
|
77
|
+
@click.option('--plain', is_flag=True, help="Plain table format")
|
|
78
|
+
@click.option('--msword', is_flag=True, help="Microsoft Word table format")
|
|
79
|
+
@click.option('--watch', '-w', is_flag=True, help="Watch the changes")
|
|
80
|
+
@click.option('-o', '--output', type=click.Choice(["json", "yaml", "wide"]), help="Specify output format")
|
|
81
|
+
@click.option('--profile', help="Configuration profile to use")
|
|
82
|
+
@click.pass_context
|
|
83
|
+
def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch, output, profile):
|
|
84
|
+
"""Display clusters with optional filtering and real-time monitoring."""
|
|
85
|
+
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
86
|
+
login_profile(profile)
|
|
87
|
+
|
|
88
|
+
project_id = find_project_id_by_name(project_name)
|
|
89
|
+
cluster_id = get_cluster_id()
|
|
90
|
+
|
|
91
|
+
params = {}
|
|
92
|
+
params['project_id'] = project_id
|
|
93
|
+
|
|
94
|
+
if cluster_name:
|
|
95
|
+
params['name'] = cluster_name
|
|
96
|
+
if deleted:
|
|
97
|
+
params['deleted'] = True
|
|
98
|
+
|
|
99
|
+
field_names = ["NAME", "CREATED", "UPDATED", "STATUS", "DEFAULT"]
|
|
100
|
+
|
|
101
|
+
data = do_request("GET", 'clusters', params=params)
|
|
102
|
+
|
|
103
|
+
if output == "wide":
|
|
104
|
+
field_names.insert(0, "ID")
|
|
105
|
+
field_names.append("VERSION")
|
|
106
|
+
field_names.append("CONTROL PLANE")
|
|
107
|
+
elif output:
|
|
108
|
+
print_output(data, output)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
table = prettytable.PrettyTable()
|
|
112
|
+
table.field_names = field_names
|
|
113
|
+
|
|
114
|
+
table._min_width = {"CREATED": 13, "UPDATED": 13, "STATUS": 10}
|
|
115
|
+
|
|
116
|
+
if plain or watch:
|
|
117
|
+
table.set_style(prettytable.PLAIN_COLUMNS)
|
|
118
|
+
|
|
119
|
+
if msword:
|
|
120
|
+
table.set_style(prettytable.MSWORD_FRIENDLY)
|
|
121
|
+
|
|
122
|
+
def format_row(cluster):
|
|
123
|
+
status = cluster['statuses']['status']
|
|
124
|
+
|
|
125
|
+
is_default = True if cluster.get('id') == cluster_id else False
|
|
126
|
+
|
|
127
|
+
if status == 'ready':
|
|
128
|
+
msg = click.style(status, fg='green')
|
|
129
|
+
elif status == 'failed' or status == 'deleted':
|
|
130
|
+
msg = click.style(status, fg='red')
|
|
131
|
+
elif status == 'deploying':
|
|
132
|
+
msg = click.style(status, fg='yellow')
|
|
133
|
+
else:
|
|
134
|
+
msg = status
|
|
135
|
+
|
|
136
|
+
name = click.style(cluster['name'], bold=True)
|
|
137
|
+
if is_default:
|
|
138
|
+
default = "*"
|
|
139
|
+
else:
|
|
140
|
+
default = ""
|
|
141
|
+
|
|
142
|
+
created_at = dateutil.parser.parse(cluster['statuses']['created_at'])
|
|
143
|
+
updated_at = dateutil.parser.parse(cluster['statuses']['updated_at'])
|
|
144
|
+
now = datetime.datetime.now(tz = created_at.tzinfo)
|
|
145
|
+
|
|
146
|
+
row = [name, human_readable.date_time(now - created_at), human_readable.date_time(now - updated_at), msg, default]
|
|
147
|
+
|
|
148
|
+
if output == "wide":
|
|
149
|
+
row.insert(0, cluster['id'])
|
|
150
|
+
row.append(cluster['version'])
|
|
151
|
+
row.append(cluster['control_planes'])
|
|
152
|
+
|
|
153
|
+
return row, status, cluster['name']
|
|
154
|
+
|
|
155
|
+
initial_clusters = {}
|
|
156
|
+
for cluster in data:
|
|
157
|
+
row, _, name = format_row(cluster)
|
|
158
|
+
table.add_row(row)
|
|
159
|
+
initial_clusters[name] = cluster
|
|
160
|
+
|
|
161
|
+
click.echo(table)
|
|
162
|
+
|
|
163
|
+
if watch:
|
|
164
|
+
total_sleep = 0
|
|
165
|
+
try:
|
|
166
|
+
while True:
|
|
167
|
+
time.sleep(2)
|
|
168
|
+
total_sleep += 2
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
data = do_request("GET", 'clusters', params=params)
|
|
172
|
+
except click.ClickException as err:
|
|
173
|
+
click.echo(f"Error during watch: {err}")
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
current_cluster_names = {cluster['name'] for cluster in data}
|
|
177
|
+
|
|
178
|
+
for name, cluster in list(initial_clusters.items()):
|
|
179
|
+
if name not in current_cluster_names:
|
|
180
|
+
deleted_cluster = cluster.copy()
|
|
181
|
+
deleted_cluster['statuses']['status'] = 'deleted'
|
|
182
|
+
|
|
183
|
+
row, current_status, _ = format_row(deleted_cluster)
|
|
184
|
+
|
|
185
|
+
new_table = format_changed_row(table, row)
|
|
186
|
+
click.echo(new_table)
|
|
187
|
+
|
|
188
|
+
del initial_clusters[name]
|
|
189
|
+
|
|
190
|
+
for cluster in data:
|
|
191
|
+
row, current_status, name = format_row(cluster)
|
|
192
|
+
|
|
193
|
+
if name not in initial_clusters:
|
|
194
|
+
new_table = format_changed_row(table, row)
|
|
195
|
+
click.echo(new_table)
|
|
196
|
+
initial_clusters[name] = cluster
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
stored_cluster = initial_clusters[name]
|
|
200
|
+
cluster_status = stored_cluster.get('statuses').get('status')
|
|
201
|
+
if cluster_status != current_status:
|
|
202
|
+
new_table = format_changed_row(table, row)
|
|
203
|
+
click.echo(new_table)
|
|
204
|
+
initial_clusters[name] = cluster
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
if total_sleep % 10 == 0 and is_interesting_status(current_status):
|
|
208
|
+
new_table = format_changed_row(table, row)
|
|
209
|
+
click.echo(new_table)
|
|
210
|
+
initial_clusters[name] = cluster
|
|
211
|
+
|
|
212
|
+
except KeyboardInterrupt:
|
|
213
|
+
click.echo("\nWatch stopped.")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# GET CLUSTER BY NAME
|
|
217
|
+
@cluster.command('get', help="Get a cluster by name")
|
|
218
|
+
@click.option('--project-name', '-p', required = False, help="Project Name")
|
|
219
|
+
@click.option('--name', 'cluster_name', required=False, help="Cluster Name")
|
|
220
|
+
@click.option('--cluster-name', '-c', required=False, help="Cluster Name")
|
|
221
|
+
@click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
222
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
223
|
+
@click.pass_context
|
|
224
|
+
def cluster_get_command(ctx, project_name, cluster_name, output, profile):
|
|
225
|
+
"""Retrieve and display detailed information about a specific cluster."""
|
|
226
|
+
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
227
|
+
login_profile(profile)
|
|
228
|
+
|
|
229
|
+
project_id = find_project_id_by_name(project_name)
|
|
230
|
+
cluster_id = find_cluster_id_by_name(project_id, cluster_name)
|
|
231
|
+
|
|
232
|
+
data = do_request("GET", f'clusters/{cluster_id}')
|
|
233
|
+
|
|
234
|
+
print_output(data, output)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _create_cluster(project_name, cluster_config, output):
|
|
238
|
+
"""Create a new cluster with interactive setup for missing profiles/projects."""
|
|
239
|
+
profiles = profile_list()
|
|
240
|
+
ctx = click.get_current_context()
|
|
241
|
+
|
|
242
|
+
if profiles == {} and click.confirm("Looks like there is no profile set.\nWould you like to add a profile to proceed with cluster creation?"):
|
|
243
|
+
|
|
244
|
+
profile_name = "default"
|
|
245
|
+
region = click.prompt("Choose the region", type=click.Choice(['eu-west-2', 'cloudgouv-eu-west-1'], case_sensitive=False))
|
|
246
|
+
endpoint = None
|
|
247
|
+
|
|
248
|
+
if click.confirm("Do you want to use a custom endpoint?"):
|
|
249
|
+
endpoint = click.prompt("Endpoint")
|
|
250
|
+
|
|
251
|
+
profile_type = click.prompt("Choose the profile type", type=click.Choice(['ak/sk', 'username'], case_sensitive=False))
|
|
252
|
+
|
|
253
|
+
if profile_type == "ak/sk":
|
|
254
|
+
access_key = click.prompt("AccessKey")
|
|
255
|
+
secret_key = click.prompt("SecretKey")
|
|
256
|
+
ctx.invoke(add_profile,
|
|
257
|
+
profile_name=profile_name,
|
|
258
|
+
access_key=access_key,
|
|
259
|
+
secret_key=secret_key,
|
|
260
|
+
region=region,
|
|
261
|
+
endpoint=endpoint)
|
|
262
|
+
else:
|
|
263
|
+
username = click.prompt("Username")
|
|
264
|
+
password = click.prompt("Password")
|
|
265
|
+
ctx.invoke(add_profile,
|
|
266
|
+
profile_name=profile_name,
|
|
267
|
+
username=username,
|
|
268
|
+
password=password,
|
|
269
|
+
region=region,
|
|
270
|
+
endpoint=endpoint)
|
|
271
|
+
|
|
272
|
+
login_profile(profile_name)
|
|
273
|
+
|
|
274
|
+
project_name = project_name or "default"
|
|
275
|
+
projects = do_request("GET", 'projects', params={"name": project_name})
|
|
276
|
+
|
|
277
|
+
cluster_template = get_template('cluster')
|
|
278
|
+
cluster_template.update(cluster_config)
|
|
279
|
+
|
|
280
|
+
project_name_styled = click.style(project_name, bold=True)
|
|
281
|
+
cluster_name_styled = click.style(cluster_template.get("name"), bold=True)
|
|
282
|
+
|
|
283
|
+
msg = f"\nYour cluster {cluster_name_styled} is currently being created.\n"
|
|
284
|
+
|
|
285
|
+
if len(projects) != 1 and click.confirm(f"Unable to find project: {project_name_styled}\nDo you want create a new project for your cluster?"):
|
|
286
|
+
ctx.invoke(project_create, project_name = project_name, output="silent")
|
|
287
|
+
msg = f"\nYour cluster {cluster_name_styled} and project {project_name_styled} are currently being created.\n"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
project_id = find_project_id_by_name(project_name)
|
|
291
|
+
ctx.invoke(project_login, project_name = project_name)
|
|
292
|
+
|
|
293
|
+
cluster_template['project_id'] = project_id
|
|
294
|
+
|
|
295
|
+
text = f"{msg}To monitor the progress, please use the following commands.\n\nTo check the progress of project provisioning:\n$ oks-cli project list\n\nAnd to check the progress after cluster provisioning:\n$ oks-cli cluster list\n"
|
|
296
|
+
|
|
297
|
+
cluster_create_in_background(cluster_template, text)
|
|
298
|
+
|
|
299
|
+
else:
|
|
300
|
+
project_id = find_project_id_by_name(project_name)
|
|
301
|
+
|
|
302
|
+
cluster_template = get_template('cluster')
|
|
303
|
+
cluster_template.update(cluster_config)
|
|
304
|
+
|
|
305
|
+
do_request("GET", f'projects/{project_id}')
|
|
306
|
+
cluster_template['project_id'] = project_id
|
|
307
|
+
|
|
308
|
+
data = do_request("POST", 'clusters', json=cluster_template)
|
|
309
|
+
print_output(data, output)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# CLUSTER CREATE BY NAME
|
|
313
|
+
@cluster.command('create', help="Create a new cluster")
|
|
314
|
+
@click.option('--project-name', '-p', required=False, help="Project Name")
|
|
315
|
+
@click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name")
|
|
316
|
+
@click.option('--description', help="Description of the cluster")
|
|
317
|
+
@click.option('--admin', help="Admin Whitelist")
|
|
318
|
+
@click.option('--version', shell_complete=shell_completions, help="Kubernetes version")
|
|
319
|
+
@click.option('--cidr-pods', help="CIDR of pods")
|
|
320
|
+
@click.option('--cidr-service', help='CIDR of services')
|
|
321
|
+
@click.option('--control-plane', shell_complete=shell_completions, help="Controlplane plan")
|
|
322
|
+
@click.option('--zone', multiple=True, shell_complete=shell_completions, help="List of Control Plane availability zones")
|
|
323
|
+
@click.option('--enable-admission-plugins', help="List of admission plugins, separated by commas")
|
|
324
|
+
@click.option('--disable-admission-plugins', help="List of admission plugins, separated by commas")
|
|
325
|
+
@click.option('--quirk', multiple=True, help="Quirk")
|
|
326
|
+
@click.option('--tags', help="Comma-separated list of tags, example: 'key1=value1,key2=value2'")
|
|
327
|
+
@click.option('--disable-api-termination', type=click.BOOL, help="Disable delete action by API")
|
|
328
|
+
@click.option('--dry-run', is_flag=True, help="Client dry-run, only print the object that would be sent, without sending it")
|
|
329
|
+
@click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
330
|
+
@click.option('-f', '--filename', type=click.File("r"), help="Path to file to use to create the cluster ")
|
|
331
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
332
|
+
@click.pass_context
|
|
333
|
+
def cluster_create_command(ctx, project_name, cluster_name, description, admin, version, cidr_pods, cidr_service, control_plane, zone, enable_admission_plugins, disable_admission_plugins, quirk, tags, disable_api_termination, dry_run, output, filename, profile):
|
|
334
|
+
"""CLI command to create a new Kubernetes cluster with optional configuration parameters."""
|
|
335
|
+
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
336
|
+
login_profile(profile)
|
|
337
|
+
|
|
338
|
+
cluster_config = {}
|
|
339
|
+
|
|
340
|
+
if filename:
|
|
341
|
+
input_data = filename.read()
|
|
342
|
+
cluster_config = detect_and_parse_input(input_data)
|
|
343
|
+
|
|
344
|
+
if not cluster_name and "name" not in cluster_config:
|
|
345
|
+
raise click.BadArgumentUsage("Missing option '--cluster-name' / '-c'.")
|
|
346
|
+
|
|
347
|
+
if cluster_name:
|
|
348
|
+
cluster_config['name'] = cluster_name
|
|
349
|
+
|
|
350
|
+
if description:
|
|
351
|
+
cluster_config['description'] = description
|
|
352
|
+
|
|
353
|
+
if admin:
|
|
354
|
+
cluster_config['admin_whitelist'] = admin.split(',')
|
|
355
|
+
|
|
356
|
+
if version:
|
|
357
|
+
cluster_config['version'] = version
|
|
358
|
+
|
|
359
|
+
if cidr_pods:
|
|
360
|
+
cluster_config['cidr_pods'] = cidr_pods
|
|
361
|
+
|
|
362
|
+
if cidr_service:
|
|
363
|
+
cluster_config['cidr_service'] = cidr_service
|
|
364
|
+
|
|
365
|
+
if control_plane:
|
|
366
|
+
cluster_config['control_planes'] = control_plane
|
|
367
|
+
|
|
368
|
+
if zone:
|
|
369
|
+
if len(zone) > 1:
|
|
370
|
+
cluster_config['cp_multi_az'] = True
|
|
371
|
+
cluster_config['cp_subregions'] = list(zone) # see kube_quirks section
|
|
372
|
+
|
|
373
|
+
if enable_admission_plugins is not None or disable_admission_plugins is not None:
|
|
374
|
+
cluster_config['admission_flags'] = {}
|
|
375
|
+
|
|
376
|
+
if enable_admission_plugins is not None:
|
|
377
|
+
cluster_config['admission_flags']['enable_admission_plugins'] = enable_admission_plugins.split(',')
|
|
378
|
+
|
|
379
|
+
if disable_admission_plugins is not None:
|
|
380
|
+
cluster_config['admission_flags']['disable_admission_plugins'] = disable_admission_plugins.split(',')
|
|
381
|
+
|
|
382
|
+
if tags:
|
|
383
|
+
parsed_tags = {}
|
|
384
|
+
|
|
385
|
+
pairs = tags.split(',')
|
|
386
|
+
for pair in pairs:
|
|
387
|
+
if '=' not in pair:
|
|
388
|
+
raise click.ClickException(f"Malformed tags: '{pair}' (expected key=value)")
|
|
389
|
+
key, value = pair.split('=', 1)
|
|
390
|
+
parsed_tags[key.strip()] = value.strip()
|
|
391
|
+
|
|
392
|
+
cluster_config['tags'] = parsed_tags
|
|
393
|
+
|
|
394
|
+
if quirk:
|
|
395
|
+
# Convert the tuple to a list because multiple=True in the decorator returns a tuple
|
|
396
|
+
cluster_config['quirks'] = transform_tuple(quirk)
|
|
397
|
+
|
|
398
|
+
if disable_api_termination is not None:
|
|
399
|
+
cluster_config["disable_api_termination"] = disable_api_termination
|
|
400
|
+
|
|
401
|
+
if not dry_run:
|
|
402
|
+
_create_cluster(project_name, cluster_config, output)
|
|
403
|
+
else:
|
|
404
|
+
cluster_template = get_template("cluster")
|
|
405
|
+
cluster_template.update(cluster_config)
|
|
406
|
+
print_output(cluster_template, output)
|
|
407
|
+
|
|
408
|
+
# UPDATE CLUSTER
|
|
409
|
+
@cluster.command('update', help="Update a cluster by name")
|
|
410
|
+
@click.option('--project-name', '-p', required=False, help="Project name")
|
|
411
|
+
@click.option('--name', 'cluster_name', required=False, help="Cluster name")
|
|
412
|
+
@click.option('--cluster-name', '-c', required=False, help="Cluster name")
|
|
413
|
+
@click.option('--description', help="Description of the cluster")
|
|
414
|
+
@click.option('--admin', help="Admin Whitelist")
|
|
415
|
+
@click.option('--version', shell_complete=shell_completions, help="Kubernetes version")
|
|
416
|
+
@click.option('--tags', help="Comma-separated list of tags, example: 'key1=value1,key2=value2'")
|
|
417
|
+
@click.option('--enable-admission-plugins', help="List of admission plugins, separated by commas")
|
|
418
|
+
@click.option('--disable-admission-plugins', help="List of admission plugins, separated by commas")
|
|
419
|
+
@click.option('--quirk', multiple=True, help="Quirk")
|
|
420
|
+
@click.option('--disable-api-termination', type=click.BOOL, help="Disable delete action by API")
|
|
421
|
+
@click.option('--control-plane', shell_complete=shell_completions, help="Controlplane plan")
|
|
422
|
+
@click.option('--dry-run', is_flag=True, help="Client dry-run, only print the object that would be sent, without sending it")
|
|
423
|
+
@click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
424
|
+
@click.option('-f', '--filename', type=click.File("r"), help="Path to file to use to update the cluster ")
|
|
425
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
426
|
+
@click.pass_context
|
|
427
|
+
def cluster_update_command(ctx, project_name, cluster_name, description, admin, version, tags, enable_admission_plugins, disable_admission_plugins, quirk, disable_api_termination, control_plane, dry_run, output, filename, profile):
|
|
428
|
+
"""CLI command to update an existing Kubernetes cluster with new configuration options."""
|
|
429
|
+
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
430
|
+
login_profile(profile)
|
|
431
|
+
|
|
432
|
+
project_id = find_project_id_by_name(project_name)
|
|
433
|
+
cluster_id = find_cluster_id_by_name(project_id, cluster_name)
|
|
434
|
+
|
|
435
|
+
cluster_config = {}
|
|
436
|
+
|
|
437
|
+
if filename:
|
|
438
|
+
input_data = filename.read()
|
|
439
|
+
cluster_config = detect_and_parse_input(input_data)
|
|
440
|
+
|
|
441
|
+
if description:
|
|
442
|
+
cluster_config['description'] = description
|
|
443
|
+
|
|
444
|
+
if admin is not None:
|
|
445
|
+
if len(admin) == 0:
|
|
446
|
+
cluster_config['admin_whitelist'] = []
|
|
447
|
+
else:
|
|
448
|
+
cluster_config['admin_whitelist'] = admin.split(',')
|
|
449
|
+
|
|
450
|
+
if version is not None:
|
|
451
|
+
cluster_config['version'] = version
|
|
452
|
+
|
|
453
|
+
if tags is not None:
|
|
454
|
+
parsed_tags = {}
|
|
455
|
+
|
|
456
|
+
if not len(tags) == 0:
|
|
457
|
+
pairs = tags.split(',')
|
|
458
|
+
for pair in pairs:
|
|
459
|
+
if '=' not in pair:
|
|
460
|
+
raise click.ClickException(f"Malformed tags: '{pair}' (expected key=value)")
|
|
461
|
+
key, value = pair.split('=', 1)
|
|
462
|
+
parsed_tags[key.strip()] = value.strip()
|
|
463
|
+
|
|
464
|
+
cluster_config['tags'] = parsed_tags
|
|
465
|
+
|
|
466
|
+
if enable_admission_plugins is not None or disable_admission_plugins is not None:
|
|
467
|
+
cluster_config['admission_flags'] = {}
|
|
468
|
+
|
|
469
|
+
if enable_admission_plugins is not None:
|
|
470
|
+
if len(enable_admission_plugins) == 0:
|
|
471
|
+
cluster_config['admission_flags']['enable_admission_plugins'] = []
|
|
472
|
+
else:
|
|
473
|
+
cluster_config['admission_flags']['enable_admission_plugins'] = enable_admission_plugins.split(',')
|
|
474
|
+
|
|
475
|
+
if disable_admission_plugins is not None:
|
|
476
|
+
if len(disable_admission_plugins) == 0:
|
|
477
|
+
cluster_config['admission_flags']['disable_admission_plugins'] = []
|
|
478
|
+
else:
|
|
479
|
+
cluster_config['admission_flags']['disable_admission_plugins'] = disable_admission_plugins.split(',')
|
|
480
|
+
|
|
481
|
+
if quirk:
|
|
482
|
+
cluster_config['quirks'] = transform_tuple(quirk)
|
|
483
|
+
|
|
484
|
+
if disable_api_termination is not None:
|
|
485
|
+
cluster_config["disable_api_termination"] = disable_api_termination
|
|
486
|
+
|
|
487
|
+
if control_plane:
|
|
488
|
+
cluster_config['control_planes'] = control_plane
|
|
489
|
+
|
|
490
|
+
if dry_run:
|
|
491
|
+
print_output(cluster_config, output)
|
|
492
|
+
else:
|
|
493
|
+
data = do_request("PATCH", f'clusters/{cluster_id}', json=cluster_config)
|
|
494
|
+
print_output(data, output)
|
|
495
|
+
|
|
496
|
+
# UPGRADE CLUSTER
|
|
497
|
+
@cluster.command('upgrade', help="Upgrade a cluster by name")
|
|
498
|
+
@click.option('--project-name', '-p', required=False, help="Project name")
|
|
499
|
+
@click.option('--name', 'cluster_name', required=False, help="Cluster name")
|
|
500
|
+
@click.option('--cluster-name', '-c', required=False, help="Cluster name")
|
|
501
|
+
@click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
502
|
+
@click.option('--force', is_flag=True, help="Force upgrade")
|
|
503
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
504
|
+
@click.pass_context
|
|
505
|
+
def cluster_update_command(ctx, project_name, cluster_name, output, force, profile):
|
|
506
|
+
"""CLI command to upgrade an existing Kubernetes cluster to the latest supported version."""
|
|
507
|
+
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
508
|
+
login_profile(profile)
|
|
509
|
+
|
|
510
|
+
project_id = find_project_id_by_name(project_name)
|
|
511
|
+
cluster_id = find_cluster_id_by_name(project_id, cluster_name)
|
|
512
|
+
cluster_name = get_cluster_name(cluster_name)
|
|
513
|
+
|
|
514
|
+
if force or click.confirm(f"Are you sure you want to upgrade the cluster with name {cluster_name}?", abort=True):
|
|
515
|
+
data = do_request("PATCH", f'clusters/{cluster_id}/upgrade')
|
|
516
|
+
print_output(data, output)
|
|
517
|
+
|
|
518
|
+
# DELETE CLUSTER BY NAME
|
|
519
|
+
@cluster.command('delete', help="Delete a cluster by name")
|
|
520
|
+
@click.option('--project-name', '-p', required=False, help="Project name")
|
|
521
|
+
@click.option('--name', 'cluster_name', required=False, help="Cluster name")
|
|
522
|
+
@click.option('--cluster-name', '-c', required=False, help="Cluster name")
|
|
523
|
+
@click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
524
|
+
@click.option('--dry-run', is_flag=True, help="Run without any action")
|
|
525
|
+
@click.option('--force', is_flag=True, help="Force deletion without confirmation")
|
|
526
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
527
|
+
@click.pass_context
|
|
528
|
+
def cluster_delete_command(ctx, project_name, cluster_name, output, dry_run, force, profile):
|
|
529
|
+
"""CLI command to delete an existing Kubernetes cluster by name."""
|
|
530
|
+
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
531
|
+
login_profile(profile)
|
|
532
|
+
|
|
533
|
+
project_id = find_project_id_by_name(project_name)
|
|
534
|
+
cluster_id = find_cluster_id_by_name(project_id, cluster_name)
|
|
535
|
+
cluster_name = get_cluster_name(cluster_name)
|
|
536
|
+
|
|
537
|
+
cluster_login = get_cluster_id()
|
|
538
|
+
|
|
539
|
+
if dry_run:
|
|
540
|
+
message = {"message": "Dry run: The cluster would be deleted."}
|
|
541
|
+
print_output(message, output)
|
|
542
|
+
return
|
|
543
|
+
|
|
544
|
+
if force or click.confirm(f"Are you sure you want to delete the cluster with name {cluster_name}?", abort=True):
|
|
545
|
+
data = do_request("DELETE", f'clusters/{cluster_id}')
|
|
546
|
+
if cluster_id == cluster_login:
|
|
547
|
+
set_cluster_id("")
|
|
548
|
+
print_output(data, output)
|
|
549
|
+
|
|
550
|
+
# GET KUBECONFIG
|
|
551
|
+
@cluster.command('kubeconfig', help="Fetch the kubeconfig for a cluster")
|
|
552
|
+
@click.option('--project-name', '-p', required=False, help="Project Name")
|
|
553
|
+
@click.option('--name', 'cluster_name', required=False, help="Cluster name")
|
|
554
|
+
@click.option('--cluster-name', '-c', required=False, help="Cluster Name")
|
|
555
|
+
@click.option('--print-path', is_flag=True, help="Print path to saved kubeconfig")
|
|
556
|
+
@click.option('--refresh', '--force', is_flag=True, help="Force refresh saved kubeconfig")
|
|
557
|
+
@click.option('--nacl', is_flag=True, help="Use public key encryption on wire (require api support)")
|
|
558
|
+
@click.option('--user', type=click.STRING, help="User")
|
|
559
|
+
@click.option('--group', type=click.STRING, help="Group")
|
|
560
|
+
@click.option('--ttl', type=click.STRING, help="TTL in human readable format (5h, 1d, 1w)")
|
|
561
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
562
|
+
@click.pass_context
|
|
563
|
+
def cluster_kubeconfig_command(ctx, project_name, cluster_name, print_path, refresh, nacl, user, group, ttl, profile):
|
|
564
|
+
"""CLI command to fetch and optionally print the kubeconfig for a specified cluster."""
|
|
565
|
+
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
566
|
+
login_profile(profile)
|
|
567
|
+
|
|
568
|
+
project_id = find_project_id_by_name(project_name)
|
|
569
|
+
cluster_id = find_cluster_id_by_name(project_id, cluster_name)
|
|
570
|
+
|
|
571
|
+
# @TODO: check expiration in get_cache() code, etc
|
|
572
|
+
kubeconfig_path = get_cache(project_id, cluster_id, 'kubeconfig', user, group)
|
|
573
|
+
|
|
574
|
+
kubeconfig = None
|
|
575
|
+
is_cert_valid = False
|
|
576
|
+
|
|
577
|
+
if kubeconfig_path:
|
|
578
|
+
with open(kubeconfig_path) as f:
|
|
579
|
+
kubeconfig = f.read()
|
|
580
|
+
|
|
581
|
+
if kubeconfig:
|
|
582
|
+
is_cert_valid = verify_certificate(kubeconfig)
|
|
583
|
+
|
|
584
|
+
if not kubeconfig_path or refresh or not is_cert_valid:
|
|
585
|
+
logging.info("extracting kubeconfig by api")
|
|
586
|
+
|
|
587
|
+
params = {}
|
|
588
|
+
if user:
|
|
589
|
+
params["user"] = user
|
|
590
|
+
if group:
|
|
591
|
+
params["group"] = group
|
|
592
|
+
if ttl:
|
|
593
|
+
params["ttl"] = ttl
|
|
594
|
+
|
|
595
|
+
if nacl:
|
|
596
|
+
ephemeral = PrivateKey.generate()
|
|
597
|
+
unsealbox = SealedBox(ephemeral)
|
|
598
|
+
|
|
599
|
+
headers = {
|
|
600
|
+
'x-encrypt-nacl': ephemeral.public_key.encode(Base64Encoder).decode('ascii')
|
|
601
|
+
}
|
|
602
|
+
kubeconfig_raw = do_request("POST", f'clusters/{cluster_id}/kubeconfig', params = params, headers = headers)['data']['kubeconfig']
|
|
603
|
+
else:
|
|
604
|
+
kubeconfig_raw = do_request("GET", f'clusters/{cluster_id}/kubeconfig', params = params)['data']['kubeconfig']
|
|
605
|
+
|
|
606
|
+
if not kubeconfig_raw:
|
|
607
|
+
logging.error("empty response")
|
|
608
|
+
raise SystemExit()
|
|
609
|
+
elif nacl:
|
|
610
|
+
logging.info("decrypting received kubeconfig")
|
|
611
|
+
kubeconfig = unsealbox.decrypt(kubeconfig_raw.encode('ascii'), encoder = Base64Encoder).decode('ascii')
|
|
612
|
+
else:
|
|
613
|
+
kubeconfig = kubeconfig_raw
|
|
614
|
+
|
|
615
|
+
kubeconfig_path = save_cache(project_id, cluster_id, 'kubeconfig', kubeconfig, user, group)
|
|
616
|
+
|
|
617
|
+
if print_path:
|
|
618
|
+
print(kubeconfig_path)
|
|
619
|
+
else:
|
|
620
|
+
print(kubeconfig)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
|
|
624
|
+
"""Run a kubectl command using the cached kubeconfig for the specified cluster, refreshing it if needed."""
|
|
625
|
+
# @TODO: check expiration in get_cache() code, etc
|
|
626
|
+
kubeconfig_path = get_cache(project_id, cluster_id, 'kubeconfig', user, group)
|
|
627
|
+
|
|
628
|
+
kubeconfig = None
|
|
629
|
+
is_cert_valid = False
|
|
630
|
+
|
|
631
|
+
if kubeconfig_path :
|
|
632
|
+
with open(kubeconfig_path) as f:
|
|
633
|
+
kubeconfig = f.read()
|
|
634
|
+
|
|
635
|
+
if kubeconfig:
|
|
636
|
+
is_cert_valid = verify_certificate(kubeconfig)
|
|
637
|
+
|
|
638
|
+
if not kubeconfig_path or not is_cert_valid:
|
|
639
|
+
logging.info("extracting kubeconfig by api")
|
|
640
|
+
|
|
641
|
+
kubeconfig_raw = do_request(
|
|
642
|
+
"GET", f'clusters/{cluster_id}/kubeconfig')['data']['kubeconfig']
|
|
643
|
+
|
|
644
|
+
if not kubeconfig_raw:
|
|
645
|
+
print("Cannot get kubeconfig")
|
|
646
|
+
raise SystemExit()
|
|
647
|
+
|
|
648
|
+
kubeconfig_path = save_cache(project_id, cluster_id, 'kubeconfig', kubeconfig_raw, user, group)
|
|
649
|
+
|
|
650
|
+
env = dict(os.environ)
|
|
651
|
+
env['KUBECONFIG'] = str(kubeconfig_path)
|
|
652
|
+
cmd = ['kubectl']
|
|
653
|
+
cmd += list(args)
|
|
654
|
+
logging.info("running %s", cmd)
|
|
655
|
+
if not input:
|
|
656
|
+
return subprocess.run(cmd, env = env)
|
|
657
|
+
else:
|
|
658
|
+
return subprocess.run(cmd, input=input, text=True, env = env)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
@cluster.command('kubectl', help='Fetch the kubeconfig for a cluster and run kubectl against it', context_settings={"ignore_unknown_options": True})
|
|
662
|
+
@click.option('--project-name', '-p', required=False, help="Project Name")
|
|
663
|
+
@click.option('--cluster-name', '-c', required=False, help="Cluster Name")
|
|
664
|
+
@click.option('--user', type=click.STRING, help="User")
|
|
665
|
+
@click.option('--group', type=click.STRING, help="Group")
|
|
666
|
+
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
|
|
667
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
668
|
+
@click.pass_context
|
|
669
|
+
def cluster_kubectl_command(ctx, project_name, cluster_name, user, group, args, profile):
|
|
670
|
+
"""CLI command to run kubectl against a specified cluster using its kubeconfig."""
|
|
671
|
+
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
672
|
+
login_profile(profile)
|
|
673
|
+
|
|
674
|
+
project_id = find_project_id_by_name(project_name)
|
|
675
|
+
cluster_id = find_cluster_id_by_name(project_id, cluster_name)
|
|
676
|
+
|
|
677
|
+
_run_kubectl(project_id, cluster_id, user, group, args)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
@click.group(help="nodepool related commands.")
|
|
681
|
+
@click.option('--project-name', '-p', required=False, help="Project Name")
|
|
682
|
+
@click.option('--cluster-name', '-c', required=False, help="Cluster Name")
|
|
683
|
+
@click.option('--user', type=click.STRING, help="User")
|
|
684
|
+
@click.option('--group', type=click.STRING, help="Group")
|
|
685
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
686
|
+
@click.pass_context
|
|
687
|
+
def nodepool(ctx, project_name, cluster_name, user, group, profile):
|
|
688
|
+
"""CLI group for nodepool-related commands, managing project, cluster, user, and profile context."""
|
|
689
|
+
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
690
|
+
login_profile(profile)
|
|
691
|
+
|
|
692
|
+
ctx.obj['project_id'] = find_project_id_by_name(project_name)
|
|
693
|
+
ctx.obj['cluster_id'] = find_cluster_id_by_name(
|
|
694
|
+
ctx.obj['project_id'], cluster_name)
|
|
695
|
+
ctx.obj['user'] = user
|
|
696
|
+
ctx.obj['group'] = group
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
cluster.add_command(nodepool)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
@nodepool.command('list')
|
|
703
|
+
@click.pass_context
|
|
704
|
+
def nodepool_list(ctx):
|
|
705
|
+
"""List nodepools in the specified cluster using kubectl."""
|
|
706
|
+
_run_kubectl(ctx.obj['project_id'], ctx.obj['cluster_id'], ctx.obj['user'], ctx.obj['group'], [
|
|
707
|
+
'get', 'nodepool', '-o', 'wide'])
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
@nodepool.command('create')
|
|
711
|
+
@click.option('--nodepool-name', '-n', default="nodepool01", help="Nodepool Name")
|
|
712
|
+
@click.option('--count', default=2, help="Count of nodes")
|
|
713
|
+
@click.option('--type', 'vmtype', default="tinav6.c2r4p3", help="Type of VMs")
|
|
714
|
+
@click.option('--zone', default=["eu-west-2a"], multiple=True, help="Provide zone")
|
|
715
|
+
@click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
716
|
+
@click.option('--dry-run', is_flag=True, help="Run without any action")
|
|
717
|
+
@click.option('-f', '--filename', type=click.File("r"), help="Path to file to use to create the Nodepool ")
|
|
718
|
+
@click.pass_context
|
|
719
|
+
def setup_worker_pool(ctx, nodepool_name, count, vmtype, zone, output, dry_run, filename):
|
|
720
|
+
"""Create a new nodepool in the cluster, optionally from a file or parameters."""
|
|
721
|
+
nodepool = get_template("nodepool")
|
|
722
|
+
|
|
723
|
+
if filename:
|
|
724
|
+
input_data = filename.read()
|
|
725
|
+
nodepool = detect_and_parse_input(input_data)
|
|
726
|
+
else:
|
|
727
|
+
nodepool['metadata']["name"] = nodepool_name
|
|
728
|
+
nodepool['spec']["desiredNodes"] = count
|
|
729
|
+
nodepool['spec']["nodeType"] = vmtype
|
|
730
|
+
if zone:
|
|
731
|
+
nodepool['spec']["zones"] = list(zone)
|
|
732
|
+
|
|
733
|
+
if dry_run:
|
|
734
|
+
print_output(nodepool, output)
|
|
735
|
+
else:
|
|
736
|
+
_run_kubectl(ctx.obj['project_id'], ctx.obj['cluster_id'], ctx.obj['user'], ctx.obj['group'], [
|
|
737
|
+
'create', '-f', '-'], input=json.dumps(nodepool))
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
@nodepool.command('delete')
|
|
741
|
+
@click.option('--nodepool-name', '-n', required=True, help="Nodepool Name")
|
|
742
|
+
@click.pass_context
|
|
743
|
+
def delete_worker_pool(ctx, nodepool_name):
|
|
744
|
+
"""Delete a nodepool by name from the cluster."""
|
|
745
|
+
_run_kubectl(ctx.obj['project_id'], ctx.obj['cluster_id'], ctx.obj['user'], ctx.obj['group'], [
|
|
746
|
+
'delete', 'nodepool', nodepool_name])
|