oks-cli 1.18__tar.gz → 1.20__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {oks_cli-1.18 → oks_cli-1.20}/PKG-INFO +3 -2
  2. {oks_cli-1.18 → oks_cli-1.20}/README.md +1 -0
  3. {oks_cli-1.18 → oks_cli-1.20}/oks_cli/cluster.py +39 -13
  4. {oks_cli-1.18 → oks_cli-1.20}/oks_cli/main.py +2 -0
  5. oks_cli-1.20/oks_cli/netpeering.py +293 -0
  6. {oks_cli-1.18 → oks_cli-1.20}/oks_cli/project.py +27 -7
  7. {oks_cli-1.18 → oks_cli-1.20}/oks_cli/utils.py +112 -17
  8. {oks_cli-1.18 → oks_cli-1.20}/oks_cli.egg-info/PKG-INFO +3 -2
  9. {oks_cli-1.18 → oks_cli-1.20}/oks_cli.egg-info/SOURCES.txt +2 -0
  10. {oks_cli-1.18 → oks_cli-1.20}/oks_cli.egg-info/requires.txt +1 -1
  11. {oks_cli-1.18 → oks_cli-1.20}/setup.py +2 -2
  12. {oks_cli-1.18 → oks_cli-1.20}/tests/test_cluster.py +1 -1
  13. oks_cli-1.20/tests/test_netpeering.py +899 -0
  14. {oks_cli-1.18 → oks_cli-1.20}/tests/test_nodepool.py +1 -1
  15. {oks_cli-1.18 → oks_cli-1.20}/tests/test_shell_completion.py +2 -2
  16. {oks_cli-1.18 → oks_cli-1.20}/LICENSE +0 -0
  17. {oks_cli-1.18 → oks_cli-1.20}/oks_cli/__init__.py +0 -0
  18. {oks_cli-1.18 → oks_cli-1.20}/oks_cli/cache.py +0 -0
  19. {oks_cli-1.18 → oks_cli-1.20}/oks_cli/profile.py +0 -0
  20. {oks_cli-1.18 → oks_cli-1.20}/oks_cli/quotas.py +0 -0
  21. {oks_cli-1.18 → oks_cli-1.20}/oks_cli.egg-info/dependency_links.txt +0 -0
  22. {oks_cli-1.18 → oks_cli-1.20}/oks_cli.egg-info/entry_points.txt +0 -0
  23. {oks_cli-1.18 → oks_cli-1.20}/oks_cli.egg-info/top_level.txt +0 -0
  24. {oks_cli-1.18 → oks_cli-1.20}/setup.cfg +0 -0
  25. {oks_cli-1.18 → oks_cli-1.20}/tests/test_cache.py +0 -0
  26. {oks_cli-1.18 → oks_cli-1.20}/tests/test_profile.py +0 -0
  27. {oks_cli-1.18 → oks_cli-1.20}/tests/test_project.py +0 -0
  28. {oks_cli-1.18 → oks_cli-1.20}/tests/test_quota.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oks-cli
3
- Version: 1.18
3
+ Version: 1.20
4
4
  Author: Outscale SAS
5
5
  Author-email: opensource@outscale.com
6
6
  License: BSD
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: certifi>=2024.8.30
19
19
  Requires-Dist: charset-normalizer>=3.3.2
20
- Requires-Dist: click<8.3.0,>=8.1.7
20
+ Requires-Dist: click<8.3.0,>=8.2.0
21
21
  Requires-Dist: colorama>=0.4.6
22
22
  Requires-Dist: idna>=3.10
23
23
  Requires-Dist: pyyaml>=6.0.2
@@ -223,6 +223,7 @@ oks-cli/
223
223
  │ ├── cache.py
224
224
  │ ├── cluster.py
225
225
  │ ├── main.py
226
+ │ ├── netpeering.py
226
227
  │ ├── profile.py
227
228
  │ ├── project.py
228
229
  │ ├── quotas.py
@@ -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
@@ -74,6 +74,13 @@ def cluster_logout(ctx, profile):
74
74
  """Clear the current default cluster selection."""
75
75
  _, _, profile = ctx_update(ctx, None, None, profile)
76
76
  login_profile(profile)
77
+
78
+ current_cluster = get_cluster_id()
79
+
80
+ if not current_cluster:
81
+ click.echo("You are not connected to any cluster.")
82
+ return
83
+
77
84
  set_cluster_id("")
78
85
  click.echo("Logged out from the current cluster")
79
86
 
@@ -81,7 +88,7 @@ def cluster_logout(ctx, profile):
81
88
  @cluster.command('list', help="List all clusters")
82
89
  @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
83
90
  @click.option('--cluster-name', '--name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
84
- @click.option('--deleted', '-x', is_flag=True, help="List deleted clusters") # x pour "deleted" / "removed"
91
+ @click.option('--deleted', '-x', is_flag=True, deprecated="List deleted clusters - Will be removed") # x pour "deleted" / "removed"
85
92
  @click.option('--plain', is_flag=True, help="Plain table format")
86
93
  @click.option('--msword', is_flag=True, help="Microsoft Word table format")
87
94
  @click.option('--watch', '-w', is_flag=True, help="Watch the changes")
@@ -140,7 +147,7 @@ def cluster_list(ctx, project_name, cluster_name, deleted, plain, msword, watch,
140
147
  table.set_style(TableStyle.PLAIN_COLUMNS)
141
148
 
142
149
  if msword:
143
- table.set_style(prettytable.MSWORD_FRIENDLY)
150
+ table.set_style(TableStyle.MSWORD_FRIENDLY)
144
151
 
145
152
  initial_clusters = {}
146
153
 
@@ -371,8 +378,9 @@ def _create_cluster(project_name, cluster_config, output):
371
378
  @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
372
379
  @click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to create the cluster ")
373
380
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
381
+ @click.option('--set', 'set_fields', multiple=True, help="Set arbitrary nested fields, e.g. auth.oidc.issuer-url=value")
374
382
  @click.pass_context
375
- def cluster_create_command(ctx, project_name, cluster_name, description, admin, version, cidr_pods, cidr_service, control_plane, zone, enable_admission_plugins, disable_admission_plugins, quirk, tags, disable_api_termination, cp_multi_az, dry_run, output, filename, profile):
383
+ def cluster_create_command(ctx, project_name, cluster_name, description, admin, version, cidr_pods, cidr_service, control_plane, zone, enable_admission_plugins, disable_admission_plugins, quirk, tags, disable_api_termination, cp_multi_az, dry_run, output, filename, profile, set_fields):
376
384
  """CLI command to create a new Kubernetes cluster with optional configuration parameters."""
377
385
  project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
378
386
  login_profile(profile)
@@ -440,9 +448,11 @@ def cluster_create_command(ctx, project_name, cluster_name, description, admin,
440
448
  if disable_api_termination is not None:
441
449
  cluster_config["disable_api_termination"] = disable_api_termination
442
450
 
443
- if cp_multi_az is not None:
451
+ if cp_multi_az:
444
452
  cluster_config["cp_multi_az"] = cp_multi_az
445
453
 
454
+ apply_set_fields(cluster_config, set_fields)
455
+
446
456
  if not dry_run:
447
457
  _create_cluster(project_name, cluster_config, output)
448
458
  else:
@@ -466,8 +476,9 @@ def cluster_create_command(ctx, project_name, cluster_name, description, admin,
466
476
  @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
467
477
  @click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to update the cluster ")
468
478
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
479
+ @click.option('--set', 'set_fields', multiple=True, help="Set arbitrary nested fields, e.g. auth.oidc.issuer-url=value")
469
480
  @click.pass_context
470
- def cluster_update_command(ctx, project_name, cluster_name, description, admin, version, tags, enable_admission_plugins, disable_admission_plugins, quirk, disable_api_termination, control_plane, dry_run, output, filename, profile):
481
+ def cluster_update_command(ctx, project_name, cluster_name, description, admin, version, tags, enable_admission_plugins, disable_admission_plugins, quirk, disable_api_termination, control_plane, dry_run, output, filename, profile, set_fields):
471
482
  """CLI command to update an existing Kubernetes cluster with new configuration options."""
472
483
  project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
473
484
  login_profile(profile)
@@ -504,7 +515,7 @@ def cluster_update_command(ctx, project_name, cluster_name, description, admin,
504
515
  raise click.ClickException(f"Unable to resolve 'my-ip': {e}")
505
516
  else:
506
517
  resolved_ips.append(ip)
507
- cluster_config['admin_whitelist'] = resolved_ips
518
+ cluster_config['admin_whitelist'] = list(dict.fromkeys(resolved_ips))
508
519
 
509
520
  if version is not None:
510
521
  cluster_config['version'] = version
@@ -545,6 +556,8 @@ def cluster_update_command(ctx, project_name, cluster_name, description, admin,
545
556
 
546
557
  if control_plane:
547
558
  cluster_config['control_planes'] = control_plane
559
+
560
+ apply_set_fields(cluster_config, set_fields)
548
561
 
549
562
  if dry_run:
550
563
  print_output(cluster_config, output)
@@ -700,7 +713,7 @@ def cluster_kubeconfig_command(ctx, project_name, cluster_name, print_path, outp
700
713
  click.echo(kubeconfig)
701
714
 
702
715
 
703
- def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
716
+ def _run_kubectl(project_id, cluster_id, user, group, args, input=None, capture=False):
704
717
  """Run a kubectl command using the cached kubeconfig for the specified cluster, refreshing it if needed."""
705
718
  # @TODO: check expiration in get_cache() code, etc
706
719
  kubeconfig_path = get_cache(project_id, cluster_id, 'kubeconfig', user, group)
@@ -733,9 +746,9 @@ def _run_kubectl(project_id, cluster_id, user, group, args, input=None):
733
746
  cmd += list(args)
734
747
  logging.info("running %s", cmd)
735
748
  if not input:
736
- return subprocess.run(cmd, env = env)
749
+ return subprocess.run(cmd, env=env, capture_output=capture)
737
750
  else:
738
- return subprocess.run(cmd, input=input, text=True, env = env)
751
+ return subprocess.run(cmd, input=input, text=True, env=env, capture_output=capture)
739
752
 
740
753
 
741
754
  @cluster.command('kubectl', help='Fetch the kubeconfig for a cluster and run kubectl against it', context_settings={"ignore_unknown_options": True})
@@ -822,8 +835,21 @@ def setup_worker_pool(ctx, nodepool_name, count, vmtype, zone, output, dry_run,
822
835
 
823
836
  @nodepool.command('delete')
824
837
  @click.option('--nodepool-name', '-n', required=True, help="Nodepool Name")
838
+ @click.option('--force', is_flag=True, help="Delete without confirmation")
825
839
  @click.pass_context
826
- def delete_worker_pool(ctx, nodepool_name):
840
+ def delete_worker_pool(ctx, nodepool_name, force):
827
841
  """Delete a nodepool by name from the cluster."""
828
- _run_kubectl(ctx.obj['project_id'], ctx.obj['cluster_id'], ctx.obj['user'], ctx.obj['group'], [
829
- 'delete', 'nodepool', nodepool_name])
842
+
843
+ if not force:
844
+ click.confirm(
845
+ f"Are you sure you want to delete the nodepool '{nodepool_name}'?",
846
+ abort=True
847
+ )
848
+
849
+ _run_kubectl(
850
+ ctx.obj['project_id'],
851
+ ctx.obj['cluster_id'],
852
+ ctx.obj['user'],
853
+ ctx.obj['group'],
854
+ ['delete', 'nodepool', nodepool_name]
855
+ )
@@ -8,6 +8,7 @@ from .cluster import cluster
8
8
  from .profile import profile
9
9
  from .cache import cache
10
10
  from .quotas import quotas
11
+ from .netpeering import netpeering
11
12
 
12
13
  from .utils import ctx_update, install_completions, profile_completer, cluster_completer, project_completer
13
14
 
@@ -60,6 +61,7 @@ cli.add_command(cluster)
60
61
  cli.add_command(profile)
61
62
  cli.add_command(cache)
62
63
  cli.add_command(quotas)
64
+ cli.add_command(netpeering)
63
65
 
64
66
  def recursive_help(cmd, parent=None):
65
67
  """Recursively prints help for all commands and subcommands."""
@@ -0,0 +1,293 @@
1
+ import click
2
+ import json
3
+ import yaml
4
+ import time
5
+ import ipaddress
6
+ import uuid
7
+ from subprocess import CalledProcessError
8
+
9
+ from .utils import cluster_completer, print_output, find_project_id_by_name, \
10
+ find_cluster_id_by_name, login_profile, ctx_update, \
11
+ profile_completer, project_completer, find_project_by_name, \
12
+ do_request, get_cluster_name, get_project_name, get_template
13
+ from .cluster import _run_kubectl
14
+
15
+ @click.group(help="NetPeering related commands.")
16
+ @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
17
+ @click.option('--cluster-name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
18
+ @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
19
+ @click.option('--user', type=click.STRING, help="User for the kubeconfig of the source cluster")
20
+ @click.option('--group', type=click.STRING, help="Group for the kubeconfig of the source cluster")
21
+ @click.pass_context
22
+ def netpeering(ctx, project_name, cluster_name, profile, user, group):
23
+ """Group of commands related to netpeering management"""
24
+ project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
25
+
26
+ ctx.obj['user'] = user
27
+ ctx.obj['group'] = group
28
+
29
+ @netpeering.command('list', help="List NetPeering from a project/cluster")
30
+ @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
31
+ @click.option('--cluster-name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
32
+ @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
33
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml", "wide"]), help="Specify output format")
34
+ @click.pass_context
35
+ def netpeering_list(ctx, project_name, cluster_name, profile, output):
36
+ """List netpeering in the specified cluster"""
37
+ project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
38
+ login_profile(profile)
39
+
40
+ project_id = find_project_id_by_name(project_name)
41
+ cluster_id = find_cluster_id_by_name(project_id, cluster_name)
42
+
43
+ cmd = ['get', 'netpeerings']
44
+ if output:
45
+ cmd.extend(['-o', output])
46
+
47
+ _run_kubectl(project_id, cluster_id, ctx.obj['user'], ctx.obj['group'], cmd)
48
+
49
+
50
+ @netpeering.command('get', help="Get information about a NetPeering")
51
+ @click.option('--project-name', '-p', required=False, help="Project Name", shell_complete=project_completer)
52
+ @click.option('--cluster-name', '-c', required=False, help="Cluster Name", shell_complete=cluster_completer)
53
+ @click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
54
+ @click.option('--netpeering-id', required=True, type=click.STRING, help="NetPeering to get information from")
55
+ @click.option('--output', '-o', default='json', required=False, type=click.Choice(["json", "yaml", "wide"]), help="Specify output format, default json")
56
+ @click.pass_context
57
+ def netpeering_get(ctx, project_name, cluster_name, profile, netpeering_id, output):
58
+ """Retrieve information about a NetPeering"""
59
+ project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
60
+ login_profile(profile)
61
+
62
+ project_id = find_project_id_by_name(project_name)
63
+ cluster_id = find_cluster_id_by_name(project_id, cluster_name)
64
+
65
+ _run_kubectl(project_id, cluster_id, ctx.obj['user'], ctx.obj['group'],
66
+ ['get', 'netpeering', netpeering_id, '-o', output])
67
+
68
+
69
+ @netpeering.command('delete', help="Delete a NetPeering from a project/cluster")
70
+ @click.option('--project-name', '-p', required=False, type=click.STRING, help="Source project name to create netpeering from", shell_complete=project_completer)
71
+ @click.option('--cluster-name', '-c', required=False, type=click.STRING, help="Source cluster to create netpeering from", shell_complete=cluster_completer)
72
+ @click.option('--netpeering-id', required=True, type=click.STRING, help="NetPeering to remove")
73
+ @click.option('--dry-run', required=False, is_flag=True, help="Run without any action")
74
+ @click.option('--force', is_flag=True, help="Force deletion without confirmation")
75
+ @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
76
+ @click.pass_context
77
+ def netpeering_delete(ctx, project_name, cluster_name, netpeering_id, dry_run, force, profile):
78
+ """Delete a NetPeering between 2 projects"""
79
+ project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
80
+ login_profile(profile)
81
+
82
+ project_id = find_project_id_by_name(project_name)
83
+ cluster_id = find_cluster_id_by_name(project_id, cluster_name)
84
+
85
+ if dry_run:
86
+ message = {"message": f"Dry run: The netpeering {netpeering_id} would be deleted."}
87
+ print_output(message, 'json')
88
+ return
89
+
90
+ if force or click.confirm(f"Are you sure you want to delete NetPeering with id {netpeering_id}?", abort=True):
91
+ try:
92
+ cmd = _run_kubectl(project_id, cluster_id, ctx.obj['user'], ctx.obj['group'],
93
+ ['delete', 'netpeering', netpeering_id], capture=True)
94
+ if cmd.returncode:
95
+ raise click.ClickException(f"Could not delete NetPeering {netpeering_id}: {cmd.stderr.decode('utf-8')}")
96
+ click.echo(f"It may take some times for NetPeering {netpeering_id} to automatically disappear from both projects. Please be patient")
97
+ except CalledProcessError as e:
98
+ raise click.ClickException(f"Could not delete NetPeering {netpeering_id}: {e}")
99
+
100
+
101
+ def _gather_info(project: str=None, cluster: str=None) -> dict:
102
+ """
103
+ Gather information about project and cluster required to create a NetPeering
104
+ Set:
105
+ - cluster_name
106
+ - project_name
107
+ - project_id
108
+ - cluster_id
109
+ - project_cidr
110
+ """
111
+ info = dict()
112
+
113
+ if not project or not cluster:
114
+ raise click.ClickException("Project and cluster name are required")
115
+
116
+ project_data = find_project_by_name(project)
117
+ info.update({'cluster_name': cluster, 'project_name': project})
118
+ info.update({'project_id': project_data.get('id'), 'project_cidr': project_data.get('cidr')})
119
+ info.update({'cluster_id': find_cluster_id_by_name(info.get('project_id'), info.get('cluster_name'))})
120
+ return info
121
+
122
+
123
+ def _netpeering_exists(source: dict=None, target: dict=None, user: str=None, group: str=None) -> bool:
124
+ """
125
+ Checks if an existing NetPeering already exists between similar project/cluster id
126
+ """
127
+ # We check if there's not already a NetPeering available and active
128
+ if not source or not target:
129
+ raise AttributeError("source and target must be passesd as dict")
130
+
131
+ try:
132
+ netpeerings = json.loads(_run_kubectl(source.get('project_id'), source.get('cluster_id'), user, group,
133
+ ['get', 'netpeering', '-o', 'json'],
134
+ capture=True).stdout.decode('utf-8'))
135
+
136
+ for item in netpeerings.get('items'):
137
+ status = item.get('status')
138
+ if status.get('accepterNetId') == target.get('network_id') and \
139
+ status.get('accepterOwnerId') == target.get('account_id') and \
140
+ status.get('sourceNetId') == source.get('network_id') and \
141
+ status.get('sourceOwnerId') == source.get('account_id') and \
142
+ status.get('netPeeringState') == 'active':
143
+
144
+ return True
145
+
146
+ except CalledProcessError as e:
147
+ raise click.ClickException(f"Cannot list NetPeerings: {e}")
148
+
149
+ return False
150
+
151
+ def _get_vpc_id(project_id: str):
152
+ response = do_request("GET", f"projects/{project_id}/nets")
153
+
154
+ if len(response) == 0:
155
+ raise click.ClickException("Cannot get vpc id")
156
+
157
+ return response[0]["NetId"]
158
+
159
+ def _get_iaas_owner_id(project_id: str):
160
+ response = do_request("GET", f"projects/{project_id}/quotas")["data"]
161
+
162
+ if len(response["quotas"]) == 0:
163
+ raise click.ClickException("Cannot get iaas owner id")
164
+
165
+ return response["quotas"][0]["AccountId"]
166
+
167
+ def _create_netpeering_request(name: str, source: dict, target: dict, user, group):
168
+ netpeering_request = get_template("netpeeringrequest")
169
+ netpeering_request['metadata']['name'] = name
170
+ netpeering_request['spec']['accepterNetId'] = target.get('network_id')
171
+ netpeering_request['spec']['accepterOwnerId'] = target.get('account_id')
172
+
173
+ _run_kubectl(source.get('project_id'), source.get('cluster_id'), user, group,
174
+ ['create', '-o', 'json', '-f', '-'], input=json.dumps(netpeering_request), capture=True)
175
+
176
+ # For security, we wait a bit for the status to be availabe
177
+ time.sleep(3)
178
+
179
+ netpeering_request_cmd = _run_kubectl(source.get('project_id'), source.get('cluster_id'), user, group,
180
+ ['get', 'netpeeringrequests', '-o', 'json', f"{name}"],
181
+ capture=True)
182
+ if netpeering_request_cmd.returncode:
183
+ raise click.ClickException(f"Cannot create NetPeeringRequest: {netpeering_request_cmd.stderr}")
184
+
185
+ netpeering_request = json.loads(netpeering_request_cmd.stdout.decode('utf-8'))
186
+
187
+ return netpeering_request
188
+
189
+ def _create_netpeering_acceptance(name: str, netpeering_request_status: dict, target: dict, user, group):
190
+
191
+ netpeering_acceptance = get_template("netpeeringacceptance")
192
+ netpeering_id = netpeering_request_status.get('netPeeringId')
193
+ netpeering_acceptance['metadata']['name'] = name
194
+ netpeering_acceptance['spec']['netPeeringId'] = netpeering_id
195
+
196
+ netpeering_request_status = netpeering_request_status.get('netPeeringState')
197
+ if netpeering_request_status != 'pending-acceptance':
198
+ raise click.ClickException(f"NetPeeringAcceptance is in wrong state: {netpeering_request_status}")
199
+
200
+ netpeering_acceptance_cmd = _run_kubectl(target.get('project_id'), target.get('cluster_id'), user, group,
201
+ ["create", "-f", "-"], input=json.dumps(netpeering_acceptance),
202
+ capture=True)
203
+
204
+ if netpeering_acceptance_cmd.returncode:
205
+ raise click.ClickException(f"Could not create NetPeeringAcceptance object {netpeering_id}: {netpeering_acceptance_cmd.stderr}")
206
+
207
+ def _dry_run(name_nr: str, name_na: str, output: str):
208
+ netpeering_request = get_template("netpeeringrequest")
209
+ netpeering_request['metadata']['name'] = name_nr
210
+
211
+ netpeering_acceptance = get_template("netpeeringacceptance")
212
+ netpeering_acceptance['metadata']['name'] = name_na
213
+
214
+ manifests = [netpeering_request, netpeering_acceptance]
215
+
216
+ if output == "yaml":
217
+ # print multiple yaml, separated by --- (kubernetes friendly format for multiple manifests)
218
+ output_data = yaml.dump_all(manifests, sort_keys=False)
219
+ click.echo(output_data)
220
+ else:
221
+ print_output([netpeering_request, netpeering_acceptance], output)
222
+
223
+ @netpeering.command('create', help="Create a NetPeering between 2 projects")
224
+ @click.option('--project-name', '--source-project', '-p', required=False, type=click.STRING, help="Source project name to create netpeering from", shell_complete=project_completer)
225
+ @click.option('--cluster-name', '--source-cluster', '-c', required=False, type=click.STRING, help="Source cluster to create netpeering from", shell_complete=cluster_completer)
226
+ @click.option('--target-project', required=True, type=click.STRING, help="Project name to create netpeering to", shell_complete=project_completer)
227
+ @click.option('--target-cluster', required=True, type=click.STRING, help="Target cluster to create netpeering to")
228
+ @click.option('--netpeering-name', required=False, type=click.STRING, help="Name of the NetPeeringRequest, default to '{from-project}-to-{to-project}",
229
+ default=None)
230
+ @click.option('--force', required=False, is_flag=True, help="Create netpeering resources without confirmation")
231
+ @click.option('--user', type=click.STRING, help="User for the kubeconfig of the source cluster")
232
+ @click.option('--group', type=click.STRING, help="Group for the kubeconfig of the source cluster")
233
+ @click.option('--dry-run', is_flag=True, help="Client dry-run, only print the object that would be sent, without sending it")
234
+ @click.option('--output', '-o', type=click.Choice(['json', 'yaml']), default="json", help="Specify output format, by default is json")
235
+ @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
236
+ @click.pass_context
237
+ def netpeering_create(ctx, project_name, cluster_name, target_project, target_cluster, netpeering_name, force,
238
+ user, group, dry_run, output, profile):
239
+ """Create NetPeering between 2 projects"""
240
+ project_name, cluster_name, profile = ctx_update(ctx, project_name, cluster_name, profile)
241
+ login_profile(profile)
242
+
243
+ project_name = get_project_name(project_name)
244
+ cluster_name = get_cluster_name(cluster_name)
245
+
246
+ source = _gather_info(project=project_name, cluster=cluster_name)
247
+ target = _gather_info(project=target_project, cluster=target_cluster)
248
+
249
+ # Generate name
250
+ if not netpeering_name:
251
+ netpeering_name = f"{source.get('project_name')}-to-{target.get('project_name')}"
252
+
253
+ netpeering_name += f"-{str(uuid.uuid4().fields[-1])[:6]}"
254
+ netpeering_request_name = f"{netpeering_name}-npr"
255
+ netpeering_acceptance_name = f"{netpeering_name}-npa"
256
+
257
+ if dry_run:
258
+ _dry_run(netpeering_request_name, netpeering_acceptance_name, output)
259
+ return
260
+
261
+ source_cidr = ipaddress.ip_network(source.get('project_cidr'))
262
+ target_cidr = ipaddress.ip_network(target.get('project_cidr'))
263
+
264
+ if source_cidr.overlaps(target_cidr) or target_cidr.overlaps(source_cidr):
265
+ raise click.ClickException(f"Source network {source.get('project_cidr')} and target network {target.get('project_cidr')} overlap, you can't create netpeering. Aborted!")
266
+
267
+ source_vpc_id = _get_vpc_id(source.get('project_id'))
268
+ source_iaas_owner_id = _get_iaas_owner_id(source.get('project_id'))
269
+
270
+ target_vpc_id = _get_vpc_id(target.get('project_id'))
271
+ target_iaas_owner_id = _get_iaas_owner_id(target.get('project_id'))
272
+
273
+ source.update({'network_id': source_vpc_id, 'account_id': source_iaas_owner_id})
274
+ target.update({'network_id': target_vpc_id, 'account_id': target_iaas_owner_id})
275
+
276
+ if _netpeering_exists(source=source, target=target, user=user, group=group):
277
+ raise click.ClickException(f"A NetPeering already exists between projects {source.get('project_name')} and {target.get('project_name')}. Aborting!")
278
+
279
+ if not force and \
280
+ not click.confirm(f"Are you sure you want to create NetPeering between projects {source.get('project_name')} and {target.get('project_name')}?", abort=False):
281
+ return "Abort."
282
+
283
+ netpeering_request = _create_netpeering_request(netpeering_request_name, source, target, user, group)
284
+ netpeering_id = netpeering_request.get('status').get('netPeeringId')
285
+
286
+ _create_netpeering_acceptance(netpeering_acceptance_name, netpeering_request.get('status'), target, user, group)
287
+
288
+ # Wait a bit for NetPeering to appear
289
+ time.sleep(3)
290
+
291
+ _run_kubectl(target.get('project_id'), target.get('cluster_id'), user, group,
292
+ ['get', 'netpeering', '-o', output, netpeering_id,])
293
+
@@ -10,7 +10,7 @@ from prettytable import TableStyle
10
10
  from .utils import do_request, print_output, print_table, find_project_id_by_name, get_project_id, set_project_id, \
11
11
  detect_and_parse_input, transform_tuple, ctx_update, set_cluster_id, get_template, get_project_name, \
12
12
  format_changed_row, is_interesting_status, login_profile, profile_completer, project_completer, \
13
- format_row
13
+ format_row, apply_set_fields
14
14
 
15
15
  # DEIFNE THE PROJECT COMMAND GROUP
16
16
  @click.group(help="Project related commands.")
@@ -56,6 +56,13 @@ def project_logout(ctx, profile):
56
56
  """Unset the current default project and log out."""
57
57
  _, _, profile = ctx_update(ctx, None, None, profile)
58
58
  login_profile(profile)
59
+
60
+ current_project = get_project_id()
61
+
62
+ if not current_project:
63
+ click.echo("You are not connected to any project.")
64
+ return
65
+
59
66
  set_project_id("")
60
67
  set_cluster_id("")
61
68
  click.echo("Logged out from the current project")
@@ -63,7 +70,7 @@ def project_logout(ctx, profile):
63
70
  # LIST PROJECTS
64
71
  @project.command('list', help="List all projects")
65
72
  @click.option('--project-name', '-p', help="Name of project", type=click.STRING, shell_complete=project_completer)
66
- @click.option('--deleted', '-x', is_flag=True, help="List deleted projects")
73
+ @click.option('--deleted', '-x', is_flag=True, deprecated="List deleted projects - Will be removed")
67
74
  @click.option('--plain', is_flag=True, help="Plain table format")
68
75
  @click.option('--msword', is_flag=True, help="Microsoft Word table format")
69
76
  @click.option('--uuid', is_flag=True, help="Show UUID")
@@ -106,7 +113,7 @@ def project_list(ctx, project_name, deleted, plain, msword, uuid, watch, output,
106
113
  table.set_style(TableStyle.PLAIN_COLUMNS)
107
114
 
108
115
  if msword:
109
- table.set_style(prettytable.MSWORD_FRIENDLY)
116
+ table.set_style(TableStyle.MSWORD_FRIENDLY)
110
117
 
111
118
  initial_projects = {}
112
119
 
@@ -188,8 +195,9 @@ def project_list(ctx, project_name, deleted, plain, msword, uuid, watch, output,
188
195
  @click.option('--output', '-o', type=click.Choice(["json", "yaml", "silent"]), help="Specify output format, by default is json")
189
196
  @click.option('--filename', '-f', type=click.File("r"), help="Path to file to use to create the project")
190
197
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
198
+ @click.option('--set', 'set_fields', multiple=True, help="Set arbitrary nested fields, e.g. auth.oidc.issuer-url=value")
191
199
  @click.pass_context
192
- def project_create(ctx, project_name, description, cidr, quirk, tags, disable_api_termination, dry_run, output, filename, profile):
200
+ def project_create(ctx, project_name, description, cidr, quirk, tags, disable_api_termination, dry_run, output, filename, profile, set_fields):
193
201
  """Create a new project from options or file, with support for dry-run and output formatting."""
194
202
  project_name, _, profile = ctx_update(ctx, project_name, None, profile)
195
203
  login_profile(profile)
@@ -230,6 +238,8 @@ def project_create(ctx, project_name, description, cidr, quirk, tags, disable_ap
230
238
 
231
239
  if disable_api_termination is not None:
232
240
  project_config["disable_api_termination"] = disable_api_termination
241
+
242
+ apply_set_fields(project_config, set_fields)
233
243
 
234
244
  if not dry_run:
235
245
  data = do_request("POST", 'projects', json=project_config)
@@ -295,8 +305,9 @@ def project_delete_command(ctx, project_name, output, dry_run, force, profile):
295
305
  @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
296
306
  @click.option('--dry-run', is_flag=True, help="Run without any action")
297
307
  @click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
308
+ @click.option('--set', 'set_fields', multiple=True, help="Set arbitrary nested fields, e.g. auth.oidc.issuer-url=value")
298
309
  @click.pass_context
299
- def project_update_command(ctx, project_name, description, quirk, tags, disable_api_termination, output, dry_run, profile):
310
+ def project_update_command(ctx, project_name, description, quirk, tags, disable_api_termination, output, dry_run, profile, set_fields):
300
311
  """Update project details by name, supporting dry-run and output formatting."""
301
312
  project_name, _, profile = ctx_update(ctx, project_name, None, profile)
302
313
  login_profile(profile)
@@ -326,6 +337,8 @@ def project_update_command(ctx, project_name, description, quirk, tags, disable_
326
337
  parsed_tags[key.strip()] = value.strip()
327
338
 
328
339
  project_config['tags'] = parsed_tags
340
+
341
+ apply_set_fields(project_config, set_fields)
329
342
 
330
343
  if dry_run:
331
344
  print_output(project_config, output)
@@ -398,7 +411,7 @@ def project_get_public_ips(ctx, project_name, output, profile):
398
411
  # GET NETS BY PROJECT NAME
399
412
  @project.command('nets', help="Get project nets")
400
413
  @click.option('--project-name', '-p', help="Name of the project", shell_complete=project_completer)
401
- @click.option('--output', '-o', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
414
+ @click.option('--output', '-o', type=click.Choice(["json", "yaml", "table"]), help="Specify output format, by default is json")
402
415
  @click.option('--profile',help="Configuration profile to use")
403
416
  @click.pass_context
404
417
  def project_get_public_ips(ctx, project_name, output, profile):
@@ -409,4 +422,11 @@ def project_get_public_ips(ctx, project_name, output, profile):
409
422
  project_id = find_project_id_by_name(project_name)
410
423
 
411
424
  data = do_request("GET", f'projects/{project_id}/nets')
412
- print_output(data, output)
425
+ if output == "table":
426
+ print_table(data, [["DHCP options set id", "DhcpOptionsSetId"],
427
+ ["Ip range", "IpRange"],
428
+ ["Net id", "NetId"],
429
+ ["State", "State"],
430
+ ["Tenancy", "Tenancy"]])
431
+ else:
432
+ print_output(data, output)