This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new a5e5e1a74be Reduce K8s test resource usage with single-node Kind 
clusters (#73998)
a5e5e1a74be is described below

commit a5e5e1a74be25f18629a346f85028fde27439b66
Author: Aaron Chen <[email protected]>
AuthorDate: Sun Oct 18 11:21:09 2026 -0700

    Reduce K8s test resource usage with single-node Kind clusters (#73998)
    
    * Reduce K8s test resource usage with single-node Kind clusters
    
    * Add test for forwarded node port matching Kind config and NodePort service
    
    * Refactor Kubernetes configuration and tests to use centralized port 
rendering
---
 .github/workflows/kustomize-overlays-tests.yml     |  6 +-
 contributing-docs/testing/k8s_tests.rst            |  9 +--
 dev/breeze/doc/05_test_commands.rst                |  2 +-
 .../src/airflow_breeze/utils/kubernetes_utils.py   | 34 +++++++--
 dev/breeze/tests/test_kubernetes_utils.py          | 86 ++++++++++++++++++++++
 scripts/ci/kubernetes/kind-cluster-conf.yaml       |  5 +-
 6 files changed, 123 insertions(+), 19 deletions(-)

diff --git a/.github/workflows/kustomize-overlays-tests.yml 
b/.github/workflows/kustomize-overlays-tests.yml
index 94637194bfa..1cca2b85000 100644
--- a/.github/workflows/kustomize-overlays-tests.yml
+++ b/.github/workflows/kustomize-overlays-tests.yml
@@ -102,13 +102,13 @@ jobs:
       #                          convention (sources come from the base
       #                          image, not the workspace).
       #   5. upload-k8s-image  -> loads the now-built kubernetes-variant
-      #                           image into every kind node, so kubelet
+      #                           image into the kind node, so kubelet
       #                           can resolve the chart's image reference
       #                           without hitting ghcr (which is private).
       #   6. deploy-airflow
       #   7. smoke-test-overlay  -> renders the overlay, auto-preloads the
       #                             overlay's own images into the kind
-      #                             nodes (no per-overlay images list
+      #                             node (no per-overlay images list
       #                             needed - discovery walks the rendered
       #                             manifest), applies, walks the
       #                             STATUS.yaml verify: block with
@@ -124,7 +124,7 @@ jobs:
         run: breeze k8s configure-cluster
       - name: "Build kubernetes-variant image on top of the restored PROD 
image"
         run: breeze k8s build-k8s-image --no-copy-local-sources
-      - name: "Upload kubernetes-variant image to kind nodes"
+      - name: "Upload kubernetes-variant image to the kind node"
         run: breeze k8s upload-k8s-image
       - name: "Deploy Airflow chart"
         run: breeze k8s deploy-airflow --executor CeleryExecutor
diff --git a/contributing-docs/testing/k8s_tests.rst 
b/contributing-docs/testing/k8s_tests.rst
index f0b121bcee7..3a6afc11221 100644
--- a/contributing-docs/testing/k8s_tests.rst
+++ b/contributing-docs/testing/k8s_tests.rst
@@ -281,7 +281,6 @@ The typical session for tests with Kubernetes looks like 
follows:
         apiServerPort: 48366
       nodes:
         - role: control-plane
-        - role: worker
           extraPortMappings:
             - containerPort: 30007
               hostPort: 18150
@@ -290,12 +289,11 @@ The typical session for tests with Kubernetes looks like 
follows:
 
       Creating cluster "airflow-python-3.11-v1.24.2" ...
        ✓ Ensuring node image (kindest/node:v1.24.2) đŸ–ŧ
-       ✓ Preparing nodes đŸ“Ļ đŸ“Ļ
+       ✓ Preparing nodes đŸ“Ļ
        ✓ Writing configuration 📜
        ✓ Starting control-plane đŸ•šī¸
        ✓ Installing CNI 🔌
        ✓ Installing StorageClass 💾
-       ✓ Joining worker nodes 🚜
       Set kubectl context to "kind-airflow-python-3.11-v1.24.2"
       You can now use your cluster with:
 
@@ -377,13 +375,11 @@ The typical session for tests with Kubernetes looks like 
follows:
       coredns-6d4b75cb6d-rwp9d                                           1/1   
  Running   0          71s
       coredns-6d4b75cb6d-vqnrc                                           1/1   
  Running   0          71s
       etcd-airflow-python-3.11-v1.24.2-control-plane                     1/1   
  Running   0          84s
-      kindnet-ckc8l                                                      1/1   
  Running   0          69s
       kindnet-qqt8k                                                      1/1   
  Running   0          71s
       kube-apiserver-airflow-python-3.11-v1.24.2-control-plane           1/1   
  Running   0          84s
       kube-controller-manager-airflow-python-3.11-v1.24.2-control-plane  1/1   
  Running   0          84s
-      kube-proxy-6g7hn                                                   1/1   
  Running   0          69s
       kube-proxy-dwfvp                                                   1/1   
  Running   0          71s
-      kube-scheduler-airflow-python-3.11-v1.24.2-control-plane            1/1  
   Running   0          84s
+      kube-scheduler-airflow-python-3.11-v1.24.2-control-plane           1/1   
  Running   0          84s
 
       KinD Cluster API server URL: http://localhost:48366
       Connecting to localhost:18150. Num try: 1
@@ -446,7 +442,6 @@ The typical session for tests with Kubernetes looks like 
follows:
       Good version of helm installed: 3.9.2 in 
/Users/jarek/IdeaProjects/airflow/kubernetes-tests/.venv/bin
       Stable repo is already added
       Uploading Airflow image 
ghcr.io/apache/airflow/main/prod/python3.11-kubernetes to cluster 
airflow-python-3.11-v1.24.2
-      Image: "ghcr.io/apache/airflow/main/prod/python3.11-kubernetes" with ID 
"sha256:fb6195f7c2c2ad97788a563a3fe9420bf3576c85575378d642cd7985aff97412" not 
yet present on node "airflow-python-3.11-v1.24.2-worker", loading...
       Image: "ghcr.io/apache/airflow/main/prod/python3.11-kubernetes" with ID 
"sha256:fb6195f7c2c2ad97788a563a3fe9420bf3576c85575378d642cd7985aff97412" not 
yet present on node "airflow-python-3.11-v1.24.2-control-plane", loading...
 
       NEXT STEP: You might now deploy Airflow by:
diff --git a/dev/breeze/doc/05_test_commands.rst 
b/dev/breeze/doc/05_test_commands.rst
index 980caf471f0..84b1edcb590 100644
--- a/dev/breeze/doc/05_test_commands.rst
+++ b/dev/breeze/doc/05_test_commands.rst
@@ -703,7 +703,7 @@ The runner is overlay-agnostic. For every overlay it:
 
 * renders the overlay and substitutes ``RELEASE-NAME`` / ``NAMESPACE``,
 * **auto-preloads every ``image:`` referenced by the rendered manifest**
-  into the kind nodes via ``docker pull`` (with retry on Docker Hub
+  into the kind node via ``docker pull`` (with retry on Docker Hub
   rate limits) + ``kind load docker-image``, so the test does not flake
   on registry availability,
 * applies the overlay,
diff --git a/dev/breeze/src/airflow_breeze/utils/kubernetes_utils.py 
b/dev/breeze/src/airflow_breeze/utils/kubernetes_utils.py
index 9b9eb299bdb..d6e9c343b9b 100644
--- a/dev/breeze/src/airflow_breeze/utils/kubernetes_utils.py
+++ b/dev/breeze/src/airflow_breeze/utils/kubernetes_utils.py
@@ -379,6 +379,20 @@ def _get_kind_cluster_config_content(python: str, 
kubernetes_version: str) -> di
     return yaml.safe_load(config_path.read_text())
 
 
+# Must match nodePort in scripts/ci/kubernetes/nodeport.yaml.
+FORWARDED_NODE_PORT = 30007
+
+
+def _render_kind_cluster_config(forwarded_port_number: int, 
k8s_api_server_port: int) -> str:
+    return (
+        (SCRIPTS_CI_KUBERNETES_PATH / "kind-cluster-conf.yaml")
+        .read_text()
+        .replace("{{FORWARDED_NODE_PORT}}", str(FORWARDED_NODE_PORT))
+        .replace("{{FORWARDED_PORT_NUMBER}}", str(forwarded_port_number))
+        .replace("{{API_SERVER_PORT}}", str(k8s_api_server_port))
+    )
+
+
 def set_random_cluster_ports(python: str, kubernetes_version: str, output: 
Output | None) -> None:
     """
     Creates cluster config file and returns sockets keeping the ports bound.
@@ -390,11 +404,8 @@ def set_random_cluster_ports(python: str, 
kubernetes_version: str, output: Outpu
         f"[info]Random ports: K8S API: {k8s_api_server_port}, API Server: 
{forwarded_port_number}"
     )
     cluster_conf_path = get_kind_cluster_config_path(python=python, 
kubernetes_version=kubernetes_version)
-    config = (
-        (AIRFLOW_ROOT_PATH / "scripts" / "ci" / "kubernetes" / 
"kind-cluster-conf.yaml")
-        .read_text()
-        .replace("{{FORWARDED_PORT_NUMBER}}", str(forwarded_port_number))
-        .replace("{{API_SERVER_PORT}}", str(k8s_api_server_port))
+    config = _render_kind_cluster_config(
+        forwarded_port_number=forwarded_port_number, 
k8s_api_server_port=k8s_api_server_port
     )
     cluster_conf_path.write_text(config)
     get_console(output=output).print(f"[info]Config created in 
{cluster_conf_path}:\n")
@@ -402,12 +413,23 @@ def set_random_cluster_ports(python: str, 
kubernetes_version: str, output: Outpu
     get_console(output=output).print("\n")
 
 
+def _extract_forwarded_host_port(conf: dict[str, Any]) -> int:
+    for node in conf["nodes"]:
+        for port_mapping in node.get("extraPortMappings", []):
+            if port_mapping["containerPort"] == FORWARDED_NODE_PORT:
+                return port_mapping["hostPort"]
+    raise ValueError(
+        f"No node in the kind cluster config has an extraPortMappings entry 
for "
+        f"containerPort {FORWARDED_NODE_PORT}. Delete and recreate the 
cluster."
+    )
+
+
 def get_kubernetes_port_numbers(python: str, kubernetes_version: str) -> 
tuple[int, int]:
     conf = _get_kind_cluster_config_content(python=python, 
kubernetes_version=kubernetes_version)
     if not conf:
         return 0, 0
     k8s_api_server_port = conf["networking"]["apiServerPort"]
-    api_server_port = conf["nodes"][1]["extraPortMappings"][0]["hostPort"]
+    api_server_port = _extract_forwarded_host_port(conf)
     return k8s_api_server_port, api_server_port
 
 
diff --git a/dev/breeze/tests/test_kubernetes_utils.py 
b/dev/breeze/tests/test_kubernetes_utils.py
new file mode 100644
index 00000000000..88dee2d715f
--- /dev/null
+++ b/dev/breeze/tests/test_kubernetes_utils.py
@@ -0,0 +1,86 @@
+# 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
+
+from unittest.mock import patch
+
+import pytest
+import yaml
+
+from airflow_breeze.utils.kubernetes_utils import (
+    FORWARDED_NODE_PORT,
+    SCRIPTS_CI_KUBERNETES_PATH,
+    _render_kind_cluster_config,
+    get_kubernetes_port_numbers,
+)
+
+# Rendered config of a cluster created before the switch to single-node 
clusters —
+# port extraction must keep working for clusters that already exist on disk.
+LEGACY_TWO_NODE_CONFIG = """\
+kind: Cluster
+apiVersion: kind.x-k8s.io/v1alpha4
+networking:
+  ipFamily: ipv4
+  apiServerAddress: "127.0.0.1"
+  apiServerPort: 48366
+nodes:
+  - role: control-plane
+  - role: worker
+    extraPortMappings:
+      - containerPort: 30007
+        hostPort: 18150
+        listenAddress: "127.0.0.1"
+        protocol: TCP
+"""
+
+NO_FORWARDED_PORT_CONFIG = """\
+kind: Cluster
+apiVersion: kind.x-k8s.io/v1alpha4
+networking:
+  ipFamily: ipv4
+  apiServerAddress: "127.0.0.1"
+  apiServerPort: 48366
+nodes:
+  - role: control-plane
+"""
+
+
[email protected](
+    "config",
+    [
+        _render_kind_cluster_config(forwarded_port_number=18150, 
k8s_api_server_port=48366),
+        LEGACY_TWO_NODE_CONFIG,
+    ],
+    ids=["current-template", "legacy-two-node"],
+)
+@patch("airflow_breeze.utils.kubernetes_utils._get_kind_cluster_config_content",
 autospec=True)
+def 
test_get_kubernetes_port_numbers_extracts_ports_from_any_node(mock_get_content, 
config):
+    mock_get_content.return_value = yaml.safe_load(config)
+    assert get_kubernetes_port_numbers(python="3.10", 
kubernetes_version="v1.30.13") == (48366, 18150)
+
+
+@patch("airflow_breeze.utils.kubernetes_utils._get_kind_cluster_config_content",
 autospec=True)
+def 
test_get_kubernetes_port_numbers_raises_when_forwarded_port_mapping_is_missing(mock_get_content):
+    mock_get_content.return_value = yaml.safe_load(NO_FORWARDED_PORT_CONFIG)
+    with pytest.raises(ValueError, match="extraPortMappings"):
+        get_kubernetes_port_numbers(python="3.10", 
kubernetes_version="v1.30.13")
+
+
+def test_nodeport_service_exposes_forwarded_node_port():
+    node_port_service = yaml.safe_load((SCRIPTS_CI_KUBERNETES_PATH / 
"nodeport.yaml").read_text())
+    assert [port["nodePort"] for port in node_port_service["spec"]["ports"]] 
== [FORWARDED_NODE_PORT]
diff --git a/scripts/ci/kubernetes/kind-cluster-conf.yaml 
b/scripts/ci/kubernetes/kind-cluster-conf.yaml
index 4e891f80ddf..d9e93e9646b 100644
--- a/scripts/ci/kubernetes/kind-cluster-conf.yaml
+++ b/scripts/ci/kubernetes/kind-cluster-conf.yaml
@@ -22,10 +22,11 @@ networking:
   apiServerAddress: "127.0.0.1"
   apiServerPort: {{API_SERVER_PORT}}
 nodes:
+  # A single node on purpose: kind drops the control-plane NoSchedule taint 
only for single-node
+  # clusters, so the Airflow and KubernetesPodOperator pods are scheduled here.
   - role: control-plane
-  - role: worker
     extraPortMappings:
-      - containerPort: 30007
+      - containerPort: {{FORWARDED_NODE_PORT}}
         hostPort: {{FORWARDED_PORT_NUMBER}}
         listenAddress: "127.0.0.1"
         protocol: TCP

Reply via email to