This is an automated email from the ASF dual-hosted git repository.
Miretpl pushed a commit to branch chart/v1-2x-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/chart/v1-2x-test by this push:
new a5caeec970c Add Webserver HTTPRoute support to the Helm chart 1.2x
line (#70425)
a5caeec970c is described below
commit a5caeec970c6cfc53ca2cca4383c034fb001ca88
Author: Aaron Chen <[email protected]>
AuthorDate: Sun Aug 2 05:28:43 2026 +0800
Add Webserver HTTPRoute support to the Helm chart 1.2x line (#70425)
* Add Webserver HTTPRoute support to the Helm chart 1.2x line
* Update helm-tests/tests/helm_tests/webserver/test_httproute_webserver.py
Co-authored-by: Przemysław Mirowski
<[email protected]>
* Update chart/values.yaml
Co-authored-by: Przemysław Mirowski
<[email protected]>
* Update chart/docs/production-guide.rst
Co-authored-by: Przemysław Mirowski
<[email protected]>
* Keep web Ingress label test and add Webserver HTTPRoute case
---
chart/docs/production-guide.rst | 31 +-
chart/templates/webserver/webserver-httproute.yaml | 70 ++++
chart/templates/webserver/webserver-ingress.yaml | 3 +
chart/values.schema.json | 291 ++++++---------
chart/values.yaml | 46 +++
.../airflow_aux/test_basic_helm_chart.py | 42 ++-
.../webserver/test_httproute_webserver.py | 407 +++++++++++++++++++++
7 files changed, 692 insertions(+), 198 deletions(-)
diff --git a/chart/docs/production-guide.rst b/chart/docs/production-guide.rst
index 22f2f88923c..49d12a99946 100644
--- a/chart/docs/production-guide.rst
+++ b/chart/docs/production-guide.rst
@@ -485,7 +485,7 @@ Gateway API (HTTPRoute)
^^^^^^^^^^^^^^^^^^^^^^^
As an alternative to ``Ingress``, the chart can create
-`Kubernetes Gateway API <https://gateway-api.sigs.k8s.io/>`_ ``HTTPRoute``
resources for the API server and Flower.
+`Kubernetes Gateway API <https://gateway-api.sigs.k8s.io/>`_ ``HTTPRoute``
resources for the API server, Flower, and the Webserver.
This requires the Gateway API CRDs to be installed in the cluster and a
``Gateway`` to already exist —
the chart only creates the ``HTTPRoute`` resources and attaches them to the
Gateway via ``parentRefs``.
@@ -515,13 +515,34 @@ the chart only creates the ``HTTPRoute`` resources and
attaches them to the Gate
hostnames:
- flower.example.com
+The Webserver exists only on Airflow 2 (the API server replaces it on Airflow
3), so its ``HTTPRoute``
+is configured in an Airflow 2 deployment:
+
+.. code-block:: yaml
+ :caption: values.yaml (Airflow 2)
+
+ defaultAirflowTag: "2.11.0"
+ airflowVersion: "2.11.0"
+
+ webserver:
+ httpRoute:
+ enabled: true
+ parentRefs:
+ - name: main-gateway
+ namespace: gateway-system
+ sectionName: https
+ hostnames:
+ - airflow.example.com
+
Flower HTTPRoute resources are only created when Flower itself is created, so
``flower.enabled`` must be
``true`` and the executor must include ``CeleryExecutor`` or
``CeleryKubernetesExecutor``.
-For fine-grained routing, supply ``apiServer.httpRoute.rules`` or
``flower.httpRoute.rules`` directly —
-the entry mirrors the upstream ``HTTPRouteRule`` schema and overrides the
default rule generated from
-the corresponding ``apiServer.httpRoute.path`` and
``apiServer.httpRoute.pathType``, or
-``flower.httpRoute.path`` and ``flower.httpRoute.pathType`` values.
+For fine-grained routing, supply ``apiServer.httpRoute.rules``,
``flower.httpRoute.rules``, or
+``webserver.httpRoute.rules`` directly — the entry mirrors the upstream
``HTTPRouteRule`` schema and
+overrides the default rule generated from the corresponding
+``apiServer.httpRoute.path`` and ``apiServer.httpRoute.pathType``,
+``flower.httpRoute.path`` and ``flower.httpRoute.pathType``, or
+``webserver.httpRoute.path`` and ``webserver.httpRoute.pathType`` values.
.. note::
diff --git a/chart/templates/webserver/webserver-httproute.yaml
b/chart/templates/webserver/webserver-httproute.yaml
new file mode 100644
index 00000000000..3b3d38fc65e
--- /dev/null
+++ b/chart/templates/webserver/webserver-httproute.yaml
@@ -0,0 +1,70 @@
+{{/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/}}
+
+##############################
+## Airflow Webserver HTTPRoute
+##############################
+{{- if and .Values.webserver.enabled .Values.webserver.httpRoute.enabled
(semverCompare "<3.0.0" .Values.airflowVersion) }}
+{{- if or .Values.ingress.web.enabled .Values.ingress.enabled }}
+{{- fail "`webserver.httpRoute.enabled` and
`ingress.web.enabled`/`ingress.enabled` are both enabled. HTTPRoute (Gateway
API) is an alternative to the Webserver Ingress; enable only one of them." }}
+{{- end }}
+{{- if not (.Capabilities.APIVersions.Has "gateway.networking.k8s.io/v1") }}
+{{- fail "`webserver.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 `webserver.httpRoute`." }}
+{{- end }}
+{{- if not .Values.webserver.httpRoute.parentRefs }}
+{{- fail "`webserver.httpRoute.enabled` is `true` but
`webserver.httpRoute.parentRefs` is empty. An HTTPRoute must reference at least
one parent Gateway; set `webserver.httpRoute.parentRefs`." }}
+{{- end }}
+{{- $fullname := include "airflow.fullname" . }}
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+ name: {{ $fullname }}-webserver-httproute
+ labels:
+ tier: airflow
+ component: webserver-httproute
+ release: {{ .Release.Name }}
+ chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+ heritage: {{ .Release.Service }}
+ {{- if or .Values.labels .Values.webserver.labels
.Values.webserver.httpRoute.labels }}
+ {{- mustMerge .Values.webserver.httpRoute.labels
.Values.webserver.labels .Values.labels | toYaml | nindent 4 }}
+ {{- end }}
+ {{- with .Values.webserver.httpRoute.annotations }}
+ annotations: {{- toYaml . | nindent 4 }}
+ {{- end }}
+spec:
+ parentRefs: {{- toYaml .Values.webserver.httpRoute.parentRefs | nindent 4 }}
+ {{- with .Values.webserver.httpRoute.hostnames }}
+ hostnames:
+ {{- range . }}
+ - {{ tpl . $ | quote }}
+ {{- end }}
+ {{- end }}
+ rules:
+ {{- if .Values.webserver.httpRoute.rules }}
+ {{- toYaml .Values.webserver.httpRoute.rules | nindent 4 }}
+ {{- else }}
+ - matches:
+ - path:
+ type: {{ .Values.webserver.httpRoute.pathType }}
+ value: {{ .Values.webserver.httpRoute.path | quote }}
+ backendRefs:
+ - name: {{ $fullname }}-webserver
+ port: {{ .Values.ports.airflowUI }}
+ {{- end }}
+{{- end }}
diff --git a/chart/templates/webserver/webserver-ingress.yaml
b/chart/templates/webserver/webserver-ingress.yaml
index 1b9f8d41b4a..7363c6140fc 100644
--- a/chart/templates/webserver/webserver-ingress.yaml
+++ b/chart/templates/webserver/webserver-ingress.yaml
@@ -21,6 +21,9 @@
## Airflow Webserver Ingress
#################################
{{- if and .Values.webserver.enabled (or .Values.ingress.web.enabled
.Values.ingress.enabled) (semverCompare "<3.0.0" .Values.airflowVersion) }}
+{{- if .Values.webserver.httpRoute.enabled }}
+{{- fail "`ingress.web.enabled`/`ingress.enabled` and
`webserver.httpRoute.enabled` are both enabled. The Webserver 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 7bb9cc90d36..092e5abc2d1 100644
--- a/chart/values.schema.json
+++ b/chart/values.schema.json
@@ -7079,100 +7079,8 @@
"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": {}
- }
- }
- }
+ "$ref": "#/definitions/httpRoute",
+ "description": "Kubernetes Gateway API (HTTPRoute)
configuration for the API server."
},
"configMapAnnotations": {
"description": "Extra annotations to apply to the API
server configmap.",
@@ -7948,6 +7856,10 @@
"type": "boolean",
"default": true
},
+ "httpRoute": {
+ "$ref": "#/definitions/httpRoute",
+ "description": "Kubernetes Gateway API (HTTPRoute)
configuration for the Webserver."
+ },
"configMapAnnotations": {
"description": "Extra annotations to apply to the
webserver configmap.",
"type": "object",
@@ -8768,100 +8680,8 @@
"default": false
},
"httpRoute": {
- "description": "Kubernetes Gateway API (HTTPRoute)
configuration for Flower.",
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "enabled": {
- "description": "Enable Flower HTTPRoute resource.",
- "type": "boolean",
- "default": false
- },
- "labels": {
- "description": "Extra labels for the Flower
HTTPRoute.",
- "type": "object",
- "default": {},
- "additionalProperties": {
- "type": "string"
- }
- },
- "annotations": {
- "description": "Annotations for the Flower
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 `flower.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": {}
- }
- }
- }
+ "$ref": "#/definitions/httpRoute",
+ "description": "Kubernetes Gateway API (HTTPRoute)
configuration for Flower."
},
"livenessProbe": {
"description": "Liveness probe configuration.",
@@ -12544,6 +12364,101 @@
}
},
"definitions": {
+ "httpRoute": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "description": "Enable the HTTPRoute resource.",
+ "type": "boolean",
+ "default": false
+ },
+ "labels": {
+ "description": "Extra labels for the HTTPRoute.",
+ "type": "object",
+ "default": {},
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "annotations": {
+ "description": "Annotations for the 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
`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": {}
+ }
+ }
+ }
+ },
"io.k8s.api.apps.v1.DeploymentStrategy": {
"description": "DeploymentStrategy describes how to replace
existing pods with new ones.",
"properties": {
diff --git a/chart/values.yaml b/chart/values.yaml
index 7d848823458..274a8d79b4a 100644
--- a/chart/values.yaml
+++ b/chart/values.yaml
@@ -202,6 +202,7 @@ ingress:
# Configs for the Ingress of the web Service (Airflow <3.0.0)
web:
# Enable web ingress resource
+ # Mutually exclusive with `webserver.httpRoute.enabled` (Gateway API);
enable only one of them.
enabled: false
# Annotations for the web Ingress
@@ -2540,6 +2541,51 @@ apiServer:
webserver:
enabled: true
+ # Kubernetes Gateway API (HTTPRoute) configuration for the Webserver.
+ # 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`.
+ httpRoute:
+ # Enable Webserver HTTPRoute resource.
+ # Mutually exclusive with `ingress.web.enabled` and the legacy
`ingress.enabled`;
+ # enable only one routing mechanism.
+ enabled: false
+
+ # Extra labels for the Webserver HTTPRoute
+ labels: {}
+
+ # Annotations for the Webserver 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 `webserver.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`.
+ # Use this for non-standard Webserver service port layouts.
+ # See
https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteRule
+ rules: []
+ # - matches:
+ # - path:
+ # type: PathPrefix
+ # value: /
+ # backendRefs:
+ # - name: my-webserver-service
+ # port: 8080
+
# Add custom annotations to the webserver ConfigMap
configMapAnnotations: {}
diff --git a/helm-tests/tests/helm_tests/airflow_aux/test_basic_helm_chart.py
b/helm-tests/tests/helm_tests/airflow_aux/test_basic_helm_chart.py
index 22120bc2805..228b2ed0979 100644
--- a/helm-tests/tests/helm_tests/airflow_aux/test_basic_helm_chart.py
+++ b/helm-tests/tests/helm_tests/airflow_aux/test_basic_helm_chart.py
@@ -372,7 +372,7 @@ class TestBaseChartTest:
],
)
@pytest.mark.parametrize(
- ("flower_routing_values", "flower_routing_resource", "api_versions"),
+ ("routing_values", "flower_routing_resource",
"webserver_routing_resource", "api_versions"),
[
pytest.param(
{
@@ -384,8 +384,9 @@ class TestBaseChartTest:
"flower": {"enabled": True},
},
("flower-ingress", "Ingress", "flower-ingress"),
+ ("ingress", "Ingress", "airflow-ingress"),
[],
- id="flower-ingress",
+ id="ingress",
),
pytest.param(
{
@@ -402,17 +403,41 @@ class TestBaseChartTest:
},
},
("flower-httproute", "HTTPRoute", "flower-httproute"),
+ ("ingress", "Ingress", "airflow-ingress"),
["gateway.networking.k8s.io/v1"],
id="flower-httproute",
),
+ pytest.param(
+ {
+ "ingress": {"apiServer": {"enabled": True}},
+ "flower": {
+ "enabled": True,
+ "httpRoute": {
+ "enabled": True,
+ "parentRefs": [{"name": "main-gateway"}],
+ },
+ },
+ "webserver": {
+ "httpRoute": {
+ "enabled": True,
+ "parentRefs": [{"name": "main-gateway"}],
+ },
+ },
+ },
+ ("flower-httproute", "HTTPRoute", "flower-httproute"),
+ ("webserver-httproute", "HTTPRoute", "webserver-httproute"),
+ ["gateway.networking.k8s.io/v1"],
+ id="httproute",
+ ),
],
)
def test_labels_are_valid(
self,
airflow_version,
executor,
- flower_routing_values,
+ routing_values,
flower_routing_resource,
+ webserver_routing_resource,
api_versions,
):
"""Test labels are correctly applied on all objects created by this
chart."""
@@ -445,7 +470,7 @@ class TestBaseChartTest:
{"name": "class1", "value": 10000},
],
}
- values.update(flower_routing_values)
+ values.update(routing_values)
if airflow_version != "default":
values["airflowVersion"] = airflow_version
@@ -457,6 +482,9 @@ class TestBaseChartTest:
}
flower_routing_name, flower_routing_kind, flower_routing_component =
flower_routing_resource
+ webserver_routing_name, webserver_routing_kind,
webserver_routing_component = (
+ webserver_routing_resource
+ )
kind_names_tuples = [
(f"{release_name}-airflow-cleanup", "ServiceAccount",
"airflow-cleanup-pods"),
@@ -535,7 +563,11 @@ class TestBaseChartTest:
(f"{release_name}-webserver", "Service", "webserver"),
(f"{release_name}-webserver-secret-key", "Secret",
"webserver"),
(f"{release_name}-webserver-policy", "NetworkPolicy",
"airflow-webserver-policy"),
- (f"{release_name}-ingress", "Ingress", "airflow-ingress"),
+ (
+ f"{release_name}-{webserver_routing_name}",
+ webserver_routing_kind,
+ webserver_routing_component,
+ ),
]
cleanup_kubernetes_executor_only_objects = {
diff --git a/helm-tests/tests/helm_tests/webserver/test_httproute_webserver.py
b/helm-tests/tests/helm_tests/webserver/test_httproute_webserver.py
new file mode 100644
index 00000000000..597bc05ef4a
--- /dev/null
+++ b/helm-tests/tests/helm_tests/webserver/test_httproute_webserver.py
@@ -0,0 +1,407 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import jmespath
+import pytest
+from chart_utils.helm_template_generator import HelmFailedError, render_chart
+
+SHOW_ONLY = ["templates/webserver/webserver-httproute.yaml"]
+WEBSERVER_INGRESS_SHOW_ONLY = ["templates/webserver/webserver-ingress.yaml"]
+
+GATEWAY_API_VERSIONS = ["gateway.networking.k8s.io/v1"]
+MINIMAL_PARENT_REFS = [{"name": "main-gateway"}]
+# The Webserver only exists on Airflow 2 (`airflowVersion` < 3.0.0); the API
server replaces it on 3.
+AIRFLOW_2 = "2.11.0"
+
+
+class TestHTTPRouteWebserver:
+ """Tests HTTPRoute Webserver (Kubernetes Gateway API)."""
+
+ def test_should_pass_validation_with_minimal_config(self):
+ docs = render_chart(
+ values={
+ "airflowVersion": AIRFLOW_2,
+ "webserver": {
+ "enabled": True,
+ "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-webserver-httproute"
+
+ def test_should_set_api_version_and_kind(self):
+ docs = render_chart(
+ values={
+ "airflowVersion": AIRFLOW_2,
+ "webserver": {
+ "enabled": True,
+ "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"
+
+ def test_should_allow_more_than_one_annotation(self):
+ docs = render_chart(
+ values={
+ "airflowVersion": AIRFLOW_2,
+ "webserver": {
+ "enabled": True,
+ "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={
+ "airflowVersion": AIRFLOW_2,
+ "webserver": {
+ "enabled": True,
+ "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={
+ "airflowVersion": AIRFLOW_2,
+ "webserver": {
+ "enabled": True,
+ "httpRoute": {
+ "enabled": True,
+ "parentRefs": [
+ {"name": "main-gateway", "namespace":
"gateway-system"},
+ {"name": "main-gateway", "namespace":
"gateway-system", "sectionName": "https"},
+ ],
+ },
+ },
+ },
+ show_only=SHOW_ONLY,
+ api_versions=GATEWAY_API_VERSIONS,
+ )
+ 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={
+ "airflowVersion": AIRFLOW_2,
+ "webserver": {
+ "enabled": True,
+ "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",
+ "airflow2.example.com",
+ ]
+
+ def test_hostnames_should_be_templated(self):
+ docs = render_chart(
+ name="airflow",
+ values={
+ "airflowVersion": AIRFLOW_2,
+ "webserver": {
+ "enabled": True,
+ "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_webserver_backend(self):
+ docs = render_chart(
+ name="my-release",
+ values={
+ "airflowVersion": AIRFLOW_2,
+ "webserver": {
+ "enabled": True,
+ "httpRoute": {"enabled": True, "parentRefs":
MINIMAL_PARENT_REFS},
+ },
+ },
+ show_only=SHOW_ONLY,
+ api_versions=GATEWAY_API_VERSIONS,
+ )
+ assert jmespath.search("spec.rules", docs[0]) == [
+ {
+ "matches": [{"path": {"type": "PathPrefix", "value": "/"}}],
+ "backendRefs": [{"name": "my-release-webserver", "port":
8080}],
+ },
+ ]
+
+ def test_custom_path_and_path_type_should_apply(self):
+ docs = render_chart(
+ values={
+ "airflowVersion": AIRFLOW_2,
+ "webserver": {
+ "enabled": True,
+ "httpRoute": {
+ "enabled": True,
+ "parentRefs": MINIMAL_PARENT_REFS,
+ "path": "/web",
+ "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])
== "/web"
+
+ def test_custom_rules_override_default_rule(self):
+ docs = render_chart(
+ values={
+ "airflowVersion": AIRFLOW_2,
+ "webserver": {
+ "enabled": True,
+ "httpRoute": {
+ "enabled": True,
+ "parentRefs": MINIMAL_PARENT_REFS,
+ "rules": [
+ {
+ "matches": [{"path": {"type": "PathPrefix",
"value": "/custom-web"}}],
+ "backendRefs": [{"name": "external-webserver",
"port": 8443}],
+ },
+ ],
+ },
+ },
+ },
+ show_only=SHOW_ONLY,
+ api_versions=GATEWAY_API_VERSIONS,
+ )
+ assert jmespath.search("spec.rules", docs[0]) == [
+ {
+ "matches": [{"path": {"type": "PathPrefix", "value":
"/custom-web"}}],
+ "backendRefs": [{"name": "external-webserver", "port": 8443}],
+ },
+ ]
+
+ def test_should_use_airflow_ui_port_for_default_backend(self):
+ docs = render_chart(
+ values={
+ "airflowVersion": AIRFLOW_2,
+ "ports": {"airflowUI": 9000},
+ "webserver": {
+ "enabled": True,
+ "httpRoute": {"enabled": True, "parentRefs":
MINIMAL_PARENT_REFS},
+ },
+ },
+ show_only=SHOW_ONLY,
+ api_versions=GATEWAY_API_VERSIONS,
+ )
+ assert jmespath.search("spec.rules[0].backendRefs[0].port", docs[0])
== 9000
+
+ def test_httproute_not_created_when_unset(self):
+ docs = render_chart(
+ values={"airflowVersion": AIRFLOW_2, "webserver": {"enabled":
True, "httpRoute": {}}},
+ show_only=SHOW_ONLY,
+ api_versions=GATEWAY_API_VERSIONS,
+ )
+ assert docs == []
+
+ def test_httproute_not_created_when_disabled(self):
+ docs = render_chart(
+ values={
+ "airflowVersion": AIRFLOW_2,
+ "webserver": {"enabled": True, "httpRoute": {"enabled":
False}},
+ },
+ show_only=SHOW_ONLY,
+ api_versions=GATEWAY_API_VERSIONS,
+ )
+ assert docs == []
+
+ def test_should_not_render_when_webserver_disabled(self):
+ docs = render_chart(
+ values={
+ "airflowVersion": AIRFLOW_2,
+ "webserver": {
+ "enabled": False,
+ "httpRoute": {"enabled": True, "parentRefs":
MINIMAL_PARENT_REFS},
+ },
+ },
+ show_only=SHOW_ONLY,
+ api_versions=GATEWAY_API_VERSIONS,
+ )
+ assert docs == []
+
+ def test_should_not_render_on_airflow_3(self):
+ docs = render_chart(
+ values={
+ "webserver": {
+ "enabled": True,
+ "httpRoute": {"enabled": True, "parentRefs":
MINIMAL_PARENT_REFS},
+ },
+ },
+ show_only=SHOW_ONLY,
+ api_versions=GATEWAY_API_VERSIONS,
+ )
+ assert docs == []
+
+ def test_should_fail_when_gateway_api_crd_missing(self):
+ with pytest.raises(HelmFailedError) as exc_info:
+ render_chart(
+ values={
+ "airflowVersion": AIRFLOW_2,
+ "webserver": {
+ "enabled": True,
+ "httpRoute": {"enabled": True, "parentRefs":
MINIMAL_PARENT_REFS},
+ },
+ },
+ show_only=SHOW_ONLY,
+ )
+ assert "Gateway API HTTPRoute CRD" in exc_info.value.stderr.decode()
+
+ def test_should_fail_when_parent_refs_empty(self):
+ with pytest.raises(HelmFailedError) as exc_info:
+ render_chart(
+ values={
+ "airflowVersion": AIRFLOW_2,
+ "webserver": {"enabled": True, "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):
+ with pytest.raises(HelmFailedError) as exc_info:
+ render_chart(
+ values={
+ "airflowVersion": AIRFLOW_2,
+ "webserver": {"enabled": True, "httpRoute": {"enabled":
True, "parentRefs": []}},
+ },
+ show_only=SHOW_ONLY,
+ api_versions=GATEWAY_API_VERSIONS,
+ )
+ assert "parentRefs" in exc_info.value.stderr.decode()
+
+ @pytest.mark.parametrize(
+ "ingress_values",
+ [{"web": {"enabled": True}}, {"enabled": True}],
+ ids=["web-ingress", "legacy-ingress"],
+ )
+ def test_should_fail_when_ingress_and_httproute_both_enabled(self,
ingress_values):
+ with pytest.raises(HelmFailedError) as exc_info:
+ render_chart(
+ values={
+ "airflowVersion": AIRFLOW_2,
+ "ingress": ingress_values,
+ "webserver": {"enabled": True, "httpRoute": {"enabled":
True}},
+ },
+ show_only=SHOW_ONLY,
+ api_versions=GATEWAY_API_VERSIONS,
+ )
+ assert "enable only one of them" in exc_info.value.stderr.decode()
+
+ @pytest.mark.parametrize(
+ "ingress_values",
+ [{"web": {"enabled": True}}, {"enabled": True}],
+ ids=["web-ingress", "legacy-ingress"],
+ )
+ def
test_should_fail_when_ingress_template_sees_ingress_and_httproute_both_enabled(self,
ingress_values):
+ with pytest.raises(HelmFailedError) as exc_info:
+ render_chart(
+ values={
+ "airflowVersion": AIRFLOW_2,
+ "ingress": ingress_values,
+ "webserver": {"enabled": True, "httpRoute": {"enabled":
True}},
+ },
+ show_only=WEBSERVER_INGRESS_SHOW_ONLY,
+ api_versions=GATEWAY_API_VERSIONS,
+ )
+ 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={
+ "airflowVersion": AIRFLOW_2,
+ "fullnameOverride": "airflow-fullname-override",
+ "useStandardNaming": True,
+ "webserver": {
+ "enabled": True,
+ "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])
+ == "airflow-fullname-override-webserver"
+ )
+
+ def test_should_add_component_specific_labels(self):
+ docs = render_chart(
+ values={
+ "airflowVersion": AIRFLOW_2,
+ "labels": {"label1": "value1", "label2": "value2"},
+ "webserver": {
+ "enabled": True,
+ "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 jmespath.search('metadata.labels."label1"', docs[0]) == "value1"
+ assert jmespath.search('metadata.labels."test_label"', docs[0]) ==
"test_label_value"
+ assert jmespath.search('metadata.labels."route_label"', docs[0]) ==
"route_value"