Skip to content

Commit a62f179

Browse files
fix(BREV-9479): inline lifecycle script body when deploying launchable via CLI (#394)
* fix(BREV-9479): inline lifecycle script body when deploying launchable via CLI * test(BREV-9479): cover inlineLaunchableLifeCycleScript branches
1 parent f6e6b00 commit a62f179

3 files changed

Lines changed: 137 additions & 8 deletions

File tree

pkg/cmd/gpucreate/gpucreate.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ type GPUCreateStore interface {
105105
DeleteWorkspace(workspaceID string) (*entity.Workspace, error)
106106
GetAllInstanceTypesWithWorkspaceGroups(orgID string) (*gpusearch.AllInstanceTypesResponse, error)
107107
GetLaunchable(launchableID string) (*store.LaunchableResponse, error)
108+
GetLaunchableLifeCycleScript(launchableID, scriptID string) (*store.LifeCycleScriptResponse, error)
108109
RedeemCouponCode(organizationID string, code string) (*store.RedeemCouponCodeResponse, error)
109110
}
110111

@@ -394,6 +395,11 @@ func fetchAndDisplayLaunchable(gpuCreateStore GPUCreateStore, t *terminal.Termin
394395
return nil, fmt.Errorf("failed to fetch launchable %q: %w", launchableID, err)
395396
}
396397

398+
// Inline the script body; the launchable GET only returns its id.
399+
if err := inlineLaunchableLifeCycleScript(gpuCreateStore, launchableID, info); err != nil {
400+
return nil, err
401+
}
402+
397403
t.Vprintf("Deploying launchable: %q\n", info.Name)
398404
if info.Description != "" {
399405
t.Vprintf("Description: %s\n", info.Description)
@@ -410,6 +416,24 @@ func fetchAndDisplayLaunchable(gpuCreateStore GPUCreateStore, t *terminal.Termin
410416
return info, nil
411417
}
412418

419+
func inlineLaunchableLifeCycleScript(gpuCreateStore GPUCreateStore, launchableID string, info *store.LaunchableResponse) error {
420+
if info == nil || info.BuildRequest.VMBuild == nil {
421+
return nil
422+
}
423+
attr := info.BuildRequest.VMBuild.LifeCycleScriptAttr
424+
if attr == nil || attr.ID == "" {
425+
return nil
426+
}
427+
resp, err := gpuCreateStore.GetLaunchableLifeCycleScript(launchableID, attr.ID)
428+
if err != nil {
429+
return fmt.Errorf("failed to fetch lifecycle script %q for launchable %q: %w", attr.ID, launchableID, err)
430+
}
431+
if resp != nil && resp.Attrs != nil {
432+
attr.Script = resp.Attrs.Script
433+
}
434+
return nil
435+
}
436+
413437
func launchableBuildModeName(info *store.LaunchableResponse) string {
414438
switch {
415439
case info.BuildRequest.CustomContainer != nil:

pkg/cmd/gpucreate/gpucreate_test.go

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ import (
1414

1515
// MockGPUCreateStore is a mock implementation of GPUCreateStore for testing
1616
type MockGPUCreateStore struct {
17-
User *entity.User
18-
Org *entity.Organization
19-
Workspaces map[string]*entity.Workspace
20-
CreateError error
21-
CreateErrorTypes map[string]error // Errors for specific instance types
22-
DeleteError error
23-
CreatedWorkspaces []*entity.Workspace
24-
DeletedWorkspaceIDs []string
17+
User *entity.User
18+
Org *entity.Organization
19+
Workspaces map[string]*entity.Workspace
20+
CreateError error
21+
CreateErrorTypes map[string]error // Errors for specific instance types
22+
DeleteError error
23+
CreatedWorkspaces []*entity.Workspace
24+
DeletedWorkspaceIDs []string
25+
FetchedLifeCycleScriptIDs []string
2526
}
2627

2728
func NewMockGPUCreateStore() *MockGPUCreateStore {
@@ -110,6 +111,13 @@ func (m *MockGPUCreateStore) GetLaunchable(launchableID string) (*store.Launchab
110111
}, nil
111112
}
112113

114+
func (m *MockGPUCreateStore) GetLaunchableLifeCycleScript(launchableID, scriptID string) (*store.LifeCycleScriptResponse, error) {
115+
m.FetchedLifeCycleScriptIDs = append(m.FetchedLifeCycleScriptIDs, scriptID)
116+
return &store.LifeCycleScriptResponse{
117+
Attrs: &store.LifeCycleScriptAttr{ID: scriptID, Script: "echo mock-script"},
118+
}, nil
119+
}
120+
113121
func (m *MockGPUCreateStore) RedeemCouponCode(organizationID string, code string) (*store.RedeemCouponCodeResponse, error) {
114122
return &store.RedeemCouponCodeResponse{}, nil
115123
}
@@ -665,3 +673,77 @@ func TestPollUntilReadyReportsWorkspaceFailureMessage(t *testing.T) {
665673

666674
assert.ErrorContains(t, err, "instance test failed: unexpected end of JSON input")
667675
}
676+
677+
func TestInlineLaunchableLifeCycleScript(t *testing.T) {
678+
t.Run("fetches and inlines lifecycle script body", func(t *testing.T) {
679+
mockStore := NewMockGPUCreateStore()
680+
info := &store.LaunchableResponse{
681+
BuildRequest: store.LaunchableBuildRequest{
682+
VMBuild: &store.VMBuild{
683+
LifeCycleScriptAttr: &store.LifeCycleScriptAttr{ID: "ls-abc"},
684+
},
685+
},
686+
}
687+
688+
err := inlineLaunchableLifeCycleScript(mockStore, "env-abc", info)
689+
690+
assert.NoError(t, err)
691+
assert.Equal(t, []string{"ls-abc"}, mockStore.FetchedLifeCycleScriptIDs)
692+
assert.Equal(t, "echo mock-script", info.BuildRequest.VMBuild.LifeCycleScriptAttr.Script)
693+
})
694+
695+
t.Run("skips fetch when info is nil", func(t *testing.T) {
696+
mockStore := NewMockGPUCreateStore()
697+
698+
err := inlineLaunchableLifeCycleScript(mockStore, "env-abc", nil)
699+
700+
assert.NoError(t, err)
701+
assert.Empty(t, mockStore.FetchedLifeCycleScriptIDs)
702+
})
703+
704+
t.Run("container build skips lifecycle script fetch", func(t *testing.T) {
705+
mockStore := NewMockGPUCreateStore()
706+
info := &store.LaunchableResponse{
707+
BuildRequest: store.LaunchableBuildRequest{
708+
CustomContainer: &store.CustomContainer{ContainerURL: "nvcr.io/nvidia/test:latest"},
709+
},
710+
}
711+
712+
err := inlineLaunchableLifeCycleScript(mockStore, "env-abc", info)
713+
714+
assert.NoError(t, err)
715+
assert.Empty(t, mockStore.FetchedLifeCycleScriptIDs)
716+
})
717+
718+
t.Run("skips fetch when launchable has no lifecycle script", func(t *testing.T) {
719+
mockStore := NewMockGPUCreateStore()
720+
info := &store.LaunchableResponse{
721+
BuildRequest: store.LaunchableBuildRequest{
722+
VMBuild: &store.VMBuild{ForceJupyterInstall: true},
723+
},
724+
}
725+
726+
err := inlineLaunchableLifeCycleScript(mockStore, "env-abc", info)
727+
728+
assert.NoError(t, err)
729+
assert.Empty(t, mockStore.FetchedLifeCycleScriptIDs)
730+
assert.Nil(t, info.BuildRequest.VMBuild.LifeCycleScriptAttr)
731+
})
732+
733+
t.Run("skips fetch when script ID is empty", func(t *testing.T) {
734+
mockStore := NewMockGPUCreateStore()
735+
info := &store.LaunchableResponse{
736+
BuildRequest: store.LaunchableBuildRequest{
737+
VMBuild: &store.VMBuild{
738+
LifeCycleScriptAttr: &store.LifeCycleScriptAttr{Name: "stale"},
739+
},
740+
},
741+
}
742+
743+
err := inlineLaunchableLifeCycleScript(mockStore, "env-abc", info)
744+
745+
assert.NoError(t, err)
746+
assert.Empty(t, mockStore.FetchedLifeCycleScriptIDs)
747+
assert.Equal(t, "", info.BuildRequest.VMBuild.LifeCycleScriptAttr.Script)
748+
})
749+
}

pkg/store/workspace.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,29 @@ func (s AuthHTTPStore) GetLaunchable(launchableID string) (*LaunchableResponse,
283283
return &result, nil
284284
}
285285

286+
// LifeCycleScriptResponse holds a lifecycle script with the script body populated.
287+
type LifeCycleScriptResponse struct {
288+
Attrs *LifeCycleScriptAttr `json:"attrs"`
289+
}
290+
291+
// GetLaunchableLifeCycleScript fetches the full lifecycle script for a launchable.
292+
func (s AuthHTTPStore) GetLaunchableLifeCycleScript(launchableID, scriptID string) (*LifeCycleScriptResponse, error) {
293+
var result LifeCycleScriptResponse
294+
res, err := s.authHTTPClient.restyClient.R().
295+
SetHeader("Content-Type", "application/json").
296+
SetQueryParam("envId", launchableID).
297+
SetQueryParam("scriptId", scriptID).
298+
SetResult(&result).
299+
Get("api/launchable/lifecycle-script")
300+
if err != nil {
301+
return nil, breverrors.WrapAndTrace(err)
302+
}
303+
if res.IsError() {
304+
return nil, NewHTTPResponseError(res)
305+
}
306+
return &result, nil
307+
}
308+
286309
type GetWorkspacesOptions struct {
287310
UserID string
288311
Name string

0 commit comments

Comments
 (0)