urun-cli 0.7.7__tar.gz → 0.7.8__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.
- {urun_cli-0.7.7 → urun_cli-0.7.8}/CHANGELOG.md +25 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/PKG-INFO +33 -1
- {urun_cli-0.7.7 → urun_cli-0.7.8}/README.md +32 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/pyproject.toml +2 -1
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/api.py +12 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/catalog.py +4 -4
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/cli.py +77 -31
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/deps.py +193 -28
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/hub_builds.py +27 -0
- urun_cli-0.7.8/src/urun/kubectl_plugin.py +688 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/rust.py +1 -1
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/serve.py +70 -1
- {urun_cli-0.7.7 → urun_cli-0.7.8}/.gitignore +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/LICENSE +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/SECURITY.md +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/hatch_build.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/__init__.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/_bin/README.md +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/buildlogs.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/client_tokens.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/config.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/dev.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/devconsole.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/devstream.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/discovery.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/errors.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/fleet.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/frontend_launch.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/launch_auth.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/manifest.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/nodes.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/openai_proxy_bin.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/repo_deps.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/reserve.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/retry.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/runtime_health.py +0 -0
- {urun_cli-0.7.7 → urun_cli-0.7.8}/src/urun/scratch.py +0 -0
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
## Unreleased
|
|
2
2
|
|
|
3
|
+
- `urun release promote <release_id> [--environment main]` — make an existing
|
|
4
|
+
release current again (clean rollback, no marker commits). Server side it
|
|
5
|
+
takes the same path as a same-content `urun deploy`: the control plane
|
|
6
|
+
re-registers the release and republishes the release pointer.
|
|
7
|
+
- Deploy deps collector: `@app.function(deps=...)` may now reference a
|
|
8
|
+
module-level `Dependencies(...)` constant imported from a file shipped with
|
|
9
|
+
the app (`from sense_deploy import SENSE_DEPS`) — resolved statically by the
|
|
10
|
+
same side-effect-free evaluator, including re-export chains and the
|
|
11
|
+
`import mod` + `mod.SYM` attribute form. An attached `deps=` that cannot be
|
|
12
|
+
statically resolved now FAILS THE DEPLOY LOUDLY naming `file:line`, the
|
|
13
|
+
import it came through, and the fix — previously it silently shipped an
|
|
14
|
+
EMPTY manifest (no python pins, no files) and the app's boot self-test died
|
|
15
|
+
with `No module named ...` after fleet rollout (sense, 2026-10-27/28).
|
|
16
|
+
- `kubectl urun` — a kubectl plugin (`kubectl-urun`, installed alongside
|
|
17
|
+
`urun`) with four verbs over the `gpu.urun.sh/v1alpha1` GPUWorker surface:
|
|
18
|
+
`logs` (the `logs.urun.sh/v1` log subresource, with `-f` follow
|
|
19
|
+
passthrough, `--since`, `--tail`, `-s <service>`), `cordon`/`uncordon`
|
|
20
|
+
(`spec.unschedulable`), `drain` (cordon, then wait for the `Draining`
|
|
21
|
+
condition to reach `False`/`Drained`, nonzero on `--timeout`), and
|
|
22
|
+
`describe` (node block, services, conditions, events, liveness lease,
|
|
23
|
+
claim summary). Claim-owned workers are cordoned through their OWNING
|
|
24
|
+
GPUWorkerClaim (resolved via `spec.claimRef`) because the claim
|
|
25
|
+
reconciler re-renders the worker's spec; the patched object is printed.
|
|
26
|
+
All verbs ride a bounded `kubectl proxy --port=0` child, so
|
|
27
|
+
`--kubeconfig`/`--context`/`-n` resolve exactly as kubectl resolves them.
|
|
3
28
|
- Security (ENG-570 / pentest urun-cli-01, mirrored in urun-python
|
|
4
29
|
`urun/core/store/repo.py`): `store.repo(...)` specs parsed from untrusted
|
|
5
30
|
app source are validated before any git call. The `@<ref>` pin must be a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: urun-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.8
|
|
4
4
|
Summary: End-user CLI for deploying apps to urun
|
|
5
5
|
Project-URL: Homepage, https://urun.sh
|
|
6
6
|
Project-URL: Repository, https://github.com/urun-sh/urun-cli
|
|
@@ -534,6 +534,33 @@ What it does:
|
|
|
534
534
|
sooner. `--no-logs` skips streaming the occupant's log after creation
|
|
535
535
|
(Ctrl-C also detaches without tearing the occupant down).
|
|
536
536
|
|
|
537
|
+
## Operator: GPUWorker nodes (`kubectl urun`)
|
|
538
|
+
|
|
539
|
+
Installing `urun-cli` also installs the `kubectl-urun` binary; with it on
|
|
540
|
+
`PATH`, kubectl dispatches `kubectl urun` to it as a standard kubectl plugin.
|
|
541
|
+
These verbs are the operator surface over the `gpu.urun.sh/v1alpha1`
|
|
542
|
+
GPUWorker custom resources (the cluster must have the urun controller
|
|
543
|
+
installed); they resolve kubeconfig/context through kubectl itself, so
|
|
544
|
+
`--kubeconfig`, `--context`, and `-n` behave like any other kubectl command:
|
|
545
|
+
|
|
546
|
+
```bash
|
|
547
|
+
kubectl urun logs <worker> -n urun # one-shot aggregated log
|
|
548
|
+
kubectl urun logs <worker> -n urun -s vineyard --tail 200 --since 30m
|
|
549
|
+
kubectl urun logs <worker> -n urun -f # follow; Ctrl-C detaches
|
|
550
|
+
kubectl urun cordon <worker> -n urun # mark unschedulable
|
|
551
|
+
kubectl urun uncordon <worker> -n urun
|
|
552
|
+
kubectl urun drain <worker> -n urun --timeout 300 # cordon + wait for Drained
|
|
553
|
+
kubectl urun describe <worker> -n urun # node/services/conditions/events/lease
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
`cordon` sets `spec.unschedulable`. Claim-owned workers (workers with a
|
|
557
|
+
`spec.claimRef`) are re-rendered from their claim by the claim reconciler, so
|
|
558
|
+
`cordon`/`uncordon`/`drain` patch the OWNING CLAIM's `spec.unschedulable`
|
|
559
|
+
instead and print which object they patched; workers without a claim are
|
|
560
|
+
patched directly. `drain` cordons and then watches the worker's `Draining`
|
|
561
|
+
condition until it flips to `False`/`Drained` (no sessions left), exiting
|
|
562
|
+
nonzero on `--timeout` (default 300s).
|
|
563
|
+
|
|
537
564
|
## What gets deployed
|
|
538
565
|
|
|
539
566
|
`urun deploy` creates a source manifest from your Python entrypoint:
|
|
@@ -579,6 +606,11 @@ def superres(): ...
|
|
|
579
606
|
- `files=` is **purely additive**. With no `files=`, collection is exactly what
|
|
580
607
|
it was before.
|
|
581
608
|
- Entries are **individual files**, not directories or globs. List each file.
|
|
609
|
+
- `deps=` may be a module-level `Dependencies(...)` constant imported from a
|
|
610
|
+
file shipped with the app (`from deps_mod import APP_DEPS`) — the deploy
|
|
611
|
+
collector resolves it statically. Anything it cannot resolve (an import
|
|
612
|
+
outside the app directory, a non-literal) **fails the deploy loudly**, naming
|
|
613
|
+
`file:line` and the fix — it never ships an empty manifest.
|
|
582
614
|
- A declared path that does not exist, escapes the app directory (`..`, a
|
|
583
615
|
symlink pointing outside), or is absolute **fails the deploy immediately**,
|
|
584
616
|
naming the path and the directory it was looked up under. A declared file is
|
|
@@ -509,6 +509,33 @@ What it does:
|
|
|
509
509
|
sooner. `--no-logs` skips streaming the occupant's log after creation
|
|
510
510
|
(Ctrl-C also detaches without tearing the occupant down).
|
|
511
511
|
|
|
512
|
+
## Operator: GPUWorker nodes (`kubectl urun`)
|
|
513
|
+
|
|
514
|
+
Installing `urun-cli` also installs the `kubectl-urun` binary; with it on
|
|
515
|
+
`PATH`, kubectl dispatches `kubectl urun` to it as a standard kubectl plugin.
|
|
516
|
+
These verbs are the operator surface over the `gpu.urun.sh/v1alpha1`
|
|
517
|
+
GPUWorker custom resources (the cluster must have the urun controller
|
|
518
|
+
installed); they resolve kubeconfig/context through kubectl itself, so
|
|
519
|
+
`--kubeconfig`, `--context`, and `-n` behave like any other kubectl command:
|
|
520
|
+
|
|
521
|
+
```bash
|
|
522
|
+
kubectl urun logs <worker> -n urun # one-shot aggregated log
|
|
523
|
+
kubectl urun logs <worker> -n urun -s vineyard --tail 200 --since 30m
|
|
524
|
+
kubectl urun logs <worker> -n urun -f # follow; Ctrl-C detaches
|
|
525
|
+
kubectl urun cordon <worker> -n urun # mark unschedulable
|
|
526
|
+
kubectl urun uncordon <worker> -n urun
|
|
527
|
+
kubectl urun drain <worker> -n urun --timeout 300 # cordon + wait for Drained
|
|
528
|
+
kubectl urun describe <worker> -n urun # node/services/conditions/events/lease
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
`cordon` sets `spec.unschedulable`. Claim-owned workers (workers with a
|
|
532
|
+
`spec.claimRef`) are re-rendered from their claim by the claim reconciler, so
|
|
533
|
+
`cordon`/`uncordon`/`drain` patch the OWNING CLAIM's `spec.unschedulable`
|
|
534
|
+
instead and print which object they patched; workers without a claim are
|
|
535
|
+
patched directly. `drain` cordons and then watches the worker's `Draining`
|
|
536
|
+
condition until it flips to `False`/`Drained` (no sessions left), exiting
|
|
537
|
+
nonzero on `--timeout` (default 300s).
|
|
538
|
+
|
|
512
539
|
## What gets deployed
|
|
513
540
|
|
|
514
541
|
`urun deploy` creates a source manifest from your Python entrypoint:
|
|
@@ -554,6 +581,11 @@ def superres(): ...
|
|
|
554
581
|
- `files=` is **purely additive**. With no `files=`, collection is exactly what
|
|
555
582
|
it was before.
|
|
556
583
|
- Entries are **individual files**, not directories or globs. List each file.
|
|
584
|
+
- `deps=` may be a module-level `Dependencies(...)` constant imported from a
|
|
585
|
+
file shipped with the app (`from deps_mod import APP_DEPS`) — the deploy
|
|
586
|
+
collector resolves it statically. Anything it cannot resolve (an import
|
|
587
|
+
outside the app directory, a non-literal) **fails the deploy loudly**, naming
|
|
588
|
+
`file:line` and the fix — it never ships an empty manifest.
|
|
557
589
|
- A declared path that does not exist, escapes the app directory (`..`, a
|
|
558
590
|
symlink pointing outside), or is absolute **fails the deploy immediately**,
|
|
559
591
|
naming the path and the directory it was looked up under. A declared file is
|
|
@@ -20,7 +20,7 @@ name = "urun-cli"
|
|
|
20
20
|
# so every merge remains on the already-published version until this value is
|
|
21
21
|
# advanced. Wiring semantic-release into CI remains separate work; this patch
|
|
22
22
|
# bump is the minimal release unblock (post-v0.6.3 fixes + the Rust feature minor).
|
|
23
|
-
version = "0.7.
|
|
23
|
+
version = "0.7.8"
|
|
24
24
|
description = "End-user CLI for deploying apps to urun"
|
|
25
25
|
readme = "README.md"
|
|
26
26
|
requires-python = ">=3.11"
|
|
@@ -48,6 +48,7 @@ Issues = "https://github.com/urun-sh/urun-cli/issues"
|
|
|
48
48
|
[project.scripts]
|
|
49
49
|
urun = "urun.cli:main"
|
|
50
50
|
urun-cli = "urun.cli:main"
|
|
51
|
+
kubectl-urun = "urun.kubectl_plugin:main"
|
|
51
52
|
|
|
52
53
|
[project.optional-dependencies]
|
|
53
54
|
# Rust app discovery (urun/rust.py) parses with the pinned native
|
|
@@ -461,6 +461,18 @@ class ApiClient:
|
|
|
461
461
|
def get_release(self, release_id: str) -> dict[str, Any]:
|
|
462
462
|
return self._get(f"/cli/releases/{urllib.parse.quote(release_id, safe='')}")
|
|
463
463
|
|
|
464
|
+
def promote_release(self, release_id: str, environment: str) -> dict[str, Any]:
|
|
465
|
+
"""Make an EXISTING release current for ``environment`` (rollback).
|
|
466
|
+
|
|
467
|
+
``POST /cli/releases/<id>/promote`` re-fires the release's deploy event —
|
|
468
|
+
the same path a same-content ``urun deploy`` takes — so the control plane
|
|
469
|
+
re-registers it and republishes the release pointer.
|
|
470
|
+
"""
|
|
471
|
+
return self._post(
|
|
472
|
+
f"/cli/releases/{urllib.parse.quote(release_id, safe='')}/promote",
|
|
473
|
+
{"environment": environment},
|
|
474
|
+
)
|
|
475
|
+
|
|
464
476
|
def query_logs(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
465
477
|
return self._get("/cli/logs", params)
|
|
466
478
|
|
|
@@ -120,7 +120,7 @@ ENGINE_DEPS: dict[str, list[str]] = {
|
|
|
120
120
|
"nvidia-cuda-crt==13.0.88",
|
|
121
121
|
"nvidia-cuda-cccl==13.0.85",
|
|
122
122
|
"nvidia-nvvm==13.0.88",
|
|
123
|
-
"nvidia-cuda-runtime==13.0
|
|
123
|
+
"nvidia-cuda-runtime==13.0.*",
|
|
124
124
|
"numpy<2",
|
|
125
125
|
"pillow>=10.0.0",
|
|
126
126
|
"orjson",
|
|
@@ -162,7 +162,7 @@ ENGINE_DEPS: dict[str, list[str]] = {
|
|
|
162
162
|
"nvidia-cuda-crt==13.0.88",
|
|
163
163
|
"nvidia-cuda-cccl==13.0.85",
|
|
164
164
|
"nvidia-nvvm==13.0.88",
|
|
165
|
-
"nvidia-cuda-runtime==13.0
|
|
165
|
+
"nvidia-cuda-runtime==13.0.*",
|
|
166
166
|
"numpy<2",
|
|
167
167
|
"pillow>=10.0.0",
|
|
168
168
|
"orjson",
|
|
@@ -368,7 +368,7 @@ ENGINE_DEPS: dict[str, list[str]] = {
|
|
|
368
368
|
"nvidia-cuda-crt==13.0.88",
|
|
369
369
|
"nvidia-cuda-cccl==13.0.85",
|
|
370
370
|
"nvidia-nvvm==13.0.88",
|
|
371
|
-
"nvidia-cuda-runtime==13.0
|
|
371
|
+
"nvidia-cuda-runtime==13.0.*",
|
|
372
372
|
"numpy<2",
|
|
373
373
|
"pillow>=10.0.0",
|
|
374
374
|
"orjson",
|
|
@@ -446,7 +446,7 @@ ENGINE_DEPS: dict[str, list[str]] = {
|
|
|
446
446
|
"nvidia-cuda-crt==13.0.88",
|
|
447
447
|
"nvidia-cuda-cccl==13.0.85",
|
|
448
448
|
"nvidia-nvvm==13.0.88",
|
|
449
|
-
"nvidia-cuda-runtime==13.0
|
|
449
|
+
"nvidia-cuda-runtime==13.0.*",
|
|
450
450
|
"ninja",
|
|
451
451
|
"pip",
|
|
452
452
|
"nvidia-cutlass-dsl>=4.6.0.dev0,<5",
|
|
@@ -744,6 +744,14 @@ def add_session_parsers(sub: argparse._SubParsersAction) -> None:
|
|
|
744
744
|
|
|
745
745
|
def add_secret_parsers(sub: argparse._SubParsersAction) -> None:
|
|
746
746
|
parser = sub.add_parser("secret", help="Manage secrets (list, get, set, delete)")
|
|
747
|
+
parser.add_argument(
|
|
748
|
+
"--hub-url",
|
|
749
|
+
help=(
|
|
750
|
+
"Regional hub base URL; manage the hub's per-org sealed secret "
|
|
751
|
+
"store instead of the legacy control plane. Defaults to "
|
|
752
|
+
"URUN_HUB_URL; unset = the legacy path."
|
|
753
|
+
),
|
|
754
|
+
)
|
|
747
755
|
secret_sub = parser.add_subparsers(dest="secret_command")
|
|
748
756
|
|
|
749
757
|
list_p = secret_sub.add_parser("list", help="List secret metadata")
|
|
@@ -802,7 +810,7 @@ def add_capacity_parsers(sub: argparse._SubParsersAction) -> None:
|
|
|
802
810
|
|
|
803
811
|
|
|
804
812
|
def add_release_parsers(sub: argparse._SubParsersAction) -> None:
|
|
805
|
-
parser = sub.add_parser("release", help="Manage releases (list, get)")
|
|
813
|
+
parser = sub.add_parser("release", help="Manage releases (list, get, promote)")
|
|
806
814
|
release_sub = parser.add_subparsers(dest="release_command")
|
|
807
815
|
|
|
808
816
|
list_p = release_sub.add_parser("list", help="List releases")
|
|
@@ -814,6 +822,13 @@ def add_release_parsers(sub: argparse._SubParsersAction) -> None:
|
|
|
814
822
|
get_p.add_argument("target")
|
|
815
823
|
add_json_api(get_p)
|
|
816
824
|
|
|
825
|
+
promote_p = release_sub.add_parser(
|
|
826
|
+
"promote", help="Make an existing release current (rollback / re-promote)"
|
|
827
|
+
)
|
|
828
|
+
promote_p.add_argument("target", help="Release id (see `urun release list`)")
|
|
829
|
+
add_environment_arg(promote_p, "Environment to promote into (default: configured, else main)")
|
|
830
|
+
add_json_api(promote_p)
|
|
831
|
+
|
|
817
832
|
|
|
818
833
|
def add_environment_parsers(sub: argparse._SubParsersAction) -> None:
|
|
819
834
|
parser = sub.add_parser("environment", help="Manage environments (list, get, create)")
|
|
@@ -2234,13 +2249,25 @@ def _hub_client_for_deploy(
|
|
|
2234
2249
|
of the login, not an engineer-only flag). Unset = the legacy
|
|
2235
2250
|
control-plane path, byte-for-byte unchanged.
|
|
2236
2251
|
"""
|
|
2252
|
+
hub_url = _hub_url_selected(args)
|
|
2253
|
+
if not hub_url:
|
|
2254
|
+
return None
|
|
2255
|
+
return HubBuildClient(hub_url, api_key, on_retry=on_retry)
|
|
2256
|
+
|
|
2257
|
+
|
|
2258
|
+
def _hub_url_selected(args: argparse.Namespace) -> str | None:
|
|
2259
|
+
"""The hub URL when hub mode is selected, else None.
|
|
2260
|
+
|
|
2261
|
+
Precedence: ``--hub-url`` flag > ``URUN_HUB_URL`` env > the SAVED LOGIN's
|
|
2262
|
+
``hub_url`` (written by ``urun login --hub-url``). The ONE copy of this
|
|
2263
|
+
order — every hub-mode client resolution (deploy, secrets) goes through
|
|
2264
|
+
here so none of them can drift onto a different surface.
|
|
2265
|
+
"""
|
|
2237
2266
|
hub_url = getattr(args, "hub_url", None) or os.getenv("URUN_HUB_URL")
|
|
2238
2267
|
if not hub_url:
|
|
2239
2268
|
saved = load_credentials().get("hub_url")
|
|
2240
2269
|
hub_url = saved if isinstance(saved, str) and saved else None
|
|
2241
|
-
|
|
2242
|
-
return None
|
|
2243
|
-
return HubBuildClient(hub_url, api_key, on_retry=on_retry)
|
|
2270
|
+
return hub_url
|
|
2244
2271
|
|
|
2245
2272
|
|
|
2246
2273
|
def hub_deployment_doc(
|
|
@@ -2495,27 +2522,29 @@ def deploy_once(args: argparse.Namespace) -> DeployOutcome:
|
|
|
2495
2522
|
creds_to_push: dict[str, str] = {}
|
|
2496
2523
|
creds_from_store: list[str] = []
|
|
2497
2524
|
declared_credential_names: list[str] = []
|
|
2498
|
-
secret_client: ApiClient | None = None
|
|
2499
|
-
if (required_env or optional_env) and hub is not None:
|
|
2500
|
-
# HUB ISOLATION (owner ruling 2026-10-27): a hub deploy must never touch
|
|
2501
|
-
# the legacy control plane — silently reading or pushing secrets to
|
|
2502
|
-
# api_url is exactly the bug that created prod rows from hub-mode
|
|
2503
|
-
# deploys. The hub has no secret surface yet, so fail loud; declaring
|
|
2504
|
-
# creds on a hub deploy needs that surface first.
|
|
2505
|
-
raise UrunError(
|
|
2506
|
-
"deploy --hub-url cannot deliver declared Credentials(...) yet: the "
|
|
2507
|
-
"hub has no secret surface, and pushing them to the legacy control "
|
|
2508
|
-
"plane would silently touch prod. Remove the Credentials(...) "
|
|
2509
|
-
"declaration, or deploy without --hub-url."
|
|
2510
|
-
)
|
|
2525
|
+
secret_client: ApiClient | HubBuildClient | None = None
|
|
2511
2526
|
if required_env or optional_env:
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2527
|
+
if hub is not None:
|
|
2528
|
+
# HUB SECRETS CONTRACT: the hub keeps a per-org sealed secret store
|
|
2529
|
+
# (GET/PUT/DELETE /v1/secrets on the org API key); values are
|
|
2530
|
+
# delivered to nodes only at claim time over the worker channel
|
|
2531
|
+
# (POST /v1/worker/secrets, claim-fenced). The legacy control plane
|
|
2532
|
+
# is never contacted in hub mode — not even constructed.
|
|
2533
|
+
secret_client = hub
|
|
2534
|
+
all_secrets = secret_client.list_secrets().get("secrets", [])
|
|
2535
|
+
# Same two-set union as the legacy path: org-level (``app_id`` null)
|
|
2536
|
+
# plus this app's app-level secrets — the precise set the runtime
|
|
2537
|
+
# injects (see control-plane urun_org_secret_env).
|
|
2538
|
+
store_names = {s["name"] for s in all_secrets if not s.get("app_id")}
|
|
2539
|
+
store_names |= {s["name"] for s in all_secrets if s.get("app_id") == app_name}
|
|
2540
|
+
else:
|
|
2541
|
+
secret_client = ApiClient(api_url, api_key)
|
|
2542
|
+
# Org-level secrets (``app_id`` null) plus this app's app-level secrets —
|
|
2543
|
+
# the precise set the runtime injects (see control-plane urun_org_secret_env).
|
|
2544
|
+
all_secrets = secret_client.list_secrets(None).get("secrets", [])
|
|
2545
|
+
store_names = {s["name"] for s in all_secrets if not s.get("app_id")}
|
|
2546
|
+
app_secrets = secret_client.list_secrets(app_name).get("secrets", [])
|
|
2547
|
+
store_names |= {s["name"] for s in app_secrets}
|
|
2519
2548
|
creds_to_push, _missing_required, creds_from_store = resolve_declared_credentials(
|
|
2520
2549
|
required_env, optional_env, store_names
|
|
2521
2550
|
)
|
|
@@ -3503,6 +3532,19 @@ def _client(args: argparse.Namespace) -> ApiClient:
|
|
|
3503
3532
|
return ApiClient(api_url, api_key)
|
|
3504
3533
|
|
|
3505
3534
|
|
|
3535
|
+
def _secrets_client(args: argparse.Namespace) -> ApiClient | HubBuildClient:
|
|
3536
|
+
"""The secret-store client: the hub when hub mode is selected, else legacy.
|
|
3537
|
+
|
|
3538
|
+
A `--hub-url` on a secret command must never dial the legacy control
|
|
3539
|
+
plane behind it — same precedence as :func:`_hub_client_for_deploy`.
|
|
3540
|
+
"""
|
|
3541
|
+
api_url, api_key = resolve_api_credentials(args)
|
|
3542
|
+
hub_url = _hub_url_selected(args)
|
|
3543
|
+
if hub_url:
|
|
3544
|
+
return HubBuildClient(hub_url, api_key)
|
|
3545
|
+
return ApiClient(api_url, api_key)
|
|
3546
|
+
|
|
3547
|
+
|
|
3506
3548
|
def _print_json(payload: Any) -> None:
|
|
3507
3549
|
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
3508
3550
|
|
|
@@ -3528,8 +3570,9 @@ def _filter_params(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
3528
3570
|
return {k: v for k, v in params.items() if v is not None and v is not False}
|
|
3529
3571
|
|
|
3530
3572
|
|
|
3531
|
-
def generic_list(args: argparse.Namespace, target: str) -> int:
|
|
3532
|
-
client
|
|
3573
|
+
def generic_list(args: argparse.Namespace, target: str, client: Any = None) -> int:
|
|
3574
|
+
if client is None:
|
|
3575
|
+
client = _client(args)
|
|
3533
3576
|
params = _filter_params(args)
|
|
3534
3577
|
fetchers: dict[str, Callable[[], dict[str, Any]]] = {
|
|
3535
3578
|
"shapes": lambda: client.list_shapes(params),
|
|
@@ -3574,7 +3617,7 @@ def _mcp_only_failure(command: str, mcp_tool: str) -> int:
|
|
|
3574
3617
|
|
|
3575
3618
|
def _get_secret_metadata(args: argparse.Namespace) -> dict[str, Any]:
|
|
3576
3619
|
"""Metadata-only: the store NEVER returns secret values."""
|
|
3577
|
-
client =
|
|
3620
|
+
client = _secrets_client(args)
|
|
3578
3621
|
secrets = client.list_secrets(getattr(args, "app", None)).get("secrets", [])
|
|
3579
3622
|
matches = [s for s in secrets if s.get("name") == args.target]
|
|
3580
3623
|
if not matches:
|
|
@@ -3614,7 +3657,7 @@ def _session_stop_command(args: argparse.Namespace) -> int:
|
|
|
3614
3657
|
def secret_command(args: argparse.Namespace) -> int:
|
|
3615
3658
|
command = getattr(args, "secret_command", None)
|
|
3616
3659
|
if command == "list":
|
|
3617
|
-
return generic_list(args, "secrets")
|
|
3660
|
+
return generic_list(args, "secrets", client=_secrets_client(args))
|
|
3618
3661
|
if command == "get":
|
|
3619
3662
|
response = _get_secret_metadata(args)
|
|
3620
3663
|
return print_or_table(response, args, "secret")
|
|
@@ -3637,7 +3680,7 @@ def _secret_set_command(args: argparse.Namespace) -> int:
|
|
|
3637
3680
|
value = getpass.getpass(f"Value for secret {args.name}: ")
|
|
3638
3681
|
if value == "":
|
|
3639
3682
|
raise UrunError("empty secret value from prompt; aborting")
|
|
3640
|
-
response =
|
|
3683
|
+
response = _secrets_client(args).set_secret(args.name, {"value": value, "app": args.app})
|
|
3641
3684
|
return print_or_table(response, args, "secret")
|
|
3642
3685
|
|
|
3643
3686
|
|
|
@@ -3645,7 +3688,7 @@ def _secret_delete_command(args: argparse.Namespace) -> int:
|
|
|
3645
3688
|
if not getattr(args, "yes", False) and not _confirm_action(f"Delete secret {args.target}?"):
|
|
3646
3689
|
print("Aborted.")
|
|
3647
3690
|
return 1
|
|
3648
|
-
response =
|
|
3691
|
+
response = _secrets_client(args).delete_secret(args.target, app=getattr(args, "app", None))
|
|
3649
3692
|
return print_or_table(response, args, "secret")
|
|
3650
3693
|
|
|
3651
3694
|
|
|
@@ -3678,7 +3721,10 @@ def release_command(args: argparse.Namespace) -> int:
|
|
|
3678
3721
|
if command == "get":
|
|
3679
3722
|
response = _client(args).get_release(args.target)
|
|
3680
3723
|
return print_or_table(response, args, "release")
|
|
3681
|
-
|
|
3724
|
+
if command == "promote":
|
|
3725
|
+
response = _client(args).promote_release(args.target, resolve_environment(args))
|
|
3726
|
+
return print_or_table(response, args, "release")
|
|
3727
|
+
print("usage: urun release {list,get,promote} [options]", file=sys.stderr)
|
|
3682
3728
|
return 2
|
|
3683
3729
|
|
|
3684
3730
|
|