|
| 1 | +# OpenShell SDK for Go |
| 2 | + |
| 3 | +[](https://pkg.go.dev/github.com/NVIDIA/OpenShell/sdk/go) |
| 4 | +[](../../LICENSE) |
| 5 | + |
| 6 | +> [!IMPORTANT] |
| 7 | +> **[Read the full documentation](https://ro14nd.de/openshell-sdk-go/)** for guides, API reference with gRPC mapping, and testing patterns. |
| 8 | +
|
| 9 | +A Go SDK for interacting with [OpenShell](https://github.com/NVIDIA/OpenShell) |
| 10 | +servers, providing idiomatic Go bindings for shell session management, command |
| 11 | +execution, provider configuration, and service exposure. |
| 12 | + |
| 13 | +## Why a Go SDK? |
| 14 | + |
| 15 | +Go is the language of the Kubernetes ecosystem. If you want to build an |
| 16 | +operator, controller, or any automation that manages OpenShell resources as |
| 17 | +native Kubernetes objects, you need a Go client. |
| 18 | + |
| 19 | +This SDK is modeled after |
| 20 | +[`k8s.io/client-go`](https://github.com/kubernetes/client-go), the standard |
| 21 | +Kubernetes client library that every Go operator developer already knows. The |
| 22 | +patterns will look familiar: |
| 23 | + |
| 24 | +- **Typed sub-clients per resource**: `client.Sandboxes()`, `client.Providers()`, |
| 25 | + `client.Exec()`, just like `clientset.CoreV1().Pods()` |
| 26 | +- **Domain types separated from wire formats**: clean Go structs in a `types` |
| 27 | + package, no proto leakage into the public API (like `k8s.io/api`) |
| 28 | +- **Watch primitives**: channel-based watchers with `ResultChan()` and `Stop()`, |
| 29 | + identical to `watch.Interface` in client-go |
| 30 | +- **Functional options**: variadic option patterns for list filtering, |
| 31 | + pagination, and watch configuration |
| 32 | +- **Composable auth with token refresh**: wraps `oauth2.TokenSource` for |
| 33 | + automatic token caching and coalesced refresh, following the k8s client-go |
| 34 | + `cachingTokenSource` pattern |
| 35 | +- **Fake client for testing**: an in-memory implementation of the full client |
| 36 | + interface (like `k8s.io/client-go/kubernetes/fake`), so operators can be tested |
| 37 | + without a real gateway |
| 38 | + |
| 39 | +## Quick Start |
| 40 | + |
| 41 | +```go |
| 42 | +import v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1" |
| 43 | + |
| 44 | +// Connect to a gateway |
| 45 | +client, err := v1.NewClient(v1.Config{ |
| 46 | + Address: "gateway.example.com:443", |
| 47 | + Auth: v1.StaticToken("my-token"), |
| 48 | +}) |
| 49 | +if err != nil { |
| 50 | + log.Fatal(err) |
| 51 | +} |
| 52 | +defer client.Close() |
| 53 | + |
| 54 | +// Create a sandbox and wait until it's ready |
| 55 | +sandbox, err := client.Sandboxes().Create(ctx, "my-sandbox", &v1.SandboxSpec{ |
| 56 | + Template: &v1.SandboxTemplate{Image: "python:3.12"}, |
| 57 | +}, nil) |
| 58 | +if err != nil { |
| 59 | + log.Fatal(err) |
| 60 | +} |
| 61 | +sandbox, err = client.Sandboxes().WaitReady(ctx, sandbox.Name) |
| 62 | +if err != nil { |
| 63 | + log.Fatal(err) |
| 64 | +} |
| 65 | + |
| 66 | +// Run a command |
| 67 | +result, err := client.Exec().Run(ctx, sandbox.Name, |
| 68 | + []string{"python3", "-c", "print('hello from sandbox')"}, |
| 69 | + v1.ExecOptions{}, |
| 70 | +) |
| 71 | +if err != nil { |
| 72 | + log.Fatal(err) |
| 73 | +} |
| 74 | +fmt.Println(string(result.Stdout)) |
| 75 | +``` |
| 76 | + |
| 77 | +### With automatic token refresh |
| 78 | + |
| 79 | +For OIDC gateways, use `RefreshableToken` to wrap any `oauth2.TokenSource` with |
| 80 | +automatic caching and coalesced refresh: |
| 81 | + |
| 82 | +```go |
| 83 | +import "golang.org/x/oauth2" |
| 84 | + |
| 85 | +tokenSource := oauth2Config.TokenSource(ctx, initialToken) |
| 86 | +auth, err := v1.RefreshableToken(tokenSource, |
| 87 | + v1.WithLeeway(30*time.Second), |
| 88 | +) |
| 89 | +if err != nil { |
| 90 | + log.Fatal(err) |
| 91 | +} |
| 92 | +client, err := v1.NewClient(v1.Config{ |
| 93 | + Address: "gateway.example.com:443", |
| 94 | + Auth: auth, |
| 95 | +}) |
| 96 | +if err != nil { |
| 97 | + log.Fatal(err) |
| 98 | +} |
| 99 | +defer client.Close() |
| 100 | +``` |
| 101 | + |
| 102 | +Concurrent callers share a single refresh call. If the token source fails, the |
| 103 | +SDK falls back to the cached token with a logged warning. See the |
| 104 | +[Auth](https://ro14nd.de/openshell-sdk-go/api/auth.html) docs for details. |
| 105 | + |
| 106 | +### With edge proxy headers |
| 107 | + |
| 108 | +When a gateway sits behind a zero-trust reverse proxy, use `WithExtraHeaders` to |
| 109 | +attach proxy-specific headers alongside standard auth: |
| 110 | + |
| 111 | +```go |
| 112 | +base := v1.StaticToken("my-gateway-token") |
| 113 | +auth, err := v1.WithExtraHeaders(base, map[string]string{ |
| 114 | + "x-proxy-auth": "proxy-secret", |
| 115 | +}) |
| 116 | +if err != nil { |
| 117 | + log.Fatal(err) |
| 118 | +} |
| 119 | +client, err := v1.NewClient(v1.Config{ |
| 120 | + Address: "gateway.example.com:443", |
| 121 | + Auth: auth, |
| 122 | +}) |
| 123 | +``` |
| 124 | + |
| 125 | +For Cloudflare Access, use the convenience constructor in the `edge` package: |
| 126 | + |
| 127 | +```go |
| 128 | +import "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/edge" |
| 129 | + |
| 130 | +auth, err := edge.CloudflareAccess(base, os.Getenv("CF_ACCESS_TOKEN")) |
| 131 | +``` |
| 132 | + |
| 133 | +For gRPC behind edge proxies that reject HTTP/2, use the WebSocket tunnel: |
| 134 | + |
| 135 | +```go |
| 136 | +tunnel, err := edge.NewTunnelProxy( |
| 137 | + "wss://gateway.example.com/ws", |
| 138 | + os.Getenv("CF_ACCESS_TOKEN"), |
| 139 | +) |
| 140 | +if err != nil { |
| 141 | + log.Fatal(err) |
| 142 | +} |
| 143 | +defer tunnel.Close() |
| 144 | + |
| 145 | +client, err := v1.NewClient(v1.Config{ |
| 146 | + Address: tunnel.Addr(), |
| 147 | + Auth: v1.StaticToken("my-token"), |
| 148 | + TLS: &v1.TLSConfig{Insecure: true}, // local tunnel, no TLS |
| 149 | +}) |
| 150 | +``` |
| 151 | + |
| 152 | +### OIDC Login |
| 153 | + |
| 154 | +The `oidc` package provides gateway-aware OIDC authentication with browser, |
| 155 | +keyboard, device code, and client credentials flows: |
| 156 | + |
| 157 | +```go |
| 158 | +import "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/oidc" |
| 159 | + |
| 160 | +// Gateway-aware login: reads OIDC config from gateway metadata |
| 161 | +token, err := oidc.Login(ctx, "my-gateway") |
| 162 | +if err != nil { |
| 163 | + log.Fatal(err) |
| 164 | +} |
| 165 | + |
| 166 | +// Use the token with the SDK client |
| 167 | +client, err := v1.NewClient(v1.Config{ |
| 168 | + Address: "gateway.example.com:443", |
| 169 | + Auth: v1.StaticToken(token.AccessToken), |
| 170 | +}) |
| 171 | +``` |
| 172 | + |
| 173 | +For headless environments, use the device code flow: |
| 174 | + |
| 175 | +```go |
| 176 | +token, err := oidc.DeviceLogin(ctx, |
| 177 | + oidc.WithIssuer("https://auth.example.com"), |
| 178 | + oidc.WithClientID("my-app"), |
| 179 | +) |
| 180 | +``` |
| 181 | + |
| 182 | +For service accounts, use client credentials: |
| 183 | + |
| 184 | +```go |
| 185 | +token, err := oidc.ClientCredentials(ctx, |
| 186 | + oidc.WithGateway("my-gateway"), |
| 187 | + oidc.WithClientSecret("service-secret"), |
| 188 | +) |
| 189 | +``` |
| 190 | + |
| 191 | +See the [oidc package docs](https://pkg.go.dev/github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/oidc) for all options and flows. |
| 192 | + |
| 193 | +See the [Getting Started](https://ro14nd.de/openshell-sdk-go/getting-started.html) guide for the full walkthrough. |
| 194 | + |
| 195 | +### Inference Route Management |
| 196 | + |
| 197 | +Configure how inference requests are routed for a workspace: |
| 198 | + |
| 199 | +```go |
| 200 | +// Set an inference route |
| 201 | +route, err := client.Inference().SetRoute(ctx, "my-workspace", &v1.InferenceRouteConfig{ |
| 202 | + ProviderName: "openai", |
| 203 | + ModelID: "gpt-4", |
| 204 | + RouteName: "", // empty string = default route |
| 205 | + TimeoutSecs: 120, |
| 206 | +}) |
| 207 | +if err != nil { |
| 208 | + log.Fatal(err) |
| 209 | +} |
| 210 | +fmt.Printf("Route v%d: %s/%s\n", route.Version, route.ProviderName, route.ModelID) |
| 211 | + |
| 212 | +// Retrieve the route |
| 213 | +route, err = client.Inference().GetRoute(ctx, "my-workspace", "") |
| 214 | +if err != nil { |
| 215 | + log.Fatal(err) |
| 216 | +} |
| 217 | + |
| 218 | +// Delete the route |
| 219 | +err = client.Inference().DeleteRoute(ctx, "my-workspace", "") |
| 220 | +if err != nil { |
| 221 | + log.Fatal(err) |
| 222 | +} |
| 223 | +``` |
| 224 | + |
| 225 | +## Architecture |
| 226 | + |
| 227 | +``` |
| 228 | +Client |
| 229 | + ├── Sandboxes() → SandboxInterface (create, get, list, delete, watch, wait, logs) |
| 230 | + ├── Exec() → ExecInterface (run, stream, interactive) |
| 231 | + ├── Files() → FileInterface (upload, download) |
| 232 | + ├── Health() → HealthInterface (health check, gateway info, current user) |
| 233 | + ├── Services() → ServiceInterface (expose, get, list, delete) |
| 234 | + ├── Providers() → ProviderInterface (CRUD + ensure) |
| 235 | + │ ├── Profiles() → ProfileInterface (list, get, import, update, lint, delete) |
| 236 | + │ └── Refresh() → RefreshInterface (configure, status, rotate, delete) |
| 237 | + ├── Workspaces() → WorkspaceInterface (create, get, list, delete, members) |
| 238 | + ├── Inference() → InferenceInterface (set, get, delete inference routes) |
| 239 | + └── Policy() → PolicyInterface (draft review, approve, reject, merge, status) |
| 240 | +``` |
| 241 | + |
| 242 | +All domain types live in `openshell/v1/types/`. Proto-to-Go conversions happen in |
| 243 | +an internal converter layer. The public API surface uses type aliases so |
| 244 | +consumers import a single package. See the [Architecture](https://ro14nd.de/openshell-sdk-go/architecture.html) overview for details. |
| 245 | + |
| 246 | +## Features |
| 247 | + |
| 248 | +| Feature | Interface | Docs | |
| 249 | +|---------|-----------|------| |
| 250 | +| Sandbox lifecycle (create, get, list, delete, watch, wait) | `SandboxInterface` | [Sandboxes](https://ro14nd.de/openshell-sdk-go/api/sandboxes.html) | |
| 251 | +| Command execution (collected, streamed, interactive PTY) | `ExecInterface` | [Exec](https://ro14nd.de/openshell-sdk-go/api/exec.html) | |
| 252 | +| Provider management (CRUD + idempotent ensure) | `ProviderInterface` | [Providers](https://ro14nd.de/openshell-sdk-go/api/providers.html) | |
| 253 | +| Provider profiles (list, import, lint, update) | `ProfileInterface` | [Profiles](https://ro14nd.de/openshell-sdk-go/api/profiles.html) | |
| 254 | +| Credential refresh (configure, rotate, status) | `RefreshInterface` | [Refresh](https://ro14nd.de/openshell-sdk-go/api/refresh.html) | |
| 255 | +| Service exposure (expose, list, delete) | `ServiceInterface` | [Services](https://ro14nd.de/openshell-sdk-go/api/services.html) | |
| 256 | +| File transfer (upload, download) | `FileInterface` | [Files](https://ro14nd.de/openshell-sdk-go/api/files.html) | |
| 257 | +| Policy management (draft review, approve, reject, merge, global policy) | `PolicyInterface` | [Policy](https://ro14nd.de/openshell-sdk-go/api/policy.html) | |
| 258 | +| Sandbox logs (streaming retrieval) | `SandboxInterface` | [Sandboxes](https://ro14nd.de/openshell-sdk-go/api/sandboxes.html) | |
| 259 | +| Workspace management (create, get, list, delete, members) | `WorkspaceInterface` | [Workspaces](https://ro14nd.de/openshell-sdk-go/api/workspaces.html) | |
| 260 | +| Inference route management (set, get, delete) | `InferenceInterface` | [Inference](https://ro14nd.de/openshell-sdk-go/api/inference.html) | |
| 261 | +| Gateway info and current user identity | `HealthInterface` | [Health](https://ro14nd.de/openshell-sdk-go/api/health.html) | |
| 262 | +| Health checking | `HealthInterface` | [Health](https://ro14nd.de/openshell-sdk-go/api/health.html) | |
| 263 | +| SSH tunneling and TCP forwarding | `SSHInterface`, `TCPInterface` | [SSH](https://ro14nd.de/openshell-sdk-go/api/ssh.html), [TCP](https://ro14nd.de/openshell-sdk-go/api/tcp.html) | |
| 264 | +| Auth: static token, refreshable token (oauth2.TokenSource) | `AuthProvider` | [Auth](https://ro14nd.de/openshell-sdk-go/api/auth.html) | |
| 265 | +| Edge auth: extra headers, Cloudflare Access, WebSocket tunnel | `AuthProvider`, `edge.TunnelProxy` | [Edge](https://ro14nd.de/openshell-sdk-go/api/edge.html) | |
| 266 | +| Typed errors (`IsNotFound`, `IsAlreadyExists`, `IsConflict`, ...) | `StatusError` | [Error Handling](https://ro14nd.de/openshell-sdk-go/error-handling.html) | |
| 267 | +| Real-time watch with auto-stop on terminal phase | `WatchInterface[T]` | [Sandboxes](https://ro14nd.de/openshell-sdk-go/api/sandboxes.html) | |
| 268 | +| Fake client for testing (no gRPC server needed) | `fake.Client` | [Testing](https://ro14nd.de/openshell-sdk-go/testing.html) | |
| 269 | +| OIDC login (browser, keyboard, device code, client credentials) | `oidc.Login`, `oidc.DeviceLogin`, `oidc.ClientCredentials` | [OIDC](https://pkg.go.dev/github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/oidc) | |
| 270 | +| Gateway config convenience (load CLI gateway configs, auto-wire auth) | `gateway.NewClient`, `gateway.LoadConfig` | [Gateway](https://ro14nd.de/openshell-sdk-go/api/gateway.html) | |
| 271 | + |
| 272 | +## Prerequisites |
| 273 | + |
| 274 | +- Go 1.25 or later |
| 275 | +- [mise](https://mise.jdx.dev) (recommended for reproducible builds) |
| 276 | + |
| 277 | +## Build and Test |
| 278 | + |
| 279 | +```bash |
| 280 | +git clone https://github.com/NVIDIA/OpenShell.git |
| 281 | +cd OpenShell/sdk/go |
| 282 | + |
| 283 | +mise run test # Run tests with coverage |
| 284 | +mise run lint # Run golangci-lint |
| 285 | +mise run ci # Full CI pipeline (lint + build + test) |
| 286 | +``` |
| 287 | + |
| 288 | +Build commands use [mise](https://mise.jdx.dev) for reproducible tool management. |
| 289 | + |
| 290 | +## Documentation |
| 291 | + |
| 292 | +Full API documentation is available at the [OpenShell Go SDK Docs](https://ro14nd.de/openshell-sdk-go/) site. |
| 293 | + |
| 294 | +To build the docs locally: |
| 295 | + |
| 296 | +```bash |
| 297 | +cargo install mdbook |
| 298 | +mdbook serve docs |
| 299 | +``` |
| 300 | + |
| 301 | +## License |
| 302 | + |
| 303 | +Apache-2.0. See [LICENSE](../../LICENSE) for details. |
| 304 | + |
| 305 | +Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
0 commit comments