Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions .github/workflows/branch-helm-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

name: Branch Helm E2E

on:
push:
branches:
- "pull-request/[0-9]+"
workflow_dispatch: {}

permissions: {}

jobs:
pr_metadata:
name: Resolve PR metadata
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
should_run: ${{ steps.gate.outputs.should_run }}
steps:
- uses: actions/checkout@v6

- id: gate
uses: ./.github/actions/pr-gate
with:
required_label: test:e2e-helm

build-gateway:
needs: [pr_metadata]
if: needs.pr_metadata.outputs.should_run == 'true'
permissions:
contents: read
packages: write
uses: ./.github/workflows/docker-build.yml
with:
component: gateway
platform: linux/amd64

build-supervisor:
needs: [pr_metadata]
if: needs.pr_metadata.outputs.should_run == 'true'
permissions:
contents: read
packages: write
uses: ./.github/workflows/docker-build.yml
with:
component: supervisor
platform: linux/amd64

helm-e2e:
name: Helm E2E (Rust smoke)
needs: [pr_metadata, build-gateway, build-supervisor]
if: needs.pr_metadata.outputs.should_run == 'true'
# Bare runner: running kind-in-container hits nested-Docker / kubeconfig
# complications. The runner has Docker; mise installs helm, kubectl, and
# the Rust toolchain.
runs-on: linux-amd64-cpu8
timeout-minutes: 60
permissions:
contents: read
packages: read
env:
MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
KIND_CLUSTER_NAME: helm-e2e-${{ github.run_id }}
steps:
- uses: actions/checkout@v6

- name: Install mise
run: |
curl https://mise.run | sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "$HOME/.local/share/mise/shims" >> "$GITHUB_PATH"

- name: Install tools
run: mise install --locked

# The openshell-policy crate transitively pulls in z3-sys, whose
# build script needs the z3 C/C++ headers and clang/bindgen to
# compile. The bare runner doesn't ship them; the CI container
# image used by other Rust e2e jobs does, but we can't run helm-e2e
# there (the runner's container handler injects its own --network
# bridge, which conflicts with the --network host we need so kind's
# API server is reachable from the test process).
- name: Install z3 build deps
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libz3-dev clang

- name: Log in to GHCR
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin

- name: Create kind cluster
uses: helm/kind-action@v1
with:
cluster_name: ${{ env.KIND_CLUSTER_NAME }}
wait: 120s

# mise.toml sets KUBECONFIG="{{config_root}}/kubeconfig"; helm/kind-action
# writes to ~/.kube/config. Materialize the kind context at the mise path
# so `mise run e2e:helm` (and the wrapper's `kubectl --context=…`) finds
# the kind cluster.
- name: Export kind kubeconfig to mise path
run: |
set -euo pipefail
kind get kubeconfig --name "$KIND_CLUSTER_NAME" > "$GITHUB_WORKSPACE/kubeconfig"
chmod 600 "$GITHUB_WORKSPACE/kubeconfig"

# Pre-pull and side-load: kind nodes don't have ghcr credentials, and
# tagging IMAGE_TAG to a SHA means the chart's IfNotPresent pull policy
# is satisfied once the image is loaded into the node's containerd.
- name: Load gateway and supervisor images into kind
run: |
set -euo pipefail
for component in gateway supervisor; do
image="ghcr.io/nvidia/openshell/${component}:${{ github.sha }}"
docker pull "$image"
kind load docker-image "$image" --name "$KIND_CLUSTER_NAME"
done

- name: Run Helm E2E (Rust smoke)
env:
OPENSHELL_E2E_KUBE_CONTEXT: kind-${{ env.KIND_CLUSTER_NAME }}
IMAGE_TAG: ${{ github.sha }}
OPENSHELL_REGISTRY: ghcr.io/nvidia/openshell
run: mise run --no-deps --skip-deps e2e:helm
3 changes: 2 additions & 1 deletion .github/workflows/e2e-label-help.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ permissions: {}
jobs:
hint:
name: Post next-step hint for E2E label
if: github.event.label.name == 'test:e2e' || github.event.label.name == 'test:e2e-gpu'
if: github.event.label.name == 'test:e2e' || github.event.label.name == 'test:e2e-gpu' || github.event.label.name == 'test:e2e-helm'
runs-on: ubuntu-latest
permissions:
pull-requests: write
Expand All @@ -40,6 +40,7 @@ jobs:
case "$LABEL_NAME" in
test:e2e) workflow_file=branch-e2e.yml; workflow_name="Branch E2E Checks" ;;
test:e2e-gpu) workflow_file=test-gpu.yml; workflow_name="GPU Test" ;;
test:e2e-helm) workflow_file=branch-helm-e2e.yml; workflow_name="Branch Helm E2E" ;;
*) echo "Unrecognized label $LABEL_NAME"; exit 1 ;;
esac

Expand Down
27 changes: 27 additions & 0 deletions e2e/rust/e2e-helm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Run the Rust e2e suite against a Helm-deployed OpenShell gateway. Set
# OPENSHELL_E2E_KUBE_CONTEXT to target an existing cluster; otherwise an
# ephemeral k3d cluster is created and torn down by with-kube-gateway.sh.
# Set OPENSHELL_E2E_KUBE_TEST to scope to a single integration test
# (e.g. smoke) for local debugging.

set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"

cargo build -p openshell-cli --features openshell-core/dev-settings

test_filter=()
if [ -n "${OPENSHELL_E2E_KUBE_TEST:-}" ]; then
test_filter+=(--test "${OPENSHELL_E2E_KUBE_TEST}")
fi

exec "${ROOT}/e2e/with-kube-gateway.sh" \
cargo test --manifest-path "${ROOT}/e2e/rust/Cargo.toml" \
--features e2e \
--no-fail-fast \
${test_filter[@]+"${test_filter[@]}"} \
-- --nocapture
20 changes: 20 additions & 0 deletions e2e/rust/src/harness/driver.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Active compute-driver detection for tests with driver-specific assumptions.

/// Returns true and prints a skip notice when running against the kube driver.
///
/// Tests that depend on docker/podman host-network features (e.g.
/// `host.openshell.internal` reachability, sibling-container test servers)
/// can early-return when this is true.
pub fn skip_if_kube(reason: &str) -> bool {
if matches!(
std::env::var("OPENSHELL_E2E_DRIVER").as_deref(),
Ok("kubernetes")
) {
eprintln!("skipping on kubernetes driver: {reason}");
return true;
}
false
}
1 change: 1 addition & 0 deletions e2e/rust/src/harness/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
pub mod binary;
pub mod container;
pub mod driver;
pub mod gateway;
pub mod output;
pub mod port;
Expand Down
4 changes: 4 additions & 0 deletions e2e/rust/tests/forward_proxy_graphql_l7.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use std::io::Write;

use openshell_e2e::harness::container::ContainerHttpServer;
use openshell_e2e::harness::driver::skip_if_kube;
use openshell_e2e::harness::sandbox::SandboxGuard;
use tempfile::NamedTempFile;

Expand Down Expand Up @@ -131,6 +132,9 @@ network_policies:
#[tokio::test]
#[allow(clippy::too_many_lines)]
async fn graphql_l7_enforces_allow_and_deny_rules_on_forward_and_connect_paths() {
if skip_if_kube("uses host.openshell.internal to reach a sibling container") {
return;
}
let server = start_test_server().await.expect("start test server");
let policy = write_graphql_policy(&server.host, server.port).expect("write custom policy");
let policy_path = policy
Expand Down
7 changes: 7 additions & 0 deletions e2e/rust/tests/forward_proxy_l7_bypass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use std::io::Write;

use openshell_e2e::harness::container::ContainerHttpServer;
use openshell_e2e::harness::driver::skip_if_kube;
use openshell_e2e::harness::sandbox::SandboxGuard;
use tempfile::NamedTempFile;

Expand Down Expand Up @@ -98,6 +99,9 @@ network_policies:
/// GET /allowed should succeed — the L7 policy explicitly allows it.
#[tokio::test]
async fn forward_proxy_allows_l7_permitted_request() {
if skip_if_kube("uses host.openshell.internal to reach a sibling container") {
return;
}
let server = start_test_server().await.expect("start test server");
let policy =
write_policy_with_l7_rules(&server.host, server.port).expect("write custom policy");
Expand Down Expand Up @@ -138,6 +142,9 @@ except Exception as e:
/// POST /allowed should be denied — the L7 policy only allows GET.
#[tokio::test]
async fn forward_proxy_denies_l7_blocked_request() {
if skip_if_kube("uses host.openshell.internal to reach a sibling container") {
return;
}
let server = start_test_server().await.expect("start test server");
let policy =
write_policy_with_l7_rules(&server.host, server.port).expect("write custom policy");
Expand Down
10 changes: 10 additions & 0 deletions e2e/rust/tests/host_gateway_alias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::process::Stdio;
use std::sync::Mutex;

use openshell_e2e::harness::binary::openshell_cmd;
use openshell_e2e::harness::driver::skip_if_kube;
use openshell_e2e::harness::sandbox::SandboxGuard;
use tempfile::NamedTempFile;
use tokio::io::AsyncReadExt;
Expand Down Expand Up @@ -190,6 +191,9 @@ network_policies:

#[tokio::test]
async fn sandbox_reaches_host_openshell_internal_via_host_gateway_alias() {
if skip_if_kube("requires host.openshell.internal alias") {
return;
}
let server = HostServer::start(r#"{"message":"hello-from-host"}"#)
.await
.expect("start host echo server");
Expand Down Expand Up @@ -225,6 +229,9 @@ async fn sandbox_reaches_host_openshell_internal_via_host_gateway_alias() {

#[tokio::test]
async fn sandbox_inference_local_routes_to_host_openshell_internal() {
if skip_if_kube("requires host.openshell.internal alias") {
return;
}
let _inference_lock = INFERENCE_ROUTE_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
Expand Down Expand Up @@ -301,6 +308,9 @@ async fn sandbox_inference_local_routes_to_host_openshell_internal() {

#[tokio::test]
async fn inference_set_supports_no_verify_for_unreachable_endpoint() {
if skip_if_kube("uses host.openshell.internal as the unreachable target") {
return;
}
let _inference_lock = INFERENCE_ROUTE_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
Expand Down
Loading
Loading