From 44fc931fef9c6d340654dbbe38455391ec025cf3 Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Wed, 1 Jul 2026 08:53:54 -0700 Subject: [PATCH] feat(BRE2-966): uptake dev-plane instancetypes API --- pkg/cmd/gpucreate/gpucreate.go | 55 +++++++++----- pkg/cmd/gpucreate/gpucreate_test.go | 39 +++++++++- pkg/cmd/gpusearch/gpusearch.go | 27 ++++++- pkg/store/instancetypes.go | 110 ++++++++++++++++++++++++---- pkg/store/instancetypes_test.go | 64 ++++++++++++++++ pkg/store/workspace.go | 8 ++ 6 files changed, 264 insertions(+), 39 deletions(-) create mode 100644 pkg/store/instancetypes_test.go diff --git a/pkg/cmd/gpucreate/gpucreate.go b/pkg/cmd/gpucreate/gpucreate.go index 7b379dbd7..5110d1ad4 100644 --- a/pkg/cmd/gpucreate/gpucreate.go +++ b/pkg/cmd/gpucreate/gpucreate.go @@ -105,7 +105,7 @@ type GPUCreateStore interface { GetWorkspace(workspaceID string) (*entity.Workspace, error) CreateWorkspace(organizationID string, options *store.CreateWorkspacesOptions) (*entity.Workspace, error) DeleteWorkspace(workspaceID string) (*entity.Workspace, error) - GetAllInstanceTypesWithWorkspaceGroups(orgID string) (*gpusearch.AllInstanceTypesResponse, error) + GetAllInstanceTypesWithCloudCreds(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) @@ -768,11 +768,11 @@ func newCreateContext(t *terminal.Terminal, store GPUCreateStore, opts GPUCreate } ctx.org = org - // Fetch instance types with workspace groups - allInstanceTypes, err := store.GetAllInstanceTypesWithWorkspaceGroups(org.ID) + // Fetch instance types with cloud credentials. + allInstanceTypes, err := store.GetAllInstanceTypesWithCloudCreds(org.ID) if err != nil { - ctx.logf("Warning: could not fetch instance types with workspace groups: %s\n", err.Error()) - ctx.logf("Falling back to default workspace group\n") + ctx.logf("Warning: could not fetch instance types with cloud credentials: %s\n", err.Error()) + ctx.logf("Falling back to default cloud credential\n") } ctx.allInstanceTypes = allInstanceTypes @@ -791,7 +791,7 @@ func (c *createContext) validateInstanceTypeAvailability(instanceType string) er if c.allInstanceTypes == nil { return nil } - if c.allInstanceTypes.GetWorkspaceGroupID(instanceType) != "" { + if c.allInstanceTypes.GetCloudCredID(instanceType) != "" { return nil } if !c.allInstanceTypes.HasInstanceType(instanceType) { @@ -1042,8 +1042,8 @@ func (c *createContext) createWorkspace(name string, spec InstanceSpec) (*entity } if c.allInstanceTypes != nil { - if wgID := c.allInstanceTypes.GetWorkspaceGroupID(spec.Type); wgID != "" { - cwOptions.WorkspaceGroupID = wgID + if cloudCredID := c.allInstanceTypes.GetCloudCredID(spec.Type); cloudCredID != "" { + cwOptions.WithCloudCredID(cloudCredID) } } @@ -1060,7 +1060,7 @@ func (c *createContext) createWorkspace(name string, spec InstanceSpec) (*entity if cwOptions.WorkspaceGroupID == "" { if c.allInstanceTypes == nil { return nil, breverrors.NewValidationError(fmt.Sprintf( - "could not resolve workspace group for %q (instance-type listing was unavailable); please retry", + "could not resolve cloud credential for %q (instance-type listing was unavailable); please retry", spec.Type, )) } @@ -1176,11 +1176,22 @@ func applyLaunchableConfig(cwOptions *store.CreateWorkspacesOptions, launchableI return } - wsReq := info.CreateWorkspaceRequest + applyLaunchableWorkspaceRequest(cwOptions, info.CreateWorkspaceRequest) + applyLaunchableBuildRequest(cwOptions, info.BuildRequest) + applyLaunchableFile(cwOptions, info.File) + applyLaunchableLabels(cwOptions, launchableID, info) +} - // Use launchable's workspace group if not already resolved from instance types +func applyLaunchableWorkspaceRequest(cwOptions *store.CreateWorkspacesOptions, wsReq store.LaunchableWorkspaceRequest) { + // Use launchable's cloud credential if not already resolved from instance types. + if cwOptions.CloudCredID == "" && wsReq.CloudCredID != "" { + cwOptions.WithCloudCredID(wsReq.CloudCredID) + } if cwOptions.WorkspaceGroupID == "" && wsReq.WorkspaceGroupID != "" { cwOptions.WorkspaceGroupID = wsReq.WorkspaceGroupID + if cwOptions.CloudCredID == "" { + cwOptions.CloudCredID = wsReq.WorkspaceGroupID + } } // Location / sub-location @@ -1198,8 +1209,13 @@ func applyLaunchableConfig(cwOptions *store.CreateWorkspacesOptions, launchableI cwOptions.DiskStorage = normalizeDiskStorage(wsReq.Storage) } + if len(wsReq.FirewallRules) > 0 { + cwOptions.FirewallRules = resolveFirewallRulesClientIP(wsReq.FirewallRules, publicIPLookup) + } +} + +func applyLaunchableBuildRequest(cwOptions *store.CreateWorkspacesOptions, build store.LaunchableBuildRequest) { // Build configuration from launchable - build := info.BuildRequest switch { case build.VMBuild != nil: cwOptions.VMBuild = build.VMBuild @@ -1219,18 +1235,18 @@ func applyLaunchableConfig(cwOptions *store.CreateWorkspacesOptions, launchableI } cwOptions.PortMappings = portMappings } +} - if len(wsReq.FirewallRules) > 0 { - cwOptions.FirewallRules = resolveFirewallRulesClientIP(wsReq.FirewallRules, publicIPLookup) - } - +func applyLaunchableFile(cwOptions *store.CreateWorkspacesOptions, file *store.LaunchableFile) { // Files from launchable - if info.File != nil { + if file != nil { cwOptions.Files = []map[string]string{ - {"url": info.File.URL, "path": info.File.Path}, + {"url": file.URL, "path": file.Path}, } } +} +func applyLaunchableLabels(cwOptions *store.CreateWorkspacesOptions, launchableID string, info *store.LaunchableResponse) { // Labels for tracking and UI rendering — merge with any existing labels var labels map[string]string if existing, ok := cwOptions.Labels.(map[string]string); ok && existing != nil { @@ -1239,7 +1255,8 @@ func applyLaunchableConfig(cwOptions *store.CreateWorkspacesOptions, launchableI labels = make(map[string]string) } labels["launchableId"] = launchableID - labels["launchableInstanceType"] = wsReq.InstanceType + labels["launchableInstanceType"] = info.CreateWorkspaceRequest.InstanceType + labels["cloudCredId"] = cwOptions.CloudCredID labels["workspaceGroupId"] = cwOptions.WorkspaceGroupID labels["launchableCreatedByUserId"] = info.CreatedByUserID labels["launchableCreatedByOrgId"] = info.CreatedByOrgID diff --git a/pkg/cmd/gpucreate/gpucreate_test.go b/pkg/cmd/gpucreate/gpucreate_test.go index 5f71cdaec..f7b13f39c 100644 --- a/pkg/cmd/gpucreate/gpucreate_test.go +++ b/pkg/cmd/gpucreate/gpucreate_test.go @@ -14,6 +14,7 @@ import ( "github.com/brevdev/brev-cli/pkg/store" "github.com/brevdev/brev-cli/pkg/terminal" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // MockGPUCreateStore is a mock implementation of GPUCreateStore for testing @@ -24,6 +25,7 @@ type MockGPUCreateStore struct { CreateError error CreateErrorTypes map[string]error // Errors for specific instance types DeleteError error + CreatedOptions []*store.CreateWorkspacesOptions CreatedWorkspaces []*entity.Workspace DeletedWorkspaceIDs []string FetchedLifeCycleScriptIDs []string @@ -85,6 +87,7 @@ func (m *MockGPUCreateStore) CreateWorkspace(organizationID string, options *sto Status: entity.Running, } m.Workspaces[ws.ID] = ws + m.CreatedOptions = append(m.CreatedOptions, options) m.CreatedWorkspaces = append(m.CreatedWorkspaces, ws) return ws, nil } @@ -104,7 +107,7 @@ func (m *MockGPUCreateStore) GetWorkspaceByNameOrID(orgID string, nameOrID strin return []entity.Workspace{}, nil } -func (m *MockGPUCreateStore) GetAllInstanceTypesWithWorkspaceGroups(orgID string) (*gpusearch.AllInstanceTypesResponse, error) { +func (m *MockGPUCreateStore) GetAllInstanceTypesWithCloudCreds(orgID string) (*gpusearch.AllInstanceTypesResponse, error) { return nil, nil } @@ -247,7 +250,8 @@ func TestApplyLaunchableConfig(t *testing.T) { //nolint:funlen // test applyLaunchableConfig(cwOptions, "env-abc123", info) - // Workspace group from launchable + // Cloud credential from launchable compatibility input. + assert.Equal(t, "GCP", cwOptions.CloudCredID) assert.Equal(t, "GCP", cwOptions.WorkspaceGroupID) // Location / sub-location assert.Equal(t, "us-west1", cwOptions.Location) @@ -274,6 +278,7 @@ func TestApplyLaunchableConfig(t *testing.T) { //nolint:funlen // test assert.True(t, ok) assert.Equal(t, "env-abc123", labels["launchableId"]) assert.Equal(t, "n2-standard-4", labels["launchableInstanceType"]) + assert.Equal(t, "GCP", labels["cloudCredId"]) assert.Equal(t, "GCP", labels["workspaceGroupId"]) assert.Equal(t, "user-1", labels["launchableCreatedByUserId"]) assert.Equal(t, "org-1", labels["launchableCreatedByOrgId"]) @@ -281,6 +286,7 @@ func TestApplyLaunchableConfig(t *testing.T) { //nolint:funlen // test t.Run("preserves existing workspace group", func(t *testing.T) { cwOptions := &store.CreateWorkspacesOptions{ + CloudCredID: "existing-wg", WorkspaceGroupID: "existing-wg", } info := &store.LaunchableResponse{ @@ -293,6 +299,7 @@ func TestApplyLaunchableConfig(t *testing.T) { //nolint:funlen // test applyLaunchableConfig(cwOptions, "env-abc", info) assert.Equal(t, "existing-wg", cwOptions.WorkspaceGroupID) + assert.Equal(t, "existing-wg", cwOptions.CloudCredID) }) t.Run("storage already has Gi suffix", func(t *testing.T) { @@ -1116,6 +1123,34 @@ func TestCreateInstancesWithTypeSkipsUnavailableType(t *testing.T) { assert.Empty(t, mock.CreatedWorkspaces, "CreateWorkspace must not be called when no workspace group is available") } +func TestCreateInstancesWithTypeSetsCloudCredIDFromCatalog(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{ + AllInstanceTypes: []gpusearch.InstanceType{ + { + Type: "hyperstack_H100_sxm5x8", + CloudCredID: "cc-shadeform", + }, + }, + }, + } + ctx.logf = func(_ string, _ ...interface{}) {} + + result := ctx.createInstancesWithType(InstanceSpec{Type: "hyperstack_H100_sxm5x8"}, 0, 1) + + assert.False(t, result.hadFailure) + require.Len(t, mock.CreatedOptions, 1) + assert.Equal(t, "cc-shadeform", mock.CreatedOptions[0].CloudCredID) + assert.Equal(t, "cc-shadeform", mock.CreatedOptions[0].WorkspaceGroupID) +} + func TestCreateInstancesWithTypeBypassesValidationForLaunchable(t *testing.T) { mock := NewMockGPUCreateStore() ctx := &createContext{ diff --git a/pkg/cmd/gpusearch/gpusearch.go b/pkg/cmd/gpusearch/gpusearch.go index 059431418..be3bb384b 100644 --- a/pkg/cmd/gpusearch/gpusearch.go +++ b/pkg/cmd/gpusearch/gpusearch.go @@ -55,6 +55,14 @@ type WorkspaceGroup struct { PlatformType string `json:"platformType"` } +// CloudCred represents a cloud credential that can run an instance type. +type CloudCred struct { + ID string `json:"id"` + Name string `json:"name"` + PlatformType string `json:"platformType"` + TenantType string `json:"tenantType"` +} + // InstanceType represents an instance type from the API type InstanceType struct { Type string `json:"type"` @@ -69,6 +77,8 @@ type InstanceType struct { SubLocation string `json:"sub_location"` AvailableLocations []string `json:"available_locations"` Provider string `json:"provider"` + CloudCredID string `json:"cloud_cred_id"` + CloudCreds []CloudCred `json:"cloud_creds"` WorkspaceGroups []WorkspaceGroup `json:"workspace_groups"` EstimatedDeployTime string `json:"estimated_deploy_time"` Stoppable bool `json:"stoppable"` @@ -81,15 +91,21 @@ type InstanceTypesResponse struct { Items []InstanceType `json:"items"` } -// AllInstanceTypesResponse represents the authenticated API response with workspace groups +// AllInstanceTypesResponse represents the authenticated API response with cloud credentials. type AllInstanceTypesResponse struct { AllInstanceTypes []InstanceType `json:"allInstanceTypes"` } -// GetWorkspaceGroupID returns the workspace group ID for an instance type, or empty string if not found -func (r *AllInstanceTypesResponse) GetWorkspaceGroupID(instanceType string) string { +// GetCloudCredID returns the cloud credential ID for an instance type, or empty string if not found. +func (r *AllInstanceTypesResponse) GetCloudCredID(instanceType string) string { for _, it := range r.AllInstanceTypes { if it.Type == instanceType { + if it.CloudCredID != "" { + return it.CloudCredID + } + if len(it.CloudCreds) > 0 { + return it.CloudCreds[0].ID + } if len(it.WorkspaceGroups) > 0 { return it.WorkspaceGroups[0].ID } @@ -98,6 +114,11 @@ func (r *AllInstanceTypesResponse) GetWorkspaceGroupID(instanceType string) stri return "" } +// GetWorkspaceGroupID is a compatibility alias while create still accepts workspaceGroupId. +func (r *AllInstanceTypesResponse) GetWorkspaceGroupID(instanceType string) string { + return r.GetCloudCredID(instanceType) +} + // HasInstanceType reports whether the type exists in the API listing, independent of capacity. func (r *AllInstanceTypesResponse) HasInstanceType(instanceType string) bool { for _, it := range r.AllInstanceTypes { diff --git a/pkg/store/instancetypes.go b/pkg/store/instancetypes.go index 416b11bac..f13d7464d 100644 --- a/pkg/store/instancetypes.go +++ b/pkg/store/instancetypes.go @@ -2,18 +2,19 @@ package store import ( "encoding/json" - "fmt" - "runtime" + devplaneapiv1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1" "github.com/brevdev/brev-cli/pkg/cmd/gpusearch" "github.com/brevdev/brev-cli/pkg/config" breverrors "github.com/brevdev/brev-cli/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" ) const ( - instanceTypesAPIPath = "v1/instance/types" - // Authenticated API for instance types with workspace groups - allInstanceTypesPathPattern = "api/instances/alltypesavailable/%s" + instanceTypesAPIPath = "v1/instance/types" + organizationAvailableTypesRPCPath = "devplaneapi.v1.InstanceService/ListOrganizationAvailableInstanceTypes" + tenantTypeShared = "shared" + tenantTypeIsolated = "isolated" ) // GetInstanceTypes fetches all available instance types from the public API @@ -53,17 +54,23 @@ func fetchInstanceTypes(includeCPU bool) (*gpusearch.InstanceTypesResponse, erro return &result, nil } -// GetAllInstanceTypesWithWorkspaceGroups fetches instance types with workspace groups from the authenticated API -func (s AuthHTTPStore) GetAllInstanceTypesWithWorkspaceGroups(orgID string) (*gpusearch.AllInstanceTypesResponse, error) { - path := fmt.Sprintf(allInstanceTypesPathPattern, orgID) +// GetAllInstanceTypesWithCloudCreds fetches org-scoped instance types from dev-plane's public Connect API. +func (s AuthHTTPStore) GetAllInstanceTypesWithCloudCreds(orgID string) (*gpusearch.AllInstanceTypesResponse, error) { + publicClient := NewAuthHTTPClient(s.authHTTPClient.auth, config.NewConstants().GetBrevPublicAPIURL()).restyClient - var result gpusearch.AllInstanceTypesResponse - res, err := s.authHTTPClient.restyClient.R(). + res, err := publicClient.R(). SetHeader("Content-Type", "application/json"). - SetQueryParam("utm_source", "cli"). - SetQueryParam("os", runtime.GOOS). - SetResult(&result). - Get(path) + SetBody(map[string]interface{}{ + "organizationId": orgID, + "options": map[string]interface{}{ + "includeUnavailable": false, + "includePreemptible": false, + "includeCpu": true, + "uniqueInstanceType": true, + "skipAccessFilter": false, + }, + }). + Post(organizationAvailableTypesRPCPath) if err != nil { return nil, breverrors.WrapAndTrace(err) } @@ -71,5 +78,78 @@ func (s AuthHTTPStore) GetAllInstanceTypesWithWorkspaceGroups(orgID string) (*gp return nil, NewHTTPResponseError(res) } - return &result, nil + var protoResp devplaneapiv1.ListInstanceTypeResponse + if err := protojson.Unmarshal(res.Body(), &protoResp); err != nil { + return nil, breverrors.WrapAndTrace(err) + } + + return mapProtoInstanceTypesToGPUSearchResponse(protoResp.Items), nil +} + +// GetAllInstanceTypesWithWorkspaceGroups is a compatibility alias while create still accepts workspaceGroupId. +func (s AuthHTTPStore) GetAllInstanceTypesWithWorkspaceGroups(orgID string) (*gpusearch.AllInstanceTypesResponse, error) { + return s.GetAllInstanceTypesWithCloudCreds(orgID) +} + +func mapProtoInstanceTypesToGPUSearchResponse(instanceTypes []*devplaneapiv1.InstanceType) *gpusearch.AllInstanceTypesResponse { + items := make([]gpusearch.InstanceType, 0, len(instanceTypes)) + for _, instanceType := range instanceTypes { + if instanceType == nil { + continue + } + item := gpusearch.InstanceType{ + Type: instanceType.GetType(), + VCPU: int(instanceType.GetVcpu()), + Location: instanceType.GetLocation(), + SubLocation: instanceType.GetSubLocation(), + AvailableLocations: instanceType.GetAvailableLocations(), + Provider: instanceType.GetProvider(), + CloudCredID: instanceType.GetCloudCredId(), + EstimatedDeployTime: instanceType.GetEstimatedDeployTime(), + Stoppable: instanceType.GetStoppable(), + Rebootable: instanceType.GetRebootable(), + CanModifyFirewallRules: instanceType.GetCanModifyFirewallRules(), + } + if basePrice := instanceType.GetBasePrice(); basePrice != nil { + item.BasePrice = gpusearch.BasePrice{ + Currency: basePrice.GetCurrency(), + Amount: basePrice.GetAmount(), + } + } + for _, gpu := range instanceType.GetSupportedGpus() { + item.SupportedGPUs = append(item.SupportedGPUs, gpusearch.GPU{ + Count: int(gpu.GetCount()), + Name: gpu.GetName(), + Manufacturer: gpu.GetManufacturer(), + Memory: gpu.GetMemory(), + }) + } + if cloudCred := instanceType.GetCloudCred(); cloudCred != nil { + mappedCloudCred := gpusearch.CloudCred{ + ID: cloudCred.GetCloudCredId(), + Name: cloudCred.GetName(), + PlatformType: cloudCred.GetProviderId(), + TenantType: mapTenantType(cloudCred.GetTenantType()), + } + item.CloudCreds = []gpusearch.CloudCred{mappedCloudCred} + item.WorkspaceGroups = []gpusearch.WorkspaceGroup{{ + ID: mappedCloudCred.ID, + Name: mappedCloudCred.Name, + PlatformType: mappedCloudCred.PlatformType, + }} + } + items = append(items, item) + } + return &gpusearch.AllInstanceTypesResponse{AllInstanceTypes: items} +} + +func mapTenantType(tenantType devplaneapiv1.TenantType) string { + switch tenantType { + case devplaneapiv1.TenantType_TENANT_TYPE_ISOLATED: + return tenantTypeIsolated + case devplaneapiv1.TenantType_TENANT_TYPE_SHARED: + return tenantTypeShared + default: + return "" + } } diff --git a/pkg/store/instancetypes_test.go b/pkg/store/instancetypes_test.go new file mode 100644 index 000000000..36c435182 --- /dev/null +++ b/pkg/store/instancetypes_test.go @@ -0,0 +1,64 @@ +package store + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetAllInstanceTypesWithCloudCredsUsesDevPlanePublicAPI(t *testing.T) { + var gotAuth string + var gotOrgID string + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/devplaneapi.v1.InstanceService/ListOrganizationAvailableInstanceTypes", r.URL.Path) + gotAuth = r.Header.Get("Authorization") + + var req struct { + OrganizationID string `json:"organizationId"` + } + require.NoError(t, json.NewDecoder(r.Body).Decode(&req)) + gotOrgID = req.OrganizationID + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{ + "items": [{ + "type": "h100-1x", + "cloudCredId": "cc-org-1", + "cloudCred": { + "cloudCredId": "cc-org-1", + "providerId": "aws", + "name": "Org AWS", + "tenantType": "TENANT_TYPE_ISOLATED" + }, + "availableLocations": ["us-east-1"], + "isAvailable": true + }] + }`)) + })) + defer server.Close() + + t.Setenv("BREV_PUBLIC_API_URL", server.URL) + legacy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("legacy brev-deploy API should not be called: %s", r.URL.Path) + })) + defer legacy.Close() + + token := "tok" + s := MakeMockNoHTTPStore().WithAuthHTTPClient(NewAuthHTTPClient(MockAuth{token: &token}, legacy.URL)) + + resp, err := s.GetAllInstanceTypesWithCloudCreds("org-1") + require.NoError(t, err) + + assert.Equal(t, "Bearer tok", gotAuth) + assert.Equal(t, "org-1", gotOrgID) + 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, "cc-org-1", resp.GetWorkspaceGroupID("h100-1x")) + } +} diff --git a/pkg/store/workspace.go b/pkg/store/workspace.go index 40a5a6105..27049cdd9 100644 --- a/pkg/store/workspace.go +++ b/pkg/store/workspace.go @@ -80,6 +80,7 @@ type Registry struct { type CreateWorkspacesOptions struct { Name string `json:"name"` + CloudCredID string `json:"cloudCredId,omitempty"` WorkspaceGroupID string `json:"workspaceGroupId"` WorkspaceClassID string `json:"workspaceClassId"` WorkspaceTemplateID string `json:"workspaceTemplateId"` @@ -141,6 +142,7 @@ type LaunchableResponse struct { } type LaunchableWorkspaceRequest struct { + CloudCredID string `json:"cloudCredId,omitempty"` WorkspaceGroupID string `json:"workspaceGroupId,omitempty"` InstanceType string `json:"instanceType"` Storage string `json:"storage,omitempty"` @@ -258,6 +260,12 @@ func (c *CreateWorkspacesOptions) WithWorkspaceGroupID(workspaceGroupID string) return c } +func (c *CreateWorkspacesOptions) WithCloudCredID(cloudCredID string) *CreateWorkspacesOptions { + c.CloudCredID = cloudCredID + c.WorkspaceGroupID = cloudCredID + return c +} + func (s AuthHTTPStore) CreateWorkspace(organizationID string, options *CreateWorkspacesOptions) (*entity.Workspace, error) { if options == nil { return nil, fmt.Errorf("options can not be nil")