diff --git a/go.mod b/go.mod index bc7a4fa..33b3169 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,6 @@ require ( github.com/nebius/gosdk v0.2.22 github.com/pkg/errors v0.9.1 github.com/sfcompute/nodes-go v0.1.0-alpha.4 - github.com/sfcompute/sfc-go v0.1.0-preview.3 github.com/stretchr/testify v1.11.1 github.com/verda-cloud/verdacloud-sdk-go v1.4.2 golang.org/x/crypto v0.52.0 @@ -85,7 +84,6 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/spyzhov/ajson v0.8.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect diff --git a/go.sum b/go.sum index 26a8343..02a74c7 100644 --- a/go.sum +++ b/go.sum @@ -156,16 +156,12 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sfcompute/nodes-go v0.1.0-alpha.4 h1:oFBWcMPSpqLYm/NDs5I1jTvzgx9rsXDL9Ghsm30Hc0Q= github.com/sfcompute/nodes-go v0.1.0-alpha.4/go.mod h1:nUviHgK+Fgt2hDFcRL3M8VoyiypC8fc0dsY8C30QU8M= -github.com/sfcompute/sfc-go v0.1.0-preview.3 h1:azKThmbm9ljQ+z8RP4039XwV4bJMTcYKNpKcxrpNf5A= -github.com/sfcompute/sfc-go v0.1.0-preview.3/go.mod h1:SDgYqB2R6gFM+bzLBeF/Fb+J1HHaTlDuStSkiFuMWDU= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spyzhov/ajson v0.8.0 h1:sFXyMbi4Y/BKjrsfkUZHSjA2JM1184enheSjjoT/zCc= -github.com/spyzhov/ajson v0.8.0/go.mod h1:63V+CGM6f1Bu/p4nLIN8885ojBdt88TbLoSFzyqMuVA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/v1/providers/sfcomputev2/api_client.go b/v1/providers/sfcomputev2/api_client.go new file mode 100644 index 0000000..95c48da --- /dev/null +++ b/v1/providers/sfcomputev2/api_client.go @@ -0,0 +1,208 @@ +package v2 + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" +) + +const ( + defaultAPIURL = "https://api.sfcompute.com" + brevAPIPath = "/integrations/brev/v1" +) + +type apiClient struct { + apiKey string + baseURL string + httpClient *http.Client +} + +type createInstanceRequest struct { + Name *string `json:"name,omitempty"` + Pool string `json:"pool"` + Image string `json:"image"` + InstanceSKU string `json:"instance_sku"` + CloudInitUserData *string `json:"cloud_init_user_data,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + PreviewEnableInfiniband bool `json:"_preview_enable_infiniband"` +} + +type instanceStatus string + +const ( + instanceStatusAwaitingAllocation instanceStatus = "awaiting_allocation" + instanceStatusRunning instanceStatus = "running" + instanceStatusTerminated instanceStatus = "terminated" + instanceStatusFailed instanceStatus = "failed" +) + +type instanceSKUSummary struct { + ID string `json:"id"` +} + +type instanceResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Status instanceStatus `json:"status"` + InstanceSKU *instanceSKUSummary `json:"instance_sku"` + CreatedAt int64 `json:"created_at"` + Tags map[string]string `json:"tags"` +} + +type listInstancesResponse struct { + Data []instanceResponse `json:"data"` +} + +type instanceSSHInfo struct { + Hostname string `json:"hostname"` + Port int64 `json:"port"` +} + +func (i *instanceSSHInfo) GetHostname() string { + if i == nil { + return "" + } + return i.Hostname +} + +func (i *instanceSSHInfo) GetPort() int64 { + if i == nil { + return 0 + } + return i.Port +} + +type scheduleEntry struct { + StartAt int64 `json:"start_at"` + EndAt *int64 `json:"end_at"` + NodeCount int `json:"node_count"` +} + +type allocationSchedule struct { + ByInstanceSKU map[string][]scheduleEntry `json:"by_instance_sku"` +} + +type poolResponse struct { + AllocationSchedule allocationSchedule `json:"allocation_schedule"` +} + +type apiError struct { + statusCode int + body string +} + +func (e *apiError) Error() string { + return fmt.Sprintf("SFCompute API request failed: status %d: %s", e.statusCode, e.body) +} + +func newAPIClient(apiKey string) *apiClient { + return &apiClient{ + apiKey: apiKey, + baseURL: defaultAPIURL, + httpClient: &http.Client{Timeout: 60 * time.Second}, + } +} + +func (c *apiClient) createInstance(ctx context.Context, request createInstanceRequest) (*instanceResponse, error) { + var response instanceResponse + if err := c.do(ctx, http.MethodPost, "/instances", nil, request, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (c *apiClient) getInstance(ctx context.Context, id string) (*instanceResponse, error) { + var response instanceResponse + if err := c.do(ctx, http.MethodGet, "/instances/"+url.PathEscape(id), nil, nil, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (c *apiClient) listInstances(ctx context.Context, workspace, pool string) (*listInstancesResponse, error) { + query := url.Values{"workspace": {workspace}, "pool": {pool}, "limit": {"50"}} + var response listInstancesResponse + if err := c.do(ctx, http.MethodGet, "/instances", query, nil, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (c *apiClient) terminateInstance(ctx context.Context, id string) error { + var response instanceResponse + return c.do(ctx, http.MethodPost, "/instances/"+url.PathEscape(id)+"/terminate", nil, nil, &response) +} + +func (c *apiClient) getSSHInfo(ctx context.Context, id string) (*instanceSSHInfo, error) { + var response instanceSSHInfo + if err := c.do(ctx, http.MethodGet, "/instances/"+url.PathEscape(id)+"/ssh", nil, nil, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (c *apiClient) getPool(ctx context.Context, id string) (*poolResponse, error) { + var response poolResponse + if err := c.do(ctx, http.MethodGet, "/pools/"+url.PathEscape(id), nil, nil, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (c *apiClient) do( + ctx context.Context, + method string, + path string, + query url.Values, + requestBody any, + responseBody any, +) error { + var body io.Reader + if requestBody != nil { + encoded, err := json.Marshal(requestBody) + if err != nil { + return err + } + body = bytes.NewReader(encoded) + } + + request, err := http.NewRequestWithContext( + ctx, + method, + strings.TrimRight(c.baseURL, "/")+brevAPIPath+path, + body, + ) + if err != nil { + return err + } + request.URL.RawQuery = query.Encode() + request.Header.Set("Accept", "application/json") + request.Header.Set("Authorization", "Bearer "+c.apiKey) + if requestBody != nil { + request.Header.Set("Content-Type", "application/json") + } + + response, err := c.httpClient.Do(request) + if err != nil { + return err + } + defer response.Body.Close() + + responseBytes, err := io.ReadAll(response.Body) + if err != nil { + return err + } + if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { + return &apiError{statusCode: response.StatusCode, body: string(responseBytes)} + } + if responseBody == nil || len(responseBytes) == 0 { + return nil + } + return json.Unmarshal(responseBytes, responseBody) +} diff --git a/v1/providers/sfcomputev2/api_client_test.go b/v1/providers/sfcomputev2/api_client_test.go new file mode 100644 index 0000000..ce1fc8b --- /dev/null +++ b/v1/providers/sfcomputev2/api_client_test.go @@ -0,0 +1,115 @@ +package v2 + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAPIClientUsesBrevContract(t *testing.T) { + t.Parallel() + + handler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + require.Equal(t, "Bearer api-key", request.Header.Get("Authorization")) + + switch request.Method + " " + request.URL.Path { + case "POST /integrations/brev/v1/instances": + var body map[string]any + require.NoError(t, json.NewDecoder(request.Body).Decode(&body)) + require.Equal(t, "sfc:pool:account:workspace:default", body["pool"]) + require.Equal(t, "sfc:image:sfcompute:public:ubuntu", body["image"]) + require.Equal(t, "is_sku", body["instance_sku"]) + require.Equal(t, "cloud-init", body["cloud_init_user_data"]) + require.Equal(t, "brev-ref", body["tags"].(map[string]any)[tagKeyRefID]) + require.Equal(t, false, body["_preview_enable_infiniband"]) + writeJSON(t, writer, instanceResponse{ID: "inst_created", Status: instanceStatusAwaitingAllocation}) + case "GET /integrations/brev/v1/instances": + require.Equal(t, "sfc:workspace:account:workspace", request.URL.Query().Get("workspace")) + require.Equal(t, []string{"sfc:pool:account:workspace:default"}, request.URL.Query()["pool"]) + require.Equal(t, "50", request.URL.Query().Get("limit")) + writeJSON(t, writer, listInstancesResponse{Data: []instanceResponse{{ID: "inst_listed"}}}) + case "GET /integrations/brev/v1/instances/inst_test": + writeJSON(t, writer, instanceResponse{ID: "inst_test", Status: instanceStatusRunning}) + case "GET /integrations/brev/v1/instances/inst_test/ssh": + writeJSON(t, writer, instanceSSHInfo{Hostname: "192.0.2.1", Port: 22}) + case "POST /integrations/brev/v1/instances/inst_test/terminate": + writeJSON(t, writer, instanceResponse{ID: "inst_test", Status: instanceStatusTerminated}) + case "GET /integrations/brev/v1/pools/sfc:pool:account:workspace:default": + writeJSON(t, writer, poolResponse{AllocationSchedule: allocationSchedule{ + ByInstanceSKU: map[string][]scheduleEntry{"is_sku": {{StartAt: 0, NodeCount: 1}}}, + }}) + default: + http.NotFound(writer, request) + } + }) + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + + client := newAPIClient("api-key") + client.baseURL = server.URL + ctx := context.Background() + + created, err := client.createInstance(ctx, createInstanceRequest{ + Pool: "sfc:pool:account:workspace:default", + Image: "sfc:image:sfcompute:public:ubuntu", + InstanceSKU: "is_sku", + CloudInitUserData: pointerTo("cloud-init"), + Tags: map[string]string{tagKeyRefID: "brev-ref"}, + }) + require.NoError(t, err) + require.Equal(t, "inst_created", created.ID) + + listed, err := client.listInstances( + ctx, + "sfc:workspace:account:workspace", + "sfc:pool:account:workspace:default", + ) + require.NoError(t, err) + require.Equal(t, "inst_listed", listed.Data[0].ID) + + instance, err := client.getInstance(ctx, "inst_test") + require.NoError(t, err) + require.Equal(t, instanceStatusRunning, instance.Status) + + sshInfo, err := client.getSSHInfo(ctx, "inst_test") + require.NoError(t, err) + require.Equal(t, "192.0.2.1", sshInfo.Hostname) + + require.NoError(t, client.terminateInstance(ctx, "inst_test")) + + pool, err := client.getPool(ctx, "sfc:pool:account:workspace:default") + require.NoError(t, err) + require.Equal(t, 1, pool.AllocationSchedule.ByInstanceSKU["is_sku"][0].NodeCount) +} + +func TestAPIClientReturnsResponseErrors(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { + http.Error(writer, `{"error":"not found"}`, http.StatusNotFound) + })) + t.Cleanup(server.Close) + + client := newAPIClient("api-key") + client.baseURL = server.URL + + _, err := client.getInstance(context.Background(), "inst_missing") + var responseError *apiError + require.ErrorAs(t, err, &responseError) + require.Equal(t, http.StatusNotFound, responseError.statusCode) + require.Contains(t, responseError.body, "not found") +} + +func writeJSON(t *testing.T, writer http.ResponseWriter, value any) { + t.Helper() + writer.Header().Set("Content-Type", "application/json") + require.NoError(t, json.NewEncoder(writer).Encode(value)) +} + +func pointerTo[T any](value T) *T { + return &value +} diff --git a/v1/providers/sfcomputev2/client.go b/v1/providers/sfcomputev2/client.go index 309450e..19b5d59 100644 --- a/v1/providers/sfcomputev2/client.go +++ b/v1/providers/sfcomputev2/client.go @@ -4,7 +4,6 @@ import ( "context" v1 "github.com/brevdev/cloud/v1" - sfc "github.com/sfcompute/sfc-go" ) const CloudProviderID = "sfcompute" @@ -50,7 +49,7 @@ type SFCClientV2 struct { organization string workspace string location string - client *sfc.SDK + client *apiClient logger v1.Logger } @@ -70,7 +69,7 @@ func (c *SFCCredentialV2) MakeClientWithOptions(_ context.Context, location stri organization: c.Organization, workspace: c.Workspace, location: location, - client: sfc.New(sfc.WithSecurity(c.APIKey)), + client: newAPIClient(c.APIKey), logger: &v1.NoopLogger{}, } diff --git a/v1/providers/sfcomputev2/instance.go b/v1/providers/sfcomputev2/instance.go index f3c1076..b076c54 100644 --- a/v1/providers/sfcomputev2/instance.go +++ b/v1/providers/sfcomputev2/instance.go @@ -12,9 +12,6 @@ import ( "github.com/alecthomas/units" "github.com/brevdev/cloud/internal/errors" v1 "github.com/brevdev/cloud/v1" - "github.com/sfcompute/sfc-go/models/components" - "github.com/sfcompute/sfc-go/models/operations" - "github.com/sfcompute/sfc-go/optionalnullable" ) // SFC instance names must match `[a-zA-Z0-9][a-zA-Z0-9._-]{0,254}`: start with an @@ -56,33 +53,33 @@ func (c *SFCClientV2) CreateInstance(ctx context.Context, attrs v1.CreateInstanc } cloudInit := sshKeyCloudInit(attrs.PublicKey) - req := components.CreateInstanceRequest{ + req := createInstanceRequest{ Pool: c.GetDefaultPoolResourcePath(), Image: c.GetDefaultImageResourcePath(), - InstanceSku: sku, + InstanceSKU: sku, CloudInitUserData: &cloudInit, - Tags: optionalnullable.From(&tags), + Tags: tags, } // name is optional; sanitize the requested name to SFC's format and send it only if // something valid remains. Otherwise omit it — identity is preserved in the tags above. if name := sanitizeSFCName(attrs.Name); sfcNamePattern.MatchString(name) { - req.Name = optionalnullable.From(&name) + req.Name = &name } - resp, err := c.client.Instances.Create(ctx, req) + resp, err := c.client.createInstance(ctx, req) if err != nil { return nil, errors.WrapAndTrace(err) } - if resp.InstanceResponse == nil { + if resp == nil { return nil, errors.WrapAndTrace(fmt.Errorf("no instance returned from create")) } - instance, err := c.sfcInstanceToBrevInstance(resp.InstanceResponse, nil) + instance, err := c.sfcInstanceToBrevInstance(resp, nil) if err != nil { return nil, errors.WrapAndTrace(err) } c.logger.Debug(ctx, "sfcv2: CreateInstance end", - v1.LogField("instanceID", resp.InstanceResponse.ID), + v1.LogField("instanceID", resp.ID), v1.LogField("instanceSku", sku), ) @@ -99,27 +96,27 @@ func (c *SFCClientV2) GetInstance(ctx context.Context, id v1.CloudProviderInstan v1.LogField("instanceID", id), ) - resp, err := c.client.Instances.Fetch(ctx, string(id)) + resp, err := c.client.getInstance(ctx, string(id)) if err != nil { return nil, errors.WrapAndTrace(err) } - if resp.InstanceResponse == nil { + if resp == nil { return nil, errors.WrapAndTrace(fmt.Errorf("instance %s not found", id)) } - sshInfo, err := c.getSSHInfo(ctx, string(id), resp.InstanceResponse.Status) + sshInfo, err := c.getSSHInfo(ctx, string(id), resp.Status) if err != nil { return nil, errors.WrapAndTrace(err) } - instance, err := c.sfcInstanceToBrevInstance(resp.InstanceResponse, sshInfo) + instance, err := c.sfcInstanceToBrevInstance(resp, sshInfo) if err != nil { return nil, errors.WrapAndTrace(err) } c.logger.Debug(ctx, "sfcv2: GetInstance end", v1.LogField("instanceID", id), - v1.LogField("status", resp.InstanceResponse.Status), + v1.LogField("status", resp.Status), ) return instance, nil @@ -131,19 +128,16 @@ func (c *SFCClientV2) ListInstances(ctx context.Context, args v1.ListInstancesAr ) poolID := c.GetDefaultPoolResourcePath() - resp, err := c.client.Instances.List(ctx, operations.ListInstancesRequest{ - Workspace: c.GetWorkspaceResourcePath(), - Pool: []string{poolID}, - }) + resp, err := c.client.listInstances(ctx, c.GetWorkspaceResourcePath(), poolID) if err != nil { return nil, errors.WrapAndTrace(err) } - if resp.ListInstancesResponse == nil { + if resp == nil { return []v1.Instance{}, nil } var instances []v1.Instance - for _, inst := range resp.ListInstancesResponse.Data { + for _, inst := range resp.Data { // Filter by instance IDs if specified. if len(args.InstanceIDs) > 0 && !slices.Contains(args.InstanceIDs, v1.CloudProviderInstanceID(inst.ID)) { continue @@ -181,8 +175,7 @@ func (c *SFCClientV2) TerminateInstance(ctx context.Context, id v1.CloudProvider v1.LogField("instanceID", id), ) - _, err := c.client.Instances.TerminateInstance(ctx, string(id)) - if err != nil { + if err := c.client.terminateInstance(ctx, string(id)); err != nil { return errors.WrapAndTrace(err) } @@ -193,24 +186,24 @@ func (c *SFCClientV2) TerminateInstance(ctx context.Context, id v1.CloudProvider return nil } -func (c *SFCClientV2) getSSHInfo(ctx context.Context, id string, status components.InstanceStatus) (*components.InstanceSSHInfo, error) { - if status != components.InstanceStatusRunning { +func (c *SFCClientV2) getSSHInfo(ctx context.Context, id string, status instanceStatus) (*instanceSSHInfo, error) { + if status != instanceStatusRunning { return nil, nil } - resp, err := c.client.Instances.GetSSHInfoForInstance(ctx, id) + resp, err := c.client.getSSHInfo(ctx, id) if err != nil { return nil, errors.WrapAndTrace(err) } - if resp.InstanceSSHInfo == nil { + if resp == nil { return nil, nil } - return resp.InstanceSSHInfo, nil + return resp, nil } -func (c *SFCClientV2) sfcInstanceToBrevInstance(inst *components.InstanceResponse, sshInfo *components.InstanceSSHInfo) (*v1.Instance, error) { - tags, _ := inst.GetTags().GetOrZero() +func (c *SFCClientV2) sfcInstanceToBrevInstance(inst *instanceResponse, sshInfo *instanceSSHInfo) (*v1.Instance, error) { + tags := inst.Tags cloudCredRefID := tags[tagKeyCloudCredRefID] if cloudCredRefID == "" { @@ -259,15 +252,15 @@ func (c *SFCClientV2) sfcInstanceToBrevInstance(inst *components.InstanceRespons }, nil } -func sfcStatusToLifecycleStatus(status components.InstanceStatus) v1.LifecycleStatus { +func sfcStatusToLifecycleStatus(status instanceStatus) v1.LifecycleStatus { switch status { - case components.InstanceStatusAwaitingAllocation: + case instanceStatusAwaitingAllocation: return v1.LifecycleStatusPending - case components.InstanceStatusRunning: + case instanceStatusRunning: return v1.LifecycleStatusRunning - case components.InstanceStatusTerminated: + case instanceStatusTerminated: return v1.LifecycleStatusTerminated - case components.InstanceStatusFailed: + case instanceStatusFailed: return v1.LifecycleStatusFailed default: return v1.LifecycleStatusPending diff --git a/v1/providers/sfcomputev2/instancetype.go b/v1/providers/sfcomputev2/instancetype.go index 79ebe5e..fa8990b 100644 --- a/v1/providers/sfcomputev2/instancetype.go +++ b/v1/providers/sfcomputev2/instancetype.go @@ -10,8 +10,6 @@ import ( "github.com/bojanz/currency" "github.com/brevdev/cloud/internal/errors" v1 "github.com/brevdev/cloud/v1" - "github.com/sfcompute/sfc-go/models/components" - "github.com/sfcompute/sfc-go/models/operations" ) const ( @@ -140,38 +138,34 @@ func (c *SFCClientV2) GetInstanceTypes(ctx context.Context, args v1.GetInstanceT func (c *SFCClientV2) skuFreeCapacity(ctx context.Context) (map[string]int, error) { poolID := c.GetDefaultPoolResourcePath() - poolResp, err := c.client.Pools.Fetch(ctx, poolID, nil) + poolResp, err := c.client.getPool(ctx, poolID) if err != nil { return nil, errors.WrapAndTrace(err) } - if poolResp.PoolResponse == nil { + if poolResp == nil { return map[string]int{}, nil } now := time.Now().Unix() free := make(map[string]int) - for skuID, schedule := range poolResp.PoolResponse.AllocationSchedule.ByInstanceSku { + for skuID, schedule := range poolResp.AllocationSchedule.ByInstanceSKU { free[skuID] = currentScheduleAllocation(schedule, now) } - resp, err := c.client.Instances.List(ctx, operations.ListInstancesRequest{ - Workspace: c.GetWorkspaceResourcePath(), - Pool: []string{poolID}, - }) + resp, err := c.client.listInstances(ctx, c.GetWorkspaceResourcePath(), poolID) if err != nil { return nil, errors.WrapAndTrace(err) } - if resp.ListInstancesResponse != nil { - for _, inst := range resp.ListInstancesResponse.Data { + if resp != nil { + for _, inst := range resp.Data { // Every non-terminated instance occupies a slot on its SKU, including failed ones. - if inst.Status == components.InstanceStatusTerminated { + if inst.Status == instanceStatusTerminated { continue } - sku, ok := inst.GetInstanceSku().Get() - if !ok || sku == nil { + if inst.InstanceSKU == nil { continue } - free[sku.ID]-- + free[inst.InstanceSKU.ID]-- } } @@ -184,13 +178,13 @@ func (c *SFCClientV2) skuFreeCapacity(ctx context.Context) (map[string]int, erro // currentScheduleAllocation returns the NodeCount from the schedule entry whose // [StartAt, EndAt) range is currently in effect. EndAt is null only on the final, unbounded // entry. Returns 0 if no entry is in effect. -func currentScheduleAllocation(schedule []components.ScheduleEntry, now int64) int { +func currentScheduleAllocation(schedule []scheduleEntry, now int64) int { for _, entry := range schedule { if entry.StartAt > now { continue } // A set, non-null EndAt bounds the range; the final entry's null EndAt is unbounded. - if endAt, ok := entry.EndAt.Get(); ok && endAt != nil && now >= *endAt { + if entry.EndAt != nil && now >= *entry.EndAt { continue } return entry.NodeCount