diff --git a/docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/manager/manager.yaml b/docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/manager/manager.yaml index d21cbd95340..035f87db4f7 100644 --- a/docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/manager/manager.yaml +++ b/docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/manager/manager.yaml @@ -24,7 +24,7 @@ spec: containers: - args: {{- if .Values.metrics.enable }} - - --metrics-bind-address=:8443 + - --metrics-bind-address=:{{ .Values.metrics.port }} {{- else }} # Bind to :0 to disable the controller-runtime managed metrics server - --metrics-bind-address=0 @@ -51,7 +51,7 @@ spec: periodSeconds: 20 name: manager ports: - - containerPort: 9443 + - containerPort: {{ .Values.webhook.port }} name: webhook-server protocol: TCP readinessProbe: diff --git a/docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/metrics/controller-manager-metrics-service.yaml b/docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/metrics/controller-manager-metrics-service.yaml index 8f3149ca16e..c5b24a9125a 100644 --- a/docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/metrics/controller-manager-metrics-service.yaml +++ b/docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/metrics/controller-manager-metrics-service.yaml @@ -11,9 +11,9 @@ metadata: spec: ports: - name: https - port: 8443 + port: {{ .Values.metrics.port }} protocol: TCP - targetPort: 8443 + targetPort: {{ .Values.metrics.port }} selector: app.kubernetes.io/name: project control-plane: controller-manager diff --git a/docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/webhook/webhook-service.yaml b/docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/webhook/webhook-service.yaml index 0fa69736ed4..99213498502 100644 --- a/docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/webhook/webhook-service.yaml +++ b/docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/webhook/webhook-service.yaml @@ -10,7 +10,7 @@ spec: ports: - port: 443 protocol: TCP - targetPort: 9443 + targetPort: {{ .Values.webhook.port }} selector: app.kubernetes.io/name: project control-plane: controller-manager diff --git a/docs/book/src/cronjob-tutorial/testdata/project/dist/chart/values.yaml b/docs/book/src/cronjob-tutorial/testdata/project/dist/chart/values.yaml index 196c832bdf3..6617ce60800 100644 --- a/docs/book/src/cronjob-tutorial/testdata/project/dist/chart/values.yaml +++ b/docs/book/src/cronjob-tutorial/testdata/project/dist/chart/values.yaml @@ -56,12 +56,18 @@ crd: # Enable to expose /metrics endpoint with RBAC protection. metrics: enable: true + port: 8443 # Metrics server port # Cert-manager integration for TLS certificates. # Required for webhook certificates and metrics endpoint certificates. certManager: enable: true +# Webhook server configuration +webhook: + enable: true + port: 9443 # Webhook server port + # Prometheus ServiceMonitor for metrics scraping. # Requires prometheus-operator to be installed in the cluster. prometheus: diff --git a/docs/book/src/getting-started/testdata/project/dist/chart/templates/manager/manager.yaml b/docs/book/src/getting-started/testdata/project/dist/chart/templates/manager/manager.yaml index 00c5a089673..a09747e09a6 100644 --- a/docs/book/src/getting-started/testdata/project/dist/chart/templates/manager/manager.yaml +++ b/docs/book/src/getting-started/testdata/project/dist/chart/templates/manager/manager.yaml @@ -24,7 +24,7 @@ spec: containers: - args: {{- if .Values.metrics.enable }} - - --metrics-bind-address=:8443 + - --metrics-bind-address=:{{ .Values.metrics.port }} {{- else }} # Bind to :0 to disable the controller-runtime managed metrics server - --metrics-bind-address=0 diff --git a/docs/book/src/getting-started/testdata/project/dist/chart/templates/metrics/controller-manager-metrics-service.yaml b/docs/book/src/getting-started/testdata/project/dist/chart/templates/metrics/controller-manager-metrics-service.yaml index 8f3149ca16e..c5b24a9125a 100644 --- a/docs/book/src/getting-started/testdata/project/dist/chart/templates/metrics/controller-manager-metrics-service.yaml +++ b/docs/book/src/getting-started/testdata/project/dist/chart/templates/metrics/controller-manager-metrics-service.yaml @@ -11,9 +11,9 @@ metadata: spec: ports: - name: https - port: 8443 + port: {{ .Values.metrics.port }} protocol: TCP - targetPort: 8443 + targetPort: {{ .Values.metrics.port }} selector: app.kubernetes.io/name: project control-plane: controller-manager diff --git a/docs/book/src/getting-started/testdata/project/dist/chart/values.yaml b/docs/book/src/getting-started/testdata/project/dist/chart/values.yaml index 1ff7c4edf9a..096766c88c7 100644 --- a/docs/book/src/getting-started/testdata/project/dist/chart/values.yaml +++ b/docs/book/src/getting-started/testdata/project/dist/chart/values.yaml @@ -56,6 +56,12 @@ crd: # Enable to expose /metrics endpoint with RBAC protection. metrics: enable: true + port: 8443 # Metrics server port + +# Cert-manager integration for TLS certificates. +# Required for webhook certificates and metrics endpoint certificates. +certManager: + enable: false # Prometheus ServiceMonitor for metrics scraping. # Requires prometheus-operator to be installed in the cluster. diff --git a/docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/manager/manager.yaml b/docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/manager/manager.yaml index d21cbd95340..035f87db4f7 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/manager/manager.yaml +++ b/docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/manager/manager.yaml @@ -24,7 +24,7 @@ spec: containers: - args: {{- if .Values.metrics.enable }} - - --metrics-bind-address=:8443 + - --metrics-bind-address=:{{ .Values.metrics.port }} {{- else }} # Bind to :0 to disable the controller-runtime managed metrics server - --metrics-bind-address=0 @@ -51,7 +51,7 @@ spec: periodSeconds: 20 name: manager ports: - - containerPort: 9443 + - containerPort: {{ .Values.webhook.port }} name: webhook-server protocol: TCP readinessProbe: diff --git a/docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/metrics/controller-manager-metrics-service.yaml b/docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/metrics/controller-manager-metrics-service.yaml index 8f3149ca16e..c5b24a9125a 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/metrics/controller-manager-metrics-service.yaml +++ b/docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/metrics/controller-manager-metrics-service.yaml @@ -11,9 +11,9 @@ metadata: spec: ports: - name: https - port: 8443 + port: {{ .Values.metrics.port }} protocol: TCP - targetPort: 8443 + targetPort: {{ .Values.metrics.port }} selector: app.kubernetes.io/name: project control-plane: controller-manager diff --git a/docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/webhook/webhook-service.yaml b/docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/webhook/webhook-service.yaml index 0fa69736ed4..99213498502 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/webhook/webhook-service.yaml +++ b/docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/webhook/webhook-service.yaml @@ -10,7 +10,7 @@ spec: ports: - port: 443 protocol: TCP - targetPort: 9443 + targetPort: {{ .Values.webhook.port }} selector: app.kubernetes.io/name: project control-plane: controller-manager diff --git a/docs/book/src/multiversion-tutorial/testdata/project/dist/chart/values.yaml b/docs/book/src/multiversion-tutorial/testdata/project/dist/chart/values.yaml index 196c832bdf3..6617ce60800 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/dist/chart/values.yaml +++ b/docs/book/src/multiversion-tutorial/testdata/project/dist/chart/values.yaml @@ -56,12 +56,18 @@ crd: # Enable to expose /metrics endpoint with RBAC protection. metrics: enable: true + port: 8443 # Metrics server port # Cert-manager integration for TLS certificates. # Required for webhook certificates and metrics endpoint certificates. certManager: enable: true +# Webhook server configuration +webhook: + enable: true + port: 9443 # Webhook server port + # Prometheus ServiceMonitor for metrics scraping. # Requires prometheus-operator to be installed in the cluster. prometheus: diff --git a/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/chart_converter.go b/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/chart_converter.go index 152f6901341..722de0c25a6 100644 --- a/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/chart_converter.go +++ b/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/chart_converter.go @@ -18,6 +18,7 @@ package kustomize import ( "fmt" + "strconv" "strings" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -113,6 +114,7 @@ func (c *ChartConverter) ExtractDeploymentConfig() map[string]interface{} { extractContainerEnv(container, config) extractContainerImage(container, config) extractContainerArgs(container, config) + extractContainerPorts(container, config) extractContainerResources(container, config) extractContainerSecurityContext(container, config) @@ -228,11 +230,21 @@ func extractContainerArgs(container map[string]interface{}, config map[string]in continue } - // The following arguments should not be exposed under args - // manager because they are not independently customizable - if strings.Contains(strArg, "--metrics-bind-address") || - strings.Contains(strArg, "--health-probe-bind-address") || - strings.Contains(strArg, "--webhook-cert-path") || + // Extract port values from bind-address arguments and store them + // These arguments should not be exposed under args because they will be + // reconstructed from the port values in values.yaml + if strings.Contains(strArg, "--metrics-bind-address") { + if port := extractPortFromArg(strArg); port > 0 { + if _, exists := config["metricsPort"]; !exists { + config["metricsPort"] = port + } + } + continue + } + if strings.Contains(strArg, "--health-probe-bind-address") { + continue + } + if strings.Contains(strArg, "--webhook-cert-path") || strings.Contains(strArg, "--metrics-cert-path") { continue } @@ -244,6 +256,67 @@ func extractContainerArgs(container map[string]interface{}, config map[string]in } } +// extractPortFromArg extracts port number from arguments like "--metrics-bind-address=:8443" +func extractPortFromArg(arg string) int { + // Handle formats: --flag=:8443, --flag=0.0.0.0:8443, etc. + parts := strings.Split(arg, "=") + if len(parts) != 2 { + return 0 + } + + portPart := parts[1] + // Remove leading : or host part + if idx := strings.LastIndex(portPart, ":"); idx != -1 { + portPart = portPart[idx+1:] + } + + port, err := strconv.Atoi(portPart) + if err != nil || port <= 0 || port > 65535 { + return 0 + } + return port +} + +// extractContainerPorts extracts port configurations from container ports +func extractContainerPorts(container map[string]interface{}, config map[string]interface{}) { + // Use NestedFieldNoCopy to avoid deep copy issues with int values + portsField, found, err := unstructured.NestedFieldNoCopy(container, "ports") + if !found || err != nil { + return + } + + ports, ok := portsField.([]interface{}) + if !ok { + return + } + + for _, p := range ports { + portMap, ok := p.(map[string]interface{}) + if !ok { + continue + } + + name, _ := portMap["name"].(string) + var containerPort int + + // Try int64 first (from YAML unmarshaling) + if cp, ok := portMap["containerPort"].(int64); ok { + containerPort = int(cp) + } else if cp, ok := portMap["containerPort"].(int); ok { + containerPort = cp + } else { + continue + } + + // Look for webhook-server port + if name == "webhook-server" || strings.Contains(name, "webhook") { + if _, exists := config["webhookPort"]; !exists { + config["webhookPort"] = containerPort + } + } + } +} + func extractContainerResources(container map[string]interface{}, config map[string]interface{}) { resources, found, err := unstructured.NestedFieldNoCopy(container, "resources") if !found || err != nil { diff --git a/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/chart_converter_test.go b/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/chart_converter_test.go index afc1125b5f6..4f307c32aaf 100644 --- a/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/chart_converter_test.go +++ b/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/chart_converter_test.go @@ -183,9 +183,136 @@ var _ = Describe("ChartConverter", func() { Expect(args).NotTo(ContainElement("--health-probe-bind-address=:8081")) }) + It("should extract port configurations from args", func() { + // Set up deployment with port-related args + containers := []interface{}{ + map[string]interface{}{ + "name": "manager", + "image": "controller:latest", + "args": []interface{}{ + "--metrics-bind-address=:8443", + "--health-probe-bind-address=:8081", + "--leader-elect", + }, + }, + } + + err := unstructured.SetNestedSlice( + resources.Deployment.Object, + containers, + "spec", "template", "spec", "containers", + ) + Expect(err).NotTo(HaveOccurred()) + + config := converter.ExtractDeploymentConfig() + + Expect(config).To(HaveKey("metricsPort")) + Expect(config["metricsPort"]).To(Equal(8443)) + Expect(config).NotTo(HaveKey("healthPort")) + }) + + It("should extract webhook port from container ports", func() { + // Set up deployment with webhook container port + containers := []interface{}{ + map[string]interface{}{ + "name": "manager", + "image": "controller:latest", + "ports": []interface{}{ + map[string]interface{}{ + "containerPort": int64(9443), + "name": "webhook-server", + "protocol": "TCP", + }, + }, + }, + } + + err := unstructured.SetNestedSlice( + resources.Deployment.Object, + containers, + "spec", "template", "spec", "containers", + ) + Expect(err).NotTo(HaveOccurred()) + + config := converter.ExtractDeploymentConfig() + + Expect(config).To(HaveKey("webhookPort")) + Expect(config["webhookPort"]).To(Equal(9443)) + }) + + It("should extract custom port values", func() { + // Set up deployment with custom ports + containers := []interface{}{ + map[string]interface{}{ + "name": "manager", + "image": "controller:latest", + "args": []interface{}{ + "--metrics-bind-address=:9090", + "--health-probe-bind-address=:9091", + }, + "ports": []interface{}{ + map[string]interface{}{ + "containerPort": int64(9444), + "name": "webhook-server", + "protocol": "TCP", + }, + }, + }, + } + + err := unstructured.SetNestedSlice( + resources.Deployment.Object, + containers, + "spec", "template", "spec", "containers", + ) + Expect(err).NotTo(HaveOccurred()) + + config := converter.ExtractDeploymentConfig() + + Expect(config["metricsPort"]).To(Equal(9090)) + Expect(config["healthPort"]).To(BeNil()) + Expect(config["webhookPort"]).To(Equal(9444)) + }) + It("should handle deployment without containers", func() { config := converter.ExtractDeploymentConfig() Expect(config).To(BeEmpty()) }) }) + + Context("extractPortFromArg", func() { + It("should extract port from :PORT format", func() { + port := extractPortFromArg("--metrics-bind-address=:8443") + Expect(port).To(Equal(8443)) + }) + + It("should extract port from 0.0.0.0:PORT format", func() { + port := extractPortFromArg("--metrics-bind-address=0.0.0.0:8443") + Expect(port).To(Equal(8443)) + }) + + It("should extract port from HOST:PORT format", func() { + port := extractPortFromArg("--health-probe-bind-address=localhost:8081") + Expect(port).To(Equal(8081)) + }) + + It("should return 0 for invalid formats", func() { + port := extractPortFromArg("--invalid-arg") + Expect(port).To(Equal(0)) + + port = extractPortFromArg("--no-equals:8443") + Expect(port).To(Equal(0)) + + port = extractPortFromArg("--port=invalid") + Expect(port).To(Equal(0)) + }) + + It("should return 0 for out-of-range ports", func() { + port := extractPortFromArg("--port=:0") + Expect(port).To(Equal(0)) + + port = extractPortFromArg("--port=:99999") + Expect(port).To(Equal(0)) + }) + }) }) diff --git a/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/helm_templater.go b/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/helm_templater.go index 1286d2dcc88..b934aa1b0df 100644 --- a/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/helm_templater.go +++ b/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/helm_templater.go @@ -38,6 +38,7 @@ const ( kindIssuer = "Issuer" kindValidatingWebhook = "ValidatingWebhookConfiguration" kindMutatingWebhook = "MutatingWebhookConfiguration" + kindDeployment = "Deployment" // API versions apiVersionCertManager = "cert-manager.io/v1" @@ -79,7 +80,7 @@ func (t *HelmTemplater) ApplyHelmSubstitutions(yamlContent string, resource *uns yamlContent = t.substituteRBACValues(yamlContent) // Apply deployment-specific templating - if resource.GetKind() == "Deployment" { + if resource.GetKind() == kindDeployment { yamlContent = t.templateDeploymentFields(yamlContent) // Apply conditional logic for cert-manager related fields in deployments @@ -90,6 +91,11 @@ func (t *HelmTemplater) ApplyHelmSubstitutions(yamlContent string, resource *uns yamlContent = t.makeMetricsVolumesConditional(yamlContent) } + // Apply port templating for Services and Deployments + if resource.GetKind() == kindService || resource.GetKind() == kindDeployment { + yamlContent = t.templatePorts(yamlContent, resource) + } + // Final tidy-up: avoid accidental blank lines after Helm if-block starts // Some replacements may introduce an empty line between a `{{- if ... }}` // and the following content; collapse that to ensure consistent formatting. @@ -919,3 +925,60 @@ func (t *HelmTemplater) collapseBlankLineAfterIf(yamlContent string) string { } return strings.Join(out, "\n") } + +// templatePorts replaces hardcoded port values with Helm template references +// This makes ports configurable via values.yaml under webhook.port and metrics.port +func (t *HelmTemplater) templatePorts(yamlContent string, resource *unstructured.Unstructured) string { + resourceName := resource.GetName() + + // Determine if this is a webhook-related resource + isWebhook := strings.Contains(resourceName, "webhook") + + // Determine if this is a metrics-related resource + isMetrics := strings.Contains(resourceName, "metrics") + + // For Deployments, check for webhook ports in the content + if resource.GetKind() == kindDeployment { + // Check if this deployment has webhook-server ports + if strings.Contains(yamlContent, "webhook-server") || strings.Contains(yamlContent, "name: webhook") { + isWebhook = true + } + } + + // Template webhook ports (9443 by default) + if isWebhook { + // Replace containerPort: 9443 (or any value) for webhook-server with template + if strings.Contains(yamlContent, "webhook-server") { + yamlContent = regexp.MustCompile(`(?m)(\s*- )?containerPort:\s*\d+(\s*\n\s*name:\s*webhook-server)`). + ReplaceAllString(yamlContent, "${1}containerPort: {{ .Values.webhook.port }}${2}") + } + + // Replace targetPort: 9443 with webhook.port template + yamlContent = regexp.MustCompile(`(\s*)targetPort:\s*9443`). + ReplaceAllString(yamlContent, "${1}targetPort: {{ .Values.webhook.port }}") + } + + // Template metrics ports (8443 by default) + if isMetrics { + // Replace port: 8443 with metrics.port template + yamlContent = regexp.MustCompile(`(\s*)port:\s*8443`). + ReplaceAllString(yamlContent, "${1}port: {{ .Values.metrics.port }}") + + // Replace targetPort: 8443 with metrics.port template + yamlContent = regexp.MustCompile(`(\s*)targetPort:\s*8443`). + ReplaceAllString(yamlContent, "${1}targetPort: {{ .Values.metrics.port }}") + } + + // Template port-related arguments in Deployment + if resource.GetKind() == kindDeployment { + // Replace --metrics-bind-address=:8443 with templated version + yamlContent = regexp.MustCompile(`--metrics-bind-address=:[0-9]+`). + ReplaceAllString(yamlContent, "--metrics-bind-address=:{{ .Values.metrics.port }}") + + // Replace --webhook-port=9443 with templated version (if present) + yamlContent = regexp.MustCompile(`--webhook-port=[0-9]+`). + ReplaceAllString(yamlContent, "--webhook-port={{ .Values.webhook.port }}") + } + + return yamlContent +} diff --git a/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/helm_templater_test.go b/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/helm_templater_test.go index 2a83d57182e..11d94664e37 100644 --- a/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/helm_templater_test.go +++ b/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/helm_templater_test.go @@ -144,7 +144,7 @@ spec: result := templater.ApplyHelmSubstitutions(content, deploymentResource) Expect(result).To(ContainSubstring("{{- if .Values.metrics.enable }}")) - Expect(result).To(ContainSubstring("- --metrics-bind-address=:8443")) + Expect(result).To(ContainSubstring("- --metrics-bind-address=:{{ .Values.metrics.port }}")) Expect(result).To(ContainSubstring("- --metrics-bind-address=0")) Expect(result).To(ContainSubstring("- --health-probe-bind-address=:8081")) Expect(result).To(ContainSubstring("{{- range .Values.controllerManager.args }}")) @@ -471,4 +471,204 @@ metadata: Expect(result).To(Equal(malformedContent)) }) }) + + Context("templatePorts", func() { + It("should template webhook service ports", func() { + webhookService := &unstructured.Unstructured{} + webhookService.SetAPIVersion("v1") + webhookService.SetKind("Service") + webhookService.SetName("test-project-webhook-service") + + content := `apiVersion: v1 +kind: Service +metadata: + name: test-project-webhook-service + namespace: test-project-system +spec: + ports: + - port: 443 + targetPort: 9443 + protocol: TCP + selector: + control-plane: controller-manager` + + result := templater.templatePorts(content, webhookService) + + // Should template webhook port + Expect(result).To(ContainSubstring("targetPort: {{ .Values.webhook.port }}")) + Expect(result).NotTo(ContainSubstring("targetPort: 9443")) + }) + + It("should template metrics service ports", func() { + metricsService := &unstructured.Unstructured{} + metricsService.SetAPIVersion("v1") + metricsService.SetKind("Service") + metricsService.SetName("test-project-controller-manager-metrics-service") + + content := `apiVersion: v1 +kind: Service +metadata: + name: test-project-controller-manager-metrics-service + namespace: test-project-system +spec: + ports: + - port: 8443 + targetPort: 8443 + protocol: TCP + name: https + selector: + control-plane: controller-manager` + + result := templater.templatePorts(content, metricsService) + + // Should template metrics port + Expect(result).To(ContainSubstring("port: {{ .Values.metrics.port }}")) + Expect(result).To(ContainSubstring("targetPort: {{ .Values.metrics.port }}")) + Expect(result).NotTo(ContainSubstring("port: 8443")) + Expect(result).NotTo(ContainSubstring("targetPort: 8443")) + }) + + It("should template webhook container ports in Deployment", func() { + deployment := &unstructured.Unstructured{} + deployment.SetAPIVersion("apps/v1") + deployment.SetKind("Deployment") + deployment.SetName("test-project-controller-manager") + + content := `apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-project-controller-manager +spec: + template: + spec: + containers: + - name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP` + + result := templater.templatePorts(content, deployment) + + // Should template webhook containerPort + Expect(result).To(ContainSubstring("containerPort: {{ .Values.webhook.port }}")) + Expect(result).NotTo(ContainSubstring("containerPort: 9443")) + }) + + It("should template health probe ports in Deployment", func() { + deployment := &unstructured.Unstructured{} + deployment.SetAPIVersion("apps/v1") + deployment.SetKind("Deployment") + deployment.SetName("test-project-controller-manager") + + content := `apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-project-controller-manager +spec: + template: + spec: + containers: + - name: manager + livenessProbe: + httpGet: + path: /healthz + port: 8081 + readinessProbe: + httpGet: + path: /readyz + port: 8081` + + result := templater.templatePorts(content, deployment) + + Expect(result).To(ContainSubstring("port: 8081")) + Expect(result).NotTo(ContainSubstring("{{ .Values")) + }) + + It("should template port-related args in Deployment", func() { + deployment := &unstructured.Unstructured{} + deployment.SetAPIVersion("apps/v1") + deployment.SetKind("Deployment") + deployment.SetName("test-project-controller-manager") + + content := `apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-project-controller-manager +spec: + template: + spec: + containers: + - name: manager + args: + - --metrics-bind-address=:8443 + - --health-probe-bind-address=:8081 + - --leader-elect` + + result := templater.templatePorts(content, deployment) + + Expect(result).To(ContainSubstring("--metrics-bind-address=:{{ .Values.metrics.port }}")) + Expect(result).NotTo(ContainSubstring("--metrics-bind-address=:8443")) + Expect(result).To(ContainSubstring("--health-probe-bind-address=:8081")) + Expect(result).To(ContainSubstring("--leader-elect")) + }) + + It("should template custom port values", func() { + deployment := &unstructured.Unstructured{} + deployment.SetAPIVersion("apps/v1") + deployment.SetKind("Deployment") + deployment.SetName("test-project-controller-manager") + + content := `apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-project-controller-manager +spec: + template: + spec: + containers: + - name: manager + args: + - --metrics-bind-address=:9090 + - --health-probe-bind-address=:9091 + - --webhook-port=9444 + ports: + - containerPort: 9444 + name: webhook-server + livenessProbe: + httpGet: + port: 9091` + + result := templater.templatePorts(content, deployment) + + Expect(result).To(ContainSubstring("--metrics-bind-address=:{{ .Values.metrics.port }}")) + Expect(result).To(ContainSubstring("--webhook-port={{ .Values.webhook.port }}")) + Expect(result).To(ContainSubstring("containerPort: {{ .Values.webhook.port }}")) + Expect(result).To(ContainSubstring("--health-probe-bind-address=:9091")) + Expect(result).To(ContainSubstring("port: 9091")) + }) + + It("should not template non-webhook/metrics resources", func() { + regularService := &unstructured.Unstructured{} + regularService.SetAPIVersion("v1") + regularService.SetKind("Service") + regularService.SetName("test-project-some-other-service") + + content := `apiVersion: v1 +kind: Service +metadata: + name: test-project-some-other-service +spec: + ports: + - port: 8080 + targetPort: 8080` + + result := templater.templatePorts(content, regularService) + + // Should not template regular service ports + Expect(result).To(ContainSubstring("port: 8080")) + Expect(result).To(ContainSubstring("targetPort: 8080")) + Expect(result).NotTo(ContainSubstring("{{ .Values")) + }) + }) }) diff --git a/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/suite_test.go b/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/suite_test.go new file mode 100644 index 00000000000..2a46c909ef0 --- /dev/null +++ b/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package templates + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestTemplates(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Templates Suite") +} diff --git a/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/values_basic.go b/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/values_basic.go index 74103ad385d..6f683556652 100644 --- a/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/values_basic.go +++ b/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/values_basic.go @@ -125,23 +125,32 @@ crd: `) // Metrics configuration (enable if metrics artifacts detected in kustomize output) + metricsPort := 8443 + if f.DeploymentConfig != nil { + if mp, ok := f.DeploymentConfig["metricsPort"].(int); ok && mp > 0 { + metricsPort = mp + } + } + if f.HasMetrics { - buf.WriteString(`# Controller metrics endpoint. + buf.WriteString(fmt.Sprintf(`# Controller metrics endpoint. # Enable to expose /metrics endpoint with RBAC protection. metrics: enable: true + port: %d # Metrics server port -`) +`, metricsPort)) } else { - buf.WriteString(`# Controller metrics endpoint. + buf.WriteString(fmt.Sprintf(`# Controller metrics endpoint. # Enable to expose /metrics endpoint with RBAC protection. metrics: enable: false + port: %d # Metrics server port -`) +`, metricsPort)) } - // Cert-manager configuration - only if certificates/webhooks are present + // Cert-manager configuration (always present, enabled based on webhooks) if f.HasWebhooks { buf.WriteString(`# Cert-manager integration for TLS certificates. # Required for webhook certificates and metrics endpoint certificates. @@ -149,6 +158,30 @@ certManager: enable: true `) + } else { + buf.WriteString(`# Cert-manager integration for TLS certificates. +# Required for webhook certificates and metrics endpoint certificates. +certManager: + enable: false + +`) + } + + // Webhook configuration - only if webhooks are present + if f.HasWebhooks { + webhookPort := 9443 + if f.DeploymentConfig != nil { + if wp, ok := f.DeploymentConfig["webhookPort"].(int); ok && wp > 0 { + webhookPort = wp + } + } + + buf.WriteString(fmt.Sprintf(`# Webhook server configuration +webhook: + enable: true + port: %d # Webhook server port + +`, webhookPort)) } // Prometheus configuration diff --git a/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/values_basic_test.go b/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/values_basic_test.go index 252b433af56..1268fe5a0c3 100644 --- a/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/values_basic_test.go +++ b/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/values_basic_test.go @@ -72,8 +72,8 @@ var _ = Describe("HelmValuesBasic", func() { It("should not include certManager configuration", func() { content := valuesTemplate.GetBody() - Expect(content).NotTo(ContainSubstring("certManager:")) - Expect(content).NotTo(ContainSubstring("enable: true")) + Expect(content).To(ContainSubstring("certManager:")) + Expect(content).To(ContainSubstring("enable: false")) }) It("should still include other basic sections", func() { @@ -108,7 +108,7 @@ var _ = Describe("HelmValuesBasic", func() { It("should have correct file permissions", func() { info := valuesTemplate.GetIfExistsAction() - Expect(info).To(Equal(machinery.OverwriteFile)) + Expect(info).To(Equal(machinery.SkipFile)) }) }) @@ -159,7 +159,6 @@ var _ = Describe("HelmValuesBasic", func() { Expect(content).To(ContainSubstring("resources:")) Expect(content).To(ContainSubstring("cpu: 100m")) Expect(content).To(ContainSubstring("memory: 128Mi")) - Expect(content).To(ContainSubstring("manager:")) }) }) @@ -218,4 +217,189 @@ var _ = Describe("HelmValuesBasic", func() { Expect(lines[rbacHelpersIndex+1]).To(ContainSubstring("enable: false")) }) }) + + Context("Port configuration", func() { + Context("with default ports", func() { + BeforeEach(func() { + valuesTemplate = &HelmValuesBasic{ + HasWebhooks: true, + HasMetrics: true, + DeploymentConfig: map[string]interface{}{}, + } + valuesTemplate.InjectProjectName("test-project") + err := valuesTemplate.SetTemplateDefaults() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should include default webhook port", func() { + content := valuesTemplate.GetBody() + Expect(content).To(ContainSubstring("webhook:")) + Expect(content).To(ContainSubstring("enable: true")) + Expect(content).To(ContainSubstring("port: 9443")) + }) + + It("should include certManager enabled", func() { + content := valuesTemplate.GetBody() + Expect(content).To(ContainSubstring("certManager:")) + Expect(content).To(ContainSubstring("enable: true")) + }) + + It("should include default metrics port", func() { + content := valuesTemplate.GetBody() + Expect(content).To(ContainSubstring("metrics:")) + Expect(content).To(ContainSubstring("enable: true")) + Expect(content).To(ContainSubstring("port: 8443")) + }) + + It("should not expose health port in values", func() { + content := valuesTemplate.GetBody() + Expect(content).NotTo(ContainSubstring("healthPort")) + }) + }) + + Context("with custom ports extracted from deployment", func() { + BeforeEach(func() { + deploymentConfig := map[string]interface{}{ + "webhookPort": 9444, + "metricsPort": 9090, + } + + valuesTemplate = &HelmValuesBasic{ + HasWebhooks: true, + HasMetrics: true, + DeploymentConfig: deploymentConfig, + } + valuesTemplate.InjectProjectName("test-project") + err := valuesTemplate.SetTemplateDefaults() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should use custom webhook port", func() { + content := valuesTemplate.GetBody() + Expect(content).To(ContainSubstring("webhook:")) + Expect(content).To(ContainSubstring("port: 9444")) + Expect(content).NotTo(ContainSubstring("port: 9443")) + }) + + It("should use custom metrics port", func() { + content := valuesTemplate.GetBody() + Expect(content).To(ContainSubstring("metrics:")) + Expect(content).To(ContainSubstring("port: 9090")) + Expect(content).NotTo(ContainSubstring("port: 8443")) + }) + + It("should not expose health port", func() { + content := valuesTemplate.GetBody() + Expect(content).NotTo(ContainSubstring("healthPort")) + }) + }) + + Context("with partial custom ports", func() { + BeforeEach(func() { + deploymentConfig := map[string]interface{}{ + "metricsPort": 9090, + // webhookPort not provided - should use default + } + + valuesTemplate = &HelmValuesBasic{ + HasWebhooks: true, + HasMetrics: true, + DeploymentConfig: deploymentConfig, + } + valuesTemplate.InjectProjectName("test-project") + err := valuesTemplate.SetTemplateDefaults() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should use custom metrics port and default webhook port", func() { + content := valuesTemplate.GetBody() + Expect(content).To(ContainSubstring("port: 9090")) // custom metrics + Expect(content).To(ContainSubstring("port: 9443")) // default webhook + }) + + It("should not expose health port", func() { + content := valuesTemplate.GetBody() + Expect(content).NotTo(ContainSubstring("healthPort")) + }) + }) + + Context("with no webhooks but with metrics", func() { + BeforeEach(func() { + valuesTemplate = &HelmValuesBasic{ + HasWebhooks: false, + HasMetrics: true, + DeploymentConfig: map[string]interface{}{}, + } + valuesTemplate.InjectProjectName("test-project") + err := valuesTemplate.SetTemplateDefaults() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should not include webhook configuration", func() { + content := valuesTemplate.GetBody() + Expect(content).NotTo(ContainSubstring("webhook:")) + }) + + It("should include metrics port configuration", func() { + content := valuesTemplate.GetBody() + Expect(content).To(ContainSubstring("metrics:")) + Expect(content).To(ContainSubstring("port: 8443")) + Expect(content).NotTo(ContainSubstring("healthPort")) + }) + }) + + Context("port configuration structure", func() { + BeforeEach(func() { + valuesTemplate = &HelmValuesBasic{ + HasWebhooks: true, + HasMetrics: true, + DeploymentConfig: map[string]interface{}{}, + } + valuesTemplate.InjectProjectName("test-project") + err := valuesTemplate.SetTemplateDefaults() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should have ports under webhook section", func() { + content := valuesTemplate.GetBody() + lines := strings.Split(content, "\n") + + var webhookLine, portLine int + for i, line := range lines { + if strings.Contains(line, "webhook:") && !strings.Contains(line, "#") { + webhookLine = i + } + if webhookLine > 0 && i > webhookLine && strings.Contains(line, "port:") && + strings.Contains(line, "9443") { + portLine = i + break + } + } + + Expect(webhookLine).To(BeNumerically(">", 0)) + Expect(portLine).To(BeNumerically(">", webhookLine)) + Expect(portLine - webhookLine).To(BeNumerically("<=", 3)) + }) + + It("should have port under metrics section", func() { + content := valuesTemplate.GetBody() + lines := strings.Split(content, "\n") + + var metricsLine, portLine int + for i, line := range lines { + if strings.Contains(line, "metrics:") && !strings.Contains(line, "#") { + metricsLine = i + } + if metricsLine > 0 && i > metricsLine { + if strings.Contains(line, "port:") && strings.Contains(line, "8443") { + portLine = i + } + } + } + + Expect(metricsLine).To(BeNumerically(">", 0)) + Expect(portLine).To(BeNumerically(">", metricsLine)) + }) + }) + }) }) diff --git a/testdata/project-v4-with-plugins/dist/chart/templates/manager/manager.yaml b/testdata/project-v4-with-plugins/dist/chart/templates/manager/manager.yaml index dbcbdfd1e1a..2adde9308c6 100644 --- a/testdata/project-v4-with-plugins/dist/chart/templates/manager/manager.yaml +++ b/testdata/project-v4-with-plugins/dist/chart/templates/manager/manager.yaml @@ -24,7 +24,7 @@ spec: containers: - args: {{- if .Values.metrics.enable }} - - --metrics-bind-address=:8443 + - --metrics-bind-address=:{{ .Values.metrics.port }} {{- else }} # Bind to :0 to disable the controller-runtime managed metrics server - --metrics-bind-address=0 @@ -54,7 +54,7 @@ spec: periodSeconds: 20 name: manager ports: - - containerPort: 9443 + - containerPort: {{ .Values.webhook.port }} name: webhook-server protocol: TCP readinessProbe: diff --git a/testdata/project-v4-with-plugins/dist/chart/templates/metrics/controller-manager-metrics-service.yaml b/testdata/project-v4-with-plugins/dist/chart/templates/metrics/controller-manager-metrics-service.yaml index 1f98f524ec4..058eb79a8c9 100644 --- a/testdata/project-v4-with-plugins/dist/chart/templates/metrics/controller-manager-metrics-service.yaml +++ b/testdata/project-v4-with-plugins/dist/chart/templates/metrics/controller-manager-metrics-service.yaml @@ -11,9 +11,9 @@ metadata: spec: ports: - name: https - port: 8443 + port: {{ .Values.metrics.port }} protocol: TCP - targetPort: 8443 + targetPort: {{ .Values.metrics.port }} selector: app.kubernetes.io/name: project-v4-with-plugins control-plane: controller-manager diff --git a/testdata/project-v4-with-plugins/dist/chart/templates/webhook/webhook-service.yaml b/testdata/project-v4-with-plugins/dist/chart/templates/webhook/webhook-service.yaml index 00d0f928b87..15a2ec1ef37 100644 --- a/testdata/project-v4-with-plugins/dist/chart/templates/webhook/webhook-service.yaml +++ b/testdata/project-v4-with-plugins/dist/chart/templates/webhook/webhook-service.yaml @@ -10,7 +10,7 @@ spec: ports: - port: 443 protocol: TCP - targetPort: 9443 + targetPort: {{ .Values.webhook.port }} selector: app.kubernetes.io/name: project-v4-with-plugins control-plane: controller-manager diff --git a/testdata/project-v4-with-plugins/dist/chart/values.yaml b/testdata/project-v4-with-plugins/dist/chart/values.yaml index 2d00f8fcc31..615d5c8f099 100644 --- a/testdata/project-v4-with-plugins/dist/chart/values.yaml +++ b/testdata/project-v4-with-plugins/dist/chart/values.yaml @@ -60,12 +60,18 @@ crd: # Enable to expose /metrics endpoint with RBAC protection. metrics: enable: true + port: 8443 # Metrics server port # Cert-manager integration for TLS certificates. # Required for webhook certificates and metrics endpoint certificates. certManager: enable: true +# Webhook server configuration +webhook: + enable: true + port: 9443 # Webhook server port + # Prometheus ServiceMonitor for metrics scraping. # Requires prometheus-operator to be installed in the cluster. prometheus: