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 e9cb6515c95..e20c77f4953 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 @@ -14,6 +14,9 @@ manager: # Environment variables env: [] + # Image pull secrets + imagePullSecrets: [] + # Pod-level security settings podSecurityContext: runAsNonRoot: true 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 0856fc67073..1bb5ce80370 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 @@ -14,6 +14,9 @@ manager: # Environment variables env: [] + # Image pull secrets + imagePullSecrets: [] + # Pod-level security settings podSecurityContext: runAsNonRoot: true 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 e9cb6515c95..e20c77f4953 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 @@ -14,6 +14,9 @@ manager: # Environment variables env: [] + # Image pull secrets + imagePullSecrets: [] + # Pod-level security settings podSecurityContext: runAsNonRoot: true 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 722de0c25a6..403a7926ad1 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 @@ -105,7 +105,7 @@ func (c *ChartConverter) ExtractDeploymentConfig() map[string]interface{} { } extractPodSecurityContext(specMap, config) - + extractImagePullSecrets(specMap, config) container := firstManagerContainer(specMap) if container == nil { return config @@ -135,6 +135,20 @@ func extractDeploymentSpec(deployment *unstructured.Unstructured) map[string]int return specMap } +func extractImagePullSecrets(specMap map[string]interface{}, config map[string]interface{}) { + imagePullSecrets, found, err := unstructured.NestedFieldNoCopy(specMap, "imagePullSecrets") + if !found || err != nil { + return + } + + imagePullSecretsList, ok := imagePullSecrets.([]interface{}) + if !ok || len(imagePullSecretsList) == 0 { + return + } + + config["imagePullSecrets"] = imagePullSecretsList +} + func extractPodSecurityContext(specMap map[string]interface{}, config map[string]interface{}) { podSecurityContext, found, err := unstructured.NestedFieldNoCopy(specMap, "securityContext") 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 4f307c32aaf..be31e0244a1 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 @@ -274,6 +274,39 @@ var _ = Describe("ChartConverter", func() { Expect(config["webhookPort"]).To(Equal(9444)) }) + It("should extract imagePullSecrets", func() { + // Set up deployment with image pull secrets + containers := []interface{}{ + map[string]interface{}{ + "name": "manager", + "image": "controller:latest", + }, + } + imagePullSecrets := []interface{}{ + map[string]interface{}{ + "name": "test-secret", + }, + } + // Set the image pull secrets + err := unstructured.SetNestedSlice( + resources.Deployment.Object, + imagePullSecrets, + "spec", "template", "spec", "imagePullSecrets", + ) + Expect(err).NotTo(HaveOccurred()) + // Set the containers + err = unstructured.SetNestedSlice( + resources.Deployment.Object, + containers, + "spec", "template", "spec", "containers", + ) + Expect(err).NotTo(HaveOccurred()) + + config := converter.ExtractDeploymentConfig() + Expect(config).To(HaveKey("imagePullSecrets")) + Expect(config["imagePullSecrets"]).To(Equal(imagePullSecrets)) + }) + It("should handle deployment without containers", func() { config := converter.ExtractDeploymentConfig() Expect(config).To(BeEmpty()) 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 de57e97e9f4..5be9a4c7e69 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 @@ -183,6 +183,7 @@ func (t *HelmTemplater) templateDeploymentFields(yamlContent string) string { // Template configuration fields yamlContent = t.templateImageReference(yamlContent) yamlContent = t.templateEnvironmentVariables(yamlContent) + yamlContent = t.templateImagePullSecrets(yamlContent) yamlContent = t.templatePodSecurityContext(yamlContent) yamlContent = t.templateContainerSecurityContext(yamlContent) yamlContent = t.templateResources(yamlContent) @@ -325,6 +326,57 @@ func (t *HelmTemplater) templateVolumes(yamlContent string) string { return yamlContent } +// templateImagePullSecrets exposes imagePullSecrets via values.yaml +func (t *HelmTemplater) templateImagePullSecrets(yamlContent string) string { + if !strings.Contains(yamlContent, "imagePullSecrets:") { + return yamlContent + } + + lines := strings.Split(yamlContent, "\n") + for i := 0; i < len(lines); i++ { + // Use prefix to allow `imagePullSecrets: []` to be preserved + if !strings.HasPrefix(strings.TrimSpace(lines[i]), "imagePullSecrets:") { + continue + } + indentStr, indentLen := leadingWhitespace(lines[i]) + end := i + 1 + for ; end < len(lines); end++ { + trimmed := strings.TrimSpace(lines[end]) + if trimmed == "" { + break + } + lineIndent := len(lines[end]) - len(strings.TrimLeft(lines[end], " \t")) + if lineIndent < indentLen { + break + } + if lineIndent == indentLen && !strings.HasPrefix(trimmed, "-") { + break + } + } + + if i+1 < len(lines) && strings.Contains(lines[i+1], ".Values.manager.imagePullSecrets") { + return yamlContent + } + + childIndent := indentStr + " " + childIndentWidth := strconv.Itoa(len(childIndent)) + + block := []string{ + indentStr + "{{- if .Values.manager.imagePullSecrets }}", + indentStr + "imagePullSecrets:", + childIndent + "{{- toYaml .Values.manager.imagePullSecrets | nindent " + childIndentWidth + " }}", + indentStr + "{{- end }}", + } + + newLines := append([]string{}, lines[:i]...) + newLines = append(newLines, block...) + newLines = append(newLines, lines[end:]...) + return strings.Join(newLines, "\n") + } + + return yamlContent +} + // templatePodSecurityContext exposes podSecurityContext via values.yaml func (t *HelmTemplater) templatePodSecurityContext(yamlContent string) string { if !strings.Contains(yamlContent, "securityContext:") { 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 9f945441ef6..07f3378a25e 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 @@ -230,6 +230,58 @@ metadata: Expect(result).NotTo(ContainSubstring(`{{- include "chart.labels"`)) Expect(result).NotTo(ContainSubstring(`{{- include "chart.annotations"`)) }) + + It("should template imagePullSecrets", 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: + imagePullSecrets: + - name: test-secret + containers: + - args: + - --metrics-bind-address=:8443 + - --health-probe-bind-address=:8081 + - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs/tls.crt + - --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs/tls.crt + - --leader-elect` + + result := templater.ApplyHelmSubstitutions(content, deploymentResource) + + Expect(result).To(ContainSubstring("imagePullSecrets:")) + Expect(result).NotTo(ContainSubstring("test-secret")) + }) + + It("should template empty imagePullSecrets", 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: + imagePullSecrets: [] + containers: + - args: + - --metrics-bind-address=:8443 + - --health-probe-bind-address=:8081 + - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs/tls.crt + - --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs/tls.crt + - --leader-elect` + + result := templater.ApplyHelmSubstitutions(content, deploymentResource) + + Expect(result).To(ContainSubstring("imagePullSecrets:")) + }) }) Context("conditional wrapping", func() { 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 1a2c13f648d..4ec27c51729 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 @@ -228,6 +228,25 @@ func (f *HelmValuesBasic) addDeploymentConfig(buf *bytes.Buffer) { buf.WriteString(" env: []\n\n") } + // Add image pull secrets + if imagePullSecrets, exists := f.DeploymentConfig["imagePullSecrets"]; exists && imagePullSecrets != nil { + buf.WriteString(" # Image pull secrets\n") + buf.WriteString(" imagePullSecrets:\n") + if imagePullSecretsYaml, err := yaml.Marshal(imagePullSecrets); err == nil { + lines := bytes.Split(imagePullSecretsYaml, []byte("\n")) + for _, line := range lines { + if len(line) > 0 { + buf.WriteString(" ") + buf.Write(line) + buf.WriteString("\n") + } + } + } + buf.WriteString("\n") + } else { + f.addDefaultImagePullSecrets(buf) + } + // Add podSecurityContext if podSecCtx, exists := f.DeploymentConfig["podSecurityContext"]; exists && podSecCtx != nil { buf.WriteString(" # Pod-level security settings\n") @@ -291,6 +310,7 @@ func (f *HelmValuesBasic) addDefaultDeploymentSections(buf *bytes.Buffer) { buf.WriteString(" # Environment variables\n") buf.WriteString(" env: []\n\n") + f.addDefaultImagePullSecrets(buf) f.addDefaultPodSecurityContext(buf) f.addDefaultSecurityContext(buf) f.addDefaultResources(buf) @@ -323,6 +343,12 @@ func (f *HelmValuesBasic) addArgsSection(buf *bytes.Buffer) { buf.WriteString(" args: []\n\n") } +// addDefaultImagePullSecrets adds default imagePullSecrets section +func (f *HelmValuesBasic) addDefaultImagePullSecrets(buf *bytes.Buffer) { + buf.WriteString(" # Image pull secrets\n") + buf.WriteString(" imagePullSecrets: []\n\n") +} + // addDefaultPodSecurityContext adds default podSecurityContext section func (f *HelmValuesBasic) addDefaultPodSecurityContext(buf *bytes.Buffer) { buf.WriteString(" # Pod-level security settings\n") 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 25a534b03e3..b19f8d8392d 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 @@ -55,6 +55,7 @@ var _ = Describe("HelmValuesBasic", func() { Expect(content).To(ContainSubstring("metrics:")) Expect(content).To(ContainSubstring("prometheus:")) Expect(content).To(ContainSubstring("rbacHelpers:")) + Expect(content).To(ContainSubstring("imagePullSecrets: []")) }) }) @@ -84,6 +85,7 @@ var _ = Describe("HelmValuesBasic", func() { Expect(content).To(ContainSubstring("metrics:")) Expect(content).To(ContainSubstring("prometheus:")) Expect(content).To(ContainSubstring("rbacHelpers:")) + Expect(content).To(ContainSubstring("imagePullSecrets: []")) }) }) @@ -162,6 +164,33 @@ var _ = Describe("HelmValuesBasic", func() { }) }) + Context("with multiple imagePullSecrets", func() { + BeforeEach(func() { + valuesTemplate = &HelmValuesBasic{ + DeploymentConfig: map[string]interface{}{ + "imagePullSecrets": []interface{}{ + map[string]interface{}{ + "name": "test-secret", + }, + map[string]interface{}{ + "name": "test-secret2", + }, + }, + }, + } + valuesTemplate.InjectProjectName("test-private-project") + err := valuesTemplate.SetTemplateDefaults() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should render multiple imagePullSecrets", func() { + content := valuesTemplate.GetBody() + Expect(content).To(ContainSubstring("imagePullSecrets:")) + Expect(content).To(ContainSubstring("- name: test-secret")) + Expect(content).To(ContainSubstring("- name: test-secret2")) + }) + }) + Context("with complex env variables", func() { BeforeEach(func() { valuesTemplate = &HelmValuesBasic{ diff --git a/testdata/project-v4-with-plugins/dist/chart/values.yaml b/testdata/project-v4-with-plugins/dist/chart/values.yaml index 94b6c6c4896..13f65a202e7 100644 --- a/testdata/project-v4-with-plugins/dist/chart/values.yaml +++ b/testdata/project-v4-with-plugins/dist/chart/values.yaml @@ -18,6 +18,9 @@ manager: - name: MEMCACHED_IMAGE value: memcached:1.6.26-alpine3.19 + # Image pull secrets + imagePullSecrets: [] + # Pod-level security settings podSecurityContext: runAsNonRoot: true