oks-cli 1.18__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.18 → oks_cli-1.19}/PKG-INFO +2 -1
- {oks_cli-1.18 → oks_cli-1.19}/README.md +1 -0
- {oks_cli-1.18 → oks_cli-1.19}/oks_cli/cluster.py +32 -13
- {oks_cli-1.18 → oks_cli-1.19}/oks_cli/main.py +2 -0
- oks_cli-1.19/oks_cli/netpeering.py +293 -0
- {oks_cli-1.18 → oks_cli-1.19}/oks_cli/project.py +11 -5
- {oks_cli-1.18 → oks_cli-1.19}/oks_cli/utils.py +113 -15
- {oks_cli-1.18 → oks_cli-1.19}/oks_cli.egg-info/PKG-INFO +2 -1
- {oks_cli-1.18 → oks_cli-1.19}/oks_cli.egg-info/SOURCES.txt +2 -0
- {oks_cli-1.18 → oks_cli-1.19}/setup.py +1 -1
- {oks_cli-1.18 → oks_cli-1.19}/tests/test_cluster.py +1 -1
- oks_cli-1.19/tests/test_netpeering.py +899 -0
- {oks_cli-1.18 → oks_cli-1.19}/tests/test_nodepool.py +1 -1
- {oks_cli-1.18 → oks_cli-1.19}/LICENSE +0 -0
- {oks_cli-1.18 → oks_cli-1.19}/oks_cli/__init__.py +0 -0
- {oks_cli-1.18 → oks_cli-1.19}/oks_cli/cache.py +0 -0
- {oks_cli-1.18 → oks_cli-1.19}/oks_cli/profile.py +0 -0
- {oks_cli-1.18 → oks_cli-1.19}/oks_cli/quotas.py +0 -0
- {oks_cli-1.18 → oks_cli-1.19}/oks_cli.egg-info/dependency_links.txt +0 -0
- {oks_cli-1.18 → oks_cli-1.19}/oks_cli.egg-info/entry_points.txt +0 -0
- {oks_cli-1.18 → oks_cli-1.19}/oks_cli.egg-info/requires.txt +0 -0
- {oks_cli-1.18 → oks_cli-1.19}/oks_cli.egg-info/top_level.txt +0 -0
- {oks_cli-1.18 → oks_cli-1.19}/setup.cfg +0 -0
- {oks_cli-1.18 → oks_cli-1.19}/tests/test_cache.py +0 -0
- {oks_cli-1.18 → oks_cli-1.19}/tests/test_profile.py +0 -0
- {oks_cli-1.18 → oks_cli-1.19}/tests/test_project.py +0 -0
- {oks_cli-1.18 → oks_cli-1.19}/tests/test_quota.py +0 -0
- {oks_cli-1.18 → 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
|
|
|
@@ -371,8 +371,9 @@ def _create_cluster(project_name, cluster_config, output):
|
|
|
371
371
|
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
372
372
|
@click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to create the cluster ")
|
|
373
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")
|
|
374
375
|
@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):
|
|
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):
|
|
376
377
|
"""CLI command to create a new Kubernetes cluster with optional configuration parameters."""
|
|
377
378
|
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
378
379
|
login_profile(profile)
|
|
@@ -440,9 +441,11 @@ def cluster_create_command(ctx, project_name, cluster_name, description, admin,
|
|
|
440
441
|
if disable_api_termination is not None:
|
|
441
442
|
cluster_config["disable_api_termination"] = disable_api_termination
|
|
442
443
|
|
|
443
|
-
if cp_multi_az
|
|
444
|
+
if cp_multi_az:
|
|
444
445
|
cluster_config["cp_multi_az"] = cp_multi_az
|
|
445
446
|
|
|
447
|
+
apply_set_fields(cluster_config, set_fields)
|
|
448
|
+
|
|
446
449
|
if not dry_run:
|
|
447
450
|
_create_cluster(project_name, cluster_config, output)
|
|
448
451
|
else:
|
|
@@ -466,8 +469,9 @@ def cluster_create_command(ctx, project_name, cluster_name, description, admin,
|
|
|
466
469
|
@click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
467
470
|
@click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to update the cluster ")
|
|
468
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")
|
|
469
473
|
@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):
|
|
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):
|
|
471
475
|
"""CLI command to update an existing Kubernetes cluster with new configuration options."""
|
|
472
476
|
project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
|
|
473
477
|
login_profile(profile)
|
|
@@ -504,7 +508,7 @@ def cluster_update_command(ctx, project_name, cluster_name, description, admin,
|
|
|
504
508
|
raise click.ClickException(f"Unable to resolve 'my-ip': {e}")
|
|
505
509
|
else:
|
|
506
510
|
resolved_ips.append(ip)
|
|
507
|
-
cluster_config['admin_whitelist'] = resolved_ips
|
|
511
|
+
cluster_config['admin_whitelist'] = list(dict.fromkeys(resolved_ips))
|
|
508
512
|
|
|
509
513
|
if version is not None:
|
|
510
514
|
cluster_config['version'] = version
|
|
@@ -545,6 +549,8 @@ def cluster_update_command(ctx, project_name, cluster_name, description, admin,
|
|
|
545
549
|
|
|
546
550
|
if control_plane:
|
|
547
551
|
cluster_config['control_planes'] = control_plane
|
|
552
|
+
|
|
553
|
+
apply_set_fields(cluster_config, set_fields)
|
|
548
554
|
|
|
549
555
|
if dry_run:
|
|
550
556
|
print_output(cluster_config, output)
|
|
@@ -700,7 +706,7 @@ def cluster_kubeconfig_command(ctx, project_name, cluster_name, print_path, outp
|
|
|
700
706
|
click.echo(kubeconfig)
|
|
701
707
|
|
|
702
708
|
|
|
703
|
-
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):
|
|
704
710
|
"""Run a kubectl command using the cached kubeconfig for the specified cluster, refreshing it if needed."""
|
|
705
711
|
# @TODO: check expiration in get_cache() code, etc
|
|
706
712
|
kubeconfig_path = get_cache(project_id, cluster_id, 'kubeconfig', user, group)
|
|
@@ -733,9 +739,9 @@ def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
|
|
|
733
739
|
cmd += list(args)
|
|
734
740
|
logging.info("running %s", cmd)
|
|
735
741
|
if not input:
|
|
736
|
-
return subprocess.run(cmd, env =
|
|
742
|
+
return subprocess.run(cmd, env=env, capture_output=capture)
|
|
737
743
|
else:
|
|
738
|
-
return subprocess.run(cmd, input=input, text=True, env =
|
|
744
|
+
return subprocess.run(cmd, input=input, text=True, env=env, capture_output=capture)
|
|
739
745
|
|
|
740
746
|
|
|
741
747
|
@cluster.command('kubectl', help='Fetch the kubeconfig for a cluster and run kubectl against it', context_settings={"ignore_unknown_options": True})
|
|
@@ -822,8 +828,21 @@ def setup_worker_pool(ctx, nodepool_name, count, vmtype, zone, output, dry_run,
|
|
|
822
828
|
|
|
823
829
|
@nodepool.command('delete')
|
|
824
830
|
@click.option('--nodepool-name', '-n', required=True, help="Nodepool Name")
|
|
831
|
+
@click.option('--force', is_flag=True, help="Delete without confirmation")
|
|
825
832
|
@click.pass_context
|
|
826
|
-
def delete_worker_pool(ctx, nodepool_name):
|
|
833
|
+
def delete_worker_pool(ctx, nodepool_name, force):
|
|
827
834
|
"""Delete a nodepool by name from the cluster."""
|
|
828
|
-
|
|
829
|
-
|
|
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
|
+
|
|
@@ -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)
|