feat(sdk/go)!: replace positional labels with functional Create options (#3198)

* feat(sdk/go)!: replace positional labels with functional Create options

Move labels and annotations from separate parameter positions into
functional options (WithLabels, WithAnnotations) on Create() and
CreateFromTemplate().

Before: Create(ctx, ws, name, spec, labels, CreateOptions{Annotations: ann})
After:  Create(ctx, ws, name, spec, WithLabels(labels), WithAnnotations(ann))

This unifies two resource metadata fields that were inconsistently
placed (labels as positional, annotations in a struct) into a single
extensible options pattern matching the existing LogOption convention.

BREAKING CHANGE: Create() and CreateFromTemplate() signatures changed.
All callers must use WithLabels() and WithAnnotations() functional
options instead of positional labels and CreateOptions struct.

Closes #2807

Signed-off-by: Roland Huß <rhuss@redhat.com>

* fix(sdk/go): ignore nil Create options

Callers of the previous signature passed nil for the positional labels
parameter to create an unlabeled sandbox:

    Create(ctx, ws, name, spec, nil)

That call still compiles against the new variadic signature, because an
untyped nil converts to CreateOption. ApplyCreateOptions then invoked it
and panicked with a nil pointer dereference, so the migration was unsafe
for existing consumers.

Skip nil options in ApplyCreateOptions and cover the legacy trailing-nil
call with regression tests against ApplyCreateOptions directly, the fake
client (Create and CreateFromTemplate), and the gRPC client.

Signed-off-by: Roland Huß <rhuss@redhat.com>

* refactor(sdk/go): apply Create options through the shared options helper

Signed-off-by: Roland Huß <rhuss@redhat.com>

* docs(sdk/go): use WithLabels in CreateSandboxFromTemplate examples

Signed-off-by: Roland Huß <rhuss@redhat.com>

---------

Signed-off-by: Roland Huß <rhuss@redhat.com>
This commit is contained in:
Roland Huss
2026-10-16 07:23:42 +00:00
committed by GitHub
parent b2069a267c
commit 5add9df2cd
27 changed files with 311 additions and 131 deletions
+5 -7
View File
@@ -539,13 +539,11 @@ console.log(sandbox.serviceUrls[''])
```go
sandbox, err := client.Sandboxes().Create(
ctx, "default", "app-server", spec, nil,
v1.CreateOptions{ServiceExposures: []v1.ServiceExposure{
{
TargetPort: 4500,
AuthorizationMode: v1.ServiceAuthorizationModeBearerPassthrough,
},
}},
ctx, "default", "app-server", spec,
v1.WithServiceExposures(v1.ServiceExposure{
TargetPort: 4500,
AuthorizationMode: v1.ServiceAuthorizationModeBearerPassthrough,
}),
)
fmt.Println(sandbox.ServiceURLs[""])
```
+1 -1
View File
@@ -56,7 +56,7 @@ 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)
}
+1 -1
View File
@@ -19,7 +19,7 @@ func TestSandboxLifecycle(t *testing.T) {
ctx := context.Background()
sb, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{}, nil)
sb, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{})
require.NoError(t, err)
assert.Equal(t, types.SandboxProvisioning, sb.Status.Phase)
+1 -1
View File
@@ -70,7 +70,7 @@ sb, err := client.CreateSandboxFromTemplate(
Providers: []string{"openai"},
Policy: policy,
},
map[string]string{"job": "training"},
v1.WithLabels(map[string]string{"job": "training"}),
)
```
+4 -6
View File
@@ -7,7 +7,7 @@ wait for readiness, watch state changes, and retrieve logs.
## Create
Creates a new sandbox with the given name, spec, and labels.
Creates a new sandbox with the given name and spec.
```go
sb, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{
@@ -15,11 +15,9 @@ sb, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSp
Image: "nvcr.io/nvidia/openshell:latest",
},
Providers: []string{"openai"},
}, map[string]string{
}, v1.WithLabels(map[string]string{
"team": "platform",
}, v1.CreateOptions{ServiceExposures: []v1.ServiceExposure{
{TargetPort: 8080},
}})
}), v1.WithServiceExposures(v1.ServiceExposure{TargetPort: 8080}))
fmt.Println(sb.ServiceURLs[""])
```
@@ -49,7 +47,7 @@ sb, err := client.CreateSandboxFromTemplate(ctx,
Providers: []string{"openai"},
Policy: policy,
},
map[string]string{"team": "platform"},
v1.WithLabels(map[string]string{"team": "platform"}),
)
```
+1 -1
View File
@@ -62,7 +62,7 @@ Handle missing resources gracefully:
sb, err := client.Sandboxes().Get(ctx, "default", "my-sandbox")
if v1.IsNotFound(err) {
fmt.Println("Sandbox does not exist, creating...")
sb, err = client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{}, nil)
sb, err = client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{})
}
if err != nil {
log.Fatal(err)
+1 -1
View File
@@ -68,7 +68,7 @@ Create a sandbox with a Python image:
sandbox, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{
Template: &v1.SandboxTemplate{Image: "python:3.12"},
Environment: map[string]string{"LANG": "en_US.UTF-8"},
}, nil)
})
if err != nil {
log.Fatal(err)
}
+4 -4
View File
@@ -18,7 +18,7 @@ func TestMyOperator(t *testing.T) {
ctx := context.Background()
// Use client exactly like the real SDK
sb, err := client.Sandboxes().Create(ctx, "default", "test-sandbox", &v1.SandboxSpec{}, nil)
sb, err := client.Sandboxes().Create(ctx, "default", "test-sandbox", &v1.SandboxSpec{})
require.NoError(t, err)
assert.Equal(t, "Provisioning", string(sb.Status.Phase))
}
@@ -79,7 +79,7 @@ client := fake.NewClient()
ctx := context.Background()
// Create starts in Provisioning
sb, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{}, nil)
sb, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{})
assert.Equal(t, types.SandboxProvisioning, sb.Status.Phase)
// WaitReady transitions to Ready (synchronous in fake)
@@ -109,7 +109,7 @@ require.NoError(t, err)
defer watcher.Stop()
// Create a sandbox — triggers an ADDED event
client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{}, nil)
client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{})
// Read the event from the channel
event := <-watcher.ResultChan()
@@ -128,7 +128,7 @@ watcher, err := client.Sandboxes().Watch(ctx, "default", "my-sandbox", v1.WatchO
require.NoError(t, err)
// Create and transition to Ready
client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{}, nil)
client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{})
client.Sandboxes().WaitReady(ctx, "default", "my-sandbox")
// Drain events — channel closes after the Ready event
+3 -3
View File
@@ -20,7 +20,7 @@ type Config = types.Config
type ClientInterface interface {
Sandboxes() SandboxInterface
SandboxTemplates() SandboxTemplateInterface
CreateSandboxFromTemplate(ctx context.Context, workspace, name, templateName string, spec *SandboxSpec, labels map[string]string, opts ...CreateOptions) (*Sandbox, error)
CreateSandboxFromTemplate(ctx context.Context, workspace, name, templateName string, spec *SandboxSpec, opts ...CreateOption) (*Sandbox, error)
Providers() ProviderInterface
Services() ServiceInterface
Exec() ExecInterface
@@ -122,8 +122,8 @@ func (c *Client) SandboxTemplates() SandboxTemplateInterface { return c.template
// CreateSandboxFromTemplate creates a sandbox from a named workload template
// without changing the legacy Sandboxes() interface.
func (c *Client) CreateSandboxFromTemplate(ctx context.Context, workspace, name, templateName string, spec *SandboxSpec, labels map[string]string, opts ...CreateOptions) (*Sandbox, error) {
return c.templateCreate.CreateFromTemplate(ctx, workspace, name, templateName, spec, labels, opts...)
func (c *Client) CreateSandboxFromTemplate(ctx context.Context, workspace, name, templateName string, spec *SandboxSpec, opts ...CreateOption) (*Sandbox, error) {
return c.templateCreate.CreateFromTemplate(ctx, workspace, name, templateName, spec, opts...)
}
// Providers returns the provider sub-client.
+2 -2
View File
@@ -25,7 +25,7 @@
// sandbox, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{
// Template: &v1.SandboxTemplate{Image: "python:3.12"},
// Environment: map[string]string{"LANG": "en_US.UTF-8"},
// }, nil)
// })
// if err != nil {
// log.Fatal(err)
// }
@@ -320,7 +320,7 @@
// },
// },
// },
// }, nil)
// })
//
// Replace the full policy at runtime via configuration update:
//
+2 -2
View File
@@ -104,7 +104,7 @@ func ExampleNewClient_watchEvents() {
defer watcher.Stop()
// Create triggers an ADDED event
_, err = client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{}, nil)
_, err = client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{})
if err != nil {
log.Fatal(err)
}
@@ -135,7 +135,7 @@ func ExampleNewClient_stopOnTerminal() {
}
// Create and transition to Ready
_, err = client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{}, nil)
_, err = client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{})
if err != nil {
log.Fatal(err)
}
+3 -3
View File
@@ -21,7 +21,7 @@ func ExampleClient_Sandboxes() {
ctx := context.Background()
// Create a sandbox
sb, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{}, nil)
sb, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{})
if err != nil {
log.Fatal(err)
}
@@ -154,13 +154,13 @@ func ExampleIsAlreadyExists() {
ctx := context.Background()
// Create a sandbox
_, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{}, nil)
_, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{})
if err != nil {
log.Fatal(err)
}
// Try to create the same sandbox again
_, err = client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{}, nil)
_, err = client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{})
if v1.IsAlreadyExists(err) {
fmt.Println("Sandbox already exists")
}
+1 -1
View File
@@ -34,7 +34,7 @@ func (r *stubSandboxResolver) Get(_ context.Context, _, name string) (*Sandbox,
return &Sandbox{ID: "sb-" + name, Name: name}, nil
}
func (r *stubSandboxResolver) Create(context.Context, string, string, *SandboxSpec, map[string]string, ...CreateOptions) (*Sandbox, error) {
func (r *stubSandboxResolver) Create(context.Context, string, string, *SandboxSpec, ...CreateOption) (*Sandbox, error) {
panic("not implemented")
}
func (r *stubSandboxResolver) List(string, ...ListOptions) (*Pager[*Sandbox], error) {
+2 -2
View File
@@ -117,8 +117,8 @@ func (fc *Client) SandboxTemplates() v1.SandboxTemplateInterface { return fc.tem
// CreateSandboxFromTemplate creates a sandbox from a named workload template
// without changing the legacy Sandboxes() interface.
func (fc *Client) CreateSandboxFromTemplate(ctx context.Context, workspace, name, templateName string, spec *types.SandboxSpec, labels map[string]string, opts ...types.CreateOptions) (*types.Sandbox, error) {
return fc.templateCreate.CreateFromTemplate(ctx, workspace, name, templateName, spec, labels, opts...)
func (fc *Client) CreateSandboxFromTemplate(ctx context.Context, workspace, name, templateName string, spec *types.SandboxSpec, opts ...types.CreateOption) (*types.Sandbox, error) {
return fc.templateCreate.CreateFromTemplate(ctx, workspace, name, templateName, spec, opts...)
}
// Providers returns the provider sub-client.
+1 -1
View File
@@ -38,7 +38,7 @@ func TestFakeClient_Sandboxes_AfterClose(t *testing.T) {
_ = fc.Close()
_, err := fc.Sandboxes().Create(ctx, "default", "test", &types.SandboxSpec{}, nil)
_, err := fc.Sandboxes().Create(ctx, "default", "test", &types.SandboxSpec{})
require.Error(t, err)
assert.True(t, types.IsUnavailable(err))
}
+8 -14
View File
@@ -309,7 +309,7 @@ func newFakeSandboxClient(
}
// Create creates a new sandbox with Provisioning phase.
func (c *fakeSandboxClient) Create(_ context.Context, workspace, name string, spec *types.SandboxSpec, labels map[string]string, opts ...types.CreateOptions) (*types.Sandbox, error) {
func (c *fakeSandboxClient) Create(_ context.Context, workspace, name string, spec *types.SandboxSpec, opts ...types.CreateOption) (*types.Sandbox, error) {
if c.closedFunc() {
return nil, &types.StatusError{Code: types.ErrorUnavailable, Message: "client is closed"}
}
@@ -318,17 +318,14 @@ func (c *fakeSandboxClient) Create(_ context.Context, workspace, name string, sp
spec = &types.SandboxSpec{}
}
var annotations map[string]string
if len(opts) > 0 {
annotations = copyStringMap(opts[0].Annotations)
}
cfg := types.ApplyCreateOptions(opts)
sb := &types.Sandbox{
Name: name,
Workspace: workspace,
CreatedAt: time.Now(),
Labels: copyStringMap(labels),
Annotations: annotations,
Labels: copyStringMap(cfg.Labels()),
Annotations: copyStringMap(cfg.Annotations()),
ResourceVersion: 1,
Spec: copySandboxSpec(*spec),
Status: types.SandboxStatus{
@@ -350,7 +347,7 @@ func (c *fakeSandboxClient) Create(_ context.Context, workspace, name string, sp
}
// CreateFromTemplate creates a new sandbox from a named template with Provisioning phase.
func (c *fakeSandboxClient) CreateFromTemplate(_ context.Context, workspace, name, templateName string, spec *types.SandboxSpec, labels map[string]string, opts ...types.CreateOptions) (*types.Sandbox, error) {
func (c *fakeSandboxClient) CreateFromTemplate(_ context.Context, workspace, name, templateName string, spec *types.SandboxSpec, opts ...types.CreateOption) (*types.Sandbox, error) {
if c.closedFunc() {
return nil, &types.StatusError{Code: types.ErrorUnavailable, Message: "client is closed"}
}
@@ -368,10 +365,7 @@ func (c *fakeSandboxClient) CreateFromTemplate(_ context.Context, workspace, nam
spec = &types.SandboxSpec{}
}
var annotations map[string]string
if len(opts) > 0 {
annotations = copyStringMap(opts[0].Annotations)
}
cfg := types.ApplyCreateOptions(opts)
resolvedSpec := sandboxSpecFromWorkloadTemplate(template)
resolvedSpec.Providers = copyStringSlice(spec.Providers)
@@ -383,8 +377,8 @@ func (c *fakeSandboxClient) CreateFromTemplate(_ context.Context, workspace, nam
Name: name,
Workspace: workspace,
CreatedAt: time.Now(),
Labels: copyStringMap(labels),
Annotations: annotations,
Labels: copyStringMap(cfg.Labels()),
Annotations: copyStringMap(cfg.Annotations()),
ResourceVersion: 1,
Spec: resolvedSpec,
CreatedFromWorkloadTemplate: &types.SandboxWorkloadTemplateProvenance{
@@ -233,7 +233,7 @@ func TestSandboxTemplate_CreateSandboxFromTemplateResolvesWorkloadAndGovernance(
Providers: []string{"github"},
Policy: policy,
},
map[string]string{"team": "runtime"},
types.WithLabels(map[string]string{"team": "runtime"}),
)
require.NoError(t, err)
@@ -346,7 +346,6 @@ func TestSandboxTemplate_CreateSandboxFromTemplatePreservesDefaultGPURequest(t *
"job-default-gpu",
"default-gpu",
nil,
nil,
)
require.NoError(t, err)
+75 -41
View File
@@ -31,7 +31,7 @@ func TestSandbox_Create(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
sb, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{LogLevel: "debug"}, map[string]string{"env": "test"})
sb, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{LogLevel: "debug"}, types.WithLabels(map[string]string{"env": "test"}))
require.NoError(t, err)
assert.Equal(t, "test-sb", sb.Name)
assert.Equal(t, "debug", sb.Spec.LogLevel)
@@ -45,20 +45,54 @@ func TestSandbox_Create_AlreadyExists(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
_, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
_, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
require.NoError(t, err)
_, err = sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
_, err = sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
require.Error(t, err)
assert.True(t, types.IsAlreadyExists(err))
}
// Before functional options, an unlabeled sandbox was created by passing nil
// for the positional labels parameter. Such calls still compile against the new
// variadic signature, so they must keep working instead of panicking.
func TestSandbox_Create_LegacyTrailingNil(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
sb, err := sc.Create(ctx, "default", "legacy-sb", &types.SandboxSpec{LogLevel: "debug"}, nil)
require.NoError(t, err)
assert.Equal(t, "legacy-sb", sb.Name)
assert.Empty(t, sb.Labels)
assert.Empty(t, sb.Annotations)
}
func TestSandbox_CreateFromTemplate_LegacyTrailingNil(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
sc.templateStore.Insert("default", &types.SandboxWorkloadTemplate{
Name: "gpu-kata",
ResourceVersion: 1,
Spec: types.SandboxWorkloadTemplateSpec{
Workload: &types.SandboxWorkloadConfig{
Image: "registry.example.com/agent:latest",
},
},
})
sb, err := sc.CreateFromTemplate(ctx, "default", "legacy-tpl", "gpu-kata", &types.SandboxSpec{}, nil)
require.NoError(t, err)
assert.Equal(t, "legacy-tpl", sb.Name)
assert.Empty(t, sb.Labels)
}
func TestSandbox_Create_WithAnnotations(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
sb, err := sc.Create(ctx, "default", "annotated", &types.SandboxSpec{}, nil,
types.CreateOptions{Annotations: map[string]string{"source": "cli", "user": "admin"}})
sb, err := sc.Create(ctx, "default", "annotated", &types.SandboxSpec{},
types.WithAnnotations(map[string]string{"source": "cli", "user": "admin"}))
require.NoError(t, err)
assert.Equal(t, "cli", sb.Annotations["source"])
assert.Equal(t, "admin", sb.Annotations["user"])
@@ -73,8 +107,8 @@ func TestSandbox_Create_WithAnnotationsDeepCopy(t *testing.T) {
ctx := context.Background()
input := map[string]string{"key": "original"}
sb, err := sc.Create(ctx, "default", "dc-test", &types.SandboxSpec{}, nil,
types.CreateOptions{Annotations: input})
sb, err := sc.Create(ctx, "default", "dc-test", &types.SandboxSpec{},
types.WithAnnotations(input))
require.NoError(t, err)
input["key"] = "MUTATED"
@@ -85,7 +119,7 @@ func TestSandbox_Create_NoAnnotations(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
sb, err := sc.Create(ctx, "default", "no-ann", &types.SandboxSpec{}, nil)
sb, err := sc.Create(ctx, "default", "no-ann", &types.SandboxSpec{})
require.NoError(t, err)
assert.Nil(t, sb.Annotations)
}
@@ -161,7 +195,7 @@ func TestSandbox_Create_NilSpec(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
sb, err := sc.Create(ctx, "default", "test-sb", nil, nil)
sb, err := sc.Create(ctx, "default", "test-sb", nil)
require.NoError(t, err)
assert.Equal(t, "test-sb", sb.Name)
}
@@ -170,7 +204,7 @@ func TestSandbox_Get(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
_, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{LogLevel: "info"}, nil)
_, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{LogLevel: "info"})
require.NoError(t, err)
got, err := sc.Get(ctx, "default", "test-sb")
@@ -201,8 +235,8 @@ func TestSandbox_List(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
_, _ = sc.Create(ctx, "default", "sb-1", &types.SandboxSpec{}, nil)
_, _ = sc.Create(ctx, "default", "sb-2", &types.SandboxSpec{}, nil)
_, _ = sc.Create(ctx, "default", "sb-1", &types.SandboxSpec{})
_, _ = sc.Create(ctx, "default", "sb-2", &types.SandboxSpec{})
list, err := sc.ListAll(ctx, "default")
require.NoError(t, err)
@@ -213,7 +247,7 @@ func TestSandbox_Delete(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
_, _ = sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
_, _ = sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
_, err := sc.Delete(ctx, "default", "test-sb")
require.NoError(t, err)
@@ -242,7 +276,7 @@ func TestSandbox_DeepCopy_OnCreate(t *testing.T) {
Environment: map[string]string{"KEY": "value"},
}
sb, err := sc.Create(ctx, "default", "test-sb", spec, labels)
sb, err := sc.Create(ctx, "default", "test-sb", spec, types.WithLabels(labels))
require.NoError(t, err)
// Mutating inputs should not affect stored object
@@ -269,7 +303,7 @@ func TestSandbox_DeepCopy_OnGet(t *testing.T) {
_, _ = sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{
Environment: map[string]string{"KEY": "value"},
}, nil)
})
got, err := sc.Get(ctx, "default", "test-sb")
require.NoError(t, err)
@@ -287,7 +321,7 @@ func TestSandbox_WaitReady(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
_, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
_, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
require.NoError(t, err)
sb, err := sc.WaitReady(ctx, "default", "test-sb")
@@ -312,7 +346,7 @@ func TestSandbox_WaitReady_NotFound(t *testing.T) {
func TestSandbox_WaitReady_ContextCancellation(t *testing.T) {
sc := newTestSandboxClient()
_, err := sc.Create(context.Background(), "default", "test-sb", &types.SandboxSpec{}, nil)
_, err := sc.Create(context.Background(), "default", "test-sb", &types.SandboxSpec{})
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
@@ -327,7 +361,7 @@ func TestSandbox_WaitReady_ContextCancellation(t *testing.T) {
func TestSandbox_WaitReady_ContextDeadlineExceeded(t *testing.T) {
sc := newTestSandboxClient()
_, err := sc.Create(context.Background(), "default", "test-sb", &types.SandboxSpec{}, nil)
_, err := sc.Create(context.Background(), "default", "test-sb", &types.SandboxSpec{})
require.NoError(t, err)
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
@@ -342,7 +376,7 @@ func TestSandbox_WaitReady_AlreadyReady(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
_, _ = sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
_, _ = sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
// Make it ready
_, err := sc.WaitReady(ctx, "default", "test-sb")
@@ -358,7 +392,7 @@ func TestSandbox_WaitReady_IncrementsResourceVersion(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
created, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
created, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
require.NoError(t, err)
initialVersion := created.ResourceVersion
@@ -370,7 +404,7 @@ func TestSandbox_WaitReady_IncrementsResourceVersion(t *testing.T) {
func TestSandbox_WaitReady_ContextTimeout(t *testing.T) {
sc := newTestSandboxClient()
_, err := sc.Create(context.Background(), "default", "test-sb", &types.SandboxSpec{}, nil)
_, err := sc.Create(context.Background(), "default", "test-sb", &types.SandboxSpec{})
require.NoError(t, err)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
@@ -394,7 +428,7 @@ func TestSandbox_Watch_AddedOnCreate(t *testing.T) {
require.NoError(t, err)
defer w.Stop()
_, err = sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{LogLevel: "info"}, nil)
_, err = sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{LogLevel: "info"})
require.NoError(t, err)
select {
@@ -411,7 +445,7 @@ func TestSandbox_Watch_DeletedOnDelete(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
_, _ = sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
_, _ = sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
w, err := sc.Watch(ctx, "default", "")
require.NoError(t, err)
@@ -433,7 +467,7 @@ func TestSandbox_Watch_ModifiedOnWaitReady(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
_, _ = sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
_, _ = sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
w, err := sc.Watch(ctx, "default", "")
require.NoError(t, err)
@@ -461,10 +495,10 @@ func TestSandbox_Watch_NameFiltering(t *testing.T) {
defer w.Stop()
// Create "beta" — should not be received
_, _ = sc.Create(ctx, "default", "beta", &types.SandboxSpec{}, nil)
_, _ = sc.Create(ctx, "default", "beta", &types.SandboxSpec{})
// Create "alpha" — should be received
_, _ = sc.Create(ctx, "default", "alpha", &types.SandboxSpec{}, nil)
_, _ = sc.Create(ctx, "default", "alpha", &types.SandboxSpec{})
select {
case ev := <-w.ResultChan():
@@ -487,7 +521,7 @@ func TestSandbox_Watch_MultipleWatchers(t *testing.T) {
require.NoError(t, err)
defer w2.Stop()
_, _ = sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
_, _ = sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
for _, w := range []types.WatchInterface[*types.Sandbox]{w1, w2} {
select {
@@ -517,7 +551,7 @@ func TestSandbox_Watch_DeletedEventContainsFullObject(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
_, _ = sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{LogLevel: "debug"}, map[string]string{"env": "test"})
_, _ = sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{LogLevel: "debug"}, types.WithLabels(map[string]string{"env": "test"}))
w, err := sc.Watch(ctx, "default", "")
require.NoError(t, err)
@@ -565,7 +599,7 @@ func TestSandbox_ConcurrentCreateGetDeleteWatch(t *testing.T) {
defer wg.Done()
for j := 0; j < opsPerGoroutine; j++ {
name := fmt.Sprintf("sb-%d-%d", id, j)
_, _ = sc.Create(ctx, "default", name, &types.SandboxSpec{LogLevel: "info"}, nil)
_, _ = sc.Create(ctx, "default", name, &types.SandboxSpec{LogLevel: "info"})
_, _ = sc.Get(ctx, "default", name)
_, _ = sc.ListAll(ctx, "default")
_, _ = sc.WaitReady(ctx, "default", name)
@@ -586,7 +620,7 @@ func TestSandbox_AttachProvider(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
sb, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
sb, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
require.NoError(t, err)
result, err := sc.AttachProvider(ctx, "default", "test-sb", "openai", sb.ResourceVersion)
@@ -600,7 +634,7 @@ func TestSandbox_AttachProvider_AlreadyAttached(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
sb, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
sb, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
require.NoError(t, err)
result, err := sc.AttachProvider(ctx, "default", "test-sb", "openai", sb.ResourceVersion)
@@ -626,7 +660,7 @@ func TestSandbox_DetachProvider(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
sb, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
sb, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
require.NoError(t, err)
result, err := sc.AttachProvider(ctx, "default", "test-sb", "openai", sb.ResourceVersion)
@@ -642,7 +676,7 @@ func TestSandbox_DetachProvider_NotAttached(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
sb, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
sb, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
require.NoError(t, err)
result, err := sc.DetachProvider(ctx, "default", "test-sb", "openai", sb.ResourceVersion)
@@ -663,7 +697,7 @@ func TestSandbox_ListProviders(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
sb, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
sb, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
require.NoError(t, err)
// No providers yet
@@ -709,7 +743,7 @@ func TestSandbox_AttachProvider_BroadcastsModified(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
sb, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
sb, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
require.NoError(t, err)
w, err := sc.Watch(ctx, "default", "")
@@ -734,7 +768,7 @@ func TestSandbox_Watch_StopOnTerminal_Ready(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
_, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
_, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
require.NoError(t, err)
w, err := sc.Watch(ctx, "default", "test-sb", v1.WatchOptions{StopOnTerminal: true})
@@ -759,7 +793,7 @@ func TestSandbox_Watch_StopOnTerminal_Error(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
sb, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
sb, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
require.NoError(t, err)
w, err := sc.Watch(ctx, "default", "test-sb", v1.WatchOptions{StopOnTerminal: true})
@@ -789,7 +823,7 @@ func TestSandbox_Watch_StopOnTerminal_False_DoesNotClose(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
_, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{}, nil)
_, err := sc.Create(ctx, "default", "test-sb", &types.SandboxSpec{})
require.NoError(t, err)
// Watch WITHOUT StopOnTerminal
@@ -854,7 +888,7 @@ func TestFakeSandboxCreateWithPolicy(t *testing.T) {
},
}
created, err := sc.Create(ctx, "default", "policy-sb", spec, nil)
created, err := sc.Create(ctx, "default", "policy-sb", spec)
require.NoError(t, err)
// Verify created sandbox has policy
@@ -911,7 +945,7 @@ func TestFakeSandboxCreateWithNilPolicy(t *testing.T) {
sc := newTestSandboxClient()
ctx := context.Background()
created, err := sc.Create(ctx, "default", "no-policy-sb", &types.SandboxSpec{LogLevel: "info"}, nil)
created, err := sc.Create(ctx, "default", "no-policy-sb", &types.SandboxSpec{LogLevel: "info"})
require.NoError(t, err)
assert.Nil(t, created.Spec.Policy)
@@ -938,7 +972,7 @@ func TestFakeSandboxCreateFromTemplatePreservesCommandAndTTY(t *testing.T) {
TTY: true,
}
created, err := sc.CreateFromTemplate(ctx, "default", "job-1", "gpu-kata", spec, map[string]string{"team": "runtime"})
created, err := sc.CreateFromTemplate(ctx, "default", "job-1", "gpu-kata", spec, types.WithLabels(map[string]string{"team": "runtime"}))
require.NoError(t, err)
assert.Equal(t, []string{"/opt/worker", "--serve"}, created.Spec.Command)
+1 -1
View File
@@ -52,7 +52,7 @@ func TestIntegration_SandboxExecSmoke(t *testing.T) {
_, err = client.Sandboxes().Create(ctx, "default", name, &SandboxSpec{
Template: &SandboxTemplate{Image: image},
}, nil)
})
require.NoError(t, err)
t.Cleanup(func() {
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 2*time.Minute)
+11 -2
View File
@@ -9,8 +9,17 @@ import (
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
)
// CreateOptions configures resource creation.
type CreateOptions = types.CreateOptions
// CreateOption configures a Create call.
type CreateOption = types.CreateOption
// WithLabels sets labels on the created resource.
var WithLabels = types.WithLabels
// WithAnnotations sets annotations on the created resource.
var WithAnnotations = types.WithAnnotations
// WithServiceExposures registers loopback HTTP services with the created sandbox.
var WithServiceExposures = types.WithServiceExposures
// ListOptions configures resource listing with pagination and filtering.
type ListOptions = types.ListOptions
+2 -2
View File
@@ -86,7 +86,7 @@ var WithLogMinLevel = types.WithLogMinLevel
// SandboxInterface defines lifecycle operations on sandboxes.
type SandboxInterface interface {
Create(ctx context.Context, workspace, name string, spec *SandboxSpec, labels map[string]string, opts ...CreateOptions) (*Sandbox, error)
Create(ctx context.Context, workspace, name string, spec *SandboxSpec, opts ...CreateOption) (*Sandbox, error)
Get(ctx context.Context, workspace, name string) (*Sandbox, error)
List(workspace string, opts ...ListOptions) (*Pager[*Sandbox], error)
ListAll(ctx context.Context, workspace string, opts ...ListOptions) ([]*Sandbox, error)
@@ -108,5 +108,5 @@ type SandboxInterface interface {
// SandboxTemplateCreateInterface defines additive sandbox creation from named
// workload templates without widening SandboxInterface.
type SandboxTemplateCreateInterface interface {
CreateFromTemplate(ctx context.Context, workspace, name, templateName string, spec *SandboxSpec, labels map[string]string, opts ...CreateOptions) (*Sandbox, error)
CreateFromTemplate(ctx context.Context, workspace, name, templateName string, spec *SandboxSpec, opts ...CreateOption) (*Sandbox, error)
}
+13 -15
View File
@@ -28,20 +28,19 @@ func newSandboxClient(conn grpc.ClientConnInterface) *sandboxClient {
return &sandboxClient{client: pb.NewOpenShellClient(conn)}
}
func (s *sandboxClient) Create(ctx context.Context, workspace, name string, spec *SandboxSpec, labels map[string]string, opts ...CreateOptions) (*Sandbox, error) {
func (s *sandboxClient) Create(ctx context.Context, workspace, name string, spec *SandboxSpec, opts ...CreateOption) (*Sandbox, error) {
protoSpec, err := converter.SandboxSpecToProtoChecked(spec)
if err != nil {
return nil, &StatusError{Code: ErrorInvalidArgument, Message: err.Error()}
}
cfg := types.ApplyCreateOptions(opts)
req := &pb.CreateSandboxRequest{
Name: name,
Spec: protoSpec,
Labels: labels,
WorkspaceScope: namedWorkspaceScope(workspace),
}
if len(opts) > 0 {
req.Annotations = converter.CopyStringMap(opts[0].Annotations)
req.ServiceExposures = serviceExposuresToProto(opts[0].ServiceExposures)
Name: name,
Spec: protoSpec,
Labels: converter.CopyStringMap(cfg.Labels()),
Annotations: converter.CopyStringMap(cfg.Annotations()),
WorkspaceScope: namedWorkspaceScope(workspace),
ServiceExposures: serviceExposuresToProto(cfg.ServiceExposures()),
}
resp, err := s.client.CreateSandbox(ctx, req)
if err != nil {
@@ -54,7 +53,7 @@ func (s *sandboxClient) Create(ctx context.Context, workspace, name string, spec
return sandbox, nil
}
func (s *sandboxClient) CreateFromTemplate(ctx context.Context, workspace, name, templateName string, spec *SandboxSpec, labels map[string]string, opts ...CreateOptions) (*Sandbox, error) {
func (s *sandboxClient) CreateFromTemplate(ctx context.Context, workspace, name, templateName string, spec *SandboxSpec, opts ...CreateOption) (*Sandbox, error) {
if templateName == "" {
return nil, &StatusError{Code: ErrorInvalidArgument, Message: "template name is required"}
}
@@ -65,16 +64,15 @@ func (s *sandboxClient) CreateFromTemplate(ctx context.Context, workspace, name,
if err != nil {
return nil, &StatusError{Code: ErrorInvalidArgument, Message: err.Error()}
}
cfg := types.ApplyCreateOptions(opts)
req := &pb.CreateSandboxRequest{
Name: name,
Spec: protoSpec,
Labels: labels,
Labels: converter.CopyStringMap(cfg.Labels()),
Annotations: converter.CopyStringMap(cfg.Annotations()),
WorkspaceScope: namedWorkspaceScope(workspace),
WorkloadTemplate: templateName,
}
if len(opts) > 0 {
req.Annotations = converter.CopyStringMap(opts[0].Annotations)
req.ServiceExposures = serviceExposuresToProto(opts[0].ServiceExposures)
ServiceExposures: serviceExposuresToProto(cfg.ServiceExposures()),
}
resp, err := s.client.CreateSandbox(ctx, req)
if err != nil {
+67 -10
View File
@@ -315,15 +315,15 @@ func TestSandboxCreate(t *testing.T) {
"default",
"my-sandbox",
spec,
labels,
CreateOptions{ServiceExposures: []ServiceExposure{
{TargetPort: 4500},
{
WithLabels(labels),
WithServiceExposures(
ServiceExposure{TargetPort: 4500},
ServiceExposure{
Service: "metrics",
TargetPort: 9090,
AuthorizationMode: ServiceAuthorizationModeBearerPassthrough,
},
}},
),
)
require.NoError(t, err)
@@ -343,6 +343,63 @@ func TestSandboxCreate(t *testing.T) {
assert.Equal(t, pb.ServiceAuthorizationMode_SERVICE_AUTHORIZATION_MODE_BEARER_PASSTHROUGH, mock.createRequest.GetServiceExposures()[1].GetAuthorizationMode())
}
// Callers that passed nil for the old positional labels parameter still compile
// against the variadic signature, so the nil option must be ignored rather than
// invoked.
func TestSandboxCreate_LegacyTrailingNil(t *testing.T) {
mock := newMockSandboxServer()
client, cleanup := setupSandboxTest(t, mock)
defer cleanup()
result, err := client.Create(context.Background(), "default", "legacy-sandbox", &SandboxSpec{}, nil)
require.NoError(t, err)
require.NotNil(t, result)
mock.mu.Lock()
defer mock.mu.Unlock()
require.NotNil(t, mock.createRequest)
assert.Empty(t, mock.createRequest.Labels)
assert.Empty(t, mock.createRequest.Annotations)
}
func TestSandboxCreate_WithAnnotations(t *testing.T) {
mock := newMockSandboxServer()
client, cleanup := setupSandboxTest(t, mock)
defer cleanup()
annotations := map[string]string{"purpose": "ci-test"}
result, err := client.Create(context.Background(), "default", "ann-sandbox", &SandboxSpec{}, WithAnnotations(annotations))
require.NoError(t, err)
require.NotNil(t, result)
mock.mu.Lock()
defer mock.mu.Unlock()
require.NotNil(t, mock.createRequest)
assert.Equal(t, map[string]string{"purpose": "ci-test"}, mock.createRequest.Annotations)
}
func TestSandboxCreate_WithLabelsAndAnnotations(t *testing.T) {
mock := newMockSandboxServer()
client, cleanup := setupSandboxTest(t, mock)
defer cleanup()
labels := map[string]string{"env": "staging"}
annotations := map[string]string{"owner": "team-a"}
result, err := client.Create(context.Background(), "default", "both-sandbox", &SandboxSpec{},
WithLabels(labels), WithAnnotations(annotations))
require.NoError(t, err)
require.NotNil(t, result)
mock.mu.Lock()
defer mock.mu.Unlock()
require.NotNil(t, mock.createRequest)
assert.Equal(t, map[string]string{"env": "staging"}, mock.createRequest.Labels)
assert.Equal(t, map[string]string{"owner": "team-a"}, mock.createRequest.Annotations)
}
func TestSandboxCreate_DefaultGPURequest(t *testing.T) {
mock := newMockSandboxServer()
client, cleanup := setupSandboxTest(t, mock)
@@ -350,7 +407,7 @@ func TestSandboxCreate_DefaultGPURequest(t *testing.T) {
result, err := client.Create(context.Background(), "default", "gpu-sandbox", &SandboxSpec{
GPU: true,
}, nil)
})
require.NoError(t, err)
require.NotNil(t, result)
@@ -373,7 +430,7 @@ func TestSandboxCreate_RejectsUnrepresentableResourcesBeforeRPC(t *testing.T) {
_, err := client.Create(context.Background(), "default", "bad", &SandboxSpec{
Template: &SandboxTemplate{Resources: map[string]any{"invalid": make(chan int)}},
}, nil)
})
require.Error(t, err)
assert.True(t, IsInvalidArgument(err))
mock.mu.Lock()
@@ -388,7 +445,7 @@ func TestSandboxCreateFromTemplateRejectsGPUOverrideBeforeRPC(t *testing.T) {
_, err := client.CreateFromTemplate(context.Background(), "default", "bad", "gpu-kata", &SandboxSpec{
GPU: true,
}, nil)
})
require.Error(t, err)
assert.True(t, IsInvalidArgument(err))
@@ -406,7 +463,7 @@ func TestSandboxCreateFromTemplateSendsCommandAndTTY(t *testing.T) {
Providers: []string{"github"},
Command: []string{"/opt/worker", "--serve"},
TTY: true,
}, map[string]string{"team": "runtime"})
}, WithLabels(map[string]string{"team": "runtime"}))
require.NoError(t, err)
require.NotNil(t, result)
@@ -428,7 +485,7 @@ func TestSandboxCreate_AlreadyExists(t *testing.T) {
client, cleanup := setupSandboxTest(t, mock)
defer cleanup()
_, err := client.Create(context.Background(), "default", "dup", &SandboxSpec{}, nil)
_, err := client.Create(context.Background(), "default", "dup", &SandboxSpec{})
require.Error(t, err)
assert.True(t, IsAlreadyExists(err))
+1 -1
View File
@@ -138,7 +138,7 @@ type mockSandboxResolver struct {
err error
}
func (m *mockSandboxResolver) Create(_ context.Context, _, _ string, _ *SandboxSpec, _ map[string]string, _ ...CreateOptions) (*Sandbox, error) {
func (m *mockSandboxResolver) Create(_ context.Context, _, _ string, _ *SandboxSpec, _ ...CreateOption) (*Sandbox, error) {
return nil, nil
}
+1 -1
View File
@@ -993,7 +993,7 @@ func (r *flippableResolver) Get(_ context.Context, _, name string) (*Sandbox, er
return &Sandbox{ID: "sb-" + name, Name: name}, nil
}
func (r *flippableResolver) Create(context.Context, string, string, *SandboxSpec, map[string]string, ...CreateOptions) (*Sandbox, error) {
func (r *flippableResolver) Create(context.Context, string, string, *SandboxSpec, ...CreateOption) (*Sandbox, error) {
panic("not implemented")
}
func (r *flippableResolver) List(string, ...ListOptions) (*Pager[*Sandbox], error) {
+59 -6
View File
@@ -3,13 +3,66 @@
package types
import "time"
import (
"time"
// CreateOptions configures resource creation.
type CreateOptions struct {
Annotations map[string]string
// ServiceExposures are loopback HTTP services registered with the sandbox.
ServiceExposures []ServiceExposure
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/internal/options"
)
// createConfig holds configuration for Create calls.
type createConfig struct {
labels map[string]string
annotations map[string]string
serviceExposures []ServiceExposure
}
// CreateOption configures a Create call.
type CreateOption func(*createConfig)
// WithLabels sets labels on the created resource.
func WithLabels(labels map[string]string) CreateOption {
return func(c *createConfig) {
c.labels = labels
}
}
// WithAnnotations sets annotations on the created resource.
func WithAnnotations(annotations map[string]string) CreateOption {
return func(c *createConfig) {
c.annotations = annotations
}
}
// WithServiceExposures registers loopback HTTP services with the created
// sandbox. Leave Service empty for the unnamed endpoint.
func WithServiceExposures(exposures ...ServiceExposure) CreateOption {
return func(c *createConfig) {
c.serviceExposures = exposures
}
}
// ApplyCreateOptions applies options and returns the config. Nil options are
// ignored so that callers migrating from the positional labels parameter, which
// they passed as nil for an unlabeled sandbox, do not panic.
func ApplyCreateOptions(opts []CreateOption) createConfig { //nolint:revive // unexported return is intentional; consumed only by v1 package
var cfg createConfig
options.Apply(&cfg, opts)
return cfg
}
// Labels returns the configured labels.
func (c *createConfig) Labels() map[string]string {
return c.labels
}
// Annotations returns the configured annotations.
func (c *createConfig) Annotations() map[string]string {
return c.annotations
}
// ServiceExposures returns the configured service exposures.
func (c *createConfig) ServiceExposures() []ServiceExposure {
return c.serviceExposures
}
// ListOptions configures resource listing with pagination and filtering.
+40
View File
@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package types
import (
"testing"
"github.com/stretchr/testify/assert"
)
// Callers migrating from the positional labels parameter passed nil for an
// unlabeled sandbox. That nil still satisfies CreateOption, so it reaches
// ApplyCreateOptions and must not be invoked.
func TestApplyCreateOptions_IgnoresNilOption(t *testing.T) {
cfg := ApplyCreateOptions([]CreateOption{nil})
assert.Nil(t, cfg.Labels())
assert.Nil(t, cfg.Annotations())
}
func TestApplyCreateOptions_IgnoresNilAmongRealOptions(t *testing.T) {
cfg := ApplyCreateOptions([]CreateOption{
nil,
WithLabels(map[string]string{"env": "dev"}),
nil,
WithAnnotations(map[string]string{"source": "cli"}),
nil,
})
assert.Equal(t, map[string]string{"env": "dev"}, cfg.Labels())
assert.Equal(t, map[string]string{"source": "cli"}, cfg.Annotations())
}
func TestApplyCreateOptions_NoOptions(t *testing.T) {
cfg := ApplyCreateOptions(nil)
assert.Nil(t, cfg.Labels())
assert.Nil(t, cfg.Annotations())
}