From 33511bd3c6c7e03683501987aa68443ed8bfefdb Mon Sep 17 00:00:00 2001 From: harshsharma Date: Tue, 19 May 2026 19:38:58 +0530 Subject: [PATCH 1/2] fix(BREV-9479): inline lifecycle script body when deploying launchable via CLI --- pkg/cmd/gpucreate/gpucreate.go | 24 ++++++++++++++++++++++++ pkg/cmd/gpucreate/gpucreate_test.go | 6 ++++++ pkg/store/workspace.go | 23 +++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/pkg/cmd/gpucreate/gpucreate.go b/pkg/cmd/gpucreate/gpucreate.go index 9950fe86..6034414f 100644 --- a/pkg/cmd/gpucreate/gpucreate.go +++ b/pkg/cmd/gpucreate/gpucreate.go @@ -105,6 +105,7 @@ type GPUCreateStore interface { DeleteWorkspace(workspaceID string) (*entity.Workspace, error) GetAllInstanceTypesWithWorkspaceGroups(orgID string) (*gpusearch.AllInstanceTypesResponse, error) GetLaunchable(launchableID string) (*store.LaunchableResponse, error) + GetLaunchableLifeCycleScript(launchableID, scriptID string) (*store.LifeCycleScriptResponse, error) RedeemCouponCode(organizationID string, code string) (*store.RedeemCouponCodeResponse, error) } @@ -394,6 +395,11 @@ func fetchAndDisplayLaunchable(gpuCreateStore GPUCreateStore, t *terminal.Termin return nil, fmt.Errorf("failed to fetch launchable %q: %w", launchableID, err) } + // Inline the script body; the launchable GET only returns its id. + if err := inlineLaunchableLifeCycleScript(gpuCreateStore, launchableID, info); err != nil { + return nil, err + } + t.Vprintf("Deploying launchable: %q\n", info.Name) if info.Description != "" { t.Vprintf("Description: %s\n", info.Description) @@ -410,6 +416,24 @@ func fetchAndDisplayLaunchable(gpuCreateStore GPUCreateStore, t *terminal.Termin return info, nil } +func inlineLaunchableLifeCycleScript(gpuCreateStore GPUCreateStore, launchableID string, info *store.LaunchableResponse) error { + if info == nil || info.BuildRequest.VMBuild == nil { + return nil + } + attr := info.BuildRequest.VMBuild.LifeCycleScriptAttr + if attr == nil || attr.ID == "" || attr.Script != "" { + return nil + } + resp, err := gpuCreateStore.GetLaunchableLifeCycleScript(launchableID, attr.ID) + if err != nil { + return fmt.Errorf("failed to fetch lifecycle script %q for launchable %q: %w", attr.ID, launchableID, err) + } + if resp != nil && resp.Attrs != nil { + attr.Script = resp.Attrs.Script + } + return nil +} + func launchableBuildModeName(info *store.LaunchableResponse) string { switch { case info.BuildRequest.CustomContainer != nil: diff --git a/pkg/cmd/gpucreate/gpucreate_test.go b/pkg/cmd/gpucreate/gpucreate_test.go index be791982..d315a928 100644 --- a/pkg/cmd/gpucreate/gpucreate_test.go +++ b/pkg/cmd/gpucreate/gpucreate_test.go @@ -110,6 +110,12 @@ func (m *MockGPUCreateStore) GetLaunchable(launchableID string) (*store.Launchab }, nil } +func (m *MockGPUCreateStore) GetLaunchableLifeCycleScript(launchableID, scriptID string) (*store.LifeCycleScriptResponse, error) { + return &store.LifeCycleScriptResponse{ + Attrs: &store.LifeCycleScriptAttr{ID: scriptID, Script: "echo mock-script"}, + }, nil +} + func (m *MockGPUCreateStore) RedeemCouponCode(organizationID string, code string) (*store.RedeemCouponCodeResponse, error) { return &store.RedeemCouponCodeResponse{}, nil } diff --git a/pkg/store/workspace.go b/pkg/store/workspace.go index 35f5d4f1..030eef07 100644 --- a/pkg/store/workspace.go +++ b/pkg/store/workspace.go @@ -283,6 +283,29 @@ func (s AuthHTTPStore) GetLaunchable(launchableID string) (*LaunchableResponse, return &result, nil } +// LifeCycleScriptResponse holds a lifecycle script with the script body populated. +type LifeCycleScriptResponse struct { + Attrs *LifeCycleScriptAttr `json:"attrs"` +} + +// GetLaunchableLifeCycleScript fetches the full lifecycle script for a launchable. +func (s AuthHTTPStore) GetLaunchableLifeCycleScript(launchableID, scriptID string) (*LifeCycleScriptResponse, error) { + var result LifeCycleScriptResponse + res, err := s.authHTTPClient.restyClient.R(). + SetHeader("Content-Type", "application/json"). + SetQueryParam("envId", launchableID). + SetQueryParam("scriptId", scriptID). + SetResult(&result). + Get("api/launchable/lifecycle-script") + if err != nil { + return nil, breverrors.WrapAndTrace(err) + } + if res.IsError() { + return nil, NewHTTPResponseError(res) + } + return &result, nil +} + type GetWorkspacesOptions struct { UserID string Name string From 2472b41ccac9faeb52be8df127d04adbd530b5d1 Mon Sep 17 00:00:00 2001 From: harshsharma Date: Tue, 2 Jun 2026 17:21:25 +0530 Subject: [PATCH 2/2] test(BREV-9479): cover inlineLaunchableLifeCycleScript branches --- pkg/cmd/gpucreate/gpucreate.go | 2 +- pkg/cmd/gpucreate/gpucreate_test.go | 92 ++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/gpucreate/gpucreate.go b/pkg/cmd/gpucreate/gpucreate.go index 6034414f..0c4ec69b 100644 --- a/pkg/cmd/gpucreate/gpucreate.go +++ b/pkg/cmd/gpucreate/gpucreate.go @@ -421,7 +421,7 @@ func inlineLaunchableLifeCycleScript(gpuCreateStore GPUCreateStore, launchableID return nil } attr := info.BuildRequest.VMBuild.LifeCycleScriptAttr - if attr == nil || attr.ID == "" || attr.Script != "" { + if attr == nil || attr.ID == "" { return nil } resp, err := gpuCreateStore.GetLaunchableLifeCycleScript(launchableID, attr.ID) diff --git a/pkg/cmd/gpucreate/gpucreate_test.go b/pkg/cmd/gpucreate/gpucreate_test.go index d315a928..60e845f4 100644 --- a/pkg/cmd/gpucreate/gpucreate_test.go +++ b/pkg/cmd/gpucreate/gpucreate_test.go @@ -14,14 +14,15 @@ import ( // MockGPUCreateStore is a mock implementation of GPUCreateStore for testing type MockGPUCreateStore struct { - User *entity.User - Org *entity.Organization - Workspaces map[string]*entity.Workspace - CreateError error - CreateErrorTypes map[string]error // Errors for specific instance types - DeleteError error - CreatedWorkspaces []*entity.Workspace - DeletedWorkspaceIDs []string + User *entity.User + Org *entity.Organization + Workspaces map[string]*entity.Workspace + CreateError error + CreateErrorTypes map[string]error // Errors for specific instance types + DeleteError error + CreatedWorkspaces []*entity.Workspace + DeletedWorkspaceIDs []string + FetchedLifeCycleScriptIDs []string } func NewMockGPUCreateStore() *MockGPUCreateStore { @@ -111,6 +112,7 @@ func (m *MockGPUCreateStore) GetLaunchable(launchableID string) (*store.Launchab } func (m *MockGPUCreateStore) GetLaunchableLifeCycleScript(launchableID, scriptID string) (*store.LifeCycleScriptResponse, error) { + m.FetchedLifeCycleScriptIDs = append(m.FetchedLifeCycleScriptIDs, scriptID) return &store.LifeCycleScriptResponse{ Attrs: &store.LifeCycleScriptAttr{ID: scriptID, Script: "echo mock-script"}, }, nil @@ -671,3 +673,77 @@ func TestPollUntilReadyReportsWorkspaceFailureMessage(t *testing.T) { assert.ErrorContains(t, err, "instance test failed: unexpected end of JSON input") } + +func TestInlineLaunchableLifeCycleScript(t *testing.T) { + t.Run("fetches and inlines lifecycle script body", func(t *testing.T) { + mockStore := NewMockGPUCreateStore() + info := &store.LaunchableResponse{ + BuildRequest: store.LaunchableBuildRequest{ + VMBuild: &store.VMBuild{ + LifeCycleScriptAttr: &store.LifeCycleScriptAttr{ID: "ls-abc"}, + }, + }, + } + + err := inlineLaunchableLifeCycleScript(mockStore, "env-abc", info) + + assert.NoError(t, err) + assert.Equal(t, []string{"ls-abc"}, mockStore.FetchedLifeCycleScriptIDs) + assert.Equal(t, "echo mock-script", info.BuildRequest.VMBuild.LifeCycleScriptAttr.Script) + }) + + t.Run("skips fetch when info is nil", func(t *testing.T) { + mockStore := NewMockGPUCreateStore() + + err := inlineLaunchableLifeCycleScript(mockStore, "env-abc", nil) + + assert.NoError(t, err) + assert.Empty(t, mockStore.FetchedLifeCycleScriptIDs) + }) + + t.Run("container build skips lifecycle script fetch", func(t *testing.T) { + mockStore := NewMockGPUCreateStore() + info := &store.LaunchableResponse{ + BuildRequest: store.LaunchableBuildRequest{ + CustomContainer: &store.CustomContainer{ContainerURL: "nvcr.io/nvidia/test:latest"}, + }, + } + + err := inlineLaunchableLifeCycleScript(mockStore, "env-abc", info) + + assert.NoError(t, err) + assert.Empty(t, mockStore.FetchedLifeCycleScriptIDs) + }) + + t.Run("skips fetch when launchable has no lifecycle script", func(t *testing.T) { + mockStore := NewMockGPUCreateStore() + info := &store.LaunchableResponse{ + BuildRequest: store.LaunchableBuildRequest{ + VMBuild: &store.VMBuild{ForceJupyterInstall: true}, + }, + } + + err := inlineLaunchableLifeCycleScript(mockStore, "env-abc", info) + + assert.NoError(t, err) + assert.Empty(t, mockStore.FetchedLifeCycleScriptIDs) + assert.Nil(t, info.BuildRequest.VMBuild.LifeCycleScriptAttr) + }) + + t.Run("skips fetch when script ID is empty", func(t *testing.T) { + mockStore := NewMockGPUCreateStore() + info := &store.LaunchableResponse{ + BuildRequest: store.LaunchableBuildRequest{ + VMBuild: &store.VMBuild{ + LifeCycleScriptAttr: &store.LifeCycleScriptAttr{Name: "stale"}, + }, + }, + } + + err := inlineLaunchableLifeCycleScript(mockStore, "env-abc", info) + + assert.NoError(t, err) + assert.Empty(t, mockStore.FetchedLifeCycleScriptIDs) + assert.Equal(t, "", info.BuildRequest.VMBuild.LifeCycleScriptAttr.Script) + }) +}