From 874bb32d5996f3278682dc8f850ae30f12939494 Mon Sep 17 00:00:00 2001 From: Nicolas Dieumegarde Date: Wed, 12 Nov 2025 09:44:26 +0100 Subject: [PATCH 1/3] fix: #181 Service processor missing IP family configuration support - Add ipFamilyPolicy and ipFamilies fields to service templates - Add parseIPFamily function to handle IP family configuration - Update service template to include IP family spec between selector and ports - Add test coverage for IP family functionality - Update example charts to demonstrate the feature - Add IPv6 service example to test data This allows helmify to preserve IP family configuration when converting Kubernetes Service manifests that use IPv6-only or dual-stack networking. --- .../app/templates/myapp-ipv6-service.yaml | 21 ++++++++++ examples/app/values.yaml | 9 ++++ pkg/processor/service/service.go | 40 +++++++++++++++++- pkg/processor/service/service_test.go | 42 ++++++++++++++++++- test_data/sample-app.yaml | 19 +++++++++ 5 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 examples/app/templates/myapp-ipv6-service.yaml diff --git a/examples/app/templates/myapp-ipv6-service.yaml b/examples/app/templates/myapp-ipv6-service.yaml new file mode 100644 index 00000000..c49d2b45 --- /dev/null +++ b/examples/app/templates/myapp-ipv6-service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "app.fullname" . }}-myapp-ipv6-service + labels: + app: myapp + {{- include "app.labels" . | nindent 4 }} +spec: + type: {{ .Values.myappIpv6Service.type }} + selector: + app: myapp + {{- include "app.selectorLabels" . | nindent 4 }} + {{- if .Values.myappIpv6Service.ipFamilyPolicy }} + ipFamilyPolicy: {{ .Values.myappIpv6Service.ipFamilyPolicy }} + {{- end }} + {{- if .Values.myappIpv6Service.ipFamilies }} + ipFamilies: + {{- .Values.myappIpv6Service.ipFamilies | toYaml | nindent 2 }} + {{- end }} + ports: + {{- .Values.myappIpv6Service.ports | toYaml | nindent 2 }} diff --git a/examples/app/values.yaml b/examples/app/values.yaml index 0023b067..26696d0e 100644 --- a/examples/app/values.yaml +++ b/examples/app/values.yaml @@ -103,6 +103,15 @@ myapp: revisionHistoryLimit: 5 tolerations: [] topologySpreadConstraints: [] +myappIpv6Service: + ipFamilies: + - IPv6 + ipFamilyPolicy: PreferDualStack + ports: + - name: https + port: 8443 + targetPort: https + type: ClusterIP myappLbService: loadBalancerSourceRanges: - 10.0.0.0/8 diff --git a/pkg/processor/service/service.go b/pkg/processor/service/service.go index 89ad6404..0cc77354 100644 --- a/pkg/processor/service/service.go +++ b/pkg/processor/service/service.go @@ -25,7 +25,7 @@ spec: type: {{ .Values.%[1]s.type }} selector: %[2]s - {{- include "%[3]s.selectorLabels" . | nindent 4 }} + {{- include "%[3]s.selectorLabels" . | nindent 4 }}%[4]s ports: {{- .Values.%[1]s.ports | toYaml | nindent 2 }}` ) @@ -36,6 +36,17 @@ const ( {{- .Values.%[1]s.loadBalancerSourceRanges | toYaml | nindent 2 }}` ) +const ( + ipFamilyTempSpec = ` + {{- if .Values.%[1]s.ipFamilyPolicy }} + ipFamilyPolicy: {{ .Values.%[1]s.ipFamilyPolicy }} + {{- end }} + {{- if .Values.%[1]s.ipFamilies }} + ipFamilies: + {{- .Values.%[1]s.ipFamilies | toYaml | nindent 2 }} + {{- end }}` +) + var svcGVC = schema.GroupVersionKind{ Group: "", Version: "v1", @@ -102,7 +113,9 @@ func (r svc) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured } _ = unstructured.SetNestedSlice(values, ports, shortNameCamel, "ports") - res := meta + fmt.Sprintf(svcTempSpec, shortNameCamel, selector, appMeta.ChartName()) + + ipFamilySpec := parseIPFamily(values, service, shortNameCamel) + res := meta + fmt.Sprintf(svcTempSpec, shortNameCamel, selector, appMeta.ChartName(), ipFamilySpec) res += parseLoadBalancerSourceRanges(values, service, shortNameCamel) @@ -116,6 +129,29 @@ func (r svc) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured }, nil } +func parseIPFamily(values helmify.Values, service corev1.Service, shortNameCamel string) string { + hasIPFamilyPolicy := service.Spec.IPFamilyPolicy != nil + hasIPFamilies := len(service.Spec.IPFamilies) > 0 + + if !hasIPFamilyPolicy && !hasIPFamilies { + return "" + } + + if hasIPFamilyPolicy { + _ = unstructured.SetNestedField(values, string(*service.Spec.IPFamilyPolicy), shortNameCamel, "ipFamilyPolicy") + } + + if hasIPFamilies { + ipFamilies := make([]interface{}, len(service.Spec.IPFamilies)) + for i, fam := range service.Spec.IPFamilies { + ipFamilies[i] = string(fam) + } + _ = unstructured.SetNestedSlice(values, ipFamilies, shortNameCamel, "ipFamilies") + } + + return fmt.Sprintf(ipFamilyTempSpec, shortNameCamel) +} + func parseLoadBalancerSourceRanges(values helmify.Values, service corev1.Service, shortNameCamel string) string { if len(service.Spec.LoadBalancerSourceRanges) < 1 { return "" diff --git a/pkg/processor/service/service_test.go b/pkg/processor/service/service_test.go index fd310f4f..134e6c91 100644 --- a/pkg/processor/service/service_test.go +++ b/pkg/processor/service/service_test.go @@ -3,10 +3,10 @@ package service import ( "testing" - "github.com/arttor/helmify/pkg/metadata" - "github.com/arttor/helmify/internal" + "github.com/arttor/helmify/pkg/metadata" "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) const svcYaml = `apiVersion: v1 @@ -24,6 +24,25 @@ spec: selector: control-plane: controller-manager` +const svcWithIPFamilyYaml = `apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + name: my-operator-controller-manager-metrics-service + namespace: my-operator-system +spec: + ipFamilyPolicy: PreferDualStack + ipFamilies: + - IPv4 + - IPv6 + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager` + func Test_svc_Process(t *testing.T) { var testInstance svc @@ -39,4 +58,23 @@ func Test_svc_Process(t *testing.T) { assert.NoError(t, err) assert.Equal(t, false, processed) }) + + t.Run("processed with IP family", func(t *testing.T) { + obj := internal.GenerateObj(svcWithIPFamilyYaml) + processed, template, err := testInstance.Process(&metadata.Service{}, obj) + assert.NoError(t, err) + assert.Equal(t, true, processed) + assert.NotNil(t, template) + + values := template.Values() + ipFamilyPolicy, found, err := unstructured.NestedString(values, "myOperatorControllerManagerMetricsService", "ipFamilyPolicy") + assert.NoError(t, err) + assert.True(t, found) + assert.Equal(t, "PreferDualStack", ipFamilyPolicy) + + ipFamilies, found, err := unstructured.NestedSlice(values, "myOperatorControllerManagerMetricsService", "ipFamilies") + assert.NoError(t, err) + assert.True(t, found) + assert.Len(t, ipFamilies, 2) + }) } diff --git a/test_data/sample-app.yaml b/test_data/sample-app.yaml index 4e754939..a2c4aecd 100644 --- a/test_data/sample-app.yaml +++ b/test_data/sample-app.yaml @@ -156,6 +156,25 @@ spec: loadBalancerSourceRanges: - 10.0.0.0/8 --- +apiVersion: v1 +kind: Service +metadata: + labels: + app: myapp + name: myapp-ipv6-service + namespace: my-ns +spec: + ipFamilyPolicy: PreferDualStack + ipFamilies: + - IPv6 + ports: + - name: https + port: 8443 + targetPort: https + selector: + app: myapp +--- +--- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: From cbf9c8ccb6fbfe30f44e3963b82b606fb8fa2e6a Mon Sep 17 00:00:00 2001 From: Nicolas Dieumegarde Date: Thu, 13 Nov 2025 20:05:49 +0100 Subject: [PATCH 2/3] fix: force IPv4 when using ipFamilyPolicy --- .../app/templates/myapp-ipfamily-service.yaml | 21 +++++++++++++++++++ .../app/templates/myapp-ipv6-service.yaml | 21 ------------------- examples/app/values.yaml | 6 +++--- test_data/sample-app.yaml | 7 +++---- 4 files changed, 27 insertions(+), 28 deletions(-) create mode 100644 examples/app/templates/myapp-ipfamily-service.yaml delete mode 100644 examples/app/templates/myapp-ipv6-service.yaml diff --git a/examples/app/templates/myapp-ipfamily-service.yaml b/examples/app/templates/myapp-ipfamily-service.yaml new file mode 100644 index 00000000..fbfc76c6 --- /dev/null +++ b/examples/app/templates/myapp-ipfamily-service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "app.fullname" . }}-myapp-ipfamily-service + labels: + app: myapp + {{- include "app.labels" . | nindent 4 }} +spec: + type: {{ .Values.myappIpfamilyService.type }} + selector: + app: myapp + {{- include "app.selectorLabels" . | nindent 4 }} + {{- if .Values.myappIpfamilyService.ipFamilyPolicy }} + ipFamilyPolicy: {{ .Values.myappIpfamilyService.ipFamilyPolicy }} + {{- end }} + {{- if .Values.myappIpfamilyService.ipFamilies }} + ipFamilies: + {{- .Values.myappIpfamilyService.ipFamilies | toYaml | nindent 2 }} + {{- end }} + ports: + {{- .Values.myappIpfamilyService.ports | toYaml | nindent 2 }} diff --git a/examples/app/templates/myapp-ipv6-service.yaml b/examples/app/templates/myapp-ipv6-service.yaml deleted file mode 100644 index c49d2b45..00000000 --- a/examples/app/templates/myapp-ipv6-service.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "app.fullname" . }}-myapp-ipv6-service - labels: - app: myapp - {{- include "app.labels" . | nindent 4 }} -spec: - type: {{ .Values.myappIpv6Service.type }} - selector: - app: myapp - {{- include "app.selectorLabels" . | nindent 4 }} - {{- if .Values.myappIpv6Service.ipFamilyPolicy }} - ipFamilyPolicy: {{ .Values.myappIpv6Service.ipFamilyPolicy }} - {{- end }} - {{- if .Values.myappIpv6Service.ipFamilies }} - ipFamilies: - {{- .Values.myappIpv6Service.ipFamilies | toYaml | nindent 2 }} - {{- end }} - ports: - {{- .Values.myappIpv6Service.ports | toYaml | nindent 2 }} diff --git a/examples/app/values.yaml b/examples/app/values.yaml index 26696d0e..59fd62a2 100644 --- a/examples/app/values.yaml +++ b/examples/app/values.yaml @@ -103,10 +103,10 @@ myapp: revisionHistoryLimit: 5 tolerations: [] topologySpreadConstraints: [] -myappIpv6Service: +myappIpfamilyService: ipFamilies: - - IPv6 - ipFamilyPolicy: PreferDualStack + - IPv4 + ipFamilyPolicy: PreferSingleStack ports: - name: https port: 8443 diff --git a/test_data/sample-app.yaml b/test_data/sample-app.yaml index a2c4aecd..3b74f161 100644 --- a/test_data/sample-app.yaml +++ b/test_data/sample-app.yaml @@ -161,12 +161,12 @@ kind: Service metadata: labels: app: myapp - name: myapp-ipv6-service + name: myapp-ipfamily-service namespace: my-ns spec: - ipFamilyPolicy: PreferDualStack + ipFamilyPolicy: PreferSingleStack ipFamilies: - - IPv6 + - IPv4 ports: - name: https port: 8443 @@ -174,7 +174,6 @@ spec: selector: app: myapp --- ---- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: From 6aa115f21ac1672a88362c914201a27da20937b5 Mon Sep 17 00:00:00 2001 From: Nicolas Dieumegarde Date: Fri, 14 Nov 2025 22:01:24 +0100 Subject: [PATCH 3/3] fix: service ipFamilyPolicy due to unsuported value --- examples/app/values.yaml | 2 +- test_data/sample-app.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/app/values.yaml b/examples/app/values.yaml index 59fd62a2..54f03b4b 100644 --- a/examples/app/values.yaml +++ b/examples/app/values.yaml @@ -106,7 +106,7 @@ myapp: myappIpfamilyService: ipFamilies: - IPv4 - ipFamilyPolicy: PreferSingleStack + ipFamilyPolicy: SingleStack ports: - name: https port: 8443 diff --git a/test_data/sample-app.yaml b/test_data/sample-app.yaml index 3b74f161..e3e13197 100644 --- a/test_data/sample-app.yaml +++ b/test_data/sample-app.yaml @@ -164,7 +164,7 @@ metadata: name: myapp-ipfamily-service namespace: my-ns spec: - ipFamilyPolicy: PreferSingleStack + ipFamilyPolicy: SingleStack ipFamilies: - IPv4 ports: