Skip to content

Latest commit

 

History

History
350 lines (285 loc) · 13.7 KB

File metadata and controls

350 lines (285 loc) · 13.7 KB

OpenShell SDK for Go

Go Reference License

Important

Read the full documentation for guides, API reference with gRPC mapping, and testing patterns.

A Go SDK for interacting with OpenShell servers, providing idiomatic Go bindings for shell session management, command execution, provider configuration, and service exposure.

Why a Go SDK?

Go is the language of the Kubernetes ecosystem. If you want to build an operator, controller, or any automation that manages OpenShell resources as native Kubernetes objects, you need a Go client.

This SDK is modeled after k8s.io/client-go, the standard Kubernetes client library that every Go operator developer already knows. The patterns will look familiar:

  • Typed sub-clients per resource: client.Sandboxes(), client.Providers(), client.Exec(), just like clientset.CoreV1().Pods()
  • Domain types separated from wire formats: clean Go structs in a types package, no proto leakage into the public API (like k8s.io/api)
  • Watch primitives: channel-based watchers with ResultChan() and Stop(), identical to watch.Interface in client-go
  • Functional options: variadic option patterns for list filtering, pagination, and watch configuration. Nil options are silently ignored at every entry point, so conditional option lists are safe to pass without filtering out nil entries.
  • Composable auth with token refresh: wraps oauth2.TokenSource for automatic token caching and coalesced refresh, following the k8s client-go cachingTokenSource pattern
  • Fake client for testing: an in-memory implementation of the full client interface (like k8s.io/client-go/kubernetes/fake), so operators can be tested without a real gateway

Quick Start

import v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1"

// Connect to a gateway
client, err := v1.NewClient(v1.Config{
    Address: "gateway.example.com:443",
    Auth:    v1.StaticToken("my-token"),
})
if err != nil {
    log.Fatal(err)
}
defer client.Close()

// Create a sandbox and wait until it's ready
sandbox, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{
    Template: &v1.SandboxTemplate{Image: "python:3.12"},
}, nil)
if err != nil {
    log.Fatal(err)
}
sandbox, err = client.Sandboxes().WaitReady(ctx, "default", sandbox.Name)
if err != nil {
    log.Fatal(err)
}

// Run a command
result, err := client.Exec().Run(ctx, "default", sandbox.Name,
    []string{"python3", "-c", "print('hello from sandbox')"},
    v1.ExecOptions{},
)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(result.Stdout))

Pagination

List methods return a lazy pager without issuing a request. NextPage fetches one page with the supplied context, while ListAll explicitly exhausts every page. PageSize is a per-request maximum and PageToken resumes a prior query.

pager, err := client.Sandboxes().List("default", v1.ListOptions{PageSize: 100})
if err != nil {
    log.Fatal(err)
}
for {
    page, err := pager.NextPage(ctx)
    if err != nil {
        log.Fatal(err)
    }
    if page == nil {
        break
    }
    for _, sandbox := range page.Items {
        fmt.Println(sandbox.Name)
    }
}

With automatic token refresh

For OIDC gateways, use RefreshableToken to wrap any oauth2.TokenSource with automatic caching and coalesced refresh:

import "golang.org/x/oauth2"

tokenSource := oauth2Config.TokenSource(ctx, initialToken)
auth, err := v1.RefreshableToken(tokenSource,
    v1.WithLeeway(30*time.Second),
)
if err != nil {
    log.Fatal(err)
}
client, err := v1.NewClient(v1.Config{
    Address: "gateway.example.com:443",
    Auth:    auth,
})
if err != nil {
    log.Fatal(err)
}
defer client.Close()

Concurrent callers share a single refresh call. If the token source fails, the SDK falls back to the cached token with a logged warning. See the Auth docs for details.

With edge proxy headers

When a gateway sits behind a zero-trust reverse proxy, use WithExtraHeaders to attach proxy-specific headers alongside standard auth:

base := v1.StaticToken("my-gateway-token")
auth, err := v1.WithExtraHeaders(base, map[string]string{
    "x-proxy-auth": "proxy-secret",
})
if err != nil {
    log.Fatal(err)
}
client, err := v1.NewClient(v1.Config{
    Address: "gateway.example.com:443",
    Auth:    auth,
})

For Cloudflare Access, use the convenience constructor in the edge package:

import "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/edge"

auth, err := edge.CloudflareAccess(base, os.Getenv("CF_ACCESS_TOKEN"))

For gRPC behind edge proxies that reject HTTP/2, use the WebSocket tunnel:

tunnel, err := edge.NewTunnelProxy(
    "wss://gateway.example.com/ws",
    os.Getenv("CF_ACCESS_TOKEN"),
)
if err != nil {
    log.Fatal(err)
}
defer tunnel.Close()

client, err := v1.NewClient(v1.Config{
    Address: tunnel.Addr(),
    Auth:    v1.StaticToken("my-token"),
    TLS:     &v1.TLSConfig{Insecure: true}, // local tunnel, no TLS
})

OIDC Login

The oidc package provides gateway-aware OIDC authentication with browser, keyboard, device code, and client credentials flows:

import "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/oidc"

// Gateway-aware login: reads OIDC config from gateway metadata
token, err := oidc.Login(ctx, "my-gateway")
if err != nil {
    log.Fatal(err)
}

// Use the token with the SDK client
client, err := v1.NewClient(v1.Config{
    Address: "gateway.example.com:443",
    Auth:    v1.StaticToken(token.AccessToken),
})

For headless environments, use the device code flow:

token, err := oidc.DeviceLogin(ctx,
    oidc.WithIssuer("https://auth.example.com"),
    oidc.WithClientID("my-app"),
)

For a one-shot service-account token exchange, use ClientCredentials. For a long-running SDK client, attach the renewable, memory-only auth provider:

auth, err := oidc.NewClientCredentialsAuth(
    oidc.WithGateway("my-gateway"),
    oidc.WithClientSecretProvider(func(context.Context) (string, error) {
        return os.Getenv("OPENSHELL_OIDC_CLIENT_SECRET"), nil
    }),
)
if err != nil {
    log.Fatal(err)
}

client, err := v1.NewClient(v1.Config{
    Address: "gateway.example.com:443",
    Auth:    auth,
    TLS:     tlsConfig,
})
if err != nil {
    log.Fatal(err)
}
defer client.Close()

You can use explicit WithIssuer, WithClientID, WithScopes, and WithAudience options instead of WithGateway. The provider repeats the grant before expiry and never writes the secret or access token to disk.

See the oidc package docs for all options and flows.

See the Getting Started guide for the full walkthrough.

Migrating from v0.0.101

The pre-1.0 SDK intentionally includes source-incompatible API corrections:

  • TCP.Listen returns a ForwardListener lifecycle handle. The SDK owns the accept loop; callers dial Addr() and call Close() instead of calling Accept() or passing the handle to http.Serve.
  • Resource operations take an explicit workspace, and workspace-bearing domain types preserve that scope.
  • Several public struct field orders changed. Use keyed struct literals.
  • Initialisms use Go spelling, including JSONRPCMaxBodyBytes.
  • Provider profile durations use the exact RefreshBefore, MaxLifetime, and CacheTTL fields. The legacy whole-second fields were removed.

These changes are intentional while the module remains below v1. Update callers as one migration rather than relying on the v0.0.101 API shape.

Architecture

Client
  ├── Sandboxes()   → SandboxInterface    (create, get, list, delete, watch, wait, logs)
  ├── Exec()        → ExecInterface       (run, stream, interactive)
  ├── Files()       → FileInterface       (upload, download)
  ├── Health()      → HealthInterface     (health check, gateway info, current user)
  ├── Services()    → ServiceInterface    (expose, get, list, delete)
  ├── Providers()   → ProviderInterface   (CRUD + ensure)
  │     ├── Profiles() → ProfileInterface (list, get, import, update, lint, delete)
  │     └── Refresh()  → RefreshInterface (configure, status, rotate, delete)
  ├── Workspaces()  → WorkspaceInterface  (create, get, list, delete, members)
  └── Policy()      → PolicyInterface     (draft review, approve, reject, merge, status)

All domain types live in openshell/v1/types/. Proto-to-Go conversions happen in an internal converter layer. The public API surface uses type aliases so consumers import a single package. See the Architecture overview for details.

Features

Use CloseInteractiveInput(session) to close stdin and resize input while keeping output readable. SDK sessions implement the optional InteractiveSessionControl interface (CloseWrite() and Cancel()); the original InteractiveSession interface remains unchanged for existing mocks and wrappers. Input closure returns ErrorUnimplemented for sessions without that capability and leaves them open. CancelInteractive(session) uses Cancel() when available and otherwise calls Close(). SDK close/cancel operations are idempotent; writes and resizes after input closure return io.ErrClosedPipe. Drain Read concurrently with waiting for ExitCode(). ExitCode() waits for final gRPC status and returns any observed process exit code alongside a later stream error. An exit event alone does not establish successful stream completion.

Feature Interface Docs
Sandbox lifecycle (create, get, list, delete, watch, wait) SandboxInterface Sandboxes
Command execution (collected, streamed, interactive PTY) ExecInterface Exec
Provider management (CRUD + idempotent ensure) ProviderInterface Providers
Provider profiles (list, import, lint, update) ProfileInterface Profiles
Credential refresh (configure, rotate, status) RefreshInterface Refresh
Service exposure (expose, list, delete) ServiceInterface Services
File transfer API (transport capability-gated) FileInterface Files
Policy management (draft review, approve, reject, merge, global policy) PolicyInterface Policy
Sandbox logs (streaming retrieval) SandboxInterface Sandboxes
Workspace management (create, get, list, delete, members) WorkspaceInterface Workspaces
Sandbox provider attachment (attach, detach, list) SandboxInterface Sandboxes
Gateway info and current user identity HealthInterface Health
Health checking HealthInterface Health
SSH tunneling and TCP forwarding SSHInterface, TCPInterface SSH, TCP
Auth: static token, refreshable token (oauth2.TokenSource) AuthProvider Auth
Edge auth: extra headers, Cloudflare Access, WebSocket tunnel AuthProvider, edge.TunnelProxy Edge
Typed errors (IsNotFound, IsAlreadyExists, IsConflict, ...) StatusError Error Handling
Real-time watch with auto-stop on terminal phase WatchInterface[T] Sandboxes
Fake client for testing (no gRPC server needed) fake.Client Testing
OIDC login and renewable service auth oidc.Login, oidc.DeviceLogin, oidc.ClientCredentials, oidc.NewClientCredentialsAuth OIDC
Gateway config convenience (load CLI gateway configs, auto-wire auth) gateway.NewClient, gateway.LoadConfig Gateway

Prerequisites

  • Go 1.25.13 or later
  • mise (recommended for reproducible builds)

Build and Test

git clone https://github.com/NVIDIA/OpenShell.git
cd OpenShell/sdk/go

mise run test    # Run tests with coverage
mise run lint    # Run golangci-lint
mise run ci      # Full CI pipeline (lint + build + test)

Build commands use mise for reproducible tool management.

Documentation

Full API documentation is available at the OpenShell Go SDK Docs site.

To build the docs locally:

cargo install mdbook
mdbook serve docs

License

Apache-2.0. See LICENSE for details.

Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.