add fixture-invoicing example, sample-room wrap, kind cluster support
- examples/fixture-invoicing/: FastAPI + Vue + Postgres demo (4-entity invoice fixture)
- cfg/sample/: wraps the fixture (managed.repos points at examples/)
- ctrl/kind-{up,down,status}.sh + per-room k8s render in soleprint/ctrl/k8s/
- build.py: relative repo paths, resilient rmtree, optional k8s render hook
- cfg/.gitignore: stop ignoring sample/ and standalone/ template rooms
Manifests render cleanly but kind cluster has not been run end-to-end yet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
46
build.py
46
build.py
@@ -26,6 +26,8 @@ import argparse
|
||||
import json
|
||||
import logging
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -48,6 +50,35 @@ def ensure_dir(path: Path):
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def _rmtree_resilient(path: Path):
|
||||
"""Remove path tree, tolerating root-owned files written by containers.
|
||||
|
||||
Docker containers that mount gen/ as a volume sometimes write files as
|
||||
root (e.g. __pycache__). A plain shutil.rmtree then fails with EACCES.
|
||||
We first try shutil.rmtree; if that hits a PermissionError we fall back
|
||||
to deleting the offending files from inside an ephemeral alpine container.
|
||||
"""
|
||||
def _chmod_and_retry(func, target, exc_info):
|
||||
try:
|
||||
Path(target).chmod(stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR)
|
||||
func(target)
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
try:
|
||||
shutil.rmtree(path, onerror=_chmod_and_retry)
|
||||
return
|
||||
except PermissionError:
|
||||
pass
|
||||
|
||||
log.info(" (falling back to docker-based cleanup)")
|
||||
subprocess.run(
|
||||
["docker", "run", "--rm", "-v", f"{path.parent}:/work",
|
||||
"alpine:3", "sh", "-c", f"rm -rf /work/{path.name}"],
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def copy_path(source: Path, target: Path, quiet: bool = False):
|
||||
"""Copy file or directory, resolving symlinks."""
|
||||
if target.is_symlink():
|
||||
@@ -162,9 +193,11 @@ def build_managed(output_dir: Path, cfg_name: str, config: dict):
|
||||
|
||||
log.info(f"Building managed ({managed_name})...")
|
||||
|
||||
# Copy repos
|
||||
# Copy repos (relative paths resolve from SPR_ROOT)
|
||||
for repo_name, repo_path in repos.items():
|
||||
source = Path(repo_path)
|
||||
if not source.is_absolute():
|
||||
source = SPR_ROOT / source
|
||||
target = managed_dir / repo_name
|
||||
if copy_repo(source, target):
|
||||
log.info(f" {repo_name}/")
|
||||
@@ -332,7 +365,7 @@ def build(output_dir: Path, cfg_name: str | None = None, clean: bool = True):
|
||||
# Clean output directory first
|
||||
if clean and output_dir.exists():
|
||||
log.info(f"Cleaning {output_dir}...")
|
||||
shutil.rmtree(output_dir)
|
||||
_rmtree_resilient(output_dir)
|
||||
|
||||
ensure_dir(output_dir)
|
||||
|
||||
@@ -349,6 +382,15 @@ def build(output_dir: Path, cfg_name: str | None = None, clean: bool = True):
|
||||
# Standalone: everything in output_dir
|
||||
build_soleprint(output_dir, room)
|
||||
|
||||
# Layer 7 (optional): render kind-cluster manifests
|
||||
try:
|
||||
from soleprint.ctrl.k8s import render_k8s
|
||||
from soleprint.ctrl.k8s.render import k8s_enabled
|
||||
if k8s_enabled(config):
|
||||
render_k8s(room=room, config=config, gen_dir=output_dir)
|
||||
except ImportError as e:
|
||||
log.warning(f"k8s rendering unavailable: {e}")
|
||||
|
||||
log.info(f"\n✓ Built: {output_dir}")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user