ocmo-cli 0.8.19__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.
- LICENSE +201 -0
- NOTICE +16 -0
- ocmo_cli/__init__.py +3 -0
- ocmo_cli/__main__.py +4 -0
- ocmo_cli/_address.py +182 -0
- ocmo_cli/_audit_item.py +33 -0
- ocmo_cli/_auth_resolve.py +66 -0
- ocmo_cli/_body_payload.py +203 -0
- ocmo_cli/_click_groups.py +227 -0
- ocmo_cli/_client.py +351 -0
- ocmo_cli/_client_commands.py +35 -0
- ocmo_cli/_command_registry.py +29 -0
- ocmo_cli/_commands_map.py +65 -0
- ocmo_cli/_config.py +183 -0
- ocmo_cli/_config_content.py +43 -0
- ocmo_cli/_delete_item_output.py +129 -0
- ocmo_cli/_diff_input.py +144 -0
- ocmo_cli/_dry_run.py +292 -0
- ocmo_cli/_errors.py +151 -0
- ocmo_cli/_exit.py +13 -0
- ocmo_cli/_export_metadata.py +47 -0
- ocmo_cli/_generated_registry.py +45 -0
- ocmo_cli/_get_cast_output.py +52 -0
- ocmo_cli/_get_item_list.py +23 -0
- ocmo_cli/_globalpermission_output.py +98 -0
- ocmo_cli/_import_metadata.py +140 -0
- ocmo_cli/_import_plan.py +221 -0
- ocmo_cli/_item_output.py +223 -0
- ocmo_cli/_lazy_mixin.py +29 -0
- ocmo_cli/_ls_wide.py +286 -0
- ocmo_cli/_mutating.py +39 -0
- ocmo_cli/_notice.py +17 -0
- ocmo_cli/_options.py +77 -0
- ocmo_cli/_output.py +424 -0
- ocmo_cli/_output_manifest.py +332 -0
- ocmo_cli/_resolve_draft.py +50 -0
- ocmo_cli/_resolve_options.py +143 -0
- ocmo_cli/_resolve_output.py +228 -0
- ocmo_cli/_resolve_parameters_output.py +72 -0
- ocmo_cli/_resolve_series.py +89 -0
- ocmo_cli/_resolve_series_chart.py +409 -0
- ocmo_cli/_resolve_write.py +185 -0
- ocmo_cli/_resolver_output.py +34 -0
- ocmo_cli/_resource_aliases.py +22 -0
- ocmo_cli/_sdk_dispatch.py +310 -0
- ocmo_cli/_typing.py +27 -0
- ocmo_cli/_version_output.py +39 -0
- ocmo_cli/_whoami_output.py +43 -0
- ocmo_cli/commands/__init__.py +1 -0
- ocmo_cli/commands/api.py +125 -0
- ocmo_cli/commands/api_health.py +12 -0
- ocmo_cli/commands/apply.py +153 -0
- ocmo_cli/commands/auth.py +786 -0
- ocmo_cli/commands/can_i.py +51 -0
- ocmo_cli/commands/completion.py +59 -0
- ocmo_cli/commands/config_cmd.py +235 -0
- ocmo_cli/commands/describe.py +109 -0
- ocmo_cli/commands/diff.py +86 -0
- ocmo_cli/commands/edit.py +200 -0
- ocmo_cli/commands/export.py +242 -0
- ocmo_cli/commands/generated.py +907 -0
- ocmo_cli/commands/gp_move.py +108 -0
- ocmo_cli/commands/import_.py +595 -0
- ocmo_cli/commands/item_relocate.py +187 -0
- ocmo_cli/commands/ls.py +481 -0
- ocmo_cli/commands/resolve.py +658 -0
- ocmo_cli/commands/resolve_draft.py +102 -0
- ocmo_cli/commands/resolve_parameters.py +65 -0
- ocmo_cli/commands/resolve_series.py +109 -0
- ocmo_cli/commands/schema.py +112 -0
- ocmo_cli/commands/timeline.py +135 -0
- ocmo_cli/commands/version_cmd.py +102 -0
- ocmo_cli/commands/whoami.py +12 -0
- ocmo_cli/main.py +241 -0
- ocmo_cli/output_formats.yaml +295 -0
- ocmo_cli-0.8.19.dist-info/METADATA +169 -0
- ocmo_cli-0.8.19.dist-info/RECORD +81 -0
- ocmo_cli-0.8.19.dist-info/WHEEL +4 -0
- ocmo_cli-0.8.19.dist-info/entry_points.txt +2 -0
- ocmo_cli-0.8.19.dist-info/licenses/LICENSE +201 -0
- ocmo_cli-0.8.19.dist-info/licenses/NOTICE +16 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""ocmo can-i — check permissions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from .._client import OcmoCtx
|
|
10
|
+
from .._errors import sdk_command
|
|
11
|
+
from .._options import can_i_output_option, namespace_option
|
|
12
|
+
from .._output import as_dict
|
|
13
|
+
from .._output_manifest import emit_command_output
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command(
|
|
17
|
+
"can-i",
|
|
18
|
+
help="Check whether the current principal may perform one or more operations.",
|
|
19
|
+
)
|
|
20
|
+
@click.argument("operations", nargs=-1, required=True, metavar="OPERATION...")
|
|
21
|
+
@namespace_option()
|
|
22
|
+
@can_i_output_option()
|
|
23
|
+
@click.option("--resource", default=None, help="Tree path for in-namespace permission probes.")
|
|
24
|
+
@click.pass_obj
|
|
25
|
+
@sdk_command
|
|
26
|
+
def can_i_cmd(
|
|
27
|
+
ctx: OcmoCtx,
|
|
28
|
+
operations: tuple[str, ...],
|
|
29
|
+
namespace: str | None,
|
|
30
|
+
output_fmt: str | None,
|
|
31
|
+
resource: str | None,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Examples:
|
|
34
|
+
|
|
35
|
+
ocmo can-i config:read config:write -n prod
|
|
36
|
+
ocmo can-i secret:read --namespace prod --resource app/db
|
|
37
|
+
"""
|
|
38
|
+
ns = namespace or ctx.namespace
|
|
39
|
+
result = ctx.client().can_i(
|
|
40
|
+
operations=list(operations),
|
|
41
|
+
namespace=ns,
|
|
42
|
+
resource=resource,
|
|
43
|
+
)
|
|
44
|
+
data = as_dict(result)
|
|
45
|
+
emit_command_output("can-i", data, output_fmt, ctx_fmt=ctx.output)
|
|
46
|
+
allowed = data.get("allowed")
|
|
47
|
+
if isinstance(allowed, dict):
|
|
48
|
+
if not all(allowed.values()):
|
|
49
|
+
sys.exit(4)
|
|
50
|
+
elif not allowed:
|
|
51
|
+
sys.exit(4)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""ocmo completion — emit shell completion scripts.
|
|
2
|
+
|
|
3
|
+
Completion works offline (no network required).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
from click.shell_completion import get_completion_class
|
|
12
|
+
|
|
13
|
+
_SHELLS = ("bash", "zsh", "fish")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command("completion")
|
|
17
|
+
@click.argument("shell", type=click.Choice(list(_SHELLS)), required=False)
|
|
18
|
+
def completion_cmd(shell: str | None) -> None:
|
|
19
|
+
"""Emit a shell completion script.
|
|
20
|
+
|
|
21
|
+
\b
|
|
22
|
+
Usage:
|
|
23
|
+
# bash
|
|
24
|
+
source <(ocmo completion bash)
|
|
25
|
+
|
|
26
|
+
# zsh
|
|
27
|
+
source <(ocmo completion zsh)
|
|
28
|
+
|
|
29
|
+
# fish
|
|
30
|
+
ocmo completion fish | source
|
|
31
|
+
"""
|
|
32
|
+
if not shell:
|
|
33
|
+
detected = _detect_shell()
|
|
34
|
+
if detected:
|
|
35
|
+
shell = detected
|
|
36
|
+
else:
|
|
37
|
+
raise click.UsageError(f"SHELL is required. Valid options: {', '.join(_SHELLS)}")
|
|
38
|
+
|
|
39
|
+
comp_cls = get_completion_class(shell)
|
|
40
|
+
if comp_cls is None:
|
|
41
|
+
raise click.UsageError(f"Unsupported shell: {shell}")
|
|
42
|
+
|
|
43
|
+
from ocmo_cli.main import cli
|
|
44
|
+
|
|
45
|
+
env_var = f"_{_prog_name().upper()}_COMPLETE"
|
|
46
|
+
comp = comp_cls(cli, ctx_args={}, prog_name=_prog_name(), complete_var=env_var)
|
|
47
|
+
click.echo(comp.source(), nl=False)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _detect_shell() -> str | None:
|
|
51
|
+
shell_path = os.environ.get("SHELL", "")
|
|
52
|
+
for s in _SHELLS:
|
|
53
|
+
if s in shell_path:
|
|
54
|
+
return s
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _prog_name() -> str:
|
|
59
|
+
return "ocmo"
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""ocmo config — manage the local configuration file and contexts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from .._auth_resolve import ensure_auth_entry
|
|
10
|
+
from .._click_groups import ConfigPathHelpGroup
|
|
11
|
+
from .._config import (
|
|
12
|
+
Context,
|
|
13
|
+
format_config_path_note,
|
|
14
|
+
load_config,
|
|
15
|
+
save_config,
|
|
16
|
+
)
|
|
17
|
+
from .._output import err
|
|
18
|
+
|
|
19
|
+
_SETTABLE_CONTEXT_KEYS = ("server", "namespace", "token-file", "auth")
|
|
20
|
+
|
|
21
|
+
_CONFIG_HELP = """\
|
|
22
|
+
Manage local CLI configuration (server, contexts, auth).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@click.group("config", cls=ConfigPathHelpGroup, help=_CONFIG_HELP)
|
|
27
|
+
def config_group() -> None:
|
|
28
|
+
"""Manage local CLI configuration (server, contexts, auth)."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@config_group.command("view")
|
|
32
|
+
@click.option(
|
|
33
|
+
"--show-secrets", is_flag=True, default=False, help="Show credential fields (CAUTION: may expose secrets)."
|
|
34
|
+
)
|
|
35
|
+
def view_cmd(show_secrets: bool) -> None:
|
|
36
|
+
"""Print the current configuration file, redacting credentials by default."""
|
|
37
|
+
import yaml # deferred
|
|
38
|
+
|
|
39
|
+
cfg = load_config()
|
|
40
|
+
raw: dict[str, Any] = {
|
|
41
|
+
"current-context": cfg.current_context,
|
|
42
|
+
"contexts": {},
|
|
43
|
+
"auths": {},
|
|
44
|
+
}
|
|
45
|
+
for name, ctx in cfg.contexts.items():
|
|
46
|
+
raw["contexts"][name] = {
|
|
47
|
+
"server": ctx.server,
|
|
48
|
+
"namespace": ctx.namespace,
|
|
49
|
+
"auth": ctx.auth,
|
|
50
|
+
}
|
|
51
|
+
if ctx.token_file:
|
|
52
|
+
raw["contexts"][name]["token-file"] = ctx.token_file
|
|
53
|
+
|
|
54
|
+
for name, auth in cfg.auths.items():
|
|
55
|
+
entry: dict[str, Any] = {"mode": auth.mode}
|
|
56
|
+
if auth.issuer:
|
|
57
|
+
entry["issuer"] = auth.issuer
|
|
58
|
+
if auth.client_id:
|
|
59
|
+
entry["client_id"] = auth.client_id
|
|
60
|
+
if show_secrets:
|
|
61
|
+
if auth.client_secret:
|
|
62
|
+
entry["client_secret"] = auth.client_secret
|
|
63
|
+
if auth.token:
|
|
64
|
+
entry["token"] = auth.token
|
|
65
|
+
else:
|
|
66
|
+
if auth.client_secret:
|
|
67
|
+
entry["client_secret"] = "***REDACTED***"
|
|
68
|
+
if auth.token:
|
|
69
|
+
entry["token"] = "***REDACTED***"
|
|
70
|
+
if auth.client_secret_file:
|
|
71
|
+
entry["client_secret_file"] = auth.client_secret_file
|
|
72
|
+
if auth.token_file:
|
|
73
|
+
entry["token_file"] = auth.token_file
|
|
74
|
+
raw["auths"][name] = entry
|
|
75
|
+
|
|
76
|
+
print(yaml.safe_dump(raw, default_flow_style=False), end="")
|
|
77
|
+
print(f"\n# {format_config_path_note()}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@config_group.command("get-contexts")
|
|
81
|
+
def get_contexts_cmd() -> None:
|
|
82
|
+
"""List available contexts."""
|
|
83
|
+
cfg = load_config()
|
|
84
|
+
if not cfg.contexts:
|
|
85
|
+
print("No contexts configured.")
|
|
86
|
+
return
|
|
87
|
+
current = cfg.current_context
|
|
88
|
+
for name, ctx in cfg.contexts.items():
|
|
89
|
+
marker = "*" if name == current else " "
|
|
90
|
+
print(f"{marker} {name:20s} {ctx.server:40s} {ctx.namespace}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@config_group.command("current-context")
|
|
94
|
+
def current_context_cmd() -> None:
|
|
95
|
+
"""Print the name of the current context."""
|
|
96
|
+
cfg = load_config()
|
|
97
|
+
print(cfg.current_context or "(none)")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@config_group.command("current-ns")
|
|
101
|
+
def current_ns_cmd() -> None:
|
|
102
|
+
"""Print the effective namespace (OCMO_NAMESPACE or current context)."""
|
|
103
|
+
cfg = load_config()
|
|
104
|
+
print(cfg.namespace() or "(none)")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@config_group.command("use-context")
|
|
108
|
+
@click.argument("name")
|
|
109
|
+
def use_context_cmd(name: str) -> None:
|
|
110
|
+
"""Set the current context."""
|
|
111
|
+
cfg = load_config()
|
|
112
|
+
if name not in cfg.contexts:
|
|
113
|
+
available = list(cfg.contexts)
|
|
114
|
+
err(
|
|
115
|
+
f"Context {name!r} not found. "
|
|
116
|
+
+ (f"Available: {', '.join(available)}" if available else "No contexts configured.")
|
|
117
|
+
)
|
|
118
|
+
raise SystemExit(1)
|
|
119
|
+
cfg.current_context = name
|
|
120
|
+
save_config(cfg)
|
|
121
|
+
print(f"Switched to context {name!r}.")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@config_group.command("set-context")
|
|
125
|
+
@click.argument("name")
|
|
126
|
+
@click.option("--server", default=None)
|
|
127
|
+
@click.option("--namespace", default=None)
|
|
128
|
+
@click.option("--auth", default=None)
|
|
129
|
+
@click.option("--token-file", default=None)
|
|
130
|
+
def set_context_cmd(
|
|
131
|
+
name: str,
|
|
132
|
+
server: str | None,
|
|
133
|
+
namespace: str | None,
|
|
134
|
+
auth: str | None,
|
|
135
|
+
token_file: str | None,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Create or update a named context."""
|
|
138
|
+
cfg = load_config()
|
|
139
|
+
ctx = cfg.contexts.get(name, Context())
|
|
140
|
+
if server:
|
|
141
|
+
ctx.server = server
|
|
142
|
+
if namespace:
|
|
143
|
+
ctx.namespace = namespace
|
|
144
|
+
if auth:
|
|
145
|
+
ctx.auth = auth
|
|
146
|
+
ensure_auth_entry(cfg, auth)
|
|
147
|
+
if token_file:
|
|
148
|
+
ctx.token_file = token_file
|
|
149
|
+
cfg.contexts[name] = ctx
|
|
150
|
+
save_config(cfg)
|
|
151
|
+
print(f"Context {name!r} updated.")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@config_group.command("set-auth")
|
|
155
|
+
@click.argument("name")
|
|
156
|
+
@click.option("--mode", default=None, help="Auth mode (default: oidc).")
|
|
157
|
+
@click.option("--issuer", default=None, help="OIDC issuer URL.")
|
|
158
|
+
@click.option("--client-id", default=None, help="OIDC client ID.")
|
|
159
|
+
@click.option("--client-secret", default=None, help="OIDC client secret (inline).")
|
|
160
|
+
@click.option("--client-secret-file", default=None, help="Path to OIDC client secret file.")
|
|
161
|
+
@click.option("--token", default=None, help="Bearer or resolver token (inline).")
|
|
162
|
+
@click.option("--token-file", default=None, help="Path to bearer/resolver token file.")
|
|
163
|
+
def set_auth_cmd(
|
|
164
|
+
name: str,
|
|
165
|
+
mode: str | None,
|
|
166
|
+
issuer: str | None,
|
|
167
|
+
client_id: str | None,
|
|
168
|
+
client_secret: str | None,
|
|
169
|
+
client_secret_file: str | None,
|
|
170
|
+
token: str | None,
|
|
171
|
+
token_file: str | None,
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Create or update a named auth block.
|
|
174
|
+
|
|
175
|
+
``issuer`` and ``client_id`` are optional in the config file; at runtime they
|
|
176
|
+
are resolved from environment variables or ``GET /api/version`` for the context
|
|
177
|
+
server when unset.
|
|
178
|
+
"""
|
|
179
|
+
cfg = load_config()
|
|
180
|
+
entry = ensure_auth_entry(cfg, name, mode=mode or "oidc")
|
|
181
|
+
if mode:
|
|
182
|
+
entry.mode = mode
|
|
183
|
+
if issuer is not None:
|
|
184
|
+
entry.issuer = issuer
|
|
185
|
+
if client_id is not None:
|
|
186
|
+
entry.client_id = client_id
|
|
187
|
+
if client_secret is not None:
|
|
188
|
+
entry.client_secret = client_secret
|
|
189
|
+
if client_secret_file is not None:
|
|
190
|
+
entry.client_secret_file = client_secret_file
|
|
191
|
+
if token is not None:
|
|
192
|
+
entry.token = token
|
|
193
|
+
if token_file is not None:
|
|
194
|
+
entry.token_file = token_file
|
|
195
|
+
cfg.auths[name] = entry
|
|
196
|
+
save_config(cfg)
|
|
197
|
+
print(f"Auth {name!r} updated.")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@config_group.command("set")
|
|
201
|
+
@click.argument("key", type=click.Choice(list(_SETTABLE_CONTEXT_KEYS)))
|
|
202
|
+
@click.argument("value")
|
|
203
|
+
def set_cmd(key: str, value: str) -> None:
|
|
204
|
+
"""Set a field in the current context.
|
|
205
|
+
|
|
206
|
+
\b
|
|
207
|
+
Supported keys:
|
|
208
|
+
server — API base URL
|
|
209
|
+
namespace — default namespace for -n
|
|
210
|
+
token-file — path to a resolver or bearer token file
|
|
211
|
+
auth — name of an auths: entry to attach
|
|
212
|
+
"""
|
|
213
|
+
cfg = load_config()
|
|
214
|
+
if not cfg.current_context:
|
|
215
|
+
# Create a default context
|
|
216
|
+
cfg.current_context = "default"
|
|
217
|
+
if "default" not in cfg.contexts:
|
|
218
|
+
cfg.contexts["default"] = Context()
|
|
219
|
+
|
|
220
|
+
ctx_name = cfg.current_context
|
|
221
|
+
ctx = cfg.contexts.get(ctx_name, Context())
|
|
222
|
+
|
|
223
|
+
if key == "server":
|
|
224
|
+
ctx.server = value
|
|
225
|
+
elif key == "namespace":
|
|
226
|
+
ctx.namespace = value
|
|
227
|
+
elif key == "token-file":
|
|
228
|
+
ctx.token_file = value
|
|
229
|
+
elif key == "auth":
|
|
230
|
+
ctx.auth = value
|
|
231
|
+
ensure_auth_entry(cfg, value)
|
|
232
|
+
|
|
233
|
+
cfg.contexts[ctx_name] = ctx
|
|
234
|
+
save_config(cfg)
|
|
235
|
+
print(f"Set {key} = {value!r} in context {ctx_name!r}.")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""ocmo describe — read or set an item's description.
|
|
2
|
+
|
|
3
|
+
Read: no --description flag → fetch and print description to stdout.
|
|
4
|
+
Write: --description flag → POST description to the API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from .._address import parse_address_or_exit
|
|
15
|
+
from .._client import OcmoCtx
|
|
16
|
+
from .._errors import sdk_command
|
|
17
|
+
from .._mutating import run_mutating
|
|
18
|
+
from .._options import namespace_option, yes_option
|
|
19
|
+
from .._output import as_dict
|
|
20
|
+
|
|
21
|
+
_DESCRIBE_HELP = """\
|
|
22
|
+
Read or set the Markdown description of a tree item.
|
|
23
|
+
|
|
24
|
+
Without --description, prints the current description. With --description, replaces it.
|
|
25
|
+
|
|
26
|
+
\b
|
|
27
|
+
Examples:
|
|
28
|
+
ocmo -n prod describe app/web
|
|
29
|
+
ocmo -n prod describe app/web --description "Production web config"
|
|
30
|
+
ocmo -n prod describe app/web --description ./NOTES.md
|
|
31
|
+
echo "My description" | ocmo -n prod describe app/web --description -
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def resolve_description_value(value: str) -> str:
|
|
36
|
+
"""Resolve --description to text from a literal, stdin, or file path."""
|
|
37
|
+
if value == "-":
|
|
38
|
+
return sys.stdin.read()
|
|
39
|
+
if value.startswith("/") or value.startswith("."):
|
|
40
|
+
return Path(value).read_text(encoding="utf-8")
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@click.command("describe", help=_DESCRIBE_HELP)
|
|
45
|
+
@click.argument("address")
|
|
46
|
+
@click.option(
|
|
47
|
+
"--description",
|
|
48
|
+
"description_value",
|
|
49
|
+
default=None,
|
|
50
|
+
metavar="TEXT|-|PATH",
|
|
51
|
+
help=("New description text, '-' for stdin, or a file path starting with '/' or '.'."),
|
|
52
|
+
)
|
|
53
|
+
@namespace_option()
|
|
54
|
+
@yes_option(help="Skip overwrite confirmation.")
|
|
55
|
+
@click.pass_obj
|
|
56
|
+
@sdk_command
|
|
57
|
+
def describe_cmd(
|
|
58
|
+
ctx: OcmoCtx,
|
|
59
|
+
address: str,
|
|
60
|
+
description_value: str | None,
|
|
61
|
+
namespace: str | None,
|
|
62
|
+
yes: bool,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Read or set the description of a tree item.
|
|
65
|
+
|
|
66
|
+
\b
|
|
67
|
+
Read (no --description):
|
|
68
|
+
ocmo -n prod describe app/web
|
|
69
|
+
|
|
70
|
+
Write (--description):
|
|
71
|
+
ocmo -n prod describe app/web --description "Notes for operators"
|
|
72
|
+
ocmo -n prod describe app/web --description ./NOTES.md
|
|
73
|
+
ocmo -n prod describe app/web --description - # from stdin
|
|
74
|
+
"""
|
|
75
|
+
path, _version = parse_address_or_exit(address)
|
|
76
|
+
|
|
77
|
+
view = ctx.namespace_view(namespace)
|
|
78
|
+
|
|
79
|
+
if description_value is not None:
|
|
80
|
+
content = resolve_description_value(description_value)
|
|
81
|
+
ns = ctx.require_namespace(namespace)
|
|
82
|
+
|
|
83
|
+
from .._dry_run import format_describe_dry_run # deferred
|
|
84
|
+
|
|
85
|
+
def _action() -> None:
|
|
86
|
+
view.describe_item(path=path, description=content)
|
|
87
|
+
print(f"Description updated for {path!r}.")
|
|
88
|
+
|
|
89
|
+
run_mutating(
|
|
90
|
+
ctx,
|
|
91
|
+
dry_run=False,
|
|
92
|
+
yes=yes,
|
|
93
|
+
plan_lines=[
|
|
94
|
+
format_describe_dry_run(
|
|
95
|
+
path=path,
|
|
96
|
+
namespace=ns,
|
|
97
|
+
char_count=len(content),
|
|
98
|
+
)
|
|
99
|
+
],
|
|
100
|
+
confirm_message=f"Overwrite description for {path!r}?",
|
|
101
|
+
action=_action,
|
|
102
|
+
abort_exit_code=1,
|
|
103
|
+
abort_via_err=True,
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
item = view.get_item(path=path)
|
|
107
|
+
data = as_dict(item)
|
|
108
|
+
description = data.get("description") or ""
|
|
109
|
+
print(description, end="" if description.endswith("\n") else "\n")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""ocmo diff — coloured unified diff between two versions or two items."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from .._address import AddressError
|
|
10
|
+
from .._client import OcmoCtx
|
|
11
|
+
from .._diff_input import diff_sdk_kwargs, parse_diff_spec, render_diff_response
|
|
12
|
+
from .._errors import sdk_command
|
|
13
|
+
from .._exit import USAGE_ERROR
|
|
14
|
+
from .._options import namespace_option, tree_version_option
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.command("diff")
|
|
18
|
+
@click.argument("addresses", nargs=-1, required=True)
|
|
19
|
+
@tree_version_option()
|
|
20
|
+
@click.option("--from-version", default=None, metavar="VER", help="Left-side version or tag.")
|
|
21
|
+
@click.option("--to-version", default=None, metavar="VER", help="Right-side version or tag.")
|
|
22
|
+
@click.option(
|
|
23
|
+
"--reveal",
|
|
24
|
+
is_flag=True,
|
|
25
|
+
default=False,
|
|
26
|
+
help="Decrypt secret values for diff (requires secret:read).",
|
|
27
|
+
)
|
|
28
|
+
@namespace_option()
|
|
29
|
+
@click.pass_obj
|
|
30
|
+
@sdk_command
|
|
31
|
+
def diff_cmd(
|
|
32
|
+
ctx: OcmoCtx,
|
|
33
|
+
addresses: tuple[str, ...],
|
|
34
|
+
version_flag: str | None,
|
|
35
|
+
from_version: str | None,
|
|
36
|
+
to_version: str | None,
|
|
37
|
+
reveal: bool,
|
|
38
|
+
namespace: str | None,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Show a unified diff between two versions or two items.
|
|
41
|
+
|
|
42
|
+
\b
|
|
43
|
+
Examples:
|
|
44
|
+
ocmo -n prod diff app/web
|
|
45
|
+
ocmo -n prod diff app/web@2..13
|
|
46
|
+
ocmo -n prod diff app/web --from-version 3 --to-version 5
|
|
47
|
+
ocmo -n prod diff app/web@2 app/staging/web@14
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
spec = parse_diff_spec(
|
|
51
|
+
addresses,
|
|
52
|
+
version_flag=version_flag,
|
|
53
|
+
from_version=from_version,
|
|
54
|
+
to_version=to_version,
|
|
55
|
+
)
|
|
56
|
+
except AddressError as e:
|
|
57
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
58
|
+
raise SystemExit(USAGE_ERROR)
|
|
59
|
+
|
|
60
|
+
path, kwargs = diff_sdk_kwargs(spec, reveal=reveal)
|
|
61
|
+
result = ctx.namespace_view(namespace).diff_item(path=path, **kwargs)
|
|
62
|
+
|
|
63
|
+
if getattr(result, "decryption_required", False):
|
|
64
|
+
print(
|
|
65
|
+
"Error: secret diff requires --reveal to compare decrypted values.",
|
|
66
|
+
file=sys.stderr,
|
|
67
|
+
)
|
|
68
|
+
raise SystemExit(USAGE_ERROR)
|
|
69
|
+
|
|
70
|
+
diff_text = render_diff_response(result)
|
|
71
|
+
if sys.stdout.isatty():
|
|
72
|
+
_print_coloured_diff(diff_text)
|
|
73
|
+
else:
|
|
74
|
+
print(diff_text, end="" if diff_text.endswith("\n") else "\n")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _print_coloured_diff(text: str) -> None:
|
|
78
|
+
try:
|
|
79
|
+
from rich.console import Console
|
|
80
|
+
from rich.syntax import Syntax
|
|
81
|
+
|
|
82
|
+
console = Console()
|
|
83
|
+
syntax = Syntax(text, "diff", theme="monokai", line_numbers=False)
|
|
84
|
+
console.print(syntax)
|
|
85
|
+
except ImportError:
|
|
86
|
+
print(text, end="" if text.endswith("\n") else "\n")
|