Skip to content

Commit 14e5c1e

Browse files
feat(otdf-sdk-mgr): manage platform service + install scenario (DSPX-3302)
1 parent de2cc5c commit 14e5c1e

3 files changed

Lines changed: 421 additions & 8 deletions

File tree

otdf-sdk-mgr/src/otdf_sdk_mgr/cli_install.py

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@
1111
install_app = typer.Typer(help="Install SDK CLI artifacts from registries or source.")
1212

1313

14+
def _register_scenario_cmd() -> None:
15+
"""Defer scenario import so pydantic is only imported when needed."""
16+
from otdf_sdk_mgr.cli_scenario import install_scenario_cmd
17+
18+
install_app.command("scenario")(install_scenario_cmd)
19+
20+
21+
_register_scenario_cmd()
22+
23+
1424
@install_app.command()
1525
def stable(
1626
sdks: Annotated[
@@ -32,9 +42,27 @@ def lts(
3242
] = None,
3343
) -> None:
3444
"""Install LTS versions for each SDK."""
45+
from otdf_sdk_mgr.config import LTS_VERSIONS
3546
from otdf_sdk_mgr.installers import cmd_lts
36-
37-
cmd_lts(sdks or ALL_SDKS)
47+
from otdf_sdk_mgr.platform_installer import (
48+
PlatformInstallError,
49+
install_platform_release,
50+
)
51+
52+
requested = sdks or ALL_SDKS
53+
sdk_targets = [s for s in requested if s != "platform"]
54+
if "platform" in requested:
55+
version = LTS_VERSIONS.get("platform")
56+
if version is None:
57+
typer.echo("Warning: no LTS version defined for platform; skipping", err=True)
58+
else:
59+
try:
60+
install_platform_release(version)
61+
except PlatformInstallError as e:
62+
typer.echo(f"Error: {e}", err=True)
63+
raise typer.Exit(1)
64+
if sdk_targets:
65+
cmd_lts(sdk_targets)
3866

3967

4068
@install_app.command()
@@ -46,23 +74,80 @@ def tip(
4674
) -> None:
4775
"""Source checkout + build from main."""
4876
from otdf_sdk_mgr.installers import cmd_tip
49-
50-
cmd_tip(sdks or ALL_SDKS)
77+
from otdf_sdk_mgr.platform_installer import (
78+
PlatformInstallError,
79+
install_platform_source,
80+
)
81+
82+
requested = sdks or ALL_SDKS
83+
sdk_targets = [s for s in requested if s != "platform"]
84+
if "platform" in requested:
85+
try:
86+
install_platform_source("main", dist_name="tip")
87+
except PlatformInstallError as e:
88+
typer.echo(f"Error: {e}", err=True)
89+
raise typer.Exit(1)
90+
if sdk_targets:
91+
cmd_tip(sdk_targets)
5192

5293

5394
@install_app.command()
5495
def release(
5596
specs: Annotated[
5697
list[str],
57-
typer.Argument(help="Version specs as SDK:VERSION (e.g., go:v0.24.0)"),
98+
typer.Argument(help="Version specs as SDK:VERSION (e.g., go:v0.24.0, platform:v0.9.0)"),
5899
],
59100
) -> None:
60-
"""Install specific released versions."""
101+
"""Install specific released versions.
102+
103+
`sdk` may be one of go/js/java or the literal `platform`. Platform is
104+
built from source against the `service/<version>` tag in the
105+
`opentdf/platform` monorepo.
106+
"""
61107
from otdf_sdk_mgr.installers import InstallError, cmd_release
108+
from otdf_sdk_mgr.platform_installer import (
109+
PlatformInstallError,
110+
install_platform_release,
111+
)
112+
113+
sdk_specs: list[str] = []
114+
for spec in specs:
115+
if ":" not in spec:
116+
typer.echo(f"Error: invalid spec '{spec}'. Use SDK:VERSION.", err=True)
117+
raise typer.Exit(1)
118+
sdk, version = spec.split(":", 1)
119+
if sdk == "platform":
120+
try:
121+
install_platform_release(version)
122+
except PlatformInstallError as e:
123+
typer.echo(f"Error: {e}", err=True)
124+
raise typer.Exit(1)
125+
else:
126+
sdk_specs.append(spec)
127+
if sdk_specs:
128+
try:
129+
cmd_release(sdk_specs)
130+
except InstallError as e:
131+
typer.echo(f"Error: {e}", err=True)
132+
raise typer.Exit(1)
133+
134+
135+
@install_app.command()
136+
def scripts(
137+
branch: Annotated[
138+
str,
139+
typer.Option(help="Branch of opentdf/platform to pull scripts from"),
140+
] = "main",
141+
) -> None:
142+
"""Refresh shared platform helper scripts under xtest/platform/scripts/."""
143+
from otdf_sdk_mgr.platform_installer import (
144+
PlatformInstallError,
145+
install_helper_scripts,
146+
)
62147

63148
try:
64-
cmd_release(specs)
65-
except InstallError as e:
149+
install_helper_scripts(branch)
150+
except PlatformInstallError as e:
66151
typer.echo(f"Error: {e}", err=True)
67152
raise typer.Exit(1)
68153

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""Scenario-driven install command.
2+
3+
Reads a `scenarios.yaml` (or standalone `instance.yaml`) and installs every
4+
artifact referenced — platform service binary, per-KAS binaries (each at
5+
its own pinned version), and encrypt/decrypt SDK CLIs. Writes
6+
`installed.json` next to the manifest so downstream tools (`otdf-local`,
7+
plugin skills) can locate the dist paths without re-resolving.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import json
13+
from pathlib import Path
14+
from typing import Annotated
15+
16+
import typer
17+
18+
from otdf_sdk_mgr.installers import InstallError, install_release
19+
from otdf_sdk_mgr.platform_installer import (
20+
PlatformInstallError,
21+
install_helper_scripts,
22+
install_platform_release,
23+
install_platform_source,
24+
)
25+
from otdf_sdk_mgr.schema import KasPin, PlatformPin, Scenario, load_instance, load_scenario
26+
27+
28+
def _install_platform_pin(pin: PlatformPin | KasPin, label: str) -> dict[str, str]:
29+
if pin.image is not None:
30+
raise typer.BadParameter(
31+
f"{label}: container-image platform pins are not supported in v1; use dist or source"
32+
)
33+
if pin.dist is not None:
34+
dist_dir = install_platform_release(pin.dist)
35+
return {"kind": "dist", "version": pin.dist, "path": str(dist_dir)}
36+
assert pin.source is not None # by schema invariant
37+
dist_dir = install_platform_source(pin.source.ref)
38+
return {"kind": "source", "ref": pin.source.ref, "path": str(dist_dir)}
39+
40+
41+
def install_scenario_cmd(
42+
path: Annotated[Path, typer.Argument(help="Path to scenarios.yaml or instance.yaml")],
43+
skip_scripts: Annotated[
44+
bool,
45+
typer.Option("--skip-scripts", help="Skip refreshing helper scripts from main"),
46+
] = False,
47+
) -> None:
48+
"""Install every artifact declared by a scenarios.yaml or instance.yaml."""
49+
if not path.exists():
50+
typer.echo(f"Error: {path} not found", err=True)
51+
raise typer.Exit(1)
52+
53+
raw_kind = _peek_kind(path)
54+
scenario: Scenario | None = None
55+
if raw_kind == "Scenario":
56+
scenario = load_scenario(path)
57+
instance = scenario.instance
58+
elif raw_kind == "Instance":
59+
instance = load_instance(path)
60+
else:
61+
typer.echo(f"Error: {path} has unknown kind {raw_kind!r}", err=True)
62+
raise typer.Exit(1)
63+
64+
installed: dict[str, object] = {"manifest": str(path), "platform": None, "kas": {}, "sdks": {}}
65+
66+
try:
67+
installed["platform"] = _install_platform_pin(instance.platform, "platform")
68+
for kas_name, kas_pin in instance.kas.items():
69+
installed["kas"][kas_name] = _install_platform_pin(kas_pin, f"kas.{kas_name}")
70+
if not skip_scripts:
71+
install_helper_scripts()
72+
except PlatformInstallError as e:
73+
typer.echo(f"Error installing platform artifacts: {e}", err=True)
74+
raise typer.Exit(1)
75+
76+
if scenario is not None:
77+
sdks = scenario.sdks.union()
78+
for sdk_name, sdk_pin in sdks.items():
79+
try:
80+
dist_dir = install_release(sdk_name, sdk_pin.version, source=sdk_pin.source)
81+
installed["sdks"][sdk_name] = {
82+
"version": sdk_pin.version,
83+
"source": sdk_pin.source,
84+
"path": str(dist_dir),
85+
}
86+
except InstallError as e:
87+
typer.echo(f"Error installing SDK {sdk_name}: {e}", err=True)
88+
raise typer.Exit(1)
89+
90+
out = path.parent / f"{path.stem}.installed.json"
91+
out.write_text(json.dumps(installed, indent=2) + "\n")
92+
typer.echo(f" Wrote {out}")
93+
94+
95+
def _peek_kind(path: Path) -> str | None:
96+
"""Cheap pre-validation read so we can dispatch to the right model loader."""
97+
from ruamel.yaml import YAML
98+
99+
y = YAML(typ="safe")
100+
raw = y.load(path.read_text())
101+
if isinstance(raw, dict):
102+
kind = raw.get("kind")
103+
return kind if isinstance(kind, str) else None
104+
return None

0 commit comments

Comments
 (0)