diff --git a/v1/providers/shadeform/instance.go b/v1/providers/shadeform/instance.go index 8175cd6..3a85d4d 100644 --- a/v1/providers/shadeform/instance.go +++ b/v1/providers/shadeform/instance.go @@ -2,6 +2,7 @@ package v1 import ( "context" + "encoding/json" "fmt" "io" "strings" @@ -21,6 +22,15 @@ const ( instanceNameSeparator = "_" ) +const ( + OutOfStockErrorCode = "OUT_OF_STOCK" +) + +type DefaultErrorResponse struct { + ErrorCode string `json:"error_code"` + Error string `json:"error"` +} + func (c *ShadeformClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) { //nolint:gocyclo,funlen // ok authCtx := c.makeAuthContext(ctx) @@ -110,8 +120,26 @@ func (c *ShadeformClient) CreateInstance(ctx context.Context, attrs v1.CreateIns defer func() { _ = httpResp.Body.Close() }() } if err != nil { - httpMessage, _ := io.ReadAll(httpResp.Body) - return nil, errors.WrapAndTrace(fmt.Errorf("failed to create instance: %w, %s", err, string(httpMessage))) + if httpResp.StatusCode == 409 { + // Shadeform provides more details on 409 errors in the response body + httpMessage, _ := io.ReadAll(httpResp.Body) + jsonStr := string(httpMessage) + + var errorResponse DefaultErrorResponse + err = json.Unmarshal([]byte(jsonStr), &errorResponse) + if err != nil { + return nil, errors.WrapAndTrace(fmt.Errorf("failed to create instance: %w, %s", err, string(httpMessage))) + } + + if errorResponse.ErrorCode == OutOfStockErrorCode { + return nil, v1.ErrInsufficientResources + } else { + return nil, errors.WrapAndTrace(fmt.Errorf("failed to create instance: %w, %s", err, string(httpMessage))) + } + } else { + httpMessage, _ := io.ReadAll(httpResp.Body) + return nil, errors.WrapAndTrace(fmt.Errorf("failed to create instance: %w, %s", err, string(httpMessage))) + } } if resp == nil { diff --git a/v1/providers/shadeform/validation_test.go b/v1/providers/shadeform/validation_test.go index d62756b..65a5f60 100644 --- a/v1/providers/shadeform/validation_test.go +++ b/v1/providers/shadeform/validation_test.go @@ -2,14 +2,14 @@ package v1 import ( "context" + "github.com/brevdev/cloud/internal/validation" + openapi "github.com/brevdev/cloud/v1/providers/shadeform/gen/shadeform" "os" "testing" "time" "github.com/brevdev/cloud/internal/ssh" - "github.com/brevdev/cloud/internal/validation" v1 "github.com/brevdev/cloud/v1" - openapi "github.com/brevdev/cloud/v1/providers/shadeform/gen/shadeform" "github.com/google/uuid" "github.com/stretchr/testify/require" ) @@ -118,6 +118,59 @@ func TestInstanceTypeFilter(t *testing.T) { }) } +func TestOutOfStockError(t *testing.T) { + checkSkip(t) + apiKey := getAPIKey() + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + t.Cleanup(cancel) + + client := NewShadeformClient("validation-test", apiKey) + client.WithConfiguration(Configuration{}) + + _, err := client.CreateInstance(ctx, v1.CreateInstanceAttrs{ + RefID: uuid.New().String(), + InstanceType: "datacrunch_H200x8", + Location: "abc123", // Put a region that is not valid + PublicKey: ssh.GetTestPublicKey(), + Name: "test_name", + FirewallRules: v1.FirewallRules{ + EgressRules: []v1.FirewallRule{ + { + ID: "test-rule1", + FromPort: 80, + ToPort: 8080, + IPRanges: []string{"127.0.0.1", "10.0.0.0/24"}, + }, + { + ID: "test-rule2", + FromPort: 5432, + ToPort: 5432, + IPRanges: []string{"127.0.0.1", "10.0.0.0/24"}, + }, + }, + IngressRules: []v1.FirewallRule{ + { + ID: "test-rule3", + FromPort: 80, + ToPort: 8080, + IPRanges: []string{"127.0.0.1", "10.0.0.0/24"}, + }, + { + ID: "test-rule4", + FromPort: 5432, + ToPort: 5432, + IPRanges: []string{"127.0.0.1", "10.0.0.0/24"}, + }, + }, + }, + }) + if err == nil { + t.Fatalf("ValidateCreateInstance failed: Should have resulted in an insufficientResourcesError") + } + require.True(t, err.Error() == v1.ErrInsufficientResources.Error(), "Error must be ErrInsufficientResources") +} + func checkSkip(t *testing.T) { apiKey := getAPIKey() isValidationTest := os.Getenv("VALIDATION_TEST")