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 4b09d27c50c..3f74ec20a4a 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 @@ -34,7 +34,12 @@ spec: {{- end }} command: - /manager + {{- if .Values.controllerManager.image.digest }} + image: "{{ .Values.controllerManager.image.repository }}@{{ .Values.controllerManager.image.digest }}" + {{- else }} image: "{{ .Values.controllerManager.image.repository }}:{{ .Values.controllerManager.image.tag }}" + {{- end }} + imagePullPolicy: "{{ .Values.controllerManager.image.pullPolicy }}" livenessProbe: httpGet: path: /healthz @@ -52,13 +57,10 @@ spec: port: 8081 initialDelaySeconds: 5 periodSeconds: 10 + {{- with .Values.controllerManager.resources }} resources: - limits: - cpu: 500m - memory: 128Mi - requests: - cpu: 10m - memory: 64Mi +{{- toYaml . | nindent 20 }} + {{- end }} securityContext: allowPrivilegeEscalation: false capabilities: 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 f0dd2dcc57d..574068175f6 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 @@ -28,7 +28,12 @@ spec: - --health-probe-bind-address=:8081 command: - /manager + {{- if .Values.controllerManager.image.digest }} + image: "{{ .Values.controllerManager.image.repository }}@{{ .Values.controllerManager.image.digest }}" + {{- else }} image: "{{ .Values.controllerManager.image.repository }}:{{ .Values.controllerManager.image.tag }}" + {{- end }} + imagePullPolicy: "{{ .Values.controllerManager.image.pullPolicy }}" livenessProbe: httpGet: path: /healthz @@ -43,13 +48,10 @@ spec: port: 8081 initialDelaySeconds: 5 periodSeconds: 10 + {{- with .Values.controllerManager.resources }} resources: - limits: - cpu: 500m - memory: 128Mi - requests: - cpu: 10m - memory: 64Mi +{{- toYaml . | nindent 20 }} + {{- end }} securityContext: allowPrivilegeEscalation: false capabilities: 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 4b09d27c50c..3f74ec20a4a 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 @@ -34,7 +34,12 @@ spec: {{- end }} command: - /manager + {{- if .Values.controllerManager.image.digest }} + image: "{{ .Values.controllerManager.image.repository }}@{{ .Values.controllerManager.image.digest }}" + {{- else }} image: "{{ .Values.controllerManager.image.repository }}:{{ .Values.controllerManager.image.tag }}" + {{- end }} + imagePullPolicy: "{{ .Values.controllerManager.image.pullPolicy }}" livenessProbe: httpGet: path: /healthz @@ -52,13 +57,10 @@ spec: port: 8081 initialDelaySeconds: 5 periodSeconds: 10 + {{- with .Values.controllerManager.resources }} resources: - limits: - cpu: 500m - memory: 128Mi - requests: - cpu: 10m - memory: 64Mi +{{- toYaml . | nindent 20 }} + {{- end }} securityContext: allowPrivilegeEscalation: false capabilities: 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 b7d7c1b264a..1e89497fa22 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" + "strings" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -128,12 +129,30 @@ func (c *ChartConverter) ExtractDeploymentConfig() map[string]interface{} { return config } - // Use the first container (manager container) - firstContainer, ok := containersList[0].(map[string]interface{}) - if !ok { - return config + // Find manager container by name, fallback to first container + var targetContainer map[string]interface{} + for _, c := range containersList { + container, ok := c.(map[string]interface{}) + if !ok { + continue + } + if name, nameOk := container["name"].(string); nameOk && name == "manager" { + targetContainer = container + break + } + } + + // Fallback to first container if manager not found + if targetContainer == nil { + if firstContainer, ok := containersList[0].(map[string]interface{}); ok { + targetContainer = firstContainer + } else { + return config + } } + firstContainer := targetContainer + // Extract environment variables if env, envFound, envErr := unstructured.NestedFieldNoCopy(firstContainer, "env"); envFound && envErr == nil { if envList, envOk := env.([]interface{}); envOk && len(envList) > 0 { @@ -157,5 +176,48 @@ func (c *ChartConverter) ExtractDeploymentConfig() map[string]interface{} { } } + // Extract image configuration + if image, found, err := unstructured.NestedString(firstContainer, "image"); found && err == nil && image != "" { + config["image"] = parseImageString(image) + } + + // Extract imagePullPolicy + if pullPolicy, found, err := unstructured.NestedString(firstContainer, "imagePullPolicy"); found && err == nil && pullPolicy != "" { + config["imagePullPolicy"] = pullPolicy + } + return config } + +// parseImageString parses "[@]" or "[:]". +// It distinguishes registry ports from tags by requiring the tag colon +// to come AFTER the last '/'. +func parseImageString(image string) map[string]interface{} { + out := make(map[string]interface{}) + + // Digest form takes precedence + if at := strings.IndexByte(image, '@'); at != -1 { + out["repository"] = image[:at] + if at+1 < len(image) { + out["digest"] = image[at+1:] + } + return out + } + + lastSlash := strings.LastIndexByte(image, '/') + lastColon := strings.LastIndexByte(image, ':') + + // Tag only if the colon comes after the last slash + if lastColon != -1 && lastColon > lastSlash { + out["repository"] = image[:lastColon] + if lastColon+1 < len(image) { + out["tag"] = image[lastColon+1:] + } + return out + } + + // Untagged/undigested; kube will pull :latest, but we surface it explicitly + out["repository"] = image + out["tag"] = "latest" + return out +} 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 ca889be0cd0..c3faad86b0a 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 @@ -158,6 +158,12 @@ var _ = Describe("ChartConverter", func() { Expect(config).NotTo(BeNil()) Expect(config).To(HaveKey("env")) Expect(config).To(HaveKey("resources")) + + // Verify image extraction + Expect(config).To(HaveKey("image")) + imageConfig := config["image"].(map[string]interface{}) + Expect(imageConfig["repository"]).To(Equal("controller")) + Expect(imageConfig["tag"]).To(Equal("latest")) }) It("should handle deployment without containers", func() { 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 d32ce64523b..bbabf02b322 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 @@ -219,8 +219,8 @@ func (t *HelmTemplater) substituteRBACValues(yamlContent string) string { func (t *HelmTemplater) templateDeploymentFields(yamlContent string) string { // Template configuration fields yamlContent = t.templateImageReference(yamlContent) - yamlContent = t.templateEnvironmentVariables(yamlContent) yamlContent = t.templateResources(yamlContent) + yamlContent = t.templateEnvironmentVariables(yamlContent) yamlContent = t.templateSecurityContexts(yamlContent) yamlContent = t.templateVolumeMounts(yamlContent) yamlContent = t.templateVolumes(yamlContent) @@ -237,9 +237,115 @@ func (t *HelmTemplater) templateEnvironmentVariables(yamlContent string) string // templateResources converts resource sections to Helm templates func (t *HelmTemplater) templateResources(yamlContent string) string { - // This ensures that volumeMounts, volumes, and other fields are preserved - // The resources will remain as-is from the kustomize output and can be templated later - return yamlContent + // Find the containers: block and its indent + contHdr := regexp.MustCompile(`(?m)^(\s*)containers:\s*$`) + hdrLoc := contHdr.FindStringSubmatchIndex(yamlContent) + if hdrLoc == nil { + return yamlContent + } + + // Find list items - they should be after containers: + afterContainers := yamlContent[hdrLoc[1]:] + + // Find first list item to determine actual indentation + firstItemRe := regexp.MustCompile(`(?m)^(\s*)- `) + firstItemMatch := firstItemRe.FindStringSubmatchIndex(afterContainers) + if firstItemMatch == nil { + return yamlContent + } + + itemIndent := afterContainers[firstItemMatch[2]:firstItemMatch[3]] + + // Process with the detected indentation + rest := afterContainers + itemRe := regexp.MustCompile(`(?m)^` + regexp.QuoteMeta(itemIndent) + `- `) + + var out strings.Builder + out.WriteString(yamlContent[:hdrLoc[1]]) + idx := 0 + for { + start := itemRe.FindStringIndex(rest[idx:]) + if start == nil { + out.WriteString(rest[idx:]) + break + } + // Absolute bounds in 'rest' + s := idx + start[0] + e := idx + start[1] + + // Find end of this item: next item at same indent (search from end of current match) + next := itemRe.FindStringIndex(rest[e:]) + itemEnd := len(rest) + if next != nil { + itemEnd = e + next[0] + } + item := rest[s:itemEnd] + + // Only touch the item that has name: manager|controller-manager + // Handle both cases: "- name: manager" and separate " name: manager" lines + // Capture the indent from the name line for consistent field alignment + nameRe := regexp.MustCompile(`(?m)^([ \t]*)-?\s*name:\s*(manager|controller-manager)\s*$`) + nameMatch := nameRe.FindStringSubmatchIndex(item) + if nameMatch != nil { + // If already templated, do nothing to avoid double insertion + if !strings.Contains(item, "{{- with .Values.controllerManager.resources }}") { + // Get the indent for fields in this container + rawIndent := item[nameMatch[2]:nameMatch[3]] + // If the name is on the same line as "- ", we need to add 2 spaces for field indentation + // Otherwise, use the existing indentation + indent := rawIndent + if strings.Contains(item[nameMatch[0]:nameMatch[1]], "- ") { + indent = rawIndent + " " // Add 2 spaces for field alignment after "- " + } + nindent := len(indent) + 2 + + // Build resources template with proper indentation + resourcesTemplate := indent + `{{- with .Values.controllerManager.resources }}` + "\n" + + indent + `resources:` + "\n" + + `{{- toYaml . | nindent ` + fmt.Sprint(nindent) + ` }}` + "\n" + + indent + `{{- end }}` + + // Find the resources section specifically + // Look for "resources:" at the correct indent, then find where it ends + resStartRe := regexp.MustCompile(`(?m)^` + regexp.QuoteMeta(indent) + `resources:\s*$`) + if resMatch := resStartRe.FindStringIndex(item); resMatch != nil { + // Find where the resources section ends (next field at same indent level) + afterResources := item[resMatch[1]:] + + // Look for the next field at the same indentation level + nextFieldRe := regexp.MustCompile(`(?m)^` + regexp.QuoteMeta(indent) + `[a-zA-Z]`) + endMatch := nextFieldRe.FindStringIndex(afterResources) + + var resourcesEnd int + if endMatch != nil { + // Found next field, resources ends there + resourcesEnd = resMatch[1] + endMatch[0] + } else { + // No next field, resources goes to end of container + resourcesEnd = len(item) + } + + // Replace just the resources section + item = item[:resMatch[0]] + resourcesTemplate + "\n" + item[resourcesEnd:] + } else { + // Prefer insertion right after the image digest/tag conditional end if present + endRe := regexp.MustCompile(`(?m)^` + regexp.QuoteMeta(indent) + `{{- end }}\s*$`) + if m := endRe.FindStringIndex(item); m != nil { + item = item[:m[1]] + "\n" + resourcesTemplate + "\n" + item[m[1]:] + } else { + // Fallback: insert immediately after the "name: manager" line + nameLineRe := regexp.MustCompile(`(?m)^` + regexp.QuoteMeta(indent) + `name:\s*(manager|controller-manager)\s*$`) + item = nameLineRe.ReplaceAllString(item, `$0`+"\n"+resourcesTemplate+"\n") + } + } + } + } + + out.WriteString(rest[idx:s]) + out.WriteString(item) + idx = itemEnd + } + return out.String() } // templateSecurityContexts preserves security contexts from kustomize output @@ -265,27 +371,106 @@ func (t *HelmTemplater) templateVolumes(yamlContent string) string { // templateImageReference converts hardcoded image references to Helm templates func (t *HelmTemplater) templateImageReference(yamlContent string) string { - // Replace hardcoded controller image with Helm template - // This handles the common case where kustomize outputs "controller:latest" - // or other hardcoded image references - imagePattern := regexp.MustCompile(`(\s+)image:\s+controller:latest`) - yamlContent = imagePattern.ReplaceAllString(yamlContent, - `${1}image: "{{ .Values.controllerManager.image.repository }}:{{ .Values.controllerManager.image.tag }}"`) - - // Also handle any other common image patterns that might appear - imagePattern2 := regexp.MustCompile(`(\s+)image:\s+([^"'\s]+):(latest|[\w\.\-]+)`) - yamlContent = imagePattern2.ReplaceAllStringFunc(yamlContent, func(match string) string { - // Only replace if it looks like a controller image (contains "controller" or "manager") - if strings.Contains(match, "controller") || strings.Contains(match, "manager") { - indentMatch := regexp.MustCompile(`^(\s+)`) - indent := indentMatch.FindString(match) - return fmt.Sprintf( - `%simage: "{{ .Values.controllerManager.image.repository }}:{{ .Values.controllerManager.image.tag }}"`, indent) + // Find the containers: block and its indent + contHdr := regexp.MustCompile(`(?m)^(\s*)containers:\s*$`) + hdrLoc := contHdr.FindStringSubmatchIndex(yamlContent) + if hdrLoc == nil { + return yamlContent + } + + // Find list items - they should be after containers: + afterContainers := yamlContent[hdrLoc[1]:] + + // Find first list item to determine actual indentation + firstItemRe := regexp.MustCompile(`(?m)^(\s*)- `) + firstItemMatch := firstItemRe.FindStringSubmatchIndex(afterContainers) + if firstItemMatch == nil { + return yamlContent + } + + itemIndent := afterContainers[firstItemMatch[2]:firstItemMatch[3]] + + // Process with the detected indentation + rest := afterContainers + itemRe := regexp.MustCompile(`(?m)^` + regexp.QuoteMeta(itemIndent) + `- `) + + var out strings.Builder + out.WriteString(yamlContent[:hdrLoc[1]]) + + idx := 0 + for { + start := itemRe.FindStringIndex(rest[idx:]) + if start == nil { + out.WriteString(rest[idx:]) + break + } + // Absolute bounds in 'rest' + s := idx + start[0] + e := idx + start[1] + + // Find end of this item: next item at same indent (search from end of current match) + next := itemRe.FindStringIndex(rest[e:]) + itemEnd := len(rest) + if next != nil { + itemEnd = e + next[0] } - return match - }) + item := rest[s:itemEnd] + + // Only touch the item that has name: manager|controller-manager + // Handle both cases: "- name: manager" and separate " name: manager" lines + // Capture the indent from the name line for consistent field alignment + nameRe := regexp.MustCompile(`(?m)^([ \t]*)-?\s*name:\s*(manager|controller-manager)\s*$`) + nameMatch := nameRe.FindStringSubmatchIndex(item) + if nameMatch != nil { + // Get the indent for fields in this container + rawIndent := item[nameMatch[2]:nameMatch[3]] + // If the name is on the same line as "- ", we need to add 2 spaces for field indentation + // Otherwise, use the existing indentation + indent := rawIndent + if strings.Contains(item[nameMatch[0]:nameMatch[1]], "- ") { + indent = rawIndent + " " // Add 2 spaces for field alignment after "- " + } - return yamlContent + // Build the image block with proper indentation + imageBlock := + indent + `{{- if .Values.controllerManager.image.digest }}` + "\n" + + indent + `image: "{{ .Values.controllerManager.image.repository }}@{{ .Values.controllerManager.image.digest }}"` + "\n" + + indent + `{{- else }}` + "\n" + + indent + `image: "{{ .Values.controllerManager.image.repository }}:{{ .Values.controllerManager.image.tag }}"` + "\n" + + indent + `{{- end }}` + + // Replace existing image line or insert after name + imgLineRe := regexp.MustCompile(`(?m)^[ \t]*image:\s*.*$`) + if imgLineRe.MatchString(item) { + item = imgLineRe.ReplaceAllString(item, imageBlock) + } else { + // Insert after name line + item = nameRe.ReplaceAllString(item, `$0`+"\n"+imageBlock) + } + + // Handle imagePullPolicy without relying on backrefs + ppRe := regexp.MustCompile(`(?m)^[ \t]*imagePullPolicy:\s*.*$`) + if ppRe.MatchString(item) { + item = ppRe.ReplaceAllStringFunc(item, func(_ string) string { + return indent + `imagePullPolicy: "{{ .Values.controllerManager.image.pullPolicy }}"` + }) + } else { + // Insert immediately after the image block + endLine := indent + `{{- end }}` + item = strings.Replace( + item, + endLine, + endLine+"\n"+indent+`imagePullPolicy: "{{ .Values.controllerManager.image.pullPolicy }}"`, + 1, + ) + } + } + + out.WriteString(rest[idx:s]) + out.WriteString(item) + idx = itemEnd + } + return out.String() } // makeWebhookAnnotationsConditional makes only cert-manager annotations conditional, not the entire webhook 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 ca66eb15682..ec5ead2ca38 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 @@ -428,4 +428,111 @@ metadata: Expect(result).To(Equal(malformedContent)) }) }) + + Context("image and resources templating", func() { + It("should template manager image, pullPolicy and resources with correct indentation", func() { + deploymentResource := &unstructured.Unstructured{} + deploymentResource.SetAPIVersion("apps/v1") + deploymentResource.SetKind("Deployment") + deploymentResource.SetName("test-project-controller-manager") + + content := `apiVersion: apps/v1 +kind: Deployment +spec: + template: + spec: + containers: + - name: manager + image: gcr.io/project/controller:v1.2.3 + imagePullPolicy: Always + resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi` + + result := templater.ApplyHelmSubstitutions(content, deploymentResource) + + // Should template image with digest/tag conditional + Expect(result).To(ContainSubstring(`{{- if .Values.controllerManager.image.digest }}`)) + Expect(result).To(ContainSubstring(`image: "{{ .Values.controllerManager.image.repository }}@{{ .Values.controllerManager.image.digest }}"`)) + Expect(result).To(ContainSubstring(`{{- else }}`)) + Expect(result).To(ContainSubstring(`image: "{{ .Values.controllerManager.image.repository }}:{{ .Values.controllerManager.image.tag }}"`)) + Expect(result).To(ContainSubstring(`{{- end }}`)) + + // Should template pullPolicy with quotes + Expect(result).To(ContainSubstring(`imagePullPolicy: "{{ .Values.controllerManager.image.pullPolicy }}"`)) + + // Should template resources with correct indentation (8 + 2 = 10 spaces for nindent) + Expect(result).To(ContainSubstring("{{- with .Values.controllerManager.resources }}")) + Expect(result).To(ContainSubstring("{{- toYaml . | nindent 10 }}")) + + // Should NOT have hardcoded values + Expect(result).NotTo(ContainSubstring("gcr.io/project/controller:v1.2.3")) + Expect(result).NotTo(ContainSubstring("imagePullPolicy: Always")) + Expect(result).NotTo(ContainSubstring("cpu: 200m")) + + // Should NOT have double indentation (no literal spaces before toYaml) + Expect(result).NotTo(ContainSubstring(" {{- toYaml")) + }) + + It("should preserve env and securityContext unchanged", func() { + deploymentResource := &unstructured.Unstructured{} + deploymentResource.SetAPIVersion("apps/v1") + deploymentResource.SetKind("Deployment") + deploymentResource.SetName("test-project-controller-manager") + + content := `apiVersion: apps/v1 +kind: Deployment +spec: + template: + spec: + containers: + - name: manager + env: + - name: CLUSTER_DOMAIN + value: cluster.local + securityContext: + runAsNonRoot: true` + + result := templater.ApplyHelmSubstitutions(content, deploymentResource) + + // env and securityContext should be preserved exactly + Expect(result).To(ContainSubstring("CLUSTER_DOMAIN")) + Expect(result).To(ContainSubstring("cluster.local")) + Expect(result).To(ContainSubstring("runAsNonRoot: true")) + }) + + It("should template image with digest OR tag conditional logic", func() { + deploymentResource := &unstructured.Unstructured{} + deploymentResource.SetAPIVersion("apps/v1") + deploymentResource.SetKind("Deployment") + deploymentResource.SetName("test-project-controller-manager") + + content := `apiVersion: apps/v1 +kind: Deployment +spec: + template: + spec: + containers: + - name: manager + image: gcr.io/project/controller@sha256:abc123def456 + imagePullPolicy: Always` + + result := templater.ApplyHelmSubstitutions(content, deploymentResource) + + // Should template image with digest/tag conditional (same as tag test since we use conditional logic) + Expect(result).To(ContainSubstring(`{{- if .Values.controllerManager.image.digest }}`)) + Expect(result).To(ContainSubstring(`image: "{{ .Values.controllerManager.image.repository }}@{{ .Values.controllerManager.image.digest }}"`)) + + // Should template pullPolicy + Expect(result).To(ContainSubstring(`imagePullPolicy: "{{ .Values.controllerManager.image.pullPolicy }}"`)) + + // Should NOT have hardcoded values + Expect(result).NotTo(ContainSubstring("gcr.io/project/controller@sha256:abc123def456")) + Expect(result).NotTo(ContainSubstring("imagePullPolicy: Always")) + }) + }) }) 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 b72f2214099..b634e4aa1c5 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 @@ -69,17 +69,48 @@ func (f *HelmValuesBasic) SetTemplateDefaults() error { func (f *HelmValuesBasic) generateBasicValues() string { var buf bytes.Buffer + // Extract values from kustomize output + imageRepo := "controller" // default + imageTag := "latest" // default + imageDigest := "" // empty by default + pullPolicy := "IfNotPresent" // default + + if f.DeploymentConfig != nil { + // Use extracted image values from kustomize + if img, ok := f.DeploymentConfig["image"].(map[string]interface{}); ok { + if repo, ok := img["repository"].(string); ok { + imageRepo = repo + } + // Digest takes precedence over tag + if dig, ok := img["digest"].(string); ok && dig != "" { + imageDigest = dig + imageTag = "" // clear tag when using digest + } else if tag, ok := img["tag"].(string); ok && tag != "" { + imageTag = tag + } + } + if pp, ok := f.DeploymentConfig["imagePullPolicy"].(string); ok && pp != "" { + pullPolicy = pp + } + } + // Controller Manager configuration buf.WriteString(`# Configure the controller manager deployment controllerManager: replicas: 1 image: - repository: controller - tag: latest - pullPolicy: IfNotPresent + repository: ` + imageRepo + "\n") -`) + // Only include tag or digest, not both + if imageDigest != "" { + buf.WriteString(` # Using digest from kustomize + digest: ` + imageDigest + "\n") + } else { + buf.WriteString(` tag: ` + imageTag + "\n") + } + + buf.WriteString(` pullPolicy: ` + pullPolicy + "\n\n") // Add extracted deployment configuration f.addDeploymentConfig(&buf) 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 e8cfbab712b..6b449a81d4f 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 @@ -34,7 +34,7 @@ var _ = Describe("HelmValuesBasic", func() { HasWebhooks: true, DeploymentConfig: map[string]interface{}{}, } - valuesTemplate.InjectProjectName("test-project") + valuesTemplate.ProjectName = "test-project" err := valuesTemplate.SetTemplateDefaults() Expect(err).NotTo(HaveOccurred()) }) @@ -49,7 +49,7 @@ var _ = Describe("HelmValuesBasic", func() { It("should include all basic sections", func() { content := valuesTemplate.GetBody() - Expect(content).To(ContainSubstring("replicaCount:")) + Expect(content).To(ContainSubstring("controllerManager:")) Expect(content).To(ContainSubstring("metrics:")) Expect(content).To(ContainSubstring("prometheus:")) Expect(content).To(ContainSubstring("rbacHelpers:")) @@ -62,7 +62,7 @@ var _ = Describe("HelmValuesBasic", func() { HasWebhooks: false, DeploymentConfig: map[string]interface{}{}, } - valuesTemplate.InjectProjectName("test-project") + valuesTemplate.ProjectName = "test-project" err := valuesTemplate.SetTemplateDefaults() Expect(err).NotTo(HaveOccurred()) }) @@ -71,17 +71,60 @@ var _ = Describe("HelmValuesBasic", func() { content := valuesTemplate.GetBody() Expect(content).NotTo(ContainSubstring("certManager:")) - Expect(content).NotTo(ContainSubstring("enable: true")) }) It("should still include other basic sections", func() { content := valuesTemplate.GetBody() - Expect(content).To(ContainSubstring("replicaCount:")) + Expect(content).To(ContainSubstring("controllerManager:")) Expect(content).To(ContainSubstring("metrics:")) Expect(content).To(ContainSubstring("prometheus:")) Expect(content).To(ContainSubstring("rbacHelpers:")) }) + + It("should use extracted values from DeploymentConfig", func() { + // Test with extracted deployment config + extractedConfig := map[string]interface{}{ + "image": map[string]interface{}{ + "repository": "custom-controller", + "tag": "v2.1.0", + }, + "imagePullPolicy": "Always", + "resources": map[string]interface{}{ + "limits": map[string]interface{}{ + "cpu": "800m", + "memory": "256Mi", + }, + "requests": map[string]interface{}{ + "cpu": "50m", + "memory": "128Mi", + }, + }, + } + + valuesWithConfig := &HelmValuesBasic{ + DeploymentConfig: extractedConfig, + OutputDir: "dist", + } + valuesWithConfig.ProjectName = "test-project" + err := valuesWithConfig.SetTemplateDefaults() + Expect(err).NotTo(HaveOccurred()) + + content := valuesWithConfig.GetBody() + + // Should use extracted values, not defaults + Expect(content).To(ContainSubstring("repository: custom-controller")) + Expect(content).To(ContainSubstring("tag: v2.1.0")) + Expect(content).To(ContainSubstring("pullPolicy: Always")) + Expect(content).To(ContainSubstring("cpu: 800m")) + Expect(content).To(ContainSubstring("memory: 256Mi")) + Expect(content).To(ContainSubstring("cpu: 50m")) + Expect(content).To(ContainSubstring("memory: 128Mi")) + + // Should NOT contain default hardcoded values + Expect(content).NotTo(ContainSubstring("repository: controller")) + Expect(content).NotTo(ContainSubstring("tag: latest")) + }) }) Context("template path and content", func() { @@ -89,13 +132,15 @@ var _ = Describe("HelmValuesBasic", func() { valuesTemplate = &HelmValuesBasic{ OutputDir: "dist", } - valuesTemplate.InjectProjectName("test-project") + valuesTemplate.ProjectName = "test-project" err := valuesTemplate.SetTemplateDefaults() Expect(err).NotTo(HaveOccurred()) }) It("should have correct path", func() { - Expect(valuesTemplate.GetPath()).To(Equal("dist/chart/values.yaml")) + path := valuesTemplate.GetPath() + // Handle both Windows and Unix path separators + Expect(path).To(SatisfyAny(Equal("dist/chart/values.yaml"), Equal("dist\\chart\\values.yaml"))) }) It("should implement Builder interface", func() { @@ -105,7 +150,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)) }) }) @@ -130,14 +175,14 @@ var _ = Describe("HelmValuesBasic", func() { HasWebhooks: false, DeploymentConfig: deploymentConfig, } - valuesTemplate.InjectProjectName("test-project") + valuesTemplate.ProjectName = "test-project" err := valuesTemplate.SetTemplateDefaults() Expect(err).NotTo(HaveOccurred()) }) It("should include deployment configuration", func() { content := valuesTemplate.GetBody() - Expect(content).To(ContainSubstring("manager:")) + Expect(content).To(ContainSubstring("controllerManager:")) }) }) @@ -146,7 +191,7 @@ var _ = Describe("HelmValuesBasic", func() { valuesTemplate = &HelmValuesBasic{ HasWebhooks: false, } - valuesTemplate.InjectProjectName("test-project") + valuesTemplate.ProjectName = "test-project" err := valuesTemplate.SetTemplateDefaults() Expect(err).NotTo(HaveOccurred()) }) 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 95df284bbe5..9572d17e57b 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 @@ -36,7 +36,12 @@ spec: value: busybox:1.36.1 - name: MEMCACHED_IMAGE value: memcached:1.6.26-alpine3.19 + {{- if .Values.controllerManager.image.digest }} + image: "{{ .Values.controllerManager.image.repository }}@{{ .Values.controllerManager.image.digest }}" + {{- else }} image: "{{ .Values.controllerManager.image.repository }}:{{ .Values.controllerManager.image.tag }}" + {{- end }} + imagePullPolicy: "{{ .Values.controllerManager.image.pullPolicy }}" livenessProbe: httpGet: path: /healthz @@ -54,13 +59,10 @@ spec: port: 8081 initialDelaySeconds: 5 periodSeconds: 10 + {{- with .Values.controllerManager.resources }} resources: - limits: - cpu: 500m - memory: 128Mi - requests: - cpu: 10m - memory: 64Mi +{{- toYaml . | nindent 20 }} + {{- end }} securityContext: allowPrivilegeEscalation: false capabilities: