From 67756a71e273a95b2ae4d89f729fc37134b2c858 Mon Sep 17 00:00:00 2001 From: AdamMagued Date: Fri, 2 Oct 2026 01:23:03 +0000 Subject: [PATCH] feat(daemonset): preserve updateStrategy in output template and values --- pkg/processor/daemonset/daemonset.go | 70 +++++++++++++ pkg/processor/daemonset/daemonset_test.go | 114 +++++++++++++++++++++- 2 files changed, 183 insertions(+), 1 deletion(-) diff --git a/pkg/processor/daemonset/daemonset.go b/pkg/processor/daemonset/daemonset.go index 7caac534..469323a7 100644 --- a/pkg/processor/daemonset/daemonset.go +++ b/pkg/processor/daemonset/daemonset.go @@ -16,6 +16,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/intstr" ) var daemonsetGVC = schema.GroupVersionKind{ @@ -27,6 +28,9 @@ var daemonsetGVC = schema.GroupVersionKind{ var daemonsetTempl, _ = template.New("daemonset").Parse( `{{- .Meta }} spec: +{{- if .UpdateStrategy }} +{{ .UpdateStrategy }} +{{- end }} selector: {{ .Selector }} template: @@ -66,6 +70,10 @@ func (d daemonset) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstru values := helmify.Values{} name := appMeta.TrimName(obj.GetName()) + updateStrategy, err := processUpdateStrategy(name, &dae, &values) + if err != nil { + return true, nil, err + } matchLabels, err := yamlformat.Marshal(map[string]interface{}{"matchLabels": dae.Spec.Selector.MatchLabels}, 0) if err != nil { @@ -118,12 +126,14 @@ func (d daemonset) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstru values: values, data: struct { Meta string + UpdateStrategy string Selector string PodLabels string PodAnnotations string Spec string }{ Meta: meta, + UpdateStrategy: updateStrategy, Selector: selector, PodLabels: podLabels, PodAnnotations: podAnnotations, @@ -132,9 +142,69 @@ func (d daemonset) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstru }, nil } +func processUpdateStrategy(name string, dae *appsv1.DaemonSet, values *helmify.Values) (string, error) { + if dae.Spec.UpdateStrategy.Type == "" && dae.Spec.UpdateStrategy.RollingUpdate == nil { + return "", nil + } + if dae.Spec.UpdateStrategy.Type == "" { + dae.Spec.UpdateStrategy.Type = appsv1.RollingUpdateDaemonSetStrategyType + } + allowedStrategyTypes := map[appsv1.DaemonSetUpdateStrategyType]bool{ + appsv1.RollingUpdateDaemonSetStrategyType: true, + appsv1.OnDeleteDaemonSetStrategyType: true, + } + if !allowedStrategyTypes[dae.Spec.UpdateStrategy.Type] { + return "", fmt.Errorf("invalid daemonset update strategy type: %s", dae.Spec.UpdateStrategy.Type) + } + strategyTypeTpl, err := values.Add(string(dae.Spec.UpdateStrategy.Type), name, "updateStrategy", "type") + if err != nil { + return "", err + } + strategyMap := map[string]interface{}{ + "type": strategyTypeTpl, + } + if dae.Spec.UpdateStrategy.Type == appsv1.RollingUpdateDaemonSetStrategyType { + if rollingUpdate := dae.Spec.UpdateStrategy.RollingUpdate; rollingUpdate != nil { + rollingUpdateMap := map[string]interface{}{} + setRollingUpdateField := func(value *intstr.IntOrString, fieldName string) error { + var tpl string + var err error + if value.Type == intstr.Int { + tpl, err = values.Add(value.IntValue(), name, "updateStrategy", "rollingUpdate", fieldName) + } else { + tpl, err = values.Add(value.String(), name, "updateStrategy", "rollingUpdate", fieldName) + } + if err != nil { + return err + } + rollingUpdateMap[fieldName] = tpl + return nil + } + if rollingUpdate.MaxSurge != nil { + if err := setRollingUpdateField(rollingUpdate.MaxSurge, "maxSurge"); err != nil { + return "", err + } + } + if rollingUpdate.MaxUnavailable != nil { + if err := setRollingUpdateField(rollingUpdate.MaxUnavailable, "maxUnavailable"); err != nil { + return "", err + } + } + strategyMap["rollingUpdate"] = rollingUpdateMap + } + } + strategy, err := yamlformat.Marshal(map[string]interface{}{"updateStrategy": strategyMap}, 2) + if err != nil { + return "", err + } + strategy = strings.ReplaceAll(strategy, "'", "") + return strategy, nil +} + type result struct { data struct { Meta string + UpdateStrategy string Selector string PodLabels string PodAnnotations string diff --git a/pkg/processor/daemonset/daemonset_test.go b/pkg/processor/daemonset/daemonset_test.go index 28abe1f3..0c90b74d 100644 --- a/pkg/processor/daemonset/daemonset_test.go +++ b/pkg/processor/daemonset/daemonset_test.go @@ -1,8 +1,10 @@ package daemonset import ( + "bytes" "testing" + "github.com/arttor/helmify/pkg/helmify" "github.com/arttor/helmify/pkg/metadata" "github.com/arttor/helmify/internal" @@ -55,6 +57,53 @@ spec: - name: varlibdockercontainers hostPath: path: /var/lib/docker/containers +` + strDaeWithRollingUpdate = `apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: fluentd-elasticsearch + namespace: kube-system + labels: + k8s-app: fluentd-logging +spec: + updateStrategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 20% + maxSurge: 1 + selector: + matchLabels: + name: fluentd-elasticsearch + template: + metadata: + labels: + name: fluentd-elasticsearch + spec: + containers: + - name: fluentd-elasticsearch + image: quay.io/fluentd_elasticsearch/fluentd:v2.5.2 +` + strDaeWithOnDelete = `apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: fluentd-elasticsearch + namespace: kube-system + labels: + k8s-app: fluentd-logging +spec: + updateStrategy: + type: OnDelete + selector: + matchLabels: + name: fluentd-elasticsearch + template: + metadata: + labels: + name: fluentd-elasticsearch + spec: + containers: + - name: fluentd-elasticsearch + image: quay.io/fluentd_elasticsearch/fluentd:v2.5.2 ` ) @@ -63,9 +112,72 @@ func Test_daemonset_Process(t *testing.T) { t.Run("processed", func(t *testing.T) { obj := internal.GenerateObj(strDepl) - processed, _, err := testInstance.Process(&metadata.Service{}, obj) + processed, res, err := testInstance.Process(&metadata.Service{}, obj) assert.NoError(t, err) assert.Equal(t, true, processed) + + var buf bytes.Buffer + err = res.Write(&buf) + assert.NoError(t, err) + assert.NotContains(t, buf.String(), "updateStrategy") + }) + t.Run("processed_with_rolling_update_strategy", func(t *testing.T) { + obj := internal.GenerateObj(strDaeWithRollingUpdate) + processed, res, err := testInstance.Process(&metadata.Service{}, obj) + assert.NoError(t, err) + assert.Equal(t, true, processed) + + var buf bytes.Buffer + err = res.Write(&buf) + assert.NoError(t, err) + + expectedTemplate := `spec: + updateStrategy: + rollingUpdate: + maxSurge: {{ .Values.fluentdElasticsearch.updateStrategy.rollingUpdate.maxSurge + }} + maxUnavailable: {{ .Values.fluentdElasticsearch.updateStrategy.rollingUpdate.maxUnavailable + | quote }} + type: {{ .Values.fluentdElasticsearch.updateStrategy.type | quote }} + selector:` + assert.Contains(t, buf.String(), expectedTemplate) + + expectedValues := helmify.Values{ + "fluentdElasticsearch": map[string]interface{}{ + "updateStrategy": map[string]interface{}{ + "type": "RollingUpdate", + "rollingUpdate": map[string]interface{}{ + "maxUnavailable": "20%", + "maxSurge": int64(1), + }, + }, + }, + } + values := res.Values() + assert.Equal(t, "RollingUpdate", values["fluentdElasticsearch"].(map[string]interface{})["updateStrategy"].(map[string]interface{})["type"]) + ru := values["fluentdElasticsearch"].(map[string]interface{})["updateStrategy"].(map[string]interface{})["rollingUpdate"].(map[string]interface{}) + assert.Equal(t, "20%", ru["maxUnavailable"]) + assert.Equal(t, int64(1), ru["maxSurge"]) + _ = expectedValues + }) + t.Run("processed_with_on_delete_strategy", func(t *testing.T) { + obj := internal.GenerateObj(strDaeWithOnDelete) + processed, res, err := testInstance.Process(&metadata.Service{}, obj) + assert.NoError(t, err) + assert.Equal(t, true, processed) + + var buf bytes.Buffer + err = res.Write(&buf) + assert.NoError(t, err) + + expectedTemplate := `spec: + updateStrategy: + type: {{ .Values.fluentdElasticsearch.updateStrategy.type | quote }} + selector:` + assert.Contains(t, buf.String(), expectedTemplate) + + values := res.Values() + assert.Equal(t, "OnDelete", values["fluentdElasticsearch"].(map[string]interface{})["updateStrategy"].(map[string]interface{})["type"]) }) t.Run("skipped", func(t *testing.T) { obj := internal.TestNs