diff --git a/.gitignore b/.gitignore index 6abbf941..4fb29b8b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ package stack_orchestrator/data/build_tag.txt /build .worktrees +.pebbles/pebbles.db diff --git a/.pebbles/events.jsonl b/.pebbles/events.jsonl index d9ac495f..868f8d1d 100644 --- a/.pebbles/events.jsonl +++ b/.pebbles/events.jsonl @@ -23,3 +23,7 @@ {"type": "create", "timestamp": "2026-03-20T23:05:00.000000Z", "issue_id": "so-n1n", "payload": {"title": "Merge kind-mount-propagation branch — HostToContainer propagation for extraMounts", "type": "feature", "priority": "2", "description": "The kind-mount-root feature was cherry-picked to main (commit 8d03083d) but the mount propagation fix (commit 929bdab8 on branch enya-ac868cc4-kind-mount-propagation-fix) adds HostToContainer propagation so host submounts propagate into the Kind node. This is needed for ZFS child datasets and tmpfs mounts under the root. Cherry-pick 929bdab8 to main."}} {"type": "create", "timestamp": "2026-03-20T23:05:00.000000Z", "issue_id": "so-o2o", "payload": {"title": "etcd cert backup not persisting across cluster deletion", "type": "bug", "priority": "1", "description": "The extraMount for etcd at data/cluster-backups//etcd is configured but after cluster deletion the directory is empty. Caddy TLS certificates stored in etcd are lost. Either etcd isn't writing to the host mount, or the cleanup code is deleting the backup. Investigate _clean_etcd_keeping_certs in helpers.py."}} {"type": "create", "timestamp": "2026-03-21T00:20:00.000000Z", "issue_id": "so-p3p", "payload": {"title": "laconic-so should manage Caddy ingress image lifecycle", "type": "feature", "priority": "2", "description": "The Caddy ingress controller image is hardcoded in ingress-caddy-kind-deploy.yaml. There's no mechanism to update it without manual kubectl commands or cluster recreation. laconic-so should: 1) Allow spec.yml to specify a custom Caddy image, 2) Support updating the Caddy image as part of deployment restart, 3) Set strategy: Recreate on the Caddy Deployment (hostPort pods can't do RollingUpdate). This would let cryovial or similar tooling trigger Caddy updates through the normal deployment pipeline."}} +{"type":"create","timestamp":"2026-04-08T05:51:31.557582604Z","issue_id":"so-5cd","payload":{"description":"The DockerDeployer.up() in stack_orchestrator/deploy/compose/deploy_docker.py accepts image_overrides as a parameter but silently drops it — only k8s mode (deploy_k8s.py) actually applies overrides.\n\nImpact: the --image container=image CLI flag on 'laconic-so deployment start' is a no-op for compose-mode deployments. Spec-level image-overrides: keys are also ignored in compose mode (they reach up() via deployment.py but are never applied).\n\nUse case: gorchain-stacks test scripts build :local images via build-containers, but compose files reference ghcr.io/gorbagana-dev/*:latest (so prod pulls work). Without image override support in compose mode, tests either need to docker tag the builds or the compose file needs to be rewritten before start — both ugly workarounds for what should be a first-class mechanism.\n\nFix sketch: in DockerDeployer.up(), when image_overrides is non-empty, write a temporary docker-compose.override.yml with {services: {name: {image: ref}}} and construct a new DockerClient with compose_files + [override_path]. Keeps k8s path untouched, reuses existing --image CLI flag and spec-level image-overrides: plumbing.","priority":"2","title":"Compose deployer ignores image_overrides","type":"bug"}} +{"type": "create", "timestamp": "2026-04-13T09:54:05.207241Z", "issue_id": "so-c71", "payload": {"title": "extraPortMappings maps all compose ports unconditionally", "type": "bug", "priority": "2", "description": "Commit fb69cc58 added compose service port mapping to Kind extraPortMappings. The intent was to support network_mode: host services (RPC, gossip), but the implementation maps ALL compose ports unconditionally. Internal-only ports (postgres 5432, redis 6379) get exposed on the host, causing conflicts with local services. The port mapping should only apply to services with network_mode: host, or be controlled by a spec-level opt-in.", "source_commit": "fb69cc58"}} +{"type": "create", "timestamp": "2026-04-14T09:53:31.040118Z", "issue_id": "so-078", "payload": {"title": "Deployments should be self-sufficient: copy hooks into deployment dir", "type": "feature", "priority": "1", "description": "deploy/commands.py hooks are resolved from the stack repo at runtime via get_stack_path. The deployment dir has no copy. This means: (1) the repo must remain at the same path after deploy create, (2) deployment start/restart fail with 'stack does not exist' if cwd differs from deploy create time (stack-source in deployment.yml is relative), (3) deployments cannot be moved or run independently of the source repo. Fix: deploy create should copy deploy/commands.py into the deployment dir alongside compose files and configmaps. call_stack_deploy_start should load from the deployment dir. The deployment becomes self-sufficient."}} +{"type": "update", "timestamp": "2026-04-14T10:01:14.937483Z", "issue_id": "so-c71", "payload": {"status": "resolved", "resolution": "Fixed in commit e909357a on fix/extraport-host-only branch. Only map ports for services with network_mode: host. Ports 80/443 for Caddy always mapped."}} diff --git a/.pebbles/pebbles.db b/.pebbles/pebbles.db deleted file mode 100644 index f1587825..00000000 Binary files a/.pebbles/pebbles.db and /dev/null differ diff --git a/stack_orchestrator/deploy/deployment_create.py b/stack_orchestrator/deploy/deployment_create.py index 074b6ba8..be3670ce 100644 --- a/stack_orchestrator/deploy/deployment_create.py +++ b/stack_orchestrator/deploy/deployment_create.py @@ -128,7 +128,7 @@ def _get_named_volumes(stack): # so the deployment will start # Also warn if the path is absolute and doesn't exist def _create_bind_dir_if_relative(volume, path_string, compose_dir): - path = Path(path_string) + path = Path(os.path.expanduser(path_string)) if not path.is_absolute(): absolute_path = Path(compose_dir).parent.joinpath(path) absolute_path.mkdir(parents=True, exist_ok=True) @@ -1118,12 +1118,11 @@ def _write_deployment_files( # (user-defined in spec.yml). Fall back to the config/ dir # convention if no value is provided. if configmap_path and not str(configmap_path).startswith("./"): - # configmap_path is relative to the repo root (cwd during - # restart). get_stack_path gives us a path like - # "stack-orchestrator/stacks/dumpster" — also relative to - # repo root. The configmap_path is already repo-relative, - # so use it directly (cwd is repo root during restart). - source_config_dir = Path(configmap_path) + # User-defined source path. Can be: + # - repo-relative: "stack-orchestrator/compose/maintenance" + # - home-relative: "~/.credentials/local-certs/s3" + # - absolute: "/path/to/dir" + source_config_dir = Path(os.path.expanduser(configmap_path)) else: source_config_dir = resolve_config_dir(stack_name, configmap_name) if os.path.exists(source_config_dir): diff --git a/stack_orchestrator/deploy/k8s/helpers.py b/stack_orchestrator/deploy/k8s/helpers.py index 396c286b..d0c66483 100644 --- a/stack_orchestrator/deploy/k8s/helpers.py +++ b/stack_orchestrator/deploy/k8s/helpers.py @@ -748,12 +748,18 @@ def _generate_kind_port_mappings(parsed_pod_files): f" - containerPort: {port_string}\n hostPort: {port_string}\n" ) seen.add((port_string, "TCP")) - # Map ports declared in compose services + # Map ports only for services with network_mode: host. + # Other service ports are internal — they go through the Ingress on + # 80/443 and don't need host port mappings. Mapping all compose ports + # unconditionally (the previous behavior) caused conflicts with local + # services like postgres (5432) and redis (6379). for pod in parsed_pod_files: parsed_pod_file = parsed_pod_files[pod] if "services" in parsed_pod_file: for service_name in parsed_pod_file["services"]: service_obj = parsed_pod_file["services"][service_name] + if service_obj.get("network_mode") != "host": + continue for port_entry in service_obj.get("ports", []): port_str = str(port_entry) protocol = "TCP"