From 78b5af747e75272d0ee6d134e9cee669a1c23549 Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Sun, 8 Mar 2026 23:47:13 +0100 Subject: [PATCH] feat(depends): Use kubernetes API for depends_on management We were using netcat to port to check if a service is up, but actually we can do like Docker / Podman compose and check the status. For now, I'm using the endpoint status, but maybe we can just check if the object is "up". --- doc/docs/usage.md | 20 +++---- internal/generator/deployment.go | 49 ++++++++++++++++- internal/generator/deployment_test.go | 55 +++++++++++++++++++ internal/generator/labels/katenaryLabels.go | 1 + .../generator/labels/katenaryLabelsDoc.yaml | 21 +++++++ 5 files changed, 133 insertions(+), 13 deletions(-) diff --git a/doc/docs/usage.md b/doc/docs/usage.md index 9d0ae8b..16baaf0 100644 --- a/doc/docs/usage.md +++ b/doc/docs/usage.md @@ -97,7 +97,8 @@ Katenary transforms compose services this way: - environment variables will be stored inside a `ConfigMap` - image, tags, and ingresses configuration are also stored in `values.yaml` file - if named volumes are declared, Katenary create `PersistentVolumeClaims` - not enabled in values file -- `depends_on` needs that the pointed service declared a port. If not, you can use labels to inform Katenary +- `depends_on` uses Kubernetes API by default to check if the service endpoint is ready. No port required. + Use label `katenary.v3/depends-on: legacy` to use the old netcat method (requires port). For any other specific configuration, like binding local files as `ConfigMap`, bind variables, add values with documentation, etc. You'll need to use labels. @@ -147,10 +148,8 @@ Katenary proposes a lot of labels to configure the helm chart generation, but so ### Work with Depends On? -Kubernetes does not provide service or pod starting detection from others pods. But Katenary will create `initContainer` -to make you able to wait for a service to respond. But you'll probably need to adapt a bit the compose file. - -See this compose file: +Katenary creates `initContainer` to wait for dependent services to be ready. By default, it uses the Kubernetes API +to check if the service endpoint has ready addresses - no port required. ```yaml version: "3" @@ -167,9 +166,7 @@ services: MYSQL_ROOT_PASSWORD: foobar ``` -In this case, `webapp` needs to know the `database` port because the `depends_on` points on it and Kubernetes has not -(yet) solution to check the database startup. Katenary wants to create a `initContainer` to hit on the related service. -So, instead of exposing the port in the compose definition, let's declare this to Katenary with labels: +If you need the old netcat-based method (requires port), add the `katenary.v3/depends-on: legacy` label to the dependent service: ```yaml version: "3" @@ -179,14 +176,15 @@ services: image: php:8-apache depends_on: - database + labels: + katenary.v3/depends-on: legacy database: image: mariadb environment: MYSQL_ROOT_PASSWORD: foobar - labels: - katenary.v3/ports: |- - - 3306 + ports: + - 3306:3306 ``` ### Declare ingresses diff --git a/internal/generator/deployment.go b/internal/generator/deployment.go index 3d0616e..e692226 100644 --- a/internal/generator/deployment.go +++ b/internal/generator/deployment.go @@ -262,9 +262,21 @@ func (d *Deployment) BindFrom(service types.ServiceConfig, binded *Deployment) { // DependsOn adds a initContainer to the deployment that will wait for the service to be up. func (d *Deployment) DependsOn(to *Deployment, servicename string) error { - // Add a initContainer with busybox:latest using netcat to check if the service is up - // it will wait until the service responds to all ports logger.Info("Adding dependency from ", d.service.Name, " to ", to.service.Name) + + useLegacy := false + if label, ok := d.service.Labels[labels.LabelDependsOn]; ok { + useLegacy = strings.ToLower(label) == "legacy" + } + + if useLegacy { + return d.dependsOnLegacy(to, servicename) + } + + return d.dependsOnK8sAPI(to) +} + +func (d *Deployment) dependsOnLegacy(to *Deployment, servicename string) error { for _, container := range to.Spec.Template.Spec.Containers { commands := []string{} if len(container.Ports) == 0 { @@ -291,6 +303,39 @@ func (d *Deployment) DependsOn(to *Deployment, servicename string) error { return nil } +func (d *Deployment) dependsOnK8sAPI(to *Deployment) error { + script := `NAMESPACE=${NAMESPACE:-default} +SERVICE=%s +KUBERNETES_SERVICE_HOST=${KUBERNETES_SERVICE_HOST:-kubernetes.default.svc} +KUBERNETES_SERVICE_PORT=${KUBERNETES_SERVICE_PORT:-443} + +until wget -q -O- --header="Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \ + --cacert=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \ + "https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}/api/v1/namespaces/${NAMESPACE}/endpoints/${SERVICE}" \ + | grep -q '"ready":.*true'; do + sleep 2 +done` + + command := []string{"/bin/sh", "-c", fmt.Sprintf(script, to.Name)} + d.Spec.Template.Spec.InitContainers = append(d.Spec.Template.Spec.InitContainers, corev1.Container{ + Name: "wait-for-" + to.service.Name, + Image: "busybox:latest", + Command: command, + Env: []corev1.EnvVar{ + { + Name: "NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }, + }, + }) + + return nil +} + // Filename returns the filename of the deployment. func (d *Deployment) Filename() string { return d.service.Name + ".deployment.yaml" diff --git a/internal/generator/deployment_test.go b/internal/generator/deployment_test.go index 9365cf7..a83a471 100644 --- a/internal/generator/deployment_test.go +++ b/internal/generator/deployment_test.go @@ -142,6 +142,61 @@ services: if len(dt.Spec.Template.Spec.InitContainers) != 1 { t.Errorf("Expected 1 init container, got %d", len(dt.Spec.Template.Spec.InitContainers)) } + + initContainer := dt.Spec.Template.Spec.InitContainers[0] + if !strings.Contains(initContainer.Image, "busybox") { + t.Errorf("Expected busybox image, got %s", initContainer.Image) + } + + fullCommand := strings.Join(initContainer.Command, " ") + if !strings.Contains(fullCommand, "wget") { + t.Errorf("Expected wget command (K8s API method), got %s", fullCommand) + } +} + +func TestDependsOnLegacy(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + ports: + - 80:80 + depends_on: + - database + labels: + katenary.v3/depends-on: legacy + + database: + image: mariadb:10.5 + ports: + - 3306:3306 +` + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + output := internalCompileTest(t, "-s", webTemplateOutput) + dt := v1.Deployment{} + if err := yaml.Unmarshal([]byte(output), &dt); err != nil { + t.Errorf(unmarshalError, err) + } + + if len(dt.Spec.Template.Spec.InitContainers) != 1 { + t.Errorf("Expected 1 init container, got %d", len(dt.Spec.Template.Spec.InitContainers)) + } + + initContainer := dt.Spec.Template.Spec.InitContainers[0] + if !strings.Contains(initContainer.Image, "busybox") { + t.Errorf("Expected busybox image, got %s", initContainer.Image) + } + + fullCommand := strings.Join(initContainer.Command, " ") + if !strings.Contains(fullCommand, "nc") { + t.Errorf("Expected nc (netcat) command for legacy method, got %s", fullCommand) + } } func TestHelmDependencies(t *testing.T) { diff --git a/internal/generator/labels/katenaryLabels.go b/internal/generator/labels/katenaryLabels.go index bfe60c2..9ad1948 100644 --- a/internal/generator/labels/katenaryLabels.go +++ b/internal/generator/labels/katenaryLabels.go @@ -36,6 +36,7 @@ const ( LabelEnvFrom Label = KatenaryLabelPrefix + "/env-from" LabelExchangeVolume Label = KatenaryLabelPrefix + "/exchange-volumes" LabelValuesFrom Label = KatenaryLabelPrefix + "/values-from" + LabelDependsOn Label = KatenaryLabelPrefix + "/depends-on" ) var ( diff --git a/internal/generator/labels/katenaryLabelsDoc.yaml b/internal/generator/labels/katenaryLabelsDoc.yaml index 5833b3f..1a8d33c 100644 --- a/internal/generator/labels/katenaryLabelsDoc.yaml +++ b/internal/generator/labels/katenaryLabelsDoc.yaml @@ -355,4 +355,25 @@ DB_USER: database.MARIADB_USER DB_PASSWORD: database.MARIADB_PASSWORD +"depends-on": + short: "Method to check if a service is ready (for depends_on)." + long: |- + When a service uses `depends_on`, Katenary creates an initContainer to wait + for the dependent service to be ready. + + By default, Katenary uses the Kubernetes API to check if the service endpoint + has ready addresses. This method does not require the service to expose a port. + + Set this label to `legacy` to use the old netcat method that requires a port + to be defined for the dependent service. + example: |- + web: + image: nginx + depends_on: + - database + labels: + # Use legacy netcat method (requires port) + {{ .KatenaryPrefix }}/depends-on: legacy + type: "string" + # vim: ft=gotmpl.yaml