qod-cli 0.3.7__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.
- qod_cli/__init__.py +1 -0
- qod_cli/commands/__init__.py +0 -0
- qod_cli/commands/_run.py +35 -0
- qod_cli/commands/auth.py +67 -0
- qod_cli/commands/catalog.py +173 -0
- qod_cli/commands/config_cmd.py +17 -0
- qod_cli/commands/database.py +69 -0
- qod_cli/commands/federation.py +82 -0
- qod_cli/commands/group.py +28 -0
- qod_cli/commands/maintenance.py +81 -0
- qod_cli/commands/manifest.py +34 -0
- qod_cli/commands/membership.py +50 -0
- qod_cli/commands/node.py +58 -0
- qod_cli/commands/pool.py +137 -0
- qod_cli/commands/role.py +150 -0
- qod_cli/commands/sql_cmd.py +86 -0
- qod_cli/commands/tag.py +47 -0
- qod_cli/commands/telemetry.py +95 -0
- qod_cli/commands/tenant.py +61 -0
- qod_cli/commands/user.py +62 -0
- qod_cli/config.py +107 -0
- qod_cli/main.py +96 -0
- qod_cli/output.py +66 -0
- qod_cli/rest.py +73 -0
- qod_cli/sql.py +80 -0
- qod_cli-0.3.7.dist-info/METADATA +17 -0
- qod_cli-0.3.7.dist-info/RECORD +29 -0
- qod_cli-0.3.7.dist-info/WHEEL +4 -0
- qod_cli-0.3.7.dist-info/entry_points.txt +2 -0
qod_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.7"
|
|
File without changes
|
qod_cli/commands/_run.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ..output import render
|
|
8
|
+
from ..rest import ApiError, RestClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def call(
|
|
12
|
+
ctx: typer.Context,
|
|
13
|
+
method: str,
|
|
14
|
+
path: str,
|
|
15
|
+
params: dict | None = None,
|
|
16
|
+
body: Any | None = None,
|
|
17
|
+
text: bool = False,
|
|
18
|
+
) -> Any:
|
|
19
|
+
try:
|
|
20
|
+
data = RestClient(ctx.obj.settings).request(method, path, params=params, body=body, text=text)
|
|
21
|
+
except ApiError as exc:
|
|
22
|
+
typer.echo(f"error: {exc}", err=True)
|
|
23
|
+
raise typer.Exit(1)
|
|
24
|
+
render(data, ctx.obj.json_output)
|
|
25
|
+
return data
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def kv_pairs(values: list[str] | None) -> dict:
|
|
29
|
+
out: dict = {}
|
|
30
|
+
for item in values or []:
|
|
31
|
+
if "=" not in item:
|
|
32
|
+
raise typer.BadParameter(f"expected KEY=VALUE, got {item!r}")
|
|
33
|
+
key, _, value = item.partition("=")
|
|
34
|
+
out[key] = value
|
|
35
|
+
return out
|
qod_cli/commands/auth.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from urllib.parse import urlparse
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ..config import save_profile
|
|
6
|
+
from ..output import render
|
|
7
|
+
from ..rest import ApiError, RestClient
|
|
8
|
+
from ._run import call
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="Authentication mode discovery.")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command()
|
|
14
|
+
def mode(ctx: typer.Context):
|
|
15
|
+
"""Show the auth mode (db or oidc) the manager expects."""
|
|
16
|
+
call(ctx, "GET", "/api/auth/mode")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def login(
|
|
20
|
+
ctx: typer.Context,
|
|
21
|
+
url: str = typer.Option(None, "--url", help="Manager URL; defaults to the profile value."),
|
|
22
|
+
username: str = typer.Option("admin", "--username"),
|
|
23
|
+
tenant: str = typer.Option(None, "--tenant", help="Tenant for tenant-scoped admins."),
|
|
24
|
+
):
|
|
25
|
+
"""Mint a session, store it and the edge settings in the active profile."""
|
|
26
|
+
settings = ctx.obj.settings
|
|
27
|
+
manager_url = url or settings.manager_url
|
|
28
|
+
password = typer.prompt("Password", hide_input=True)
|
|
29
|
+
settings.manager_url = manager_url
|
|
30
|
+
client = RestClient(settings)
|
|
31
|
+
body = {"username": username, "password": password}
|
|
32
|
+
if tenant is not None:
|
|
33
|
+
body["tenant"] = tenant
|
|
34
|
+
try:
|
|
35
|
+
login_resp = client.request("POST", "/api/auth/login", body=body)
|
|
36
|
+
edge = client.request("GET", "/api/config/client")
|
|
37
|
+
except ApiError as exc:
|
|
38
|
+
typer.echo(f"error: {exc}", err=True)
|
|
39
|
+
raise typer.Exit(1)
|
|
40
|
+
edge_host = edge.get("flightSqlHost", "")
|
|
41
|
+
if edge_host in ("", "0.0.0.0"):
|
|
42
|
+
edge_host = urlparse(manager_url).hostname or "localhost"
|
|
43
|
+
save_profile(
|
|
44
|
+
ctx.obj.profile,
|
|
45
|
+
{
|
|
46
|
+
"manager_url": manager_url,
|
|
47
|
+
"token": login_resp["token"],
|
|
48
|
+
"sql_user": username,
|
|
49
|
+
"edge_host": edge_host,
|
|
50
|
+
"edge_port": edge.get("flightSqlPort", 31338),
|
|
51
|
+
"edge_tls": edge.get("flightSqlTls", True),
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
render(
|
|
55
|
+
{"username": login_resp.get("username", username), "profile": ctx.obj.profile},
|
|
56
|
+
ctx.obj.json_output,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def logout(ctx: typer.Context):
|
|
61
|
+
"""Revoke the current session token."""
|
|
62
|
+
call(ctx, "POST", "/api/auth/logout")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def whoami(ctx: typer.Context):
|
|
66
|
+
"""Verify the current session."""
|
|
67
|
+
call(ctx, "GET", "/api/auth/whoami")
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from ._run import call
|
|
4
|
+
|
|
5
|
+
app = typer.Typer(help="DuckLake catalog browsing, time travel, and recovery.")
|
|
6
|
+
|
|
7
|
+
TENANT = typer.Argument(..., metavar="TENANT")
|
|
8
|
+
DB = typer.Argument(..., metavar="DB")
|
|
9
|
+
SCHEMA = typer.Argument(..., metavar="SCHEMA")
|
|
10
|
+
TABLE = typer.Argument(..., metavar="TABLE")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _base(tenant: str, db: str) -> str:
|
|
14
|
+
return f"/api/catalog/tenant/{tenant}/database/{db}"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _at_most_one(**selectors) -> None:
|
|
18
|
+
given = [name for name, value in selectors.items() if value is not None]
|
|
19
|
+
if len(given) > 1:
|
|
20
|
+
raise typer.BadParameter(f"{' and '.join(given)} are mutually exclusive")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command()
|
|
24
|
+
def schemas(ctx: typer.Context, tenant: str = TENANT, db: str = DB):
|
|
25
|
+
call(ctx, "GET", f"{_base(tenant, db)}/schemas")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command()
|
|
29
|
+
def tables(ctx: typer.Context, tenant: str = TENANT, db: str = DB, schema: str = SCHEMA):
|
|
30
|
+
call(ctx, "GET", f"{_base(tenant, db)}/schemas/{schema}/tables")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.command()
|
|
34
|
+
def describe(
|
|
35
|
+
ctx: typer.Context,
|
|
36
|
+
tenant: str = TENANT,
|
|
37
|
+
db: str = DB,
|
|
38
|
+
schema: str = SCHEMA,
|
|
39
|
+
table: str = TABLE,
|
|
40
|
+
as_of: int = typer.Option(None, "--as-of", help="Snapshot id."),
|
|
41
|
+
as_of_tag: str = typer.Option(None, "--as-of-tag"),
|
|
42
|
+
as_of_ts: str = typer.Option(None, "--as-of-ts", help="ISO timestamp."),
|
|
43
|
+
):
|
|
44
|
+
_at_most_one(as_of=as_of, as_of_tag=as_of_tag, as_of_ts=as_of_ts)
|
|
45
|
+
call(
|
|
46
|
+
ctx,
|
|
47
|
+
"GET",
|
|
48
|
+
f"{_base(tenant, db)}/schemas/{schema}/tables/{table}",
|
|
49
|
+
params={"asOf": as_of, "asOfTag": as_of_tag, "asOfTs": as_of_ts},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command()
|
|
54
|
+
def snapshots(
|
|
55
|
+
ctx: typer.Context,
|
|
56
|
+
tenant: str = TENANT,
|
|
57
|
+
db: str = DB,
|
|
58
|
+
limit: int = typer.Option(None, "--limit"),
|
|
59
|
+
before: int = typer.Option(None, "--before", help="Keyset pagination: snapshot id."),
|
|
60
|
+
table: str = typer.Option(None, "--table", help="schema.table filter."),
|
|
61
|
+
):
|
|
62
|
+
call(ctx, "GET", f"{_base(tenant, db)}/snapshots", params={"limit": limit, "before": before, "table": table})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command()
|
|
66
|
+
def history(
|
|
67
|
+
ctx: typer.Context,
|
|
68
|
+
tenant: str = TENANT,
|
|
69
|
+
db: str = DB,
|
|
70
|
+
schema: str = SCHEMA,
|
|
71
|
+
table: str = TABLE,
|
|
72
|
+
limit: int = typer.Option(None, "--limit"),
|
|
73
|
+
before: int = typer.Option(None, "--before"),
|
|
74
|
+
from_: str = typer.Option(None, "--from"),
|
|
75
|
+
to: str = typer.Option(None, "--to"),
|
|
76
|
+
operation: str = typer.Option(None, "--operation"),
|
|
77
|
+
author: str = typer.Option(None, "--author"),
|
|
78
|
+
):
|
|
79
|
+
call(
|
|
80
|
+
ctx,
|
|
81
|
+
"GET",
|
|
82
|
+
f"{_base(tenant, db)}/schemas/{schema}/tables/{table}/history",
|
|
83
|
+
params={"limit": limit, "before": before, "from": from_, "to": to, "operation": operation, "author": author},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command()
|
|
88
|
+
def preview(
|
|
89
|
+
ctx: typer.Context,
|
|
90
|
+
tenant: str = TENANT,
|
|
91
|
+
db: str = DB,
|
|
92
|
+
schema: str = SCHEMA,
|
|
93
|
+
table: str = TABLE,
|
|
94
|
+
as_of: int = typer.Option(None, "--as-of"),
|
|
95
|
+
as_of_tag: str = typer.Option(None, "--as-of-tag"),
|
|
96
|
+
as_of_ts: str = typer.Option(None, "--as-of-ts"),
|
|
97
|
+
limit: int = typer.Option(None, "--limit"),
|
|
98
|
+
):
|
|
99
|
+
_at_most_one(as_of=as_of, as_of_tag=as_of_tag, as_of_ts=as_of_ts)
|
|
100
|
+
call(
|
|
101
|
+
ctx,
|
|
102
|
+
"GET",
|
|
103
|
+
f"{_base(tenant, db)}/schemas/{schema}/tables/{table}/preview",
|
|
104
|
+
params={"asOf": as_of, "asOfTag": as_of_tag, "asOfTs": as_of_ts, "limit": limit},
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.command("data-diff")
|
|
109
|
+
def data_diff(
|
|
110
|
+
ctx: typer.Context,
|
|
111
|
+
tenant: str = TENANT,
|
|
112
|
+
db: str = DB,
|
|
113
|
+
schema: str = SCHEMA,
|
|
114
|
+
table: str = TABLE,
|
|
115
|
+
from_: str = typer.Option(..., "--from", help="From snapshot selector."),
|
|
116
|
+
to: str = typer.Option(..., "--to", help="To snapshot selector."),
|
|
117
|
+
limit: int = typer.Option(None, "--limit"),
|
|
118
|
+
cursor: str = typer.Option(None, "--cursor"),
|
|
119
|
+
change_type: str = typer.Option(None, "--change-type"),
|
|
120
|
+
):
|
|
121
|
+
call(
|
|
122
|
+
ctx,
|
|
123
|
+
"GET",
|
|
124
|
+
f"{_base(tenant, db)}/schemas/{schema}/tables/{table}/data-diff",
|
|
125
|
+
params={"from": from_, "to": to, "limit": limit, "cursor": cursor, "changeType": change_type},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@app.command("schema-diff")
|
|
130
|
+
def schema_diff(
|
|
131
|
+
ctx: typer.Context,
|
|
132
|
+
tenant: str = TENANT,
|
|
133
|
+
db: str = DB,
|
|
134
|
+
schema: str = SCHEMA,
|
|
135
|
+
table: str = TABLE,
|
|
136
|
+
from_: str = typer.Option(..., "--from"),
|
|
137
|
+
to: str = typer.Option(..., "--to"),
|
|
138
|
+
):
|
|
139
|
+
call(
|
|
140
|
+
ctx,
|
|
141
|
+
"GET",
|
|
142
|
+
f"{_base(tenant, db)}/schemas/{schema}/tables/{table}/schema-diff",
|
|
143
|
+
params={"from": from_, "to": to},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@app.command()
|
|
148
|
+
def recoverable(
|
|
149
|
+
ctx: typer.Context,
|
|
150
|
+
tenant: str = TENANT,
|
|
151
|
+
db: str = DB,
|
|
152
|
+
limit: int = typer.Option(None, "--limit"),
|
|
153
|
+
):
|
|
154
|
+
"""Dropped tables still recoverable via undrop."""
|
|
155
|
+
call(ctx, "GET", f"{_base(tenant, db)}/recoverable", params={"limit": limit})
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@app.command()
|
|
159
|
+
def undrop(
|
|
160
|
+
ctx: typer.Context,
|
|
161
|
+
tenant: str = typer.Option(..., "--tenant"),
|
|
162
|
+
db: str = typer.Option(..., "--db"),
|
|
163
|
+
schema: str = typer.Option(..., "--schema"),
|
|
164
|
+
table: str = typer.Option(..., "--table"),
|
|
165
|
+
as_name: str = typer.Option(None, "--as-name", help="Recover under a different name."),
|
|
166
|
+
from_snapshot: int = typer.Option(None, "--from-snapshot"),
|
|
167
|
+
):
|
|
168
|
+
body: dict = {"tenant": tenant, "tenantDb": db, "schema": schema, "table": table}
|
|
169
|
+
if as_name is not None:
|
|
170
|
+
body["asName"] = as_name
|
|
171
|
+
if from_snapshot is not None:
|
|
172
|
+
body["fromSnapshot"] = from_snapshot
|
|
173
|
+
call(ctx, "POST", "/api/catalog/undrop", body=body)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from ._run import call
|
|
4
|
+
|
|
5
|
+
app = typer.Typer(help="Server-published configuration.")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@app.command()
|
|
9
|
+
def client(ctx: typer.Context):
|
|
10
|
+
"""Edge host/port/TLS for client bootstrapping (open endpoint)."""
|
|
11
|
+
call(ctx, "GET", "/api/config/client")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command()
|
|
15
|
+
def server(ctx: typer.Context):
|
|
16
|
+
"""Effective manager configuration."""
|
|
17
|
+
call(ctx, "GET", "/api/config/server")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from ._run import call, kv_pairs
|
|
4
|
+
|
|
5
|
+
app = typer.Typer(help="Tenant databases (DuckLake catalogs).")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@app.command("list")
|
|
9
|
+
def list_(ctx: typer.Context, tenant: str = typer.Option(..., "--tenant")):
|
|
10
|
+
call(ctx, "GET", "/api/database/list", params={"tenant": tenant})
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command()
|
|
14
|
+
def create(
|
|
15
|
+
ctx: typer.Context,
|
|
16
|
+
tenant: str = typer.Option(..., "--tenant"),
|
|
17
|
+
name: str = typer.Option(..., "--name", help="Suffix; server composes <tenant>_<name>."),
|
|
18
|
+
kind: str = typer.Option("ducklake", "--kind", help="ducklake|duckdb-file|memory"),
|
|
19
|
+
metastore: list[str] = typer.Option(None, "--metastore", help="KEY=VALUE, repeatable."),
|
|
20
|
+
data_path: str = typer.Option("", "--data-path"),
|
|
21
|
+
object_store: list[str] = typer.Option(None, "--object-store", help="KEY=VALUE, repeatable."),
|
|
22
|
+
default_database: str = typer.Option(None, "--default-database"),
|
|
23
|
+
default_schema: str = typer.Option(None, "--default-schema"),
|
|
24
|
+
init_sql: str = typer.Option("", "--init-sql"),
|
|
25
|
+
):
|
|
26
|
+
body = {
|
|
27
|
+
"tenant": tenant,
|
|
28
|
+
"name": name,
|
|
29
|
+
"kind": kind,
|
|
30
|
+
"metastore": kv_pairs(metastore),
|
|
31
|
+
"dataPath": data_path,
|
|
32
|
+
"objectStore": kv_pairs(object_store),
|
|
33
|
+
"initSql": init_sql,
|
|
34
|
+
}
|
|
35
|
+
if default_database is not None:
|
|
36
|
+
body["defaultDatabase"] = default_database
|
|
37
|
+
if default_schema is not None:
|
|
38
|
+
body["defaultSchema"] = default_schema
|
|
39
|
+
call(ctx, "POST", "/api/database/create", body=body)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command()
|
|
43
|
+
def update(
|
|
44
|
+
ctx: typer.Context,
|
|
45
|
+
tenant: str = typer.Option(..., "--tenant"),
|
|
46
|
+
name: str = typer.Option(..., "--name"),
|
|
47
|
+
metastore: list[str] = typer.Option(None, "--metastore", help="KEY=VALUE; omit = unchanged."),
|
|
48
|
+
object_store: list[str] = typer.Option(None, "--object-store"),
|
|
49
|
+
default_database: str = typer.Option(None, "--default-database"),
|
|
50
|
+
default_schema: str = typer.Option(None, "--default-schema"),
|
|
51
|
+
init_sql: str = typer.Option(None, "--init-sql"),
|
|
52
|
+
):
|
|
53
|
+
body: dict = {"tenant": tenant, "name": name}
|
|
54
|
+
if metastore is not None:
|
|
55
|
+
body["metastore"] = kv_pairs(metastore)
|
|
56
|
+
if object_store is not None:
|
|
57
|
+
body["objectStore"] = kv_pairs(object_store)
|
|
58
|
+
if default_database is not None:
|
|
59
|
+
body["defaultDatabase"] = default_database
|
|
60
|
+
if default_schema is not None:
|
|
61
|
+
body["defaultSchema"] = default_schema
|
|
62
|
+
if init_sql is not None:
|
|
63
|
+
body["initSql"] = init_sql
|
|
64
|
+
call(ctx, "POST", "/api/database/update", body=body)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.command()
|
|
68
|
+
def delete(ctx: typer.Context, tenant: str = typer.Option(..., "--tenant"), name: str = typer.Option(..., "--name")):
|
|
69
|
+
call(ctx, "POST", "/api/database/delete", body={"tenant": tenant, "name": name})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from ._run import call
|
|
4
|
+
|
|
5
|
+
app = typer.Typer(help="Federated sources per (tenant, tenant-db).")
|
|
6
|
+
secret_app = typer.Typer(help="Secrets referenced by a federated source's setup SQL.")
|
|
7
|
+
app.add_typer(secret_app, name="secret")
|
|
8
|
+
|
|
9
|
+
TENANT = typer.Argument(..., metavar="TENANT")
|
|
10
|
+
DB = typer.Argument(..., metavar="DB")
|
|
11
|
+
ALIAS = typer.Argument(..., metavar="ALIAS")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _base(tenant: str, db: str) -> str:
|
|
15
|
+
return f"/api/tenants/{tenant}/tenant-dbs/{db}/federated-sources"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("list")
|
|
19
|
+
def list_(ctx: typer.Context, tenant: str = TENANT, db: str = DB):
|
|
20
|
+
call(ctx, "GET", _base(tenant, db))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command()
|
|
24
|
+
def get(ctx: typer.Context, tenant: str = TENANT, db: str = DB, alias: str = ALIAS):
|
|
25
|
+
call(ctx, "GET", f"{_base(tenant, db)}/{alias}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command()
|
|
29
|
+
def create(
|
|
30
|
+
ctx: typer.Context,
|
|
31
|
+
tenant: str = TENANT,
|
|
32
|
+
db: str = DB,
|
|
33
|
+
alias: str = typer.Option(..., "--alias"),
|
|
34
|
+
setup_sql: str = typer.Option(..., "--setup-sql"),
|
|
35
|
+
description: str = typer.Option(None, "--description"),
|
|
36
|
+
disabled: bool = typer.Option(False, "--disabled"),
|
|
37
|
+
):
|
|
38
|
+
body: dict = {"alias": alias, "setupSql": setup_sql, "disabled": disabled}
|
|
39
|
+
if description is not None:
|
|
40
|
+
body["description"] = description
|
|
41
|
+
call(ctx, "POST", _base(tenant, db), body=body)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command()
|
|
45
|
+
def delete(ctx: typer.Context, tenant: str = TENANT, db: str = DB, alias: str = ALIAS):
|
|
46
|
+
call(ctx, "DELETE", f"{_base(tenant, db)}/{alias}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@secret_app.command("list")
|
|
50
|
+
def secret_list(ctx: typer.Context, tenant: str = TENANT, db: str = DB, alias: str = ALIAS):
|
|
51
|
+
call(ctx, "GET", f"{_base(tenant, db)}/{alias}/secrets")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@secret_app.command("set")
|
|
55
|
+
def secret_set(
|
|
56
|
+
ctx: typer.Context,
|
|
57
|
+
tenant: str = TENANT,
|
|
58
|
+
db: str = DB,
|
|
59
|
+
alias: str = ALIAS,
|
|
60
|
+
name: str = typer.Option(..., "--name"),
|
|
61
|
+
value: str = typer.Option(None, "--value", help="Inline value stored in Postgres."),
|
|
62
|
+
external_ref: str = typer.Option(None, "--external-ref", help="e.g. env:PGPASS."),
|
|
63
|
+
):
|
|
64
|
+
if (value is None) == (external_ref is None):
|
|
65
|
+
raise typer.BadParameter("pass exactly one of --value / --external-ref")
|
|
66
|
+
body: dict = {"name": name}
|
|
67
|
+
if value is not None:
|
|
68
|
+
body["value"] = value
|
|
69
|
+
if external_ref is not None:
|
|
70
|
+
body["externalRef"] = external_ref
|
|
71
|
+
call(ctx, "PUT", f"{_base(tenant, db)}/{alias}/secrets", body=body)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@secret_app.command("delete")
|
|
75
|
+
def secret_delete(
|
|
76
|
+
ctx: typer.Context,
|
|
77
|
+
tenant: str = TENANT,
|
|
78
|
+
db: str = DB,
|
|
79
|
+
alias: str = ALIAS,
|
|
80
|
+
name: str = typer.Argument(..., metavar="NAME"),
|
|
81
|
+
):
|
|
82
|
+
call(ctx, "DELETE", f"{_base(tenant, db)}/{alias}/secrets/{name}")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from ._run import call
|
|
4
|
+
|
|
5
|
+
app = typer.Typer(help="Groups.")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@app.command("list")
|
|
9
|
+
def list_(ctx: typer.Context, tenant: str = typer.Option(..., "--tenant")):
|
|
10
|
+
call(ctx, "GET", "/api/group/list", params={"tenant": tenant})
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command()
|
|
14
|
+
def create(
|
|
15
|
+
ctx: typer.Context,
|
|
16
|
+
tenant: str = typer.Option(..., "--tenant"),
|
|
17
|
+
name: str = typer.Option(..., "--name"),
|
|
18
|
+
description: str = typer.Option(None, "--description"),
|
|
19
|
+
):
|
|
20
|
+
body: dict = {"tenant": tenant, "name": name}
|
|
21
|
+
if description is not None:
|
|
22
|
+
body["description"] = description
|
|
23
|
+
call(ctx, "POST", "/api/group/create", body=body)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command()
|
|
27
|
+
def delete(ctx: typer.Context, group_id: str = typer.Argument(..., metavar="ID")):
|
|
28
|
+
call(ctx, "POST", "/api/group/delete", body={"id": group_id})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from ._run import call
|
|
4
|
+
|
|
5
|
+
app = typer.Typer(help="Managed maintenance policies and runs.")
|
|
6
|
+
|
|
7
|
+
TENANT = typer.Option(..., "--tenant")
|
|
8
|
+
DB = typer.Option(..., "--db")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@app.command()
|
|
12
|
+
def policy(ctx: typer.Context, tenant: str = TENANT, db: str = DB):
|
|
13
|
+
call(ctx, "GET", "/api/maintenance/policy", params={"tenant": tenant, "tenantDb": db})
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command("policy-upsert")
|
|
17
|
+
def policy_upsert(
|
|
18
|
+
ctx: typer.Context,
|
|
19
|
+
tenant: str = TENANT,
|
|
20
|
+
db: str = DB,
|
|
21
|
+
scope_kind: str = typer.Option(..., "--scope-kind", help="tenantdb|schema|table"),
|
|
22
|
+
scope_schema: str = typer.Option(None, "--scope-schema"),
|
|
23
|
+
scope_table: str = typer.Option(None, "--scope-table"),
|
|
24
|
+
enabled: bool = typer.Option(None, "--enabled/--disabled"),
|
|
25
|
+
retention_days: int = typer.Option(None, "--retention-days"),
|
|
26
|
+
compaction_enabled: bool = typer.Option(None, "--compaction-enabled/--compaction-disabled"),
|
|
27
|
+
target_file_size: str = typer.Option(None, "--target-file-size"),
|
|
28
|
+
small_file_min_count: int = typer.Option(None, "--small-file-min-count"),
|
|
29
|
+
rewrite_delete_threshold: float = typer.Option(None, "--rewrite-delete-threshold"),
|
|
30
|
+
cleanup_grace_days: int = typer.Option(None, "--cleanup-grace-days"),
|
|
31
|
+
orphan_min_age_days: int = typer.Option(None, "--orphan-min-age-days"),
|
|
32
|
+
cron: str = typer.Option(None, "--cron"),
|
|
33
|
+
):
|
|
34
|
+
body: dict = {"tenant": tenant, "tenantDb": db, "scopeKind": scope_kind}
|
|
35
|
+
optional = {
|
|
36
|
+
"scopeSchema": scope_schema,
|
|
37
|
+
"scopeTable": scope_table,
|
|
38
|
+
"enabled": enabled,
|
|
39
|
+
"retentionDays": retention_days,
|
|
40
|
+
"compactionEnabled": compaction_enabled,
|
|
41
|
+
"targetFileSize": target_file_size,
|
|
42
|
+
"smallFileMinCount": small_file_min_count,
|
|
43
|
+
"rewriteDeleteThreshold": rewrite_delete_threshold,
|
|
44
|
+
"cleanupGraceDays": cleanup_grace_days,
|
|
45
|
+
"orphanMinAgeDays": orphan_min_age_days,
|
|
46
|
+
"cron": cron,
|
|
47
|
+
}
|
|
48
|
+
body.update({k: v for k, v in optional.items() if v is not None})
|
|
49
|
+
call(ctx, "POST", "/api/maintenance/policy/upsert", body=body)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command("policy-delete")
|
|
53
|
+
def policy_delete(ctx: typer.Context, policy_id: str = typer.Argument(..., metavar="ID")):
|
|
54
|
+
call(ctx, "POST", "/api/maintenance/policy/delete", body={"id": policy_id})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command()
|
|
58
|
+
def run(
|
|
59
|
+
ctx: typer.Context,
|
|
60
|
+
tenant: str = TENANT,
|
|
61
|
+
db: str = DB,
|
|
62
|
+
scope: str = typer.Option(None, "--scope"),
|
|
63
|
+
operations: str = typer.Option(None, "--operations", help="Comma-separated."),
|
|
64
|
+
):
|
|
65
|
+
body: dict = {"tenant": tenant, "tenantDb": db}
|
|
66
|
+
if scope is not None:
|
|
67
|
+
body["scope"] = scope
|
|
68
|
+
if operations is not None:
|
|
69
|
+
body["operations"] = operations
|
|
70
|
+
call(ctx, "POST", "/api/maintenance/run", body=body)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command()
|
|
74
|
+
def runs(
|
|
75
|
+
ctx: typer.Context,
|
|
76
|
+
tenant: str = TENANT,
|
|
77
|
+
db: str = DB,
|
|
78
|
+
limit: int = typer.Option(None, "--limit"),
|
|
79
|
+
before: int = typer.Option(None, "--before"),
|
|
80
|
+
):
|
|
81
|
+
call(ctx, "GET", "/api/maintenance/runs", params={"tenant": tenant, "tenantDb": db, "limit": limit, "before": before})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from ..rest import ApiError, RestClient
|
|
4
|
+
from ._run import call
|
|
5
|
+
|
|
6
|
+
app = typer.Typer(help="Topology manifest export/import (YAML).")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@app.command()
|
|
10
|
+
def export(
|
|
11
|
+
ctx: typer.Context,
|
|
12
|
+
out: str = typer.Option(None, "--out", help="File path; omit = stdout."),
|
|
13
|
+
):
|
|
14
|
+
"""Export the whole control-plane topology as YAML."""
|
|
15
|
+
try:
|
|
16
|
+
text = RestClient(ctx.obj.settings).request("GET", "/api/manifest/export", text=True)
|
|
17
|
+
except ApiError as exc:
|
|
18
|
+
typer.echo(f"error: {exc}", err=True)
|
|
19
|
+
raise typer.Exit(1)
|
|
20
|
+
if out is None:
|
|
21
|
+
typer.echo(text, nl=False)
|
|
22
|
+
else:
|
|
23
|
+
with open(out, "w") as f:
|
|
24
|
+
f.write(text)
|
|
25
|
+
typer.echo(f"wrote {out}", err=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command("import")
|
|
29
|
+
def import_(
|
|
30
|
+
ctx: typer.Context,
|
|
31
|
+
file: typer.FileText = typer.Argument(..., help="Manifest YAML file, or - for stdin."),
|
|
32
|
+
):
|
|
33
|
+
"""Import a topology manifest (YAML) into the control plane."""
|
|
34
|
+
call(ctx, "POST", "/api/manifest/import", body=file.read())
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from ._run import call
|
|
4
|
+
|
|
5
|
+
app = typer.Typer(help="RBAC membership edges.")
|
|
6
|
+
user_role_app = typer.Typer(help="User to role memberships.")
|
|
7
|
+
user_group_app = typer.Typer(help="User to group memberships.")
|
|
8
|
+
group_role_app = typer.Typer(help="Group to role memberships.")
|
|
9
|
+
app.add_typer(user_role_app, name="user-role")
|
|
10
|
+
app.add_typer(user_group_app, name="user-group")
|
|
11
|
+
app.add_typer(group_role_app, name="group-role")
|
|
12
|
+
|
|
13
|
+
USER = typer.Option(..., "--user-id")
|
|
14
|
+
ROLE = typer.Option(..., "--role-id")
|
|
15
|
+
GROUP = typer.Option(..., "--group-id")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@user_role_app.command()
|
|
19
|
+
def add(ctx: typer.Context, user_id: str = USER, role_id: str = ROLE):
|
|
20
|
+
call(ctx, "POST", "/api/membership/user-role/add", body={"userId": user_id, "roleId": role_id})
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@user_role_app.command()
|
|
24
|
+
def remove(ctx: typer.Context, user_id: str = USER, role_id: str = ROLE):
|
|
25
|
+
call(ctx, "POST", "/api/membership/user-role/remove", body={"userId": user_id, "roleId": role_id})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@user_group_app.command("add")
|
|
29
|
+
def ug_add(ctx: typer.Context, user_id: str = USER, group_id: str = GROUP):
|
|
30
|
+
call(ctx, "POST", "/api/membership/user-group/add", body={"userId": user_id, "groupId": group_id})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@user_group_app.command("remove")
|
|
34
|
+
def ug_remove(ctx: typer.Context, user_id: str = USER, group_id: str = GROUP):
|
|
35
|
+
call(ctx, "POST", "/api/membership/user-group/remove", body={"userId": user_id, "groupId": group_id})
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@group_role_app.command("add")
|
|
39
|
+
def gr_add(ctx: typer.Context, group_id: str = GROUP, role_id: str = ROLE):
|
|
40
|
+
call(ctx, "POST", "/api/membership/group-role/add", body={"groupId": group_id, "roleId": role_id})
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@group_role_app.command("remove")
|
|
44
|
+
def gr_remove(ctx: typer.Context, group_id: str = GROUP, role_id: str = ROLE):
|
|
45
|
+
call(ctx, "POST", "/api/membership/group-role/remove", body={"groupId": group_id, "roleId": role_id})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@group_role_app.command("list")
|
|
49
|
+
def gr_list(ctx: typer.Context, group_id: str = GROUP):
|
|
50
|
+
call(ctx, "GET", "/api/membership/group-role/list", params={"groupId": group_id})
|