diff --git a/internal/acctest/checks.go b/internal/acctest/checks.go index 0957cb3c25..1137e038fc 100644 --- a/internal/acctest/checks.go +++ b/internal/acctest/checks.go @@ -11,21 +11,35 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/locality" ) +var ( + ErrResourceIDNotSet = errors.New("resourceID was not set") + ErrResourceNotFound = errors.New("resource was not found") + ErrResourceIDPersisted = errors.New("resource ID persisted when it should have changed") + ErrResourceIDChanged = errors.New("resource ID changed when it should have persisted") + ErrResourceAttributeNotFound = errors.New("resource attribute not found") + ErrResourceNotFoundSimple = errors.New("not found") + ErrIDMismatch = errors.New("ID mismatch") + ErrResourceNotDestroyed = errors.New("resource was not destroyed") + ErrInvalidIPv4 = errors.New("is not a valid IPv4") + ErrInvalidIPv6 = errors.New("is not a valid IPv6") + ErrInvalidIP = errors.New("is not a valid IP") +) + // CheckResourceIDChanged checks that the ID of the resource has indeed changed, in case of ForceNew for example. // It will fail if resourceID is empty so be sure to use acctest.CheckResourceIDPersisted first in a test suite. func CheckResourceIDChanged(resourceName string, resourceID *string) resource.TestCheckFunc { return func(s *terraform.State) error { if resourceID == nil || *resourceID == "" { - return errors.New("resourceID was not set") + return ErrResourceIDNotSet } rs, ok := s.RootModule().Resources[resourceName] if !ok { - return fmt.Errorf("resource was not found: %s", resourceName) + return fmt.Errorf("%w: %s", ErrResourceNotFound, resourceName) } if *resourceID == rs.Primary.ID { - return errors.New("resource ID persisted when it should have changed") + return ErrResourceIDPersisted } *resourceID = rs.Primary.ID @@ -40,11 +54,11 @@ func CheckResourceIDPersisted(resourceName string, resourceID *string) resource. return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] if !ok { - return fmt.Errorf("resource was not found: %s", resourceName) + return fmt.Errorf("%w: %s", ErrResourceNotFound, resourceName) } if *resourceID != "" && *resourceID != rs.Primary.ID { - return errors.New("resource ID changed when it should have persisted") + return ErrResourceIDChanged } *resourceID = rs.Primary.ID @@ -58,19 +72,19 @@ func CheckResourceRawIDMatches(res1, attr1, res2, attr2 string) resource.TestChe return func(s *terraform.State) error { rs1, ok1 := s.RootModule().Resources[res1] if !ok1 { - return fmt.Errorf("not found: %s", res1) + return fmt.Errorf("%w: %s", ErrResourceNotFoundSimple, res1) } rs2, ok2 := s.RootModule().Resources[res2] if !ok2 { - return fmt.Errorf("not found: %s", res2) + return fmt.Errorf("%w: %s", ErrResourceNotFoundSimple, res2) } id1 := locality.ExpandID(rs1.Primary.Attributes[attr1]) id2 := locality.ExpandID(rs2.Primary.Attributes[attr2]) if id1 != id2 { - return fmt.Errorf("ID mismatch: %s from resource %s does not match ID %s from resource %s", id1, res1, id2, res2) + return fmt.Errorf("%w: %s from resource %s does not match ID %s from resource %s", ErrIDMismatch, id1, res1, id2, res2) } return nil @@ -87,12 +101,12 @@ func CheckResourceAttrFunc(name string, key string, test func(string) error) res return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[name] if !ok { - return fmt.Errorf("resource not found: %s", name) + return fmt.Errorf("%w: %s", ErrResourceNotFoundSimple, name) } value, ok := rs.Primary.Attributes[key] if !ok { - return fmt.Errorf("key not found: %s", key) + return fmt.Errorf("%w: %s", ErrResourceAttributeNotFound, key) } err := test(value) @@ -108,7 +122,7 @@ func CheckResourceAttrIPv4(name string, key string) resource.TestCheckFunc { return CheckResourceAttrFunc(name, key, func(value string) error { ip := net.ParseIP(value) if ip.To4() == nil { - return fmt.Errorf("%s is not a valid IPv4", value) + return fmt.Errorf("%w", ErrInvalidIPv4) } return nil @@ -119,7 +133,7 @@ func CheckResourceAttrIPv6(name string, key string) resource.TestCheckFunc { return CheckResourceAttrFunc(name, key, func(value string) error { ip := net.ParseIP(value) if ip.To16() == nil { - return fmt.Errorf("%s is not a valid IPv6", value) + return fmt.Errorf("%w", ErrInvalidIPv6) } return nil @@ -130,7 +144,7 @@ func CheckResourceAttrIP(name string, key string) resource.TestCheckFunc { return CheckResourceAttrFunc(name, key, func(value string) error { ip := net.ParseIP(value) if ip == nil { - return fmt.Errorf("%s is not a valid IP", value) + return fmt.Errorf("%w", ErrInvalidIP) } return nil diff --git a/internal/acctest/validate_cassettes_test.go b/internal/acctest/validate_cassettes_test.go index 829e0d9fe4..47e1d19e6f 100644 --- a/internal/acctest/validate_cassettes_test.go +++ b/internal/acctest/validate_cassettes_test.go @@ -22,6 +22,11 @@ import ( "gopkg.in/dnaeon/go-vcr.v3/cassette" ) +var ( + ErrCassetteStatusMismatch = errors.New("status mismatch found in cassette") + ErrNoMatchingTest = errors.New("cassette has no matching test") +) + const servicesDir = "../services" func exceptionsCassettesCases() map[string]struct{} { @@ -82,7 +87,7 @@ func checkErrorCode(c *cassette.Cassette) error { for _, i := range c.Interactions { if !checkErrCodeExcept(i, c, http.StatusBadRequest, http.StatusNotFound, http.StatusTooManyRequests, http.StatusForbidden, http.StatusGone) && !isTransientStateError(i) { - return fmt.Errorf("status: %v found on %s. method: %s, url %s\nrequest body = %v\nresponse body = %v", i.Response.Code, c.Name, i.Request.Method, i.Request.URL, i.Request.Body, i.Response.Body) + return fmt.Errorf("%w: status: %v found on %s. method: %s, url %s\nrequest body = %v\nresponse body = %v", ErrCassetteStatusMismatch, i.Response.Code, c.Name, i.Request.Method, i.Request.URL, i.Request.Body, i.Response.Body) } } @@ -190,7 +195,7 @@ func TestAccCassettes_CheckOrphans(t *testing.T) { // Look for cassettes with no matching test for actualCassettePath := range actualCassettesPaths { if _, ok := expectedCassettesPaths[actualCassettePath]; !ok { - cassetteWithNoTestErrs = append(cassetteWithNoTestErrs, fmt.Errorf("+ cassette [%s] has no matching test", actualCassettePath)) + cassetteWithNoTestErrs = append(cassetteWithNoTestErrs, fmt.Errorf("%w + cassette [%s] has no matching test", ErrNoMatchingTest, actualCassettePath)) } } diff --git a/internal/cdf/locality.go b/internal/cdf/locality.go index e2f2ed04c3..b3f853728f 100644 --- a/internal/cdf/locality.go +++ b/internal/cdf/locality.go @@ -12,6 +12,11 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/meta" ) +var ( + ErrMissingLocality = errors.New("missing locality zone or region to check IDs") + ErrDifferentLocality = errors.New("has different locality than the resource") +) + // expandListKeys return the list of keys for an attribute in a list // example for private-networks.#.id in a list of size 2 // will return private-networks.0.id and private-networks.1.id @@ -71,7 +76,7 @@ func LocalityCheck(keys ...string) schema.CustomizeDiffFunc { l := getLocality(diff, m) if l == "" { - return errors.New("missing locality zone or region to check IDs") + return ErrMissingLocality } for _, key := range keys { @@ -82,13 +87,13 @@ func LocalityCheck(keys ...string) schema.CustomizeDiffFunc { for _, listKey := range listKeys { IDLocality, _, err := locality.ParseLocalizedID(diff.Get(listKey).(string)) if err == nil && !locality.CompareLocalities(IDLocality, l) { - return fmt.Errorf("given %s %s has different locality than the resource %q", listKey, diff.Get(listKey), l) + return fmt.Errorf("given %s %s %w %q", listKey, diff.Get(listKey), ErrDifferentLocality, l) } } } else { IDLocality, _, err := locality.ParseLocalizedID(diff.Get(key).(string)) if err == nil && !locality.CompareLocalities(IDLocality, l) { - return fmt.Errorf("given %s %s has different locality than the resource %q", key, diff.Get(key), l) + return fmt.Errorf("given %s %s %w %q", key, diff.Get(key), ErrDifferentLocality, l) } } } diff --git a/internal/datasource/search.go b/internal/datasource/search.go index 05b67fcabf..8db929c3b2 100644 --- a/internal/datasource/search.go +++ b/internal/datasource/search.go @@ -7,6 +7,13 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" ) +var ( + ErrMultipleElementsFound = errors.New("multiple elements found") + ErrNoElementFound = errors.New("no element found") + ErrMultipleMatches = errors.New("multiple matches") + ErrNoMatchesFound = errors.New("no matches found") +) + // FindExact finds the first element in 'slice' matching the condition defined by 'finder'. // It returns the first matching element and an error if either no match is found or multiple matches are found. func FindExact[T any](slice []T, finder func(T) bool, searchName string) (T, error) { @@ -20,7 +27,7 @@ func FindExact[T any](slice []T, finder func(T) bool, searchName string) (T, err // More than one element found with the same search name var zero T - return zero, fmt.Errorf("multiple elements found with the name %s", searchName) + return zero, fmt.Errorf("%w with the name %s", ErrMultipleElementsFound, searchName) } found = elem @@ -31,7 +38,7 @@ func FindExact[T any](slice []T, finder func(T) bool, searchName string) (T, err if !foundFlag { var zero T - return zero, fmt.Errorf("no element found with the name %s", searchName) + return zero, fmt.Errorf("%w with the name %s", ErrNoElementFound, searchName) } return found, nil @@ -41,10 +48,10 @@ func FindExact[T any](slice []T, finder func(T) bool, searchName string) (T, err func SingularDataSourceFindError(resourceType string, err error) error { if notFound(err) { if errors.Is(err, &TooManyResultsError{}) { - return fmt.Errorf("multiple %[1]ss matched; use additional constraints to reduce matches to a single %[1]s", resourceType) + return fmt.Errorf("%w; use additional constraints to reduce matches to a single %[1]s", ErrMultipleMatches, resourceType) } - return fmt.Errorf("no matching %[1]s found", resourceType) + return fmt.Errorf("%w", ErrNoMatchesFound) } return fmt.Errorf("reading %s: %w", resourceType, err) diff --git a/internal/httperrors/http_test.go b/internal/httperrors/http_test.go index d8df713a78..36082fa708 100644 --- a/internal/httperrors/http_test.go +++ b/internal/httperrors/http_test.go @@ -10,11 +10,15 @@ import ( "github.com/stretchr/testify/assert" ) +var ( + ErrNotAnHTTPError = errors.New("not an http error") +) + func TestIsHTTPCodeError(t *testing.T) { assert.True(t, httperrors.IsHTTPCodeError(&scw.ResponseError{StatusCode: http.StatusBadRequest}, http.StatusBadRequest)) assert.False(t, httperrors.IsHTTPCodeError(nil, http.StatusBadRequest)) assert.False(t, httperrors.IsHTTPCodeError(&scw.ResponseError{StatusCode: http.StatusBadRequest}, http.StatusNotFound)) - assert.False(t, httperrors.IsHTTPCodeError(errors.New("not an http error"), http.StatusNotFound)) + assert.False(t, httperrors.IsHTTPCodeError(ErrNotAnHTTPError, http.StatusNotFound)) } func TestIs404Error(t *testing.T) { diff --git a/internal/locality/parsing.go b/internal/locality/parsing.go index dd2c590754..29127cb798 100644 --- a/internal/locality/parsing.go +++ b/internal/locality/parsing.go @@ -1,15 +1,20 @@ package locality import ( + "errors" "fmt" "strings" ) +var ( + ErrInvalidLocalizedID = errors.New("invalid localized ID format") +) + // ParseLocalizedID parses a localizedID and extracts the resource locality and id. func ParseLocalizedID(localizedID string) (locality, id string, err error) { tab := strings.Split(localizedID, "/") if len(tab) != 2 { - return "", localizedID, fmt.Errorf("cant parse localized id: %s", localizedID) + return "", localizedID, fmt.Errorf("%w: %s", ErrInvalidLocalizedID, localizedID) } return tab[0], tab[1], nil @@ -19,7 +24,7 @@ func ParseLocalizedID(localizedID string) (locality, id string, err error) { func ParseLocalizedNestedID(localizedID string) (locality string, innerID, outerID string, err error) { tab := strings.Split(localizedID, "/") if len(tab) < 3 { - return "", "", localizedID, fmt.Errorf("cant parse localized id: %s", localizedID) + return "", "", localizedID, fmt.Errorf("%w: %s", ErrInvalidLocalizedID, localizedID) } return tab[0], tab[1], strings.Join(tab[2:], "/"), nil @@ -37,7 +42,7 @@ func ParseLocalizedNestedOwnerID(localizedID string) (locality string, innerID, case 3: locality, innerID, outerID, err = ParseLocalizedNestedID(localizedID) default: - err = fmt.Errorf("cant parse localized id: %s", localizedID) + err = fmt.Errorf("%w: %s", ErrInvalidLocalizedID, localizedID) } if err != nil { diff --git a/internal/services/account/project_data_source_test.go b/internal/services/account/project_data_source_test.go index cd5153b2ac..66667018a5 100644 --- a/internal/services/account/project_data_source_test.go +++ b/internal/services/account/project_data_source_test.go @@ -1,6 +1,7 @@ package account_test import ( + "errors" "fmt" "strconv" "testing" @@ -9,6 +10,10 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/acctest" ) +var ( + ErrExpectedAtLeastOneProject = errors.New("expected at least one project") +) + const dummyOrgID = "AB7BD9BF-E1BD-41E8-9F1D-F16A2E3F3925" func TestAccDataSourceProject_Basic(t *testing.T) { @@ -151,7 +156,7 @@ func TestAccDataSourceProject_List(t *testing.T) { } if count < 1 { - return fmt.Errorf("expected at least one project, got %d", count) + return fmt.Errorf("%w, got %d", ErrExpectedAtLeastOneProject, count) } return nil diff --git a/internal/services/account/project_test.go b/internal/services/account/project_test.go index 360636db28..45e32ffac6 100644 --- a/internal/services/account/project_test.go +++ b/internal/services/account/project_test.go @@ -2,6 +2,7 @@ package account_test import ( "context" + "errors" "fmt" "testing" "time" @@ -15,6 +16,11 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/account" ) +var ( + ErrResourceNotFound = errors.New("resource not found") + ErrResourceStillExists = errors.New("resource still exists") +) + var DestroyWaitTimeout = 3 * time.Minute func TestAccProject_Basic(t *testing.T) { @@ -91,7 +97,7 @@ func isProjectPresent(tt *acctest.TestTools, name string) resource.TestCheckFunc return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[name] if !ok { - return fmt.Errorf("resource not found: %s", name) + return fmt.Errorf("%w: %s", ErrResourceNotFound, name) } accountAPI := account.NewProjectAPI(tt.Meta) @@ -124,7 +130,7 @@ func isProjectDestroyed(tt *acctest.TestTools) resource.TestCheckFunc { switch { case err == nil: - return retry.RetryableError(fmt.Errorf("resource %s(%s) still exists", rs.Type, rs.Primary.ID)) + return retry.RetryableError(fmt.Errorf("%w %s(%s)", ErrResourceStillExists, rs.Type, rs.Primary.ID)) case httperrors.Is404(err): continue default: diff --git a/internal/services/applesilicon/os_data_source.go b/internal/services/applesilicon/os_data_source.go index 30a779aea4..952a1d694d 100644 --- a/internal/services/applesilicon/os_data_source.go +++ b/internal/services/applesilicon/os_data_source.go @@ -14,6 +14,11 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/verify" ) +var ( + ErrOSNotFound = errors.New("no OS found") + ErrOSNotFoundWithFilter = errors.New("no OS found with given filter") +) + func DataSourceOS() *schema.Resource { return &schema.Resource{ ReadContext: DataSourceOSRead, @@ -76,7 +81,7 @@ func DataSourceOSRead(ctx context.Context, d *schema.ResourceData, m any) diag.D } if res.TotalCount == 0 { - return diag.FromErr(errors.New("no OS found: something went wrong when listing OS")) + return diag.FromErr(fmt.Errorf("%w: something went wrong when listing OS", ErrOSNotFound)) } for _, os := range res.Os { @@ -89,7 +94,8 @@ func DataSourceOSRead(ctx context.Context, d *schema.ResourceData, m any) diag.D if osID == "" { return diag.FromErr(fmt.Errorf( - "no OS found with name=%q and version=%q in zone %s", + "%w with name=%q and version=%q in zone %s", + ErrOSNotFoundWithFilter, d.Get("name"), d.Get("version"), zone, diff --git a/internal/services/applesilicon/os_data_source_test.go b/internal/services/applesilicon/os_data_source_test.go index f035a16069..7b3b4c9775 100644 --- a/internal/services/applesilicon/os_data_source_test.go +++ b/internal/services/applesilicon/os_data_source_test.go @@ -1,6 +1,7 @@ package applesilicon_test import ( + "errors" "fmt" "testing" @@ -11,6 +12,10 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/locality/zonal" ) +var ( + ErrAppleSiliconOSNotFound = errors.New("not found") +) + func TestAccDataSourceOS_Basic(t *testing.T) { tt := acctest.NewTestTools(t) defer tt.Cleanup() @@ -50,7 +55,7 @@ func testAccCheckAppleSiliconOsExists(tt *acctest.TestTools, n string) resource. rs, ok := s.RootModule().Resources[n] if !ok { - return fmt.Errorf("not found: %s", n) + return fmt.Errorf("%w: %s", ErrAppleSiliconOSNotFound, n) } zone, ID, err := zonal.ParseID(rs.Primary.ID) diff --git a/internal/services/applesilicon/runner_test.go b/internal/services/applesilicon/runner_test.go index 9d909e8ba1..64458b629f 100644 --- a/internal/services/applesilicon/runner_test.go +++ b/internal/services/applesilicon/runner_test.go @@ -1,6 +1,7 @@ package applesilicon_test import ( + "errors" "fmt" "testing" @@ -12,6 +13,11 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/applesilicon" ) +var ( + ErrAppleSiliconResourceNotFound = errors.New("resource not found") + ErrAppleSiliconRunnerStillExists = errors.New("runner still exists") +) + func TestAccRunner_BasicGithub(t *testing.T) { tt := acctest.NewTestTools(t) defer tt.Cleanup() @@ -63,7 +69,7 @@ func isRunnerPresent(tt *acctest.TestTools, resourceName string) resource.TestCh return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] if !ok { - return fmt.Errorf("resource not found: %s", resourceName) + return fmt.Errorf("%w: %s", ErrAppleSiliconResourceNotFound, resourceName) } api, zone, id, err := applesilicon.NewAPIWithZoneAndID(tt.Meta, rs.Primary.ID) @@ -97,7 +103,7 @@ func isRunnerDestroyed(tt *acctest.TestTools) resource.TestCheckFunc { RunnerID: id, }) if err == nil { - return fmt.Errorf("runner still exists: %s", rs.Primary.ID) + return fmt.Errorf("%w: %s", ErrAppleSiliconRunnerStillExists, rs.Primary.ID) } if !httperrors.Is403(err) { diff --git a/internal/services/applesilicon/server_test.go b/internal/services/applesilicon/server_test.go index 1951c07ef8..3a63e15db4 100644 --- a/internal/services/applesilicon/server_test.go +++ b/internal/services/applesilicon/server_test.go @@ -1,6 +1,7 @@ package applesilicon_test import ( + "errors" "fmt" "os" "testing" @@ -14,8 +15,10 @@ import ( ) var ( - githubUrl = os.Getenv("GITHUB_URL_AS") - githubToken = os.Getenv("GITHUB_TOKEN_AS") + githubUrl = os.Getenv("GITHUB_URL_AS") + githubToken = os.Getenv("GITHUB_TOKEN_AS") + ErrAppleSiliconServerNotFound = errors.New("resource not found") + ErrAppleSiliconServerStillExists = errors.New("server still exists") ) func TestAccServer_Basic(t *testing.T) { @@ -377,7 +380,7 @@ func isServerPresent(tt *acctest.TestTools, n string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { - return fmt.Errorf("resource not found: %s", n) + return fmt.Errorf("%w: %s", ErrAppleSiliconServerNotFound, n) } asAPI, zone, ID, err := applesilicon.NewAPIWithZoneAndID(tt.Meta, rs.Primary.ID) @@ -416,7 +419,7 @@ func isServerDestroyed(tt *acctest.TestTools) resource.TestCheckFunc { // If no error resource still exist if err == nil { - return fmt.Errorf("server (%s) still exists", rs.Primary.ID) + return fmt.Errorf("%w (%s)", ErrAppleSiliconServerStillExists, rs.Primary.ID) } // Unexpected api error we return it diff --git a/internal/services/audittrail/event_data_source.go b/internal/services/audittrail/event_data_source.go index 468df4f076..0a54e1060e 100644 --- a/internal/services/audittrail/event_data_source.go +++ b/internal/services/audittrail/event_data_source.go @@ -3,6 +3,7 @@ package audittrail import ( "context" _ "embed" + "errors" "fmt" "github.com/google/uuid" @@ -19,6 +20,10 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/verify" ) +var ( + ErrInvalidOrderByValue = errors.New("invalid order_by value") +) + //go:embed descriptions/event.md var eventDescription string @@ -312,7 +317,7 @@ func readOptionalData(d *schema.ResourceData, req *audittrailSDK.ListEventsReque case "recorded_at_desc": req.OrderBy = audittrailSDK.ListEventsRequestOrderByRecordedAtDesc default: - return fmt.Errorf("invalid order_by value: %s, must be 'recorded_at_asc' or 'recorded_at_desc'", orderBy) + return fmt.Errorf("%w: %s, must be 'recorded_at_asc' or 'recorded_at_desc'", ErrInvalidOrderByValue, orderBy) } } diff --git a/internal/services/audittrail/helpers.go b/internal/services/audittrail/helpers.go index 07a733547f..bb488d8afb 100644 --- a/internal/services/audittrail/helpers.go +++ b/internal/services/audittrail/helpers.go @@ -13,6 +13,11 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/meta" ) +var ( + ErrResourceNotFound = errors.New("not found") + ErrUnexpectedEventCount = errors.New("unexpected event count") +) + // newAPIWithRegionAndProjectID returns a new Audit Trail API, with region and projectID func newAPIWithRegion(d *schema.ResourceData, m any) (*audittrailSDK.API, scw.Region, error) { api := audittrailSDK.NewAPI(meta.ExtractScwClient(m)) @@ -29,7 +34,7 @@ func CheckEventsOccurrence(resourceName string) resource.TestCheckFunc { return func(state *terraform.State) error { rs, ok := state.RootModule().Resources[resourceName] if !ok { - return errors.New("not found: " + resourceName) + return fmt.Errorf("%w: %s", ErrResourceNotFound, resourceName) } countStr := rs.Primary.Attributes["events.#"] @@ -40,7 +45,7 @@ func CheckEventsOccurrence(resourceName string) resource.TestCheckFunc { } if count != 1 { - return fmt.Errorf("expected exactly 1 event, got %d", count) + return fmt.Errorf("%w: expected exactly 1 event, got %d", ErrUnexpectedEventCount, count) } return nil diff --git a/internal/services/audittrail/helpers_test.go b/internal/services/audittrail/helpers_test.go index 2a5398a331..95dac9bc6c 100644 --- a/internal/services/audittrail/helpers_test.go +++ b/internal/services/audittrail/helpers_test.go @@ -15,6 +15,13 @@ import ( "github.com/scaleway/scaleway-sdk-go/scw" ) +var ( + ErrAuditEventNotFoundYet = errors.New("audit event not found yet for resource, retrying") + ErrResourceNotFound = errors.New("resource not found") + ErrAttributeNotFound = errors.New("attribute not found") + ErrUnexpectedResourceID = errors.New("unexpected resource ID") +) + const ( defaultAuditTrailEventsTimeout = 20 * time.Second destroyWaitTimeout = 3 * time.Minute @@ -54,7 +61,7 @@ func waitForAuditTrailEvents(t *testing.T, ctx context.Context, api *audit_trail } // Not found yet - return retry.RetryableError(errors.New("audit event not found yet for resource, retrying...")) + return retry.RetryableError(ErrAuditEventNotFoundYet) }) if err != nil { t.Fatalf("timed out waiting for audit trail event: %v", err) @@ -115,35 +122,35 @@ func checkSecretResourceIDMatchesEvent(resourceName, resourceIDAttr, eventsDataS // Retrieve the secret resource and relevant attribute resource, ok := s.RootModule().Resources[resourceName] if !ok { - return fmt.Errorf("resource not found: %s", resourceName) + return fmt.Errorf("%w: %s", ErrResourceNotFound, resourceName) } resourceID, ok := resource.Primary.Attributes[resourceIDAttr] if !ok { - return fmt.Errorf("attribute not found: %s %s", resource, resourceIDAttr) + return fmt.Errorf("%w: %s %s", ErrAttributeNotFound, resource, resourceIDAttr) } // Retrieve the events data source and relevant attributes events, ok := s.RootModule().Resources[eventsDataSourceName] if !ok { - return fmt.Errorf("resource not found: %s", eventsDataSourceName) + return fmt.Errorf("%w: %s", ErrResourceNotFound, eventsDataSourceName) } eventResourceID, ok := events.Primary.Attributes[eventResourceIDAttr] if !ok { - return fmt.Errorf("attribute not found: %s %s", events, eventResourceIDAttr) + return fmt.Errorf("%w: %s %s", ErrAttributeNotFound, events, eventResourceIDAttr) } eventLocality, ok := events.Primary.Attributes[eventLocalityAttr] if !ok { - return fmt.Errorf("attribute not found: %s %s", events, eventLocalityAttr) + return fmt.Errorf("%w: %s %s", ErrAttributeNotFound, events, eventLocalityAttr) } // Format event resource ID to match the secret resource ID pattern eventResourceFormattedID := fmt.Sprintf("%s/%s", eventLocality, eventResourceID) if resourceID != eventResourceFormattedID { - return fmt.Errorf("expected %s, got %s", resourceID, eventResourceFormattedID) + return fmt.Errorf("%w: expected %s, got %s", ErrUnexpectedResourceID, resourceID, eventResourceFormattedID) } return nil diff --git a/internal/services/autoscaling/instance_group_test.go b/internal/services/autoscaling/instance_group_test.go index f330a3fc77..8415751c3f 100644 --- a/internal/services/autoscaling/instance_group_test.go +++ b/internal/services/autoscaling/instance_group_test.go @@ -1,6 +1,7 @@ package autoscaling_test import ( + "errors" "fmt" "testing" @@ -12,6 +13,11 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/autoscaling" ) +var ( + ErrAutoscalingResourceNotFound = errors.New("resource not found") + ErrAutoscalingStillExists = errors.New("autoscaling instance group still exists") +) + func TestAccInstanceGroup_Basic(t *testing.T) { tt := acctest.NewTestTools(t) defer tt.Cleanup() @@ -117,7 +123,7 @@ func testAccCheckInstanceGroupExists(tt *acctest.TestTools, n string) resource.T return func(state *terraform.State) error { rs, ok := state.RootModule().Resources[n] if !ok { - return fmt.Errorf("resource not found: %s", n) + return fmt.Errorf("%w: %s", ErrAutoscalingResourceNotFound, n) } api, zone, id, err := autoscaling.NewAPIWithZoneAndID(tt.Meta, rs.Primary.ID) @@ -154,7 +160,7 @@ func testAccCheckInstanceGroupDestroy(tt *acctest.TestTools) resource.TestCheckF Zone: zone, }) if err == nil { - return fmt.Errorf("autoscaling instance group (%s) still exists", rs.Primary.ID) + return fmt.Errorf("%w (%s)", ErrAutoscalingStillExists, rs.Primary.ID) } if !httperrors.Is404(err) { diff --git a/internal/services/autoscaling/instance_policy_test.go b/internal/services/autoscaling/instance_policy_test.go index 0375914e46..c5d2cbeabb 100644 --- a/internal/services/autoscaling/instance_policy_test.go +++ b/internal/services/autoscaling/instance_policy_test.go @@ -1,6 +1,7 @@ package autoscaling_test import ( + "errors" "fmt" "testing" @@ -12,6 +13,11 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/autoscaling" ) +var ( + ErrAutoscalingPolicyResourceNotFound = errors.New("resource not found") + ErrAutoscalingPolicyStillExists = errors.New("autoscaling instance policy still exists") +) + func TestAccInstancePolicy_Basic(t *testing.T) { tt := acctest.NewTestTools(t) defer tt.Cleanup() @@ -133,7 +139,7 @@ func testAccCheckInstancePolicyExists(tt *acctest.TestTools, n string) resource. return func(state *terraform.State) error { rs, ok := state.RootModule().Resources[n] if !ok { - return fmt.Errorf("resource not found: %s", n) + return fmt.Errorf("%w: %s", ErrAutoscalingPolicyResourceNotFound, n) } api, zone, id, err := autoscaling.NewAPIWithZoneAndID(tt.Meta, rs.Primary.ID) @@ -170,7 +176,7 @@ func testAccCheckInstancePolicyDestroy(tt *acctest.TestTools) resource.TestCheck Zone: zone, }) if err == nil { - return fmt.Errorf("autoscaling instance policy (%s) still exists", rs.Primary.ID) + return fmt.Errorf("%w (%s)", ErrAutoscalingPolicyStillExists, rs.Primary.ID) } if !httperrors.Is404(err) { diff --git a/internal/services/autoscaling/instance_template_test.go b/internal/services/autoscaling/instance_template_test.go index 0ff00167df..6432778d5b 100644 --- a/internal/services/autoscaling/instance_template_test.go +++ b/internal/services/autoscaling/instance_template_test.go @@ -1,6 +1,7 @@ package autoscaling_test import ( + "errors" "fmt" "testing" @@ -12,6 +13,11 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/autoscaling" ) +var ( + ErrAutoscalingTemplateResourceNotFound = errors.New("resource not found") + ErrAutoscalingTemplateStillExists = errors.New("autoscaling instance template still exists") +) + func TestAccInstanceTemplate_Basic(t *testing.T) { tt := acctest.NewTestTools(t) defer tt.Cleanup() @@ -128,7 +134,7 @@ func testAccCheckInstanceTemplateExists(tt *acctest.TestTools, n string) resourc return func(state *terraform.State) error { rs, ok := state.RootModule().Resources[n] if !ok { - return fmt.Errorf("resource not found: %s", n) + return fmt.Errorf("%w: %s", ErrAutoscalingTemplateResourceNotFound, n) } api, zone, id, err := autoscaling.NewAPIWithZoneAndID(tt.Meta, rs.Primary.ID) @@ -165,7 +171,7 @@ func testAccCheckInstanceTemplateDestroy(tt *acctest.TestTools) resource.TestChe Zone: zone, }) if err == nil { - return fmt.Errorf("autoscaling instance template (%s) still exists", rs.Primary.ID) + return fmt.Errorf("%w (%s)", ErrAutoscalingTemplateStillExists, rs.Primary.ID) } if !httperrors.Is404(err) { diff --git a/internal/services/az/availability_zones_data_source.go b/internal/services/az/availability_zones_data_source.go index 7730533788..6e5dd56805 100644 --- a/internal/services/az/availability_zones_data_source.go +++ b/internal/services/az/availability_zones_data_source.go @@ -2,6 +2,7 @@ package az import ( "context" + "errors" "fmt" "time" @@ -12,6 +13,10 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/datasource" ) +var ( + ErrNotSupportedRegion = errors.New("not a supported region") +) + func DataSourceAvailabilityZones() *schema.Resource { return &schema.Resource{ ReadWithoutTimeout: dataSourceAvailabilityZonesRead, @@ -47,7 +52,7 @@ func dataSourceAvailabilityZonesRead(_ context.Context, d *schema.ResourceData, regionStr := d.Get("region").(string) if !validation.IsRegion(regionStr) { - return diag.FromErr(datasource.SingularDataSourceFindError("Availability Zone", fmt.Errorf("not a supported region %s", regionStr))) + return diag.FromErr(datasource.SingularDataSourceFindError("Availability Zone", fmt.Errorf("%w %s", ErrNotSupportedRegion, regionStr))) } region := scw.Region(regionStr) diff --git a/internal/services/block/testfuncs/checks.go b/internal/services/block/testfuncs/checks.go index 124ed3c6e6..e66f2f73c3 100644 --- a/internal/services/block/testfuncs/checks.go +++ b/internal/services/block/testfuncs/checks.go @@ -1,6 +1,7 @@ package blocktestfuncs import ( + "errors" "fmt" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -11,11 +12,17 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/block" ) +var ( + ErrBlockResourceNotFound = errors.New("resource not found") + ErrBlockVolumeStillExists = errors.New("block volume still exists") + ErrBlockSnapshotStillExists = errors.New("block snapshot still exists") +) + func IsSnapshotPresent(tt *acctest.TestTools, n string) resource.TestCheckFunc { return func(state *terraform.State) error { rs, ok := state.RootModule().Resources[n] if !ok { - return fmt.Errorf("resource not found: %s", n) + return fmt.Errorf("%w: %s", ErrBlockResourceNotFound, n) } api, zone, id, err := block.NewAPIWithZoneAndID(tt.Meta, rs.Primary.ID) @@ -39,7 +46,7 @@ func IsVolumePresent(tt *acctest.TestTools, n string) resource.TestCheckFunc { return func(state *terraform.State) error { rs, ok := state.RootModule().Resources[n] if !ok { - return fmt.Errorf("resource not found: %s", n) + return fmt.Errorf("%w: %s", ErrBlockResourceNotFound, n) } api, zone, id, err := block.NewAPIWithZoneAndID(tt.Meta, rs.Primary.ID) @@ -76,7 +83,7 @@ func IsVolumeDestroyed(tt *acctest.TestTools) resource.TestCheckFunc { Zone: zone, }) if err == nil { - return fmt.Errorf("block volume (%s) still exists", rs.Primary.ID) + return fmt.Errorf("%w (%s)", ErrBlockVolumeStillExists, rs.Primary.ID) } if !httperrors.Is404(err) && !httperrors.Is410(err) { @@ -105,7 +112,7 @@ func IsSnapshotDestroyed(tt *acctest.TestTools) resource.TestCheckFunc { Zone: zone, }) if err == nil { - return fmt.Errorf("block snapshot (%s) still exists", rs.Primary.ID) + return fmt.Errorf("%w (%s)", ErrBlockSnapshotStillExists, rs.Primary.ID) } if !httperrors.Is404(err) { diff --git a/internal/services/cockpit/action_trigger_test_alert_action_test.go b/internal/services/cockpit/action_trigger_test_alert_action_test.go index 66bbcc9020..497907db02 100644 --- a/internal/services/cockpit/action_trigger_test_alert_action_test.go +++ b/internal/services/cockpit/action_trigger_test_alert_action_test.go @@ -2,6 +2,7 @@ package cockpit_test import ( "errors" + "fmt" "strings" "testing" @@ -10,6 +11,11 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/acctest" ) +var ( + ErrCockpitEventNotFound = errors.New("not found") + ErrTriggerTestAlertNotFound = errors.New("did not find the TriggerTestAlert event") +) + func TestAccActionCockpitTriggerTestAlert_Basic(t *testing.T) { if acctest.IsRunningOpenTofu() { t.Skip("Skipping TestAccActionCockpitTriggerTestAlert_Basic because actions are not yet supported on OpenTofu") @@ -96,7 +102,7 @@ func TestAccActionCockpitTriggerTestAlert_Basic(t *testing.T) { func(state *terraform.State) error { rs, ok := state.RootModule().Resources["data.scaleway_audit_trail_event.cockpit"] if !ok { - return errors.New("not found: data.scaleway_audit_trail_event.cockpit") + return fmt.Errorf("%w: data.scaleway_audit_trail_event.cockpit", ErrCockpitEventNotFound) } for key, value := range rs.Primary.Attributes { @@ -109,7 +115,7 @@ func TestAccActionCockpitTriggerTestAlert_Basic(t *testing.T) { } } - return errors.New("did not find the TriggerTestAlert event") + return ErrTriggerTestAlertNotFound }, ), }, diff --git a/internal/services/cockpit/alert_manager.go b/internal/services/cockpit/alert_manager.go index 82c8a677e1..6ca7e08148 100644 --- a/internal/services/cockpit/alert_manager.go +++ b/internal/services/cockpit/alert_manager.go @@ -17,6 +17,11 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/verify" ) +var ( + ErrInvalidContactPointFormat = errors.New("invalid contact point format") + ErrInvalidEmailFormat = errors.New("invalid email format") +) + func ResourceCockpitAlertManager() *schema.Resource { return &schema.Resource{ CreateContext: ResourceCockpitAlertManagerCreate, @@ -140,12 +145,12 @@ func ResourceCockpitAlertManagerCreate(ctx context.Context, d *schema.ResourceDa for _, cp := range contactPoints { cpMap, ok := cp.(map[string]any) if !ok { - return diag.FromErr(errors.New("invalid contact point format")) + return diag.FromErr(ErrInvalidContactPointFormat) } email, ok := cpMap["email"].(string) if !ok { - return diag.FromErr(errors.New("invalid email format")) + return diag.FromErr(ErrInvalidEmailFormat) } emailCP := &cockpit.ContactPointEmail{ diff --git a/internal/services/cockpit/alert_manager_test.go b/internal/services/cockpit/alert_manager_test.go index 7eebc08902..db4bce3e13 100644 --- a/internal/services/cockpit/alert_manager_test.go +++ b/internal/services/cockpit/alert_manager_test.go @@ -16,6 +16,18 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/meta" ) +var ( + ErrAlertManagerNotFound = errors.New("alert manager not found") + ErrContactPointNotFound = errors.New("contact point not found") + ErrAlertManagerStillEnabled = errors.New("cockpit alert manager still enabled") + ErrAlertManagerIDEmpty = errors.New("alert manager ID is empty") + ErrInvalidAlertManagerIDParts = errors.New("alert manager ID should have 3 parts") + ErrEmptyRegionPart = errors.New("region part of ID is empty") + ErrEmptyProjectIDPart = errors.New("project ID part of ID is empty") + ErrInvalidThirdPart = errors.New("third part of ID should be '1'") + ErrProjectIDMismatch = errors.New("project_id mismatch") +) + func TestAccCockpitAlertManager_CreateWithSingleContact(t *testing.T) { tt := acctest.NewTestTools(t) defer tt.Cleanup() @@ -229,7 +241,7 @@ func testAccCheckCockpitContactPointExists(tt *acctest.TestTools, resourceName s return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] if !ok { - return errors.New("alert manager not found: " + resourceName) + return fmt.Errorf("%w: %s", ErrAlertManagerNotFound, resourceName) } api := cockpit.NewRegionalAPI(meta.ExtractScwClient(tt.Meta)) @@ -248,7 +260,7 @@ func testAccCheckCockpitContactPointExists(tt *acctest.TestTools, resourceName s } } - return errors.New("contact point with email " + rs.Primary.Attributes["emails.0"] + " not found in project " + projectID) + return fmt.Errorf("%w: contact point with email %s not found in project %s", ErrContactPointNotFound, rs.Primary.Attributes["emails.0"], projectID) } } @@ -276,7 +288,7 @@ func testAccCockpitAlertManagerAndContactsDestroy(tt *acctest.TestTools) resourc } if alertManager.AlertManagerEnabled { - return errors.New("cockpit alert manager (" + rs.Primary.ID + ") is still enabled") + return fmt.Errorf("%w (%s)", ErrAlertManagerStillEnabled, rs.Primary.ID) } } @@ -289,12 +301,12 @@ func testAccCheckAlertManagerIDFormat(tt *acctest.TestTools, resourceName string return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] if !ok { - return errors.New("alert manager not found: " + resourceName) + return fmt.Errorf("%w: %s", ErrAlertManagerNotFound, resourceName) } id := rs.Primary.ID if id == "" { - return errors.New("alert manager ID is empty") + return ErrAlertManagerIDEmpty } parts := strings.Split(id, "/") @@ -440,7 +452,7 @@ func testAccCheckPreconfiguredAlertsCount(tt *acctest.TestTools, resourceName st return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] if !ok { - return errors.New("alert manager not found: " + resourceName) + return fmt.Errorf("%w: %s", ErrAlertManagerNotFound, resourceName) } actualCountStr := rs.Primary.Attributes["preconfigured_alert_ids.#"] @@ -501,7 +513,7 @@ func testAccCheckManagedAlertsEnabled(tt *acctest.TestTools, resourceName string return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] if !ok { - return errors.New("alert manager not found: " + resourceName) + return fmt.Errorf("%w: %s", ErrAlertManagerNotFound, resourceName) } api := cockpit.NewRegionalAPI(meta.ExtractScwClient(tt.Meta)) diff --git a/internal/services/cockpit/helpers_cockpit.go b/internal/services/cockpit/helpers_cockpit.go index 9f16f665a9..cc0fc54c5e 100644 --- a/internal/services/cockpit/helpers_cockpit.go +++ b/internal/services/cockpit/helpers_cockpit.go @@ -2,6 +2,7 @@ package cockpit import ( "context" + "errors" "fmt" "strconv" "strings" @@ -19,6 +20,12 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/types" ) +var ( + ErrInvalidAlertManagerIDFormat = errors.New("invalid alert manager ID format") + ErrInvalidCockpitID = errors.New("invalid cockpit ID") + ErrInvalidUpgradeIDType = errors.New("upgrade: expected 'id' to be a string") +) + const ( DefaultCockpitTimeout = 5 * time.Minute pathMetricsURL = "/api/v1/push" @@ -62,7 +69,7 @@ func NewAPIWithRegionAndProjectID(m any, id string) (*cockpit.RegionalAPI, scw.R parts := strings.Split(id, "/") if len(parts) != 3 { - return nil, "", "", fmt.Errorf("invalid alert manager ID format: %s, expected region/projectID/1", id) + return nil, "", "", fmt.Errorf("%w: %s, expected region/projectID/1", ErrInvalidAlertManagerIDFormat, id) } return api, scw.Region(parts[0]), parts[1], nil @@ -97,7 +104,7 @@ func cockpitIDWithProjectID(projectID string, id string) string { func parseCockpitID(id string) (projectID string, cockpitID string, err error) { parts := strings.Split(id, "/") if len(parts) != 2 { - return "", "", fmt.Errorf("invalid cockpit ID: %s", id) + return "", "", fmt.Errorf("%w: %s", ErrInvalidCockpitID, id) } return parts[0], parts[1], nil @@ -122,7 +129,7 @@ func cockpitTokenV1UpgradeFunc(_ context.Context, rawState map[string]any, _ any rawState["id"] = regional.NewIDString(defaultRegion, ID) } } else { - return nil, fmt.Errorf("upgrade: expected 'id' to be a string, got %T", rawState["id"]) + return nil, fmt.Errorf("%w, got %T", ErrInvalidUpgradeIDType, rawState["id"]) } return rawState, nil diff --git a/internal/services/cockpit/types.go b/internal/services/cockpit/types.go index f20ae4554f..385e23f8a3 100644 --- a/internal/services/cockpit/types.go +++ b/internal/services/cockpit/types.go @@ -1,12 +1,17 @@ package cockpit import ( + "errors" "fmt" "github.com/scaleway/scaleway-sdk-go/api/cockpit/v1" "github.com/scaleway/scaleway-sdk-go/scw" ) +var ( + ErrInvalidDataSourceType = errors.New("invalid data source type") +) + var scopeMapping = map[string]cockpit.TokenScope{ "query_metrics": cockpit.TokenScopeReadOnlyMetrics, "write_metrics": cockpit.TokenScopeWriteOnlyMetrics, @@ -81,7 +86,7 @@ func createCockpitPushURL(sourceType cockpit.DataSourceType, url string) (string case cockpit.DataSourceTypeTraces: return url + pathTracesURL, nil default: - return "", fmt.Errorf("invalid data source type: %v", sourceType) + return "", fmt.Errorf("%w: %v", ErrInvalidDataSourceType, sourceType) } } diff --git a/internal/services/instance/helpers_instance.go b/internal/services/instance/helpers_instance.go index 3601351175..363f1a6304 100644 --- a/internal/services/instance/helpers_instance.go +++ b/internal/services/instance/helpers_instance.go @@ -25,6 +25,19 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/types" ) +var ( + ErrServerLocked = errors.New("server is locked") + ErrServerInvalidState = errors.New("server is in an invalid state") + ErrServerTransientState = errors.New("server is in a transient state") + ErrUnknownStateTransition = errors.New("don't know how to reach state") + ErrVolumeSizeConstraint = errors.New("total local volume size constraint violation") + ErrVolumeSizeMustBeEqual = errors.New("total local volume size must be equal") + ErrVolumeSizeMustBeBetween = errors.New("total local volume size must be between") + ErrPrivateNetworkNotFound = errors.New("could not find private network ID") + ErrServerProjectIDNotFound = errors.New("no project ID found for server") + ErrServerProjectIDRetrieval = errors.New("get private NIC's project ID") +) + const ( // InstanceServerStateStopped transient state of the instance event stop InstanceServerStateStopped = "stopped" @@ -127,10 +140,10 @@ func serverStateFlatten(fromState instance.ServerState) (string, error) { case instance.ServerStateRunning: return InstanceServerStateStarted, nil case instance.ServerStateLocked: - return "", errors.New("server is locked, please contact Scaleway support: https://console.scaleway.com/support/tickets") + return "", fmt.Errorf("%w, please contact Scaleway support: https://console.scaleway.com/support/tickets", ErrServerLocked) } - return "", errors.New("server is in an invalid state, someone else might be executing action at the same time") + return "", fmt.Errorf("%w, someone else might be executing action at the same time", ErrServerInvalidState) } // serverStateExpand converts terraform state to an API state or return an error. @@ -146,7 +159,7 @@ func serverStateExpand(rawState string) (instance.ServerState, error) { }[rawState] if !exist { - return "", errors.New("server is in a transient state, someone else might be executing another action at the same time") + return "", fmt.Errorf("%w, someone else might be executing another action at the same time", ErrServerTransientState) } return apiState, nil @@ -178,7 +191,7 @@ func reachState(ctx context.Context, api *instancehelpers.BlockAndInstanceAPI, z actions, exist := transitionMap[[2]instance.ServerState{fromState, toState}] if !exist { - return fmt.Errorf("don't know how to reach state %s from state %s for server %s", toState, fromState, serverID) + return fmt.Errorf("%w %s from state %s for server %s", ErrUnknownStateTransition, toState, fromState, serverID) } // We need to check that all volumes are ready @@ -260,12 +273,12 @@ func validateLocalVolumeSizes(volumes map[string]*instance.VolumeServerTemplate, if localVolumeTotalSize < volumeConstraint.MinSize || localVolumeTotalSize > volumeConstraint.MaxSize { minSize := humanize.Bytes(uint64(volumeConstraint.MinSize)) if volumeConstraint.MinSize == volumeConstraint.MaxSize { - return fmt.Errorf("%s total local volume size must be equal to %s", commercialType, minSize) + return fmt.Errorf("%s %w to %s", commercialType, ErrVolumeSizeMustBeEqual, minSize) } maxSize := humanize.Bytes(uint64(volumeConstraint.MaxSize)) - return fmt.Errorf("%s total local volume size must be between %s and %s", commercialType, minSize, maxSize) + return fmt.Errorf("%s %w %s and %s", commercialType, ErrVolumeSizeMustBeBetween, minSize, maxSize) } return nil @@ -422,7 +435,7 @@ func (ph *privateNICsHandler) get(key string) (any, error) { pn, ok := ph.privateNICsMap[id] if !ok { - return nil, fmt.Errorf("could not find private network ID %s on locality %s", key, loc) + return nil, fmt.Errorf("%w %s on locality %s", ErrPrivateNetworkNotFound, key, loc) } return map[string]any{ @@ -547,11 +560,11 @@ func getServerProjectID(ctx context.Context, api *instance.API, zone scw.Zone, s ServerID: serverID, }, scw.WithContext(ctx)) if err != nil { - return "", fmt.Errorf("get private NIC's project ID: error getting server %s", serverID) + return "", fmt.Errorf("%w: error getting server %s", ErrServerProjectIDRetrieval, serverID) } if server.Server.Project == "" { - return "", fmt.Errorf("no project ID found for server %s", serverID) + return "", fmt.Errorf("%w %s", ErrServerProjectIDNotFound, serverID) } return server.Server.Project, nil diff --git a/internal/services/instance/image_data_source.go b/internal/services/instance/image_data_source.go index 6cc7d36cb2..3586f89112 100644 --- a/internal/services/instance/image_data_source.go +++ b/internal/services/instance/image_data_source.go @@ -2,6 +2,7 @@ package instance import ( "context" + "errors" "fmt" "sort" @@ -15,6 +16,11 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/types" ) +var ( + ErrImageNotFound = errors.New("no image found") + ErrMultipleImagesFound = errors.New("multiple images found") +) + func DataSourceImage() *schema.Resource { return &schema.Resource{ ReadContext: DataSourceInstanceImageRead, @@ -127,11 +133,11 @@ func DataSourceInstanceImageRead(ctx context.Context, d *schema.ResourceData, m } if len(matchingImages) == 0 { - return diag.FromErr(fmt.Errorf("no image found with the name %s and architecture %s in zone %s", d.Get("name"), d.Get("architecture"), zone)) + return diag.FromErr(fmt.Errorf("%w with the name %s and architecture %s in zone %s", ErrImageNotFound, d.Get("name"), d.Get("architecture"), zone)) } if len(matchingImages) > 1 && !d.Get("latest").(bool) { - return diag.FromErr(fmt.Errorf("%d images found with the same name %s and architecture %s in zone %s", len(matchingImages), d.Get("name"), d.Get("architecture"), zone)) + return diag.FromErr(fmt.Errorf("%w: %d images found with the same name %s and architecture %s in zone %s", ErrMultipleImagesFound, len(matchingImages), d.Get("name"), d.Get("architecture"), zone)) } sort.Slice(matchingImages, func(i, j int) bool { diff --git a/internal/services/instance/private_nic_data_source.go b/internal/services/instance/private_nic_data_source.go index a4624a9eed..9c8db6931b 100644 --- a/internal/services/instance/private_nic_data_source.go +++ b/internal/services/instance/private_nic_data_source.go @@ -16,6 +16,13 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/verify" ) +var ( + ErrPrivateNICNotFound = errors.New("found no private nic with given filters") + ErrMultiplePrivateNICsFound = errors.New("found more than one private nic with given filters") + ErrMultiplePrivateNICsForPN = errors.New("found more than one private nic for request private network") + ErrPrivateNICNotFoundForPN = errors.New("could not find a private_nic for private network") +) + func DataSourcePrivateNIC() *schema.Resource { // Generate datasource schema from resource dsSchema := datasource.SchemaFromResourceSchema(ResourcePrivateNIC().SchemaFunc()) @@ -103,9 +110,9 @@ func privateNICWithFilters(privateNICs []*instance.PrivateNIC, d *schema.Resourc case len(privateNICs) == 1: return privateNICs[0], nil case len(privateNICs) == 0: - return nil, errors.New("found no private nic with given filters") + return nil, ErrPrivateNICNotFound default: - return nil, errors.New("found more than one private nic with given filters") + return nil, ErrMultiplePrivateNICsFound } } @@ -114,7 +121,7 @@ func privateNICWithFilters(privateNICs []*instance.PrivateNIC, d *schema.Resourc for _, pnic := range privateNICs { if pnic.PrivateNetworkID == privateNetworkID { if privateNIC != nil { - return nil, fmt.Errorf("found more than one private nic for request private network (%s)", privateNetworkID) + return nil, fmt.Errorf("%w (%s)", ErrMultiplePrivateNICsForPN, privateNetworkID) } privateNIC = pnic @@ -125,5 +132,5 @@ func privateNICWithFilters(privateNICs []*instance.PrivateNIC, d *schema.Resourc return privateNIC, nil } - return nil, fmt.Errorf("could not find a private_nic for private network (%s)", privateNetworkID) + return nil, fmt.Errorf("%w (%s)", ErrPrivateNICNotFoundForPN, privateNetworkID) } diff --git a/internal/services/instance/server.go b/internal/services/instance/server.go index 865f39b327..9ec8b273fc 100644 --- a/internal/services/instance/server.go +++ b/internal/services/instance/server.go @@ -40,6 +40,14 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/verify" ) +var ( + ErrInstanceMustBeStoppedForPlacementGroup = errors.New("instance must be stopped to change placement group") + ErrLocalVolumeSizeConstraint = errors.New("local volume total size does not respect type constraint") + ErrFailedToChangeServerType = errors.New("failed to change server type server") + ErrInstanceMustBeStoppedForLocalVolumes = errors.New("instance must be stopped to change local volumes") + ErrProductCatalogEntryNotFound = errors.New("could not find product catalog entry") +) + func ResourceServer() *schema.Resource { return &schema.Resource{ CreateContext: ResourceInstanceServerCreate, @@ -964,7 +972,7 @@ func ResourceInstanceServerUpdate(ctx context.Context, d *schema.ResourceData, m updateRequest.PlacementGroup = &instanceSDK.NullableStringValue{Null: true} } else { if !isStopped { - return diag.FromErr(errors.New("instance must be stopped to change placement group")) + return diag.FromErr(ErrInstanceMustBeStoppedForPlacementGroup) } updateRequest.PlacementGroup = &instanceSDK.NullableStringValue{Value: placementGroupID} @@ -1373,7 +1381,8 @@ func instanceServerCanMigrate(api *instanceSDK.API, server *instanceSDK.Server, if serverType.VolumesConstraint != nil && (localVolumeSize > serverType.VolumesConstraint.MaxSize) || (localVolumeSize < serverType.VolumesConstraint.MinSize) { - return fmt.Errorf("local volume total size does not respect type constraint, expected beteween (%dGB, %dGB), got %sGB", + return fmt.Errorf("%w, expected beteween (%dGB, %dGB), got %sGB", + ErrLocalVolumeSizeConstraint, serverType.VolumesConstraint.MinSize/scw.GB, serverType.VolumesConstraint.MaxSize/scw.GB, localVolumeSize/scw.GB) @@ -1516,7 +1525,7 @@ func ResourceInstanceServerMigrate(ctx context.Context, d *schema.ResourceData, CommercialType: types.ExpandStringPtr(d.Get("type")), }) if err != nil { - return errors.New("failed to change server type server") + return ErrFailedToChangeServerType } err = reachState(ctx, api, zone, id, beginningState) @@ -1666,7 +1675,7 @@ func instanceServerVolumesUpdate(ctx context.Context, d *schema.ResourceData, ap // local volumes can only be added when the server is stopped if volumeHasChange && !serverIsStopped && volume.IsLocal() && volume.IsAttached() { - return nil, errors.New("instance must be stopped to change local volumes") + return nil, ErrInstanceMustBeStoppedForLocalVolumes } volumes[strconv.Itoa(i+1)] = volume.VolumeTemplate() @@ -1695,7 +1704,7 @@ func GetEndOfServiceDate(ctx context.Context, client *scw.Client, zone scw.Zone, } } - return "", fmt.Errorf("could not find product catalog entry for %q in %s", commercialType, zone) + return "", fmt.Errorf("%w for %q in %s", ErrProductCatalogEntryNotFound, commercialType, zone) } func renameRootVolumeIfNeeded(d *schema.ResourceData, api *instancehelpers.BlockAndInstanceAPI, zone scw.Zone, volumes map[string]*instanceSDK.VolumeServer) error { diff --git a/internal/services/instance/testfuncs/checks.go b/internal/services/instance/testfuncs/checks.go index b168a85111..42281224df 100644 --- a/internal/services/instance/testfuncs/checks.go +++ b/internal/services/instance/testfuncs/checks.go @@ -2,6 +2,7 @@ package instancetestfuncs import ( "context" + "errors" "fmt" "time" @@ -19,13 +20,17 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/instance/instancehelpers" ) +var ( + ErrResourceNotFound = errors.New("resource not found") +) + var DestroyWaitTimeout = 3 * time.Minute func CheckIPExists(tt *acctest.TestTools, name string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[name] if !ok { - return fmt.Errorf("resource not found: %s", name) + return fmt.Errorf("%w: %s", ErrResourceNotFound, name) } instanceAPI, zone, ID, err := instance.NewAPIWithZoneAndID(tt.Meta, rs.Primary.ID) @@ -49,7 +54,7 @@ func IsServerPresent(tt *acctest.TestTools, n string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { - return fmt.Errorf("resource not found: %s", n) + return fmt.Errorf("%w: %s", ErrResourceNotFound, n) } instanceAPI, zone, ID, err := instance.NewAPIWithZoneAndID(tt.Meta, rs.Primary.ID) @@ -274,7 +279,7 @@ func IsSnapshotPresent(tt *acctest.TestTools, n string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { - return fmt.Errorf("resource not found: %s", n) + return fmt.Errorf("%w: %s", ErrResourceNotFound, n) } instanceAPI, zone, ID, err := instance.NewAPIWithZoneAndID(tt.Meta, rs.Primary.ID) diff --git a/internal/services/instance/volume.go b/internal/services/instance/volume.go index fce830f9aa..c9122fd66f 100644 --- a/internal/services/instance/volume.go +++ b/internal/services/instance/volume.go @@ -21,6 +21,12 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/verify" ) +var ( + ErrOnlyBlockVolumeCanBeResized = errors.New("only block volume can be resized") + ErrBlockVolumeCannotBeResizedDown = errors.New("block volumes cannot be resized down") + ErrVolumeStillAttachedToServer = errors.New("volume is still attached to a server") +) + func ResourceVolume() *schema.Resource { return &schema.Resource{ CreateContext: ResourceInstanceVolumeCreate, @@ -222,11 +228,11 @@ func ResourceInstanceVolumeUpdate(ctx context.Context, d *schema.ResourceData, m if d.HasChange("size_in_gb") { if d.Get("type") != instanceSDK.VolumeVolumeTypeBSSD.String() { - return diag.FromErr(errors.New("only block volume can be resized")) + return diag.FromErr(ErrOnlyBlockVolumeCanBeResized) } if oldSize, newSize := d.GetChange("size_in_gb"); oldSize.(int) > newSize.(int) { - return diag.FromErr(errors.New("block volumes cannot be resized down")) + return diag.FromErr(ErrBlockVolumeCannotBeResizedDown) } _, err = instancehelpers.WaitForVolume(ctx, instanceAPI, zone, id, d.Timeout(schema.TimeoutUpdate)) @@ -280,7 +286,7 @@ func ResourceInstanceVolumeDelete(ctx context.Context, d *schema.ResourceData, m } if volume.Server != nil { - return diag.FromErr(errors.New("volume is still attached to a server")) + return diag.FromErr(ErrVolumeStillAttachedToServer) } deleteRequest := &instanceSDK.DeleteVolumeRequest{ diff --git a/internal/services/ipam/ip_data_source.go b/internal/services/ipam/ip_data_source.go index 39936ed287..2281416aa7 100644 --- a/internal/services/ipam/ip_data_source.go +++ b/internal/services/ipam/ip_data_source.go @@ -18,6 +18,11 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/verify" ) +var ( + ErrIPNotFoundWithFilters = errors.New("no ip found with given filters") + ErrMultipleIPsFoundWithFilter = errors.New("more than one ip found with given filter") +) + func DataSourceIP() *schema.Resource { return &schema.Resource{ ReadContext: DataSourceIPAMIPRead, @@ -200,11 +205,11 @@ func DataSourceIPAMIPRead(ctx context.Context, d *schema.ResourceData, m any) di if len(resp.IPs) == 0 { // Retry if no IPs are found - return retry.RetryableError(errors.New("no ip found with given filters")) + return retry.RetryableError(ErrIPNotFoundWithFilters) } if len(resp.IPs) > 1 { - return retry.NonRetryableError(errors.New("more than one ip found with given filter")) + return retry.NonRetryableError(ErrMultipleIPsFoundWithFilter) } ip = resp.IPs[0].Address diff --git a/internal/services/vpc/helpers.go b/internal/services/vpc/helpers.go index 6e7904b57d..2841704d4d 100644 --- a/internal/services/vpc/helpers.go +++ b/internal/services/vpc/helpers.go @@ -17,6 +17,12 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/meta" ) +var ( + ErrVPCUpgradeIDNotExist = errors.New("upgrade: id not exist") + ErrVPCUpgradeLocalityRetrieval = errors.New("upgrade: could not retrieve the locality") + ErrVPCUnrecognizedIDFormat = errors.New("unrecognized ID format") +) + const defaultVPCPrivateNetworkRetryInterval = 30 * time.Second // vpcAPIWithRegion returns a new VPC API and the region for a Create request @@ -70,7 +76,7 @@ func vpcPrivateNetworkV1SUpgradeFunc(_ context.Context, rawState map[string]any, ID, exist := rawState["id"] if !exist { - return nil, errors.New("upgrade: id not exist") + return nil, ErrVPCUpgradeIDNotExist } rawState["id"], err = vpcPrivateNetworkUpgradeV1ZonalToRegionalID(ID.(string)) @@ -85,7 +91,7 @@ func vpcPrivateNetworkUpgradeV1ZonalToRegionalID(element string) (string, error) l, id, err := locality.ParseLocalizedID(element) // return error if l cannot be parsed if err != nil { - return "", fmt.Errorf("upgrade: could not retrieve the locality from `%s`", element) + return "", fmt.Errorf("%w from `%s`", ErrVPCUpgradeLocalityRetrieval, element) } // if locality is already regional return if validator.IsRegion(l) { @@ -123,7 +129,7 @@ func vpcRouteExpandResourceID(id string) (string, error) { return ID, nil default: - return "", fmt.Errorf("unrecognized ID format: %s", id) + return "", fmt.Errorf("%w: %s", ErrVPCUnrecognizedIDFormat, id) } } diff --git a/internal/services/vpc/vpc.go b/internal/services/vpc/vpc.go index 8a80d25d83..39c3af078f 100644 --- a/internal/services/vpc/vpc.go +++ b/internal/services/vpc/vpc.go @@ -14,6 +14,10 @@ import ( "github.com/scaleway/terraform-provider-scaleway/v2/internal/types" ) +var ( + ErrVPCRoutingCannotBeDisabled = errors.New("routing cannot be disabled on this VPC") +) + func ResourceVPC() *schema.Resource { return &schema.Resource{ CreateContext: ResourceVPCCreate, @@ -28,7 +32,7 @@ func ResourceVPC() *schema.Resource { CustomizeDiff: func(_ context.Context, diff *schema.ResourceDiff, _ any) error { before, after := diff.GetChange("enable_routing") if before != nil && before.(bool) && after != nil && !after.(bool) { - return errors.New("routing cannot be disabled on this VPC") + return ErrVPCRoutingCannotBeDisabled } return nil