You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@impala.apache.org by ja...@apache.org on 2026/08/13 14:55:03 UTC
(impala) branch master updated: IMPALA-15043: Add Kubernetes-in-Docker E2E scripts
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/impala.git
The following commit(s) were added to refs/heads/master by this push:
new 6e7c6b0e4 IMPALA-15043: Add Kubernetes-in-Docker E2E scripts
6e7c6b0e4 is described below
commit 6e7c6b0e46d99ead5b85a03abc14772a6afadc5d
Author: Anubhav Jindal <an...@gmail.com>
AuthorDate: Tue Aug 4 15:27:31 2026 -0700
IMPALA-15043: Add Kubernetes-in-Docker E2E scripts
Add Kubernetes-in-Docker support for external-cluster smoke tests by adding
Jenkins orchestration, runtime selection (kind or k3d), and a service-to-pod
port-forward fallback path for HS2 connectivity checks.
Move the K8s E2E trigger from bin/jenkins/all-tests.sh into
bin/run-all-tests.sh as an opt-in stage (K8S_E2E_TEST=false by default).
Update script ergonomics: use /usr/bin/env bash shebangs, preserve an
existing IMPALA_HOME, run from the repo root, use impala-python3 in the HS2
readiness probe, and reuse cleanup() for failed port-forward setup.
Set ENABLE_BEESWAX=false in the K8s external-cluster runner instead of
changing ImpalaTestSuite client setup semantics.
Relocate the architecture guide out of docs/ into
helm/impala/kubernetes-in-docker-e2e.md and update README references.
Testing:
- bash -n bin/jenkins/all-tests.sh
- bash -n bin/run-all-tests.sh
- bash -n bin/jenkins/run-k8s-e2e-tests.sh
- bash -n bin/run-k8s-e2e-tests.sh
- ./bin/run-k8s-e2e-tests.sh --dry-run -k TestK8sExternalCluster --maxfail=1
- ./bin/jenkins/run-k8s-e2e-tests.sh --dry-run
- python3 bin/jenkins/critique-gerrit-review.py --dryrun
Change-Id: I122b99c38c8c70ab535349f92212621330f2aa55
Assisted-by: GPT-5.3 (Cursor)
Reviewed-on: http://gerrit.cloudera.org:8080/24372
Reviewed-by: Jason Fehr <jf...@cloudera.com>
Tested-by: Jason Fehr <jf...@cloudera.com>
---
bin/jenkins/run-k8s-e2e-tests.sh | 190 +++++++++++++++++++++++++++
bin/run-all-tests.sh | 8 ++
bin/run-k8s-e2e-tests.sh | 214 +++++++++++++++++++++++++++++++
helm/impala/README.md | 36 ++++++
helm/impala/kubernetes-in-docker-e2e.md | 109 ++++++++++++++++
tests/infra/test_k8s_external_cluster.py | 32 +++++
6 files changed, 589 insertions(+)
diff --git a/bin/jenkins/run-k8s-e2e-tests.sh b/bin/jenkins/run-k8s-e2e-tests.sh
new file mode 100755
index 000000000..a1395c7fe
--- /dev/null
+++ b/bin/jenkins/run-k8s-e2e-tests.sh
@@ -0,0 +1,190 @@
+#!/usr/bin/env bash
+# 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.
+
+# Jenkins usage:
+# ./bin/jenkins/run-k8s-e2e-tests.sh
+#
+# This script provisions an ephemeral kind/k3d cluster, deploys the Impala Helm chart,
+# runs remote-cluster E2E smoke tests, and tears down the cluster.
+
+set -euo pipefail
+: "${IMPALA_HOME:=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
+. $IMPALA_HOME/bin/report_build_error.sh
+setup_report_build_error
+
+cd "${IMPALA_HOME}"
+
+: "${K8S_E2E_KIND_CLUSTER_NAME:=impala-e2e}"
+: "${K8S_E2E_K3D_CLUSTER_NAME:=impala-e2e}"
+: "${K8S_E2E_RUNTIME:=kind}"
+: "${K8S_E2E_NAMESPACE:=impala}"
+: "${K8S_E2E_RELEASE_NAME:=impala-e2e}"
+: "${K8S_E2E_HELM_VALUES_FILE:=helm/impala/values-example.yaml}"
+: "${K8S_E2E_TEST_TARGET:=infra/test_k8s_external_cluster.py}"
+: "${K8S_E2E_WAIT_TIMEOUT:=600s}"
+: "${K8S_E2E_KEEP_CLUSTER:=false}"
+: "${K8S_E2E_KIND_IMAGE:=}"
+: "${K8S_E2E_K3D_IMAGE:=}"
+: "${K8S_E2E_PORT_FORWARD_MODE:=auto}"
+: "${K8S_E2E_DRY_RUN:=false}"
+
+if [[ "${1:-}" == "--dry-run" ]]; then
+ K8S_E2E_DRY_RUN=true
+fi
+
+require_cmd() {
+ local tool="$1"
+ if ! command -v "${tool}" >/dev/null 2>&1; then
+ echo "Missing required tool: ${tool}"
+ return 1
+ fi
+}
+
+resolve_impalad_service() {
+ local expected_service="${K8S_E2E_RELEASE_NAME}-impala-impalad"
+ if kubectl -n "${K8S_E2E_NAMESPACE}" get svc \
+ "${expected_service}" >/dev/null 2>&1; then
+ echo "${expected_service}"
+ return 0
+ fi
+
+ kubectl -n "${K8S_E2E_NAMESPACE}" get svc -o json | python3 - \
+ "${K8S_E2E_RELEASE_NAME}" <<'PY'
+import json
+import sys
+
+release_name = sys.argv[1]
+services = json.load(sys.stdin).get("items", [])
+for service in services:
+ selector = ((service.get("spec") or {}).get("selector") or {})
+ if (
+ selector.get("app.kubernetes.io/instance") == release_name
+ and selector.get("app.kubernetes.io/name") == "impala"
+ and selector.get("app.kubernetes.io/component") == "impalad"
+ ):
+ print(service.get("metadata", {}).get("name", ""))
+ break
+PY
+}
+
+if [[ "${K8S_E2E_DRY_RUN}" == "true" ]]; then
+ echo "DRY RUN: runtime='${K8S_E2E_RUNTIME}'"
+ if [[ "${K8S_E2E_RUNTIME}" == "kind" ]]; then
+ echo "DRY RUN: would create kind cluster '${K8S_E2E_KIND_CLUSTER_NAME}'"
+ elif [[ "${K8S_E2E_RUNTIME}" == "k3d" ]]; then
+ echo "DRY RUN: would create k3d cluster '${K8S_E2E_K3D_CLUSTER_NAME}'"
+ fi
+ echo "DRY RUN: would deploy release '${K8S_E2E_RELEASE_NAME}' to namespace"
+ echo " '${K8S_E2E_NAMESPACE}' with values file"
+ echo " '${K8S_E2E_HELM_VALUES_FILE}'."
+ echo "DRY RUN: would run test target '${K8S_E2E_TEST_TARGET}'."
+ exit 0
+fi
+
+require_cmd docker
+require_cmd kubectl
+require_cmd helm
+if [[ "${K8S_E2E_RUNTIME}" == "kind" ]]; then
+ require_cmd kind
+elif [[ "${K8S_E2E_RUNTIME}" == "k3d" ]]; then
+ require_cmd k3d
+else
+ echo "Unsupported K8S_E2E_RUNTIME='${K8S_E2E_RUNTIME}'. Use kind or k3d."
+ exit 1
+fi
+
+TMP_KUBECONFIG="$(mktemp)"
+export KUBECONFIG="${TMP_KUBECONFIG}"
+RELEASE_SELECTOR="app.kubernetes.io/instance=${K8S_E2E_RELEASE_NAME},"
+RELEASE_SELECTOR+="app.kubernetes.io/name=impala"
+
+on_exit() {
+ local exit_code=$?
+ if [[ ${exit_code} -ne 0 ]]; then
+ kubectl -n "${K8S_E2E_NAMESPACE}" get pods -o wide || true
+ fi
+ if [[ "${K8S_E2E_KEEP_CLUSTER}" != "true" ]]; then
+ if [[ "${K8S_E2E_RUNTIME}" == "kind" ]]; then
+ kind delete cluster --name "${K8S_E2E_KIND_CLUSTER_NAME}" >/dev/null 2>&1 || true
+ else
+ k3d cluster delete "${K8S_E2E_K3D_CLUSTER_NAME}" >/dev/null 2>&1 || true
+ fi
+ fi
+ rm -f "${TMP_KUBECONFIG}" || true
+ return ${exit_code}
+}
+trap on_exit EXIT
+
+if [[ "${K8S_E2E_RUNTIME}" == "kind" ]]; then
+ kind delete cluster --name "${K8S_E2E_KIND_CLUSTER_NAME}" >/dev/null 2>&1 || true
+ CREATE_CLUSTER_CMD=(kind create cluster --name "${K8S_E2E_KIND_CLUSTER_NAME}")
+ if [[ -n "${K8S_E2E_KIND_IMAGE}" ]]; then
+ CREATE_CLUSTER_CMD+=(--image "${K8S_E2E_KIND_IMAGE}")
+ fi
+ "${CREATE_CLUSTER_CMD[@]}"
+ kind get kubeconfig --name "${K8S_E2E_KIND_CLUSTER_NAME}" > "${TMP_KUBECONFIG}"
+else
+ k3d cluster delete "${K8S_E2E_K3D_CLUSTER_NAME}" >/dev/null 2>&1 || true
+ CREATE_CLUSTER_CMD=(
+ k3d cluster create "${K8S_E2E_K3D_CLUSTER_NAME}" --servers 1 --agents 0 --wait
+ )
+ if [[ -n "${K8S_E2E_K3D_IMAGE}" ]]; then
+ CREATE_CLUSTER_CMD+=(--image "${K8S_E2E_K3D_IMAGE}")
+ fi
+ "${CREATE_CLUSTER_CMD[@]}"
+ k3d kubeconfig write "${K8S_E2E_K3D_CLUSTER_NAME}" \
+ --output "${TMP_KUBECONFIG}" \
+ --overwrite >/dev/null
+fi
+
+kubectl create namespace "${K8S_E2E_NAMESPACE}" >/dev/null 2>&1 || true
+
+helm upgrade --install "${K8S_E2E_RELEASE_NAME}" ./helm/impala \
+ -n "${K8S_E2E_NAMESPACE}" \
+ -f "${K8S_E2E_HELM_VALUES_FILE}" \
+ --set persistence.enabled=false
+
+DEPLOYMENTS="$(
+ kubectl -n "${K8S_E2E_NAMESPACE}" get deployments \
+ -l "${RELEASE_SELECTOR}" \
+ -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'
+)"
+if [[ -z "${DEPLOYMENTS}" ]]; then
+ echo "No Impala deployments found for release '${K8S_E2E_RELEASE_NAME}'."
+ exit 1
+fi
+while IFS= read -r deployment; do
+ if [[ -z "${deployment}" ]]; then
+ continue
+ fi
+ kubectl -n "${K8S_E2E_NAMESPACE}" rollout status \
+ "deployment/${deployment}" \
+ --timeout="${K8S_E2E_WAIT_TIMEOUT}"
+done <<< "${DEPLOYMENTS}"
+
+K8S_IMPALAD_SERVICE="$(resolve_impalad_service)"
+if [[ -z "${K8S_IMPALAD_SERVICE}" ]]; then
+ echo "Unable to resolve impalad service for release '${K8S_E2E_RELEASE_NAME}'."
+ exit 1
+fi
+
+K8S_NAMESPACE="${K8S_E2E_NAMESPACE}" \
+K8S_IMPALAD_SERVICE="${K8S_IMPALAD_SERVICE}" \
+K8S_PORT_FORWARD_MODE="${K8S_E2E_PORT_FORWARD_MODE}" \
+K8S_TEST_TARGET="${K8S_E2E_TEST_TARGET}" \
+./bin/run-k8s-e2e-tests.sh
diff --git a/bin/run-all-tests.sh b/bin/run-all-tests.sh
index 9a740c3d6..c0bd300d1 100755
--- a/bin/run-all-tests.sh
+++ b/bin/run-all-tests.sh
@@ -70,6 +70,8 @@ fi
: ${CLUSTER_TEST_FILES:=}
# Run Helm chart render assertions on demand.
: ${HELM_CHART_TEST:=false}
+# Run Kubernetes-in-Docker E2E tests on demand.
+: ${K8S_E2E_TEST:=false}
# Run JS tests
: ${JS_TEST:=false}
# Verifiers to run after all tests. Skipped if true.
@@ -420,6 +422,12 @@ do
fi
fi
+ if [[ "$K8S_E2E_TEST" == true ]]; then
+ if ! "${IMPALA_HOME}/bin/jenkins/run-k8s-e2e-tests.sh"; then
+ TEST_RET_CODE=1
+ fi
+ fi
+
if [ ! -z "${TEST_SUITE_VERIFIERS}" ]; then
mkdir -p "${TEST_SUITE_VERIFIERS_LOG_DIR}"
pushd "${IMPALA_HOME}/tests"
diff --git a/bin/run-k8s-e2e-tests.sh b/bin/run-k8s-e2e-tests.sh
new file mode 100755
index 000000000..44fa7ba99
--- /dev/null
+++ b/bin/run-k8s-e2e-tests.sh
@@ -0,0 +1,214 @@
+#!/usr/bin/env bash
+#
+# 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.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+: "${IMPALA_HOME:=$(cd "${SCRIPT_DIR}/.." && pwd)}"
+export IMPALA_HOME
+cd "${IMPALA_HOME}"
+
+DRY_RUN=false
+if [[ "${1:-}" == "--dry-run" ]]; then
+ DRY_RUN=true
+ shift
+fi
+
+if [[ "${DRY_RUN}" != "true" ]]; then
+ # Populate the standard Impala test environment (python wrapper, toolchain vars, etc).
+ set +u
+ source "${IMPALA_HOME}/bin/impala-config.sh" >/dev/null 2>&1
+ set -u
+ export PATH="${IMPALA_HOME}/bin:${PATH}"
+fi
+
+K8S_NAMESPACE="${K8S_NAMESPACE:-impala}"
+K8S_IMPALAD_SERVICE="${K8S_IMPALAD_SERVICE:-}"
+K8S_IMPALAD_POD="${K8S_IMPALAD_POD:-}"
+K8S_ENABLE_PORT_FORWARD="${K8S_ENABLE_PORT_FORWARD:-true}"
+K8S_PORT_FORWARD_MODE="${K8S_PORT_FORWARD_MODE:-auto}"
+K8S_PORT_FORWARD_LOG="${K8S_PORT_FORWARD_LOG:-/tmp/impala-k8s-port-forward.log}"
+
+K8S_TEST_HOST="${K8S_TEST_HOST:-127.0.0.1}"
+K8S_BEESWAX_PORT="${K8S_BEESWAX_PORT:-21000}"
+K8S_HS2_PORT="${K8S_HS2_PORT:-21050}"
+K8S_HS2_HTTP_PORT="${K8S_HS2_HTTP_PORT:-28000}"
+K8S_WEB_PORT="${K8S_WEB_PORT:-25000}"
+
+K8S_TEST_TARGET="${K8S_TEST_TARGET:-infra/test_k8s_external_cluster.py}"
+K8S_KUDU_MASTER_HOSTS="${K8S_KUDU_MASTER_HOSTS:-}"
+export ENABLE_BEESWAX=false
+
+REMOTE_URL="${IMPALA_REMOTE_URL:-http://${K8S_TEST_HOST}:${K8S_WEB_PORT}}"
+
+PORT_FORWARD_PID=""
+cleanup() {
+ if [[ -n "${PORT_FORWARD_PID}" ]] && kill -0 "${PORT_FORWARD_PID}" 2>/dev/null; then
+ kill "${PORT_FORWARD_PID}" >/dev/null 2>&1 || true
+ fi
+}
+trap cleanup EXIT
+
+wait_for_hs2_socket() {
+ "${IMPALA_HOME}/bin/impala-python3" - "${K8S_TEST_HOST}" "${K8S_HS2_PORT}" <<'PY'
+import socket
+import sys
+
+host = sys.argv[1]
+port = int(sys.argv[2])
+
+s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+s.settimeout(1.0)
+try:
+ s.connect((host, port))
+except Exception:
+ sys.exit(1)
+finally:
+ s.close()
+PY
+}
+
+resolve_impalad_pod() {
+ if [[ -n "${K8S_IMPALAD_POD}" ]]; then
+ echo "${K8S_IMPALAD_POD}"
+ return 0
+ fi
+
+ local selector=""
+ if [[ -n "${K8S_IMPALAD_SERVICE}" ]]; then
+ local selector_template='{{range $k, $v := .spec.selector}}'
+ selector_template+='{{printf "%s=%s," $k $v}}{{end}}'
+ selector="$(kubectl -n "${K8S_NAMESPACE}" get svc "${K8S_IMPALAD_SERVICE}" \
+ -o "go-template=${selector_template}" \
+ 2>/dev/null || true)"
+ selector="${selector%,}"
+ fi
+
+ if [[ -z "${selector}" ]]; then
+ selector="app.kubernetes.io/component=impalad"
+ fi
+
+ kubectl -n "${K8S_NAMESPACE}" get pod -l "${selector}" \
+ -o jsonpath='{.items[0].metadata.name}'
+}
+
+start_port_forward() {
+ local forward_target="$1"
+ : >"${K8S_PORT_FORWARD_LOG}"
+ kubectl -n "${K8S_NAMESPACE}" port-forward "${forward_target}" \
+ "${K8S_HS2_PORT}:21050" "${K8S_HS2_HTTP_PORT}:28000" "${K8S_WEB_PORT}:25000" \
+ >"${K8S_PORT_FORWARD_LOG}" 2>&1 &
+ PORT_FORWARD_PID="$!"
+
+ local ready=false
+ for _ in $(seq 1 30); do
+ if ! kill -0 "${PORT_FORWARD_PID}" 2>/dev/null; then
+ break
+ fi
+ if wait_for_hs2_socket; then
+ ready=true
+ break
+ fi
+ sleep 1
+ done
+
+ if [[ "${ready}" == "true" ]]; then
+ return 0
+ fi
+
+ cleanup
+ PORT_FORWARD_PID=""
+ return 1
+}
+
+if [[ "${DRY_RUN}" != "true" ]] && [[ "${K8S_ENABLE_PORT_FORWARD}" == "true" ]]; then
+ if ! command -v kubectl >/dev/null 2>&1; then
+ echo "kubectl must be installed when K8S_ENABLE_PORT_FORWARD=true"
+ exit 1
+ fi
+ if [[ "${K8S_PORT_FORWARD_MODE}" != "auto" ]] && \
+ [[ "${K8S_PORT_FORWARD_MODE}" != "service" ]] && \
+ [[ "${K8S_PORT_FORWARD_MODE}" != "pod" ]]; then
+ echo "K8S_PORT_FORWARD_MODE must be one of: auto, service, pod"
+ exit 1
+ fi
+
+ PORT_FORWARD_TARGET=""
+ if [[ "${K8S_PORT_FORWARD_MODE}" == "service" ]] || \
+ [[ "${K8S_PORT_FORWARD_MODE}" == "auto" && -n "${K8S_IMPALAD_SERVICE}" ]]; then
+ if [[ -z "${K8S_IMPALAD_SERVICE}" ]]; then
+ echo "Set K8S_IMPALAD_SERVICE when K8S_PORT_FORWARD_MODE=service"
+ exit 1
+ fi
+ PORT_FORWARD_TARGET="svc/${K8S_IMPALAD_SERVICE}"
+ echo "Starting service port-forward via ${PORT_FORWARD_TARGET}"
+ if ! start_port_forward "${PORT_FORWARD_TARGET}"; then
+ if [[ "${K8S_PORT_FORWARD_MODE}" == "service" ]]; then
+ echo "Timed out waiting for HS2 service port-forward readiness."
+ echo "See ${K8S_PORT_FORWARD_LOG} for details."
+ exit 1
+ fi
+ echo "Service port-forward failed; attempting pod port-forward fallback."
+ fi
+ fi
+
+ if [[ -z "${PORT_FORWARD_PID}" ]] || ! kill -0 "${PORT_FORWARD_PID}" 2>/dev/null; then
+ local_impalad_pod="$(resolve_impalad_pod)"
+ if [[ -z "${local_impalad_pod}" ]]; then
+ echo "Unable to resolve an impalad pod for port-forward fallback."
+ exit 1
+ fi
+ PORT_FORWARD_TARGET="pod/${local_impalad_pod}"
+ echo "Starting pod port-forward via ${PORT_FORWARD_TARGET}"
+ if ! start_port_forward "${PORT_FORWARD_TARGET}"; then
+ echo "Timed out waiting for HS2 pod port-forward readiness."
+ echo "See ${K8S_PORT_FORWARD_LOG} for details."
+ exit 1
+ fi
+ fi
+fi
+
+TEST_CMD=(
+ ./run-tests.py
+ "${K8S_TEST_TARGET}"
+ --testing_remote_cluster
+ --default_test_protocol=hs2
+ --impalad="${K8S_TEST_HOST}:${K8S_BEESWAX_PORT}"
+ --impalad_hs2_port="${K8S_HS2_PORT}"
+ --impalad_hs2_http_port="${K8S_HS2_HTTP_PORT}"
+)
+
+if [[ -n "${K8S_KUDU_MASTER_HOSTS}" ]]; then
+ TEST_CMD+=("--kudu_master_hosts=${K8S_KUDU_MASTER_HOSTS}")
+fi
+
+if [[ "$#" -gt 0 ]]; then
+ TEST_CMD+=("$@")
+fi
+
+echo "Using IMPALA_REMOTE_URL=${REMOTE_URL}"
+echo "Running: ${TEST_CMD[*]}"
+
+if [[ "${DRY_RUN}" == "true" ]]; then
+ exit 0
+fi
+
+pushd "${IMPALA_HOME}/tests" >/dev/null
+IMPALA_REMOTE_URL="${REMOTE_URL}" "${TEST_CMD[@]}"
+popd >/dev/null
diff --git a/helm/impala/README.md b/helm/impala/README.md
index 5ea830e9f..ccab1dbad 100644
--- a/helm/impala/README.md
+++ b/helm/impala/README.md
@@ -437,6 +437,42 @@ Required dependencies to run this harness:
This is a render-time validation suite; it does not require a running
Kubernetes cluster or Docker runtime.
+## Kubernetes end-to-end test smoke run
+
+Use the upstream test harness against an existing Kubernetes deployment:
+
+```bash
+K8S_NAMESPACE=impala \
+K8S_IMPALAD_SERVICE=impala-impala-impalad \
+./bin/run-k8s-e2e-tests.sh
+```
+
+By default this script port-forwards the Impalad service and runs
+`tests/infra/test_k8s_external_cluster.py` in remote-cluster mode.
+If service-level port-forwarding is not reliable in a local runtime, set
+`K8S_PORT_FORWARD_MODE=pod` (or use `auto` to try service first, then pod).
+Override the test target with `K8S_TEST_TARGET` or pass additional pytest
+arguments after the script name.
+
+For Jenkins-style ephemeral Kubernetes execution from this repository, use:
+
+```bash
+./bin/jenkins/run-k8s-e2e-tests.sh
+```
+
+This script now supports both `kind` and `k3d` Kubernetes-in-Docker runtimes:
+
+```bash
+# Default runtime is kind
+K8S_E2E_RUNTIME=kind ./bin/jenkins/run-k8s-e2e-tests.sh
+
+# Use k3d as an alternate runtime
+K8S_E2E_RUNTIME=k3d ./bin/jenkins/run-k8s-e2e-tests.sh
+```
+
+For architecture details and runtime diagrams, see
+`helm/impala/kubernetes-in-docker-e2e.md`.
+
## Control-plane guidance
Use one control path per release/namespace:
diff --git a/helm/impala/kubernetes-in-docker-e2e.md b/helm/impala/kubernetes-in-docker-e2e.md
new file mode 100644
index 000000000..afb69ce19
--- /dev/null
+++ b/helm/impala/kubernetes-in-docker-e2e.md
@@ -0,0 +1,109 @@
+<!--
+ 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.
+-->
+
+# Kubernetes-in-Docker E2E Testing
+
+This document explains how Impala Kubernetes smoke tests run inside Docker-based
+local clusters and CI runners.
+
+## Why this exists
+
+Impala already has end-to-end tests that can connect to remote clusters. For
+Kubernetes development and CI, it is useful to create an ephemeral Kubernetes
+cluster inside Docker, deploy the Impala Helm chart, run smoke tests, and then
+tear everything down.
+
+The repository includes:
+
+- `bin/jenkins/run-k8s-e2e-tests.sh` for cluster lifecycle and chart deployment
+- `bin/run-k8s-e2e-tests.sh` for remote-cluster test execution
+- `tests/infra/test_k8s_external_cluster.py` for smoke coverage
+
+## Runtime architecture
+
+At a high level, this is "container-in-container":
+
+```mermaid
+flowchart TD
+ A[Host machine] --> B[Docker Desktop or Docker Engine]
+ B --> C[kind or k3d cluster node container]
+ C --> D[k3s or kubeadm control plane]
+ D --> E[containerd runtime]
+ E --> F[Impala pods]
+ F --> G[impalad, catalogd, statestored, hms]
+```
+
+Notes:
+
+- The outer runtime is Docker.
+- Kubernetes workloads inside the node container are managed by containerd.
+- Images used for Impala components are still pulled from OCI registries.
+
+## Test flow
+
+```mermaid
+sequenceDiagram
+ participant U as User or CI
+ participant J as run-k8s-e2e-tests.sh (Jenkins)
+ participant K as kind or k3d cluster
+ participant H as Helm chart
+ participant T as run-tests.py
+
+ U->>J: Start K8s E2E run
+ J->>K: Create ephemeral cluster
+ J->>H: helm upgrade --install impala
+ J->>K: Wait for deployments rollout
+ J->>T: Run remote-cluster smoke tests
+ T->>K: Port-forward to impalad (service or pod)
+ T->>K: Execute select 1 / show databases
+ J->>K: Delete cluster (unless keep enabled)
+```
+
+## Selecting the runtime
+
+`bin/jenkins/run-k8s-e2e-tests.sh` supports:
+
+- `K8S_E2E_RUNTIME=kind` (default)
+- `K8S_E2E_RUNTIME=k3d`
+
+Example:
+
+```bash
+K8S_E2E_RUNTIME=k3d ./bin/jenkins/run-k8s-e2e-tests.sh
+```
+
+## Port-forward mode
+
+`bin/run-k8s-e2e-tests.sh` supports:
+
+- `K8S_PORT_FORWARD_MODE=auto` (default): service first, then pod fallback
+- `K8S_PORT_FORWARD_MODE=service`: service only
+- `K8S_PORT_FORWARD_MODE=pod`: pod only
+
+This helps when service-level forwarding is flaky in some local Docker-based
+Kubernetes environments.
+
+## Smoke test scope
+
+Current smoke coverage validates:
+
+- HS2 connectivity (`select 1`)
+- Catalog visibility (`show databases`)
+
+This is intentionally minimal for fast verification in ephemeral clusters.
diff --git a/tests/infra/test_k8s_external_cluster.py b/tests/infra/test_k8s_external_cluster.py
new file mode 100644
index 000000000..a7bf9c37d
--- /dev/null
+++ b/tests/infra/test_k8s_external_cluster.py
@@ -0,0 +1,32 @@
+# 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 absolute_import, division, print_function
+
+from tests.common.impala_test_suite import ImpalaTestSuite
+
+
+class TestK8sExternalCluster(ImpalaTestSuite):
+ """Remote-cluster smoke tests suitable for Kubernetes deployments."""
+
+ def test_select_one(self):
+ value = self.execute_scalar("select 1")
+ assert value == "1"
+
+ def test_show_databases(self):
+ result = self.client.execute("show databases")
+ assert len(result.data) > 0