-
Notifications
You must be signed in to change notification settings - Fork 982
feat(examples): add fake-driver-rs scriptable compute driver #2181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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 } | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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" |
| 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, | ||
| } |
There was a problem hiding this comment.
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.