From 78b5af747e75272d0ee6d134e9cee669a1c23549 Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Sun, 8 Mar 2026 23:47:13 +0100 Subject: [PATCH 01/12] 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 -- 2.49.1 From 8fc9cb31c4e9c05d294a5031f1a022ae2d1913a8 Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Sun, 8 Mar 2026 23:50:29 +0100 Subject: [PATCH 02/12] feat(depends): Check call to kubernetes API --- internal/generator/deployment_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/internal/generator/deployment_test.go b/internal/generator/deployment_test.go index a83a471..42ce86c 100644 --- a/internal/generator/deployment_test.go +++ b/internal/generator/deployment_test.go @@ -152,6 +152,31 @@ services: if !strings.Contains(fullCommand, "wget") { t.Errorf("Expected wget command (K8s API method), got %s", fullCommand) } + + if !strings.Contains(fullCommand, "/api/v1/namespaces/") { + t.Errorf("Expected Kubernetes API call to /api/v1/namespaces/, got %s", fullCommand) + } + + if !strings.Contains(fullCommand, "/endpoints/") { + t.Errorf("Expected Kubernetes API call to /endpoints/, got %s", fullCommand) + } + + if len(initContainer.Env) == 0 { + t.Errorf("Expected environment variables to be set for namespace") + } + + hasNamespace := false + for _, env := range initContainer.Env { + if env.Name == "NAMESPACE" && env.ValueFrom != nil && env.ValueFrom.FieldRef != nil { + if env.ValueFrom.FieldRef.FieldPath == "metadata.namespace" { + hasNamespace = true + break + } + } + } + if !hasNamespace { + t.Errorf("Expected NAMESPACE env var with metadata.namespace fieldRef") + } } func TestDependsOnLegacy(t *testing.T) { -- 2.49.1 From 613baaf22913df3c1a00ba80143958ac56f63b99 Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Sun, 15 Mar 2026 08:55:24 +0100 Subject: [PATCH 03/12] feat(depends): add RBAC --- go.mod | 3 + internal/generator/deployment.go | 29 ++-- internal/generator/deployment_test.go | 196 ++++++++++++++++++++++++++ internal/generator/generator.go | 65 ++++++++- internal/generator/rbac.go | 73 ++++++++++ 5 files changed, 355 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index ef72875..6298f93 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/invopop/jsonschema v0.13.0 github.com/mitchellh/go-wordwrap v1.0.1 github.com/spf13/cobra v1.10.1 + github.com/stretchr/testify v1.10.0 github.com/thediveo/netdb v1.1.2 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.34.1 @@ -19,6 +20,7 @@ require ( require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -35,6 +37,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.10 // indirect diff --git a/internal/generator/deployment.go b/internal/generator/deployment.go index e692226..a64d90e 100644 --- a/internal/generator/deployment.go +++ b/internal/generator/deployment.go @@ -33,15 +33,16 @@ type ConfigMapMount struct { // Deployment is a kubernetes Deployment. type Deployment struct { - *appsv1.Deployment `yaml:",inline"` - chart *HelmChart `yaml:"-"` - configMaps map[string]*ConfigMapMount `yaml:"-"` - volumeMap map[string]string `yaml:"-"` // keep map of fixed named to original volume name - service *types.ServiceConfig `yaml:"-"` - defaultTag string `yaml:"-"` - isMainApp bool `yaml:"-"` - exchangesVolumes map[string]*labelstructs.ExchangeVolume `yaml:"-"` - boundEnvVar []string `yaml:"-"` // environement to remove + *appsv1.Deployment `yaml:",inline"` + chart *HelmChart `yaml:"-"` + configMaps map[string]*ConfigMapMount `yaml:"-"` + volumeMap map[string]string `yaml:"-"` // keep map of fixed named to original volume name + service *types.ServiceConfig `yaml:"-"` + defaultTag string `yaml:"-"` + isMainApp bool `yaml:"-"` + exchangesVolumes map[string]*labelstructs.ExchangeVolume `yaml:"-"` + boundEnvVar []string `yaml:"-"` // environement to remove + needsServiceAccount bool `yaml:"-"` } // NewDeployment creates a new Deployment from a compose service. The appName is the name of the application taken from the project name. @@ -273,6 +274,7 @@ func (d *Deployment) DependsOn(to *Deployment, servicename string) error { return d.dependsOnLegacy(to, servicename) } + d.needsServiceAccount = true return d.dependsOnK8sAPI(to) } @@ -611,7 +613,7 @@ func (d *Deployment) Yaml() ([]byte, error) { } // manage serviceAccount, add condition to use the serviceAccount from values.yaml - if strings.Contains(line, "serviceAccountName:") { + if strings.Contains(line, "serviceAccountName:") && !d.needsServiceAccount { spaces = strings.Repeat(" ", utils.CountStartingSpaces(line)) pre := spaces + `{{- if ne .Values.` + serviceName + `.serviceAccount "" }}` post := spaces + "{{- end }}" @@ -647,6 +649,13 @@ func (d *Deployment) Yaml() ([]byte, error) { return []byte(strings.Join(content, "\n")), nil } +func (d *Deployment) SetServiceAccountName() { + if d.needsServiceAccount { + d.Spec.Template.Spec.ServiceAccountName = utils.TplName(d.service.Name, d.chart.Name) + } else { + } +} + func (d *Deployment) appendDirectoryToConfigMap(service types.ServiceConfig, appName string, volume types.ServiceVolumeConfig) { pathnme := utils.PathToName(volume.Source) if _, ok := d.configMaps[pathnme]; !ok { diff --git a/internal/generator/deployment_test.go b/internal/generator/deployment_test.go index 42ce86c..e7e92d8 100644 --- a/internal/generator/deployment_test.go +++ b/internal/generator/deployment_test.go @@ -9,8 +9,10 @@ import ( "katenary.io/internal/generator/labels" yaml3 "gopkg.in/yaml.v3" + appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" "sigs.k8s.io/yaml" ) @@ -643,3 +645,197 @@ services: t.Errorf("Expected command to be 'bar baz', got %s", strings.Join(command, " ")) } } + +func TestRestrictedRBACGeneration(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + ports: + - 80:80 + depends_on: + - database + + database: + image: mariadb:10.5 + ports: + - 3306:3306 +` + tmpDir := setup(composeFile) + defer teardown(tmpDir) + + currentDir, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(currentDir) + + rbacOutput := internalCompileTest(t, "-s", "templates/web/depends-on.rbac.yaml") + + docs := strings.Split(rbacOutput, "---\n") + + // Filter out empty documents and strip helm template comments + var filteredDocs []string + for _, doc := range docs { + if strings.TrimSpace(doc) != "" { + // Remove '# Source:' comment lines that helm template adds + lines := strings.Split(doc, "\n") + var contentLines []string + for _, line := range lines { + if !strings.HasPrefix(strings.TrimSpace(line), "# Source:") { + contentLines = append(contentLines, line) + } + } + filteredDocs = append(filteredDocs, strings.Join(contentLines, "\n")) + } + } + + if len(filteredDocs) != 3 { + t.Fatalf("Expected 3 YAML documents in RBAC file, got %d (filtered from %d)", len(filteredDocs), len(docs)) + } + + var sa corev1.ServiceAccount + if err := yaml.Unmarshal([]byte(strings.TrimSpace(filteredDocs[0])), &sa); err != nil { + t.Errorf("Failed to unmarshal ServiceAccount: %v", err) + } + if sa.Kind != "ServiceAccount" { + t.Errorf("Expected Kind=ServiceAccount, got %s", sa.Kind) + } + if !strings.Contains(sa.Name, "web") { + t.Errorf("Expected ServiceAccount name to contain 'web', got %s", sa.Name) + } + + var role rbacv1.Role + if err := yaml.Unmarshal([]byte(strings.TrimSpace(filteredDocs[1])), &role); err != nil { + t.Errorf("Failed to unmarshal Role: %v", err) + } + if role.Kind != "Role" { + t.Errorf("Expected Kind=Role, got %s", role.Kind) + } + if len(role.Rules) != 1 { + t.Errorf("Expected 1 rule in Role, got %d", len(role.Rules)) + } + + rule := role.Rules[0] + if !contains(rule.APIGroups, "") { + t.Error("Expected APIGroup to include core API ('')") + } + if !contains(rule.Resources, "endpoints") { + t.Errorf("Expected Resource to include 'endpoints', got %v", rule.Resources) + } + + for _, res := range rule.Resources { + if res == "*" { + t.Error("Role should not have wildcard (*) resource permissions") + } + } + for _, verb := range rule.Verbs { + if verb == "*" { + t.Error("Role should not have wildcard (*) verb permissions") + } + } + + var rb rbacv1.RoleBinding + if err := yaml.Unmarshal([]byte(strings.TrimSpace(filteredDocs[2])), &rb); err != nil { + t.Errorf("Failed to unmarshal RoleBinding: %v", err) + } + if rb.Kind != "RoleBinding" { + t.Errorf("Expected Kind=RoleBinding, got %s", rb.Kind) + } + if len(rb.Subjects) != 1 { + t.Errorf("Expected 1 subject in RoleBinding, got %d", len(rb.Subjects)) + } + if rb.Subjects[0].Kind != "ServiceAccount" { + t.Errorf("Expected Subject Kind=ServiceAccount, got %s", rb.Subjects[0].Kind) + } + + // Helm template renders the name, so check if it contains "web" + if !strings.Contains(rb.RoleRef.Name, "web") { + t.Errorf("Expected RoleRef Name to contain 'web', got %s", rb.RoleRef.Name) + } + if rb.RoleRef.Kind != "Role" { + t.Errorf("Expected RoleRef Kind=Role, got %s", rb.RoleRef.Kind) + } +} + +func TestDeploymentReferencesServiceAccount(t *testing.T) { + composeFile := ` +services: + web: + image: nginx:1.29 + ports: + - 80:80 + depends_on: + - database + + 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", "templates/web/deployment.yaml") + + var dt appsv1.Deployment + if err := yaml.Unmarshal([]byte(output), &dt); err != nil { + t.Errorf("Failed to unmarshal Deployment: %v", err) + } + + serviceAccountName := dt.Spec.Template.Spec.ServiceAccountName + if !strings.Contains(serviceAccountName, "web") { + t.Errorf("Expected ServiceAccountName to contain 'web', got %s", serviceAccountName) + } + + if len(dt.Spec.Template.Spec.InitContainers) == 0 { + t.Fatal("Expected at least one init container for depends_on") + } + + initContainer := dt.Spec.Template.Spec.InitContainers[0] + if initContainer.Name != "wait-for-database" { + t.Errorf("Expected init container name 'wait-for-database', got %s", initContainer.Name) + } + + fullCommand := strings.Join(initContainer.Command, " ") + if !strings.Contains(fullCommand, "wget") { + t.Error("Expected init container to use wget for K8s API calls") + } + if !strings.Contains(fullCommand, "/api/v1/namespaces/") { + t.Error("Expected init container to call /api/v1/namespaces/ endpoint") + } + if !strings.Contains(fullCommand, "/endpoints/") { + t.Error("Expected init container to access /endpoints/ resource") + } + + hasNamespace := false + for _, env := range initContainer.Env { + if env.Name == "NAMESPACE" && env.ValueFrom != nil && env.ValueFrom.FieldRef != nil { + if env.ValueFrom.FieldRef.FieldPath == "metadata.namespace" { + hasNamespace = true + break + } + } + } + if !hasNamespace { + t.Error("Expected NAMESPACE env var with metadata.namespace fieldRef") + } + + _, err := os.Stat("./chart/templates/web/depends-on.rbac.yaml") + if os.IsNotExist(err) { + t.Error("RBAC file depends-on.rbac.yaml should exist for service using depends_on with K8s API") + } else if err != nil { + t.Errorf("Unexpected error checking RBAC file: %v", err) + } +} + +func contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} diff --git a/internal/generator/generator.go b/internal/generator/generator.go index daba5f5..0951b30 100644 --- a/internal/generator/generator.go +++ b/internal/generator/generator.go @@ -22,7 +22,7 @@ import ( // The Generate function will create the HelmChart object this way: // // - Detect the service port name or leave the port number if not found. -// - Create a deployment for each service that are not ingnore. +// - Create a deployment for each service that are not ingore. // - Create a service and ingresses for each service that has ports and/or declared ingresses. // - Create a PVC or Configmap volumes for each volume. // - Create init containers for each service which has dependencies to other services. @@ -134,6 +134,12 @@ func Generate(project *types.Project) (*HelmChart, error) { } } } + + // set ServiceAccountName for deployments that need it + for _, d := range deployments { + d.SetServiceAccountName() + } + for _, name := range drops { delete(deployments, name) } @@ -142,6 +148,11 @@ func Generate(project *types.Project) (*HelmChart, error) { chart.setEnvironmentValuesFrom(s, deployments) } + // generate RBAC resources for services that need K8s API access (non-legacy depends_on) + if err := chart.generateRBAC(deployments); err != nil { + logger.Fatalf("error generating RBAC: %s", err) + } + // generate configmaps with environment variables if err := chart.generateConfigMapsAndSecrets(project); err != nil { logger.Fatalf("error generating configmaps and secrets: %s", err) @@ -440,6 +451,58 @@ func samePodVolume(service types.ServiceConfig, v types.ServiceVolumeConfig, dep return false } +// generateRBAC creates RBAC resources (ServiceAccount, Role, RoleBinding) for services that need K8s API access. +// A service needs RBAC if it has non-legacy depends_on relationships. +func (chart *HelmChart) generateRBAC(deployments map[string]*Deployment) error { + serviceMap := make(map[string]bool) + + for _, d := range deployments { + if !d.needsServiceAccount { + continue + } + + sa := NewServiceAccount(*d.service, chart.Name) + role := NewRestrictedRole(*d.service, chart.Name) + rb := NewRestrictedRoleBinding(*d.service, chart.Name) + + var buf bytes.Buffer + + saYaml, err := yaml.Marshal(sa.ServiceAccount) + if err != nil { + return fmt.Errorf("error marshaling ServiceAccount for %s: %w", d.service.Name, err) + } + buf.Write(saYaml) + buf.WriteString("---\n") + + roleYaml, err := yaml.Marshal(role.Role) + if err != nil { + return fmt.Errorf("error marshaling Role for %s: %w", d.service.Name, err) + } + buf.Write(roleYaml) + buf.WriteString("---\n") + + rbYaml, err := yaml.Marshal(rb.RoleBinding) + if err != nil { + return fmt.Errorf("error marshaling RoleBinding for %s: %w", d.service.Name, err) + } + buf.Write(rbYaml) + + filename := d.service.Name + "/depends-on.rbac.yaml" + chart.Templates[filename] = &ChartTemplate{ + Content: buf.Bytes(), + Servicename: d.service.Name, + } + + serviceMap[d.service.Name] = true + } + + for svcName := range serviceMap { + logger.Log(logger.IconPackage, "Creating RBAC", svcName) + } + + return nil +} + func fixContainerNames(project *types.Project) { // fix container names to be unique for i, service := range project.Services { diff --git a/internal/generator/rbac.go b/internal/generator/rbac.go index b888ec0..6c15abd 100644 --- a/internal/generator/rbac.go +++ b/internal/generator/rbac.go @@ -128,6 +128,79 @@ func (r *Role) Yaml() ([]byte, error) { } } +// NewServiceAccount creates a new ServiceAccount from a compose service. +func NewServiceAccount(service types.ServiceConfig, appName string) *ServiceAccount { + return &ServiceAccount{ + ServiceAccount: &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + Kind: "ServiceAccount", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: utils.TplName(service.Name, appName), + Labels: GetLabels(service.Name, appName), + Annotations: Annotations, + }, + }, + service: &service, + } +} + +// NewRestrictedRole creates a Role with minimal permissions for init containers. +func NewRestrictedRole(service types.ServiceConfig, appName string) *Role { + return &Role{ + Role: &rbacv1.Role{ + TypeMeta: metav1.TypeMeta{ + Kind: "Role", + APIVersion: "rbac.authorization.k8s.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: utils.TplName(service.Name, appName), + Labels: GetLabels(service.Name, appName), + Annotations: Annotations, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"endpoints"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + service: &service, + } +} + +// NewRestrictedRoleBinding creates a RoleBinding that binds the restricted role to the ServiceAccount. +func NewRestrictedRoleBinding(service types.ServiceConfig, appName string) *RoleBinding { + return &RoleBinding{ + RoleBinding: &rbacv1.RoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "RoleBinding", + APIVersion: "rbac.authorization.k8s.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: utils.TplName(service.Name, appName), + Labels: GetLabels(service.Name, appName), + Annotations: Annotations, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: utils.TplName(service.Name, appName), + Namespace: "{{ .Release.Namespace }}", + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "Role", + Name: utils.TplName(service.Name, appName), + APIGroup: "rbac.authorization.k8s.io", + }, + }, + service: &service, + } +} + // ServiceAccount is a kubernetes ServiceAccount. type ServiceAccount struct { *corev1.ServiceAccount -- 2.49.1 From f175416ac23f62006ea6fd3ecfe72c3428d2e3b1 Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Sun, 15 Mar 2026 09:43:16 +0100 Subject: [PATCH 04/12] feat(quality): fix duplicates and modernize --- internal/generator/deployment_test.go | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/internal/generator/deployment_test.go b/internal/generator/deployment_test.go index e7e92d8..7dbd146 100644 --- a/internal/generator/deployment_test.go +++ b/internal/generator/deployment_test.go @@ -3,13 +3,13 @@ package generator import ( "fmt" "os" + "slices" "strings" "testing" "katenary.io/internal/generator/labels" yaml3 "gopkg.in/yaml.v3" - appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -671,7 +671,7 @@ services: rbacOutput := internalCompileTest(t, "-s", "templates/web/depends-on.rbac.yaml") docs := strings.Split(rbacOutput, "---\n") - + // Filter out empty documents and strip helm template comments var filteredDocs []string for _, doc := range docs { @@ -687,7 +687,7 @@ services: filteredDocs = append(filteredDocs, strings.Join(contentLines, "\n")) } } - + if len(filteredDocs) != 3 { t.Fatalf("Expected 3 YAML documents in RBAC file, got %d (filtered from %d)", len(filteredDocs), len(docs)) } @@ -780,7 +780,7 @@ services: output := internalCompileTest(t, "-s", "templates/web/deployment.yaml") - var dt appsv1.Deployment + var dt v1.Deployment if err := yaml.Unmarshal([]byte(output), &dt); err != nil { t.Errorf("Failed to unmarshal Deployment: %v", err) } @@ -832,10 +832,5 @@ services: } func contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { - return true - } - } - return false + return slices.Contains(slice, item) } -- 2.49.1 From 7e1bbdc9b3f0c45bddf060a38c9aa1250e815878 Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Sun, 15 Mar 2026 09:43:58 +0100 Subject: [PATCH 05/12] feat(quality): remove unused modules --- go.mod | 3 --- 1 file changed, 3 deletions(-) diff --git a/go.mod b/go.mod index 6298f93..ef72875 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ require ( github.com/invopop/jsonschema v0.13.0 github.com/mitchellh/go-wordwrap v1.0.1 github.com/spf13/cobra v1.10.1 - github.com/stretchr/testify v1.10.0 github.com/thediveo/netdb v1.1.2 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.34.1 @@ -20,7 +19,6 @@ require ( require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -37,7 +35,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.10 // indirect -- 2.49.1 From 5d839035b9d9dcc2aa4644fb022014d5d7ecc2dd Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Sun, 15 Mar 2026 10:15:47 +0100 Subject: [PATCH 06/12] feat(depends): add suffix on RBAC and SA --- internal/generator/deployment.go | 2 +- internal/generator/rbac.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/generator/deployment.go b/internal/generator/deployment.go index a64d90e..1e0a7ec 100644 --- a/internal/generator/deployment.go +++ b/internal/generator/deployment.go @@ -651,7 +651,7 @@ func (d *Deployment) Yaml() ([]byte, error) { func (d *Deployment) SetServiceAccountName() { if d.needsServiceAccount { - d.Spec.Template.Spec.ServiceAccountName = utils.TplName(d.service.Name, d.chart.Name) + d.Spec.Template.Spec.ServiceAccountName = utils.TplName(d.service.Name, d.chart.Name, "dependency") } else { } } diff --git a/internal/generator/rbac.go b/internal/generator/rbac.go index 6c15abd..36b088c 100644 --- a/internal/generator/rbac.go +++ b/internal/generator/rbac.go @@ -32,7 +32,7 @@ func NewRBAC(service types.ServiceConfig, appName string) *RBAC { APIVersion: "rbac.authorization.k8s.io/v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: utils.TplName(service.Name, appName), + Name: utils.TplName(service.Name, appName, "dependency"), Labels: GetLabels(service.Name, appName), Annotations: Annotations, }, @@ -155,7 +155,7 @@ func NewRestrictedRole(service types.ServiceConfig, appName string) *Role { APIVersion: "rbac.authorization.k8s.io/v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: utils.TplName(service.Name, appName), + Name: utils.TplName(service.Name, appName, "dependency"), Labels: GetLabels(service.Name, appName), Annotations: Annotations, }, @@ -180,20 +180,20 @@ func NewRestrictedRoleBinding(service types.ServiceConfig, appName string) *Role APIVersion: "rbac.authorization.k8s.io/v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: utils.TplName(service.Name, appName), + Name: utils.TplName(service.Name, appName, "dependency"), Labels: GetLabels(service.Name, appName), Annotations: Annotations, }, Subjects: []rbacv1.Subject{ { Kind: "ServiceAccount", - Name: utils.TplName(service.Name, appName), + Name: utils.TplName(service.Name, appName, "dependency"), Namespace: "{{ .Release.Namespace }}", }, }, RoleRef: rbacv1.RoleRef{ Kind: "Role", - Name: utils.TplName(service.Name, appName), + Name: utils.TplName(service.Name, appName, "dependency"), APIGroup: "rbac.authorization.k8s.io", }, }, -- 2.49.1 From 0e133ae6dbe998bdcfad4aee0a33caceda23bf7c Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Sun, 15 Mar 2026 21:50:26 +0100 Subject: [PATCH 07/12] fix(path): fixing the default compose file check --- internal/generator/converter.go | 19 +++++++++++++++---- internal/parser/main.go | 11 ++++++++--- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/internal/generator/converter.go b/internal/generator/converter.go index ca823e8..afde74c 100644 --- a/internal/generator/converter.go +++ b/internal/generator/converter.go @@ -109,8 +109,19 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) error { // the current working directory is the directory currentDir, _ := os.Getwd() + // Filter to only existing files before chdir + var existingFiles []string + for _, f := range dockerComposeFile { + if _, err := os.Stat(f); err == nil { + existingFiles = append(existingFiles, f) + } + } + if len(existingFiles) == 0 && len(dockerComposeFile) > 0 { + return fmt.Errorf("no compose file found: %v", dockerComposeFile) + } + // go to the root of the project - if err := os.Chdir(filepath.Dir(dockerComposeFile[0])); err != nil { + if err := os.Chdir(filepath.Dir(existingFiles[0])); err != nil { logger.Failure(err.Error()) return err } @@ -122,12 +133,12 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) error { }() // repove the directory part of the docker-compose files - for i, f := range dockerComposeFile { - dockerComposeFile[i] = filepath.Base(f) + for i, f := range existingFiles { + existingFiles[i] = filepath.Base(f) } // parse the compose files - project, err := parser.Parse(config.Profiles, config.EnvFiles, dockerComposeFile...) + project, err := parser.Parse(config.Profiles, config.EnvFiles, existingFiles...) if err != nil { logger.Failure("Cannot parse compose files", err.Error()) return err diff --git a/internal/parser/main.go b/internal/parser/main.go index 6b9ad8d..c3a8cd7 100644 --- a/internal/parser/main.go +++ b/internal/parser/main.go @@ -41,16 +41,21 @@ func Parse(profiles []string, envFiles []string, dockerComposeFile ...string) (* } } - options, err := cli.NewProjectOptions(dockerComposeFile, + opts := []cli.ProjectOptionsFn{ cli.WithProfiles(profiles), cli.WithInterpolation(true), - cli.WithDefaultConfigPath, cli.WithEnvFiles(envFiles...), cli.WithOsEnv, cli.WithDotEnv, cli.WithNormalization(true), cli.WithResolvedPaths(false), - ) + } + + if len(dockerComposeFile) == 0 { + opts = append(opts, cli.WithDefaultConfigPath) + } + + options, err := cli.NewProjectOptions(dockerComposeFile, opts...) if err != nil { return nil, err } -- 2.49.1 From 985418ae51bf7a8dde3c2c78ee90f383d7f83125 Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Mon, 16 Mar 2026 21:37:50 +0100 Subject: [PATCH 08/12] feat(quality): fixes some bad patterns --- internal/generator/deployment.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/generator/deployment.go b/internal/generator/deployment.go index 1e0a7ec..51e2697 100644 --- a/internal/generator/deployment.go +++ b/internal/generator/deployment.go @@ -19,6 +19,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const shCommand = "/bin/sh" + var _ Yaml = (*Deployment)(nil) type mountPathConfig struct { @@ -294,7 +296,7 @@ func (d *Deployment) dependsOnLegacy(to *Deployment, servicename string) error { commands = append(commands, command) } - command := []string{"/bin/sh", "-c", strings.Join(commands, "\n")} + command := []string{shCommand, "-c", strings.Join(commands, "\n")} d.Spec.Template.Spec.InitContainers = append(d.Spec.Template.Spec.InitContainers, corev1.Container{ Name: "wait-for-" + to.service.Name, Image: "busybox:latest", @@ -318,7 +320,7 @@ until wget -q -O- --header="Authorization: Bearer $(cat /var/run/secrets/kuberne sleep 2 done` - command := []string{"/bin/sh", "-c", fmt.Sprintf(script, to.Name)} + command := []string{shCommand, "-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", @@ -652,7 +654,6 @@ func (d *Deployment) Yaml() ([]byte, error) { func (d *Deployment) SetServiceAccountName() { if d.needsServiceAccount { d.Spec.Template.Spec.ServiceAccountName = utils.TplName(d.service.Name, d.chart.Name, "dependency") - } else { } } -- 2.49.1 From e879c3f10f2a8c88f08dd6eb7ed0533195008a04 Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Mon, 16 Mar 2026 22:20:24 +0100 Subject: [PATCH 09/12] feat(depends): Warn user that the service is not created --- doc/docs/labels.md | 47 +++- doc/docs/packages/internal/generator.md | 206 ++++++++++-------- .../packages/internal/generator/extrafiles.md | 4 +- .../internal/generator/katenaryfile.md | 8 +- .../packages/internal/generator/labels.md | 15 +- .../internal/generator/labels/labelstructs.md | 46 ++-- doc/docs/packages/internal/logger.md | 93 +++++++- doc/docs/packages/internal/parser.md | 2 +- doc/docs/packages/internal/utils.md | 36 +-- doc/docs/usage.md | 21 ++ doc/share/man/man1/katenary.1 | 4 +- internal/generator/generator.go | 11 + .../generator/labels/katenaryLabelsDoc.yaml | 20 +- 13 files changed, 362 insertions(+), 151 deletions(-) diff --git a/doc/docs/labels.md b/doc/docs/labels.md index 43409ae..0f718ef 100644 --- a/doc/docs/labels.md +++ b/doc/docs/labels.md @@ -21,6 +21,7 @@ Katenary will try to _Unmarshal_ these labels. | `katenary.v3/configmap-files` | Inject files as Configmap. | `[]string` | | `katenary.v3/cronjob` | Create a cronjob from the service. | `object` | | `katenary.v3/dependencies` | Add Helm dependencies to the service. | `[]object` | +| `katenary.v3/depends-on` | Method to check if a service is ready (for depends_on). | `string` | | `katenary.v3/description` | Description of the service | `string` | | `katenary.v3/env-from` | Add environment variables from another service. | `[]string` | | `katenary.v3/exchange-volumes` | Add exchange volumes (empty directory on the node) to share data | `[]object` | @@ -144,6 +145,45 @@ labels: ``` +### katenary.v3/depends-on + +Method to check if a service is ready (for depends_on). + +**Type**: `string` + +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 +and does not create a Kubernetes Service automatically. + +If you need to create a Kubernetes Service for external access, use the +`katenary.v3/ports` label instead. + +Set this label to `legacy` to use the old netcat method that requires a port +to be defined for the dependent service. + +**Example:** + +```yaml +web: + image: nginx + depends_on: + - database + labels: + # Use legacy netcat method (requires port) + katenary.v3/depends-on: legacy + +database: + image: mysql + labels: + # Create a Kubernetes Service for external access + katenary.v3/ports: + - 3306 +``` + + ### katenary.v3/description Description of the service @@ -352,7 +392,12 @@ Ports to be added to the service. **Type**: `[]uint32` Only useful for services without exposed port. It is mandatory if the -service is a dependency of another service. +service is a dependency of another service AND you want to create a +Kubernetes Service for external access. + +If you only need to check if the service is ready (using depends_on), +you don't need to declare ports. The service will not be created automatically +unless you add this label. **Example:** diff --git a/doc/docs/packages/internal/generator.md b/doc/docs/packages/internal/generator.md index f22befb..a4520d9 100644 --- a/doc/docs/packages/internal/generator.md +++ b/doc/docs/packages/internal/generator.md @@ -35,7 +35,7 @@ var Version = "master" // changed at compile time ``` -## func [Convert]() +## func [Convert]() ```go func Convert(config ConvertOptions, dockerComposeFile ...string) error @@ -44,7 +44,7 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) error Convert a compose \(docker, podman...\) project to a helm chart. It calls Generate\(\) to generate the chart and then write it to the disk. -## func [GetLabels]() +## func [GetLabels]() ```go func GetLabels(serviceName, appName string) map[string]string @@ -53,7 +53,7 @@ func GetLabels(serviceName, appName string) map[string]string GetLabels returns the labels for a service. It uses the appName to replace the \_\_replace\_\_ in the labels. This is used to generate the labels in the templates. -## func [GetMatchLabels]() +## func [GetMatchLabels]() ```go func GetMatchLabels(serviceName, appName string) map[string]string @@ -62,7 +62,7 @@ func GetMatchLabels(serviceName, appName string) map[string]string GetMatchLabels returns the matchLabels for a service. It uses the appName to replace the \_\_replace\_\_ in the labels. This is used to generate the matchLabels in the templates. -## func [GetVersion]() +## func [GetVersion]() ```go func GetVersion() string @@ -71,7 +71,7 @@ func GetVersion() string GetVersion return the version of katneary. It's important to understand that the version is set at compile time for the github release. But, it the user get katneary using \`go install\`, the version should be different. -## func [Helper]() +## func [Helper]() ```go func Helper(name string) string @@ -80,7 +80,7 @@ func Helper(name string) string Helper returns the \_helpers.tpl file for a chart. -## func [NewCronJob]() +## func [NewCronJob]() ```go func NewCronJob(service types.ServiceConfig, chart *HelmChart, appName string) (*CronJob, *RBAC) @@ -89,7 +89,7 @@ func NewCronJob(service types.ServiceConfig, chart *HelmChart, appName string) ( NewCronJob creates a new CronJob from a compose service. The appName is the name of the application taken from the project name. -## func [ToK8SYaml]() +## func [ToK8SYaml]() ```go func ToK8SYaml(obj any) ([]byte, error) @@ -98,7 +98,7 @@ func ToK8SYaml(obj any) ([]byte, error) -## func [UnWrapTPL]() +## func [UnWrapTPL]() ```go func UnWrapTPL(in []byte) []byte @@ -107,7 +107,7 @@ func UnWrapTPL(in []byte) []byte UnWrapTPL removes the line wrapping from a template. -## type [ChartTemplate]() +## type [ChartTemplate]() ChartTemplate is a template of a chart. It contains the content of the template and the name of the service. This is used internally to generate the templates. @@ -119,7 +119,7 @@ type ChartTemplate struct { ``` -## type [ConfigMap]() +## type [ConfigMap]() ConfigMap is a kubernetes ConfigMap. Implements the DataMap interface. @@ -131,7 +131,7 @@ type ConfigMap struct { ``` -### func [NewConfigMap]() +### func [NewConfigMap]() ```go func NewConfigMap(service types.ServiceConfig, appName string, forFile bool) *ConfigMap @@ -140,7 +140,7 @@ func NewConfigMap(service types.ServiceConfig, appName string, forFile bool) *Co NewConfigMap creates a new ConfigMap from a compose service. The appName is the name of the application taken from the project name. The ConfigMap is filled by environment variables and labels "map\-env". -### func [NewConfigMapFromDirectory]() +### func [NewConfigMapFromDirectory]() ```go func NewConfigMapFromDirectory(service types.ServiceConfig, appName, path string) *ConfigMap @@ -149,7 +149,7 @@ func NewConfigMapFromDirectory(service types.ServiceConfig, appName, path string NewConfigMapFromDirectory creates a new ConfigMap from a compose service. This path is the path to the file or directory. If the path is a directory, all files in the directory are added to the ConfigMap. Each subdirectory are ignored. Note that the Generate\(\) function will create the subdirectories ConfigMaps. -### func \(\*ConfigMap\) [AddBinaryData]() +### func \(\*ConfigMap\) [AddBinaryData]() ```go func (c *ConfigMap) AddBinaryData(key string, value []byte) @@ -158,7 +158,7 @@ func (c *ConfigMap) AddBinaryData(key string, value []byte) AddBinaryData adds binary data to the configmap. Append or overwrite the value if the key already exists. -### func \(\*ConfigMap\) [AddData]() +### func \(\*ConfigMap\) [AddData]() ```go func (c *ConfigMap) AddData(key, value string) @@ -167,7 +167,7 @@ func (c *ConfigMap) AddData(key, value string) AddData adds a key value pair to the configmap. Append or overwrite the value if the key already exists. -### func \(\*ConfigMap\) [AppendDir]() +### func \(\*ConfigMap\) [AppendDir]() ```go func (c *ConfigMap) AppendDir(path string) error @@ -176,7 +176,7 @@ func (c *ConfigMap) AppendDir(path string) error AppendDir adds files from given path to the configmap. It is not recursive, to add all files in a directory, you need to call this function for each subdirectory. -### func \(\*ConfigMap\) [AppendFile]() +### func \(\*ConfigMap\) [AppendFile]() ```go func (c *ConfigMap) AppendFile(path string) error @@ -185,7 +185,7 @@ func (c *ConfigMap) AppendFile(path string) error -### func \(\*ConfigMap\) [Filename]() +### func \(\*ConfigMap\) [Filename]() ```go func (c *ConfigMap) Filename() string @@ -194,7 +194,7 @@ func (c *ConfigMap) Filename() string Filename returns the filename of the configmap. If the configmap is used for files, the filename contains the path. -### func \(\*ConfigMap\) [SetData]() +### func \(\*ConfigMap\) [SetData]() ```go func (c *ConfigMap) SetData(data map[string]string) @@ -203,7 +203,7 @@ func (c *ConfigMap) SetData(data map[string]string) SetData sets the data of the configmap. It replaces the entire data. -### func \(\*ConfigMap\) [Yaml]() +### func \(\*ConfigMap\) [Yaml]() ```go func (c *ConfigMap) Yaml() ([]byte, error) @@ -212,7 +212,7 @@ func (c *ConfigMap) Yaml() ([]byte, error) Yaml returns the yaml representation of the configmap -## type [ConfigMapMount]() +## type [ConfigMapMount]() @@ -223,7 +223,7 @@ type ConfigMapMount struct { ``` -## type [ConvertOptions]() +## type [ConvertOptions]() ConvertOptions are the options to convert a compose project to a helm chart. @@ -241,7 +241,7 @@ type ConvertOptions struct { ``` -## type [CronJob]() +## type [CronJob]() CronJob is a kubernetes CronJob. @@ -253,7 +253,7 @@ type CronJob struct { ``` -### func \(\*CronJob\) [Filename]() +### func \(\*CronJob\) [Filename]() ```go func (c *CronJob) Filename() string @@ -264,7 +264,7 @@ Filename returns the filename of the cronjob. Implements the Yaml interface. -### func \(\*CronJob\) [Yaml]() +### func \(\*CronJob\) [Yaml]() ```go func (c *CronJob) Yaml() ([]byte, error) @@ -275,7 +275,7 @@ Yaml returns the yaml representation of the cronjob. Implements the Yaml interface. -## type [CronJobValue]() +## type [CronJobValue]() CronJobValue is a cronjob configuration that will be saved in values.yaml. @@ -289,7 +289,7 @@ type CronJobValue struct { ``` -## type [DataMap]() +## type [DataMap]() DataMap is a kubernetes ConfigMap or Secret. It can be used to add data to the ConfigMap or Secret. @@ -301,7 +301,7 @@ type DataMap interface { ``` -## type [Deployment]() +## type [Deployment]() Deployment is a kubernetes Deployment. @@ -313,7 +313,7 @@ type Deployment struct { ``` -### func [NewDeployment]() +### func [NewDeployment]() ```go func NewDeployment(service types.ServiceConfig, chart *HelmChart) *Deployment @@ -322,7 +322,7 @@ func NewDeployment(service types.ServiceConfig, chart *HelmChart) *Deployment NewDeployment creates a new Deployment from a compose service. The appName is the name of the application taken from the project name. It also creates the Values map that will be used to create the values.yaml file. -### func \(\*Deployment\) [AddContainer]() +### func \(\*Deployment\) [AddContainer]() ```go func (d *Deployment) AddContainer(service types.ServiceConfig) @@ -331,7 +331,7 @@ func (d *Deployment) AddContainer(service types.ServiceConfig) AddContainer adds a container to the deployment. -### func \(\*Deployment\) [AddHealthCheck]() +### func \(\*Deployment\) [AddHealthCheck]() ```go func (d *Deployment) AddHealthCheck(service types.ServiceConfig, container *corev1.Container) @@ -340,7 +340,7 @@ func (d *Deployment) AddHealthCheck(service types.ServiceConfig, container *core -### func \(\*Deployment\) [AddIngress]() +### func \(\*Deployment\) [AddIngress]() ```go func (d *Deployment) AddIngress(service types.ServiceConfig, appName string) *Ingress @@ -349,7 +349,7 @@ func (d *Deployment) AddIngress(service types.ServiceConfig, appName string) *In AddIngress adds an ingress to the deployment. It creates the ingress object. -### func \(\*Deployment\) [AddLegacyVolume]() +### func \(\*Deployment\) [AddLegacyVolume]() ```go func (d *Deployment) AddLegacyVolume(name, kind string) @@ -358,7 +358,7 @@ func (d *Deployment) AddLegacyVolume(name, kind string) -### func \(\*Deployment\) [AddVolumes]() +### func \(\*Deployment\) [AddVolumes]() ```go func (d *Deployment) AddVolumes(service types.ServiceConfig, appName string) @@ -367,7 +367,7 @@ func (d *Deployment) AddVolumes(service types.ServiceConfig, appName string) AddVolumes adds a volume to the deployment. It does not create the PVC, it only adds the volumes to the deployment. If the volume is a bind volume it will warn the user that it is not supported yet. -### func \(\*Deployment\) [BindFrom]() +### func \(\*Deployment\) [BindFrom]() ```go func (d *Deployment) BindFrom(service types.ServiceConfig, binded *Deployment) @@ -376,7 +376,7 @@ func (d *Deployment) BindFrom(service types.ServiceConfig, binded *Deployment) -### func \(\*Deployment\) [BindMapFilesToContainer]() +### func \(\*Deployment\) [BindMapFilesToContainer]() ```go func (d *Deployment) BindMapFilesToContainer(service types.ServiceConfig, secrets []string, appName string) (*corev1.Container, int) @@ -385,7 +385,7 @@ func (d *Deployment) BindMapFilesToContainer(service types.ServiceConfig, secret -### func \(\*Deployment\) [DependsOn]() +### func \(\*Deployment\) [DependsOn]() ```go func (d *Deployment) DependsOn(to *Deployment, servicename string) error @@ -394,7 +394,7 @@ func (d *Deployment) DependsOn(to *Deployment, servicename string) error DependsOn adds a initContainer to the deployment that will wait for the service to be up. -### func \(\*Deployment\) [Filename]() +### func \(\*Deployment\) [Filename]() ```go func (d *Deployment) Filename() string @@ -403,7 +403,7 @@ func (d *Deployment) Filename() string Filename returns the filename of the deployment. -### func \(\*Deployment\) [MountExchangeVolumes]() +### func \(\*Deployment\) [MountExchangeVolumes]() ```go func (d *Deployment) MountExchangeVolumes() @@ -412,7 +412,7 @@ func (d *Deployment) MountExchangeVolumes() -### func \(\*Deployment\) [SetEnvFrom]() +### func \(\*Deployment\) [SetEnvFrom]() ```go func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string, samePod ...bool) @@ -420,8 +420,17 @@ func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string, sam SetEnvFrom sets the environment variables to a configmap. The configmap is created. + +### func \(\*Deployment\) [SetServiceAccountName]() + +```go +func (d *Deployment) SetServiceAccountName() +``` + + + -### func \(\*Deployment\) [Yaml]() +### func \(\*Deployment\) [Yaml]() ```go func (d *Deployment) Yaml() ([]byte, error) @@ -430,7 +439,7 @@ func (d *Deployment) Yaml() ([]byte, error) Yaml returns the yaml representation of the deployment. -## type [FileMapUsage]() +## type [FileMapUsage]() FileMapUsage is the usage of the filemap. @@ -448,7 +457,7 @@ const ( ``` -## type [HelmChart]() +## type [HelmChart]() HelmChart is a Helm Chart representation. It contains all the templates, values, versions, helpers... @@ -471,7 +480,7 @@ type HelmChart struct { ``` -### func [Generate]() +### func [Generate]() ```go func Generate(project *types.Project) (*HelmChart, error) @@ -482,7 +491,7 @@ Generate a chart from a compose project. This does not write files to disk, it o The Generate function will create the HelmChart object this way: - Detect the service port name or leave the port number if not found. -- Create a deployment for each service that are not ingnore. +- Create a deployment for each service that are not ingore. - Create a service and ingresses for each service that has ports and/or declared ingresses. - Create a PVC or Configmap volumes for each volume. - Create init containers for each service which has dependencies to other services. @@ -491,7 +500,7 @@ The Generate function will create the HelmChart object this way: - Merge the same\-pod services. -### func [NewChart]() +### func [NewChart]() ```go func NewChart(name string) *HelmChart @@ -500,7 +509,7 @@ func NewChart(name string) *HelmChart NewChart creates a new empty chart with the given name. -### func \(\*HelmChart\) [SaveTemplates]() +### func \(\*HelmChart\) [SaveTemplates]() ```go func (chart *HelmChart) SaveTemplates(templateDir string) @@ -509,7 +518,7 @@ func (chart *HelmChart) SaveTemplates(templateDir string) SaveTemplates the templates of the chart to the given directory. -## type [Ingress]() +## type [Ingress]() @@ -521,7 +530,7 @@ type Ingress struct { ``` -### func [NewIngress]() +### func [NewIngress]() ```go func NewIngress(service types.ServiceConfig, Chart *HelmChart) *Ingress @@ -530,7 +539,7 @@ func NewIngress(service types.ServiceConfig, Chart *HelmChart) *Ingress NewIngress creates a new Ingress from a compose service. -### func \(\*Ingress\) [Filename]() +### func \(\*Ingress\) [Filename]() ```go func (ingress *Ingress) Filename() string @@ -539,7 +548,7 @@ func (ingress *Ingress) Filename() string -### func \(\*Ingress\) [Yaml]() +### func \(\*Ingress\) [Yaml]() ```go func (ingress *Ingress) Yaml() ([]byte, error) @@ -548,7 +557,7 @@ func (ingress *Ingress) Yaml() ([]byte, error) -## type [IngressValue]() +## type [IngressValue]() IngressValue is a ingress configuration that will be saved in values.yaml. @@ -564,7 +573,7 @@ type IngressValue struct { ``` -## type [PersistenceValue]() +## type [PersistenceValue]() PersistenceValue is a persistence configuration that will be saved in values.yaml. @@ -578,7 +587,7 @@ type PersistenceValue struct { ``` -## type [RBAC]() +## type [RBAC]() RBAC is a kubernetes RBAC containing a role, a rolebinding and an associated serviceaccount. @@ -591,7 +600,7 @@ type RBAC struct { ``` -### func [NewRBAC]() +### func [NewRBAC]() ```go func NewRBAC(service types.ServiceConfig, appName string) *RBAC @@ -600,7 +609,7 @@ func NewRBAC(service types.ServiceConfig, appName string) *RBAC NewRBAC creates a new RBAC from a compose service. The appName is the name of the application taken from the project name. -## type [RepositoryValue]() +## type [RepositoryValue]() RepositoryValue is a docker repository image and tag that will be saved in values.yaml. @@ -612,7 +621,7 @@ type RepositoryValue struct { ``` -## type [Role]() +## type [Role]() Role is a kubernetes Role. @@ -623,8 +632,17 @@ type Role struct { } ``` + +### func [NewRestrictedRole]() + +```go +func NewRestrictedRole(service types.ServiceConfig, appName string) *Role +``` + +NewRestrictedRole creates a Role with minimal permissions for init containers. + -### func \(\*Role\) [Filename]() +### func \(\*Role\) [Filename]() ```go func (r *Role) Filename() string @@ -633,7 +651,7 @@ func (r *Role) Filename() string -### func \(\*Role\) [Yaml]() +### func \(\*Role\) [Yaml]() ```go func (r *Role) Yaml() ([]byte, error) @@ -642,7 +660,7 @@ func (r *Role) Yaml() ([]byte, error) -## type [RoleBinding]() +## type [RoleBinding]() RoleBinding is a kubernetes RoleBinding. @@ -653,8 +671,17 @@ type RoleBinding struct { } ``` + +### func [NewRestrictedRoleBinding]() + +```go +func NewRestrictedRoleBinding(service types.ServiceConfig, appName string) *RoleBinding +``` + +NewRestrictedRoleBinding creates a RoleBinding that binds the restricted role to the ServiceAccount. + -### func \(\*RoleBinding\) [Filename]() +### func \(\*RoleBinding\) [Filename]() ```go func (r *RoleBinding) Filename() string @@ -663,7 +690,7 @@ func (r *RoleBinding) Filename() string -### func \(\*RoleBinding\) [Yaml]() +### func \(\*RoleBinding\) [Yaml]() ```go func (r *RoleBinding) Yaml() ([]byte, error) @@ -672,7 +699,7 @@ func (r *RoleBinding) Yaml() ([]byte, error) -## type [Secret]() +## type [Secret]() Secret is a kubernetes Secret. @@ -686,7 +713,7 @@ type Secret struct { ``` -### func [NewSecret]() +### func [NewSecret]() ```go func NewSecret(service types.ServiceConfig, appName string) *Secret @@ -695,7 +722,7 @@ func NewSecret(service types.ServiceConfig, appName string) *Secret NewSecret creates a new Secret from a compose service -### func \(\*Secret\) [AddData]() +### func \(\*Secret\) [AddData]() ```go func (s *Secret) AddData(key, value string) @@ -704,7 +731,7 @@ func (s *Secret) AddData(key, value string) AddData adds a key value pair to the secret. -### func \(\*Secret\) [Filename]() +### func \(\*Secret\) [Filename]() ```go func (s *Secret) Filename() string @@ -713,7 +740,7 @@ func (s *Secret) Filename() string Filename returns the filename of the secret. -### func \(\*Secret\) [SetData]() +### func \(\*Secret\) [SetData]() ```go func (s *Secret) SetData(data map[string]string) @@ -722,7 +749,7 @@ func (s *Secret) SetData(data map[string]string) SetData sets the data of the secret. -### func \(\*Secret\) [Yaml]() +### func \(\*Secret\) [Yaml]() ```go func (s *Secret) Yaml() ([]byte, error) @@ -731,7 +758,7 @@ func (s *Secret) Yaml() ([]byte, error) Yaml returns the yaml representation of the secret. -## type [Service]() +## type [Service]() Service is a kubernetes Service. @@ -743,7 +770,7 @@ type Service struct { ``` -### func [NewService]() +### func [NewService]() ```go func NewService(service types.ServiceConfig, appName string) *Service @@ -752,7 +779,7 @@ func NewService(service types.ServiceConfig, appName string) *Service NewService creates a new Service from a compose service. -### func \(\*Service\) [AddPort]() +### func \(\*Service\) [AddPort]() ```go func (s *Service) AddPort(port types.ServicePortConfig, serviceName ...string) @@ -761,7 +788,7 @@ func (s *Service) AddPort(port types.ServicePortConfig, serviceName ...string) AddPort adds a port to the service. -### func \(\*Service\) [Filename]() +### func \(\*Service\) [Filename]() ```go func (s *Service) Filename() string @@ -770,7 +797,7 @@ func (s *Service) Filename() string Filename returns the filename of the service. -### func \(\*Service\) [Yaml]() +### func \(\*Service\) [Yaml]() ```go func (s *Service) Yaml() ([]byte, error) @@ -779,7 +806,7 @@ func (s *Service) Yaml() ([]byte, error) Yaml returns the yaml representation of the service. -## type [ServiceAccount]() +## type [ServiceAccount]() ServiceAccount is a kubernetes ServiceAccount. @@ -790,8 +817,17 @@ type ServiceAccount struct { } ``` + +### func [NewServiceAccount]() + +```go +func NewServiceAccount(service types.ServiceConfig, appName string) *ServiceAccount +``` + +NewServiceAccount creates a new ServiceAccount from a compose service. + -### func \(\*ServiceAccount\) [Filename]() +### func \(\*ServiceAccount\) [Filename]() ```go func (r *ServiceAccount) Filename() string @@ -800,7 +836,7 @@ func (r *ServiceAccount) Filename() string -### func \(\*ServiceAccount\) [Yaml]() +### func \(\*ServiceAccount\) [Yaml]() ```go func (r *ServiceAccount) Yaml() ([]byte, error) @@ -809,7 +845,7 @@ func (r *ServiceAccount) Yaml() ([]byte, error) -## type [TLS]() +## type [TLS]() @@ -821,7 +857,7 @@ type TLS struct { ``` -## type [Value]() +## type [Value]() Value will be saved in values.yaml. It contains configuration for all deployment and services. @@ -841,7 +877,7 @@ type Value struct { ``` -### func [NewValue]() +### func [NewValue]() ```go func NewValue(service types.ServiceConfig, main ...bool) *Value @@ -852,7 +888,7 @@ NewValue creates a new Value from a compose service. The value contains the nece If \`main\` is true, the tag will be empty because it will be set in the helm chart appVersion. -### func \(\*Value\) [AddIngress]() +### func \(\*Value\) [AddIngress]() ```go func (v *Value) AddIngress(host, path string) @@ -861,7 +897,7 @@ func (v *Value) AddIngress(host, path string) -### func \(\*Value\) [AddPersistence]() +### func \(\*Value\) [AddPersistence]() ```go func (v *Value) AddPersistence(volumeName string) @@ -870,7 +906,7 @@ func (v *Value) AddPersistence(volumeName string) AddPersistence adds persistence configuration to the Value. -## type [VolumeClaim]() +## type [VolumeClaim]() VolumeClaim is a kubernetes VolumeClaim. This is a PersistentVolumeClaim. @@ -882,7 +918,7 @@ type VolumeClaim struct { ``` -### func [NewVolumeClaim]() +### func [NewVolumeClaim]() ```go func NewVolumeClaim(service types.ServiceConfig, volumeName, appName string) *VolumeClaim @@ -891,7 +927,7 @@ func NewVolumeClaim(service types.ServiceConfig, volumeName, appName string) *Vo NewVolumeClaim creates a new VolumeClaim from a compose service. -### func \(\*VolumeClaim\) [Filename]() +### func \(\*VolumeClaim\) [Filename]() ```go func (v *VolumeClaim) Filename() string @@ -900,7 +936,7 @@ func (v *VolumeClaim) Filename() string Filename returns the suggested filename for a VolumeClaim. -### func \(\*VolumeClaim\) [Yaml]() +### func \(\*VolumeClaim\) [Yaml]() ```go func (v *VolumeClaim) Yaml() ([]byte, error) @@ -909,7 +945,7 @@ func (v *VolumeClaim) Yaml() ([]byte, error) Yaml marshals a VolumeClaim into yaml. -## type [Yaml]() +## type [Yaml]() Yaml is a kubernetes object that can be converted to yaml. diff --git a/doc/docs/packages/internal/generator/extrafiles.md b/doc/docs/packages/internal/generator/extrafiles.md index c7483d3..1c91b37 100644 --- a/doc/docs/packages/internal/generator/extrafiles.md +++ b/doc/docs/packages/internal/generator/extrafiles.md @@ -8,7 +8,7 @@ import "katenary.io/internal/generator/extrafiles" Package extrafiles provides function to generate the Chart files that are not objects. Like README.md and notes.txt... -## func [NotesFile]() +## func [NotesFile]() ```go func NotesFile(services []string) string @@ -17,7 +17,7 @@ func NotesFile(services []string) string NotesFile returns the content of the note.txt file. -## func [ReadMeFile]() +## func [ReadMeFile]() ```go func ReadMeFile(charname, description string, values map[string]any) string diff --git a/doc/docs/packages/internal/generator/katenaryfile.md b/doc/docs/packages/internal/generator/katenaryfile.md index 214133a..0885c4a 100644 --- a/doc/docs/packages/internal/generator/katenaryfile.md +++ b/doc/docs/packages/internal/generator/katenaryfile.md @@ -12,7 +12,7 @@ A katenary file, named "katenary.yml" or "katenary.yaml", is a file where you ca Formely, the file describe the same structure as in labels, and so that can be validated and completed by LSP. It also ease the use of katenary. -## func [GenerateSchema]() +## func [GenerateSchema]() ```go func GenerateSchema() string @@ -21,7 +21,7 @@ func GenerateSchema() string GenerateSchema generates the schema for the katenary.yaml file. -## func [OverrideWithConfig]() +## func [OverrideWithConfig]() ```go func OverrideWithConfig(project *types.Project) @@ -30,7 +30,7 @@ func OverrideWithConfig(project *types.Project) OverrideWithConfig overrides the project with the katenary.yaml file. It will set the labels of the services with the values from the katenary.yaml file. It work in memory, so it will not modify the original project. -## type [Service]() +## type [Service]() Service is a struct that contains the service configuration for katenary @@ -56,7 +56,7 @@ type Service struct { ``` -## type [StringOrMap]() +## type [StringOrMap]() StringOrMap is a struct that can be either a string or a map of strings. It's a helper struct to unmarshal the katenary.yaml file and produce the schema diff --git a/doc/docs/packages/internal/generator/labels.md b/doc/docs/packages/internal/generator/labels.md index 617c95c..e343b6c 100644 --- a/doc/docs/packages/internal/generator/labels.md +++ b/doc/docs/packages/internal/generator/labels.md @@ -17,7 +17,7 @@ const KatenaryLabelPrefix = "katenary.v3" ``` -## func [GetLabelHelp]() +## func [GetLabelHelp]() ```go func GetLabelHelp(asMarkdown bool) string @@ -26,7 +26,7 @@ func GetLabelHelp(asMarkdown bool) string GetLabelHelp return the help for the labels. -## func [GetLabelHelpFor]() +## func [GetLabelHelpFor]() ```go func GetLabelHelpFor(labelname string, asMarkdown bool) string @@ -35,7 +35,7 @@ func GetLabelHelpFor(labelname string, asMarkdown bool) string GetLabelHelpFor returns the help for a specific label. -## func [GetLabelNames]() +## func [GetLabelNames]() ```go func GetLabelNames() []string @@ -44,7 +44,7 @@ func GetLabelNames() []string GetLabelNames returns a sorted list of all katenary label names. -## func [Prefix]() +## func [Prefix]() ```go func Prefix() string @@ -53,7 +53,7 @@ func Prefix() string -## type [Help]() +## type [Help]() Help is the documentation of a label. @@ -67,7 +67,7 @@ type Help struct { ``` -## type [Label]() +## type [Label]() Label is a katenary label to find in compose files. @@ -95,11 +95,12 @@ const ( LabelEnvFrom Label = KatenaryLabelPrefix + "/env-from" LabelExchangeVolume Label = KatenaryLabelPrefix + "/exchange-volumes" LabelValuesFrom Label = KatenaryLabelPrefix + "/values-from" + LabelDependsOn Label = KatenaryLabelPrefix + "/depends-on" ) ``` -### func [LabelName]() +### func [LabelName]() ```go func LabelName(name string) Label diff --git a/doc/docs/packages/internal/generator/labels/labelstructs.md b/doc/docs/packages/internal/generator/labels/labelstructs.md index 2a784f5..7c9e0db 100644 --- a/doc/docs/packages/internal/generator/labels/labelstructs.md +++ b/doc/docs/packages/internal/generator/labels/labelstructs.md @@ -8,7 +8,7 @@ import "katenary.io/internal/generator/labels/labelstructs" Package labelstructs is a package that contains the structs used to represent the labels in the yaml files. -## type [ConfigMapFiles]() +## type [ConfigMapFiles]() @@ -17,7 +17,7 @@ type ConfigMapFiles []string ``` -### func [ConfigMapFileFrom]() +### func [ConfigMapFileFrom]() ```go func ConfigMapFileFrom(data string) (ConfigMapFiles, error) @@ -26,7 +26,7 @@ func ConfigMapFileFrom(data string) (ConfigMapFiles, error) -## type [CronJob]() +## type [CronJob]() @@ -40,7 +40,7 @@ type CronJob struct { ``` -### func [CronJobFrom]() +### func [CronJobFrom]() ```go func CronJobFrom(data string) (*CronJob, error) @@ -49,7 +49,7 @@ func CronJobFrom(data string) (*CronJob, error) -## type [Dependency]() +## type [Dependency]() Dependency is a dependency of a chart to other charts. @@ -64,7 +64,7 @@ type Dependency struct { ``` -### func [DependenciesFrom]() +### func [DependenciesFrom]() ```go func DependenciesFrom(data string) ([]Dependency, error) @@ -73,7 +73,7 @@ func DependenciesFrom(data string) ([]Dependency, error) DependenciesFrom returns a slice of dependencies from the given string. -## type [EnvFrom]() +## type [EnvFrom]() @@ -82,7 +82,7 @@ type EnvFrom []string ``` -### func [EnvFromFrom]() +### func [EnvFromFrom]() ```go func EnvFromFrom(data string) (EnvFrom, error) @@ -91,7 +91,7 @@ func EnvFromFrom(data string) (EnvFrom, error) EnvFromFrom returns a EnvFrom from the given string. -## type [ExchangeVolume]() +## type [ExchangeVolume]() @@ -105,7 +105,7 @@ type ExchangeVolume struct { ``` -### func [NewExchangeVolumes]() +### func [NewExchangeVolumes]() ```go func NewExchangeVolumes(data string) ([]*ExchangeVolume, error) @@ -114,7 +114,7 @@ func NewExchangeVolumes(data string) ([]*ExchangeVolume, error) -## type [HealthCheck]() +## type [HealthCheck]() @@ -126,7 +126,7 @@ type HealthCheck struct { ``` -### func [ProbeFrom]() +### func [ProbeFrom]() ```go func ProbeFrom(data string) (*HealthCheck, error) @@ -135,7 +135,7 @@ func ProbeFrom(data string) (*HealthCheck, error) -## type [Ingress]() +## type [Ingress]() @@ -152,7 +152,7 @@ type Ingress struct { ``` -### func [IngressFrom]() +### func [IngressFrom]() ```go func IngressFrom(data string) (*Ingress, error) @@ -161,7 +161,7 @@ func IngressFrom(data string) (*Ingress, error) IngressFrom creates a new Ingress from a compose service. -## type [MapEnv]() +## type [MapEnv]() @@ -170,7 +170,7 @@ type MapEnv map[string]string ``` -### func [MapEnvFrom]() +### func [MapEnvFrom]() ```go func MapEnvFrom(data string) (MapEnv, error) @@ -179,7 +179,7 @@ func MapEnvFrom(data string) (MapEnv, error) MapEnvFrom returns a MapEnv from the given string. -## type [Ports]() +## type [Ports]() @@ -188,7 +188,7 @@ type Ports []uint32 ``` -### func [PortsFrom]() +### func [PortsFrom]() ```go func PortsFrom(data string) (Ports, error) @@ -197,7 +197,7 @@ func PortsFrom(data string) (Ports, error) PortsFrom returns a Ports from the given string. -## type [Secrets]() +## type [Secrets]() @@ -206,7 +206,7 @@ type Secrets []string ``` -### func [SecretsFrom]() +### func [SecretsFrom]() ```go func SecretsFrom(data string) (Secrets, error) @@ -215,7 +215,7 @@ func SecretsFrom(data string) (Secrets, error) -## type [TLS]() +## type [TLS]() @@ -226,7 +226,7 @@ type TLS struct { ``` -## type [ValueFrom]() +## type [ValueFrom]() @@ -235,7 +235,7 @@ type ValueFrom map[string]string ``` -### func [GetValueFrom]() +### func [GetValueFrom]() ```go func GetValueFrom(data string) (*ValueFrom, error) diff --git a/doc/docs/packages/internal/logger.md b/doc/docs/packages/internal/logger.md index c1151ab..a17f6a6 100644 --- a/doc/docs/packages/internal/logger.md +++ b/doc/docs/packages/internal/logger.md @@ -8,7 +8,7 @@ import "katenary.io/internal/logger" Package logger provides simple logging functions with icons and colors. -## func [Failure]() +## func [Failure]() ```go func Failure(msg ...any) @@ -16,8 +16,35 @@ func Failure(msg ...any) Failure prints a failure message. + +## func [Failuref]() + +```go +func Failuref(format string, msg ...any) +``` + +Failuref prints a formatted failure message. + + +## func [Fatal]() + +```go +func Fatal(msg ...any) +``` + +Fatal prints a fatal error message and exits with code 1. + + +## func [Fatalf]() + +```go +func Fatalf(format string, msg ...any) +``` + +Fatalf prints a fatal error message with formatting and exits with code 1. + -## func [Info]() +## func [Info]() ```go func Info(msg ...any) @@ -25,8 +52,17 @@ func Info(msg ...any) Info prints an informational message. + +## func [Infof]() + +```go +func Infof(format string, msg ...any) +``` + +Infof prints a formatted informational message. + -## func [Log]() +## func [Log]() ```go func Log(icon Icon, msg ...any) @@ -34,8 +70,35 @@ func Log(icon Icon, msg ...any) Log prints a message with a custom icon. + +## func [Logf]() + +```go +func Logf(icon Icon, format string, msg ...any) +``` + +Logf prints a formatted message with a custom icon. + + +## func [Print]() + +```go +func Print(msg ...any) +``` + +Print prints a message without icon. + + +## func [Printf]() + +```go +func Printf(format string, msg ...any) +``` + +Printf prints a formatted message without icon. + -## func [Success]() +## func [Success]() ```go func Success(msg ...any) @@ -43,8 +106,17 @@ func Success(msg ...any) Success prints a success message. + +## func [Successf]() + +```go +func Successf(format string, msg ...any) +``` + +Successf prints a formatted success message. + -## func [Warn]() +## func [Warn]() ```go func Warn(msg ...any) @@ -52,8 +124,17 @@ func Warn(msg ...any) Warn prints a warning message. + +## func [Warnf]() + +```go +func Warnf(format string, msg ...any) +``` + +Warnf prints a formatted warning message. + -## type [Icon]() +## type [Icon]() Icon is a unicode icon diff --git a/doc/docs/packages/internal/parser.md b/doc/docs/packages/internal/parser.md index 8ec7614..431be6a 100644 --- a/doc/docs/packages/internal/parser.md +++ b/doc/docs/packages/internal/parser.md @@ -8,7 +8,7 @@ import "katenary.io/internal/parser" Package parser is a wrapper around compose\-go to parse compose files. -## func [Parse]() +## func [Parse]() ```go func Parse(profiles []string, envFiles []string, dockerComposeFile ...string) (*types.Project, error) diff --git a/doc/docs/packages/internal/utils.md b/doc/docs/packages/internal/utils.md index 88fb19d..fa5a3c7 100644 --- a/doc/docs/packages/internal/utils.md +++ b/doc/docs/packages/internal/utils.md @@ -17,7 +17,7 @@ const DirectoryPermission = 0o755 ``` -## func [AsResourceName]() +## func [AsResourceName]() ```go func AsResourceName(name string) string @@ -26,7 +26,7 @@ func AsResourceName(name string) string AsResourceName returns a resource name with underscores to respect the kubernetes naming convention. It's the opposite of FixedResourceName. -## func [Confirm]() +## func [Confirm]() ```go func Confirm(question string, icon ...logger.Icon) bool @@ -35,7 +35,7 @@ func Confirm(question string, icon ...logger.Icon) bool Confirm asks a question and returns true if the answer is y. -## func [CountStartingSpaces]() +## func [CountStartingSpaces]() ```go func CountStartingSpaces(line string) int @@ -44,7 +44,7 @@ func CountStartingSpaces(line string) int CountStartingSpaces counts the number of spaces at the beginning of a string. -## func [EncodeBasicYaml]() +## func [EncodeBasicYaml]() ```go func EncodeBasicYaml(data any) ([]byte, error) @@ -53,7 +53,7 @@ func EncodeBasicYaml(data any) ([]byte, error) EncodeBasicYaml encodes a basic yaml from an interface. -## func [FixedResourceName]() +## func [FixedResourceName]() ```go func FixedResourceName(name string) string @@ -62,7 +62,7 @@ func FixedResourceName(name string) string FixedResourceName returns a resource name without underscores to respect the kubernetes naming convention. -## func [GetContainerByName]() +## func [GetContainerByName]() ```go func GetContainerByName(name string, containers []corev1.Container) (*corev1.Container, int) @@ -71,7 +71,7 @@ func GetContainerByName(name string, containers []corev1.Container) (*corev1.Con GetContainerByName returns a container by name and its index in the array. It returns nil, \-1 if not found. -## func [GetKind]() +## func [GetKind]() ```go func GetKind(path string) (kind string) @@ -80,7 +80,7 @@ func GetKind(path string) (kind string) GetKind returns the kind of the resource from the file path. -## func [GetServiceNameByPort]() +## func [GetServiceNameByPort]() ```go func GetServiceNameByPort(port int) string @@ -89,7 +89,7 @@ func GetServiceNameByPort(port int) string GetServiceNameByPort returns the service name for a port. It the service name is not found, it returns an empty string. -## func [GetValuesFromLabel]() +## func [GetValuesFromLabel]() ```go func GetValuesFromLabel(service types.ServiceConfig, LabelValues string) map[string]*EnvConfig @@ -98,7 +98,7 @@ func GetValuesFromLabel(service types.ServiceConfig, LabelValues string) map[str GetValuesFromLabel returns a map of values from a label. -## func [HashComposefiles]() +## func [HashComposefiles]() ```go func HashComposefiles(files []string) (string, error) @@ -107,7 +107,7 @@ func HashComposefiles(files []string) (string, error) HashComposefiles returns a hash of the compose files. -## func [Int32Ptr]() +## func [Int32Ptr]() ```go func Int32Ptr(i int32) *int32 @@ -116,7 +116,7 @@ func Int32Ptr(i int32) *int32 Int32Ptr returns a pointer to an int32. -## func [PathToName]() +## func [PathToName]() ```go func PathToName(path string) string @@ -125,7 +125,7 @@ func PathToName(path string) string PathToName converts a path to a kubernetes complient name. -## func [StrPtr]() +## func [StrPtr]() ```go func StrPtr(s string) *string @@ -134,7 +134,7 @@ func StrPtr(s string) *string StrPtr returns a pointer to a string. -## func [TplName]() +## func [TplName]() ```go func TplName(serviceName, appname string, suffix ...string) string @@ -143,7 +143,7 @@ func TplName(serviceName, appname string, suffix ...string) string TplName returns the name of the kubernetes resource as a template string. It is used in the templates and defined in \_helper.tpl file. -## func [TplValue]() +## func [TplValue]() ```go func TplValue(serviceName, variable string, pipes ...string) string @@ -152,7 +152,7 @@ func TplValue(serviceName, variable string, pipes ...string) string TplValue returns a string that can be used in a template to access a value from the values file. -## func [WordWrap]() +## func [WordWrap]() ```go func WordWrap(text string, lineWidth int) string @@ -161,7 +161,7 @@ func WordWrap(text string, lineWidth int) string WordWrap wraps a string to a given line width. Warning: it may break the string. You need to check the result. -## func [Wrap]() +## func [Wrap]() ```go func Wrap(src, above, below string) string @@ -170,7 +170,7 @@ func Wrap(src, above, below string) string Wrap wraps a string with a string above and below. It will respect the indentation of the src string. -## type [EnvConfig]() +## type [EnvConfig]() EnvConfig is a struct to hold the description of an environment variable. diff --git a/doc/docs/usage.md b/doc/docs/usage.md index 16baaf0..5879252 100644 --- a/doc/docs/usage.md +++ b/doc/docs/usage.md @@ -98,6 +98,7 @@ Katenary transforms compose services this way: - 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` uses Kubernetes API by default to check if the service endpoint is ready. No port required. +- If you need to create a Kubernetes Service for external access, add the `katenary.v3/ports` label. 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 @@ -187,6 +188,26 @@ services: - 3306:3306 ``` +If you want to create a Kubernetes Service for external access, add the `katenary.v3/ports` label to the service: + +```yaml +version: "3" + +services: + webapp: + image: php:8-apache + depends_on: + - database + + database: + image: mariadb + environment: + MYSQL_ROOT_PASSWORD: foobar + labels: + katenary.v3/ports: + - 3306 +``` + ### Declare ingresses It's very common to have an Ingress resource on web application to deploy on Kubernetes. It allows exposing the diff --git a/doc/share/man/man1/katenary.1 b/doc/share/man/man1/katenary.1 index 922b672..438f735 100644 --- a/doc/share/man/man1/katenary.1 +++ b/doc/share/man/man1/katenary.1 @@ -107,8 +107,8 @@ image, tags, and ingresses configuration are also stored in \f[CR]values.yaml\f[ .IP \[bu] 2 if named volumes are declared, Katenary create \f[CR]PersistentVolumeClaims\f[R] \- not enabled in values file .IP \[bu] 2 -\f[CR]depends_on\f[R] needs that the pointed service declared a port. -If not, you can use labels to inform Katenary +\f[CR]depends_on\f[R] uses Kubernetes API to check if the service endpoint is ready. No port declaration is required. +If you need to create a Kubernetes Service for external access, use the \f[CR]katenary.v3/ports\f[R] label. .PP For any other specific configuration, like binding local files as \f[CR]ConfigMap\f[R], bind variables, add values with documentation, etc. You\[aq]ll need to use labels. diff --git a/internal/generator/generator.go b/internal/generator/generator.go index 0951b30..6170581 100644 --- a/internal/generator/generator.go +++ b/internal/generator/generator.go @@ -135,6 +135,17 @@ func Generate(project *types.Project) (*HelmChart, error) { } } + // warn users if dependent service has no ports + for _, s := range project.Services { + for _, d := range s.GetDependencies() { + if dep, ok := deployments[d]; ok { + if len(dep.service.Ports) == 0 { + logger.Warnf("Service %s is used in depends_on but has no ports declared. No Kubernetes Service will be created for it. Add katenary.v3/ports label if you need to create a Service.", d) + } + } + } + } + // set ServiceAccountName for deployments that need it for _, d := range deployments { d.SetServiceAccountName() diff --git a/internal/generator/labels/katenaryLabelsDoc.yaml b/internal/generator/labels/katenaryLabelsDoc.yaml index 1a8d33c..d30cbaa 100644 --- a/internal/generator/labels/katenaryLabelsDoc.yaml +++ b/internal/generator/labels/katenaryLabelsDoc.yaml @@ -92,7 +92,12 @@ short: "Ports to be added to the service." long: |- Only useful for services without exposed port. It is mandatory if the - service is a dependency of another service. + service is a dependency of another service AND you want to create a + Kubernetes Service for external access. + + If you only need to check if the service is ready (using depends_on), + you don't need to declare ports. The service will not be created automatically + unless you add this label. example: |- labels: {{ .KatenaryPrefix }}/ports: |- @@ -362,7 +367,11 @@ 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. + has ready addresses. This method does not require the service to expose a port + and does not create a Kubernetes Service automatically. + + If you need to create a Kubernetes Service for external access, use the + `katenary.v3/ports` label instead. Set this label to `legacy` to use the old netcat method that requires a port to be defined for the dependent service. @@ -374,6 +383,13 @@ labels: # Use legacy netcat method (requires port) {{ .KatenaryPrefix }}/depends-on: legacy + + database: + image: mysql + labels: + # Create a Kubernetes Service for external access + {{ .KatenaryPrefix }}/ports: + - 3306 type: "string" # vim: ft=gotmpl.yaml -- 2.49.1 From 0b1a45319f31f9b9edbd96606f14426fb554e81f Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Tue, 17 Mar 2026 10:39:04 +0100 Subject: [PATCH 10/12] feat(depends): fixes the API point --- internal/generator/deployment.go | 10 +++--- internal/generator/deployment_test.go | 31 ++++++++++++------- .../generator/labels/katenaryLabelsDoc.yaml | 6 ++-- internal/generator/rbac.go | 8 ++--- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/internal/generator/deployment.go b/internal/generator/deployment.go index 51e2697..ac01c71 100644 --- a/internal/generator/deployment.go +++ b/internal/generator/deployment.go @@ -309,21 +309,21 @@ func (d *Deployment) dependsOnLegacy(to *Deployment, servicename string) error { func (d *Deployment) dependsOnK8sAPI(to *Deployment) error { script := `NAMESPACE=${NAMESPACE:-default} -SERVICE=%s +DEPLOYMENT_NAME=%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)" \ +until curl -s -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 + "https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}/apis/apps/v1/namespaces/${NAMESPACE}/deployments/${DEPLOYMENT_NAME}" \ + | grep -q '"readyReplicas":\s*[1-9][0-9]*'; do sleep 2 done` command := []string{shCommand, "-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", + Image: "quay.io/curl/curl:latest", Command: command, Env: []corev1.EnvVar{ { diff --git a/internal/generator/deployment_test.go b/internal/generator/deployment_test.go index 7dbd146..c94981c 100644 --- a/internal/generator/deployment_test.go +++ b/internal/generator/deployment_test.go @@ -146,21 +146,25 @@ services: } initContainer := dt.Spec.Template.Spec.InitContainers[0] - if !strings.Contains(initContainer.Image, "busybox") { - t.Errorf("Expected busybox image, got %s", initContainer.Image) + if !strings.Contains(initContainer.Image, "quay.io/curl/curl") { + t.Errorf("Expected quay.io/curl/curl 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) + if !strings.Contains(fullCommand, "curl") { + t.Errorf("Expected curl command (K8s API method), got %s", fullCommand) } if !strings.Contains(fullCommand, "/api/v1/namespaces/") { t.Errorf("Expected Kubernetes API call to /api/v1/namespaces/, got %s", fullCommand) } - if !strings.Contains(fullCommand, "/endpoints/") { - t.Errorf("Expected Kubernetes API call to /endpoints/, got %s", fullCommand) + if !strings.Contains(fullCommand, "/deployments/") { + t.Errorf("Expected Kubernetes API call to /deployments/, got %s", fullCommand) + } + + if !strings.Contains(fullCommand, "readyReplicas") { + t.Errorf("Expected readyReplicas check, got %s", fullCommand) } if len(initContainer.Env) == 0 { @@ -718,8 +722,8 @@ services: if !contains(rule.APIGroups, "") { t.Error("Expected APIGroup to include core API ('')") } - if !contains(rule.Resources, "endpoints") { - t.Errorf("Expected Resource to include 'endpoints', got %v", rule.Resources) + if !contains(rule.Resources, "deployments") { + t.Errorf("Expected Resource to include 'deployments', got %v", rule.Resources) } for _, res := range rule.Resources { @@ -800,14 +804,17 @@ services: } fullCommand := strings.Join(initContainer.Command, " ") - if !strings.Contains(fullCommand, "wget") { - t.Error("Expected init container to use wget for K8s API calls") + if !strings.Contains(fullCommand, "curl") { + t.Error("Expected init container to use curl for K8s API calls") } if !strings.Contains(fullCommand, "/api/v1/namespaces/") { t.Error("Expected init container to call /api/v1/namespaces/ endpoint") } - if !strings.Contains(fullCommand, "/endpoints/") { - t.Error("Expected init container to access /endpoints/ resource") + if !strings.Contains(fullCommand, "/deployments/") { + t.Error("Expected init container to access /deployments/ resource") + } + if !strings.Contains(fullCommand, "readyReplicas") { + t.Error("Expected init container to check readyReplicas") } hasNamespace := false diff --git a/internal/generator/labels/katenaryLabelsDoc.yaml b/internal/generator/labels/katenaryLabelsDoc.yaml index d30cbaa..f1a3742 100644 --- a/internal/generator/labels/katenaryLabelsDoc.yaml +++ b/internal/generator/labels/katenaryLabelsDoc.yaml @@ -366,9 +366,9 @@ 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 - and does not create a Kubernetes Service automatically. + By default, Katenary uses the Kubernetes API to check if the deployment's + `readyReplicas` status is greater than 0. This method does not require the + service to expose a port and does not create a Kubernetes Service automatically. If you need to create a Kubernetes Service for external access, use the `katenary.v3/ports` label instead. diff --git a/internal/generator/rbac.go b/internal/generator/rbac.go index 36b088c..8be8f8c 100644 --- a/internal/generator/rbac.go +++ b/internal/generator/rbac.go @@ -137,7 +137,7 @@ func NewServiceAccount(service types.ServiceConfig, appName string) *ServiceAcco APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: utils.TplName(service.Name, appName), + Name: utils.TplName(service.Name, appName, "dependency"), Labels: GetLabels(service.Name, appName), Annotations: Annotations, }, @@ -161,9 +161,9 @@ func NewRestrictedRole(service types.ServiceConfig, appName string) *Role { }, Rules: []rbacv1.PolicyRule{ { - APIGroups: []string{""}, - Resources: []string{"endpoints"}, - Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"apps"}, + Resources: []string{"deployments"}, + Verbs: []string{"get"}, }, }, }, -- 2.49.1 From b40378ec2313448f21fb429f9f7a451b1a9d792e Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Tue, 17 Mar 2026 10:39:48 +0100 Subject: [PATCH 11/12] feat(quality): enhance logger Warnings are flushed after the generation to help the user to find issues. Colors are better defined. --- internal/generator/converter.go | 4 +++ internal/logger/logger.go | 59 ++++++++++++++++++++++++--------- internal/logger/logger_test.go | 22 ++++++++++++ 3 files changed, 69 insertions(+), 16 deletions(-) diff --git a/internal/generator/converter.go b/internal/generator/converter.go index afde74c..fc9c863 100644 --- a/internal/generator/converter.go +++ b/internal/generator/converter.go @@ -216,6 +216,10 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) error { // call helm update if needed callHelmUpdate(config) + + // flush warnings after all conversion is complete + logger.FlushWarnings() + return nil } diff --git a/internal/logger/logger.go b/internal/logger/logger.go index d17aaf8..2d2f021 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -4,6 +4,7 @@ package logger import ( "fmt" "os" + "sync" ) // Icon is a unicode icon @@ -27,6 +28,18 @@ const ( const reset = "\033[0m" +const ( + ColorGreen = "\033[38;5;34m" + ColorRed = "\033[38;5;196m" + ColorOrange = "\033[38;5;214m" + ColorWarning = "\033[38;5;214m" +) + +var ( + warnings []string + mu sync.Mutex +) + // Print prints a message without icon. func Print(msg ...any) { fmt.Print(msg...) @@ -49,38 +62,38 @@ func Infof(format string, msg ...any) { // Warn prints a warning message. func Warn(msg ...any) { - orange := "\033[38;5;214m" - message(orange, IconWarning, msg...) + mu.Lock() + defer mu.Unlock() + warning := fmt.Sprint(msg...) + warnings = append(warnings, warning) } // Warnf prints a formatted warning message. func Warnf(format string, msg ...any) { - orange := "\033[38;5;214m" - message(orange, IconWarning, fmt.Sprintf(format, msg...)) + mu.Lock() + defer mu.Unlock() + warning := fmt.Sprintf(format, msg...) + warnings = append(warnings, warning) } // Success prints a success message. func Success(msg ...any) { - green := "\033[38;5;34m" - message(green, IconSuccess, msg...) + message(ColorGreen, IconSuccess, msg...) } // Successf prints a formatted success message. func Successf(format string, msg ...any) { - green := "\033[38;5;34m" - message(green, IconSuccess, fmt.Sprintf(format, msg...)) + message(ColorGreen, IconSuccess, fmt.Sprintf(format, msg...)) } // Failure prints a failure message. func Failure(msg ...any) { - red := "\033[38;5;196m" - message(red, IconFailure, msg...) + message(ColorRed, IconFailure, msg...) } // Failuref prints a formatted failure message. func Failuref(format string, msg ...any) { - red := "\033[38;5;196m" - message(red, IconFailure, fmt.Sprintf(format, msg...)) + message(ColorRed, IconFailure, fmt.Sprintf(format, msg...)) } // Log prints a message with a custom icon. @@ -106,12 +119,26 @@ func fatalf(red string, icon Icon, format string, msg ...any) { // Fatal prints a fatal error message and exits with code 1. func Fatal(msg ...any) { - red := "\033[38;5;196m" - fatal(red, IconFailure, msg...) + fatal(ColorRed, IconFailure, msg...) } // Fatalf prints a fatal error message with formatting and exits with code 1. func Fatalf(format string, msg ...any) { - red := "\033[38;5;196m" - fatalf(red, IconFailure, format, msg...) + fatalf(ColorRed, IconFailure, format, msg...) +} + +// FlushWarnings prints all collected warnings at the end of the conversion. +func FlushWarnings() { + mu.Lock() + defer mu.Unlock() + if len(warnings) > 0 { + fmt.Println() + fmt.Print(ColorWarning, IconWarning, " The following issues may need attention:", reset) + fmt.Println() + for _, warning := range warnings { + fmt.Println(" •", warning) + } + fmt.Println() + warnings = nil + } } diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go index bf0296f..cbb5349 100644 --- a/internal/logger/logger_test.go +++ b/internal/logger/logger_test.go @@ -77,3 +77,25 @@ func TestLog(t *testing.T) { }() Log(IconInfo, "test log") } + +func TestWarningsCollection(t *testing.T) { + // Clear any existing warnings + warnings = nil + + // Add some warnings + Warn("test warning 1") + Warnf("test warning 2: %s", "value") + + // Check that warnings were collected + if len(warnings) != 2 { + t.Errorf("expected 2 warnings, got %d", len(warnings)) + } + + // Check the content of warnings + if warnings[0] != "test warning 1" { + t.Errorf("expected 'test warning 1', got '%s'", warnings[0]) + } + if warnings[1] != "test warning 2: value" { + t.Errorf("expected 'test warning 2: value', got '%s'", warnings[1]) + } +} -- 2.49.1 From 7ee6b74b10a0ebffa61624b64d75b4ee57895c2c Mon Sep 17 00:00:00 2001 From: Patrice Ferlet Date: Tue, 17 Mar 2026 10:46:54 +0100 Subject: [PATCH 12/12] feat(depends): fixes the tests --- internal/generator/deployment_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/generator/deployment_test.go b/internal/generator/deployment_test.go index c94981c..41c0fe4 100644 --- a/internal/generator/deployment_test.go +++ b/internal/generator/deployment_test.go @@ -155,8 +155,8 @@ services: t.Errorf("Expected curl command (K8s API method), got %s", fullCommand) } - if !strings.Contains(fullCommand, "/api/v1/namespaces/") { - t.Errorf("Expected Kubernetes API call to /api/v1/namespaces/, got %s", fullCommand) + if !strings.Contains(fullCommand, "/apis/apps/v1/namespaces/") { + t.Errorf("Expected Kubernetes API call to /apis/apps/v1/namespaces/, got %s", fullCommand) } if !strings.Contains(fullCommand, "/deployments/") { @@ -719,8 +719,8 @@ services: } rule := role.Rules[0] - if !contains(rule.APIGroups, "") { - t.Error("Expected APIGroup to include core API ('')") + if !contains(rule.APIGroups, "apps") { + t.Error("Expected APIGroup to include 'apps'") } if !contains(rule.Resources, "deployments") { t.Errorf("Expected Resource to include 'deployments', got %v", rule.Resources) @@ -807,8 +807,8 @@ services: if !strings.Contains(fullCommand, "curl") { t.Error("Expected init container to use curl for K8s API calls") } - if !strings.Contains(fullCommand, "/api/v1/namespaces/") { - t.Error("Expected init container to call /api/v1/namespaces/ endpoint") + if !strings.Contains(fullCommand, "/apis/apps/v1/namespaces/") { + t.Error("Expected init container to call /apis/apps/v1/namespaces/ endpoint") } if !strings.Contains(fullCommand, "/deployments/") { t.Error("Expected init container to access /deployments/ resource") -- 2.49.1