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.
Files changed (28) hide show
  1. {oks_cli-1.17 → oks_cli-1.19}/PKG-INFO +2 -1
  2. {oks_cli-1.17 → oks_cli-1.19}/README.md +1 -0
  3. {oks_cli-1.17 → oks_cli-1.19}/oks_cli/cluster.py +35 -13
  4. {oks_cli-1.17 → oks_cli-1.19}/oks_cli/main.py +2 -0
  5. oks_cli-1.19/oks_cli/netpeering.py +293 -0
  6. {oks_cli-1.17 → oks_cli-1.19}/oks_cli/profile.py +1 -1
  7. {oks_cli-1.17 → oks_cli-1.19}/oks_cli/project.py +29 -5
  8. {oks_cli-1.17 → oks_cli-1.19}/oks_cli/utils.py +132 -19
  9. {oks_cli-1.17 → oks_cli-1.19}/oks_cli.egg-info/PKG-INFO +2 -1
  10. {oks_cli-1.17 → oks_cli-1.19}/oks_cli.egg-info/SOURCES.txt +2 -0
  11. {oks_cli-1.17 → oks_cli-1.19}/setup.py +1 -1
  12. {oks_cli-1.17 → oks_cli-1.19}/tests/test_cluster.py +1 -1
  13. oks_cli-1.19/tests/test_netpeering.py +899 -0
  14. {oks_cli-1.17 → oks_cli-1.19}/tests/test_nodepool.py +1 -1
  15. {oks_cli-1.17 → oks_cli-1.19}/tests/test_profile.py +27 -1
  16. {oks_cli-1.17 → oks_cli-1.19}/tests/test_project.py +74 -0
  17. {oks_cli-1.17 → oks_cli-1.19}/LICENSE +0 -0
  18. {oks_cli-1.17 → oks_cli-1.19}/oks_cli/__init__.py +0 -0
  19. {oks_cli-1.17 → oks_cli-1.19}/oks_cli/cache.py +0 -0
  20. {oks_cli-1.17 → oks_cli-1.19}/oks_cli/quotas.py +0 -0
  21. {oks_cli-1.17 → oks_cli-1.19}/oks_cli.egg-info/dependency_links.txt +0 -0
  22. {oks_cli-1.17 → oks_cli-1.19}/oks_cli.egg-info/entry_points.txt +0 -0
  23. {oks_cli-1.17 → oks_cli-1.19}/oks_cli.egg-info/requires.txt +0 -0
  24. {oks_cli-1.17 → oks_cli-1.19}/oks_cli.egg-info/top_level.txt +0 -0
  25. {oks_cli-1.17 → oks_cli-1.19}/setup.cfg +0 -0
  26. {oks_cli-1.17 → oks_cli-1.19}/tests/test_cache.py +0 -0
  27. {oks_cli-1.17 → oks_cli-1.19}/tests/test_quota.py +0 -0
  28. {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.17
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
@@ -180,6 +180,7 @@ oks-cli/
180
180
  │ ├── cache.py
181
181
  │ ├── cluster.py
182
182
  │ ├── main.py
183
+ │ ├── netpeering.py
183
184
  │ ├── profile.py
184
185
  │ ├── project.py
185
186
  │ ├── 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, help="List deleted clusters") # x pour "deleted" / "removed"
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(prettytable.MSWORD_FRIENDLY)
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 is not None:
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 = env)
742
+ return subprocess.run(cmd, env=env, capture_output=capture)
734
743
  else:
735
- return subprocess.run(cmd, input=input, text=True, env = 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
- _run_kubectl(ctx.obj['project_id'], ctx.obj['cluster_id'], ctx.obj['user'], ctx.obj['group'], [
826
- 'delete', 'nodepool', nodepool_name])
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, help="List deleted projects")
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(prettytable.MSWORD_FRIENDLY)
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)