2026-03-10 19:37:02 +00:00
|
|
|
"""Binary bundler — downloads kubectl, kustomize, helm, buildctl at pinned versions.
|
feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py.
Verbs: up, down, status, apply, seed, verify, logs, restart, get,
build, mirror, bootstrap. Service scoping via ns/name target syntax.
Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in
~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
2026-03-02 20:59:57 +00:00
|
|
|
|
|
|
|
|
Binaries are cached in ~/.local/share/sunbeam/bin/ and SHA256-verified.
|
2026-03-10 19:37:02 +00:00
|
|
|
Platform (OS + arch) is detected at runtime so the same package works on
|
|
|
|
|
darwin/arm64 (development Mac), darwin/amd64, linux/arm64, and linux/amd64.
|
feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py.
Verbs: up, down, status, apply, seed, verify, logs, restart, get,
build, mirror, bootstrap. Service scoping via ns/name target syntax.
Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in
~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
2026-03-02 20:59:57 +00:00
|
|
|
"""
|
|
|
|
|
import hashlib
|
|
|
|
|
import io
|
|
|
|
|
import os
|
2026-03-10 19:37:02 +00:00
|
|
|
import platform
|
feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py.
Verbs: up, down, status, apply, seed, verify, logs, restart, get,
build, mirror, bootstrap. Service scoping via ns/name target syntax.
Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in
~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
2026-03-02 20:59:57 +00:00
|
|
|
import stat
|
|
|
|
|
import subprocess
|
|
|
|
|
import tarfile
|
|
|
|
|
import urllib.request
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
CACHE_DIR = Path.home() / ".local/share/sunbeam/bin"
|
|
|
|
|
|
2026-03-10 19:37:02 +00:00
|
|
|
# Tool specs — URL and extract templates use {version}, {os}, {arch}.
|
|
|
|
|
# {os} : darwin | linux
|
|
|
|
|
# {arch} : arm64 | amd64
|
|
|
|
|
_TOOL_SPECS: dict[str, dict] = {
|
feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py.
Verbs: up, down, status, apply, seed, verify, logs, restart, get,
build, mirror, bootstrap. Service scoping via ns/name target syntax.
Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in
~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
2026-03-02 20:59:57 +00:00
|
|
|
"kubectl": {
|
|
|
|
|
"version": "v1.32.2",
|
2026-03-10 19:37:02 +00:00
|
|
|
"url": "https://dl.k8s.io/release/{version}/bin/{os}/{arch}/kubectl",
|
|
|
|
|
# plain binary, no archive
|
feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py.
Verbs: up, down, status, apply, seed, verify, logs, restart, get,
build, mirror, bootstrap. Service scoping via ns/name target syntax.
Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in
~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
2026-03-02 20:59:57 +00:00
|
|
|
},
|
|
|
|
|
"kustomize": {
|
2026-03-03 11:32:09 +00:00
|
|
|
"version": "v5.8.1",
|
2026-03-10 19:37:02 +00:00
|
|
|
"url": (
|
|
|
|
|
"https://github.com/kubernetes-sigs/kustomize/releases/download/"
|
|
|
|
|
"kustomize%2F{version}/kustomize_{version}_{os}_{arch}.tar.gz"
|
|
|
|
|
),
|
feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py.
Verbs: up, down, status, apply, seed, verify, logs, restart, get,
build, mirror, bootstrap. Service scoping via ns/name target syntax.
Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in
~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
2026-03-02 20:59:57 +00:00
|
|
|
"extract": "kustomize",
|
|
|
|
|
},
|
|
|
|
|
"helm": {
|
2026-03-03 11:32:09 +00:00
|
|
|
"version": "v4.1.0",
|
2026-03-10 19:37:02 +00:00
|
|
|
"url": "https://get.helm.sh/helm-{version}-{os}-{arch}.tar.gz",
|
|
|
|
|
"extract": "{os}-{arch}/helm",
|
|
|
|
|
"sha256": {
|
|
|
|
|
"darwin_arm64": "82f7065bf4e08d4c8d7881b85c0a080581ef4968a4ae6df4e7b432f8f7a88d0c",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"buildctl": {
|
|
|
|
|
"version": "v0.28.0",
|
|
|
|
|
# BuildKit releases: buildkit-v0.28.0.linux.amd64.tar.gz
|
|
|
|
|
"url": (
|
|
|
|
|
"https://github.com/moby/buildkit/releases/download/{version}/"
|
|
|
|
|
"buildkit-{version}.{os}-{arch}.tar.gz"
|
|
|
|
|
),
|
|
|
|
|
"extract": "bin/buildctl",
|
feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py.
Verbs: up, down, status, apply, seed, verify, logs, restart, get,
build, mirror, bootstrap. Service scoping via ns/name target syntax.
Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in
~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
2026-03-02 20:59:57 +00:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 19:37:02 +00:00
|
|
|
# Expose as TOOLS for callers that do `if "helm" in TOOLS`.
|
|
|
|
|
TOOLS = _TOOL_SPECS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _detect_platform() -> tuple[str, str]:
|
|
|
|
|
"""Return (os_name, arch) for the current host."""
|
|
|
|
|
sys_os = platform.system().lower()
|
|
|
|
|
machine = platform.machine().lower()
|
|
|
|
|
os_name = {"darwin": "darwin", "linux": "linux"}.get(sys_os)
|
|
|
|
|
if not os_name:
|
|
|
|
|
raise RuntimeError(f"Unsupported OS: {sys_os}")
|
|
|
|
|
arch = "arm64" if machine in ("arm64", "aarch64") else "amd64"
|
|
|
|
|
return os_name, arch
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_spec(name: str) -> dict:
|
|
|
|
|
"""Return a tool spec with {os} / {arch} / {version} substituted.
|
|
|
|
|
|
|
|
|
|
Uses the module-level TOOLS dict so that tests can patch it.
|
|
|
|
|
"""
|
|
|
|
|
if name not in TOOLS:
|
|
|
|
|
raise ValueError(f"Unknown tool: {name}")
|
|
|
|
|
os_name, arch = _detect_platform()
|
|
|
|
|
raw = TOOLS[name]
|
|
|
|
|
version = raw.get("version", "")
|
|
|
|
|
fmt = {"version": version, "os": os_name, "arch": arch}
|
|
|
|
|
spec = dict(raw)
|
|
|
|
|
spec["version"] = version
|
|
|
|
|
spec["url"] = raw["url"].format(**fmt)
|
|
|
|
|
if "extract" in raw:
|
|
|
|
|
spec["extract"] = raw["extract"].format(**fmt)
|
|
|
|
|
# sha256 may be a per-platform dict {"darwin_arm64": "..."} or a plain string.
|
|
|
|
|
sha256_val = raw.get("sha256", {})
|
|
|
|
|
if isinstance(sha256_val, dict):
|
|
|
|
|
spec["sha256"] = sha256_val.get(f"{os_name}_{arch}", "")
|
|
|
|
|
return spec
|
|
|
|
|
|
feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py.
Verbs: up, down, status, apply, seed, verify, logs, restart, get,
build, mirror, bootstrap. Service scoping via ns/name target syntax.
Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in
~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
2026-03-02 20:59:57 +00:00
|
|
|
|
|
|
|
|
def _sha256(path: Path) -> str:
|
|
|
|
|
h = hashlib.sha256()
|
|
|
|
|
with open(path, "rb") as f:
|
|
|
|
|
for chunk in iter(lambda: f.read(65536), b""):
|
|
|
|
|
h.update(chunk)
|
|
|
|
|
return h.hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ensure_tool(name: str) -> Path:
|
2026-03-03 11:32:09 +00:00
|
|
|
"""Return path to cached binary, downloading + verifying if needed.
|
|
|
|
|
|
2026-03-10 19:37:02 +00:00
|
|
|
Re-downloads automatically when the pinned version in _TOOL_SPECS changes.
|
2026-03-03 11:32:09 +00:00
|
|
|
A <name>.version sidecar file records the version of the cached binary.
|
|
|
|
|
"""
|
2026-03-10 19:37:02 +00:00
|
|
|
spec = _resolve_spec(name)
|
feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py.
Verbs: up, down, status, apply, seed, verify, logs, restart, get,
build, mirror, bootstrap. Service scoping via ns/name target syntax.
Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in
~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
2026-03-02 20:59:57 +00:00
|
|
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
dest = CACHE_DIR / name
|
2026-03-03 11:32:09 +00:00
|
|
|
version_file = CACHE_DIR / f"{name}.version"
|
feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py.
Verbs: up, down, status, apply, seed, verify, logs, restart, get,
build, mirror, bootstrap. Service scoping via ns/name target syntax.
Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in
~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
2026-03-02 20:59:57 +00:00
|
|
|
|
|
|
|
|
expected_sha = spec.get("sha256", "")
|
2026-03-03 11:32:09 +00:00
|
|
|
expected_version = spec.get("version", "")
|
feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py.
Verbs: up, down, status, apply, seed, verify, logs, restart, get,
build, mirror, bootstrap. Service scoping via ns/name target syntax.
Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in
~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
2026-03-02 20:59:57 +00:00
|
|
|
|
|
|
|
|
if dest.exists():
|
2026-03-03 11:32:09 +00:00
|
|
|
version_ok = (
|
|
|
|
|
not expected_version
|
|
|
|
|
or (version_file.exists() and version_file.read_text().strip() == expected_version)
|
|
|
|
|
)
|
|
|
|
|
sha_ok = not expected_sha or _sha256(dest) == expected_sha
|
|
|
|
|
if version_ok and sha_ok:
|
feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py.
Verbs: up, down, status, apply, seed, verify, logs, restart, get,
build, mirror, bootstrap. Service scoping via ns/name target syntax.
Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in
~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
2026-03-02 20:59:57 +00:00
|
|
|
return dest
|
2026-03-10 19:37:02 +00:00
|
|
|
|
2026-03-03 11:32:09 +00:00
|
|
|
# Version mismatch or SHA mismatch — re-download
|
|
|
|
|
if dest.exists():
|
feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py.
Verbs: up, down, status, apply, seed, verify, logs, restart, get,
build, mirror, bootstrap. Service scoping via ns/name target syntax.
Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in
~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
2026-03-02 20:59:57 +00:00
|
|
|
dest.unlink()
|
2026-03-03 11:32:09 +00:00
|
|
|
if version_file.exists():
|
|
|
|
|
version_file.unlink()
|
feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py.
Verbs: up, down, status, apply, seed, verify, logs, restart, get,
build, mirror, bootstrap. Service scoping via ns/name target syntax.
Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in
~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
2026-03-02 20:59:57 +00:00
|
|
|
|
|
|
|
|
url = spec["url"]
|
|
|
|
|
with urllib.request.urlopen(url) as resp: # noqa: S310
|
|
|
|
|
data = resp.read()
|
|
|
|
|
|
|
|
|
|
extract_path = spec.get("extract")
|
|
|
|
|
if extract_path:
|
|
|
|
|
with tarfile.open(fileobj=io.BytesIO(data)) as tf:
|
|
|
|
|
member = tf.getmember(extract_path)
|
|
|
|
|
fobj = tf.extractfile(member)
|
|
|
|
|
binary_data = fobj.read()
|
|
|
|
|
else:
|
|
|
|
|
binary_data = data
|
|
|
|
|
|
|
|
|
|
dest.write_bytes(binary_data)
|
|
|
|
|
|
|
|
|
|
if expected_sha:
|
|
|
|
|
actual = _sha256(dest)
|
|
|
|
|
if actual != expected_sha:
|
|
|
|
|
dest.unlink()
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
f"SHA256 mismatch for {name}: expected {expected_sha}, got {actual}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
dest.chmod(dest.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
2026-03-03 11:32:09 +00:00
|
|
|
version_file.write_text(expected_version)
|
feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py.
Verbs: up, down, status, apply, seed, verify, logs, restart, get,
build, mirror, bootstrap. Service scoping via ns/name target syntax.
Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in
~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
2026-03-02 20:59:57 +00:00
|
|
|
return dest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_tool(name: str, *args, **kwargs) -> subprocess.CompletedProcess:
|
|
|
|
|
"""Run a bundled tool, ensuring it is downloaded first.
|
|
|
|
|
|
|
|
|
|
For kustomize: prepends CACHE_DIR to PATH so helm is found.
|
|
|
|
|
"""
|
|
|
|
|
bin_path = ensure_tool(name)
|
|
|
|
|
env = kwargs.pop("env", None)
|
|
|
|
|
if env is None:
|
|
|
|
|
env = os.environ.copy()
|
|
|
|
|
if name == "kustomize":
|
2026-03-03 11:32:09 +00:00
|
|
|
if "helm" in TOOLS:
|
2026-03-10 19:37:02 +00:00
|
|
|
ensure_tool("helm")
|
feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py.
Verbs: up, down, status, apply, seed, verify, logs, restart, get,
build, mirror, bootstrap. Service scoping via ns/name target syntax.
Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in
~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
2026-03-02 20:59:57 +00:00
|
|
|
env["PATH"] = str(CACHE_DIR) + os.pathsep + env.get("PATH", "")
|
|
|
|
|
return subprocess.run([str(bin_path), *args], env=env, **kwargs)
|