diff --git a/.pebbles/events.jsonl b/.pebbles/events.jsonl index ece11c09..1d04bb8a 100644 --- a/.pebbles/events.jsonl +++ b/.pebbles/events.jsonl @@ -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":{}} diff --git a/stack_orchestrator/deploy/deployment_create.py b/stack_orchestrator/deploy/deployment_create.py index fd7ec4f1..07029714 100644 --- a/stack_orchestrator/deploy/deployment_create.py +++ b/stack_orchestrator/deploy/deployment_create.py @@ -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)