From c6506d70edc1e2f78b19847376464d96eadd6cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Sat, 5 Sep 2026 15:30:02 +0200 Subject: [PATCH] feat(sdk/go)!: replace positional labels with functional Create options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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ß --- sdk/go/README.md | 2 +- sdk/go/docs/src/api/fake.md | 2 +- sdk/go/docs/src/api/sandboxes.md | 6 +- sdk/go/docs/src/error-handling.md | 2 +- sdk/go/docs/src/getting-started.md | 2 +- sdk/go/docs/src/testing.md | 8 +- sdk/go/openshell/v1/client.go | 6 +- sdk/go/openshell/v1/doc.go | 4 +- sdk/go/openshell/v1/example_fake_test.go | 4 +- sdk/go/openshell/v1/example_test.go | 6 +- sdk/go/openshell/v1/exec_client_test.go | 2 +- sdk/go/openshell/v1/fake/fake.go | 4 +- sdk/go/openshell/v1/fake/fake_test.go | 2 +- sdk/go/openshell/v1/fake/sandbox.go | 22 ++--- .../v1/fake/sandbox_template_test.go | 3 +- sdk/go/openshell/v1/fake/sandbox_test.go | 82 +++++++++---------- sdk/go/openshell/v1/integration_test.go | 2 +- sdk/go/openshell/v1/options.go | 10 ++- sdk/go/openshell/v1/sandbox.go | 4 +- sdk/go/openshell/v1/sandbox_client.go | 24 +++--- sdk/go/openshell/v1/sandbox_client_test.go | 49 +++++++++-- sdk/go/openshell/v1/ssh_client_test.go | 2 +- sdk/go/openshell/v1/tcp_client_test.go | 2 +- sdk/go/openshell/v1/types/options.go | 43 +++++++++- 24 files changed, 182 insertions(+), 111 deletions(-) diff --git a/sdk/go/README.md b/sdk/go/README.md index e011af01d8..d0228f7b70 100644 --- a/sdk/go/README.md +++ b/sdk/go/README.md @@ -54,7 +54,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) } diff --git a/sdk/go/docs/src/api/fake.md b/sdk/go/docs/src/api/fake.md index ae96fc9d39..ee646f5329 100644 --- a/sdk/go/docs/src/api/fake.md +++ b/sdk/go/docs/src/api/fake.md @@ -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) diff --git a/sdk/go/docs/src/api/sandboxes.md b/sdk/go/docs/src/api/sandboxes.md index 698982fe13..26f6a5bba8 100644 --- a/sdk/go/docs/src/api/sandboxes.md +++ b/sdk/go/docs/src/api/sandboxes.md @@ -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,9 +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", -}) +})) ``` Set `GPU: true` to request the active driver's default GPU assignment. Set diff --git a/sdk/go/docs/src/error-handling.md b/sdk/go/docs/src/error-handling.md index f46b796513..e57d2962f4 100644 --- a/sdk/go/docs/src/error-handling.md +++ b/sdk/go/docs/src/error-handling.md @@ -55,7 +55,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) diff --git a/sdk/go/docs/src/getting-started.md b/sdk/go/docs/src/getting-started.md index 31530c914c..693502759b 100644 --- a/sdk/go/docs/src/getting-started.md +++ b/sdk/go/docs/src/getting-started.md @@ -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) } diff --git a/sdk/go/docs/src/testing.md b/sdk/go/docs/src/testing.md index af1851ae83..544ef69393 100644 --- a/sdk/go/docs/src/testing.md +++ b/sdk/go/docs/src/testing.md @@ -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 diff --git a/sdk/go/openshell/v1/client.go b/sdk/go/openshell/v1/client.go index 06d8183360..443c07620f 100644 --- a/sdk/go/openshell/v1/client.go +++ b/sdk/go/openshell/v1/client.go @@ -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 @@ -125,8 +125,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. diff --git a/sdk/go/openshell/v1/doc.go b/sdk/go/openshell/v1/doc.go index d088ae68fc..e2515c7cef 100644 --- a/sdk/go/openshell/v1/doc.go +++ b/sdk/go/openshell/v1/doc.go @@ -24,7 +24,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) // } @@ -298,7 +298,7 @@ // }, // }, // }, -// }, nil) +// }) // // Replace the full policy at runtime via configuration update: // diff --git a/sdk/go/openshell/v1/example_fake_test.go b/sdk/go/openshell/v1/example_fake_test.go index c59256413e..9ae9122292 100644 --- a/sdk/go/openshell/v1/example_fake_test.go +++ b/sdk/go/openshell/v1/example_fake_test.go @@ -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) } diff --git a/sdk/go/openshell/v1/example_test.go b/sdk/go/openshell/v1/example_test.go index eb96fe8c11..3659cbed3f 100644 --- a/sdk/go/openshell/v1/example_test.go +++ b/sdk/go/openshell/v1/example_test.go @@ -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") } diff --git a/sdk/go/openshell/v1/exec_client_test.go b/sdk/go/openshell/v1/exec_client_test.go index 1c649ac0b7..15ae05e679 100644 --- a/sdk/go/openshell/v1/exec_client_test.go +++ b/sdk/go/openshell/v1/exec_client_test.go @@ -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(context.Context, string, ...ListOptions) ([]*Sandbox, error) { diff --git a/sdk/go/openshell/v1/fake/fake.go b/sdk/go/openshell/v1/fake/fake.go index 16a35fed8a..1e4d8733be 100644 --- a/sdk/go/openshell/v1/fake/fake.go +++ b/sdk/go/openshell/v1/fake/fake.go @@ -120,8 +120,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. diff --git a/sdk/go/openshell/v1/fake/fake_test.go b/sdk/go/openshell/v1/fake/fake_test.go index d749b1eac4..d8901219ac 100644 --- a/sdk/go/openshell/v1/fake/fake_test.go +++ b/sdk/go/openshell/v1/fake/fake_test.go @@ -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)) } diff --git a/sdk/go/openshell/v1/fake/sandbox.go b/sdk/go/openshell/v1/fake/sandbox.go index 5de3062d96..49c1ad5bff 100644 --- a/sdk/go/openshell/v1/fake/sandbox.go +++ b/sdk/go/openshell/v1/fake/sandbox.go @@ -285,7 +285,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"} } @@ -294,17 +294,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{ @@ -327,7 +324,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"} } @@ -345,10 +342,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) @@ -360,8 +354,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{ diff --git a/sdk/go/openshell/v1/fake/sandbox_template_test.go b/sdk/go/openshell/v1/fake/sandbox_template_test.go index 06a5187227..a45e15828a 100644 --- a/sdk/go/openshell/v1/fake/sandbox_template_test.go +++ b/sdk/go/openshell/v1/fake/sandbox_template_test.go @@ -245,7 +245,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) @@ -358,7 +358,6 @@ func TestSandboxTemplate_CreateSandboxFromTemplatePreservesDefaultGPURequest(t * "job-default-gpu", "default-gpu", nil, - nil, ) require.NoError(t, err) diff --git a/sdk/go/openshell/v1/fake/sandbox_test.go b/sdk/go/openshell/v1/fake/sandbox_test.go index 75ff32da2d..c652d54588 100644 --- a/sdk/go/openshell/v1/fake/sandbox_test.go +++ b/sdk/go/openshell/v1/fake/sandbox_test.go @@ -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,10 +45,10 @@ 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)) } @@ -57,8 +57,8 @@ 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 +73,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 +85,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 +161,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 +170,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 +201,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.List(ctx, "default") require.NoError(t, err) @@ -213,7 +213,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 +242,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 +269,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 +287,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 +312,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 +327,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 +342,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 +358,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 +370,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 +394,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 +411,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 +433,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 +461,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 +487,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 +517,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 +565,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.List(ctx, "default") _, _ = sc.WaitReady(ctx, "default", name) @@ -586,7 +586,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 +600,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 +626,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 +642,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 +663,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 @@ -703,7 +703,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", "") @@ -728,7 +728,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}) @@ -753,7 +753,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}) @@ -783,7 +783,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 @@ -848,7 +848,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 @@ -905,7 +905,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) @@ -932,7 +932,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) diff --git a/sdk/go/openshell/v1/integration_test.go b/sdk/go/openshell/v1/integration_test.go index d4c0beeafc..ecc184e059 100644 --- a/sdk/go/openshell/v1/integration_test.go +++ b/sdk/go/openshell/v1/integration_test.go @@ -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) diff --git a/sdk/go/openshell/v1/options.go b/sdk/go/openshell/v1/options.go index caac82c96b..2920bde0e1 100644 --- a/sdk/go/openshell/v1/options.go +++ b/sdk/go/openshell/v1/options.go @@ -7,8 +7,14 @@ 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 // ListOptions configures resource listing with pagination and filtering. type ListOptions = types.ListOptions diff --git a/sdk/go/openshell/v1/sandbox.go b/sdk/go/openshell/v1/sandbox.go index 79174ac7f3..6b5335eb19 100644 --- a/sdk/go/openshell/v1/sandbox.go +++ b/sdk/go/openshell/v1/sandbox.go @@ -53,7 +53,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(ctx context.Context, workspace string, opts ...ListOptions) ([]*Sandbox, error) Stop(ctx context.Context, workspace, name string) (*Sandbox, error) @@ -71,5 +71,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) } diff --git a/sdk/go/openshell/v1/sandbox_client.go b/sdk/go/openshell/v1/sandbox_client.go index 75d8d4caa3..d325f72419 100644 --- a/sdk/go/openshell/v1/sandbox_client.go +++ b/sdk/go/openshell/v1/sandbox_client.go @@ -28,19 +28,18 @@ 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, - Workspace: workspace, - } - if len(opts) > 0 { - req.Annotations = converter.CopyStringMap(opts[0].Annotations) + Name: name, + Spec: protoSpec, + Labels: converter.CopyStringMap(cfg.Labels()), + Annotations: converter.CopyStringMap(cfg.Annotations()), + Workspace: workspace, } resp, err := s.client.CreateSandbox(ctx, req) if err != nil { @@ -49,7 +48,7 @@ func (s *sandboxClient) Create(ctx context.Context, workspace, name string, spec return converter.SandboxFromProto(resp.GetSandbox()), 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"} } @@ -60,16 +59,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()), Workspace: workspace, WorkloadTemplateName: templateName, } - if len(opts) > 0 { - req.Annotations = converter.CopyStringMap(opts[0].Annotations) - } resp, err := s.client.CreateSandbox(ctx, req) if err != nil { return nil, converter.FromGRPCError(err) diff --git a/sdk/go/openshell/v1/sandbox_client_test.go b/sdk/go/openshell/v1/sandbox_client_test.go index 7926b28a16..cb0a26785e 100644 --- a/sdk/go/openshell/v1/sandbox_client_test.go +++ b/sdk/go/openshell/v1/sandbox_client_test.go @@ -257,7 +257,7 @@ func TestSandboxCreate(t *testing.T) { } labels := map[string]string{"env": "dev"} - result, err := client.Create(context.Background(), "default", "my-sandbox", spec, labels) + result, err := client.Create(context.Background(), "default", "my-sandbox", spec, WithLabels(labels)) require.NoError(t, err) require.NotNil(t, result) @@ -267,6 +267,43 @@ func TestSandboxCreate(t *testing.T) { assert.Equal(t, SandboxProvisioning, result.Status.Phase) } +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) @@ -274,7 +311,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) @@ -297,7 +334,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() @@ -312,7 +349,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)) @@ -330,7 +367,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) @@ -352,7 +389,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)) diff --git a/sdk/go/openshell/v1/ssh_client_test.go b/sdk/go/openshell/v1/ssh_client_test.go index 5b600b3370..0354bde78f 100644 --- a/sdk/go/openshell/v1/ssh_client_test.go +++ b/sdk/go/openshell/v1/ssh_client_test.go @@ -134,7 +134,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 } diff --git a/sdk/go/openshell/v1/tcp_client_test.go b/sdk/go/openshell/v1/tcp_client_test.go index e445ab2fab..08a6589869 100644 --- a/sdk/go/openshell/v1/tcp_client_test.go +++ b/sdk/go/openshell/v1/tcp_client_test.go @@ -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(context.Context, string, ...ListOptions) ([]*Sandbox, error) { diff --git a/sdk/go/openshell/v1/types/options.go b/sdk/go/openshell/v1/types/options.go index b0f6145999..e54a8d3de4 100644 --- a/sdk/go/openshell/v1/types/options.go +++ b/sdk/go/openshell/v1/types/options.go @@ -5,9 +5,46 @@ package types import "time" -// CreateOptions configures resource creation. -type CreateOptions struct { - Annotations map[string]string +// createConfig holds configuration for Create calls. +type createConfig struct { + labels map[string]string + annotations map[string]string +} + +// 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 + } +} + +// ApplyCreateOptions applies options and returns the config. +func ApplyCreateOptions(opts []CreateOption) createConfig { //nolint:revive // unexported return is intentional; consumed only by v1 package + var cfg createConfig + for _, opt := range opts { + opt(&cfg) + } + 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 } // ListOptions configures resource listing with pagination and filtering.