Skip to content
Merged
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
24 changes: 1 addition & 23 deletions .github/workflows/branch-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,30 +35,8 @@ jobs:
component: gateway
platform: linux/arm64

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

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/arm64

e2e:
needs: [pr_metadata, build-gateway, build-cluster, build-supervisor]
needs: [pr_metadata, build-gateway]
if: needs.pr_metadata.outputs.should_run == 'true'
permissions:
contents: read
Expand Down
27 changes: 1 addition & 26 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,9 @@ jobs:
matrix:
include:
- suite: python
cluster: e2e-python
port: "8080"
cmd: "mise run --no-deps --skip-deps e2e:python"
- suite: rust
cluster: e2e-rust
port: "8081"
cmd: "mise run --no-deps --skip-deps e2e:rust"
- suite: gateway-resume
cluster: e2e-resume
port: "8082"
cmd: "cargo test --manifest-path e2e/rust/Cargo.toml --features e2e --test gateway_resume"
container:
image: ghcr.io/nvidia/openshell/ci:latest
credentials:
Expand All @@ -46,6 +38,7 @@ jobs:
options: --privileged
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /home/runner/_work:/home/runner/_work
env:
MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
IMAGE_TAG: ${{ inputs.image-tag }}
Expand All @@ -54,37 +47,19 @@ jobs:
OPENSHELL_REGISTRY_NAMESPACE: nvidia/openshell
OPENSHELL_REGISTRY_USERNAME: ${{ github.actor }}
OPENSHELL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
OPENSHELL_GATEWAY: ${{ matrix.cluster }}
steps:
- uses: actions/checkout@v6

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

- name: Pull cluster image
run: docker pull ghcr.io/nvidia/openshell/cluster:${{ inputs.image-tag }}

- name: Install Python dependencies and generate protobuf stubs
if: matrix.suite == 'python'
run: uv sync --frozen && mise run --no-deps python:proto

- name: Build Rust CLI
if: matrix.suite != 'python'
run: cargo build -p openshell-cli --features openshell-core/dev-settings

- name: Install SSH client
if: matrix.suite != 'python'
run: apt-get update && apt-get install -y --no-install-recommends openssh-client && rm -rf /var/lib/apt/lists/*

- name: Bootstrap cluster
env:
GATEWAY_HOST: host.docker.internal
GATEWAY_PORT: ${{ matrix.port }}
CLUSTER_NAME: ${{ matrix.cluster }}
SKIP_IMAGE_PUSH: "1"
SKIP_CLUSTER_IMAGE_BUILD: "1"
OPENSHELL_CLUSTER_IMAGE: ghcr.io/nvidia/openshell/cluster:${{ inputs.image-tag }}
run: mise run --no-deps --skip-deps cluster

- name: Run tests
run: ${{ matrix.cmd }}
18 changes: 14 additions & 4 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

```bash
mise run test # Rust + Python unit tests
mise run e2e # End-to-end tests (requires a running cluster)
mise run e2e # End-to-end tests (starts a Docker-backed gateway)
mise run ci # Everything: lint, compile checks, and tests
```

Expand Down Expand Up @@ -72,8 +72,17 @@ mise run test:python # uv run pytest python/

## E2E Tests

E2E tests run against a live cluster. `mise run e2e` deploys changed components
before running the suite.
E2E tests run against a live gateway. By default, `mise run e2e` starts an
ephemeral standalone gateway with the Docker compute driver, runs the suite,
and cleans it up afterward. To run the suite against an existing plaintext
gateway, set `OPENSHELL_GATEWAY_ENDPOINT`:

```bash
OPENSHELL_GATEWAY_ENDPOINT=http://127.0.0.1:18080 mise run e2e
```

Raw endpoint mode is HTTP-only. Use a named gateway config when a gateway
requires mTLS.

### Python E2E (`e2e/python/`)

Expand Down Expand Up @@ -125,7 +134,7 @@ def test_multiply(sandbox):

| Fixture | Scope | Purpose |
|---|---|---|
| `sandbox_client` | session | gRPC client connected to the active cluster |
| `sandbox_client` | session | gRPC client connected to the active gateway |
| `sandbox` | function | Factory returning a `Sandbox` context manager |
| `inference_client` | session | Client for managing inference routes |
| `mock_inference_route` | session | Creates a mock OpenAI-protocol route for tests |
Expand Down Expand Up @@ -168,3 +177,4 @@ The harness (`e2e/rust/src/harness/`) provides:
| Variable | Purpose |
|---|---|
| `OPENSHELL_GATEWAY` | Override active gateway name for E2E tests |
| `OPENSHELL_GATEWAY_ENDPOINT` | Run E2E tests against an existing plaintext HTTP gateway endpoint |
45 changes: 39 additions & 6 deletions crates/openshell-driver-docker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,9 @@ impl DockerComputeDriver {
}
let network_name = docker_network_name(docker_config);
let bridge_gateway_ip = ensure_bridge_network(&docker, &network_name).await?;
let gateway_route = docker_gateway_route(&info, bridge_gateway_ip, gateway_port);
let host_gateway_ip = parse_optional_host_gateway_ip(&config.host_gateway_ip)?;
let gateway_route =
docker_gateway_route(&info, bridge_gateway_ip, gateway_port, host_gateway_ip);
let grpc_endpoint = docker_container_openshell_endpoint(
&config.grpc_endpoint,
HOST_OPENSHELL_INTERNAL,
Expand Down Expand Up @@ -797,11 +799,21 @@ impl ComputeDriver for DockerComputeDriver {
let request = request.into_inner();
require_sandbox_identifier(&request.sandbox_id, &request.sandbox_name)?;

Ok(Response::new(DeleteSandboxResponse {
deleted: self
.delete_sandbox_inner(&request.sandbox_id, &request.sandbox_name)
.await?,
}))
let event_sandbox_id = request.sandbox_id.clone();
let deleted = self
.delete_sandbox_inner(&request.sandbox_id, &request.sandbox_name)
.await?;
if deleted && !event_sandbox_id.is_empty() {
let _ = self.events.send(WatchSandboxesEvent {
payload: Some(watch_sandboxes_event::Payload::Deleted(
WatchSandboxesDeletedEvent {
sandbox_id: event_sandbox_id,
},
)),
});
}

Ok(Response::new(DeleteSandboxResponse { deleted }))
}

async fn watch_sandboxes(
Expand Down Expand Up @@ -1064,11 +1076,32 @@ fn docker_network_name(config: &DockerComputeConfig) -> String {
name.to_string()
}

fn parse_optional_host_gateway_ip(value: &str) -> CoreResult<Option<IpAddr>> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Ok(None);
}

trimmed.parse().map(Some).map_err(|err| {
Error::config(format!(
"invalid OPENSHELL_HOST_GATEWAY_IP value '{trimmed}': {err}"
))
})
}

fn docker_gateway_route(
info: &SystemInfo,
bridge_gateway_ip: IpAddr,
port: u16,
host_gateway_ip: Option<IpAddr>,
) -> DockerGatewayRoute {
if let Some(host_alias_ip) = host_gateway_ip {
return DockerGatewayRoute::Bridge {
bind_address: SocketAddr::new(host_alias_ip, port),
host_alias_ip,
};
}

if is_docker_desktop(info) {
DockerGatewayRoute::HostGateway
} else {
Expand Down
47 changes: 47 additions & 0 deletions crates/openshell-driver-docker/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ fn docker_gateway_route_uses_host_gateway_for_docker_desktop() {
&info,
IpAddr::V4(Ipv4Addr::new(172, 18, 0, 1)),
DEFAULT_SERVER_PORT,
None,
),
DockerGatewayRoute::HostGateway
);
Expand All @@ -174,6 +175,7 @@ fn docker_gateway_route_uses_bridge_gateway_for_linux_docker() {
&info,
IpAddr::V4(Ipv4Addr::new(172, 18, 0, 1)),
DEFAULT_SERVER_PORT,
None,
);

assert_eq!(
Expand All @@ -192,6 +194,51 @@ fn docker_gateway_route_uses_bridge_gateway_for_linux_docker() {
);
}

#[test]
fn docker_gateway_route_prefers_configured_host_gateway_ip() {
let info = SystemInfo {
operating_system: Some("Ubuntu 24.04 LTS".to_string()),
..Default::default()
};

let route = docker_gateway_route(
&info,
IpAddr::V4(Ipv4Addr::new(172, 18, 0, 1)),
DEFAULT_SERVER_PORT,
Some(IpAddr::V4(Ipv4Addr::new(172, 20, 0, 4))),
);

assert_eq!(
route,
DockerGatewayRoute::Bridge {
bind_address: "172.20.0.4:8080".parse().unwrap(),
host_alias_ip: IpAddr::V4(Ipv4Addr::new(172, 20, 0, 4)),
}
);
assert_eq!(
docker_extra_hosts(&route),
vec![
"host.docker.internal:172.20.0.4".to_string(),
"host.openshell.internal:172.20.0.4".to_string()
]
);
}

#[test]
fn parse_optional_host_gateway_ip_rejects_invalid_values() {
assert_eq!(parse_optional_host_gateway_ip("").unwrap(), None);
assert_eq!(
parse_optional_host_gateway_ip("172.20.0.4").unwrap(),
Some(IpAddr::V4(Ipv4Addr::new(172, 20, 0, 4)))
);
assert!(
parse_optional_host_gateway_ip("not-an-ip")
.unwrap_err()
.to_string()
.contains("OPENSHELL_HOST_GATEWAY_IP")
);
}

#[test]
fn parse_cpu_limit_supports_cores_and_millicores() {
assert_eq!(parse_cpu_limit("250m").unwrap(), Some(250_000_000));
Expand Down
9 changes: 9 additions & 0 deletions e2e/python/test_security_tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ def _xdg_config_home() -> pathlib.Path:


def _resolve_cluster_name() -> str:
if os.environ.get("OPENSHELL_GATEWAY_ENDPOINT"):
return os.environ.get("OPENSHELL_GATEWAY", "openshell-e2e-endpoint")
env_cluster = os.environ.get("OPENSHELL_GATEWAY")
if env_cluster:
return env_cluster
Expand All @@ -44,6 +46,13 @@ def _resolve_cluster_name() -> str:


def _cluster_metadata(cluster_name: str) -> dict:
endpoint = os.environ.get("OPENSHELL_GATEWAY_ENDPOINT")
if endpoint:
return {
"name": cluster_name,
"gateway_endpoint": endpoint,
"auth_mode": "plaintext",
}
metadata_path = (
_xdg_config_home() / "openshell" / "gateways" / cluster_name / "metadata.json"
)
Expand Down
Loading
Loading