diff --git a/stack_orchestrator/constants.py b/stack_orchestrator/constants.py index 5e7b59bf..eded528d 100644 --- a/stack_orchestrator/constants.py +++ b/stack_orchestrator/constants.py @@ -46,3 +46,4 @@ runtime_class_key = "runtime-class" high_memlock_runtime = "high-memlock" high_memlock_spec_filename = "high-memlock-spec.json" acme_email_key = "acme-email" +kind_mount_root_key = "kind-mount-root" diff --git a/stack_orchestrator/deploy/k8s/cluster_info.py b/stack_orchestrator/deploy/k8s/cluster_info.py index 5289e814..d55da7ff 100644 --- a/stack_orchestrator/deploy/k8s/cluster_info.py +++ b/stack_orchestrator/deploy/k8s/cluster_info.py @@ -455,7 +455,11 @@ class ClusterInfo: ) if self.spec.is_kind_deployment(): host_path = client.V1HostPathVolumeSource( - path=get_kind_pv_bind_mount_path(volume_name) + path=get_kind_pv_bind_mount_path( + volume_name, + kind_mount_root=self.spec.get_kind_mount_root(), + host_path=volume_path, + ) ) else: host_path = client.V1HostPathVolumeSource(path=volume_path) diff --git a/stack_orchestrator/deploy/k8s/helpers.py b/stack_orchestrator/deploy/k8s/helpers.py index 426e3125..56e49adf 100644 --- a/stack_orchestrator/deploy/k8s/helpers.py +++ b/stack_orchestrator/deploy/k8s/helpers.py @@ -444,7 +444,20 @@ def named_volumes_from_pod_files(parsed_pod_files): return named_volumes -def get_kind_pv_bind_mount_path(volume_name: str): +def get_kind_pv_bind_mount_path( + volume_name: str, + kind_mount_root: Optional[str] = None, + host_path: Optional[str] = None, +): + """Get the path inside the Kind node for a PV. + + When kind-mount-root is set and the volume's host path is under + that root, return /mnt/{relative_path} so it resolves through the + single root extraMount. Otherwise fall back to /mnt/{volume_name}. + """ + if kind_mount_root and host_path and host_path.startswith(kind_mount_root): + rel = os.path.relpath(host_path, kind_mount_root) + return f"/mnt/{rel}" return f"/mnt/{volume_name}" @@ -567,6 +580,7 @@ def _generate_kind_mounts(parsed_pod_files, deployment_dir, deployment_context): volume_definitions = [] volume_host_path_map = _get_host_paths_for_volumes(deployment_context) seen_host_path_mounts = set() # Track to avoid duplicate mounts + kind_mount_root = deployment_context.spec.get_kind_mount_root() # Cluster state backup for offline data recovery (unique per deployment) # etcd contains all k8s state; PKI certs needed to decrypt etcd offline @@ -587,6 +601,16 @@ def _generate_kind_mounts(parsed_pod_files, deployment_dir, deployment_context): f" - hostPath: {pki_host_path}\n" f" containerPath: /etc/kubernetes/pki\n" ) + # When kind-mount-root is set, emit a single extraMount for the root. + # Individual volumes whose host path starts with the root are covered + # by this single mount and don't need their own extraMount entries. + mount_root_emitted = False + if kind_mount_root: + volume_definitions.append( + f" - hostPath: {kind_mount_root}\n" f" containerPath: /mnt\n" + ) + mount_root_emitted = True + # 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. @@ -646,6 +670,12 @@ def _generate_kind_mounts(parsed_pod_files, deployment_dir, deployment_context): volume_host_path_map[volume_name], deployment_dir, ) + # Skip individual extraMount if covered + # by the kind-mount-root single mount + if mount_root_emitted and str(host_path).startswith( + kind_mount_root + ): + continue container_path = get_kind_pv_bind_mount_path( volume_name ) @@ -982,7 +1012,7 @@ def translate_sidecar_service_names( def envs_from_environment_variables_map( - map: Mapping[str, str] + map: Mapping[str, str], ) -> List[client.V1EnvVar]: result = [] for env_var, env_val in map.items(): diff --git a/stack_orchestrator/deploy/spec.py b/stack_orchestrator/deploy/spec.py index ef37bc3c..3fb97dff 100644 --- a/stack_orchestrator/deploy/spec.py +++ b/stack_orchestrator/deploy/spec.py @@ -264,6 +264,17 @@ class Spec: def is_kind_deployment(self): return self.get_deployment_type() in [constants.k8s_kind_deploy_type] + def get_kind_mount_root(self) -> typing.Optional[str]: + """Return kind-mount-root path or None. + + When set, laconic-so emits a single Kind extraMount mapping this + host path to /mnt inside the Kind node. Volumes with host paths + under this root resolve to /mnt/{relative_path} and don't need + individual extraMounts. This allows adding new volumes without + recreating the Kind cluster. + """ + return self.obj.get(constants.kind_mount_root_key) + def get_maintenance_service(self) -> typing.Optional[str]: """Return maintenance-service value (e.g. 'dumpster-maintenance:8000') or None.