Skip to content
Draft
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
18 changes: 18 additions & 0 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

[workspace]
resolver = "2"
members = ["crates/*"]
members = ["crates/*", "examples/fake-driver-rs"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally examples wouldn't need to be considered in our main cargo fie.


[workspace.package]
version = "0.0.0"
Expand Down
33 changes: 33 additions & 0 deletions examples/fake-driver-rs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

[package]
name = "fake-driver-rs"
description = "Scriptable fake compute driver for testing gateway integration"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true

[[bin]]
name = "fake-driver-rs"
path = "src/main.rs"

[dependencies]
openshell-core = { path = "../../crates/openshell-core", default-features = false }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As @krishicks points out, this may be "breaking/bending" the external driver contract a bit. Is there a way to depend on the crate the same way that an external driver would instead?


tokio = { workspace = true }
tokio-stream = { workspace = true }
tonic = { workspace = true, features = ["transport"] }
futures = { workspace = true }
serde = { workspace = true }
toml = { workspace = true }
clap = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
thiserror = { workspace = true }
miette = { workspace = true }

[lints]
workspace = true
75 changes: 75 additions & 0 deletions examples/fake-driver-rs/scenarios/happy-path.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Happy-path scenario: all RPCs succeed.
#
# Each [[rpc_name]] block is one entry in the scripted sequence. The driver
# advances through the list on each call; when the list is exhausted the last
# entry repeats (with a warning logged).
#
# To return a gRPC error instead of a success response, add:
#
# [rpc_name.error]
# code = "NOT_FOUND"
# message = "sandbox not found"

[[validate_sandbox_create]]
# success — no error field

[[create_sandbox]]
# success — no error field

[[get_sandbox]]
[get_sandbox.sandbox]
id = "sandbox-abc123"
name = "test-sandbox"
namespace = "default"

[get_sandbox.sandbox.status]
sandbox_name = "test-sandbox"
instance_id = "instance-1"
agent_fd = "127.0.0.1:12345"
sandbox_fd = "127.0.0.1:12346"

[[list_sandboxes]]
# Empty list on first call.
sandboxes = []

[[list_sandboxes]]
# Populated list on subsequent calls (last entry repeats).
[[list_sandboxes.sandboxes]]
id = "sandbox-abc123"
name = "test-sandbox"
namespace = "default"

[list_sandboxes.sandboxes.status]
sandbox_name = "test-sandbox"
instance_id = "instance-1"
agent_fd = "127.0.0.1:12345"
sandbox_fd = "127.0.0.1:12346"

[[stop_sandbox]]
# success — no error field

[[delete_sandbox]]
deleted = true

# WatchSandboxes streams these events in order, then closes.
[watch_sandboxes]
delay_ms = 100

[[watch_sandboxes.events]]
[watch_sandboxes.events.sandbox]
id = "sandbox-abc123"
name = "test-sandbox"
namespace = "default"

[watch_sandboxes.events.sandbox.status]
sandbox_name = "test-sandbox"
instance_id = "instance-1"
agent_fd = "127.0.0.1:12345"
sandbox_fd = "127.0.0.1:12346"

[[watch_sandboxes.events]]
[watch_sandboxes.events.deleted]
sandbox_id = "sandbox-abc123"
174 changes: 174 additions & 0 deletions examples/fake-driver-rs/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! TOML configuration types for the fake driver's scripted response sequences.
//!
//! Each RPC has an ordered list of entries. The driver advances through the list
//! on each call. When the sequence is exhausted the last entry is repeated and a
//! warning is logged.

use serde::Deserialize;

/// Top-level configuration loaded from the `--config` file.
#[derive(Debug, Default, Deserialize)]
pub struct Config {
#[serde(default)]
pub validate_sandbox_create: Vec<ValidateSandboxCreateEntry>,
#[serde(default)]
pub create_sandbox: Vec<SimpleEntry>,
#[serde(default)]
pub get_sandbox: Vec<GetSandboxEntry>,
#[serde(default)]
pub list_sandboxes: Vec<ListSandboxesEntry>,
#[serde(default)]
pub stop_sandbox: Vec<SimpleEntry>,
#[serde(default)]
pub delete_sandbox: Vec<DeleteSandboxEntry>,
#[serde(default)]
pub watch_sandboxes: WatchSandboxesConfig,
}

/// A gRPC error to return in place of a success response.
#[derive(Debug, Deserialize)]
pub struct GrpcError {
/// gRPC status code name, e.g. `"NOT_FOUND"`, `"INVALID_ARGUMENT"`.
pub code: String,
pub message: String,
}

impl GrpcError {
pub fn to_status(&self) -> tonic::Status {
let code = match self.code.to_uppercase().as_str() {
"CANCELLED" => tonic::Code::Cancelled,
"INVALID_ARGUMENT" => tonic::Code::InvalidArgument,
"DEADLINE_EXCEEDED" => tonic::Code::DeadlineExceeded,
"NOT_FOUND" => tonic::Code::NotFound,
"ALREADY_EXISTS" => tonic::Code::AlreadyExists,
"PERMISSION_DENIED" => tonic::Code::PermissionDenied,
"RESOURCE_EXHAUSTED" => tonic::Code::ResourceExhausted,
"FAILED_PRECONDITION" => tonic::Code::FailedPrecondition,
"ABORTED" => tonic::Code::Aborted,
"OUT_OF_RANGE" => tonic::Code::OutOfRange,
"UNIMPLEMENTED" => tonic::Code::Unimplemented,
"INTERNAL" => tonic::Code::Internal,
"UNAVAILABLE" => tonic::Code::Unavailable,
"DATA_LOSS" => tonic::Code::DataLoss,
"UNAUTHENTICATED" => tonic::Code::Unauthenticated,
_ => tonic::Code::Unknown,
};
tonic::Status::new(code, &self.message)
}
}

/// Entry for RPCs that return an empty success response.
///
/// Omit `error` for success; set `error` to return a gRPC status error.
#[derive(Debug, Default, Deserialize)]
pub struct SimpleEntry {
pub error: Option<GrpcError>,
}

/// Entry for `ValidateSandboxCreate`. Same shape as `SimpleEntry` but kept
/// distinct so the config section name is self-documenting.
#[derive(Debug, Default, Deserialize)]
pub struct ValidateSandboxCreateEntry {
pub error: Option<GrpcError>,
}

/// Entry for `GetSandbox`.
#[derive(Debug, Default, Deserialize)]
pub struct GetSandboxEntry {
pub error: Option<GrpcError>,
/// The sandbox to return on success. If absent the RPC returns `NOT_FOUND`.
pub sandbox: Option<SandboxConfig>,
}

/// Entry for `ListSandboxes`.
#[derive(Debug, Default, Deserialize)]
pub struct ListSandboxesEntry {
pub error: Option<GrpcError>,
#[serde(default)]
pub sandboxes: Vec<SandboxConfig>,
}

/// Entry for `DeleteSandbox`.
#[derive(Debug, Default, Deserialize)]
pub struct DeleteSandboxEntry {
pub error: Option<GrpcError>,
/// Whether a resource was deleted. Defaults to `true`.
#[serde(default = "default_deleted")]
pub deleted: bool,
}

fn default_deleted() -> bool {
true
}

/// Simplified sandbox descriptor used in scripted responses.
#[derive(Debug, Default, Deserialize)]
pub struct SandboxConfig {
#[serde(default)]
pub id: String,
#[serde(default)]
pub name: String,
#[serde(default)]
pub namespace: String,
pub status: Option<SandboxStatusConfig>,
}

/// Simplified sandbox status used in scripted responses.
#[derive(Debug, Default, Deserialize)]
pub struct SandboxStatusConfig {
#[serde(default)]
pub sandbox_name: String,
#[serde(default)]
pub instance_id: String,
/// Address the agent service is reachable at (e.g. `"127.0.0.1:12345"`).
#[serde(default)]
pub agent_fd: String,
/// Address the sandbox supervisor service is reachable at.
#[serde(default)]
pub sandbox_fd: String,
}

/// Configuration for the `WatchSandboxes` streaming RPC.
///
/// A single flat sequence of events is emitted in order with `delay_ms`
/// between each. The stream closes after the last event.
#[derive(Debug, Deserialize)]
pub struct WatchSandboxesConfig {
/// Milliseconds to wait between emitting consecutive events.
#[serde(default = "default_delay_ms")]
pub delay_ms: u64,
#[serde(default)]
pub events: Vec<WatchEventConfig>,
}

fn default_delay_ms() -> u64 {
100
}

impl Default for WatchSandboxesConfig {
fn default() -> Self {
Self {
delay_ms: default_delay_ms(),
events: Vec::new(),
}
}
}

/// A single event emitted on the `WatchSandboxes` stream.
///
/// Exactly one of `sandbox` or `deleted` should be set.
#[derive(Debug, Deserialize)]
pub struct WatchEventConfig {
/// Emit a sandbox-updated event with the given snapshot.
pub sandbox: Option<SandboxConfig>,
/// Emit a sandbox-deleted event with the given sandbox ID.
pub deleted: Option<DeletedEventConfig>,
}

#[derive(Debug, Deserialize)]
pub struct DeletedEventConfig {
pub sandbox_id: String,
}
Loading
Loading