diff --git a/.github/actions/setup-e2e-kind/action.yml b/.github/actions/setup-e2e-kind/action.yml index e617a27717..029f385e56 100644 --- a/.github/actions/setup-e2e-kind/action.yml +++ b/.github/actions/setup-e2e-kind/action.yml @@ -8,6 +8,10 @@ inputs: cluster-name: description: kind cluster name required: true + config: + description: Optional kind cluster configuration file + required: false + default: "" image-tag: description: OpenShell image tag to load required: true @@ -49,6 +53,7 @@ runs: uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0 with: cluster_name: ${{ inputs.cluster-name }} + config: ${{ inputs.config }} wait: 120s - name: Export kind kubeconfig to mise path diff --git a/.github/workflows/e2e-kubernetes-test.yml b/.github/workflows/e2e-kubernetes-test.yml index 449f3c25f0..80ccc65935 100644 --- a/.github/workflows/e2e-kubernetes-test.yml +++ b/.github/workflows/e2e-kubernetes-test.yml @@ -53,7 +53,7 @@ on: type: string default: "" use-envoy-gateway: - description: "Route the test through Envoy Gateway instead of a direct gateway Service port-forward" + description: "Route the test through Envoy Gateway instead of the direct gateway NodePort" required: false type: boolean default: false @@ -124,6 +124,7 @@ jobs: uses: ./.github/actions/setup-e2e-kind with: cluster-name: ${{ env.KIND_CLUSTER_NAME }} + config: ${{ !inputs.use-envoy-gateway && 'e2e/kind-nodeport.yaml' || '' }} image-tag: ${{ inputs.image-tag }} images: ${{ inputs.cluster-images }} registry-username: ${{ github.actor }} @@ -134,6 +135,7 @@ jobs: env: AGENT_SANDBOX_VERSION: ${{ inputs.agent-sandbox-version }} OPENSHELL_E2E_KUBE_CONTEXT: kind-${{ env.KIND_CLUSTER_NAME }} + OPENSHELL_E2E_KUBE_NODEPORT_PORT: ${{ !inputs.use-envoy-gateway && '17670' || '' }} OPENSHELL_E2E_KUBE_EXTRA_VALUES: ${{ inputs.extra-helm-values }} OPENSHELL_E2E_KUBE_EXTERNAL_POSTGRES_SECRET: ${{ inputs.external-postgres-secret }} OPENSHELL_E2E_KUBE_TEST: ${{ inputs.test-name }} diff --git a/CI.md b/CI.md index 9edefde745..915a73528b 100644 --- a/CI.md +++ b/CI.md @@ -135,7 +135,7 @@ Three opt-in labels enable the long-running E2E suites: (`replicaCount: 2` and bundled PostgreSQL) and the credential-driver suite (Kubernetes Secrets plus Vault) in `Branch E2E Checks` -When multiple labels are present, `Branch E2E Checks` builds each generic multi-architecture artifact set once and fans out enabled suites in parallel. Runtime-specific reusable workflows define the Docker, Podman, VM, and Kubernetes lanes. Composite actions own the replaceable Podman, KVM, kind, and mise setup. Each lane depends only on the artifact categories it consumes: VM does not wait for container-driver artifacts or supervisor images, and GPU does not wait for the gateway image. Docker, Podman, GPU, Rust, Python, MCP, and VM E2E reuse matching prebuilt gateway and CLI binaries instead of compiling debug binaries in test jobs. Standalone-driver lanes additionally reuse driver-free gateway and compute-driver artifacts. Kubernetes managed-driver lanes consume published gateway and supervisor images, while the standalone-driver lane composes its gateway image from prebuilt binaries. +When multiple labels are present, `Branch E2E Checks` builds each generic multi-architecture artifact set once and fans out enabled suites in parallel. Runtime-specific reusable workflows define the Docker, Podman, VM, and Kubernetes lanes. Composite actions own the replaceable Podman, KVM, kind, and mise setup. Direct Kubernetes E2E lanes use a kind NodePort mapped to runner localhost (`17670` → `30080`) for gateway gRPC/SSH traffic; health checks still use port-forwarding. Envoy Gateway lanes retain their proxy Service port-forward. Each lane depends only on the artifact categories it consumes: VM does not wait for container-driver artifacts or supervisor images, and GPU does not wait for the gateway image. Docker, Podman, GPU, Rust, Python, MCP, and VM E2E reuse matching prebuilt gateway and CLI binaries instead of compiling debug binaries in test jobs. Standalone-driver lanes additionally reuse driver-free gateway and compute-driver artifacts. Kubernetes managed-driver lanes consume published gateway and supervisor images, while the standalone-driver lane composes its gateway image from prebuilt binaries. The `OpenShell / E2E` and `OpenShell / GPU E2E` required statuses are evaluated from separate suite result jobs inside that workflow. `test:e2e-kubernetes` is optional while Kubernetes HA and credential-driver behavior are under active iteration: failures are visible in the workflow run but do not publish a required CI gate status. The GitHub ruleset should require the `OpenShell / ...` statuses published by diff --git a/e2e/kind-nodeport.yaml b/e2e/kind-nodeport.yaml new file mode 100644 index 0000000000..2f60136a93 --- /dev/null +++ b/e2e/kind-nodeport.yaml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: + - role: control-plane + extraPortMappings: + # Match service.nodePort in with-kube-gateway.sh. Keep the host listener local. + - containerPort: 30080 + hostPort: 17670 + listenAddress: "127.0.0.1" + protocol: TCP diff --git a/e2e/with-kube-gateway.sh b/e2e/with-kube-gateway.sh index ee755b8760..b2e3094fb4 100755 --- a/e2e/with-kube-gateway.sh +++ b/e2e/with-kube-gateway.sh @@ -7,14 +7,15 @@ # Modes: # - OPENSHELL_E2E_KUBE_CONTEXT set: # Target the named kubectl context, install the chart into an ephemeral -# namespace, and port-forward the gateway. Cluster lifecycle is the +# namespace, and connect through the configured gateway transport. Cluster lifecycle is the # caller's responsibility (e.g. CI provisions kind via helm/kind-action). # - OPENSHELL_E2E_KUBE_CONTEXT unset: # Create a local k3d cluster via tasks/scripts/helm-k3s-local.sh, install # the chart, port-forward, and tear the cluster down on exit. # # On vanilla Kubernetes, Helm e2e talks to the gateway in plaintext over -# `kubectl port-forward` (ci/values-skaffold.yaml). The certgen hook still runs +# a mapped NodePort in kind CI, or `kubectl port-forward` for other clusters +# (ci/values-skaffold.yaml). The certgen hook still runs # so the gateway has sandbox JWT signing keys. # # On OpenShift, that port-forward is too slow for `sandbox connect`. That command @@ -28,6 +29,9 @@ # Every OpenShift-specific branch below is gated on OPENSHIFT_DETECTED, so the # vanilla-Kubernetes path stays exactly the same. # +# Set OPENSHELL_E2E_KUBE_NODEPORT_PORT to the localhost port mapped to gateway +# NodePort 30080 (kind CI uses 17670). This requires a matching cluster port map. +# # Set OPENSHELL_E2E_KUBE_EXTRA_VALUES to one or more colon-separated Helm values # files, relative to the repository root or absolute, to layer additional chart # configuration on top of ci/values-skaffold.yaml. @@ -307,7 +311,7 @@ wait_for_envoy_service() { return 1 } -start_gateway_portforward() { +start_gateway_connection() { local elapsed=0 local pf_timeout=30 local target_port=8080 @@ -315,6 +319,22 @@ start_gateway_portforward() { local target_service="${RELEASE_NAME}" local target_service_ref="" + # kind CI maps this NodePort to localhost so SSH streams avoid kubectl's + # port-forward transport. Local/external clusters retain their existing path. + if [ -n "${OPENSHELL_E2E_KUBE_NODEPORT_PORT:-}" ]; then + LOCAL_PORT="${OPENSHELL_E2E_KUBE_NODEPORT_PORT}" + echo "Waiting for mapped gateway NodePort at 127.0.0.1:${LOCAL_PORT}..." + while [ "${elapsed}" -lt "${pf_timeout}" ]; do + if curl -s -o /dev/null --connect-timeout 1 "http://127.0.0.1:${LOCAL_PORT}"; then + return 0 + fi + sleep 1 + elapsed=$((elapsed + 1)) + done + echo "ERROR: mapped gateway NodePort did not accept TCP within ${pf_timeout}s" >&2 + return 1 + fi + LOCAL_PORT="$(e2e_pick_port)" if use_envoy_gateway; then target_service_ref="$(wait_for_envoy_service)" @@ -697,7 +717,7 @@ scenario_cleanup_external_pg() { cleanup_postgres_fixture my-pg-credentials } -# Run a single DB-backend scenario: install chart → port-forward → run tests → cleanup. +# Run a single DB-backend scenario: install chart → connect → run tests → cleanup. # Usage: run_scenario "label" "type" [extra --set flags...] # type: sqlite | external-pg run_scenario() { @@ -731,9 +751,9 @@ run_scenario() { return fi else - # Vanilla Kubernetes: reach the gateway in plaintext over port-forward. - if ! start_gateway_portforward; then - scenario_record_failure "${scenario_label}" "port-forward failed" + # Vanilla Kubernetes: use a mapped NodePort in kind CI, otherwise port-forward. + if ! start_gateway_connection; then + scenario_record_failure "${scenario_label}" "gateway connection failed" return fi GATEWAY_NAME="openshell-e2e-kube-${LOCAL_PORT}" @@ -1406,6 +1426,20 @@ if [ -n "${OPENSHELL_E2E_KUBE_EXTRA_VALUES:-}" ]; then helm_values_args+=(--values "${values_file}") done fi +if [ -n "${OPENSHELL_E2E_KUBE_NODEPORT_PORT:-}" ]; then + if use_envoy_gateway || [ "${OPENSHIFT_DETECTED}" = "1" ]; then + echo "ERROR: mapped gateway NodePort requires a direct vanilla Kubernetes gateway" >&2 + exit 2 + fi + if ! [[ "${OPENSHELL_E2E_KUBE_NODEPORT_PORT}" =~ ^[0-9]+$ ]] || + [ "${OPENSHELL_E2E_KUBE_NODEPORT_PORT}" -lt 1 ] || + [ "${OPENSHELL_E2E_KUBE_NODEPORT_PORT}" -gt 65535 ]; then + echo "ERROR: OPENSHELL_E2E_KUBE_NODEPORT_PORT must be a TCP port (1-65535)" >&2 + exit 2 + fi + # These overrides must follow extra values so the Service matches kind's map. + helm_values_args+=(--set "service.type=NodePort" --set "service.nodePort=30080") +fi if use_envoy_gateway; then helm_values_args+=(--values "${ROOT}/deploy/helm/openshell/ci/values-gateway.yaml") install_envoy_gateway @@ -1481,8 +1515,8 @@ else # SSH-relay `sandbox connect` suites work (port-forward stalls them). openshift_register_route_gateway || exit 1 else - # Vanilla Kubernetes: reach the gateway in plaintext over port-forward. - start_gateway_portforward || exit 1 + # Vanilla Kubernetes: use a mapped NodePort in kind CI, otherwise port-forward. + start_gateway_connection || exit 1 GATEWAY_NAME="openshell-e2e-kube-${LOCAL_PORT}" GATEWAY_ENDPOINT="http://127.0.0.1:${LOCAL_PORT}" e2e_register_plaintext_gateway \ diff --git a/skills/debug-openshell-cluster/SKILL.md b/skills/debug-openshell-cluster/SKILL.md index 2ed1381fe2..968ee41848 100644 --- a/skills/debug-openshell-cluster/SKILL.md +++ b/skills/debug-openshell-cluster/SKILL.md @@ -741,6 +741,15 @@ kubectl -n openshell get svc openshell -o wide kubectl -n openshell get endpoints openshell ``` +For kind CI diagnostics, direct Kubernetes E2E lanes expose the gateway through +NodePort `30080`, mapped to runner `127.0.0.1:17670`. The runner registers that +endpoint when `OPENSHELL_E2E_KUBE_NODEPORT_PORT=17670`; gateway health checks +still use port-forwarding. Envoy Gateway lanes retain their proxy Service +port-forward. Inspect the NodePort mapping and Service if the direct CI endpoint +is unreachable. Port-forwarding can delay SSH session cleanup and cause a +replacement connection to attach read-only, so compare transports when debugging +reconnect failures. + For local port-forward testing: ```bash