diff --git a/v1/providers/shadeform/client.go b/v1/providers/shadeform/client.go index de736def..3258d840 100644 --- a/v1/providers/shadeform/client.go +++ b/v1/providers/shadeform/client.go @@ -58,8 +58,12 @@ func (c *ShadeformCredential) GetCapabilities(ctx context.Context) (v1.Capabilit } // MakeClient creates a new Shadeform client from this credential -func (c *ShadeformCredential) MakeClient(_ context.Context, _ string) (v1.CloudClient, error) { - return NewShadeformClient(c.RefID, c.APIKey), nil +func (c *ShadeformCredential) MakeClient(ctx context.Context, location string) (v1.CloudClient, error) { + return c.MakeClientWithOptions(ctx, location) +} + +func (c *ShadeformCredential) MakeClientWithOptions(_ context.Context, _ string, opts ...ShadeformClientOption) (v1.CloudClient, error) { + return NewShadeformClient(c.RefID, c.APIKey, opts...), nil } // Shadeform implements the CloudClient interface for Shadeform diff --git a/v1/providers/shadeform/instance.go b/v1/providers/shadeform/instance.go index b16bfcf4..8175cd6c 100644 --- a/v1/providers/shadeform/instance.go +++ b/v1/providers/shadeform/instance.go @@ -2,12 +2,12 @@ package v1 import ( "context" - "errors" "fmt" "io" "strings" "github.com/alecthomas/units" + "github.com/brevdev/cloud/internal/errors" v1 "github.com/brevdev/cloud/v1" openapi "github.com/brevdev/cloud/v1/providers/shadeform/gen/shadeform" "github.com/google/uuid" @@ -24,9 +24,11 @@ const ( func (c *ShadeformClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) { //nolint:gocyclo,funlen // ok authCtx := c.makeAuthContext(ctx) + c.logger.Debug(ctx, "Creating instance", v1.LogField("instanceAttrs", attrs)) // Check if the instance type is allowed by configuration - if !c.isInstanceTypeAllowed(attrs.InstanceType) { - return nil, fmt.Errorf("instance type: %v is not deployable", attrs.InstanceType) + allowed, _ := c.isInstanceTypeAllowed(attrs.InstanceType) + if !allowed { + return nil, errors.WrapAndTrace(fmt.Errorf("instance type: %v is not deployable", attrs.InstanceType)) } sshKeyID := "" @@ -38,37 +40,38 @@ func (c *ShadeformClient) CreateInstance(ctx context.Context, attrs v1.CreateIns if keyPairName == "" { keyPairName = uuid.New().String() + c.logger.Debug(ctx, "No key pair name provided, generating new one", v1.LogField("keyPairName", keyPairName)) } if attrs.PublicKey != "" { var err error sshKeyID, err = c.addSSHKey(ctx, keyPairName, attrs.PublicKey) if err != nil && !strings.Contains(err.Error(), "name must be unique") { - return nil, fmt.Errorf("failed to add SSH key: %w", err) + return nil, errors.WrapAndTrace(fmt.Errorf("failed to add SSH key: %w", err)) } } region := attrs.Location cloud, shadeInstanceType, err := c.getShadeformCloudAndInstanceType(attrs.InstanceType) if err != nil { - return nil, err + return nil, errors.WrapAndTrace(err) } cloudEnum, err := openapi.NewCloudFromValue(cloud) if err != nil { - return nil, err + return nil, errors.WrapAndTrace(err) } // Add refID tag refIDTag, err := c.createTag(refIDTagName, attrs.RefID) if err != nil { - return nil, err + return nil, errors.WrapAndTrace(err) } // Add cloudRefID tag cloudCredRefIDTag, err := c.createTag(cloudCredRefIDTagName, c.GetReferenceID()) if err != nil { - return nil, err + return nil, errors.WrapAndTrace(err) } tags := []string{refIDTag, cloudCredRefIDTag} @@ -76,14 +79,14 @@ func (c *ShadeformClient) CreateInstance(ctx context.Context, attrs v1.CreateIns for key, value := range attrs.Tags { createdTag, err := c.createTag(key, value) if err != nil { - return nil, err + return nil, errors.WrapAndTrace(err) } tags = append(tags, createdTag) } base64Script, err := c.GenerateFirewallScript(attrs.FirewallRules) if err != nil { - return nil, err + return nil, errors.WrapAndTrace(err) } req := openapi.CreateRequest{ @@ -108,18 +111,18 @@ func (c *ShadeformClient) CreateInstance(ctx context.Context, attrs v1.CreateIns } if err != nil { httpMessage, _ := io.ReadAll(httpResp.Body) - return nil, fmt.Errorf("failed to create instance: %w, %s", err, string(httpMessage)) + return nil, errors.WrapAndTrace(fmt.Errorf("failed to create instance: %w, %s", err, string(httpMessage))) } if resp == nil { - return nil, fmt.Errorf("no instance returned from create request") + return nil, errors.WrapAndTrace(fmt.Errorf("no instance returned from create request")) } // Since Shadeform doesn't return the full instance that's created, we need to make a second API call to get // the created instance after we call create createdInstance, err := c.GetInstance(authCtx, v1.CloudProviderInstanceID(resp.Id)) if err != nil { - return nil, err + return nil, errors.WrapAndTrace(err) } return createdInstance, nil @@ -152,11 +155,11 @@ func (c *ShadeformClient) addSSHKey(ctx context.Context, keyPairName string, pub } if err != nil { httpMessage, _ := io.ReadAll(httpResp.Body) - return "", fmt.Errorf("failed to add SSH Key: %w, %s", err, string(httpMessage)) + return "", errors.WrapAndTrace(fmt.Errorf("failed to add SSH Key: %w, %s", err, string(httpMessage))) } if resp == nil { - return "", fmt.Errorf("no instance returned from post request") + return "", errors.WrapAndTrace(fmt.Errorf("no instance returned from post request")) } return resp.Id, nil @@ -170,16 +173,16 @@ func (c *ShadeformClient) GetInstance(ctx context.Context, instanceID v1.CloudPr defer func() { _ = httpResp.Body.Close() }() } if err != nil { - return nil, fmt.Errorf("failed to get instance: %w", err) + return nil, errors.WrapAndTrace(fmt.Errorf("failed to get instance: %w", err)) } if resp == nil { - return nil, fmt.Errorf("no instance returned from get request") + return nil, errors.WrapAndTrace(fmt.Errorf("no instance returned from get request")) } - instance, err := c.convertInstanceInfoResponseToV1Instance(*resp) + instance, err := c.convertInstanceInfoResponseToV1Instance(ctx, *resp) if err != nil { - return nil, err + return nil, errors.WrapAndTrace(err) } return instance, nil @@ -193,7 +196,7 @@ func (c *ShadeformClient) TerminateInstance(ctx context.Context, instanceID v1.C defer func() { _ = httpResp.Body.Close() }() } if err != nil { - return fmt.Errorf("failed to terminate instance: %w", err) + return errors.WrapAndTrace(fmt.Errorf("failed to terminate instance: %w", err)) } return nil @@ -207,14 +210,14 @@ func (c *ShadeformClient) ListInstances(ctx context.Context, _ v1.ListInstancesA defer func() { _ = httpResp.Body.Close() }() } if err != nil { - return nil, fmt.Errorf("failed to list instances: %w", err) + return nil, errors.WrapAndTrace(fmt.Errorf("failed to list instances: %w", err)) } var instances []v1.Instance for _, instance := range resp.Instances { - singleInstance, err := c.convertShadeformInstanceToV1Instance(instance) + singleInstance, err := c.convertShadeformInstanceToV1Instance(ctx, instance) if err != nil { - return nil, err + return nil, errors.WrapAndTrace(err) } instances = append(instances, *singleInstance) } @@ -254,27 +257,33 @@ func (c *ShadeformClient) getLifecycleStatus(status string) v1.LifecycleStatus { } // convertInstanceInfoResponseToV1Instance - converts Instance Info to v1 instance -func (c *ShadeformClient) convertInstanceInfoResponseToV1Instance(instanceInfo openapi.InstanceInfoResponse) (*v1.Instance, error) { +func (c *ShadeformClient) convertInstanceInfoResponseToV1Instance(ctx context.Context, instanceInfo openapi.InstanceInfoResponse) (*v1.Instance, error) { + c.logger.Debug(ctx, "converting instance info response to v1 instance", v1.LogField("instanceInfo", instanceInfo)) instanceType := c.getInstanceType(string(instanceInfo.Cloud), instanceInfo.ShadeInstanceType) lifeCycleStatus := c.getLifecycleStatus(string(instanceInfo.Status)) tags, err := c.convertShadeformTagToV1Tag(instanceInfo.Tags) if err != nil { - return nil, err + return nil, errors.WrapAndTrace(err) } refID, found := tags[refIDTagName] if !found { - return nil, errors.New("could not find refID tag") + return nil, errors.WrapAndTrace(errors.New("could not find refID tag")) } + c.logger.Debug(ctx, "refID found, deleting from tags", v1.LogField("refID", refID)) delete(tags, refIDTagName) - cloudCredRefID := tags[cloudCredRefIDTagName] - if err != nil { - return nil, errors.New("could not find cloudCredRefID tag") + cloudCredRefID, found := tags[cloudCredRefIDTagName] + if !found { + return nil, errors.WrapAndTrace(errors.New("could not find cloudCredRefID tag")) } + c.logger.Debug(ctx, "cloudCredRefID found, deleting from tags", v1.LogField("cloudCredRefID", cloudCredRefID)) delete(tags, cloudCredRefIDTagName) + diskSize := units.Base2Bytes(instanceInfo.Configuration.StorageInGb) * units.GiB + c.logger.Debug(ctx, "calculated diskSize", v1.LogField("diskSize", diskSize), v1.LogField("storageInGb", instanceInfo.Configuration.StorageInGb)) + instance := &v1.Instance{ Name: c.getProvidedInstanceName(instanceInfo.Name), CreatedAt: instanceInfo.CreatedAt, @@ -285,7 +294,7 @@ func (c *ShadeformClient) convertInstanceInfoResponseToV1Instance(instanceInfo o ImageID: instanceInfo.Configuration.Os, InstanceType: instanceType, InstanceTypeID: v1.InstanceTypeID(c.getInstanceTypeID(instanceType, instanceInfo.Region)), - DiskSize: units.Base2Bytes(instanceInfo.Configuration.StorageInGb) * units.GiB, + DiskSize: diskSize, SSHUser: instanceInfo.SshUser, SSHPort: int(instanceInfo.SshPort), Status: v1.Status{ @@ -304,27 +313,33 @@ func (c *ShadeformClient) convertInstanceInfoResponseToV1Instance(instanceInfo o // convertInstanceInfoResponseToV1Instance - converts /instances response to v1 instance; the api struct is slightly // different from instance info response and expected to diverge so keeping it as a separate function for now -func (c *ShadeformClient) convertShadeformInstanceToV1Instance(shadeformInstance openapi.Instance) (*v1.Instance, error) { +func (c *ShadeformClient) convertShadeformInstanceToV1Instance(ctx context.Context, shadeformInstance openapi.Instance) (*v1.Instance, error) { + c.logger.Debug(ctx, "converting shadeform instance to v1 instance", v1.LogField("shadeformInstance", shadeformInstance)) instanceType := c.getInstanceType(string(shadeformInstance.Cloud), shadeformInstance.ShadeInstanceType) lifeCycleStatus := c.getLifecycleStatus(string(shadeformInstance.Status)) tags, err := c.convertShadeformTagToV1Tag(shadeformInstance.Tags) if err != nil { - return nil, err + return nil, errors.WrapAndTrace(err) } refID, found := tags[refIDTagName] if !found { - return nil, errors.New("could not find refID tag") + return nil, errors.WrapAndTrace(errors.New("could not find refID tag")) } + c.logger.Debug(ctx, "refID found, deleting from tags", v1.LogField("refID", refID)) delete(tags, refIDTagName) - cloudCredRefID := tags[cloudCredRefIDTagName] - if err != nil { - return nil, errors.New("could not find cloudCredRefID tag") + cloudCredRefID, found := tags[cloudCredRefIDTagName] + if !found { + return nil, errors.WrapAndTrace(errors.New("could not find cloudCredRefID tag")) } + c.logger.Debug(ctx, "cloudCredRefID found, deleting from tags", v1.LogField("cloudCredRefID", cloudCredRefID)) delete(tags, cloudCredRefIDTagName) + diskSize := units.Base2Bytes(shadeformInstance.Configuration.StorageInGb) * units.GiB + c.logger.Debug(ctx, "calculated diskSize", v1.LogField("diskSize", diskSize), v1.LogField("storageInGb", shadeformInstance.Configuration.StorageInGb)) + instance := &v1.Instance{ Name: shadeformInstance.Name, CreatedAt: shadeformInstance.CreatedAt, @@ -334,7 +349,7 @@ func (c *ShadeformClient) convertShadeformInstanceToV1Instance(shadeformInstance Hostname: hostname, ImageID: shadeformInstance.Configuration.Os, InstanceType: instanceType, - DiskSize: units.Base2Bytes(shadeformInstance.Configuration.StorageInGb) * units.GiB, + DiskSize: diskSize, SSHUser: shadeformInstance.SshUser, SSHPort: int(shadeformInstance.SshPort), Status: v1.Status{ @@ -357,7 +372,7 @@ func (c *ShadeformClient) convertShadeformTagToV1Tag(shadeformTags []string) (v1 for _, tag := range shadeformTags { key, value, err := c.getTag(tag) if err != nil { - return nil, err + return nil, errors.WrapAndTrace(err) } tags[key] = value } @@ -366,7 +381,7 @@ func (c *ShadeformClient) convertShadeformTagToV1Tag(shadeformTags []string) (v1 func (c *ShadeformClient) createTag(key string, value string) (string, error) { if strings.Contains(key, "=") { - return "", errors.New("tags cannot contain the '=' character") + return "", errors.WrapAndTrace(errors.New("tags cannot contain the '=' character")) } return fmt.Sprintf("%v=%v", key, value), nil @@ -375,7 +390,7 @@ func (c *ShadeformClient) createTag(key string, value string) (string, error) { func (c *ShadeformClient) getTag(shadeformTag string) (string, string, error) { key, value, found := strings.Cut(shadeformTag, "=") if !found { - return "", "", fmt.Errorf("tag %v does not conform to the key value tag format", shadeformTag) + return "", "", errors.WrapAndTrace(fmt.Errorf("tag %v does not conform to the key value tag format", shadeformTag)) } return key, value, nil } diff --git a/v1/providers/shadeform/instancetype.go b/v1/providers/shadeform/instancetype.go index b01218ec..7be4ae63 100644 --- a/v1/providers/shadeform/instancetype.go +++ b/v1/providers/shadeform/instancetype.go @@ -2,7 +2,6 @@ package v1 import ( "context" - "errors" "fmt" "strings" "time" @@ -10,6 +9,7 @@ import ( "github.com/alecthomas/units" "github.com/bojanz/currency" + "github.com/brevdev/cloud/internal/errors" v1 "github.com/brevdev/cloud/v1" openapi "github.com/brevdev/cloud/v1/providers/shadeform/gen/shadeform" ) @@ -27,6 +27,7 @@ func (c *ShadeformClient) GetInstanceTypes(ctx context.Context, args v1.GetInsta request := c.client.DefaultAPI.InstancesTypes(authCtx) if len(args.Locations) > 0 && args.Locations[0] != AllRegions { regionFilter := args.Locations[0] + c.logger.Debug(ctx, "filtering by region", v1.LogField("regionFilter", regionFilter)) request = request.Region(regionFilter) } @@ -35,25 +36,35 @@ func (c *ShadeformClient) GetInstanceTypes(ctx context.Context, args v1.GetInsta defer func() { _ = httpResp.Body.Close() }() } if err != nil { - return nil, fmt.Errorf("failed to get instance types: %w", err) + return nil, errors.WrapAndTrace(fmt.Errorf("failed to get instance types: %w", err)) } var instanceTypes []v1.InstanceType + c.logger.Debug(ctx, "converting shadeform instance types to v1 instance types", v1.LogField("countToConvert", len(resp.InstanceTypes)), v1.LogField("shadeformInstanceTypes", resp.InstanceTypes)) + var errCount int for _, sfInstanceType := range resp.InstanceTypes { instanceTypesFromShadeformInstanceType, err := c.convertShadeformInstanceTypeToV1InstanceType(sfInstanceType) if err != nil { - return nil, err + return nil, errors.WrapAndTrace(err) } // Filter the list down to the instance types that are allowed by the configuration filter and the args for _, singleInstanceType := range instanceTypesFromShadeformInstanceType { if !isSelectedByArgs(singleInstanceType, args) { + c.logger.Debug(ctx, "instance type not selected by args", v1.LogField("instanceType", singleInstanceType.Type)) continue } - if c.isInstanceTypeAllowed(singleInstanceType.Type) { + allowed, err := c.isInstanceTypeAllowed(singleInstanceType.Type) + if err != nil { + errCount++ + } + if allowed { instanceTypes = append(instanceTypes, singleInstanceType) } } } + if errCount > 0 { + c.logger.Warn(ctx, "error converting instance types", v1.LogField("errCount", errCount)) + } return instanceTypes, nil } @@ -97,7 +108,7 @@ func (c *ShadeformClient) GetLocations(ctx context.Context, _ v1.GetLocationsArg } if err != nil { - return nil, fmt.Errorf("failed to get locations: %w", err) + return nil, errors.WrapAndTrace(fmt.Errorf("failed to get locations: %w", err)) } // Shadeform doesn't have a dedicated locations API but we can get the same result from using the @@ -130,25 +141,25 @@ func (c *ShadeformClient) GetLocations(ctx context.Context, _ v1.GetLocationsArg } // isInstanceTypeAllowed - determines if an instance type is allowed based on configuration -func (c *ShadeformClient) isInstanceTypeAllowed(instanceType string) bool { +func (c *ShadeformClient) isInstanceTypeAllowed(instanceType string) (bool, error) { // By default, everything is allowed if c.config == nil || c.config.AllowedInstanceTypes == nil { - return true + return true, nil } // Convert to Cloud and Instance Type cloud, shadeInstanceType, err := c.getShadeformCloudAndInstanceType(instanceType) if err != nil { - return false + return false, errors.WrapAndTrace(err) } // Convert to API Cloud Enum cloudEnum, err := openapi.NewCloudFromValue(cloud) if err != nil { - return false + return false, errors.WrapAndTrace(err) } - return c.config.isAllowed(*cloudEnum, shadeInstanceType) + return c.config.isAllowed(*cloudEnum, shadeInstanceType), nil } // getInstanceType - gets the Brev instance type from the shadeform cloud and shade instance type @@ -165,7 +176,7 @@ func (c *ShadeformClient) getInstanceTypeID(instanceType string, region string) func (c *ShadeformClient) getShadeformCloudAndInstanceType(instanceType string) (string, string, error) { shadeformCloud, shadeformInstanceType, found := strings.Cut(instanceType, "_") if !found { - return "", "", errors.New("could not determine shadeform cloud and instance type from instance type") + return "", "", errors.WrapAndTrace(errors.New("could not determine shadeform cloud and instance type from instance type")) } return shadeformCloud, shadeformInstanceType, nil } @@ -202,7 +213,7 @@ func (c *ShadeformClient) convertShadeformInstanceTypeToV1InstanceType(shadeform basePrice, err := convertHourlyPriceToAmount(shadeformInstanceType.HourlyPrice) if err != nil { - return nil, err + return nil, errors.WrapAndTrace(err) } gpuName := shadeformGPUTypeToBrevGPUName(shadeformInstanceType.Configuration.GpuType) @@ -254,7 +265,7 @@ func convertHourlyPriceToAmount(hourlyPrice int32) (*currency.Amount, error) { amount, err := currency.NewAmount(number, UsdCurrentCode) if err != nil { - return nil, err + return nil, errors.WrapAndTrace(err) } return &amount, nil }