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.
Files changed (28) hide show
  1. {oks_cli-1.18 → oks_cli-1.19}/PKG-INFO +2 -1
  2. {oks_cli-1.18 → oks_cli-1.19}/README.md +1 -0
  3. {oks_cli-1.18 → oks_cli-1.19}/oks_cli/cluster.py +32 -13
  4. {oks_cli-1.18 → 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.18 → oks_cli-1.19}/oks_cli/project.py +11 -5
  7. {oks_cli-1.18 → oks_cli-1.19}/oks_cli/utils.py +113 -15
  8. {oks_cli-1.18 → oks_cli-1.19}/oks_cli.egg-info/PKG-INFO +2 -1
  9. {oks_cli-1.18 → oks_cli-1.19}/oks_cli.egg-info/SOURCES.txt +2 -0
  10. {oks_cli-1.18 → oks_cli-1.19}/setup.py +1 -1
  11. {oks_cli-1.18 → oks_cli-1.19}/tests/test_cluster.py +1 -1
  12. oks_cli-1.19/tests/test_netpeering.py +899 -0
  13. {oks_cli-1.18 → oks_cli-1.19}/tests/test_nodepool.py +1 -1
  14. {oks_cli-1.18 → oks_cli-1.19}/LICENSE +0 -0
  15. {oks_cli-1.18 → oks_cli-1.19}/oks_cli/__init__.py +0 -0
  16. {oks_cli-1.18 → oks_cli-1.19}/oks_cli/cache.py +0 -0
  17. {oks_cli-1.18 → oks_cli-1.19}/oks_cli/profile.py +0 -0
  18. {oks_cli-1.18 → oks_cli-1.19}/oks_cli/quotas.py +0 -0
  19. {oks_cli-1.18 → oks_cli-1.19}/oks_cli.egg-info/dependency_links.txt +0 -0
  20. {oks_cli-1.18 → oks_cli-1.19}/oks_cli.egg-info/entry_points.txt +0 -0
  21. {oks_cli-1.18 → oks_cli-1.19}/oks_cli.egg-info/requires.txt +0 -0
  22. {oks_cli-1.18 → oks_cli-1.19}/oks_cli.egg-info/top_level.txt +0 -0
  23. {oks_cli-1.18 → oks_cli-1.19}/setup.cfg +0 -0
  24. {oks_cli-1.18 → oks_cli-1.19}/tests/test_cache.py +0 -0
  25. {oks_cli-1.18 → oks_cli-1.19}/tests/test_profile.py +0 -0
  26. {oks_cli-1.18 → oks_cli-1.19}/tests/test_project.py +0 -0
  27. {oks_cli-1.18 → oks_cli-1.19}/tests/test_quota.py +0 -0
  28. {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.18
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
 
@@ -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 is not None:
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 = env)
742
+ return subprocess.run(cmd, env=env, capture_output=capture)
737
743
  else:
738
- return subprocess.run(cmd, input=input, text=True, env = 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
- _run_kubectl(ctx.obj['project_id'], ctx.obj['cluster_id'], ctx.obj['user'], ctx.obj['group'], [
829
- '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
+
@@ -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)