diff --git a/chart/docs/production-guide.rst b/chart/docs/production-guide.rst index 3b5b97810581d..2c5994bc22d32 100644 --- a/chart/docs/production-guide.rst +++ b/chart/docs/production-guide.rst @@ -406,8 +406,8 @@ the chart only creates the ``HTTPRoute`` and attaches it to the Gateway via ``pa .. code-block:: yaml :caption: values.yaml - httpRoute: - apiServer: + apiServer: + httpRoute: enabled: true parentRefs: - name: main-gateway @@ -416,9 +416,17 @@ the chart only creates the ``HTTPRoute`` and attaches it to the Gateway via ``pa hostnames: - airflow.example.com -For fine-grained routing, supply ``httpRoute.apiServer.rules`` directly — the entry mirrors the +For fine-grained routing, supply ``apiServer.httpRoute.rules`` directly — the entry mirrors the upstream ``HTTPRouteRule`` schema and overrides the default rule generated from ``path`` + ``pathType``. +.. note:: + + ``HTTPRoute`` is an alternative to the API server ``Ingress``, so enable only one of + ``ingress.apiServer`` or ``apiServer.httpRoute`` — enabling both at the same time fails template + rendering. When ``apiServer.httpRoute.enabled`` is ``true``, the chart also verifies (via Helm + ``Capabilities``) that the Gateway API CRDs are installed and fails with a clear message if they + are not. + LoadBalancer Service ^^^^^^^^^^^^^^^^^^^^ diff --git a/chart/templates/api-server/api-server-httproute.yaml b/chart/templates/api-server/api-server-httproute.yaml index c30f26263d58d..5bdb0103c1399 100644 --- a/chart/templates/api-server/api-server-httproute.yaml +++ b/chart/templates/api-server/api-server-httproute.yaml @@ -20,7 +20,16 @@ ################################## ## Airflow API Server HTTPRoute ################################## -{{- if and .Values.apiServer.enabled .Values.httpRoute.apiServer.enabled }} +{{- if and .Values.apiServer.enabled .Values.apiServer.httpRoute.enabled }} +{{- if .Values.ingress.apiServer.enabled }} +{{- fail "`apiServer.httpRoute.enabled` and `ingress.apiServer.enabled` are both set to `true`. HTTPRoute (Gateway API) is an alternative to the API server Ingress; enable only one of them." }} +{{- end }} +{{- if not (.Capabilities.APIVersions.Has "gateway.networking.k8s.io/v1") }} +{{- fail "`apiServer.httpRoute.enabled` is `true` but the Gateway API HTTPRoute CRD (`gateway.networking.k8s.io/v1`) is not installed in the cluster. Install the Gateway API CRDs (https://gateway-api.sigs.k8s.io/guides/#installing-gateway-api) before enabling `apiServer.httpRoute`." }} +{{- end }} +{{- if not .Values.apiServer.httpRoute.parentRefs }} +{{- fail "`apiServer.httpRoute.enabled` is `true` but `apiServer.httpRoute.parentRefs` is empty. An HTTPRoute must reference at least one parent Gateway; set `apiServer.httpRoute.parentRefs`." }} +{{- end }} {{- $fullname := include "airflow.fullname" . }} apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute @@ -32,28 +41,28 @@ metadata: release: {{ .Release.Name }} chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" heritage: {{ .Release.Service }} - {{- if or .Values.labels .Values.apiServer.labels .Values.httpRoute.apiServer.labels }} - {{- mustMerge .Values.httpRoute.apiServer.labels .Values.apiServer.labels .Values.labels | toYaml | nindent 4 }} + {{- if or .Values.labels .Values.apiServer.labels .Values.apiServer.httpRoute.labels }} + {{- mustMerge .Values.apiServer.httpRoute.labels .Values.apiServer.labels .Values.labels | toYaml | nindent 4 }} {{- end }} - {{- with .Values.httpRoute.apiServer.annotations }} + {{- with .Values.apiServer.httpRoute.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: - parentRefs: {{- toYaml .Values.httpRoute.apiServer.parentRefs | nindent 4 }} - {{- with .Values.httpRoute.apiServer.hostnames }} + parentRefs: {{- toYaml .Values.apiServer.httpRoute.parentRefs | nindent 4 }} + {{- with .Values.apiServer.httpRoute.hostnames }} hostnames: {{- range . }} - {{ tpl . $ | quote }} {{- end }} {{- end }} rules: - {{- if .Values.httpRoute.apiServer.rules }} - {{- toYaml .Values.httpRoute.apiServer.rules | nindent 4 }} + {{- if .Values.apiServer.httpRoute.rules }} + {{- toYaml .Values.apiServer.httpRoute.rules | nindent 4 }} {{- else }} - matches: - path: - type: {{ .Values.httpRoute.apiServer.pathType }} - value: {{ .Values.httpRoute.apiServer.path | quote }} + type: {{ .Values.apiServer.httpRoute.pathType }} + value: {{ .Values.apiServer.httpRoute.path | quote }} backendRefs: - name: {{ $fullname }}-api-server port: {{ .Values.ports.apiServer }} diff --git a/chart/templates/api-server/api-server-ingress.yaml b/chart/templates/api-server/api-server-ingress.yaml index 9e530d74cd8ff..53d3b8fc2c038 100644 --- a/chart/templates/api-server/api-server-ingress.yaml +++ b/chart/templates/api-server/api-server-ingress.yaml @@ -21,6 +21,9 @@ ## Airflow API Server Ingress ################################# {{- if and .Values.apiServer.enabled (semverCompare ">=3.0.0" .Values.airflowVersion) (or .Values.ingress.apiServer.enabled .Values.ingress.enabled) }} +{{- if .Values.apiServer.httpRoute.enabled }} +{{- fail "`ingress.apiServer.enabled` and `apiServer.httpRoute.enabled` are both set to `true`. The API server Ingress is an alternative to HTTPRoute (Gateway API); enable only one of them." }} +{{- end }} {{- $fullname := include "airflow.fullname" . }} apiVersion: networking.k8s.io/v1 kind: Ingress diff --git a/chart/values.schema.json b/chart/values.schema.json index 58cafd6ca05c1..ff6e7bdc2a55b 100644 --- a/chart/values.schema.json +++ b/chart/values.schema.json @@ -764,106 +764,6 @@ } } }, - "httpRoute": { - "description": "Kubernetes Gateway API (HTTPRoute) configuration.", - "type": "object", - "x-docsSection": "Ingress", - "additionalProperties": false, - "properties": { - "apiServer": { - "description": "Configuration for the HTTPRoute of the API server.", - "type": "object", - "additionalProperties": false, - "properties": { - "enabled": { - "description": "Enable API server HTTPRoute resource.", - "type": "boolean", - "default": false - }, - "labels": { - "description": "Extra labels for the API server HTTPRoute.", - "type": "object", - "default": {}, - "additionalProperties": { - "type": "string" - } - }, - "annotations": { - "description": "Annotations for the API server HTTPRoute.", - "type": "object", - "default": {}, - "additionalProperties": { - "type": "string" - } - }, - "parentRefs": { - "description": "List of parent Gateway references this HTTPRoute attaches to. Required when enabled.", - "type": "array", - "default": [], - "items": { - "type": "object", - "properties": { - "name": { - "description": "Name of the referenced Gateway.", - "type": "string" - }, - "namespace": { - "description": "Namespace of the referenced Gateway. Defaults to the local namespace.", - "type": "string" - }, - "sectionName": { - "description": "Name of the Gateway listener section this route attaches to.", - "type": "string" - }, - "port": { - "description": "Port of the Gateway listener this route attaches to.", - "type": "integer" - }, - "kind": { - "description": "Kind of the referenced resource. Defaults to `Gateway`.", - "type": "string" - }, - "group": { - "description": "API group of the referenced resource. Defaults to `gateway.networking.k8s.io`.", - "type": "string" - } - }, - "required": [ - "name" - ] - } - }, - "hostnames": { - "description": "Hostnames this HTTPRoute should match (templated). Empty matches all.", - "type": "array", - "default": [], - "items": { - "type": "string" - } - }, - "path": { - "description": "Default routing rule path (used only when `httpRoute.apiServer.rules` is empty).", - "type": "string", - "default": "/" - }, - "pathType": { - "description": "The pathType for the default path: PathPrefix, Exact, or RegularExpression.", - "type": "string", - "default": "PathPrefix" - }, - "rules": { - "description": "Custom routing rules. When set, overrides the default rule generated from `path` + `pathType`. Each entry follows the upstream Gateway API `HTTPRouteRule` schema.", - "type": "array", - "default": [], - "items": { - "type": "object", - "additionalProperties": {} - } - } - } - } - } - }, "networkPolicies": { "description": "Network policy configuration.", "type": "object", @@ -7145,6 +7045,102 @@ "type": "boolean", "default": true }, + "httpRoute": { + "description": "Kubernetes Gateway API (HTTPRoute) configuration for the API server.", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "description": "Enable API server HTTPRoute resource.", + "type": "boolean", + "default": false + }, + "labels": { + "description": "Extra labels for the API server HTTPRoute.", + "type": "object", + "default": {}, + "additionalProperties": { + "type": "string" + } + }, + "annotations": { + "description": "Annotations for the API server HTTPRoute.", + "type": "object", + "default": {}, + "additionalProperties": { + "type": "string" + } + }, + "parentRefs": { + "description": "List of parent Gateway references this HTTPRoute attaches to. Required when enabled.", + "type": [ + "array", + "null" + ], + "default": null, + "minItems": 1, + "items": { + "type": "object", + "properties": { + "name": { + "description": "Name of the referenced Gateway.", + "type": "string" + }, + "namespace": { + "description": "Namespace of the referenced Gateway. Defaults to the local namespace.", + "type": "string" + }, + "sectionName": { + "description": "Name of the Gateway listener section this route attaches to.", + "type": "string" + }, + "port": { + "description": "Port of the Gateway listener this route attaches to.", + "type": "integer" + }, + "kind": { + "description": "Kind of the referenced resource. Defaults to `Gateway`.", + "type": "string" + }, + "group": { + "description": "API group of the referenced resource. Defaults to `gateway.networking.k8s.io`.", + "type": "string" + } + }, + "required": [ + "name" + ] + } + }, + "hostnames": { + "description": "Hostnames this HTTPRoute should match (templated). Empty matches all.", + "type": "array", + "default": [], + "items": { + "type": "string" + } + }, + "path": { + "description": "Default routing rule path (used only when `apiServer.httpRoute.rules` is empty).", + "type": "string", + "default": "/" + }, + "pathType": { + "description": "The pathType for the default path: PathPrefix, Exact, or RegularExpression.", + "type": "string", + "default": "PathPrefix" + }, + "rules": { + "description": "Custom routing rules. When set, overrides the default rule generated from `path` + `pathType`. Each entry follows the upstream Gateway API `HTTPRouteRule` schema.", + "type": "array", + "default": [], + "items": { + "type": "object", + "additionalProperties": {} + } + } + } + }, "configMapAnnotations": { "description": "Extra annotations to apply to the API server configmap.", "type": "object", diff --git a/chart/values.yaml b/chart/values.yaml index 5135433241dcc..ef45f9244cb96 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -155,7 +155,8 @@ ingress: # Configs for the Ingress of the API Server (Airflow 3+) apiServer: - # Enable API Server ingress resource + # Enable API Server ingress resource. + # Mutually exclusive with `apiServer.httpRoute.enabled` (Gateway API); enable only one of them. enabled: false # Annotations for the API Server Ingress @@ -333,50 +334,6 @@ ingress: # The Ingress Class for the PgBouncer Ingress ingressClassName: "" -# Kubernetes Gateway API (HTTPRoute) configuration. -# Requires the Gateway API CRDs to be installed in the cluster -# (https://gateway-api.sigs.k8s.io/guides/#installing-gateway-api). -# The HTTPRoute resources reference an externally managed Gateway via `parentRefs`. -httpRoute: - # Configs for the HTTPRoute of the API Server (Airflow 3+) - apiServer: - # Enable API Server HTTPRoute resource - enabled: false - - # Extra labels for the API Server HTTPRoute - labels: {} - - # Annotations for the API Server HTTPRoute - annotations: {} - - # List of parent Gateway references this HTTPRoute attaches to. Required when enabled. - # See https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.ParentReference - parentRefs: [] - # - name: my-gateway - # namespace: gateway-system - # sectionName: https - - # Hostnames this HTTPRoute should match (templated). Empty matches all. - hostnames: [] - # - "airflow.example.com" - - # Default routing rule path (used only when `httpRoute.apiServer.rules` is empty) - path: "/" - - # The pathType for the default path. PathPrefix | Exact | RegularExpression - pathType: PathPrefix - - # Custom routing rules. When set, overrides the default rule generated from `path` + `pathType`. - # See https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteRule - rules: [] - # - matches: - # - path: - # type: PathPrefix - # value: /api/ - # backendRefs: - # - name: my-release-api-server - # port: 8080 - # Network policy configuration networkPolicies: # Enabled network policies @@ -2315,6 +2272,53 @@ migrateDatabaseJob: apiServer: enabled: true + # Kubernetes Gateway API (HTTPRoute) configuration for the API server. + # Requires the Gateway API CRDs to be installed in the cluster + # (https://gateway-api.sigs.k8s.io/guides/#installing-gateway-api). + # The HTTPRoute references an externally managed Gateway via `parentRefs`. + # This is an alternative to the API server Ingress (`ingress.apiServer`); + # the two are mutually exclusive and enabling both will fail rendering. + httpRoute: + # Enable API Server HTTPRoute resource. + # Mutually exclusive with `ingress.apiServer.enabled`; enable only one of them. + enabled: false + + # Extra labels for the API Server HTTPRoute + labels: {} + + # Annotations for the API Server HTTPRoute + annotations: {} + + # List of parent Gateway references this HTTPRoute attaches to. Required when enabled. + # Defaults to `~` (null) so the schema's `minItems: 1` fails fast on an explicitly + # empty list (`[]`); a still-null value while enabled is caught by the template guard. + # See https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.ParentReference + parentRefs: ~ + # - name: my-gateway + # namespace: gateway-system + # sectionName: https + + # Hostnames this HTTPRoute should match (templated). Empty matches all. + hostnames: [] + # - "airflow.example.com" + + # Default routing rule path (used only when `apiServer.httpRoute.rules` is empty) + path: "/" + + # The pathType for the default path. PathPrefix | Exact | RegularExpression + pathType: PathPrefix + + # Custom routing rules. When set, overrides the default rule generated from `path` + `pathType`. + # See https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteRule + rules: [] + # - matches: + # - path: + # type: PathPrefix + # value: /api/ + # backendRefs: + # - name: my-release-api-server + # port: 8080 + # Number of Airflow API servers in the Deployment. # Omitted from the Deployment, when HPA is enabled. replicas: 1 diff --git a/helm-tests/tests/chart_utils/helm_template_generator.py b/helm-tests/tests/chart_utils/helm_template_generator.py index 91e1213542d3b..7891b1be3c623 100644 --- a/helm-tests/tests/chart_utils/helm_template_generator.py +++ b/helm-tests/tests/chart_utils/helm_template_generator.py @@ -141,6 +141,7 @@ def render_chart( chart_dir=None, kubernetes_version=DEFAULT_KUBERNETES_VERSION, namespace=None, + api_versions=None, ): """ Function that renders a helm chart into dictionaries. For helm chart testing only @@ -164,6 +165,8 @@ def render_chart( "--namespace", namespace, ] + for api_version in api_versions or []: + command.extend(["--api-versions", api_version]) if show_only: for i in show_only: command.extend(["--show-only", i]) diff --git a/helm-tests/tests/helm_tests/apiserver/test_httproute_apiserver.py b/helm-tests/tests/helm_tests/apiserver/test_httproute_apiserver.py index 057cac40d8268..3f0b826d6e63e 100644 --- a/helm-tests/tests/helm_tests/apiserver/test_httproute_apiserver.py +++ b/helm-tests/tests/helm_tests/apiserver/test_httproute_apiserver.py @@ -18,24 +18,38 @@ import jmespath import pytest -from chart_utils.helm_template_generator import render_chart +from chart_utils.helm_template_generator import HelmFailedError, render_chart SHOW_ONLY = ["templates/api-server/api-server-httproute.yaml"] +# The template is gated on the Gateway API CRD being installed +# (`.Capabilities.APIVersions.Has`). Pass it explicitly so `helm template` +# can render the resource offline. +GATEWAY_API_VERSIONS = ["gateway.networking.k8s.io/v1"] + +# A minimal valid `parentRefs`; the HTTPRoute now requires at least one parent +# Gateway reference when enabled, so rendering tests must supply it. +MINIMAL_PARENT_REFS = [{"name": "main-gateway"}] + class TestHTTPRouteAPIServer: """Tests HTTPRoute API Server (Kubernetes Gateway API).""" - def test_should_pass_validation_with_just_httproute_enabled(self): - render_chart( - values={"httpRoute": {"apiServer": {"enabled": True}}}, + def test_should_pass_validation_with_minimal_config(self): + docs = render_chart( + values={"apiServer": {"httpRoute": {"enabled": True, "parentRefs": MINIMAL_PARENT_REFS}}}, show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, ) + assert len(docs) == 1 + assert jmespath.search("kind", docs[0]) == "HTTPRoute" + assert jmespath.search("metadata.name", docs[0]) == "release-name-api-server-httproute" def test_should_set_api_version_and_kind(self): docs = render_chart( - values={"httpRoute": {"apiServer": {"enabled": True}}}, + values={"apiServer": {"httpRoute": {"enabled": True, "parentRefs": MINIMAL_PARENT_REFS}}}, show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, ) assert jmespath.search("apiVersion", docs[0]) == "gateway.networking.k8s.io/v1" assert jmespath.search("kind", docs[0]) == "HTTPRoute" @@ -43,28 +57,40 @@ def test_should_set_api_version_and_kind(self): def test_should_allow_more_than_one_annotation(self): docs = render_chart( values={ - "httpRoute": { - "apiServer": {"enabled": True, "annotations": {"aa": "bb", "cc": "dd"}}, + "apiServer": { + "httpRoute": { + "enabled": True, + "parentRefs": MINIMAL_PARENT_REFS, + "annotations": {"aa": "bb", "cc": "dd"}, + }, }, }, show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, ) assert jmespath.search("metadata.annotations", docs[0]) == {"aa": "bb", "cc": "dd"} def test_should_add_extra_labels(self): docs = render_chart( values={ - "httpRoute": {"apiServer": {"enabled": True, "labels": {"custom": "value"}}}, + "apiServer": { + "httpRoute": { + "enabled": True, + "parentRefs": MINIMAL_PARENT_REFS, + "labels": {"custom": "value"}, + }, + }, }, show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, ) assert jmespath.search('metadata.labels."custom"', docs[0]) == "value" def test_should_pass_parent_refs_through(self): docs = render_chart( values={ - "httpRoute": { - "apiServer": { + "apiServer": { + "httpRoute": { "enabled": True, "parentRefs": [ {"name": "main-gateway", "namespace": "gateway-system"}, @@ -74,21 +100,26 @@ def test_should_pass_parent_refs_through(self): }, }, show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, ) - assert jmespath.search("spec.parentRefs[*].name", docs[0]) == ["main-gateway", "main-gateway"] - assert jmespath.search("spec.parentRefs[1].sectionName", docs[0]) == "https" + assert jmespath.search("spec.parentRefs", docs[0]) == [ + {"name": "main-gateway", "namespace": "gateway-system"}, + {"name": "main-gateway", "namespace": "gateway-system", "sectionName": "https"}, + ] def test_should_set_hostnames(self): docs = render_chart( values={ - "httpRoute": { - "apiServer": { + "apiServer": { + "httpRoute": { "enabled": True, + "parentRefs": MINIMAL_PARENT_REFS, "hostnames": ["airflow.example.com", "airflow2.example.com"], }, }, }, show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, ) assert jmespath.search("spec.hostnames", docs[0]) == [ "airflow.example.com", @@ -99,92 +130,165 @@ def test_hostnames_should_be_templated(self): docs = render_chart( name="airflow", values={ - "httpRoute": { - "apiServer": { + "apiServer": { + "httpRoute": { "enabled": True, + "parentRefs": MINIMAL_PARENT_REFS, "hostnames": ["{{ .Release.Name }}.example.com"], }, }, }, show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, ) assert jmespath.search("spec.hostnames", docs[0]) == ["airflow.example.com"] def test_should_default_to_path_prefix_and_api_server_backend(self): docs = render_chart( name="my-release", - values={"httpRoute": {"apiServer": {"enabled": True}}}, + values={"apiServer": {"httpRoute": {"enabled": True, "parentRefs": MINIMAL_PARENT_REFS}}}, show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, ) - assert jmespath.search("spec.rules[0].matches[0].path.type", docs[0]) == "PathPrefix" - assert jmespath.search("spec.rules[0].matches[0].path.value", docs[0]) == "/" - assert jmespath.search("spec.rules[0].backendRefs[0].name", docs[0]) == "my-release-api-server" - assert jmespath.search("spec.rules[0].backendRefs[0].port", docs[0]) == 8080 + assert jmespath.search("spec.rules", docs[0]) == [ + { + "matches": [{"path": {"type": "PathPrefix", "value": "/"}}], + "backendRefs": [{"name": "my-release-api-server", "port": 8080}], + }, + ] def test_custom_path_and_path_type_should_apply(self): docs = render_chart( values={ - "httpRoute": { - "apiServer": {"enabled": True, "path": "/api", "pathType": "Exact"}, + "apiServer": { + "httpRoute": { + "enabled": True, + "parentRefs": MINIMAL_PARENT_REFS, + "path": "/api", + "pathType": "Exact", + }, }, }, show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, ) assert jmespath.search("spec.rules[0].matches[0].path.type", docs[0]) == "Exact" assert jmespath.search("spec.rules[0].matches[0].path.value", docs[0]) == "/api" def test_custom_rules_override_default_rule(self): - custom_rules = [ + docs = render_chart( + values={ + "apiServer": { + "httpRoute": { + "enabled": True, + "parentRefs": MINIMAL_PARENT_REFS, + "rules": [ + { + "matches": [{"path": {"type": "PathPrefix", "value": "/admin"}}], + "backendRefs": [{"name": "external-admin", "port": 8443}], + }, + ], + }, + }, + }, + show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, + ) + assert jmespath.search("spec.rules", docs[0]) == [ { "matches": [{"path": {"type": "PathPrefix", "value": "/admin"}}], "backendRefs": [{"name": "external-admin", "port": 8443}], }, ] + + def test_httproute_not_created_when_unset(self): docs = render_chart( - values={ - "httpRoute": { - "apiServer": {"enabled": True, "rules": custom_rules}, - }, - }, + values={"apiServer": {"httpRoute": {}}}, + show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, + ) + assert docs == [] + + def test_httproute_not_created_when_disabled(self): + docs = render_chart( + values={"apiServer": {"httpRoute": {"enabled": False}}}, show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, ) - assert jmespath.search("spec.rules[0].matches[0].path.value", docs[0]) == "/admin" - assert jmespath.search("spec.rules[0].backendRefs[0].name", docs[0]) == "external-admin" - assert jmespath.search("spec.rules[0].backendRefs[0].port", docs[0]) == 8443 - - @pytest.mark.parametrize( - ("api_server_value", "expected"), - [ - (None, False), - (False, False), - (True, True), - ], - ) - def test_httproute_created(self, api_server_value, expected): - values = {"httpRoute": {}} - if api_server_value is not None: - values["httpRoute"]["apiServer"] = {"enabled": api_server_value} - docs = render_chart(values=values, show_only=SHOW_ONLY) - assert bool(docs) is expected + assert docs == [] + + def test_httproute_created_when_enabled(self): + docs = render_chart( + values={"apiServer": {"httpRoute": {"enabled": True, "parentRefs": MINIMAL_PARENT_REFS}}}, + show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, + ) + assert len(docs) == 1 def test_should_not_render_when_api_server_disabled(self): docs = render_chart( values={ - "apiServer": {"enabled": False}, - "httpRoute": {"apiServer": {"enabled": True}}, + "apiServer": {"enabled": False, "httpRoute": {"enabled": True}}, }, show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, ) assert docs == [] + def test_should_fail_when_gateway_api_crd_missing(self): + # No api_versions -> the Gateway API CRD is treated as not installed. + with pytest.raises(HelmFailedError) as exc_info: + render_chart( + values={"apiServer": {"httpRoute": {"enabled": True}}}, + show_only=SHOW_ONLY, + ) + assert "Gateway API HTTPRoute CRD" in exc_info.value.stderr.decode() + + def test_should_fail_when_parent_refs_empty(self): + # Gateway API CRD present, but parentRefs left at its `~` default -> the template + # guard catches the still-null value while enabled. + with pytest.raises(HelmFailedError) as exc_info: + render_chart( + values={"apiServer": {"httpRoute": {"enabled": True}}}, + show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, + ) + assert "parentRefs" in exc_info.value.stderr.decode() + + def test_should_fail_schema_when_parent_refs_explicitly_empty(self): + # An explicitly empty `parentRefs: []` violates the schema's `minItems: 1` and + # fails fast during values validation, before any template guard runs. + with pytest.raises(HelmFailedError) as exc_info: + render_chart( + values={"apiServer": {"httpRoute": {"enabled": True, "parentRefs": []}}}, + show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, + ) + assert "parentRefs" in exc_info.value.stderr.decode() + + def test_should_fail_when_ingress_and_httproute_both_enabled(self): + with pytest.raises(HelmFailedError) as exc_info: + render_chart( + values={ + "ingress": {"apiServer": {"enabled": True}}, + "apiServer": {"httpRoute": {"enabled": True}}, + }, + show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, + ) + # Either reciprocal guard (httproute-side or ingress-side) may fire first; + # both share this closing phrase. + assert "enable only one of them" in exc_info.value.stderr.decode() + def test_backend_service_name_with_fullname_override(self): docs = render_chart( values={ "fullnameOverride": "airflow-fullname-override", "useStandardNaming": True, - "httpRoute": {"apiServer": {"enabled": True}}, + "apiServer": {"httpRoute": {"enabled": True, "parentRefs": MINIMAL_PARENT_REFS}}, }, show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, ) assert ( jmespath.search("spec.rules[0].backendRefs[0].name", docs[0]) @@ -195,10 +299,17 @@ def test_should_add_component_specific_labels(self): docs = render_chart( values={ "labels": {"label1": "value1", "label2": "value2"}, - "apiServer": {"labels": {"test_label": "test_label_value"}}, - "httpRoute": {"apiServer": {"enabled": True, "labels": {"route_label": "route_value"}}}, + "apiServer": { + "labels": {"test_label": "test_label_value"}, + "httpRoute": { + "enabled": True, + "parentRefs": MINIMAL_PARENT_REFS, + "labels": {"route_label": "route_value"}, + }, + }, }, show_only=SHOW_ONLY, + api_versions=GATEWAY_API_VERSIONS, ) assert "tier" in jmespath.search("metadata.labels", docs[0]) assert "release" in jmespath.search("metadata.labels", docs[0])