oks-cli 1.18__tar.gz → 1.20__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.18 → oks_cli-1.20}/PKG-INFO +3 -2
- {oks_cli-1.18 → oks_cli-1.20}/README.md +1 -0
- {oks_cli-1.18 → oks_cli-1.20}/oks_cli/cluster.py +39 -13
- {oks_cli-1.18 → oks_cli-1.20}/oks_cli/main.py +2 -0
- oks_cli-1.20/oks_cli/netpeering.py +293 -0
- {oks_cli-1.18 → oks_cli-1.20}/oks_cli/project.py +27 -7
- {oks_cli-1.18 → oks_cli-1.20}/oks_cli/utils.py +112 -17
- {oks_cli-1.18 → oks_cli-1.20}/oks_cli.egg-info/PKG-INFO +3 -2
- {oks_cli-1.18 → oks_cli-1.20}/oks_cli.egg-info/SOURCES.txt +2 -0
- {oks_cli-1.18 → oks_cli-1.20}/oks_cli.egg-info/requires.txt +1 -1
- {oks_cli-1.18 → oks_cli-1.20}/setup.py +2 -2
- {oks_cli-1.18 → oks_cli-1.20}/tests/test_cluster.py +1 -1
- oks_cli-1.20/tests/test_netpeering.py +899 -0
- {oks_cli-1.18 → oks_cli-1.20}/tests/test_nodepool.py +1 -1
- {oks_cli-1.18 → oks_cli-1.20}/tests/test_shell_completion.py +2 -2
- {oks_cli-1.18 → oks_cli-1.20}/LICENSE +0 -0
- {oks_cli-1.18 → oks_cli-1.20}/oks_cli/__init__.py +0 -0
- {oks_cli-1.18 → oks_cli-1.20}/oks_cli/cache.py +0 -0
- {oks_cli-1.18 → oks_cli-1.20}/oks_cli/profile.py +0 -0
- {oks_cli-1.18 → oks_cli-1.20}/oks_cli/quotas.py +0 -0
- {oks_cli-1.18 → oks_cli-1.20}/oks_cli.egg-info/dependency_links.txt +0 -0
- {oks_cli-1.18 → oks_cli-1.20}/oks_cli.egg-info/entry_points.txt +0 -0
- {oks_cli-1.18 → oks_cli-1.20}/oks_cli.egg-info/top_level.txt +0 -0
- {oks_cli-1.18 → oks_cli-1.20}/setup.cfg +0 -0
- {oks_cli-1.18 → oks_cli-1.20}/tests/test_cache.py +0 -0
- {oks_cli-1.18 → oks_cli-1.20}/tests/test_profile.py +0 -0
- {oks_cli-1.18 → oks_cli-1.20}/tests/test_project.py +0 -0
- {oks_cli-1.18 → oks_cli-1.20}/tests/test_quota.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oks-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.20
|
|
4
4
|
Author: Outscale SAS
|
|
5
5
|
Author-email: opensource@outscale.com
|
|
6
6
|
License: BSD
|
|
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: certifi>=2024.8.30
|
|
19
19
|
Requires-Dist: charset-normalizer>=3.3.2
|
|
20
|
-
Requires-Dist: click<8.3.0,>=8.
|
|
20
|
+
Requires-Dist: click<8.3.0,>=8.2.0
|
|
21
21
|
Requires-Dist: colorama>=0.4.6
|
|
22
22
|
Requires-Dist: idna>=3.10
|
|
23
23
|
Requires-Dist: pyyaml>=6.0.2
|
|
@@ -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
|
|
@@ -74,6 +74,13 @@ def cluster_logout(ctx, profile):
|
|
|
74
74
|
"""Clear the current default cluster selection."""
|
|
75
75
|
_, _, profile = ctx_update(ctx, None, None, profile)
|
|
76
76
|
login_profile(profile)
|
|
77
|
+
|
|
78
|
+
current_cluster = get_cluster_id()
|
|
79
|
+
|
|
80
|
+
if not current_cluster:
|
|
81
|
+
click.echo("You are not connected to any cluster.")
|
|
82
|
+
return
|
|
83
|
+
|
|
77
84
|
set_cluster_id("")
|
|
78
85
|
click.echo("Logged out from the current cluster")
|
|
79
86
|
|
|
@@ -81,7 +88,7 @@ def cluster_logout(ctx, profile):
|
|
|
81
88
|
@cluster.command('list', help="List all clusters")
|
|
82
89
|
@click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
|
|
83
90
|
@click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
|
|
84
|
-
@click.option('--deleted', '-x', is_flag=True,
|
|
91
|
+
@click.option('--deleted', '-x', is_flag=True, deprecated="List deleted clusters - Will be removed") # x pour "deleted" / "removed"
|
|
85
92
|
@click.option('--plain', is_flag=True, help="Plain table format")
|
|
86
93
|
@click.option('--msword', is_flag=True, help="Microsoft Word table format")
|
|
87
94
|
@click.option('--watch', '-w', is_flag=True, help="Watch the changes")
|
|
@@ -140,7 +147,7 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
|
|
|
140
147
|
table.set_style(TableStyle.PLAIN_COLUMNS)
|
|
141
148
|
|
|
142
149
|
if msword:
|
|
143
|
-
table.set_style(
|
|
150
|
+
table.set_style(TableStyle.MSWORD_FRIENDLY)
|
|
144
151
|
|
|
145
152
|
initial_clusters = {}
|
|
146
153
|
|
|
@@ -371,8 +378,9 @@ def _create_cluster(project_name, cluster_config, output):
|
|
|
371
378
|
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
372
379
|
@click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to create the cluster ")
|
|
373
380
|
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
381
|
+
@click.option('--set', 'set_fields', multiple=True, help="Set arbitrary nested fields, e.g. auth.oidc.issuer-url=value")
|
|
374
382
|
@click.pass_context
|
|
375
|
-
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):
|
|
383
|
+
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):
|
|
376
384
|
"""CLI command to create a new Kubernetes cluster with optional configuration parameters."""
|
|
377
385
|
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
378
386
|
login_profile(profile)
|
|
@@ -440,9 +448,11 @@ def cluster_create_command(ctx, project_name, cluster_name, description, admin,
|
|
|
440
448
|
if disable_api_termination is not None:
|
|
441
449
|
cluster_config["disable_api_termination"] = disable_api_termination
|
|
442
450
|
|
|
443
|
-
if cp_multi_az
|
|
451
|
+
if cp_multi_az:
|
|
444
452
|
cluster_config["cp_multi_az"] = cp_multi_az
|
|
445
453
|
|
|
454
|
+
apply_set_fields(cluster_config, set_fields)
|
|
455
|
+
|
|
446
456
|
if not dry_run:
|
|
447
457
|
_create_cluster(project_name, cluster_config, output)
|
|
448
458
|
else:
|
|
@@ -466,8 +476,9 @@ def cluster_create_command(ctx, project_name, cluster_name, description, admin,
|
|
|
466
476
|
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
467
477
|
@click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to update the cluster ")
|
|
468
478
|
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
479
|
+
@click.option('--set', 'set_fields', multiple=True, help="Set arbitrary nested fields, e.g. auth.oidc.issuer-url=value")
|
|
469
480
|
@click.pass_context
|
|
470
|
-
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):
|
|
481
|
+
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):
|
|
471
482
|
"""CLI command to update an existing Kubernetes cluster with new configuration options."""
|
|
472
483
|
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
473
484
|
login_profile(profile)
|
|
@@ -504,7 +515,7 @@ def cluster_update_command(ctx, project_name, cluster_name, description, admin,
|
|
|
504
515
|
raise click.ClickException(f"Unable to resolve 'my-ip': {e}")
|
|
505
516
|
else:
|
|
506
517
|
resolved_ips.append(ip)
|
|
507
|
-
cluster_config['admin_whitelist'] = resolved_ips
|
|
518
|
+
cluster_config['admin_whitelist'] = list(dict.fromkeys(resolved_ips))
|
|
508
519
|
|
|
509
520
|
if version is not None:
|
|
510
521
|
cluster_config['version'] = version
|
|
@@ -545,6 +556,8 @@ def cluster_update_command(ctx, project_name, cluster_name, description, admin,
|
|
|
545
556
|
|
|
546
557
|
if control_plane:
|
|
547
558
|
cluster_config['control_planes'] = control_plane
|
|
559
|
+
|
|
560
|
+
apply_set_fields(cluster_config, set_fields)
|
|
548
561
|
|
|
549
562
|
if dry_run:
|
|
550
563
|
print_output(cluster_config, output)
|
|
@@ -700,7 +713,7 @@ def cluster_kubeconfig_command(ctx, project_name, cluster_name, print_path, outp
|
|
|
700
713
|
click.echo(kubeconfig)
|
|
701
714
|
|
|
702
715
|
|
|
703
|
-
def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
|
|
716
|
+
def _run_kubectl(project_id, cluster_id, user, group, args, input=None, capture=False):
|
|
704
717
|
"""Run a kubectl command using the cached kubeconfig for the specified cluster, refreshing it if needed."""
|
|
705
718
|
# @TODO: check expiration in get_cache() code, etc
|
|
706
719
|
kubeconfig_path = get_cache(project_id, cluster_id, 'kubeconfig', user, group)
|
|
@@ -733,9 +746,9 @@ def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
|
|
|
733
746
|
cmd += list(args)
|
|
734
747
|
logging.info("running %s", cmd)
|
|
735
748
|
if not input:
|
|
736
|
-
return subprocess.run(cmd, env =
|
|
749
|
+
return subprocess.run(cmd, env=env, capture_output=capture)
|
|
737
750
|
else:
|
|
738
|
-
return subprocess.run(cmd, input=input, text=True, env =
|
|
751
|
+
return subprocess.run(cmd, input=input, text=True, env=env, capture_output=capture)
|
|
739
752
|
|
|
740
753
|
|
|
741
754
|
@cluster.command('kubectl', help='Fetch the kubeconfig for a cluster and run kubectl against it', context_settings={"ignore_unknown_options": True})
|
|
@@ -822,8 +835,21 @@ def setup_worker_pool(ctx, nodepool_name, count, vmtype, zone, output, dry_run,
|
|
|
822
835
|
|
|
823
836
|
@nodepool.command('delete')
|
|
824
837
|
@click.option('--nodepool-name', '-n', required=True, help="Nodepool Name")
|
|
838
|
+
@click.option('--force', is_flag=True, help="Delete without confirmation")
|
|
825
839
|
@click.pass_context
|
|
826
|
-
def delete_worker_pool(ctx, nodepool_name):
|
|
840
|
+
def delete_worker_pool(ctx, nodepool_name, force):
|
|
827
841
|
"""Delete a nodepool by name from the cluster."""
|
|
828
|
-
|
|
829
|
-
|
|
842
|
+
|
|
843
|
+
if not force:
|
|
844
|
+
click.confirm(
|
|
845
|
+
f"Are you sure you want to delete the nodepool '{nodepool_name}'?",
|
|
846
|
+
abort=True
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
_run_kubectl(
|
|
850
|
+
ctx.obj['project_id'],
|
|
851
|
+
ctx.obj['cluster_id'],
|
|
852
|
+
ctx.obj['user'],
|
|
853
|
+
ctx.obj['group'],
|
|
854
|
+
['delete', 'nodepool', nodepool_name]
|
|
855
|
+
)
|
|
@@ -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
|
+
|
|
@@ -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.")
|
|
@@ -56,6 +56,13 @@ def project_logout(ctx, profile):
|
|
|
56
56
|
"""Unset the current default project and log out."""
|
|
57
57
|
_, _, profile = ctx_update(ctx, None, None, profile)
|
|
58
58
|
login_profile(profile)
|
|
59
|
+
|
|
60
|
+
current_project = get_project_id()
|
|
61
|
+
|
|
62
|
+
if not current_project:
|
|
63
|
+
click.echo("You are not connected to any project.")
|
|
64
|
+
return
|
|
65
|
+
|
|
59
66
|
set_project_id("")
|
|
60
67
|
set_cluster_id("")
|
|
61
68
|
click.echo("Logged out from the current project")
|
|
@@ -63,7 +70,7 @@ def project_logout(ctx, profile):
|
|
|
63
70
|
# LIST PROJECTS
|
|
64
71
|
@project.command('list', help="List all projects")
|
|
65
72
|
@click.option('--project-name', '-p', help="Name of project", type=click.STRING, shell_complete=project_completer)
|
|
66
|
-
@click.option('--deleted', '-x', is_flag=True,
|
|
73
|
+
@click.option('--deleted', '-x', is_flag=True, deprecated="List deleted projects - Will be removed")
|
|
67
74
|
@click.option('--plain', is_flag=True, help="Plain table format")
|
|
68
75
|
@click.option('--msword', is_flag=True, help="Microsoft Word table format")
|
|
69
76
|
@click.option('--uuid', is_flag=True, help="Show UUID")
|
|
@@ -106,7 +113,7 @@ def project_list(ctx, project_name, deleted, plain, msword, uuid, watch, output,
|
|
|
106
113
|
table.set_style(TableStyle.PLAIN_COLUMNS)
|
|
107
114
|
|
|
108
115
|
if msword:
|
|
109
|
-
table.set_style(
|
|
116
|
+
table.set_style(TableStyle.MSWORD_FRIENDLY)
|
|
110
117
|
|
|
111
118
|
initial_projects = {}
|
|
112
119
|
|
|
@@ -188,8 +195,9 @@ def project_list(ctx, project_name, deleted, plain, msword, uuid, watch, output,
|
|
|
188
195
|
@click.option('--output', '-o', type=click.Choice(["json", "yaml", "silent"]), help="Specify output format, by default is json")
|
|
189
196
|
@click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to create the project")
|
|
190
197
|
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
198
|
+
@click.option('--set', 'set_fields', multiple=True, help="Set arbitrary nested fields, e.g. auth.oidc.issuer-url=value")
|
|
191
199
|
@click.pass_context
|
|
192
|
-
def project_create(ctx, project_name, description, cidr, quirk, tags, disable_api_termination, dry_run, output, filename, profile):
|
|
200
|
+
def project_create(ctx, project_name, description, cidr, quirk, tags, disable_api_termination, dry_run, output, filename, profile, set_fields):
|
|
193
201
|
"""Create a new project from options or file, with support for dry-run and output formatting."""
|
|
194
202
|
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
|
|
195
203
|
login_profile(profile)
|
|
@@ -230,6 +238,8 @@ def project_create(ctx, project_name, description, cidr, quirk, tags, disable_ap
|
|
|
230
238
|
|
|
231
239
|
if disable_api_termination is not None:
|
|
232
240
|
project_config["disable_api_termination"] = disable_api_termination
|
|
241
|
+
|
|
242
|
+
apply_set_fields(project_config, set_fields)
|
|
233
243
|
|
|
234
244
|
if not dry_run:
|
|
235
245
|
data = do_request("POST", 'projects', json=project_config)
|
|
@@ -295,8 +305,9 @@ def project_delete_command(ctx, project_name, output, dry_run, force, profile):
|
|
|
295
305
|
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
296
306
|
@click.option('--dry-run', is_flag=True, help="Run without any action")
|
|
297
307
|
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
308
|
+
@click.option('--set', 'set_fields', multiple=True, help="Set arbitrary nested fields, e.g. auth.oidc.issuer-url=value")
|
|
298
309
|
@click.pass_context
|
|
299
|
-
def project_update_command(ctx, project_name, description, quirk, tags, disable_api_termination, output, dry_run, profile):
|
|
310
|
+
def project_update_command(ctx, project_name, description, quirk, tags, disable_api_termination, output, dry_run, profile, set_fields):
|
|
300
311
|
"""Update project details by name, supporting dry-run and output formatting."""
|
|
301
312
|
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
|
|
302
313
|
login_profile(profile)
|
|
@@ -326,6 +337,8 @@ def project_update_command(ctx, project_name, description, quirk, tags, disable_
|
|
|
326
337
|
parsed_tags[key.strip()] = value.strip()
|
|
327
338
|
|
|
328
339
|
project_config['tags'] = parsed_tags
|
|
340
|
+
|
|
341
|
+
apply_set_fields(project_config, set_fields)
|
|
329
342
|
|
|
330
343
|
if dry_run:
|
|
331
344
|
print_output(project_config, output)
|
|
@@ -398,7 +411,7 @@ def project_get_public_ips(ctx, project_name, output, profile):
|
|
|
398
411
|
# GET NETS BY PROJECT NAME
|
|
399
412
|
@project.command('nets', help="Get project nets")
|
|
400
413
|
@click.option('--project-name', '-p', help="Name of the project", shell_complete=project_completer)
|
|
401
|
-
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
414
|
+
@click.option('--output', '-o', type=click.Choice(["json", "yaml", "table"]), help="Specify output format, by default is json")
|
|
402
415
|
@click.option('--profile',help="Configuration profile to use")
|
|
403
416
|
@click.pass_context
|
|
404
417
|
def project_get_public_ips(ctx, project_name, output, profile):
|
|
@@ -409,4 +422,11 @@ def project_get_public_ips(ctx, project_name, output, profile):
|
|
|
409
422
|
project_id = find_project_id_by_name(project_name)
|
|
410
423
|
|
|
411
424
|
data = do_request("GET", f'projects/{project_id}/nets')
|
|
412
|
-
|
|
425
|
+
if output == "table":
|
|
426
|
+
print_table(data, [["DHCP options set id", "DhcpOptionsSetId"],
|
|
427
|
+
["Ip range", "IpRange"],
|
|
428
|
+
["Net id", "NetId"],
|
|
429
|
+
["State", "State"],
|
|
430
|
+
["Tenancy", "Tenancy"]])
|
|
431
|
+
else:
|
|
432
|
+
print_output(data, output)
|