From db440bf92cb1053db83522faed36f6d36f986aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Banaszewski?= Date: Thu, 30 Jul 2026 12:31:14 +0000 Subject: [PATCH] fix(helm/ai-gateway): render service nodePort with an explicit if guard Replace the with block around the Service nodePort field with an if guard that references .Values.service.nodePort directly. The with form rebinds the dot inside the block, so a later addition that needs .Values or .Release there would break. Extend the default_values fixture to enable ingress and httproute with only the values each one requires, so the golden file covers every template with the minimum viable configuration. Add a mustNotContain list to the render test cases. Golden files are rewritten wholesale by TestUpdateGoldenFiles, so these assertions pin optional fields and resources that each fixture leaves unset, including the Service nodePort. --- helm/ai-gateway/templates/service.yaml | 4 +- helm/ai-gateway/tests/chart_test.go | 87 +++++++++++++++++++ .../tests/testdata/default_values.golden | 51 +++++++++++ .../tests/testdata/default_values.yaml | 10 +++ 4 files changed, 150 insertions(+), 2 deletions(-) diff --git a/helm/ai-gateway/templates/service.yaml b/helm/ai-gateway/templates/service.yaml index e32cac88de5..4e17d53e739 100644 --- a/helm/ai-gateway/templates/service.yaml +++ b/helm/ai-gateway/templates/service.yaml @@ -29,7 +29,7 @@ spec: port: {{ .Values.service.port }} targetPort: http protocol: TCP - {{- with .Values.service.nodePort }} - nodePort: {{ . }} + {{- if .Values.service.nodePort }} + nodePort: {{ .Values.service.nodePort }} {{- end }} {{- end }} diff --git a/helm/ai-gateway/tests/chart_test.go b/helm/ai-gateway/tests/chart_test.go index 835a22fa51a..363f55d5de6 100644 --- a/helm/ai-gateway/tests/chart_test.go +++ b/helm/ai-gateway/tests/chart_test.go @@ -22,21 +22,85 @@ var testCases = []testCase{ { name: "default_values", fixture: "default_values", + // The fixture enables every optional resource, so this case covers + // rendering of all templates with the minimum required values. + apiVersions: []string{"gateway.networking.k8s.io/v1/HTTPRoute"}, + mustNotContain: []string{ + // Service fields that only render when their value is set. + "nodePort:", + "loadBalancerClass:", + "loadBalancerIP:", + // externalTrafficPolicy only applies to NodePort and LoadBalancer. + "externalTrafficPolicy:", + // Ingress and HTTPRoute render with required fields only. + "ingressClassName:", + "hostnames:", + // No secret is configured, so nothing mounts a Secret or switches + // the probes and listener to TLS. + "secretName:", + "CODER_AI_GATEWAY_KEY_FILE", + "CODER_AI_GATEWAY_TLS_CERT_FILE", + "CODER_CLIENT_TLS_CA_FILE", + "CODER_CLIENT_TLS_CERT_FILE", + "scheme: HTTPS", + // Opt-in workload settings. + "startupProbe:", + "envFrom:", + "imagePullSecrets:", + "priorityClassName:", + "kind: ConfigMap", + }, }, { name: "networking", fixture: "networking", namespace: "ai-gateway-test", apiVersions: []string{"gateway.networking.k8s.io/v1/HTTPRoute"}, + mustNotContain: []string{ + // The Service is a LoadBalancer without an explicit nodePort, so + // Kubernetes allocates the port. + "nodePort:", + "loadBalancerIP:", + // Only the Gateway key Secret is configured. + "ai-gateway-listener", + "coder-client-ca", + "coder-client-tls", + "scheme: HTTPS", + "startupProbe:", + "envFrom:", + }, }, { name: "custom", fixture: "custom", namespace: "ai-gateway-test", + mustNotContain: []string{ + // coder.serviceAccount.disableCreate is true, so the chart uses the + // existing account instead of creating one. + "kind: ServiceAccount", + // Only the default ClusterIP Service renders. + "kind: Ingress", + "kind: HTTPRoute", + "nodePort:", + "loadBalancerClass:", + "loadBalancerIP:", + "externalTrafficPolicy:", + }, }, { name: "nodeport", fixture: "nodeport", + mustNotContain: []string{ + // LoadBalancer-only fields stay absent for a NodePort Service. + "loadBalancerClass:", + "loadBalancerIP:", + "kind: Ingress", + "kind: HTTPRoute", + "ai-gateway-listener", + "coder-client-ca", + "coder-client-tls", + "scheme: HTTPS", + }, }, { name: "missing_key_field", @@ -54,6 +118,20 @@ var testCases = []testCase{ { name: "listener_tls_with_ingress", fixture: "listener_tls_with_ingress", + mustNotContain: []string{ + // Listener TLS does not imply client TLS to coderd. + "coder-client-ca", + "coder-client-tls", + "CODER_CLIENT_TLS_CA_FILE", + "CODER_CLIENT_TLS_CERT_FILE", + // The Ingress renders with required fields only. + "ingressClassName:", + "kind: HTTPRoute", + "nodePort:", + "loadBalancerClass:", + "loadBalancerIP:", + "externalTrafficPolicy:", + }, }, { name: "partial_client_tls", @@ -104,6 +182,12 @@ type testCase struct { namespace string expectedError string apiVersions []string + // mustNotContain holds substrings that must be absent from the rendered + // output. Golden files are rewritten wholesale by TestUpdateGoldenFiles, + // so absence assertions live here to keep an unintended field from being + // baked into the golden file. Each entry names an optional field or + // resource that the fixture leaves unset. + mustNotContain []string } func (tc testCase) valuesFilePath() string { @@ -143,6 +227,9 @@ func TestRenderChart(t *testing.T) { return } require.NoError(t, err, output) + for _, absent := range tc.mustNotContain { + require.NotContains(t, output, absent) + } golden, err := os.ReadFile(tc.goldenFilePath()) require.NoError(t, err) golden = bytes.ReplaceAll(golden, []byte("\r"), nil) diff --git a/helm/ai-gateway/tests/testdata/default_values.golden b/helm/ai-gateway/tests/testdata/default_values.golden index 66be5cd0566..78c3938bfcb 100644 --- a/helm/ai-gateway/tests/testdata/default_values.golden +++ b/helm/ai-gateway/tests/testdata/default_values.golden @@ -137,3 +137,54 @@ spec: serviceAccountName: coder-ai-gateway terminationGracePeriodSeconds: 330 volumes: [] +--- +# Source: coder-ai-gateway/templates/ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: coder-ai-gateway + namespace: default + labels: + helm.sh/chart: coder-ai-gateway-0.1.0 + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm +spec: + rules: + - host: "ai.example.com" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: coder-ai-gateway + port: + number: 80 +--- +# Source: coder-ai-gateway/templates/httproute.yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: coder-ai-gateway + namespace: default + labels: + helm.sh/chart: coder-ai-gateway-0.1.0 + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm +spec: + parentRefs: + - name: shared-gateway + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: coder-ai-gateway + port: 80 diff --git a/helm/ai-gateway/tests/testdata/default_values.yaml b/helm/ai-gateway/tests/testdata/default_values.yaml index 5eb707ca0cd..46be6740cde 100644 --- a/helm/ai-gateway/tests/testdata/default_values.yaml +++ b/helm/ai-gateway/tests/testdata/default_values.yaml @@ -11,3 +11,13 @@ coder: name: ai-gateway-key key: key aigateway: {} +# Enable every optional resource with only the values each one requires, so +# the golden file covers rendering of all templates without opting into +# non-default settings such as service.nodePort. +ingress: + enable: true + host: ai.example.com +httproute: + enable: true + parentRefs: + - name: shared-gateway