mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-29 16:17:25 +09:00
Hardening plan §8. A stdlib-only, import-safe, unit-tested Python package that
ports the stabilized bash install logic into testable orchestration. Per the
plan's migration path this is ADDITIVE — install.sh stays canonical and this is
NOT wired into it yet; it is the validated foundation a future thin-bootstrap
install.sh can call.
backend/serverkit_installer/:
- _proc.py — sudo-aware subprocess helpers, mirroring app/utils/system.py
WITHOUT importing app (the bootstrap runs before the venv
exists, so it must not pull in Flask).
- distro.py — OS family mapping (a faithful port of install.sh's
os_family_from, rhel-before-fedora) + container/WSL/systemd
predicates (mirroring scripts/lib/env.sh).
- deps.py — loads scripts/deps/manifest.yaml (its primary consumer) and
resolves base/python/node/docker/pkg-manager per family.
- firewall.py — detect + pure open/close command builders (port of
scripts/lib/firewall.sh).
- service.py — init detect + pure start/stop/enable/reload builders (port of
scripts/lib/init.sh).
- main.py — composes a READ-ONLY install "plan" + `python -m
serverkit_installer plan|detect` CLI. No host mutation.
Tests: backend/tests/test_serverkit_installer.py — 46 pure-logic cases
(family mapping vs os_family_from, real-manifest resolution for all six
families, firewall/service command builders, env predicates, composed plan).
46 passed; py_compile + pyflakes clean. The CLI proves the manifest is genuinely
consumed (Debian -> apt + python3.11 + base packages), not dead config.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
56 lines
1.9 KiB
Python
56 lines
1.9 KiB
Python
"""Dependency manifest loader.
|
|
|
|
Reads scripts/deps/manifest.yaml — the declarative per-family package spec — and
|
|
resolves the pieces an installer needs (base packages, python spec, node/docker
|
|
method, package manager) for a given family. This is the manifest's primary
|
|
consumer: the bash installer can't parse YAML before Python exists, so it
|
|
mirrors these choices inline; here we read the file directly.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
try:
|
|
import yaml
|
|
except ImportError: # pragma: no cover - PyYAML is a hard backend dependency
|
|
yaml = None
|
|
|
|
|
|
def default_manifest_path() -> Path:
|
|
"""Repo-relative path to scripts/deps/manifest.yaml.
|
|
|
|
deps.py lives at backend/serverkit_installer/deps.py, so the repo root is
|
|
two parents up.
|
|
"""
|
|
return Path(__file__).resolve().parents[2] / "scripts" / "deps" / "manifest.yaml"
|
|
|
|
|
|
def load_manifest(path: Optional[str] = None) -> dict:
|
|
"""Load and parse the manifest. Raises if PyYAML is unavailable."""
|
|
if yaml is None:
|
|
raise RuntimeError("PyYAML is required to read the dependency manifest")
|
|
manifest_path = Path(path) if path else default_manifest_path()
|
|
with open(manifest_path, encoding="utf-8") as handle:
|
|
return yaml.safe_load(handle) or {}
|
|
|
|
|
|
def base_packages(manifest: dict, family: str) -> list:
|
|
return list((manifest.get("base_packages") or {}).get(family, []))
|
|
|
|
|
|
def python_spec(manifest: dict, family: str) -> dict:
|
|
return dict((manifest.get("python") or {}).get(family, {}))
|
|
|
|
|
|
def node_spec(manifest: dict, family: str) -> dict:
|
|
return dict((manifest.get("node") or {}).get(family, {}))
|
|
|
|
|
|
def docker_spec(manifest: dict, family: str) -> dict:
|
|
return dict((manifest.get("docker") or {}).get(family, {}))
|
|
|
|
|
|
def package_manager(manifest: dict, family: str) -> Optional[str]:
|
|
return (manifest.get("package_manager") or {}).get(family)
|