From 5c1607ba6e51045944c2c09c4071960f524a8fc4 Mon Sep 17 00:00:00 2001 From: hfuss Date: Fri, 22 Mar 2024 00:55:35 -0400 Subject: [PATCH 1/4] Exposing ExtraPodLabels and Tolerations on Deployments Signed-off-by: hfuss --- pkg/processor/deployment/deployment.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/processor/deployment/deployment.go b/pkg/processor/deployment/deployment.go index ad7c9234..a3834602 100644 --- a/pkg/processor/deployment/deployment.go +++ b/pkg/processor/deployment/deployment.go @@ -108,11 +108,16 @@ func (d deployment) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstr selector = strings.Trim(selector, " \n") selector = string(yamlformat.Indent([]byte(selector), 4)) + nameCamel := strcase.ToLowerCamel(name) podLabels, err := yamlformat.Marshal(depl.Spec.Template.ObjectMeta.Labels, 8) if err != nil { return true, nil, err } - podLabels += fmt.Sprintf("\n {{- include \"%s.selectorLabels\" . | nindent 8 }}", appMeta.ChartName()) + podLabels += fmt.Sprintf("\n {{- include \"%s.selectorLabels\" . | nindent 8 }}\n {{- toYaml .Values.%s.extraPodLabels | nindent 8 }}", appMeta.ChartName(), nameCamel) + err = unstructured.SetNestedField(values, make(map[string]interface{}), nameCamel, "podLabels") + if err != nil { + return true, nil, err + } podAnnotations := "" if len(depl.Spec.Template.ObjectMeta.Annotations) != 0 { From 3f0ff329fd7949927cc6d74be6088369aa177cef Mon Sep 17 00:00:00 2001 From: hfuss Date: Tue, 26 Mar 2024 23:40:13 -0400 Subject: [PATCH 2/4] fixing podLabels templating Signed-off-by: hfuss Made-with: Cursor --- pkg/processor/deployment/deployment.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/processor/deployment/deployment.go b/pkg/processor/deployment/deployment.go index a3834602..ce1f54aa 100644 --- a/pkg/processor/deployment/deployment.go +++ b/pkg/processor/deployment/deployment.go @@ -113,7 +113,7 @@ func (d deployment) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstr if err != nil { return true, nil, err } - podLabels += fmt.Sprintf("\n {{- include \"%s.selectorLabels\" . | nindent 8 }}\n {{- toYaml .Values.%s.extraPodLabels | nindent 8 }}", appMeta.ChartName(), nameCamel) + podLabels += fmt.Sprintf("\n {{- include \"%s.selectorLabels\" . | nindent 8 }}\n {{- if .Values.%s.podLabels }}\n {{- toYaml .Values.%s.podLabels | nindent 8 }}\n {{- end }}", appMeta.ChartName(), nameCamel, nameCamel) err = unstructured.SetNestedField(values, make(map[string]interface{}), nameCamel, "podLabels") if err != nil { return true, nil, err @@ -129,7 +129,6 @@ func (d deployment) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstr podAnnotations = "\n" + podAnnotations } - nameCamel := strcase.ToLowerCamel(name) specMap, podValues, err := pod.ProcessSpec(nameCamel, appMeta, depl.Spec.Template.Spec, 0) if err != nil { return true, nil, err From 6fc48906dc68da0add3e1d97b0aefdcf74cefdd6 Mon Sep 17 00:00:00 2001 From: hfuss Date: Wed, 29 Apr 2026 11:54:12 -0400 Subject: [PATCH 3/4] feat: expose podAnnotations on Deployment as values-driven field Adds podAnnotations support for Deployment resources. Static annotations from the source manifest are preserved, with a values-driven merge block appended so users can inject additional annotations at install/upgrade time without re-generating the chart. Also seeds podAnnotations: {} in values.yaml by default, consistent with the existing podLabels field. Closes #119 Made-with: Cursor --- pkg/processor/deployment/deployment.go | 12 +- pkg/processor/deployment/deployment_test.go | 121 ++++++++++++++++++++ 2 files changed, 129 insertions(+), 4 deletions(-) diff --git a/pkg/processor/deployment/deployment.go b/pkg/processor/deployment/deployment.go index ce1f54aa..3f3caae3 100644 --- a/pkg/processor/deployment/deployment.go +++ b/pkg/processor/deployment/deployment.go @@ -119,14 +119,18 @@ func (d deployment) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstr return true, nil, err } - podAnnotations := "" + podAnnotations := "\n annotations:" if len(depl.Spec.Template.ObjectMeta.Annotations) != 0 { - podAnnotations, err = yamlformat.Marshal(map[string]interface{}{"annotations": depl.Spec.Template.ObjectMeta.Annotations}, 6) + staticAnnotations, err := yamlformat.Marshal(depl.Spec.Template.ObjectMeta.Annotations, 8) if err != nil { return true, nil, err } - - podAnnotations = "\n" + podAnnotations + podAnnotations += "\n" + staticAnnotations + } + podAnnotations += fmt.Sprintf("\n {{- with .Values.%s.podAnnotations }}\n {{- toYaml . | nindent 8 }}\n {{- end }}", nameCamel) + err = unstructured.SetNestedField(values, make(map[string]interface{}), nameCamel, "podAnnotations") + if err != nil { + return true, nil, err } specMap, podValues, err := pod.ProcessSpec(nameCamel, appMeta, depl.Spec.Template.Spec, 0) diff --git a/pkg/processor/deployment/deployment_test.go b/pkg/processor/deployment/deployment_test.go index 07a2046f..2396f5d9 100644 --- a/pkg/processor/deployment/deployment_test.go +++ b/pkg/processor/deployment/deployment_test.go @@ -1,6 +1,7 @@ package deployment import ( + "bytes" "testing" "github.com/arttor/helmify/pkg/metadata" @@ -137,6 +138,126 @@ func Test_deployment_Process(t *testing.T) { }) } +const ( + // strDeplNoAnnotations has no pod template annotations — tests that podAnnotations is + // still seeded in values and the values-driven block is present in the template. + strDeplNoAnnotations = `apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + control-plane: controller-manager + name: my-operator-controller-manager + namespace: my-operator-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - name: manager + image: controller:latest +` + // strDeplWithAnnotations has static pod template annotations — tests that static + // annotations are preserved and the values-driven block is appended after them. + strDeplWithAnnotations = `apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + control-plane: controller-manager + name: my-operator-controller-manager + namespace: my-operator-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + labels: + control-plane: controller-manager + annotations: + kubectl.kubernetes.io/default-container: manager + spec: + containers: + - name: manager + image: controller:latest +` +) + +func Test_deployment_podAnnotations(t *testing.T) { + var testInstance deployment + + t.Run("no static annotations - values seeded and template block present", func(t *testing.T) { + obj := internal.GenerateObj(strDeplNoAnnotations) + processed, tmpl, err := testInstance.Process(&metadata.Service{}, obj) + assert.NoError(t, err) + assert.True(t, processed) + + // podAnnotations should be seeded as empty map in values + // the deployment name "my-operator-controller-manager" trims to "controller-manager" + // which becomes "controllerManager" in lowerCamel + vals := tmpl.Values() + controllerManager, ok := vals["myOperatorControllerManager"].(map[string]interface{}) + assert.True(t, ok, "expected myOperatorControllerManager key in values") + podAnnotations, ok := controllerManager["podAnnotations"] + assert.True(t, ok, "expected podAnnotations key in values") + assert.Equal(t, map[string]interface{}{}, podAnnotations) + + // Template output must contain the values-driven annotations block + var buf bytes.Buffer + assert.NoError(t, tmpl.Write(&buf)) + output := buf.String() + assert.Contains(t, output, "{{- with .Values.myOperatorControllerManager.podAnnotations }}") + assert.Contains(t, output, "{{- toYaml . | nindent 8 }}") + assert.Contains(t, output, "annotations:") + }) + + t.Run("static annotations preserved and values block appended", func(t *testing.T) { + obj := internal.GenerateObj(strDeplWithAnnotations) + processed, tmpl, err := testInstance.Process(&metadata.Service{}, obj) + assert.NoError(t, err) + assert.True(t, processed) + + var buf bytes.Buffer + assert.NoError(t, tmpl.Write(&buf)) + output := buf.String() + + // Static annotation must be in the output + assert.Contains(t, output, "kubectl.kubernetes.io/default-container: manager") + // Values-driven block must also be present + assert.Contains(t, output, "{{- with .Values.myOperatorControllerManager.podAnnotations }}") + }) +} + +func Test_deployment_podLabels(t *testing.T) { + var testInstance deployment + + t.Run("podLabels seeded in values and template block present", func(t *testing.T) { + obj := internal.GenerateObj(strDeplNoAnnotations) + processed, tmpl, err := testInstance.Process(&metadata.Service{}, obj) + assert.NoError(t, err) + assert.True(t, processed) + + vals := tmpl.Values() + controllerManager, ok := vals["myOperatorControllerManager"].(map[string]interface{}) + assert.True(t, ok, "expected myOperatorControllerManager key in values") + podLabels, ok := controllerManager["podLabels"] + assert.True(t, ok, "expected podLabels key in values") + assert.Equal(t, map[string]interface{}{}, podLabels) + + var buf bytes.Buffer + assert.NoError(t, tmpl.Write(&buf)) + output := buf.String() + assert.Contains(t, output, "{{- if .Values.myOperatorControllerManager.podLabels }}") + assert.Contains(t, output, "{{- toYaml .Values.myOperatorControllerManager.podLabels | nindent 8 }}") + }) +} + var singleQuotesTest = []struct { input string expected string From 515bc6e3419283f42e1f2001e88f4a83a09f3e9e Mon Sep 17 00:00:00 2001 From: hfuss Date: Wed, 29 Apr 2026 14:09:27 -0400 Subject: [PATCH 4/4] update examples Signed-off-by: hfuss --- examples/app/templates/deployment.yaml | 7 +++++++ examples/app/values.yaml | 2 ++ examples/operator/templates/deployment.yaml | 7 +++++++ examples/operator/values.yaml | 2 ++ 4 files changed, 18 insertions(+) diff --git a/examples/app/templates/deployment.yaml b/examples/app/templates/deployment.yaml index e591c525..59fc426f 100644 --- a/examples/app/templates/deployment.yaml +++ b/examples/app/templates/deployment.yaml @@ -17,6 +17,13 @@ spec: labels: app: myapp {{- include "app.selectorLabels" . | nindent 8 }} + {{- if .Values.myapp.podLabels }} + {{- toYaml .Values.myapp.podLabels | nindent 8 }} + {{- end }} + annotations: + {{- with .Values.myapp.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} spec: affinity: {{- toYaml .Values.myapp.affinity | nindent 8 }} containers: diff --git a/examples/app/values.yaml b/examples/app/values.yaml index 3c1edaed..7c3070f8 100644 --- a/examples/app/values.yaml +++ b/examples/app/values.yaml @@ -95,6 +95,8 @@ myapp: nodeSelector: region: east type: user-node + podAnnotations: {} + podLabels: {} podSecurityContext: fsGroup: 20000 runAsNonRoot: true diff --git a/examples/operator/templates/deployment.yaml b/examples/operator/templates/deployment.yaml index 5f2fc173..2aecf599 100644 --- a/examples/operator/templates/deployment.yaml +++ b/examples/operator/templates/deployment.yaml @@ -23,6 +23,13 @@ spec: labels: control-plane: controller-manager {{- include "operator.selectorLabels" . | nindent 8 }} + {{- if .Values.controllerManager.podLabels }} + {{- toYaml .Values.controllerManager.podLabels | nindent 8 }} + {{- end }} + annotations: + {{- with .Values.controllerManager.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} spec: containers: - args: {{- toYaml .Values.controllerManager.kubeRbacProxy.args | nindent 8 }} diff --git a/examples/operator/values.yaml b/examples/operator/values.yaml index 495b1641..41cebfd3 100644 --- a/examples/operator/values.yaml +++ b/examples/operator/values.yaml @@ -43,6 +43,8 @@ controllerManager: nodeSelector: region: east type: user-node + podAnnotations: {} + podLabels: {} podSecurityContext: runAsNonRoot: true replicas: 1