make deployments self-sufficient by copying hooks into deployment dir

pull/750/head
pranav 2026-04-27 19:03:04 +05:30
parent 4977e3ff43
commit befb548a6e
2 changed files with 34 additions and 10 deletions

View File

@ -54,3 +54,5 @@
{"type":"status_update","timestamp":"2026-04-21T06:08:14.457815115Z","issue_id":"so-ad7","payload":{"status":"closed"}}
{"type":"update","timestamp":"2026-04-21T09:00:47.364859946Z","issue_id":"so-p3p","payload":{"description":"## Problem\n\nThe Caddy ingress controller image is hardcoded in `ingress-caddy-kind-deploy.yaml`, with no mechanism to update it short of cluster recreation or manual `kubectl patch`. laconic-so should: (1) allow spec.yml to specify a Caddy image, (2) support updating the Caddy image as part of `deployment start`, (3) set `strategy: Recreate` on the Caddy Deployment since hostPort pods can't rolling-update.\n\n## Resolution\n\n- New spec key `caddy-ingress-image`. Fresh install uses it (fallback: manifest default). On subsequent `deployment start`, if the spec key is set and the running Caddy image differs, SO patches the Deployment and waits for rollout.\n- Spec key absent =\u003e SO does **not** touch a running Caddy, to avoid silently reverting images set out-of-band (ansible playbook, another deployment's spec).\n- `strategy: Recreate` added to the Caddy Deployment manifest.\n- Reconcile runs under both `--perform-cluster-management` and the default `--skip-cluster-management` (it's a plain k8s-API patch, not a cluster lifecycle op).\n- Image substitution locates the container by name instead of string-matching the shipped default, so the spec override wins regardless of what the manifest hardcodes.\n- Cluster-scoped caveat: `caddy-system` is shared across deployments; last `deployment start` that sets the key wins for everyone. Documented in `deployment_patterns.md`."}}
{"type":"status_update","timestamp":"2026-04-21T09:00:47.745675131Z","issue_id":"so-p3p","payload":{"status":"closed"}}
{"type":"comment","timestamp":"2026-04-27T13:41:16.962883653Z","issue_id":"so-078","payload":{"body":"Fixed. deploy create now copies commands.py into deployment_dir/hooks/. call_stack_deploy_start loads hooks from the deployment dir instead of resolving via get_stack_path, so deployment start no longer requires the stack repo to be present or cwd to be correct."}}
{"type":"close","timestamp":"2026-04-27T13:41:17.073012545Z","issue_id":"so-078","payload":{}}

View File

@ -276,16 +276,17 @@ def call_stack_deploy_start(deployment_context):
create additional k8s resources (Services, etc.) in the deployment namespace.
The namespace can be derived as f"laconic-{deployment_context.id}".
"""
python_file_paths = _commands_plugin_paths(deployment_context.stack.name)
for python_file_path in python_file_paths:
if python_file_path.exists():
spec = util.spec_from_file_location("commands", python_file_path)
if spec is None or spec.loader is None:
continue
imported_stack = util.module_from_spec(spec)
spec.loader.exec_module(imported_stack)
if _has_method(imported_stack, "start"):
imported_stack.start(deployment_context)
hooks_dir = deployment_context.deployment_dir / "hooks"
if not hooks_dir.exists():
return
for python_file_path in sorted(hooks_dir.glob("commands*.py")):
spec = util.spec_from_file_location("commands", python_file_path)
if spec is None or spec.loader is None:
continue
imported_stack = util.module_from_spec(spec)
spec.loader.exec_module(imported_stack)
if _has_method(imported_stack, "start"):
imported_stack.start(deployment_context)
# Inspect the pod yaml to find config files referenced in subdirectories
@ -1111,6 +1112,25 @@ def _safe_copy_tree(src: Path, dst: Path, exclude_patterns: Optional[List[str]]
safe_copy_file(src_path, dst_path)
def _copy_hooks(stack_name: str, target_dir: Path):
"""Copy commands.py hooks into deployment_dir/hooks/ so the deployment is self-sufficient.
Single repo: hooks/commands.py
Multi-repo: hooks/commands_0.py, hooks/commands_1.py, ... (not tested)
"""
plugin_paths = get_plugin_code_paths(stack_name)
sources = [p.joinpath("deploy", "commands.py") for p in plugin_paths if p.joinpath("deploy", "commands.py").exists()]
if not sources:
return
hooks_dir = target_dir / "hooks"
hooks_dir.mkdir(exist_ok=True)
if len(sources) == 1:
copyfile(sources[0], hooks_dir / "commands.py")
else:
for i, src in enumerate(sources):
copyfile(src, hooks_dir / f"commands_{i}.py")
def _write_deployment_files(
target_dir: Path,
spec_file: Path,
@ -1138,6 +1158,8 @@ def _write_deployment_files(
copyfile(spec_file, target_dir.joinpath(constants.spec_file_name))
copyfile(stack_file, target_dir.joinpath(constants.stack_file_name))
_copy_hooks(stack_name, target_dir)
# Create deployment file if requested
if include_deployment_file:
_create_deployment_file(target_dir, stack_source=stack_source)