Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
bdeef5c
CARRY: feat(odh): unify shared and managed E2E runs
Ygnas Oct 7, 2026
f557da2
CARRY: fix(odh): clean up managed namespaces after test exits
Ygnas Oct 8, 2026
a42a5a2
CARRY: fix(odh): align gateway values and streamline e2e runner
Ygnas Oct 12, 2026
ca18712
feat(gateway): add gRPC server reflection (#3161)
2000krysztof Oct 12, 2026
b1a28d5
Merge remote-tracking branch 'upstream/main'
moulalis Oct 12, 2026
e16a359
chore(deps): update registry.access.redhat.com/ubi9/nodejs-24-minimal…
red-hat-konflux[bot] Oct 13, 2026
4a0320b
CARRY: docs(openclaw): version sandbox image after OpenClaw
Bobbins228 Oct 12, 2026
ed9238d
CARRY: ci(openclaw): publish OpenClaw version tags
Bobbins228 Oct 13, 2026
7bbcd63
feat(rhoai): install OpenShell gateway and dashboard on OpenShift
varshaprasad96 Oct 13, 2026
f1a46d3
fix(rhoai): support administrator-selected namespaces
varshaprasad96 Oct 13, 2026
4644e04
docs(rhoai): remove unrelated cluster cleanup history
varshaprasad96 Oct 13, 2026
b99e4c2
feat(rhoai): harden installer configuration and verification
varshaprasad96 Oct 13, 2026
02d545f
chore(deps): refresh rpm lockfiles [SECURITY] (#100)
red-hat-konflux[bot] Oct 13, 2026
0b53089
chore(deps): refresh rpm lockfiles (#96)
red-hat-konflux[bot] Oct 13, 2026
99dc721
chore(deps): update registry.access.redhat.com/ubi9/nodejs-24-minimal…
red-hat-konflux[bot] Oct 14, 2026
2129e4b
Merge pull request #97 from Bobbins228/codex/rhaieng-7773-openclaw-ve…
varshaprasad96 Oct 14, 2026
635292b
fix(rhoai): close installer preflight gaps
varshaprasad96 Oct 14, 2026
3523fc0
chore(deps): refresh rpm lockfiles [SECURITY] (#104)
red-hat-konflux[bot] Oct 14, 2026
7f1f75f
Merge pull request #101 from opendatahub-io/feat/rhoai-dashboard-inst…
varshaprasad96 Oct 14, 2026
17bfa49
chore(deps): refresh rpm lockfiles [SECURITY] (#105)
red-hat-konflux[bot] Oct 15, 2026
9af2b58
chore(deps): refresh rpm lockfiles (#106)
red-hat-konflux[bot] Oct 15, 2026
775a8df
Merge pull request #89 from Ygnas/feat/unify-odh-e2e-runs
Ygnas Oct 15, 2026
ba5418a
CARRY: fix(odh): align e2e image ownership with pipeline user
Bobbins228 Oct 15, 2026
bc9be6b
chore(deps): refresh rpm lockfiles (#107)
red-hat-konflux[bot] Oct 16, 2026
79ea2f2
CARRY: ci(openclaw): derive image tag from upstream release
Bobbins228 Oct 16, 2026
217024d
Merge pull request #109 from Bobbins228/codex/odh-e2e-uid-1001
Bobbins228 Oct 16, 2026
7b93ea6
Merge pull request #110 from Bobbins228/ci/RHAIENG-7773-openclaw-tags…
varshaprasad96 Oct 19, 2026
ddbaa29
CARRY: test(odh): exclude tier1 cases and name xunit reports
Bobbins228 Oct 20, 2026
500132b
Merge pull request #113 from Bobbins228/codex/odh-e2e-tier-exclusions…
Bobbins228 Oct 20, 2026
f8533ec
chore(deps): refresh rpm lockfiles [SECURITY] (#115)
red-hat-konflux[bot] Oct 21, 2026
469e16d
chore(deps): refresh rpm lockfiles (#116)
red-hat-konflux[bot] Oct 21, 2026
83feba4
CARRY: test(odh): add gateway failover coverage
eviehoward Sep 18, 2026
4d46dd5
test(odh): automate HA failover deployment
eviehoward Oct 16, 2026
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
3 changes: 3 additions & 0 deletions .config/nextest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ slow-timeout = { period = "60s", terminate-after = 5 }
# Relative to the profile store dir (`e2e/rust/target/nextest/e2e-odh/`).
[profile.e2e-odh.junit]
path = "../../../../../results/e2e-odh.xml"
# Keep ignored destructive ODH tests visible to Jenkins as skipped rather than
# silently omitting them from the report.
report-skipped = "ignored"

# serial_test's in-process locks do not span nextest's test processes.
# Match names so this config also works without these test binaries.
Expand Down
46 changes: 46 additions & 0 deletions .github/workflows/rhoai-installer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 OpenShell contributors
# SPDX-License-Identifier: Apache-2.0

name: RHOAI Installer

on:
pull_request:
paths:
- "deploy/rhoai/**"
- ".github/workflows/rhoai-installer.yml"
- "pyproject.toml"
push:
branches: [main]
paths:
- "deploy/rhoai/**"
- ".github/workflows/rhoai-installer.yml"
- "pyproject.toml"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0
with:
version: "0.10.12"
python-version: "3.12"
- name: Installer regression tests
run: uv run --script --python 3.12 deploy/rhoai/test_install.py
- name: Authorization verifier tests
run: uv run --no-project --python 3.12 deploy/rhoai/test_verify.py
- name: Lint and formatting
run: |
uvx ruff==0.14.14 check deploy/rhoai
uvx ruff==0.14.14 format --check deploy/rhoai
2 changes: 2 additions & 0 deletions .tekton/odh-openshell-sandbox-openclaw-push.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ spec:
{"type": "npm", "path": "deploy/konflux/openclaw"},
{"type": "rpm", "path": "deploy/konflux/openclaw"}
]
- name: tag-openclaw-release
value: "true"
pipelineRef:
resolver: git
params:
Expand Down
60 changes: 0 additions & 60 deletions .tekton/odh-openshell-sandbox-openclaw-tag.yaml

This file was deleted.

15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ tokio = { version = "1.43", features = ["full"] }
# gRPC/Protobuf
tonic = "0.14"
tonic-types = "0.14"
tonic-reflection = "0.14"
tonic-prost = "0.14"
tonic-prost-build = "0.14"
prost = "0.14"
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@ The installer sets up the CLI and a local gateway. The default sandbox image is
- [Tutorials](https://docs.nvidia.com/openshell/latest/tutorials/first-network-policy): step-by-step policy and agent walkthroughs.
- [Prerelease and development builds](https://docs.nvidia.com/openshell/latest/about/installation#prerelease-and-development-builds): try an upcoming release or the latest commit on `main`.

### Test the gRPC API with grpcurl

The gateway serves the gRPC reflection v1 protocol. After starting a local
plaintext gateway, use `grpcurl` without checking out or supplying the proto
files:

```shell
grpcurl -plaintext localhost:18080 list
grpcurl -plaintext localhost:18080 describe openshell.v1.OpenShell
grpcurl -plaintext -d '{}' localhost:18080 openshell.v1.OpenShell/Health
```

The service list contains the public `openshell.v1.OpenShell` API. Reflection
does not advertise the gateway's internal compute-driver, credential-driver,
interceptor, or middleware services. For a TLS gateway, omit `-plaintext` and
supply the CA and client certificate options required by the deployment.

## Agent Skills

Install the public OpenShell skills for your coding agent:
Expand Down
1 change: 1 addition & 0 deletions crates/openshell-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ libc = "0.2"

# gRPC
tonic = { workspace = true, features = ["channel", "tls-native-roots"] }
tonic-reflection = { workspace = true }
prost = { workspace = true }
prost-reflect = { workspace = true }
prost-types = { workspace = true }
Expand Down
10 changes: 7 additions & 3 deletions crates/openshell-server/src/auth/oidc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ use tracing::{debug, error, info, warn};
/// These are structural bypasses for gRPC infrastructure that doesn't map to a
/// single RPC method. Per-method bypasses (e.g. `Health`) are declared at the
/// handler with `auth_mode: "unauthenticated"` in the proto annotation.
const UNAUTHENTICATED_PREFIXES: &[&str] = &["/grpc.reflection.", "/grpc.health."];
const UNAUTHENTICATED_PREFIXES: &[&str] =
&[crate::multiplex::REFLECTION_PATH_PREFIX, "/grpc.health."];

/// Returns `true` if the method needs no authentication at all.
pub fn is_unauthenticated_method(path: &str) -> bool {
Expand Down Expand Up @@ -1205,10 +1206,13 @@ mod tests {
#[test]
fn reflection_is_unauthenticated() {
assert!(is_unauthenticated_method(
"/grpc.reflection.v1.ServerReflection/ServerReflectionInfo"
));
assert!(!is_unauthenticated_method(
"/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo"
));
assert!(is_unauthenticated_method(
"/grpc.reflection.v1.ServerReflection/ServerReflectionInfo"
assert!(!is_unauthenticated_method(
"/grpc.reflection.v2.ServerReflection/ServerReflectionInfo"
));
}

Expand Down
69 changes: 69 additions & 0 deletions crates/openshell-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub(crate) mod policy_store;
mod provider_profile_sources;
mod provider_refresh;
mod readiness;
mod reflection;
mod sandbox_index;
mod sandbox_watch;
mod service_routing;
Expand Down Expand Up @@ -349,6 +350,9 @@ pub struct ServerState {
/// Gateway-wide gRPC request rate limiter shared by every multiplex path.
pub(crate) grpc_rate_limiter: Option<multiplex::GrpcRateLimiter>,

/// Immutable public reflection index and its per-query rate limiter.
pub(crate) reflection_service: reflection::GatewayReflectionServer,

/// Per-sandbox bound on extension credential minting, which resolves the
/// caller's effective policy on every request.
pub(crate) extension_mint_limiter: auth::extension_mint_limit::ExtensionMintLimiter,
Expand Down Expand Up @@ -426,6 +430,8 @@ impl ServerState {
compute.configure_ssh_identities(credentials.clone());
let peer_endpoint = derive_peer_endpoint(&config);
let grpc_rate_limiter = multiplex::GrpcRateLimiter::from_config(&config);
let reflection_service = reflection::build_gateway_reflection_service(&config)
.expect("compiled public gateway descriptors must be valid");
let admin_role = config
.oidc
.as_ref()
Expand Down Expand Up @@ -458,6 +464,7 @@ impl ServerState {
compute_driver_authenticator: None,
peer_authenticator: None,
grpc_rate_limiter,
reflection_service,
gateway_interceptors: None,
provider_profile_sources:
provider_profile_sources::ProviderProfileSources::with_default_sources(),
Expand Down Expand Up @@ -2219,6 +2226,68 @@ mod tests {
(listen_addr, shutdown_tx, handle, tls_dir)
}

async fn start_plaintext_gateway_listener()
-> (SocketAddr, watch::Sender<bool>, tokio::task::JoinHandle<()>) {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("failed to bind test listener");
let listen_addr = listener.local_addr().expect("failed to read local addr");
let state = test_state(listen_addr, false).await;
let service = MultiplexService::new(state);
let (shutdown_tx, shutdown_rx) = watch::channel(false);
let handle = tokio::spawn(serve_gateway_listener(
BoundGatewayListener {
listener,
address: listen_addr,
},
service,
None,
false,
shutdown_rx,
));
(listen_addr, shutdown_tx, handle)
}

#[tokio::test]
async fn production_gateway_listener_serves_reflection() {
use tonic_reflection::pb::v1::{
ServerReflectionRequest, server_reflection_client::ServerReflectionClient,
server_reflection_request::MessageRequest, server_reflection_response::MessageResponse,
};

let (addr, shutdown, handle) = start_plaintext_gateway_listener().await;
let channel = tonic::transport::Channel::from_shared(format!("http://{addr}"))
.unwrap()
.connect()
.await
.unwrap();
let mut client = ServerReflectionClient::new(channel);
let request = ServerReflectionRequest {
host: String::new(),
message_request: Some(MessageRequest::ListServices(String::new())),
};
let mut responses = client
.server_reflection_info(tokio_stream::iter([request]))
.await
.unwrap()
.into_inner();
let response = responses.message().await.unwrap().unwrap();
let Some(MessageResponse::ListServicesResponse(services)) = response.message_response
else {
panic!("expected a reflection list-services response");
};
assert_eq!(
services
.service
.iter()
.map(|service| service.name.as_str())
.collect::<Vec<_>>(),
["openshell.v1.OpenShell"]
);

stop_listener(shutdown, handle).await;
}

async fn send_plain_http(addr: SocketAddr, request: String) -> String {
let connect_addr: SocketAddr = format!("127.0.0.1:{}", addr.port())
.parse()
Expand Down
Loading