Files
roshan 1af99d61cb chore(craft): improve Kubernetes startup, image cleanup, and local debugging (#16376)
## Description

Craft development required repeated image builds, copied cluster
settings, and editor-specific Telepresence commands. Startup could
report success before services were usable.

- Share configuration resolution and launch commands between Make and VS
Code. Read deployed ConfigMaps and Secrets in memory, with personal
`.env.k8s` overrides.
- Add separate infrastructure and application readiness checks,
`craft-doctor`, and shared Telepresence connection/intercept handling.
- Reuse images, detect source changes, and provide checkout-scoped
cleanup previews. Add pause/resume and migration commands with
database-target checks.
- Default Kubernetes sandbox callbacks to the internal API Service.
Preserve explicit `ONYX_SERVER_URL` overrides and bump the Helm chart to
0.9.20.
- Update the local Kubernetes guide, Craft development skill, and
feature map.
- Give Helm chart CI a 100 GB runner disk. The previous runner exhausted
disk while unpacking OpenSearch.
- Upgrade the Storybook package family to 10.6.2 to fix the critical
audit finding GHSA-f2ph-cmg5-wxc8.

Existing `.env.k8s` files remain untouched; old service entries continue
to override chart settings. Image refresh updates the PodTemplate for
future sandboxes; existing sandbox pods retain their images.

## Review guide

[Read or download the Markdown review
guide](https://github.com/onyx-dot-app/onyx/blob/d3cbc42d89bfb150d1cfc815638645043c34b69e/docs/craft/dev/kubernetes-review-guide.md).

## How Has This Been Tested?

- Live kind and Telepresence: startup, intercept reuse, doctor, API and
worker hot reload, readiness success/failure, and migration
status/upgrade.
- Three real Craft smoke tests: sandbox provisioning, file
upload/readback, and web app preview through the frontend. No real LLM
calls were exercised.
- 76 Craft Helm tests and 27 development-tool regression tests passed.
- Pre-commit checks, explicit development-tool type checking, frontend
type coverage, and skill validation passed.
- The local web audit passes its critical-severity gate without
suppressions. The Storybook production build passes.
- Removed the disposable test cluster, local processes, and temporary
files.

## Additional Options

- [ ] [Optional] Please cherry-pick this PR to the latest release
version.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Harden local Craft Kubernetes development by sharing configuration and
launch logic between Make and VS Code, and by checking service readiness
before startup reports success.

**What’s included**
- Resolve deployed ConfigMaps, Secrets, and container env in memory
through a shared launcher; personal `.env.k8s` values take precedence,
and resolved secrets never touch disk or terminal output.
- Add shared lifecycle commands (`craft-doctor`, staged
infrastructure/application readiness, Telepresence connect and
intercept, pause, resume, migration, cleanup previews) used by both Make
and VS Code.
- Reuse built images when source is unchanged, force a rebuild with
`--rebuild-sandbox-image`, and keep probe and command invocations
bounded while preserving actionable errors.
- Default sandbox callbacks to the internal API Service, preserving
explicit `ONYX_SERVER_URL` overrides; drop the now-unneeded validation
and bump the Helm chart to 0.9.20.
- Give the Helm CI runner a 100 GB disk, since Docker and kind each
store images and OpenSearch exhausted the default disk. Bump Storybook
tooling to 10.6.2 to patch a development server vulnerability.
- Update the local Kubernetes guide, Craft development skill, feature
map, and add a downloadable PR review guide.

**Validation**
- Passed 76 Craft Helm tests, 27 development-tool regression tests, and
new unit tests for the shared runtime and lifecycle CLI.
- Tested startup, readiness, Telepresence, hot reload, migrations, and
three Craft smoke tests against kind; no real LLM calls were exercised.

<sup>Written for commit d3cbc42d89.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/onyx-dot-app/onyx/pull/16376?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="View guided diff"
src="https://www.cubic.dev/buttons/review-in-cubic-light.svg"></picture></a>
<a
href="https://www.cubic.dev/action/auto-fix/pr/onyx-dot-app/onyx/16376?returnTo=https%3A%2F%2Fgithub.com%2Fonyx-dot-app%2Fonyx%2Fpull%2F16376&source=description"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"><img
alt="Turn on auto-fix"
src="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

- **New Features**
- Added a unified local Kubernetes workflow for Craft, including API and
worker launching, connectivity, readiness checks, migrations,
pause/resume, diagnostics, cleanup, and disk management.
- Added sandbox image reuse, rebuilding, loading, and configurable image
selection.
- Sandbox callbacks now default to the internal API service URL, with
support for explicit overrides.

- **Documentation**
- Added guidance for local Kubernetes setup, configuration, lifecycle
management, troubleshooting, and verification.

- **Bug Fixes**
- Improved readiness validation and stopped sensitive generated
credentials from being printed.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-21 23:15:16 +00:00

798 lines
27 KiB
Python

#!/usr/bin/env python3
"""Local kind development commands shared by Make and VS Code."""
import argparse
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
from typing import TypedDict, cast
from runtime import Cluster
ROOT = Path(__file__).resolve().parents[3]
LABEL = "app.onyx.dev.image"
SOURCE_LABEL = "app.onyx.dev.source"
OWNER_LABEL = "app.onyx.dev.checkout"
IMAGES = {
"backend": (
os.environ.get("BACKEND_IMAGE", "onyxdotapp/onyx-backend:dev"),
"backend",
),
"sandbox": (
os.environ.get("SANDBOX_IMAGE", "onyxdotapp/sandbox:dev"),
"backend/onyx/server/features/build/sandbox/image",
),
}
class ImageConfig(TypedDict):
Labels: dict[str, str] | None
class ImageInfo(TypedDict):
Config: ImageConfig
def run(
*args: str,
capture: bool = False,
cwd: Path = ROOT,
timeout: float | None = 120,
) -> str:
"""Bound short commands; callers can allow longer builds, loads, and cleanup."""
result = subprocess.run(
args,
cwd=cwd,
check=True,
text=True,
timeout=timeout,
stdout=subprocess.PIPE if capture else None,
)
return result.stdout.strip() if capture else ""
def source_hash(context: str) -> str:
"""Fingerprint tracked and unignored source contents, paths, and file modes."""
paths = subprocess.check_output(
[
"git",
"ls-files",
"-z",
"--cached",
"--others",
"--exclude-standard",
"--",
context,
],
cwd=ROOT,
timeout=120,
).split(b"\0")
digest = hashlib.sha256()
for name in sorted(set(paths) - {b""}):
path = ROOT / os.fsdecode(name)
if path.is_symlink():
contents = os.fsencode(os.readlink(path))
elif path.is_file():
contents = path.read_bytes()
else:
continue
digest.update(name + b"\0" + str(path.lstat().st_mode).encode() + b"\0")
digest.update(hashlib.sha256(contents).digest())
return digest.hexdigest()
def nodes(cluster: str) -> list[str]:
"""Find the selected kind cluster nodes or reject a missing cluster."""
result = run("kind", "get", "nodes", "--name", cluster, capture=True).splitlines()
if not result:
raise ValueError(f"kind cluster {cluster!r} does not exist")
return result
def kube(
args: argparse.Namespace,
*command: str,
capture: bool = False,
timeout: float = 120,
) -> str:
"""Run kubectl against the selected local cluster and namespace."""
return run(
"kubectl",
"--context",
f"kind-{args.cluster_name}",
"--request-timeout=30s",
"-n",
args.namespace,
*command,
capture=capture,
timeout=timeout,
)
def inspect_image(image: str) -> ImageInfo | None:
"""Inspect a Docker image without treating daemon failures as missing images."""
run("docker", "info", "--format", "{{.ServerVersion}}", capture=True)
result = subprocess.run(
["docker", "image", "inspect", image],
capture_output=True,
text=True,
timeout=120,
)
if result.returncode:
if "No such image" in result.stderr or "No such object" in result.stderr:
return None
raise ValueError(result.stderr.strip())
return cast(ImageInfo, json.loads(result.stdout)[0])
def labels(info: ImageInfo) -> dict[str, str]:
"""Read image labels, including images with no labels."""
return info["Config"]["Labels"] or {}
def images(args: argparse.Namespace) -> int:
"""Check, build, load, and apply selected images while preserving live sandboxes."""
nodes(args.cluster_name)
selected = [args.only] if args.only else list(IMAGES)
stale = False
for name in selected:
image, context = IMAGES[name]
fingerprint = source_hash(context)
info = inspect_image(image)
matches = info is not None and labels(info).get(SOURCE_LABEL) == fingerprint
if args.check:
print(
f"{image}: {'matches checkout' if matches else 'missing or source changed'}"
)
stale |= not matches
continue
if args.reuse and info is None:
raise ValueError(f"{image} is missing; omit --reuse to build it")
if args.force or (info is None or (not args.reuse and not matches)):
run(
"docker",
"build",
"--label",
f"{LABEL}=true",
"--label",
f"{SOURCE_LABEL}={fingerprint}",
"--label",
f"{OWNER_LABEL}={ROOT}",
"-t",
image,
str(ROOT / context),
timeout=None,
)
else:
print(f"Reusing {image}; no build needed")
run(
"kind",
"load",
"docker-image",
image,
"--name",
args.cluster_name,
timeout=None,
)
if args.load_only:
continue
if name == "backend":
# Update the actual workload image, not just its rollout timestamp.
for deployment in ("onyx-sandbox-proxy", "onyx-api-server"):
workload = json.loads(
kube(
args,
"get",
"deployment",
deployment,
"-o",
"json",
capture=True,
)
)
containers = workload["spec"]["template"]["spec"]["containers"]
patches = []
for index, container in enumerate(containers):
# Telepresence's traffic-agent must retain its own image.
if container["name"] not in {"api-server", "sandbox-proxy"}:
continue
patches.extend(
[
{
"op": "add",
"path": f"/spec/template/spec/containers/{index}/image",
"value": image,
},
{
"op": "add",
"path": f"/spec/template/spec/containers/{index}/imagePullPolicy",
"value": "IfNotPresent",
},
]
)
kube(
args,
"patch",
"deployment",
deployment,
"--type=json",
"-p",
json.dumps(patches),
)
kube(args, "rollout", "restart", f"deployment/{deployment}")
kube(
args,
"rollout",
"status",
f"deployment/{deployment}",
"--timeout=180s",
"--request-timeout=200s",
timeout=210,
)
else:
template = json.loads(
run(
"kubectl",
"--context",
f"kind-{args.cluster_name}",
"--request-timeout=30s",
"-n",
args.sandbox_namespace,
"get",
"podtemplate",
"sandbox-pod",
"-o",
"json",
capture=True,
)
)
spec = template["template"]["spec"]
patches = []
for group in ("containers", "initContainers"):
for index, container in enumerate(spec.get(group, [])):
if container["name"] not in {"sandbox", "sandbox-init", "sidecar"}:
continue
for field, value in (
("image", image),
("imagePullPolicy", "IfNotPresent"),
):
patches.append(
{
"op": "add",
"path": f"/template/spec/{group}/{index}/{field}",
"value": value,
}
)
if not patches:
raise ValueError("Sandbox PodTemplate has no recognized containers")
run(
"kubectl",
"--context",
f"kind-{args.cluster_name}",
"--request-timeout=30s",
"-n",
args.sandbox_namespace,
"patch",
"podtemplate",
"sandbox-pod",
"--type=json",
"-p",
json.dumps(patches),
)
print("Existing sandboxes retain their image. No live sandbox was deleted.")
kube(
args,
"get",
"pods",
"-o",
"custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[*].image,IMAGE_ID:.status.containerStatuses[*].imageID",
)
return int(stale)
def disk(args: argparse.Namespace) -> None:
"""Report host Docker storage and each selected kind node image store."""
run("docker", "system", "df")
for node in nodes(args.cluster_name):
print(f"\nkind node: {node}", flush=True)
run("docker", "exec", node, "df", "-h", "/var/lib/containerd")
run("docker", "exec", node, "crictl", "images")
def doctor(args: argparse.Namespace) -> int:
"""Inspect prerequisites without changing the cluster or exposing secrets."""
failures = 0
def report(level: str, message: str) -> None:
"""Print one diagnostic result and count required failures."""
nonlocal failures
failures += level == "FAIL"
print(f"{level}: {message}", flush=True)
def probe(*command: str) -> subprocess.CompletedProcess[str] | None:
"""Capture diagnostic commands without exposing their output on failure."""
try:
return subprocess.run(
command,
cwd=ROOT,
capture_output=True,
text=True,
timeout=20,
check=False,
)
except (OSError, subprocess.TimeoutExpired):
return None
available = {
name: shutil.which(name)
for name in ("docker", "kind", "kubectl", "helm", "telepresence", "uv")
}
for name, path in available.items():
report("PASS" if path else "FAIL", f"{name}: {path or 'missing from PATH'}")
free_gib = shutil.disk_usage(ROOT).free / 1024**3
report(
"PASS" if free_gib >= 10 else "WARN",
f"Checkout filesystem: {free_gib:.1f} GiB free; Docker storage can use another filesystem",
)
if available["docker"]:
result = probe("docker", "info", "--format", "{{.ServerVersion}}")
report(
"PASS" if result is not None and result.returncode == 0 else "FAIL",
"Docker daemon access"
+ (
f": {result.stdout.strip()}"
if result is not None and result.returncode == 0
else "; check the daemon and socket permissions"
),
)
context = f"kind-{args.cluster_name}"
if available["kubectl"]:
result = probe(
"kubectl",
"--context",
context,
"--request-timeout=10s",
"version",
"-o",
"json",
)
if result is not None and result.returncode == 0:
versions = json.loads(result.stdout)
client = versions["clientVersion"]["gitVersion"]
server = versions["serverVersion"]["gitVersion"]
client_match = re.match(r"v(\d+)\.(\d+)", client)
server_match = re.match(r"v(\d+)\.(\d+)", server)
supported = (
client_match is not None
and server_match is not None
and client_match[1] == server_match[1]
and abs(int(client_match[2]) - int(server_match[2])) <= 1
)
report(
"PASS" if supported else "FAIL",
f"kubectl {client}; server {server}; supported minor-version skew is at most one",
)
native_sidecars = server_match is not None and (
int(server_match[1]),
int(server_match[2]),
) >= (1, 33)
report(
"PASS" if native_sidecars else "FAIL",
"Craft requires Kubernetes 1.33 or later",
)
result = probe(
"kubectl",
"--context",
context,
"--request-timeout=10s",
"get",
"nodes",
"-o",
"json",
)
items = (
json.loads(result.stdout)["items"]
if result is not None and result.returncode == 0
else []
)
ready = bool(items) and all(
any(
c["type"] == "Ready" and c["status"] == "True"
for c in node["status"].get("conditions", [])
)
for node in items
)
report("PASS" if ready else "FAIL", f"{context}: node readiness")
else:
report(
"FAIL",
f"{context}: API unavailable; start or resume the selected cluster",
)
result = probe(
"kubectl",
"--context",
context,
"--request-timeout=10s",
"-n",
args.sandbox_namespace,
"get",
"podtemplate",
"sandbox-pod",
"-o",
"json",
)
if result is not None and result.returncode == 0:
spec = json.loads(result.stdout)["template"]["spec"]
containers = [
c
for group in ("containers", "initContainers")
for c in spec.get(group, [])
if c["name"] in {"sandbox", "sandbox-init", "sidecar"}
]
matches = {c["name"] for c in containers} == {
"sandbox",
"sandbox-init",
"sidecar",
} and all(
c["image"] == IMAGES["sandbox"][0]
and c.get("imagePullPolicy") in {"IfNotPresent", "Never"}
for c in containers
)
report(
"PASS" if matches else "FAIL",
"Sandbox PodTemplate uses the selected local image without registry pulls; repair with make craft-sandbox-image",
)
else:
report("FAIL", "Sandbox PodTemplate unavailable; run make craft-up")
if available["telepresence"]:
result = probe("telepresence", "status", "--format", "json")
status = (
json.loads(result.stdout)
if result is not None and result.returncode == 0
else {}
)
user = status.get("user_daemon", {})
connected = (
user.get("kubernetes_context") == context
and user.get("namespace") == args.namespace
and user.get("status") == "Connected"
and status.get("root_daemon", {}).get("running", False)
)
report(
"PASS" if connected else "FAIL",
f"Telepresence connection to {context}/{args.namespace}"
+ (
""
if connected
else "; run telepresence connect in an interactive terminal for sudo authorization"
),
)
version = user.get("version", "")
report(
"PASS" if version.removeprefix("v").startswith("2.") else "FAIL",
f"Telepresence v2 daemon: {version or 'not running'}",
)
binary = ROOT / ".venv/bin/python"
report(
"PASS" if binary.is_file() else "FAIL", "Checkout Python environment (.venv)"
)
if available["uv"]:
result = probe(
"uv",
"sync",
"--check",
"--offline",
"--frozen",
"--no-default-groups",
"--group",
"backend",
"--group",
"dev",
"--group",
"ee",
)
report(
"PASS" if result is not None and result.returncode == 0 else "FAIL",
"Checkout Python dependencies (backend, dev, ee)"
+ (
""
if result is not None and result.returncode == 0
else "; run uv sync --frozen --no-default-groups --group backend --group dev --group ee"
),
)
if binary.is_file():
script = """import json, sys
from pathlib import Path
sys.path.insert(0, sys.argv[1])
from runtime import Cluster
cluster = Cluster(Path(sys.argv[2]), sys.argv[3], sys.argv[4], sys.argv[5])
env = cluster.environment()
required = ('POSTGRES_HOST', 'SANDBOX_PROXY_HOST', 'SANDBOX_CONTAINER_IMAGE', 'ONYX_SERVER_URL', 'ONYX_SANDBOX_PUSH_PRIVATE_KEY')
print(json.dumps({
'missing': [key for key in required if not env.get(key) or env[key].startswith('<')],
'model_key': bool(env.get('GEN_AI_API_KEY')),
'image': env.get('SANDBOX_CONTAINER_IMAGE') == sys.argv[6],
}))
"""
result = probe(
str(binary),
"-c",
script,
str(Path(__file__).parent),
str(ROOT),
args.cluster_name,
args.namespace,
args.sandbox_namespace,
IMAGES["sandbox"][0],
)
if result is None or result.returncode:
report(
"FAIL",
"Cannot resolve deployed API configuration; check cluster access, secrets, and Python dependencies",
)
else:
env_status = json.loads(result.stdout)
missing = env_status["missing"]
report(
"FAIL" if missing else "PASS",
"Effective Craft configuration"
+ (f": fill {', '.join(missing)}" if missing else " is available"),
)
report(
"PASS" if env_status["image"] else "FAIL",
"Effective sandbox image matches the selected local image",
)
report(
"PASS" if env_status["model_key"] else "WARN",
"GEN_AI_API_KEY is set"
if env_status["model_key"]
else "GEN_AI_API_KEY is unset; agent turns need a configured model provider",
)
print(f"Doctor finished: {failures} failed check(s). No resources changed.")
return int(failures > 0)
def clean(args: argparse.Namespace) -> None:
"""Preview or remove unused checkout images, with separate optional builder cache cleanup."""
candidates = run(
"docker",
"image",
"ls",
"-q",
"--filter",
"dangling=true",
"--filter",
f"label={LABEL}=true",
capture=True,
).splitlines()
for image in sorted(set(candidates)):
info = inspect_image(image)
if info is None or labels(info).get(OWNER_LABEL) != str(ROOT):
continue
if run("docker", "ps", "-aq", "--filter", f"ancestor={image}", capture=True):
continue
print(
f"{'Removing' if args.apply else 'Would remove'} unused checkout image {image}",
flush=True,
)
if args.apply:
run("docker", "image", "rm", image, timeout=None)
if args.build_cache:
print(
"Build cache cleanup affects the selected Docker builder, across all projects.",
flush=True,
)
print(
"Keeps a 10GB cache target and only considers entries unused for seven days.",
flush=True,
)
if args.apply:
run(
"docker",
"builder",
"prune",
"--force",
"--filter",
"until=168h",
"--keep-storage",
"10GB",
timeout=None,
)
print("Volumes, tagged images and kind runtime images are preserved.")
def cluster_runtime(args: argparse.Namespace) -> Cluster:
"""Bind shared runtime commands to the selected development environment."""
return Cluster(ROOT, args.cluster_name, args.namespace, args.sandbox_namespace)
def readiness(args: argparse.Namespace) -> None:
"""Check the selected infrastructure or application readiness stage."""
if args.stage == "infrastructure":
cluster_runtime(args).infrastructure_ready(args.timeout)
else:
cluster_runtime(args).application_ready(args.frontend_url, args.timeout)
def pause_resume(args: argparse.Namespace) -> None:
"""Stop or restart cluster nodes while preserving their data."""
cluster_nodes = nodes(args.cluster_name)
if args.command == "pause":
# Never quit a Telepresence connection to another cluster.
status = json.loads(
run("telepresence", "status", "--format", "json", capture=True)
)
connection = status.get("user_daemon", {})
if connection.get("kubernetes_context") == f"kind-{args.cluster_name}":
run("telepresence", "quit")
run("docker", "stop", *cluster_nodes)
print(
"Cluster data preserved. Stop your local VS Code launch compound separately."
)
else:
run("docker", "start", *cluster_nodes)
kube(
args,
"wait",
"--for=condition=Ready",
"nodes",
"--all",
"--timeout=180s",
"--request-timeout=200s",
timeout=210,
)
cluster_runtime(args).infrastructure_ready()
cluster_runtime(args).connect()
print(
"Cluster resumed. Relaunch your local services to restore the API intercept."
)
def migrate(args: argparse.Namespace) -> None:
"""Inspect or upgrade the selected database after validating local migration prerequisites."""
import psutil
env = cluster_runtime(args).database_environment(args.env_file)
host = env.get("POSTGRES_HOST", "localhost")
port = env.get("POSTGRES_PORT", "5432")
if args.operation == "upgrade":
for proc in psutil.process_iter(["pid", "cmdline", "cwd", "uids"]):
command = proc.info["cmdline"] or []
directory = proc.info["cwd"]
owner = proc.info["uids"]
if (
not directory
or not Path(directory).is_relative_to(ROOT)
or owner is None
or owner.real != os.getuid()
):
continue
launcher = str(ROOT / "deployment/helm/dev/launch.py")
shared_launch = any(
str((base / arg).resolve()) == launcher
for arg in command
if Path(arg).name == "launch.py"
for base in (Path(directory), ROOT)
) and any(role in command for role in ("api", "celery"))
api = "onyx.main:app" in command
worker = any("celery" in Path(arg).name for arg in command) and any(
arg.startswith("onyx.background.") for arg in command
)
if shared_launch or api or worker:
raise ValueError(
f"Stop local Onyx API/workers before migration (PID {proc.pid})"
)
run(
"telepresence",
"connect",
"--context",
f"kind-{args.cluster_name}",
"-n",
args.namespace,
)
print(f"Database: {host}:{port}; checkout: {ROOT}", flush=True)
binary = ROOT / ".venv/bin/python"
subprocess.run(
[str(binary), "-m", "alembic", "current"],
cwd=ROOT / "backend",
env=env,
check=True,
timeout=120,
)
if args.operation == "upgrade":
subprocess.run(
[str(binary), "-m", "alembic", "upgrade", "head"],
cwd=ROOT / "backend",
env=env,
check=True,
timeout=None,
)
subprocess.run(
[str(binary), "-m", "alembic", "current"],
cwd=ROOT / "backend",
env=env,
check=True,
timeout=120,
)
def main() -> int:
"""Dispatch local development commands and return their exit status."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--cluster-name", default="onyx-dev")
parser.add_argument("--namespace", default="onyx")
parser.add_argument("--sandbox-namespace", default="onyx-sandboxes")
commands = parser.add_subparsers(dest="command", required=True)
image_parser = commands.add_parser("images")
image_parser.add_argument("--only", choices=list(IMAGES))
action = image_parser.add_mutually_exclusive_group()
action.add_argument("--check", action="store_true")
action.add_argument("--force", action="store_true")
action.add_argument("--reuse", action="store_true")
image_parser.add_argument("--load-only", action="store_true")
commands.add_parser("disk")
commands.add_parser("doctor")
commands.add_parser("connect")
commands.add_parser("intercept")
ready = commands.add_parser("ready")
ready.add_argument(
"--stage", choices=("infrastructure", "application"), default="application"
)
ready.add_argument("--timeout", type=float, default=600)
ready.add_argument(
"--frontend-url",
default=os.environ.get("CRAFT_FRONTEND_URL", "http://localhost:3000"),
)
cleanup = commands.add_parser("clean")
cleanup.add_argument("--apply", action="store_true")
cleanup.add_argument("--build-cache", action="store_true")
commands.add_parser("pause")
commands.add_parser("resume")
migration = commands.add_parser("migrate")
migration.add_argument("operation", choices=["status", "upgrade"])
migration.add_argument("--env-file", type=Path, help="Defaults to .vscode/.env.k8s")
args = parser.parse_args()
try:
if args.command == "images":
return images(args)
if args.command == "doctor":
return doctor(args)
if args.command == "connect":
cluster_runtime(args).connect()
elif args.command == "intercept":
cluster_runtime(args).intercept()
elif args.command == "ready":
readiness(args)
elif args.command == "disk":
disk(args)
elif args.command == "clean":
clean(args)
elif args.command == "migrate":
migrate(args)
else:
pause_resume(args)
except (
ValueError,
OSError,
ImportError,
subprocess.CalledProcessError,
subprocess.TimeoutExpired,
) as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())