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