mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-31 07:44:57 +08:00
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:
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"}),
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -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"}),
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
Reference in New Issue
Block a user