From bb4414551099e91ad71e9ed04f35f93bde5e4361 Mon Sep 17 00:00:00 2001 From: "A. F. Dudley" Date: Sun, 25 Jan 2026 17:27:51 -0500 Subject: [PATCH 1/3] fix(deploy): merge volumes from stack init() instead of overwriting Previously, volumes defined in a stack's commands.py init() function were being overwritten by volumes discovered from compose files. This prevented stacks from adding infrastructure volumes like caddy-data that aren't defined in the compose files. Now volumes are merged, with init() volumes taking precedence over compose-discovered defaults. Co-Authored-By: Claude Opus 4.5 --- stack_orchestrator/deploy/deployment_create.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/stack_orchestrator/deploy/deployment_create.py b/stack_orchestrator/deploy/deployment_create.py index 601f6c77..6b049fda 100644 --- a/stack_orchestrator/deploy/deployment_create.py +++ b/stack_orchestrator/deploy/deployment_create.py @@ -465,7 +465,10 @@ def init_operation( else: volume_descriptors[named_volume] = f"./data/{named_volume}" if volume_descriptors: - spec_file_content["volumes"] = volume_descriptors + # Merge with existing volumes from stack init() + # init() volumes take precedence over compose defaults + orig_volumes = spec_file_content.get("volumes", {}) + spec_file_content["volumes"] = {**volume_descriptors, **orig_volumes} if configmap_descriptors: spec_file_content["configmaps"] = configmap_descriptors -- 2.45.2 From aa88adabc167e2aa54020f04f4a3c77f7dd9ad31 Mon Sep 17 00:00:00 2001 From: "A. F. Dudley" Date: Sun, 25 Jan 2026 17:35:53 -0500 Subject: [PATCH 2/3] feat(k8s): support acme-email config for Caddy ingress Adds support for configuring ACME email for Let's Encrypt certificates in kind deployments. The email can be specified in the spec under network.acme-email and will be used to configure the Caddy ingress controller ConfigMap. Co-Authored-By: Claude Opus 4.5 --- stack_orchestrator/deploy/k8s/deploy_k8s.py | 3 ++- stack_orchestrator/deploy/k8s/helpers.py | 17 ++++++++++++++++- stack_orchestrator/deploy/spec.py | 3 +++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/stack_orchestrator/deploy/k8s/deploy_k8s.py b/stack_orchestrator/deploy/k8s/deploy_k8s.py index 3d0b697c..ed890595 100644 --- a/stack_orchestrator/deploy/k8s/deploy_k8s.py +++ b/stack_orchestrator/deploy/k8s/deploy_k8s.py @@ -301,7 +301,8 @@ class K8sDeployer(Deployer): self.connect_api() if self.is_kind() and not self.skip_cluster_management: # Configure ingress controller (not installed by default in kind) - install_ingress_for_kind() + acme_email = self.cluster_info.spec.get_acme_email() + install_ingress_for_kind(acme_email=acme_email) # Wait for ingress to start # (deployment provisioning will fail unless this is done) wait_for_ingress_in_kind() diff --git a/stack_orchestrator/deploy/k8s/helpers.py b/stack_orchestrator/deploy/k8s/helpers.py index a125d4f5..a0f378e0 100644 --- a/stack_orchestrator/deploy/k8s/helpers.py +++ b/stack_orchestrator/deploy/k8s/helpers.py @@ -90,7 +90,7 @@ def wait_for_ingress_in_kind(): error_exit("ERROR: Timed out waiting for Caddy ingress to become ready") -def install_ingress_for_kind(): +def install_ingress_for_kind(acme_email: str = ""): api_client = client.ApiClient() ingress_install = os.path.abspath( get_k8s_dir().joinpath( @@ -101,6 +101,21 @@ def install_ingress_for_kind(): print("Installing Caddy ingress controller in kind cluster") utils.create_from_yaml(api_client, yaml_file=ingress_install) + # Patch ConfigMap with ACME email if provided + if acme_email: + if opts.o.debug: + print(f"Configuring ACME email: {acme_email}") + core_api = client.CoreV1Api() + configmap = core_api.read_namespaced_config_map( + name="caddy-ingress-controller-configmap", namespace="caddy-system" + ) + configmap.data["email"] = acme_email + core_api.patch_namespaced_config_map( + name="caddy-ingress-controller-configmap", + namespace="caddy-system", + body=configmap, + ) + def load_images_into_kind(kind_cluster_name: str, image_set: Set[str]): for image in image_set: diff --git a/stack_orchestrator/deploy/spec.py b/stack_orchestrator/deploy/spec.py index 1713f28a..002288ff 100644 --- a/stack_orchestrator/deploy/spec.py +++ b/stack_orchestrator/deploy/spec.py @@ -117,6 +117,9 @@ class Spec: def get_http_proxy(self): return self.obj.get(constants.network_key, {}).get(constants.http_proxy_key, []) + def get_acme_email(self): + return self.obj.get(constants.network_key, {}).get("acme-email", "") + def get_annotations(self): return self.obj.get(constants.annotations_key, {}) -- 2.45.2 From 1b9204da982f250776990c1f6d46bf84dc395c7e Mon Sep 17 00:00:00 2001 From: "A. F. Dudley" Date: Sun, 25 Jan 2026 19:12:44 -0500 Subject: [PATCH 3/3] Add etcd + PKI extraMounts for offline data recovery Mount /var/lib/etcd and /etc/kubernetes/pki to host filesystem so cluster state is preserved for offline recovery. Each deployment gets its own backup directory keyed by deployment ID. Directory structure: data/cluster-backups/{deployment_id}/etcd/ data/cluster-backups/{deployment_id}/pki/ This enables extracting secrets from etcd backups using etcdctl with the preserved PKI certificates. Co-Authored-By: Claude Opus 4.5 --- stack_orchestrator/deploy/k8s/helpers.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/stack_orchestrator/deploy/k8s/helpers.py b/stack_orchestrator/deploy/k8s/helpers.py index a0f378e0..10f0d0cb 100644 --- a/stack_orchestrator/deploy/k8s/helpers.py +++ b/stack_orchestrator/deploy/k8s/helpers.py @@ -253,6 +253,26 @@ def _make_absolute_host_path(data_mount_path: Path, deployment_dir: Path) -> Pat def _generate_kind_mounts(parsed_pod_files, deployment_dir, deployment_context): volume_definitions = [] volume_host_path_map = _get_host_paths_for_volumes(deployment_context) + + # Cluster state backup for offline data recovery (unique per deployment) + # etcd contains all k8s state; PKI certs needed to decrypt etcd offline + deployment_id = deployment_context.id + backup_subdir = f"cluster-backups/{deployment_id}" + + etcd_host_path = _make_absolute_host_path( + Path(f"./data/{backup_subdir}/etcd"), deployment_dir + ) + volume_definitions.append( + f" - hostPath: {etcd_host_path}\n" f" containerPath: /var/lib/etcd\n" + ) + + pki_host_path = _make_absolute_host_path( + Path(f"./data/{backup_subdir}/pki"), deployment_dir + ) + volume_definitions.append( + f" - hostPath: {pki_host_path}\n" f" containerPath: /etc/kubernetes/pki\n" + ) + # Note these paths are relative to the location of the pod files (at present) # So we need to fix up to make them correct and absolute because kind assumes # relative to the cwd. -- 2.45.2