oks-cli 1.17__tar.gz → 1.19__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.17 → oks_cli-1.19}/PKG-INFO +2 -1
- {oks_cli-1.17 → oks_cli-1.19}/README.md +1 -0
- {oks_cli-1.17 → oks_cli-1.19}/oks_cli/cluster.py +35 -13
- {oks_cli-1.17 → oks_cli-1.19}/oks_cli/main.py +2 -0
- oks_cli-1.19/oks_cli/netpeering.py +293 -0
- {oks_cli-1.17 → oks_cli-1.19}/oks_cli/profile.py +1 -1
- {oks_cli-1.17 → oks_cli-1.19}/oks_cli/project.py +29 -5
- {oks_cli-1.17 → oks_cli-1.19}/oks_cli/utils.py +132 -19
- {oks_cli-1.17 → oks_cli-1.19}/oks_cli.egg-info/PKG-INFO +2 -1
- {oks_cli-1.17 → oks_cli-1.19}/oks_cli.egg-info/SOURCES.txt +2 -0
- {oks_cli-1.17 → oks_cli-1.19}/setup.py +1 -1
- {oks_cli-1.17 → oks_cli-1.19}/tests/test_cluster.py +1 -1
- oks_cli-1.19/tests/test_netpeering.py +899 -0
- {oks_cli-1.17 → oks_cli-1.19}/tests/test_nodepool.py +1 -1
- {oks_cli-1.17 → oks_cli-1.19}/tests/test_profile.py +27 -1
- {oks_cli-1.17 → oks_cli-1.19}/tests/test_project.py +74 -0
- {oks_cli-1.17 → oks_cli-1.19}/LICENSE +0 -0
- {oks_cli-1.17 → oks_cli-1.19}/oks_cli/__init__.py +0 -0
- {oks_cli-1.17 → oks_cli-1.19}/oks_cli/cache.py +0 -0
- {oks_cli-1.17 → oks_cli-1.19}/oks_cli/quotas.py +0 -0
- {oks_cli-1.17 → oks_cli-1.19}/oks_cli.egg-info/dependency_links.txt +0 -0
- {oks_cli-1.17 → oks_cli-1.19}/oks_cli.egg-info/entry_points.txt +0 -0
- {oks_cli-1.17 → oks_cli-1.19}/oks_cli.egg-info/requires.txt +0 -0
- {oks_cli-1.17 → oks_cli-1.19}/oks_cli.egg-info/top_level.txt +0 -0
- {oks_cli-1.17 → oks_cli-1.19}/setup.cfg +0 -0
- {oks_cli-1.17 → oks_cli-1.19}/tests/test_cache.py +0 -0
- {oks_cli-1.17 → oks_cli-1.19}/tests/test_quota.py +0 -0
- {oks_cli-1.17 → oks_cli-1.19}/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.
|
|
3
|
+
Version: 1.19
|
|
4
4
|
Author: Outscale SAS
|
|
5
5
|
Author-email: opensource@outscale.com
|
|
6
6
|
License: BSD
|
|
@@ -223,6 +223,7 @@ oks-cli/
|
|
|
223
223
|
│ ├── cache.py
|
|
224
224
|
│ ├── cluster.py
|
|
225
225
|
│ ├── main.py
|
|
226
|
+
│ ├── netpeering.py
|
|
226
227
|
│ ├── profile.py
|
|
227
228
|
│ ├── project.py
|
|
228
229
|
│ ├── quotas.py
|
|
@@ -23,7 +23,7 @@ from .utils import cluster_completer, do_request, print_output,
|
|
|
23
23
|
ctx_update, set_cluster_id, get_cluster_id, get_project_id, \
|
|
24
24
|
get_template, get_cluster_name, format_changed_row, \
|
|
25
25
|
is_interesting_status, profile_completer, project_completer, \
|
|
26
|
-
kubeconfig_parse_fields, print_table, format_row
|
|
26
|
+
kubeconfig_parse_fields, print_table, format_row, apply_set_fields
|
|
27
27
|
|
|
28
28
|
from .profile import add_profile
|
|
29
29
|
from .project import project_create, project_login
|
|
@@ -81,7 +81,7 @@ def cluster_logout(ctx, profile):
|
|
|
81
81
|
@cluster.command('list', help="List all clusters")
|
|
82
82
|
@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
|
|
83
83
|
@click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
|
|
84
|
-
@click.option('--deleted', '-x', is_flag=True,
|
|
84
|
+
@click.option('--deleted', '-x', is_flag=True, deprecated="List deleted clusters - Will be removed") # x pour "deleted" / "removed"
|
|
85
85
|
@click.option('--plain', is_flag=True, help="Plain table format")
|
|
86
86
|
@click.option('--msword', is_flag=True, help="Microsoft Word table format")
|
|
87
87
|
@click.option('--watch', '-w', is_flag=True, help="Watch the changes")
|
|
@@ -140,7 +140,7 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
|
|
|
140
140
|
table.set_style(TableStyle.PLAIN_COLUMNS)
|
|
141
141
|
|
|
142
142
|
if msword:
|
|
143
|
-
table.set_style(
|
|
143
|
+
table.set_style(TableStyle.MSWORD_FRIENDLY)
|
|
144
144
|
|
|
145
145
|
initial_clusters = {}
|
|
146
146
|
|
|
@@ -257,6 +257,9 @@ def cluster_get_command(ctx, project_name, cluster_name, output, profile):
|
|
|
257
257
|
def prepare_cluster_template(cluster_config):
|
|
258
258
|
cluster_template = get_template("cluster")
|
|
259
259
|
|
|
260
|
+
if cluster_template.get("project_id") == "":
|
|
261
|
+
cluster_template.pop("project_id", None)
|
|
262
|
+
|
|
260
263
|
admin_whitelist = cluster_config.get("admin_whitelist") or []
|
|
261
264
|
if isinstance(admin_whitelist, str):
|
|
262
265
|
admin_whitelist = [admin_whitelist]
|
|
@@ -368,8 +371,9 @@ def _create_cluster(project_name, cluster_config, output):
|
|
|
368
371
|
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
369
372
|
@click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to create the cluster ")
|
|
370
373
|
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
374
|
+
@click.option('--set', 'set_fields', multiple=True, help="Set arbitrary nested fields, e.g. auth.oidc.issuer-url=value")
|
|
371
375
|
@click.pass_context
|
|
372
|
-
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, cp_multi_az, dry_run, output, filename, profile):
|
|
376
|
+
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, cp_multi_az, dry_run, output, filename, profile, set_fields):
|
|
373
377
|
"""CLI command to create a new Kubernetes cluster with optional configuration parameters."""
|
|
374
378
|
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
375
379
|
login_profile(profile)
|
|
@@ -437,9 +441,11 @@ def cluster_create_command(ctx, project_name, cluster_name, description, admin,
|
|
|
437
441
|
if disable_api_termination is not None:
|
|
438
442
|
cluster_config["disable_api_termination"] = disable_api_termination
|
|
439
443
|
|
|
440
|
-
if cp_multi_az
|
|
444
|
+
if cp_multi_az:
|
|
441
445
|
cluster_config["cp_multi_az"] = cp_multi_az
|
|
442
446
|
|
|
447
|
+
apply_set_fields(cluster_config, set_fields)
|
|
448
|
+
|
|
443
449
|
if not dry_run:
|
|
444
450
|
_create_cluster(project_name, cluster_config, output)
|
|
445
451
|
else:
|
|
@@ -463,8 +469,9 @@ def cluster_create_command(ctx, project_name, cluster_name, description, admin,
|
|
|
463
469
|
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
464
470
|
@click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to update the cluster ")
|
|
465
471
|
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
472
|
+
@click.option('--set', 'set_fields', multiple=True, help="Set arbitrary nested fields, e.g. auth.oidc.issuer-url=value")
|
|
466
473
|
@click.pass_context
|
|
467
|
-
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):
|
|
474
|
+
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, set_fields):
|
|
468
475
|
"""CLI command to update an existing Kubernetes cluster with new configuration options."""
|
|
469
476
|
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
470
477
|
login_profile(profile)
|
|
@@ -501,7 +508,7 @@ def cluster_update_command(ctx, project_name, cluster_name, description, admin,
|
|
|
501
508
|
raise click.ClickException(f"Unable to resolve 'my-ip': {e}")
|
|
502
509
|
else:
|
|
503
510
|
resolved_ips.append(ip)
|
|
504
|
-
cluster_config['admin_whitelist'] = resolved_ips
|
|
511
|
+
cluster_config['admin_whitelist'] = list(dict.fromkeys(resolved_ips))
|
|
505
512
|
|
|
506
513
|
if version is not None:
|
|
507
514
|
cluster_config['version'] = version
|
|
@@ -542,6 +549,8 @@ def cluster_update_command(ctx, project_name, cluster_name, description, admin,
|
|
|
542
549
|
|
|
543
550
|
if control_plane:
|
|
544
551
|
cluster_config['control_planes'] = control_plane
|
|
552
|
+
|
|
553
|
+
apply_set_fields(cluster_config, set_fields)
|
|
545
554
|
|
|
546
555
|
if dry_run:
|
|
547
556
|
print_output(cluster_config, output)
|
|
@@ -697,7 +706,7 @@ def cluster_kubeconfig_command(ctx, project_name, cluster_name, print_path, outp
|
|
|
697
706
|
click.echo(kubeconfig)
|
|
698
707
|
|
|
699
708
|
|
|
700
|
-
def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
|
|
709
|
+
def _run_kubectl(project_id, cluster_id, user, group, args, input=None, capture=False):
|
|
701
710
|
"""Run a kubectl command using the cached kubeconfig for the specified cluster, refreshing it if needed."""
|
|
702
711
|
# @TODO: check expiration in get_cache() code, etc
|
|
703
712
|
kubeconfig_path = get_cache(project_id, cluster_id, 'kubeconfig', user, group)
|
|
@@ -730,9 +739,9 @@ def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
|
|
|
730
739
|
cmd += list(args)
|
|
731
740
|
logging.info("running %s", cmd)
|
|
732
741
|
if not input:
|
|
733
|
-
return subprocess.run(cmd, env =
|
|
742
|
+
return subprocess.run(cmd, env=env, capture_output=capture)
|
|
734
743
|
else:
|
|
735
|
-
return subprocess.run(cmd, input=input, text=True, env =
|
|
744
|
+
return subprocess.run(cmd, input=input, text=True, env=env, capture_output=capture)
|
|
736
745
|
|
|
737
746
|
|
|
738
747
|
@cluster.command('kubectl', help='Fetch the kubeconfig for a cluster and run kubectl against it', context_settings={"ignore_unknown_options": True})
|
|
@@ -819,8 +828,21 @@ def setup_worker_pool(ctx, nodepool_name, count, vmtype, zone, output, dry_run,
|
|
|
819
828
|
|
|
820
829
|
@nodepool.command('delete')
|
|
821
830
|
@click.option('--nodepool-name', '-n', required=True, help="Nodepool Name")
|
|
831
|
+
@click.option('--force', is_flag=True, help="Delete without confirmation")
|
|
822
832
|
@click.pass_context
|
|
823
|
-
def delete_worker_pool(ctx, nodepool_name):
|
|
833
|
+
def delete_worker_pool(ctx, nodepool_name, force):
|
|
824
834
|
"""Delete a nodepool by name from the cluster."""
|
|
825
|
-
|
|
826
|
-
|
|
835
|
+
|
|
836
|
+
if not force:
|
|
837
|
+
click.confirm(
|
|
838
|
+
f"Are you sure you want to delete the nodepool '{nodepool_name}'?",
|
|
839
|
+
abort=True
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
_run_kubectl(
|
|
843
|
+
ctx.obj['project_id'],
|
|
844
|
+
ctx.obj['cluster_id'],
|
|
845
|
+
ctx.obj['user'],
|
|
846
|
+
ctx.obj['group'],
|
|
847
|
+
['delete', 'nodepool', nodepool_name]
|
|
848
|
+
)
|
|
@@ -8,6 +8,7 @@ from .cluster import cluster
|
|
|
8
8
|
from .profile import profile
|
|
9
9
|
from .cache import cache
|
|
10
10
|
from .quotas import quotas
|
|
11
|
+
from .netpeering import netpeering
|
|
11
12
|
|
|
12
13
|
from .utils import ctx_update, install_completions, profile_completer, cluster_completer, project_completer
|
|
13
14
|
|
|
@@ -60,6 +61,7 @@ cli.add_command(cluster)
|
|
|
60
61
|
cli.add_command(profile)
|
|
61
62
|
cli.add_command(cache)
|
|
62
63
|
cli.add_command(quotas)
|
|
64
|
+
cli.add_command(netpeering)
|
|
63
65
|
|
|
64
66
|
def recursive_help(cmd, parent=None):
|
|
65
67
|
"""Recursively prints help for all commands and subcommands."""
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import json
|
|
3
|
+
import yaml
|
|
4
|
+
import time
|
|
5
|
+
import ipaddress
|
|
6
|
+
import uuid
|
|
7
|
+
from subprocess import CalledProcessError
|
|
8
|
+
|
|
9
|
+
from .utils import cluster_completer, print_output, find_project_id_by_name, \
|
|
10
|
+
find_cluster_id_by_name, login_profile, ctx_update, \
|
|
11
|
+
profile_completer, project_completer, find_project_by_name, \
|
|
12
|
+
do_request, get_cluster_name, get_project_name, get_template
|
|
13
|
+
from .cluster import _run_kubectl
|
|
14
|
+
|
|
15
|
+
@click.group(help="NetPeering related commands.")
|
|
16
|
+
@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
|
|
17
|
+
@click.option('--cluster-name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
|
|
18
|
+
@click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
|
|
19
|
+
@click.option('--user', type=click.STRING, help="User for the kubeconfig of the source cluster")
|
|
20
|
+
@click.option('--group', type=click.STRING, help="Group for the kubeconfig of the source cluster")
|
|
21
|
+
@click.pass_context
|
|
22
|
+
def netpeering(ctx, project_name, cluster_name, profile, user, group):
|
|
23
|
+
"""Group of commands related to netpeering management"""
|
|
24
|
+
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
25
|
+
|
|
26
|
+
ctx.obj['user'] = user
|
|
27
|
+
ctx.obj['group'] = group
|
|
28
|
+
|
|
29
|
+
@netpeering.command('list', help="List NetPeering from a project/cluster")
|
|
30
|
+
@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
|
|
31
|
+
@click.option('--cluster-name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
|
|
32
|
+
@click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
|
|
33
|
+
@click.option('--output', '-o', type=click.Choice(["json", "yaml", "wide"]), help="Specify output format")
|
|
34
|
+
@click.pass_context
|
|
35
|
+
def netpeering_list(ctx, project_name, cluster_name, profile, output):
|
|
36
|
+
"""List netpeering in the specified cluster"""
|
|
37
|
+
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
38
|
+
login_profile(profile)
|
|
39
|
+
|
|
40
|
+
project_id = find_project_id_by_name(project_name)
|
|
41
|
+
cluster_id = find_cluster_id_by_name(project_id, cluster_name)
|
|
42
|
+
|
|
43
|
+
cmd = ['get', 'netpeerings']
|
|
44
|
+
if output:
|
|
45
|
+
cmd.extend(['-o', output])
|
|
46
|
+
|
|
47
|
+
_run_kubectl(project_id, cluster_id, ctx.obj['user'], ctx.obj['group'], cmd)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@netpeering.command('get', help="Get information about a NetPeering")
|
|
51
|
+
@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
|
|
52
|
+
@click.option('--cluster-name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
|
|
53
|
+
@click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
|
|
54
|
+
@click.option('--netpeering-id', required=True, type=click.STRING, help="NetPeering to get information from")
|
|
55
|
+
@click.option('--output', '-o', default='json', required=False, type=click.Choice(["json", "yaml", "wide"]), help="Specify output format, default json")
|
|
56
|
+
@click.pass_context
|
|
57
|
+
def netpeering_get(ctx, project_name, cluster_name, profile, netpeering_id, output):
|
|
58
|
+
"""Retrieve information about a NetPeering"""
|
|
59
|
+
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
60
|
+
login_profile(profile)
|
|
61
|
+
|
|
62
|
+
project_id = find_project_id_by_name(project_name)
|
|
63
|
+
cluster_id = find_cluster_id_by_name(project_id, cluster_name)
|
|
64
|
+
|
|
65
|
+
_run_kubectl(project_id, cluster_id, ctx.obj['user'], ctx.obj['group'],
|
|
66
|
+
['get', 'netpeering', netpeering_id, '-o', output])
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@netpeering.command('delete', help="Delete a NetPeering from a project/cluster")
|
|
70
|
+
@click.option('--project-name', '-p', required=False, type=click.STRING, help="Source project name to create netpeering from", shell_complete=project_completer)
|
|
71
|
+
@click.option('--cluster-name', '-c', required=False, type=click.STRING, help="Source cluster to create netpeering from", shell_complete=cluster_completer)
|
|
72
|
+
@click.option('--netpeering-id', required=True, type=click.STRING, help="NetPeering to remove")
|
|
73
|
+
@click.option('--dry-run', required=False, is_flag=True, help="Run without any action")
|
|
74
|
+
@click.option('--force', is_flag=True, help="Force deletion without confirmation")
|
|
75
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
76
|
+
@click.pass_context
|
|
77
|
+
def netpeering_delete(ctx, project_name, cluster_name, netpeering_id, dry_run, force, profile):
|
|
78
|
+
"""Delete a NetPeering between 2 projects"""
|
|
79
|
+
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
80
|
+
login_profile(profile)
|
|
81
|
+
|
|
82
|
+
project_id = find_project_id_by_name(project_name)
|
|
83
|
+
cluster_id = find_cluster_id_by_name(project_id, cluster_name)
|
|
84
|
+
|
|
85
|
+
if dry_run:
|
|
86
|
+
message = {"message": f"Dry run: The netpeering {netpeering_id} would be deleted."}
|
|
87
|
+
print_output(message, 'json')
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
if force or click.confirm(f"Are you sure you want to delete NetPeering with id {netpeering_id}?", abort=True):
|
|
91
|
+
try:
|
|
92
|
+
cmd = _run_kubectl(project_id, cluster_id, ctx.obj['user'], ctx.obj['group'],
|
|
93
|
+
['delete', 'netpeering', netpeering_id], capture=True)
|
|
94
|
+
if cmd.returncode:
|
|
95
|
+
raise click.ClickException(f"Could not delete NetPeering {netpeering_id}: {cmd.stderr.decode('utf-8')}")
|
|
96
|
+
click.echo(f"It may take some times for NetPeering {netpeering_id} to automatically disappear from both projects. Please be patient")
|
|
97
|
+
except CalledProcessError as e:
|
|
98
|
+
raise click.ClickException(f"Could not delete NetPeering {netpeering_id}: {e}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _gather_info(project: str=None, cluster: str=None) -> dict:
|
|
102
|
+
"""
|
|
103
|
+
Gather information about project and cluster required to create a NetPeering
|
|
104
|
+
Set:
|
|
105
|
+
- cluster_name
|
|
106
|
+
- project_name
|
|
107
|
+
- project_id
|
|
108
|
+
- cluster_id
|
|
109
|
+
- project_cidr
|
|
110
|
+
"""
|
|
111
|
+
info = dict()
|
|
112
|
+
|
|
113
|
+
if not project or not cluster:
|
|
114
|
+
raise click.ClickException("Project and cluster name are required")
|
|
115
|
+
|
|
116
|
+
project_data = find_project_by_name(project)
|
|
117
|
+
info.update({'cluster_name': cluster, 'project_name': project})
|
|
118
|
+
info.update({'project_id': project_data.get('id'), 'project_cidr': project_data.get('cidr')})
|
|
119
|
+
info.update({'cluster_id': find_cluster_id_by_name(info.get('project_id'), info.get('cluster_name'))})
|
|
120
|
+
return info
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _netpeering_exists(source: dict=None, target: dict=None, user: str=None, group: str=None) -> bool:
|
|
124
|
+
"""
|
|
125
|
+
Checks if an existing NetPeering already exists between similar project/cluster id
|
|
126
|
+
"""
|
|
127
|
+
# We check if there's not already a NetPeering available and active
|
|
128
|
+
if not source or not target:
|
|
129
|
+
raise AttributeError("source and target must be passesd as dict")
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
netpeerings = json.loads(_run_kubectl(source.get('project_id'), source.get('cluster_id'), user, group,
|
|
133
|
+
['get', 'netpeering', '-o', 'json'],
|
|
134
|
+
capture=True).stdout.decode('utf-8'))
|
|
135
|
+
|
|
136
|
+
for item in netpeerings.get('items'):
|
|
137
|
+
status = item.get('status')
|
|
138
|
+
if status.get('accepterNetId') == target.get('network_id') and \
|
|
139
|
+
status.get('accepterOwnerId') == target.get('account_id') and \
|
|
140
|
+
status.get('sourceNetId') == source.get('network_id') and \
|
|
141
|
+
status.get('sourceOwnerId') == source.get('account_id') and \
|
|
142
|
+
status.get('netPeeringState') == 'active':
|
|
143
|
+
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
except CalledProcessError as e:
|
|
147
|
+
raise click.ClickException(f"Cannot list NetPeerings: {e}")
|
|
148
|
+
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
def _get_vpc_id(project_id: str):
|
|
152
|
+
response = do_request("GET", f"projects/{project_id}/nets")
|
|
153
|
+
|
|
154
|
+
if len(response) == 0:
|
|
155
|
+
raise click.ClickException("Cannot get vpc id")
|
|
156
|
+
|
|
157
|
+
return response[0]["NetId"]
|
|
158
|
+
|
|
159
|
+
def _get_iaas_owner_id(project_id: str):
|
|
160
|
+
response = do_request("GET", f"projects/{project_id}/quotas")["data"]
|
|
161
|
+
|
|
162
|
+
if len(response["quotas"]) == 0:
|
|
163
|
+
raise click.ClickException("Cannot get iaas owner id")
|
|
164
|
+
|
|
165
|
+
return response["quotas"][0]["AccountId"]
|
|
166
|
+
|
|
167
|
+
def _create_netpeering_request(name: str, source: dict, target: dict, user, group):
|
|
168
|
+
netpeering_request = get_template("netpeeringrequest")
|
|
169
|
+
netpeering_request['metadata']['name'] = name
|
|
170
|
+
netpeering_request['spec']['accepterNetId'] = target.get('network_id')
|
|
171
|
+
netpeering_request['spec']['accepterOwnerId'] = target.get('account_id')
|
|
172
|
+
|
|
173
|
+
_run_kubectl(source.get('project_id'), source.get('cluster_id'), user, group,
|
|
174
|
+
['create', '-o', 'json', '-f', '-'], input=json.dumps(netpeering_request), capture=True)
|
|
175
|
+
|
|
176
|
+
# For security, we wait a bit for the status to be availabe
|
|
177
|
+
time.sleep(3)
|
|
178
|
+
|
|
179
|
+
netpeering_request_cmd = _run_kubectl(source.get('project_id'), source.get('cluster_id'), user, group,
|
|
180
|
+
['get', 'netpeeringrequests', '-o', 'json', f"{name}"],
|
|
181
|
+
capture=True)
|
|
182
|
+
if netpeering_request_cmd.returncode:
|
|
183
|
+
raise click.ClickException(f"Cannot create NetPeeringRequest: {netpeering_request_cmd.stderr}")
|
|
184
|
+
|
|
185
|
+
netpeering_request = json.loads(netpeering_request_cmd.stdout.decode('utf-8'))
|
|
186
|
+
|
|
187
|
+
return netpeering_request
|
|
188
|
+
|
|
189
|
+
def _create_netpeering_acceptance(name: str, netpeering_request_status: dict, target: dict, user, group):
|
|
190
|
+
|
|
191
|
+
netpeering_acceptance = get_template("netpeeringacceptance")
|
|
192
|
+
netpeering_id = netpeering_request_status.get('netPeeringId')
|
|
193
|
+
netpeering_acceptance['metadata']['name'] = name
|
|
194
|
+
netpeering_acceptance['spec']['netPeeringId'] = netpeering_id
|
|
195
|
+
|
|
196
|
+
netpeering_request_status = netpeering_request_status.get('netPeeringState')
|
|
197
|
+
if netpeering_request_status != 'pending-acceptance':
|
|
198
|
+
raise click.ClickException(f"NetPeeringAcceptance is in wrong state: {netpeering_request_status}")
|
|
199
|
+
|
|
200
|
+
netpeering_acceptance_cmd = _run_kubectl(target.get('project_id'), target.get('cluster_id'), user, group,
|
|
201
|
+
["create", "-f", "-"], input=json.dumps(netpeering_acceptance),
|
|
202
|
+
capture=True)
|
|
203
|
+
|
|
204
|
+
if netpeering_acceptance_cmd.returncode:
|
|
205
|
+
raise click.ClickException(f"Could not create NetPeeringAcceptance object {netpeering_id}: {netpeering_acceptance_cmd.stderr}")
|
|
206
|
+
|
|
207
|
+
def _dry_run(name_nr: str, name_na: str, output: str):
|
|
208
|
+
netpeering_request = get_template("netpeeringrequest")
|
|
209
|
+
netpeering_request['metadata']['name'] = name_nr
|
|
210
|
+
|
|
211
|
+
netpeering_acceptance = get_template("netpeeringacceptance")
|
|
212
|
+
netpeering_acceptance['metadata']['name'] = name_na
|
|
213
|
+
|
|
214
|
+
manifests = [netpeering_request, netpeering_acceptance]
|
|
215
|
+
|
|
216
|
+
if output == "yaml":
|
|
217
|
+
# print multiple yaml, separated by --- (kubernetes friendly format for multiple manifests)
|
|
218
|
+
output_data = yaml.dump_all(manifests, sort_keys=False)
|
|
219
|
+
click.echo(output_data)
|
|
220
|
+
else:
|
|
221
|
+
print_output([netpeering_request, netpeering_acceptance], output)
|
|
222
|
+
|
|
223
|
+
@netpeering.command('create', help="Create a NetPeering between 2 projects")
|
|
224
|
+
@click.option('--project-name', '--source-project', '-p', required=False, type=click.STRING, help="Source project name to create netpeering from", shell_complete=project_completer)
|
|
225
|
+
@click.option('--cluster-name', '--source-cluster', '-c', required=False, type=click.STRING, help="Source cluster to create netpeering from", shell_complete=cluster_completer)
|
|
226
|
+
@click.option('--target-project', required=True, type=click.STRING, help="Project name to create netpeering to", shell_complete=project_completer)
|
|
227
|
+
@click.option('--target-cluster', required=True, type=click.STRING, help="Target cluster to create netpeering to")
|
|
228
|
+
@click.option('--netpeering-name', required=False, type=click.STRING, help="Name of the NetPeeringRequest, default to '{from-project}-to-{to-project}",
|
|
229
|
+
default=None)
|
|
230
|
+
@click.option('--force', required=False, is_flag=True, help="Create netpeering resources without confirmation")
|
|
231
|
+
@click.option('--user', type=click.STRING, help="User for the kubeconfig of the source cluster")
|
|
232
|
+
@click.option('--group', type=click.STRING, help="Group for the kubeconfig of the source cluster")
|
|
233
|
+
@click.option('--dry-run', is_flag=True, help="Client dry-run, only print the object that would be sent, without sending it")
|
|
234
|
+
@click.option('--output', '-o', type=click.Choice(['json', 'yaml']), default="json", help="Specify output format, by default is json")
|
|
235
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
236
|
+
@click.pass_context
|
|
237
|
+
def netpeering_create(ctx, project_name, cluster_name, target_project, target_cluster, netpeering_name, force,
|
|
238
|
+
user, group, dry_run, output, profile):
|
|
239
|
+
"""Create NetPeering between 2 projects"""
|
|
240
|
+
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
241
|
+
login_profile(profile)
|
|
242
|
+
|
|
243
|
+
project_name = get_project_name(project_name)
|
|
244
|
+
cluster_name = get_cluster_name(cluster_name)
|
|
245
|
+
|
|
246
|
+
source = _gather_info(project=project_name, cluster=cluster_name)
|
|
247
|
+
target = _gather_info(project=target_project, cluster=target_cluster)
|
|
248
|
+
|
|
249
|
+
# Generate name
|
|
250
|
+
if not netpeering_name:
|
|
251
|
+
netpeering_name = f"{source.get('project_name')}-to-{target.get('project_name')}"
|
|
252
|
+
|
|
253
|
+
netpeering_name += f"-{str(uuid.uuid4().fields[-1])[:6]}"
|
|
254
|
+
netpeering_request_name = f"{netpeering_name}-npr"
|
|
255
|
+
netpeering_acceptance_name = f"{netpeering_name}-npa"
|
|
256
|
+
|
|
257
|
+
if dry_run:
|
|
258
|
+
_dry_run(netpeering_request_name, netpeering_acceptance_name, output)
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
source_cidr = ipaddress.ip_network(source.get('project_cidr'))
|
|
262
|
+
target_cidr = ipaddress.ip_network(target.get('project_cidr'))
|
|
263
|
+
|
|
264
|
+
if source_cidr.overlaps(target_cidr) or target_cidr.overlaps(source_cidr):
|
|
265
|
+
raise click.ClickException(f"Source network {source.get('project_cidr')} and target network {target.get('project_cidr')} overlap, you can't create netpeering. Aborted!")
|
|
266
|
+
|
|
267
|
+
source_vpc_id = _get_vpc_id(source.get('project_id'))
|
|
268
|
+
source_iaas_owner_id = _get_iaas_owner_id(source.get('project_id'))
|
|
269
|
+
|
|
270
|
+
target_vpc_id = _get_vpc_id(target.get('project_id'))
|
|
271
|
+
target_iaas_owner_id = _get_iaas_owner_id(target.get('project_id'))
|
|
272
|
+
|
|
273
|
+
source.update({'network_id': source_vpc_id, 'account_id': source_iaas_owner_id})
|
|
274
|
+
target.update({'network_id': target_vpc_id, 'account_id': target_iaas_owner_id})
|
|
275
|
+
|
|
276
|
+
if _netpeering_exists(source=source, target=target, user=user, group=group):
|
|
277
|
+
raise click.ClickException(f"A NetPeering already exists between projects {source.get('project_name')} and {target.get('project_name')}. Aborting!")
|
|
278
|
+
|
|
279
|
+
if not force and \
|
|
280
|
+
not click.confirm(f"Are you sure you want to create NetPeering between projects {source.get('project_name')} and {target.get('project_name')}?", abort=False):
|
|
281
|
+
return "Abort."
|
|
282
|
+
|
|
283
|
+
netpeering_request = _create_netpeering_request(netpeering_request_name, source, target, user, group)
|
|
284
|
+
netpeering_id = netpeering_request.get('status').get('netPeeringId')
|
|
285
|
+
|
|
286
|
+
_create_netpeering_acceptance(netpeering_acceptance_name, netpeering_request.get('status'), target, user, group)
|
|
287
|
+
|
|
288
|
+
# Wait a bit for NetPeering to appear
|
|
289
|
+
time.sleep(3)
|
|
290
|
+
|
|
291
|
+
_run_kubectl(target.get('project_id'), target.get('cluster_id'), user, group,
|
|
292
|
+
['get', 'netpeering', '-o', output, netpeering_id,])
|
|
293
|
+
|
|
@@ -18,7 +18,7 @@ def profile():
|
|
|
18
18
|
@click.option('--password', required=False, help="Password", type=click.STRING)
|
|
19
19
|
@click.option('--region', required=True, help="Region name", type=click.Choice(['eu-west-2', 'cloudgouv-eu-west-1']))
|
|
20
20
|
@click.option('--endpoint', required=False, help="API endpoint", type=click.STRING)
|
|
21
|
-
@click.option('--jwt', help="Enable JWT, by default is false")
|
|
21
|
+
@click.option('--jwt', help="Enable JWT, by default is false", type=click.BOOL)
|
|
22
22
|
def add_profile(profile_name, access_key, secret_key, username, password, region, endpoint, jwt):
|
|
23
23
|
"""Add a new profile with AK/SK or username/password authentication."""
|
|
24
24
|
if not profile_name:
|
|
@@ -10,7 +10,7 @@ from prettytable import TableStyle
|
|
|
10
10
|
from .utils import do_request, print_output, print_table, find_project_id_by_name, get_project_id, set_project_id, \
|
|
11
11
|
detect_and_parse_input, transform_tuple, ctx_update, set_cluster_id, get_template, get_project_name, \
|
|
12
12
|
format_changed_row, is_interesting_status, login_profile, profile_completer, project_completer, \
|
|
13
|
-
format_row
|
|
13
|
+
format_row, apply_set_fields
|
|
14
14
|
|
|
15
15
|
# DEIFNE THE PROJECT COMMAND GROUP
|
|
16
16
|
@click.group(help="Project related commands.")
|
|
@@ -63,7 +63,7 @@ def project_logout(ctx, profile):
|
|
|
63
63
|
# LIST PROJECTS
|
|
64
64
|
@project.command('list', help="List all projects")
|
|
65
65
|
@click.option('--project-name', '-p', help="Name of project", type=click.STRING, shell_complete=project_completer)
|
|
66
|
-
@click.option('--deleted', '-x', is_flag=True,
|
|
66
|
+
@click.option('--deleted', '-x', is_flag=True, deprecated="List deleted projects - Will be removed")
|
|
67
67
|
@click.option('--plain', is_flag=True, help="Plain table format")
|
|
68
68
|
@click.option('--msword', is_flag=True, help="Microsoft Word table format")
|
|
69
69
|
@click.option('--uuid', is_flag=True, help="Show UUID")
|
|
@@ -106,7 +106,7 @@ def project_list(ctx, project_name, deleted, plain, msword, uuid, watch, output,
|
|
|
106
106
|
table.set_style(TableStyle.PLAIN_COLUMNS)
|
|
107
107
|
|
|
108
108
|
if msword:
|
|
109
|
-
table.set_style(
|
|
109
|
+
table.set_style(TableStyle.MSWORD_FRIENDLY)
|
|
110
110
|
|
|
111
111
|
initial_projects = {}
|
|
112
112
|
|
|
@@ -188,8 +188,9 @@ def project_list(ctx, project_name, deleted, plain, msword, uuid, watch, output,
|
|
|
188
188
|
@click.option('--output', '-o', type=click.Choice(["json", "yaml", "silent"]), help="Specify output format, by default is json")
|
|
189
189
|
@click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to create the project")
|
|
190
190
|
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
191
|
+
@click.option('--set', 'set_fields', multiple=True, help="Set arbitrary nested fields, e.g. auth.oidc.issuer-url=value")
|
|
191
192
|
@click.pass_context
|
|
192
|
-
def project_create(ctx, project_name, description, cidr, quirk, tags, disable_api_termination, dry_run, output, filename, profile):
|
|
193
|
+
def project_create(ctx, project_name, description, cidr, quirk, tags, disable_api_termination, dry_run, output, filename, profile, set_fields):
|
|
193
194
|
"""Create a new project from options or file, with support for dry-run and output formatting."""
|
|
194
195
|
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
|
|
195
196
|
login_profile(profile)
|
|
@@ -230,6 +231,8 @@ def project_create(ctx, project_name, description, cidr, quirk, tags, disable_ap
|
|
|
230
231
|
|
|
231
232
|
if disable_api_termination is not None:
|
|
232
233
|
project_config["disable_api_termination"] = disable_api_termination
|
|
234
|
+
|
|
235
|
+
apply_set_fields(project_config, set_fields)
|
|
233
236
|
|
|
234
237
|
if not dry_run:
|
|
235
238
|
data = do_request("POST", 'projects', json=project_config)
|
|
@@ -295,8 +298,9 @@ def project_delete_command(ctx, project_name, output, dry_run, force, profile):
|
|
|
295
298
|
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
296
299
|
@click.option('--dry-run', is_flag=True, help="Run without any action")
|
|
297
300
|
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
301
|
+
@click.option('--set', 'set_fields', multiple=True, help="Set arbitrary nested fields, e.g. auth.oidc.issuer-url=value")
|
|
298
302
|
@click.pass_context
|
|
299
|
-
def project_update_command(ctx, project_name, description, quirk, tags, disable_api_termination, output, dry_run, profile):
|
|
303
|
+
def project_update_command(ctx, project_name, description, quirk, tags, disable_api_termination, output, dry_run, profile, set_fields):
|
|
300
304
|
"""Update project details by name, supporting dry-run and output formatting."""
|
|
301
305
|
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
|
|
302
306
|
login_profile(profile)
|
|
@@ -326,6 +330,8 @@ def project_update_command(ctx, project_name, description, quirk, tags, disable_
|
|
|
326
330
|
parsed_tags[key.strip()] = value.strip()
|
|
327
331
|
|
|
328
332
|
project_config['tags'] = parsed_tags
|
|
333
|
+
|
|
334
|
+
apply_set_fields(project_config, set_fields)
|
|
329
335
|
|
|
330
336
|
if dry_run:
|
|
331
337
|
print_output(project_config, output)
|
|
@@ -391,4 +397,22 @@ def project_get_public_ips(ctx, project_name, output, profile):
|
|
|
391
397
|
project_id = find_project_id_by_name(project_name)
|
|
392
398
|
|
|
393
399
|
data = do_request("GET", f'projects/{project_id}/public_ips')
|
|
400
|
+
print_output(data, output)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# GET NETS BY PROJECT NAME
|
|
405
|
+
@project.command('nets', help="Get project nets")
|
|
406
|
+
@click.option('--project-name', '-p', help="Name of the project", shell_complete=project_completer)
|
|
407
|
+
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
408
|
+
@click.option('--profile',help="Configuration profile to use")
|
|
409
|
+
@click.pass_context
|
|
410
|
+
def project_get_public_ips(ctx, project_name, output, profile):
|
|
411
|
+
"""Retrieve the list of Nets associated with the specified project."""
|
|
412
|
+
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
|
|
413
|
+
login_profile(profile)
|
|
414
|
+
|
|
415
|
+
project_id = find_project_id_by_name(project_name)
|
|
416
|
+
|
|
417
|
+
data = do_request("GET", f'projects/{project_id}/nets')
|
|
394
418
|
print_output(data, output)
|