feat(k8s): support init containers via compose labels

Add support for k8s init containers defined in docker-compose files
using the `laconic.init-container` label. Services with this label
set to "true" are built as init containers instead of regular
containers in the pod spec.

This enables stacks to fetch runtime-created ConfigMaps (e.g. from
deployer jobs) before the main containers start, without requiring
manual operator steps between deployments.

Example compose usage:

  services:
    fetch-config:
      image: bitnami/kubectl:latest
      labels:
        laconic.init-container: "true"
      command: ["sh", "-c", "kubectl get configmap ..."]
      volumes:
        - shared-config:/config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Prathamesh Musale 2026-03-10 10:38:24 +00:00
parent 5af6a83fa2
commit dcba9e74d9

View File

@ -446,12 +446,16 @@ class ClusterInfo:
) -> tuple:
"""Build k8s container specs from parsed compose YAML.
Returns a tuple of (containers, services, volumes) where:
Returns a tuple of (containers, init_containers, services, volumes)
where:
- containers: list of V1Container objects
- init_containers: list of V1Container objects for init containers
(compose services with label ``laconic.init-container: "true"``)
- services: the last services dict processed (used for annotations/labels)
- volumes: list of V1Volume objects
"""
containers = []
init_containers = []
services = {}
global_resources = self.spec.get_container_resources()
if not global_resources:
@ -571,15 +575,29 @@ class ClusterInfo:
),
resources=to_k8s_resource_requirements(container_resources),
)
containers.append(container)
# Services with laconic.init-container label become
# k8s init containers instead of regular containers.
svc_labels = service_info.get("labels", {})
if isinstance(svc_labels, list):
# docker-compose labels can be a list of "key=value"
svc_labels = dict(
l.split("=", 1) for l in svc_labels
)
is_init = str(
svc_labels.get("laconic.init-container", "")
).lower() in ("true", "1", "yes")
if is_init:
init_containers.append(container)
else:
containers.append(container)
volumes = volumes_for_pod_files(
parsed_yaml_map, self.spec, self.app_name
)
return containers, services, volumes
return containers, init_containers, services, volumes
# TODO: put things like image pull policy into an object-scope struct
def get_deployment(self, image_pull_policy: Optional[str] = None):
containers, services, volumes = self._build_containers(
containers, init_containers, services, volumes = self._build_containers(
self.parsed_pod_yaml_map, image_pull_policy
)
registry_config = self.spec.get_image_registry_config()
@ -650,6 +668,7 @@ class ClusterInfo:
metadata=client.V1ObjectMeta(annotations=annotations, labels=labels),
spec=client.V1PodSpec(
containers=containers,
init_containers=init_containers or None,
image_pull_secrets=image_pull_secrets,
volumes=volumes,
affinity=affinity,
@ -695,8 +714,8 @@ class ClusterInfo:
for job_file in self.parsed_job_yaml_map:
# Build containers for this single job file
single_job_map = {job_file: self.parsed_job_yaml_map[job_file]}
containers, _services, volumes = self._build_containers(
single_job_map, image_pull_policy
containers, init_containers, _services, volumes = (
self._build_containers(single_job_map, image_pull_policy)
)
# Derive job name from file path: docker-compose-<name>.yml -> <name>
@ -722,6 +741,7 @@ class ClusterInfo:
),
spec=client.V1PodSpec(
containers=containers,
init_containers=init_containers or None,
image_pull_secrets=image_pull_secrets,
volumes=volumes,
restart_policy="Never",