Skip to content

Commit 2061693

Browse files
committed
feat(sdk/go): complete Go SDK with domain clients, auth, and hardening (Drop B)
Add the remaining Go SDK packages to complete the OpenShell Go client library. This PR builds on the foundation from Drop A (PR NVIDIA#2271) with: - 13 domain clients: config, exec, file, health, inference, policy, profile, provider, refresh, service, ssh, tcp, workspace - Authentication: OIDC package (auth code, device code, browser flows) and gateway token management - Edge connectivity: Cloudflare tunnel and WebSocket proxy - Test infrastructure: fake client implementations with validation parity for all 14 domains - Proto extensions: inference and options proto definitions with generated Go bindings - Hardening: watch goroutine race fix, TLS config conflict detection, WaitReady helper extraction, deep-copy credential tests - API cleanup: removed unused option structs and config fields (YAGNI) - Documentation: API reference pages, getting started guide, architecture overview, error handling, and testing guides All changes target sdk/go/. CI integration follows in a subsequent PR. Ref NVIDIA#2044
1 parent c825b1f commit 2061693

212 files changed

Lines changed: 36635 additions & 787 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎sdk/go/.golangci.yml‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
version: "2"
5+
6+
run:
7+
timeout: 5m
8+
9+
linters:
10+
enable:
11+
- govet
12+
- errcheck
13+
- staticcheck
14+
- unused
15+
- ineffassign
16+
- revive
17+
- goheader
18+
exclusions:
19+
rules:
20+
- path: "proto/"
21+
linters:
22+
- goheader
23+
- revive
24+
25+
linters-settings:
26+
goheader:
27+
template: |-
28+
SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
29+
SPDX-License-Identifier: Apache-2.0
30+
revive:
31+
rules:
32+
- name: blank-imports
33+
- name: exported
34+
- name: var-naming
35+
- name: indent-error-flow
36+
- name: range
37+
- name: error-return
38+
- name: error-naming
39+
- name: error-strings
40+
- name: receiver-naming
41+
- name: increment-decrement
42+
- name: superfluous-else
43+
- name: unreachable-code

‎sdk/go/README.md‎

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
# OpenShell SDK for Go
2+
3+
[![Go Reference](https://pkg.go.dev/badge/github.com/NVIDIA/OpenShell/sdk/go.svg)](https://pkg.go.dev/github.com/NVIDIA/OpenShell/sdk/go)
4+
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](../../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.

‎sdk/go/buf.gen.yaml‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
33

4-
# Code generation for the Go SDK. The proto module boundary and validation
5-
# policy live in the repo-level buf.yaml; this template only drives generation.
6-
# buf compiles the module with its own compiler and runs protoc-gen-go /
7-
# protoc-gen-go-grpc from mise-managed binaries. Limited to the client-surface
8-
# closure (openshell, datamodel, sandbox, options); well-known types resolve
9-
# through google.golang.org/protobuf and are not generated.
4+
# Code generation for the Go SDK. buf compiles the proto module with its own
5+
# compiler and runs protoc-gen-go / protoc-gen-go-grpc from mise-managed
6+
# binaries. Limited to the client-surface closure (openshell, datamodel,
7+
# sandbox, options, inference).
108
version: v2
119

1210
inputs:
13-
- directory: ../../proto
11+
- directory: proto
1412
paths:
15-
- ../../proto/openshell.proto
16-
- ../../proto/datamodel.proto
17-
- ../../proto/sandbox.proto
18-
- ../../proto/options.proto
13+
- proto/openshell.proto
14+
- proto/datamodel.proto
15+
- proto/sandbox.proto
16+
- proto/options.proto
17+
- proto/inference.proto
1918

2019
plugins:
2120
- local: protoc-gen-go
@@ -26,6 +25,7 @@ plugins:
2625
- Mdatamodel.proto=github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1
2726
- Msandbox.proto=github.com/NVIDIA/OpenShell/sdk/go/proto/sandboxv1
2827
- Moptions.proto=github.com/NVIDIA/OpenShell/sdk/go/proto/optionsv1
28+
- Minference.proto=github.com/NVIDIA/OpenShell/sdk/go/proto/inferencev1
2929
- local: protoc-gen-go-grpc
3030
out: .
3131
opt:
@@ -34,3 +34,4 @@ plugins:
3434
- Mdatamodel.proto=github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1
3535
- Msandbox.proto=github.com/NVIDIA/OpenShell/sdk/go/proto/sandboxv1
3636
- Moptions.proto=github.com/NVIDIA/OpenShell/sdk/go/proto/optionsv1
37+
- Minference.proto=github.com/NVIDIA/OpenShell/sdk/go/proto/inferencev1

0 commit comments

Comments
 (0)