Skip to content
Open
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
4 changes: 3 additions & 1 deletion sdk/go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ patterns will look familiar:
- **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
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
Expand Down
21 changes: 21 additions & 0 deletions sdk/go/docs/src/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,27 @@ profiles, err := client.Providers().Profiles().List(ctx, "default")
status, err := client.Providers().Refresh().GetStatus(ctx, "default", "openai", "api-key")
```

## Functional Options

Many SDK entry points accept variadic option parameters that configure the call:

```go
auth, err := v1.RefreshableToken(tokenSource,
v1.WithLeeway(30*time.Second),
v1.WithLogger(logger),
)
```

Options are applied in the order they are passed, so later options override earlier ones when they set the same field. **Nil options are silently ignored at every entry point.** You can safely build option lists conditionally without filtering out nil entries:

```go
opts := []v1.RefreshOption{
v1.WithLeeway(30 * time.Second),
maybeLogger(), // may return nil
}
auth, err := v1.RefreshableToken(tokenSource, opts...)
```

## gRPC Layer

Underneath, the SDK communicates with the OpenShell gateway over gRPC. The single `OpenShell` service in `proto/openshell.proto` defines all RPCs. The SDK maps each interface method to one or more RPCs:
Expand Down
5 changes: 2 additions & 3 deletions sdk/go/openshell/v1/auth_refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"golang.org/x/oauth2"
"golang.org/x/sync/singleflight"

"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/internal/options"
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
)

Expand Down Expand Up @@ -160,9 +161,7 @@ func RefreshableToken(src oauth2.TokenSource, opts ...RefreshOption) (AuthProvid
}

cfg := defaultRefreshConfig()
for _, o := range opts {
o(&cfg)
}
options.Apply(&cfg, opts)

return &refreshableAuth{
source: src,
Expand Down
23 changes: 23 additions & 0 deletions sdk/go/openshell/v1/edge/options_nil_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package edge

import (
"testing"
"time"

"github.com/stretchr/testify/assert"

"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/internal/options"
)

func TestTunnelOption_NilIgnored(t *testing.T) {
cfg := tunnelConfig{closeTimeout: defaultCloseTimeout}
options.Apply(&cfg, []TunnelOption{
WithTunnelLogger(nil),
nil,
WithCloseTimeout(10 * time.Second),
})
assert.Equal(t, 10*time.Second, cfg.closeTimeout)
}
5 changes: 2 additions & 3 deletions sdk/go/openshell/v1/edge/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/coder/websocket"

"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/internal/options"
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
)

Expand Down Expand Up @@ -107,9 +108,7 @@ func NewTunnelProxy(gatewayURL, edgeToken string, opts ...TunnelOption) (*Tunnel
cfg := tunnelConfig{
closeTimeout: defaultCloseTimeout,
}
for _, o := range opts {
o(&cfg)
}
options.Apply(&cfg, opts)

listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
Expand Down
5 changes: 2 additions & 3 deletions sdk/go/openshell/v1/fake/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"sync"

v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1"
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/internal/options"
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
)

Expand Down Expand Up @@ -97,9 +98,7 @@ func NewClient(opts ...ClientOption) *Client {
fc.workspaces = newFakeWorkspaceClient(fc.workspaceStore, fc.memberStore, fc.isClosed)
fc.inference = newFakeInferenceClient(fc.isClosed)

for _, opt := range opts {
opt(fc)
}
options.Apply(fc, opts)
Comment thread
elezar marked this conversation as resolved.

return fc
}
Expand Down
30 changes: 30 additions & 0 deletions sdk/go/openshell/v1/fake/options_nil_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package fake

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
)

func TestClientOption_NilIgnored(t *testing.T) {
hr := &types.HealthResult{Healthy: false, Version: "1.2.3"}
cu := &types.CurrentUser{Subject: "user-42", DisplayName: "Test User"}

fc := NewClient(
WithHealthResult(hr),
nil,
WithCurrentUser(cu),
)
require.NotNil(t, fc)

healthClient := fc.health.(*fakeHealthClient)
assert.Equal(t, false, healthClient.result.Healthy)
assert.Equal(t, "1.2.3", healthClient.result.Version)
assert.Equal(t, "Test User", healthClient.currentUser.DisplayName)
}
5 changes: 2 additions & 3 deletions sdk/go/openshell/v1/gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"

v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1"
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/internal/options"
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
)

Expand All @@ -27,9 +28,7 @@ func NewClient(name string, opts ...ClientOption) (*v1.Client, error) {

// Apply caller options.
cc := &clientConfig{}
for _, o := range opts {
o(cc)
}
options.Apply(cc, opts)

// Resolve auth provider: caller override takes precedence.
auth := cc.auth
Expand Down
23 changes: 23 additions & 0 deletions sdk/go/openshell/v1/gateway/options_nil_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package gateway

import (
"testing"
"time"

"github.com/stretchr/testify/assert"

"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/internal/options"
)

func TestClientOption_NilIgnored(t *testing.T) {
var cfg clientConfig
options.Apply(&cfg, []ClientOption{
WithTimeout(30 * time.Second),
nil,
WithLogger(nil),
})
assert.Equal(t, 30*time.Second, cfg.timeout)
}
19 changes: 19 additions & 0 deletions sdk/go/openshell/v1/internal/options/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

// Package options provides a shared mechanism for applying functional options
// across all SDK entry points. It enforces a single nil-option rule: nil
// entries in an option list are silently ignored.
package options

// Apply folds opts into target in order, skipping nil entries.
// O is constrained to any named function type whose underlying type is
// func(*T), so callers write options.Apply(&cfg, opts) and both type
// parameters are inferred.
func Apply[T any, O ~func(*T)](target *T, opts []O) {
Comment thread
elezar marked this conversation as resolved.
for _, opt := range opts {
if opt != nil {
opt(target)
}
}
}
98 changes: 98 additions & 0 deletions sdk/go/openshell/v1/internal/options/options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package options

import (
"testing"

"github.com/stretchr/testify/assert"
)

// testConfig is a plain config struct for testing Apply.
type testConfig struct {
a int
b string
}

// testOption is a named option type, mirroring the SDK's pattern.
type testOption func(*testConfig)

func TestApply_EmptySlice(t *testing.T) {
var cfg testConfig
Apply(&cfg, []testOption{})
assert.Equal(t, testConfig{}, cfg)
}

func TestApply_NilSlice(t *testing.T) {
var cfg testConfig
Apply[testConfig, testOption](&cfg, nil)
assert.Equal(t, testConfig{}, cfg)
}

func TestApply_AllNil(t *testing.T) {
var cfg testConfig
Apply(&cfg, []testOption{nil, nil, nil})
assert.Equal(t, testConfig{}, cfg)
}

func TestApply_NilFirst(t *testing.T) {
var cfg testConfig
Apply(&cfg, []testOption{
nil,
func(c *testConfig) { c.a = 1 },
})
assert.Equal(t, 1, cfg.a)
}

func TestApply_NilMiddle(t *testing.T) {
var cfg testConfig
Apply(&cfg, []testOption{
func(c *testConfig) { c.a = 1 },
nil,
func(c *testConfig) { c.b = "two" },
})
assert.Equal(t, 1, cfg.a)
assert.Equal(t, "two", cfg.b)
}

func TestApply_NilLast(t *testing.T) {
var cfg testConfig
Apply(&cfg, []testOption{
func(c *testConfig) { c.a = 1 },
nil,
})
assert.Equal(t, 1, cfg.a)
}

func TestApply_OrderPreserved(t *testing.T) {
var cfg testConfig
Apply(&cfg, []testOption{
func(c *testConfig) { c.a = 1 },
func(c *testConfig) { c.a = 2 },
func(c *testConfig) { c.a = 3 },
})
// The last option wins because options apply in order.
assert.Equal(t, 3, cfg.a)
}

// liveObject simulates the fake.Client pattern where options mutate a live
// object rather than a plain config struct.
type liveObject struct {
name string
count int
}

type liveOption func(*liveObject)

func TestApply_LiveObjectTarget(t *testing.T) {
obj := &liveObject{name: "initial", count: 0}
Apply(obj, []liveOption{
nil,
func(o *liveObject) { o.name = "updated" },
nil,
func(o *liveObject) { o.count = 42 },
})
assert.Equal(t, "updated", obj.name)
assert.Equal(t, 42, obj.count)
}
5 changes: 2 additions & 3 deletions sdk/go/openshell/v1/oidc/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"golang.org/x/oauth2"

"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/gateway"
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/internal/options"
)

// ClientCredentials performs a non-interactive OAuth2 client credentials
Expand All @@ -37,9 +38,7 @@ func ClientCredentials(ctx context.Context, opts ...LoginOption) (*oauth2.Token,

func resolveClientCredentialsConfig(opts ...LoginOption) (*loginConfig, error) {
cfg := &loginConfig{}
for _, opt := range opts {
opt(cfg)
}
options.Apply(cfg, opts)
cfg.applyDefaults()
Comment on lines +41 to 42

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also out of scope: Why do we apply defaults after applying the inputs? Does this mean that user inputs could be overridden?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good question. applyDefaults() only fills fields still at their zero value (if len(c.scopes) == 0, if c.timeout == 0), so it never overrides a value the caller set via an option. The apply-then-default ordering is pre-existing; this PR preserves it intentionally, since the scope is nil handling plus the shared apply loop with no behavior change for non-nil options. So user inputs can't be overridden by the defaults. Agreed it's out of scope to change here.


// Client credentials should not send interactive scopes by default.
Expand Down
5 changes: 2 additions & 3 deletions sdk/go/openshell/v1/oidc/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"golang.org/x/oauth2"

"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/gateway"
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/internal/options"
)

// deviceAuthResponse holds the parsed response from the device
Expand Down Expand Up @@ -46,9 +47,7 @@ type deviceAuthResponse struct {
// - any other error: return [ErrDeviceCode]
func DeviceLogin(ctx context.Context, opts ...LoginOption) (*oauth2.Token, error) {
cfg := &loginConfig{}
for _, opt := range opts {
opt(cfg)
}
options.Apply(cfg, opts)
cfg.applyDefaults()
if _, hasDeadline := ctx.Deadline(); !hasDeadline && cfg.timeout > 0 {
var cancel context.CancelFunc
Expand Down
5 changes: 2 additions & 3 deletions sdk/go/openshell/v1/oidc/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"golang.org/x/oauth2"

"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/gateway"
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/internal/options"
)

// Login performs an interactive OIDC authorization code login.
Expand All @@ -33,9 +34,7 @@ import (
// fallback flow is used instead.
func Login(ctx context.Context, gatewayName string, opts ...LoginOption) (*oauth2.Token, error) {
cfg := &loginConfig{}
for _, opt := range opts {
opt(cfg)
}
options.Apply(cfg, opts)
cfg.applyDefaults()

// Apply configured timeout if the caller's context has no deadline.
Expand Down
23 changes: 23 additions & 0 deletions sdk/go/openshell/v1/oidc/options_nil_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package oidc

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/internal/options"
)

func TestLoginOption_NilIgnored(t *testing.T) {
var cfg loginConfig
options.Apply(&cfg, []LoginOption{
WithIssuer("https://auth.example.com"),
nil,
WithClientID("my-app"),
})
assert.Equal(t, "https://auth.example.com", cfg.issuer)
assert.Equal(t, "my-app", cfg.clientID)
}
Loading
Loading