oks-cli 1.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oks_cli/__init__.py +0 -0
- oks_cli/cache.py +63 -0
- oks_cli/cluster.py +746 -0
- oks_cli/main.py +91 -0
- oks_cli/profile.py +128 -0
- oks_cli/project.py +398 -0
- oks_cli/quotas.py +14 -0
- oks_cli/utils.py +850 -0
- oks_cli-1.14.dist-info/METADATA +270 -0
- oks_cli-1.14.dist-info/RECORD +14 -0
- oks_cli-1.14.dist-info/WHEEL +5 -0
- oks_cli-1.14.dist-info/entry_points.txt +2 -0
- oks_cli-1.14.dist-info/licenses/LICENSE +29 -0
- oks_cli-1.14.dist-info/top_level.txt +1 -0
oks_cli/main.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import click
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from .project import project
|
|
7
|
+
from .cluster import cluster
|
|
8
|
+
from .profile import profile
|
|
9
|
+
from .cache import cache
|
|
10
|
+
from .quotas import quotas
|
|
11
|
+
|
|
12
|
+
from .utils import ctx_update, login_profile, install_completions, profile_completer
|
|
13
|
+
|
|
14
|
+
# Main CLI entry point
|
|
15
|
+
@click.group(invoke_without_command=True)
|
|
16
|
+
@click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
|
|
17
|
+
@click.option('--project-name', '-p', required = False, help="Project Name")
|
|
18
|
+
@click.option('--cluster-name', '-c', required = False, help="Cluster Name")
|
|
19
|
+
@click.option('-v', '--verbose', count=True)
|
|
20
|
+
@click.pass_context
|
|
21
|
+
def cli(ctx, project_name, cluster_name, profile, verbose):
|
|
22
|
+
"""
|
|
23
|
+
CLI tool to manage projects and clusters.
|
|
24
|
+
|
|
25
|
+
You can run commands on two scopes: 'project' and 'cluster'.
|
|
26
|
+
Each scope has several actions like 'list', 'get', 'create', etc.
|
|
27
|
+
"""
|
|
28
|
+
ctx_update(ctx, project_name, cluster_name, profile)
|
|
29
|
+
|
|
30
|
+
LOGLEVEL = os.environ.get('LOGLEVEL', 'WARNING').upper()
|
|
31
|
+
|
|
32
|
+
if verbose >= 1:
|
|
33
|
+
LOGLEVEL = "INFO"
|
|
34
|
+
|
|
35
|
+
if verbose >= 2:
|
|
36
|
+
LOGLEVEL = "DEBUG"
|
|
37
|
+
|
|
38
|
+
logging.basicConfig(
|
|
39
|
+
format='%(asctime)s %(levelname)-8s %(filename)s:%(funcName)s %(message)s',
|
|
40
|
+
level=LOGLEVEL,
|
|
41
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
42
|
+
)
|
|
43
|
+
logging.info("Set loglevel to %s", LOGLEVEL)
|
|
44
|
+
|
|
45
|
+
if ctx.invoked_subcommand is None:
|
|
46
|
+
click.echo(ctx.get_help())
|
|
47
|
+
|
|
48
|
+
if not hasattr(ctx, 'obj') or not ctx.obj:
|
|
49
|
+
ctx.obj = dict()
|
|
50
|
+
|
|
51
|
+
if project_name != None:
|
|
52
|
+
ctx.obj['project_name'] = project_name
|
|
53
|
+
|
|
54
|
+
if cluster_name != None:
|
|
55
|
+
ctx.obj['cluster_name'] = cluster_name
|
|
56
|
+
|
|
57
|
+
# Register the project and cluster groups with the main CLI
|
|
58
|
+
cli.add_command(project)
|
|
59
|
+
cli.add_command(cluster)
|
|
60
|
+
cli.add_command(profile)
|
|
61
|
+
cli.add_command(cache)
|
|
62
|
+
cli.add_command(quotas)
|
|
63
|
+
|
|
64
|
+
def recursive_help(cmd, parent=None):
|
|
65
|
+
"""Recursively prints help for all commands and subcommands."""
|
|
66
|
+
ctx = click.core.Context(cmd, info_name=cmd.name, parent=parent)
|
|
67
|
+
print(cmd.get_help(ctx))
|
|
68
|
+
print()
|
|
69
|
+
commands = getattr(cmd, 'commands', {})
|
|
70
|
+
for sub in commands.values():
|
|
71
|
+
recursive_help(sub, ctx)
|
|
72
|
+
|
|
73
|
+
@cli.command('fullhelp', help="Display detailed help information for all commands.")
|
|
74
|
+
def fullhelp():
|
|
75
|
+
"""Display comprehensive help for all CLI commands."""
|
|
76
|
+
recursive_help(cli)
|
|
77
|
+
|
|
78
|
+
@cli.command('version', help="Show the current CLI version.")
|
|
79
|
+
def version():
|
|
80
|
+
"""Display the current CLI version."""
|
|
81
|
+
import importlib.metadata
|
|
82
|
+
print(importlib.metadata.version(__package__))
|
|
83
|
+
|
|
84
|
+
@cli.command("install-completion", help="Install shell completion scripts.")
|
|
85
|
+
@click.option('--type', help="Shell")
|
|
86
|
+
def install_completion(type):
|
|
87
|
+
"""Install shell completion scripts for the CLI."""
|
|
88
|
+
install_completions(type)
|
|
89
|
+
|
|
90
|
+
if __name__ == '__main__':
|
|
91
|
+
cli()
|
oks_cli/profile.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from .utils import set_profile, remove_profile, profile_list, DEFAULT_API_URL, get_profiles
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# DEFINE THE PROFILE COMMAND GROUP
|
|
6
|
+
@click.group(help="Profile related commands.")
|
|
7
|
+
def profile():
|
|
8
|
+
"""Profile management command group."""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@profile.command('add', help="Add AK/SK or username/password new profile")
|
|
13
|
+
@click.option('--profile-name', required=False, help="Name of profile, optional", type=click.STRING)
|
|
14
|
+
@click.option('--access-key', required=False, help="AK of profile", type=click.STRING)
|
|
15
|
+
@click.option('--secret-key', required=False, help="SK of profile", type=click.STRING)
|
|
16
|
+
@click.option('--username', required=False, help="Username", type=click.STRING)
|
|
17
|
+
@click.option('--password', required=False, help="Password", type=click.STRING)
|
|
18
|
+
@click.option('--region', required=True, help="Region name", type=click.Choice(['eu-west-2', 'cloudgouv-eu-west-1']))
|
|
19
|
+
@click.option('--endpoint', required=False, help="API endpoint", type=click.STRING)
|
|
20
|
+
@click.option('--jwt', required=False, help="Enable jwt, by default is false", type=click.BOOL)
|
|
21
|
+
def add_profile(profile_name, access_key, secret_key, username, password, region, endpoint, jwt):
|
|
22
|
+
"""Add a new profile with AK/SK or username/password authentication."""
|
|
23
|
+
if not profile_name:
|
|
24
|
+
profile_name = "default"
|
|
25
|
+
|
|
26
|
+
existing_profiles = get_profiles()
|
|
27
|
+
if profile_name in existing_profiles:
|
|
28
|
+
confirm = click.confirm(
|
|
29
|
+
f"The profile '{profile_name}' already exists. Do you want to replace it?",
|
|
30
|
+
default=False
|
|
31
|
+
)
|
|
32
|
+
if not confirm:
|
|
33
|
+
click.echo("Aborted.")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
obj = {
|
|
37
|
+
"region_name": region
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if endpoint:
|
|
41
|
+
obj["endpoint"] = endpoint
|
|
42
|
+
|
|
43
|
+
if access_key and secret_key:
|
|
44
|
+
obj["type"] = "ak/sk"
|
|
45
|
+
obj["access_key"] = access_key
|
|
46
|
+
obj["secret_key"] = secret_key
|
|
47
|
+
|
|
48
|
+
elif username and password:
|
|
49
|
+
obj["type"] = "username/password"
|
|
50
|
+
obj["username"] = username
|
|
51
|
+
obj["password"] = password
|
|
52
|
+
else:
|
|
53
|
+
raise click.UsageError("--access-key and --secret-key or --username and --password must be specified")
|
|
54
|
+
|
|
55
|
+
if jwt is not None:
|
|
56
|
+
obj["jwt"] = jwt
|
|
57
|
+
|
|
58
|
+
set_profile(profile_name, obj)
|
|
59
|
+
|
|
60
|
+
profile_name_styled = click.style(profile_name, bold=True)
|
|
61
|
+
click.echo(f"Profile {profile_name_styled} has been successfully added")
|
|
62
|
+
|
|
63
|
+
@profile.command('update', help="Update an existing profile")
|
|
64
|
+
@click.option('--profile-name', required=True, help="Name of profile", type=click.STRING)
|
|
65
|
+
@click.option('--region', required=False, help="Region name", type=click.Choice(['eu-west-2', 'cloudgouv-eu-west-1']))
|
|
66
|
+
@click.option('--endpoint', required=False, help="API endpoint", type=click.STRING)
|
|
67
|
+
@click.option('--jwt', required=False, help="Enable jwt, by default is false", type=click.BOOL)
|
|
68
|
+
def update_profile(profile_name, region, endpoint, jwt):
|
|
69
|
+
"""Update configuration settings for an existing profile."""
|
|
70
|
+
profiles = profile_list()
|
|
71
|
+
if profile_name not in profiles:
|
|
72
|
+
raise click.ClickException(f"There no profile with name: {profile_name}")
|
|
73
|
+
|
|
74
|
+
profile = profiles[profile_name]
|
|
75
|
+
if region:
|
|
76
|
+
profile["region_name"] = region
|
|
77
|
+
|
|
78
|
+
if endpoint:
|
|
79
|
+
profile["endpoint"] = endpoint
|
|
80
|
+
|
|
81
|
+
if jwt is not None:
|
|
82
|
+
profile["jwt"] = jwt
|
|
83
|
+
|
|
84
|
+
set_profile(profile_name, profile)
|
|
85
|
+
|
|
86
|
+
profile_name = click.style(profile_name, bold=True)
|
|
87
|
+
|
|
88
|
+
click.echo(f"Profile {profile_name} has been successfully updated")
|
|
89
|
+
|
|
90
|
+
@profile.command('delete', help="Delete a profile by name")
|
|
91
|
+
@click.option('--profile-name', required=True, help="Name of profile", type=click.STRING)
|
|
92
|
+
@click.option('--force', is_flag=True, help="Force deletion without confirmation")
|
|
93
|
+
def delete_profile(profile_name, force):
|
|
94
|
+
"""Delete a profile with confirmation."""
|
|
95
|
+
profiles = profile_list()
|
|
96
|
+
profile_name_bold = click.style(profile_name, bold=True)
|
|
97
|
+
|
|
98
|
+
if profile_name not in profiles:
|
|
99
|
+
raise click.ClickException(f"There no profile with name: {profile_name_bold}")
|
|
100
|
+
|
|
101
|
+
if force or click.confirm(f"Are you sure you want to delete the profile with name {profile_name_bold}?", abort=True):
|
|
102
|
+
remove_profile(profile_name)
|
|
103
|
+
click.echo(f"Profile {profile_name_bold} has been successfully deleted")
|
|
104
|
+
|
|
105
|
+
@profile.command('list', help="List existing profiles")
|
|
106
|
+
def list_profiles():
|
|
107
|
+
"""Display all configured profiles with their settings."""
|
|
108
|
+
profiles = profile_list()
|
|
109
|
+
|
|
110
|
+
if not profiles:
|
|
111
|
+
return click.echo("There are no profiles")
|
|
112
|
+
|
|
113
|
+
profiles_keys = list(profiles.keys())
|
|
114
|
+
|
|
115
|
+
for key in profiles_keys:
|
|
116
|
+
if 'endpoint' not in profiles[key]:
|
|
117
|
+
if 'region_name' in profiles[key]:
|
|
118
|
+
endpoint = click.style(DEFAULT_API_URL.format(region=profiles[key]['region_name']), bold=True)
|
|
119
|
+
else:
|
|
120
|
+
endpoint = None
|
|
121
|
+
else:
|
|
122
|
+
endpoint = click.style(profiles[key]["endpoint"], bold=True)
|
|
123
|
+
name = click.style(key, bold=True)
|
|
124
|
+
account_type = click.style(profiles[key]["type"], bold=True)
|
|
125
|
+
region = click.style(profiles[key]["region_name"], bold=True)
|
|
126
|
+
jwt = click.style(profiles[key].get("jwt", False), bold=True)
|
|
127
|
+
|
|
128
|
+
click.echo(f"Profile: {name} Account type: {account_type} Region: {region} Endpoint: {endpoint} Enabled JWT auth: {jwt}")
|
oks_cli/project.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import time
|
|
3
|
+
import datetime
|
|
4
|
+
import dateutil.parser
|
|
5
|
+
import human_readable
|
|
6
|
+
import prettytable
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from .utils import do_request, print_output, find_project_id_by_name, get_project_id, set_project_id, detect_and_parse_input, transform_tuple, ctx_update, set_cluster_id, get_template, get_project_name, format_changed_row, is_interesting_status, login_profile, profile_completer
|
|
10
|
+
|
|
11
|
+
# DEIFNE THE PROJECT COMMAND GROUP
|
|
12
|
+
@click.group(help="Project related commands.")
|
|
13
|
+
@click.option('--project', 'project_name', required = False, help="Project Name")
|
|
14
|
+
@click.option('--project-name', '-p', required = False, help="Project Name")
|
|
15
|
+
@click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
|
|
16
|
+
@click.pass_context
|
|
17
|
+
def project(ctx, project_name, profile):
|
|
18
|
+
"""Group of commands related to project management."""
|
|
19
|
+
ctx_update(ctx, project_name, None, profile)
|
|
20
|
+
|
|
21
|
+
# LOGIN ON PROJECT
|
|
22
|
+
@project.command('login', help="Set a default project by name")
|
|
23
|
+
@click.option('--project-name', '-p', required=False, help="Name of project", type=click.STRING)
|
|
24
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
25
|
+
@click.pass_context
|
|
26
|
+
def project_login(ctx, project_name, profile):
|
|
27
|
+
"""Set a default project by its name and log in."""
|
|
28
|
+
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
|
|
29
|
+
login_profile(profile)
|
|
30
|
+
|
|
31
|
+
data = do_request("GET", 'projects', params={"name": project_name})
|
|
32
|
+
if len(data) != 1:
|
|
33
|
+
raise click.BadParameter(
|
|
34
|
+
f"{len(data)} projects found by name: {project_name}")
|
|
35
|
+
project = data.pop()
|
|
36
|
+
|
|
37
|
+
project_id = project['id']
|
|
38
|
+
project_name = project['name']
|
|
39
|
+
|
|
40
|
+
set_project_id(project_id)
|
|
41
|
+
set_cluster_id("")
|
|
42
|
+
|
|
43
|
+
project_name = click.style(project_name, bold=True)
|
|
44
|
+
|
|
45
|
+
click.echo(f"Logged into project: {project_name}")
|
|
46
|
+
|
|
47
|
+
# LOGOUT ON PROJECT
|
|
48
|
+
@project.command('logout', help="Unset default project")
|
|
49
|
+
@click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
|
|
50
|
+
@click.pass_context
|
|
51
|
+
def project_logout(ctx, profile):
|
|
52
|
+
"""Unset the current default project and log out."""
|
|
53
|
+
_, _, profile = ctx_update(ctx, None, None, profile)
|
|
54
|
+
login_profile(profile)
|
|
55
|
+
set_project_id("")
|
|
56
|
+
set_cluster_id("")
|
|
57
|
+
click.echo("Logged out from the current project")
|
|
58
|
+
|
|
59
|
+
# LIST PROJECTS
|
|
60
|
+
@project.command('list', help="List all projects")
|
|
61
|
+
@click.option('--project-name', '-p', help="Name of project", type=click.STRING)
|
|
62
|
+
@click.option('--deleted', is_flag=True, help="List deleted projects")
|
|
63
|
+
@click.option('--plain', is_flag=True, help="Plain table format")
|
|
64
|
+
@click.option('--msword', is_flag=True, help="Microsoft Word table format")
|
|
65
|
+
@click.option('--uuid', is_flag=True, help="show uuid")
|
|
66
|
+
@click.option('--watch', '-w', is_flag=True, help="Watch the changes")
|
|
67
|
+
@click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
68
|
+
@click.option('--profile', help="Configuration profile to use")
|
|
69
|
+
@click.pass_context
|
|
70
|
+
def project_list(ctx, project_name, deleted, plain, msword, uuid, watch, output, profile):
|
|
71
|
+
"""List projects with filtering, formatting, and live watch capabilities."""
|
|
72
|
+
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
|
|
73
|
+
login_profile(profile)
|
|
74
|
+
|
|
75
|
+
project_id = get_project_id()
|
|
76
|
+
params = {}
|
|
77
|
+
if project_name:
|
|
78
|
+
params['name'] = project_name
|
|
79
|
+
|
|
80
|
+
if deleted:
|
|
81
|
+
params['deleted'] = True
|
|
82
|
+
|
|
83
|
+
data = do_request("GET", 'projects', params=params)
|
|
84
|
+
|
|
85
|
+
if output:
|
|
86
|
+
print_output(data, output)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
field_names = ["NAME", "CREATED", "UPDATED", "STATUS", "DEFAULT"]
|
|
90
|
+
if uuid:
|
|
91
|
+
field_names.append('UUID')
|
|
92
|
+
|
|
93
|
+
table = prettytable.PrettyTable()
|
|
94
|
+
table.field_names = field_names
|
|
95
|
+
|
|
96
|
+
table._min_width = {"CREATED": 13, "UPDATED": 13, "STATUS": 10}
|
|
97
|
+
|
|
98
|
+
if plain or watch:
|
|
99
|
+
table.set_style(prettytable.PLAIN_COLUMNS)
|
|
100
|
+
|
|
101
|
+
if msword:
|
|
102
|
+
table.set_style(prettytable.MSWORD_FRIENDLY)
|
|
103
|
+
|
|
104
|
+
def format_row(project):
|
|
105
|
+
status = project.get('status')
|
|
106
|
+
is_default = True if project.get('id') == project_id else False
|
|
107
|
+
|
|
108
|
+
if status == 'ready':
|
|
109
|
+
msg = click.style(status, fg='green')
|
|
110
|
+
elif status == 'failed' or status == 'deleted':
|
|
111
|
+
msg = click.style(status, fg='red')
|
|
112
|
+
elif status == 'deploying':
|
|
113
|
+
msg = click.style(status, fg='yellow')
|
|
114
|
+
else:
|
|
115
|
+
msg = status
|
|
116
|
+
|
|
117
|
+
name = click.style(project['name'], bold=True)
|
|
118
|
+
if is_default:
|
|
119
|
+
default = "*"
|
|
120
|
+
else:
|
|
121
|
+
default = ""
|
|
122
|
+
|
|
123
|
+
created_at = dateutil.parser.parse(project['created_at'])
|
|
124
|
+
updated_at = dateutil.parser.parse(project['updated_at'])
|
|
125
|
+
now = datetime.datetime.now(tz=created_at.tzinfo)
|
|
126
|
+
|
|
127
|
+
row = [name, human_readable.date_time(now - created_at), human_readable.date_time(now - updated_at), msg, default]
|
|
128
|
+
if uuid:
|
|
129
|
+
row.append(project['id'])
|
|
130
|
+
|
|
131
|
+
return row, status, project['name']
|
|
132
|
+
|
|
133
|
+
initial_projects = {}
|
|
134
|
+
|
|
135
|
+
for project in data:
|
|
136
|
+
row, _, name = format_row(project)
|
|
137
|
+
table.add_row(row)
|
|
138
|
+
initial_projects[name] = project
|
|
139
|
+
|
|
140
|
+
click.echo(table)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if watch:
|
|
144
|
+
total_sleep = 0
|
|
145
|
+
try:
|
|
146
|
+
while True:
|
|
147
|
+
time.sleep(2)
|
|
148
|
+
total_sleep += 2
|
|
149
|
+
try:
|
|
150
|
+
data = do_request("GET", 'projects', params=params)
|
|
151
|
+
except click.ClickException as err:
|
|
152
|
+
click.echo(f"Error during watch: {err}")
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
current_project_names = {project['name'] for project in data}
|
|
156
|
+
|
|
157
|
+
for name, project in list(initial_projects.items()):
|
|
158
|
+
if name not in current_project_names:
|
|
159
|
+
deleted_project = project.copy()
|
|
160
|
+
deleted_project['status'] = 'deleted'
|
|
161
|
+
|
|
162
|
+
row, current_status, _ = format_row(deleted_project)
|
|
163
|
+
|
|
164
|
+
new_table = format_changed_row(table, row)
|
|
165
|
+
click.echo(new_table)
|
|
166
|
+
|
|
167
|
+
del initial_projects[name]
|
|
168
|
+
|
|
169
|
+
for project in data:
|
|
170
|
+
row, current_status, name = format_row(project)
|
|
171
|
+
|
|
172
|
+
if name not in initial_projects:
|
|
173
|
+
new_table = format_changed_row(table, row)
|
|
174
|
+
click.echo(new_table)
|
|
175
|
+
initial_projects[name] = project
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
stored_project = initial_projects[name]
|
|
179
|
+
project_status = stored_project.get('status')
|
|
180
|
+
if project_status != current_status:
|
|
181
|
+
new_table = format_changed_row(table, row)
|
|
182
|
+
click.echo(new_table)
|
|
183
|
+
initial_projects[name] = project
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
if total_sleep % 10 == 0 and is_interesting_status(current_status):
|
|
187
|
+
new_table = format_changed_row(table, row)
|
|
188
|
+
click.echo(new_table)
|
|
189
|
+
initial_projects[name] = project
|
|
190
|
+
|
|
191
|
+
except KeyboardInterrupt:
|
|
192
|
+
click.echo("\nWatch stopped.")
|
|
193
|
+
|
|
194
|
+
# CREATE PROJECT BY NAME
|
|
195
|
+
@project.command('create', help="Create a new project")
|
|
196
|
+
@click.option('--project-name', '-p', help="Name of the project")
|
|
197
|
+
@click.option('--description', help="Description of the project")
|
|
198
|
+
@click.option('--cidr', help='CIDR for the project')
|
|
199
|
+
@click.option('--quirk', multiple=True, help="Quirk")
|
|
200
|
+
@click.option('--tags', help="Comma-separated list of tags, example: 'key1=value1,key2=value2'")
|
|
201
|
+
@click.option('--disable-api-termination', type=click.BOOL, help="Disable delete action by API")
|
|
202
|
+
@click.option('--dry-run', is_flag=True, help="Client dry-run, only print the object that would be sent, without sending it")
|
|
203
|
+
@click.option('-o', '--output', type=click.Choice(["json", "yaml", "silent"]), help="Specify output format, by default is json")
|
|
204
|
+
@click.option('-f', '--filename', type=click.File("r"), help="Path to file to use to create the project ")
|
|
205
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
206
|
+
@click.pass_context
|
|
207
|
+
def project_create(ctx, project_name, description, cidr, quirk, tags, disable_api_termination, dry_run, output, filename, profile):
|
|
208
|
+
"""Create a new project from options or file, with support for dry-run and output formatting."""
|
|
209
|
+
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
|
|
210
|
+
login_profile(profile)
|
|
211
|
+
|
|
212
|
+
project_config = None
|
|
213
|
+
|
|
214
|
+
if filename:
|
|
215
|
+
input_data = filename.read()
|
|
216
|
+
project_config = detect_and_parse_input(input_data)
|
|
217
|
+
else:
|
|
218
|
+
project_config = get_template("project")
|
|
219
|
+
if os.getenv("OKS_REGION"):
|
|
220
|
+
project_config["region"] = os.getenv("OKS_REGION")
|
|
221
|
+
|
|
222
|
+
if project_name:
|
|
223
|
+
project_config['name'] = project_name
|
|
224
|
+
|
|
225
|
+
if description:
|
|
226
|
+
project_config['description'] = description
|
|
227
|
+
|
|
228
|
+
if cidr:
|
|
229
|
+
project_config['cidr'] = cidr
|
|
230
|
+
|
|
231
|
+
if quirk:
|
|
232
|
+
project_config['quirks'] = transform_tuple(quirk)
|
|
233
|
+
|
|
234
|
+
if tags:
|
|
235
|
+
parsed_tags = {}
|
|
236
|
+
|
|
237
|
+
pairs = tags.split(',')
|
|
238
|
+
for pair in pairs:
|
|
239
|
+
if '=' not in pair:
|
|
240
|
+
raise click.ClickException(f"Malformed tags: '{pair}' (expected key=value)")
|
|
241
|
+
key, value = pair.split('=', 1)
|
|
242
|
+
parsed_tags[key.strip()] = value.strip()
|
|
243
|
+
|
|
244
|
+
project_config['tags'] = parsed_tags
|
|
245
|
+
|
|
246
|
+
if disable_api_termination is not None:
|
|
247
|
+
project_config["disable_api_termination"] = disable_api_termination
|
|
248
|
+
|
|
249
|
+
if not dry_run:
|
|
250
|
+
data = do_request("POST", 'projects', json=project_config)
|
|
251
|
+
print_output(data, output)
|
|
252
|
+
else:
|
|
253
|
+
print_output(project_config, output)
|
|
254
|
+
|
|
255
|
+
# GET PROJECT BY NAME
|
|
256
|
+
@project.command('get', help="Get default project or the project by name")
|
|
257
|
+
@click.option('--project-name', '-p', help="Name of the project")
|
|
258
|
+
@click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
259
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
260
|
+
@click.pass_context
|
|
261
|
+
def project_get(ctx, project_name, output, profile):
|
|
262
|
+
"""Retrieve and display project details by name or default project."""
|
|
263
|
+
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
|
|
264
|
+
login_profile(profile)
|
|
265
|
+
|
|
266
|
+
project_id = find_project_id_by_name(project_name)
|
|
267
|
+
|
|
268
|
+
data = do_request("GET", f'projects/{project_id}')
|
|
269
|
+
print_output(data, output)
|
|
270
|
+
|
|
271
|
+
# DELETE PROJECT BY NAME
|
|
272
|
+
@project.command('delete', help="Delete a project by name")
|
|
273
|
+
@click.option('--project-name', '-p', required=False, help="Project Name")
|
|
274
|
+
@click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
275
|
+
@click.option('--dry-run', is_flag=True, help="Run without any action")
|
|
276
|
+
@click.option('--force', is_flag=True, help="Force deletion without confirmation")
|
|
277
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
278
|
+
@click.pass_context
|
|
279
|
+
def project_delete_command(ctx, project_name, output, dry_run, force, profile):
|
|
280
|
+
"""Delete a project by name, with optional dry-run and confirmation."""
|
|
281
|
+
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
|
|
282
|
+
login_profile(profile)
|
|
283
|
+
|
|
284
|
+
project_id = find_project_id_by_name(project_name)
|
|
285
|
+
project_name = get_project_name(project_name)
|
|
286
|
+
|
|
287
|
+
current_project_id = get_project_id()
|
|
288
|
+
|
|
289
|
+
if dry_run:
|
|
290
|
+
message = {"message": "Dry run: The project would be deleted."}
|
|
291
|
+
print_output(message, output)
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
if force or click.confirm(f"Are you sure you want to delete the project with name {project_name}?", abort=True):
|
|
295
|
+
data = do_request("DELETE", f'projects/{project_id}')
|
|
296
|
+
|
|
297
|
+
if current_project_id == project_id:
|
|
298
|
+
set_project_id("")
|
|
299
|
+
set_cluster_id("")
|
|
300
|
+
print_output(data, output)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# UPDATE PROJECT BY NAME
|
|
304
|
+
@project.command('update', help="Update a project by name")
|
|
305
|
+
@click.option('--project-name', '-p', required=False, help="Project Name")
|
|
306
|
+
@click.option('--description', help="Description of the project")
|
|
307
|
+
@click.option('--quirk', multiple=True, help="Quirk")
|
|
308
|
+
@click.option('--tags', help="Comma-separated list of tags, example: 'key1=value1,key2=value2'")
|
|
309
|
+
@click.option('--disable-api-termination', type=click.BOOL, help="Disable delete action by API")
|
|
310
|
+
@click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
311
|
+
@click.option('--dry-run', is_flag=True, help="Run without any action")
|
|
312
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
313
|
+
@click.pass_context
|
|
314
|
+
def project_update_command(ctx, project_name, description, quirk, tags, disable_api_termination, output, dry_run, profile):
|
|
315
|
+
"""Update project details by name, supporting dry-run and output formatting."""
|
|
316
|
+
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
|
|
317
|
+
login_profile(profile)
|
|
318
|
+
|
|
319
|
+
project_id = find_project_id_by_name(project_name)
|
|
320
|
+
|
|
321
|
+
project_config = {}
|
|
322
|
+
|
|
323
|
+
if disable_api_termination is not None:
|
|
324
|
+
project_config["disable_api_termination"] = disable_api_termination
|
|
325
|
+
|
|
326
|
+
if description:
|
|
327
|
+
project_config['description'] = description
|
|
328
|
+
|
|
329
|
+
if quirk:
|
|
330
|
+
project_config['quirks'] = transform_tuple(quirk)
|
|
331
|
+
|
|
332
|
+
if tags is not None:
|
|
333
|
+
parsed_tags = {}
|
|
334
|
+
|
|
335
|
+
if not len(tags) == 0:
|
|
336
|
+
pairs = tags.split(',')
|
|
337
|
+
for pair in pairs:
|
|
338
|
+
if '=' not in pair:
|
|
339
|
+
raise click.ClickException(f"Malformed tags: '{pair}' (expected key=value)")
|
|
340
|
+
key, value = pair.split('=', 1)
|
|
341
|
+
parsed_tags[key.strip()] = value.strip()
|
|
342
|
+
|
|
343
|
+
project_config['tags'] = parsed_tags
|
|
344
|
+
|
|
345
|
+
if dry_run:
|
|
346
|
+
print_output(project_config, output)
|
|
347
|
+
else:
|
|
348
|
+
data = do_request("PATCH", f'projects/{project_id}', json = project_config)
|
|
349
|
+
print_output(data, output)
|
|
350
|
+
|
|
351
|
+
# GET PROJECT QUOTAS BY PROJECT NAME
|
|
352
|
+
@project.command('quotas', help="Get project quotas")
|
|
353
|
+
@click.option('--project-name', '-p', help="Name of the project")
|
|
354
|
+
@click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
355
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
356
|
+
@click.pass_context
|
|
357
|
+
def project_get_quotas(ctx, project_name, output, profile):
|
|
358
|
+
"""Retrieve resource quotas for the specified project."""
|
|
359
|
+
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
|
|
360
|
+
login_profile(profile)
|
|
361
|
+
|
|
362
|
+
project_id = find_project_id_by_name(project_name)
|
|
363
|
+
|
|
364
|
+
data = do_request("GET", f'projects/{project_id}/quotas')["data"]
|
|
365
|
+
print_output(data, output)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# GET PROJECT SNAPSHOTS BY PROJECT NAME
|
|
369
|
+
@project.command('snapshots', help="Get project snapshots")
|
|
370
|
+
@click.option('--project-name', '-p', help="Name of the project")
|
|
371
|
+
@click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
372
|
+
@click.option('--profile', help="Configuration profile to use", shell_complete=profile_completer)
|
|
373
|
+
@click.pass_context
|
|
374
|
+
def project_get(ctx, project_name, output, profile):
|
|
375
|
+
"""Retrieve snapshots associated with the specified project."""
|
|
376
|
+
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
|
|
377
|
+
login_profile(profile)
|
|
378
|
+
|
|
379
|
+
project_id = find_project_id_by_name(project_name)
|
|
380
|
+
|
|
381
|
+
response = do_request("GET", f'projects/{project_id}/snapshots')
|
|
382
|
+
print_output(response, output)
|
|
383
|
+
|
|
384
|
+
# GET PUBLIC IPS BY PROJECT NAME
|
|
385
|
+
@project.command('publicips', help="Get project public ips")
|
|
386
|
+
@click.option('--project-name', '-p', help="Name of the project")
|
|
387
|
+
@click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
388
|
+
@click.option('--profile', help="Configuration profile to use")
|
|
389
|
+
@click.pass_context
|
|
390
|
+
def project_get_public_ips(ctx, project_name, output, profile):
|
|
391
|
+
"""Retrieve the list of public IPs associated with the specified project."""
|
|
392
|
+
project_name, _, profile = ctx_update(ctx, project_name, None, profile)
|
|
393
|
+
login_profile(profile)
|
|
394
|
+
|
|
395
|
+
project_id = find_project_id_by_name(project_name)
|
|
396
|
+
|
|
397
|
+
data = do_request("GET", f'projects/{project_id}/public_ips')
|
|
398
|
+
print_output(data, output)
|
oks_cli/quotas.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from .utils import do_request, print_output, ctx_update, login_profile, profile_completer
|
|
3
|
+
|
|
4
|
+
@click.command(help="Get Quotas")
|
|
5
|
+
@click.option("--profile", help="Configuration profile to use", shell_complete=profile_completer)
|
|
6
|
+
@click.option('-o', '--output', type=click.Choice(["json", "yaml"]), help="Specify output format, by default is json")
|
|
7
|
+
@click.pass_context
|
|
8
|
+
def quotas(ctx, profile, output):
|
|
9
|
+
"""Retrieve global quotas across all projects for the given profile."""
|
|
10
|
+
_, _, profile = ctx_update(ctx, None, None, profile)
|
|
11
|
+
login_profile(profile)
|
|
12
|
+
|
|
13
|
+
data = do_request("GET", 'quotas')
|
|
14
|
+
print_output(data, output)
|