Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 29 additions & 8 deletions pkg/cmd/gpucreate/gpucreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -895,14 +895,15 @@ func formatInstanceSpecs(specs []InstanceSpec) string {

// createContext holds shared state for instance creation
type createContext struct {
t *terminal.Terminal
store GPUCreateStore
opts GPUCreateOptions
org *entity.Organization
user *entity.User
allInstanceTypes *gpusearch.AllInstanceTypesResponse
piped bool
logf func(format string, a ...interface{})
t *terminal.Terminal
store GPUCreateStore
opts GPUCreateOptions
org *entity.Organization
user *entity.User
allInstanceTypes *gpusearch.AllInstanceTypesResponse
publicInstanceTypes *gpusearch.InstanceTypesResponse
piped bool
logf func(format string, a ...interface{})
}

// newCreateContext initializes the context for instance creation
Expand Down Expand Up @@ -952,6 +953,11 @@ func newCreateContext(t *terminal.Terminal, store GPUCreateStore, opts GPUCreate
ctx.logf("Falling back to default cloud credential\n")
}
ctx.allInstanceTypes = allInstanceTypes
publicInstanceTypes, publicErr := store.GetInstanceTypes(false)
if publicErr != nil {
ctx.logf("Warning: could not fetch public instance types: %s\n", publicErr.Error())
}
ctx.publicInstanceTypes = publicInstanceTypes

return ctx, nil
}
Expand All @@ -972,6 +978,13 @@ func (c *createContext) validateInstanceTypeAvailability(instanceType string) er
return nil
}
if !c.allInstanceTypes.HasInstanceType(instanceType) {
if c.publicInstanceTypes != nil {
for _, it := range c.publicInstanceTypes.Items {
if it.Type == instanceType {
return nil
}
}
}
return breverrors.NewValidationError(fmt.Sprintf(
"instance type %q is not a recognized type; run 'brev search' to see available types",
instanceType,
Expand Down Expand Up @@ -1223,6 +1236,14 @@ func (c *createContext) createWorkspace(name string, spec InstanceSpec) (*entity
cwOptions.WithCloudCredID(cloudCredID)
}
}
if cwOptions.CloudCredID == "" && c.publicInstanceTypes != nil {
for _, it := range c.publicInstanceTypes.Items {
if it.Type == spec.Type && it.CloudCredID != "" {
cwOptions.WithCloudCredID(it.CloudCredID)
break
}
}
}

// Apply launchable config or build mode
if c.opts.LaunchableID != "" {
Expand Down
31 changes: 31 additions & 0 deletions pkg/cmd/gpucreate/gpucreate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,16 @@ func TestValidateInstanceTypeAvailability(t *testing.T) {
assert.Contains(t, err.Error(), "brev search")
})

t.Run("accepts a public Shadeform type omitted from the org listing", func(t *testing.T) {
ctx := &createContext{
allInstanceTypes: &gpusearch.AllInstanceTypesResponse{},
publicInstanceTypes: &gpusearch.InstanceTypesResponse{Items: []gpusearch.InstanceType{
{Type: "verda_RTXPro6000"},
}},
}
assert.NoError(t, ctx.validateInstanceTypeAvailability("verda_RTXPro6000"))
})

t.Run("returns unavailable error for known type without a cloud credential", func(t *testing.T) {
ctx := &createContext{
allInstanceTypes: &gpusearch.AllInstanceTypesResponse{
Expand Down Expand Up @@ -1252,6 +1262,27 @@ func TestCreateInstancesWithTypeSetsCloudCredIDFromCatalog(t *testing.T) {
assert.Equal(t, "cc-shadeform", mock.CreatedOptions[0].CloudCredID)
}

func TestCreateInstancesWithTypeSetsCloudCredIDFromPublicCatalog(t *testing.T) {
mock := NewMockGPUCreateStore()
ctx := &createContext{
t: terminal.New(),
store: mock,
opts: GPUCreateOptions{Count: 1, Parallel: 1, Name: "jt-4"},
org: mock.Org,
user: mock.User,
piped: true,
allInstanceTypes: &gpusearch.AllInstanceTypesResponse{},
publicInstanceTypes: &gpusearch.InstanceTypesResponse{Items: []gpusearch.InstanceType{{Type: "verda_RTXPro6000", CloudCredID: "cc-public-shadeform"}}},
}
ctx.logf = func(_ string, _ ...interface{}) {}

result := ctx.createInstancesWithType(InstanceSpec{Type: "verda_RTXPro6000"}, 0, 1)

assert.False(t, result.hadFailure)
require.Len(t, mock.CreatedOptions, 1)
assert.Equal(t, "cc-public-shadeform", mock.CreatedOptions[0].CloudCredID)
}

func TestCreateInstancesWithTypeBypassesValidationForLaunchable(t *testing.T) {
mock := NewMockGPUCreateStore()
ctx := &createContext{
Expand Down
5 changes: 4 additions & 1 deletion pkg/store/instancetypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ func (s AuthHTTPStore) GetAllInstanceTypesWithCloudCreds(orgID string) (*gpusear
includePreemptible := false
includeCPU := true
uniqueInstanceType := true
skipAccessFilter := false
// The create flow must see every type the organization can launch through
// its cloud credentials, including reserved-pool types surfaced by search.
// Capacity is still filtered by IncludeUnavailable above.
skipAccessFilter := true
res, err := client.ListOrganizationAvailableInstanceTypes(context.Background(), connect.NewRequest(&devplaneapiv1.ListOrganizationAvailableInstanceTypesRequest{
OrganizationId: orgID,
Options: &devplaneapiv1.ListInstanceTypeOptions{
Expand Down
25 changes: 16 additions & 9 deletions pkg/store/instancetypes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type instanceCatalogTestHandler struct {
gotOrgID string
gotConnectProtocolVersion string
gotIncludeCPU bool
gotSkipAccessFilter bool
}

func (h *instanceCatalogTestHandler) ListPublicInstanceType(
Expand Down Expand Up @@ -79,19 +80,24 @@ func (h *instanceCatalogTestHandler) ListOrganizationAvailableInstanceTypes(
h.gotAuth = req.Header().Get("Authorization")
h.gotOrgID = req.Msg.GetOrganizationId()
h.gotConnectProtocolVersion = req.Header().Get("Connect-Protocol-Version")
return connect.NewResponse(&devplaneapiv1.ListOrganizationAvailableInstanceTypesResponse{
Items: []*devplaneapiv1.InstanceType{{
Type: "h100-1x",
h.gotSkipAccessFilter = req.Msg.GetOptions().GetSkipAccessFilter()
items := []*devplaneapiv1.InstanceType{}
if h.gotSkipAccessFilter {
items = append(items, &devplaneapiv1.InstanceType{
Type: "verda_RTXPro6000",
CloudCredId: "cc-org-1",
CloudCred: &devplaneapiv1.CloudCredMetadata{
CloudCredId: "cc-org-1",
ProviderId: "aws",
Name: "Org AWS",
ProviderId: "shadeform",
Name: "Shadeform",
TenantType: devplaneapiv1.TenantType_TENANT_TYPE_ISOLATED,
},
AvailableLocations: []string{"us-east-1"},
AvailableLocations: []string{"us-central-1"},
IsAvailable: true,
}},
})
}
return connect.NewResponse(&devplaneapiv1.ListOrganizationAvailableInstanceTypesResponse{
Items: items,
}), nil
}

Expand Down Expand Up @@ -119,9 +125,10 @@ func TestGetAllInstanceTypesWithCloudCredsUsesDevPlanePublicAPI(t *testing.T) {
assert.Equal(t, "Bearer tok", catalogHandler.gotAuth)
assert.Equal(t, "1", catalogHandler.gotConnectProtocolVersion)
assert.Equal(t, "org-1", catalogHandler.gotOrgID)
assert.True(t, catalogHandler.gotSkipAccessFilter)
if assert.Len(t, resp.AllInstanceTypes, 1) {
assert.Equal(t, "h100-1x", resp.AllInstanceTypes[0].Type)
assert.Equal(t, "cc-org-1", resp.GetCloudCredID("h100-1x"))
assert.Equal(t, "verda_RTXPro6000", resp.AllInstanceTypes[0].Type)
assert.Equal(t, "cc-org-1", resp.GetCloudCredID("verda_RTXPro6000"))
}
}

Expand Down