From d282d3db841180db26d1d979285330f0dfbcf600 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 22 Apr 2026 01:01:20 -0400 Subject: [PATCH] feat(drivers): map godo ErrorResponse to interfaces sentinel errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add internal/drivers/errors.go with WrapGodoError(err error) error that inspects *godo.ErrorResponse.Response.StatusCode and wraps with the matching interfaces.Err* sentinel (workflow v0.17.0): 401 → ErrUnauthorized 403 → ErrForbidden 404/405 → ErrResourceNotFound 409 → ErrResourceAlreadyExists 400/422 → ErrValidation 429 → ErrRateLimited 5xx → ErrTransient other → pass-through unchanged Apply WrapGodoError at ~57 godo client call sites across 12 drivers (api_gateway, app_platform, cache, certificate, database, dns, droplet, firewall, kubernetes, load_balancer, registry, vpc). iam_role and spaces use non-godo clients and are unaffected. Bump workflow dep to v0.17.0. Co-Authored-By: Claude Sonnet 4.6 --- go.mod | 2 +- go.sum | 4 +- internal/drivers/api_gateway.go | 8 ++-- internal/drivers/app_platform.go | 14 +++--- internal/drivers/cache.go | 12 ++--- internal/drivers/certificate.go | 6 +-- internal/drivers/database.go | 12 ++--- internal/drivers/dns.go | 12 ++--- internal/drivers/droplet.go | 6 +-- internal/drivers/errors.go | 53 +++++++++++++++++++++ internal/drivers/errors_test.go | 76 +++++++++++++++++++++++++++++++ internal/drivers/firewall.go | 8 ++-- internal/drivers/kubernetes.go | 12 ++--- internal/drivers/load_balancer.go | 8 ++-- internal/drivers/registry.go | 8 ++-- internal/drivers/vpc.go | 8 ++-- 16 files changed, 189 insertions(+), 60 deletions(-) create mode 100644 internal/drivers/errors.go create mode 100644 internal/drivers/errors_test.go diff --git a/go.mod b/go.mod index cbccc24..2b0a409 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/GoCodeAlone/workflow-plugin-digitalocean go 1.26.0 require ( - github.com/GoCodeAlone/workflow v0.15.2 + github.com/GoCodeAlone/workflow v0.17.0 github.com/aws/aws-sdk-go-v2 v1.41.5 github.com/aws/aws-sdk-go-v2/credentials v1.19.12 github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2 diff --git a/go.sum b/go.sum index be2a7ed..fffbc61 100644 --- a/go.sum +++ b/go.sum @@ -56,8 +56,8 @@ github.com/GoCodeAlone/modular/modules/jsonschema v1.15.0 h1:xb1mI4NZkzvNKQ2F6nk github.com/GoCodeAlone/modular/modules/jsonschema v1.15.0/go.mod h1:hhGouwAVsonmJ4Lain4jINZ9nZCoc9l9eF3BHbmR8eE= github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.8.0 h1:cvdLHbM/vzvygQTcAWSJsy+dAPzzwWyjzKMmTBFcFIo= github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.8.0/go.mod h1:/9ipMG4qM2CHQ14BfXKdVlYRJelef6M8MFI5TbZv67M= -github.com/GoCodeAlone/workflow v0.15.2 h1:3zVt22diMNiXl1Alru0nPzpwh8LGaSvcUFm2lIWjwOI= -github.com/GoCodeAlone/workflow v0.15.2/go.mod h1:MGC8lxQzA1TLRIK09mGEN5JNpRzFUcTWgqk3M0/H5FI= +github.com/GoCodeAlone/workflow v0.17.0 h1:Fp4eOdaZKNnIsBvLJT4PcxSmv+++M3X9McKjKMEMz3g= +github.com/GoCodeAlone/workflow v0.17.0/go.mod h1:MGC8lxQzA1TLRIK09mGEN5JNpRzFUcTWgqk3M0/H5FI= github.com/GoCodeAlone/yaegi v0.17.2 h1:WK6Y6e0t1a6U7r+S2dN3CGWW1PizYD3zO0zneToZPxM= github.com/GoCodeAlone/yaegi v0.17.2/go.mod h1:z5Pr6Wse6QJcQvpgxTxzMAevFarH0N37TG88Y9dprx0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= diff --git a/internal/drivers/api_gateway.go b/internal/drivers/api_gateway.go index 5abbf6e..45a0cd7 100644 --- a/internal/drivers/api_gateway.go +++ b/internal/drivers/api_gateway.go @@ -45,7 +45,7 @@ func (d *APIGatewayDriver) Create(ctx context.Context, spec interfaces.ResourceS app, _, err := d.client.Create(ctx, &godo.AppCreateRequest{Spec: appSpec}) if err != nil { - return nil, fmt.Errorf("api_gateway create %q: %w", spec.Name, err) + return nil, fmt.Errorf("api_gateway create %q: %w", spec.Name, WrapGodoError(err)) } return apiGatewayOutput(app), nil } @@ -53,7 +53,7 @@ func (d *APIGatewayDriver) Create(ctx context.Context, spec interfaces.ResourceS func (d *APIGatewayDriver) Read(ctx context.Context, ref interfaces.ResourceRef) (*interfaces.ResourceOutput, error) { app, _, err := d.client.Get(ctx, ref.ProviderID) if err != nil { - return nil, fmt.Errorf("api_gateway read %q: %w", ref.Name, err) + return nil, fmt.Errorf("api_gateway read %q: %w", ref.Name, WrapGodoError(err)) } return apiGatewayOutput(app), nil } @@ -64,7 +64,7 @@ func (d *APIGatewayDriver) Update(ctx context.Context, ref interfaces.ResourceRe app, _, err := d.client.Update(ctx, ref.ProviderID, &godo.AppUpdateRequest{Spec: appSpec}) if err != nil { - return nil, fmt.Errorf("api_gateway update %q: %w", ref.Name, err) + return nil, fmt.Errorf("api_gateway update %q: %w", ref.Name, WrapGodoError(err)) } return apiGatewayOutput(app), nil } @@ -72,7 +72,7 @@ func (d *APIGatewayDriver) Update(ctx context.Context, ref interfaces.ResourceRe func (d *APIGatewayDriver) Delete(ctx context.Context, ref interfaces.ResourceRef) error { _, err := d.client.Delete(ctx, ref.ProviderID) if err != nil { - return fmt.Errorf("api_gateway delete %q: %w", ref.Name, err) + return fmt.Errorf("api_gateway delete %q: %w", ref.Name, WrapGodoError(err)) } return nil } diff --git a/internal/drivers/app_platform.go b/internal/drivers/app_platform.go index 527716d..3f68b05 100644 --- a/internal/drivers/app_platform.go +++ b/internal/drivers/app_platform.go @@ -70,7 +70,7 @@ func (d *AppPlatformDriver) Create(ctx context.Context, spec interfaces.Resource app, _, err := d.client.Create(ctx, req) if err != nil { - return nil, fmt.Errorf("app platform create %q: %w", spec.Name, err) + return nil, fmt.Errorf("app platform create %q: %w", spec.Name, WrapGodoError(err)) } return appOutput(app), nil } @@ -81,7 +81,7 @@ func (d *AppPlatformDriver) Read(ctx context.Context, ref interfaces.ResourceRef } app, _, err := d.client.Get(ctx, ref.ProviderID) if err != nil { - return nil, fmt.Errorf("app platform read %q: %w", ref.Name, err) + return nil, fmt.Errorf("app platform read %q: %w", ref.Name, WrapGodoError(err)) } return appOutput(app), nil } @@ -93,7 +93,7 @@ func (d *AppPlatformDriver) findAppByName(ctx context.Context, name string) (*in for { apps, resp, err := d.client.List(ctx, opts) if err != nil { - return nil, fmt.Errorf("app platform list: %w", err) + return nil, fmt.Errorf("app platform list: %w", WrapGodoError(err)) } for _, app := range apps { if app.Spec != nil && app.Spec.Name == name { @@ -138,7 +138,7 @@ func (d *AppPlatformDriver) Update(ctx context.Context, ref interfaces.ResourceR app, _, err := d.client.Update(ctx, ref.ProviderID, req) if err != nil { - return nil, fmt.Errorf("app platform update %q: %w", ref.Name, err) + return nil, fmt.Errorf("app platform update %q: %w", ref.Name, WrapGodoError(err)) } return appOutput(app), nil } @@ -146,7 +146,7 @@ func (d *AppPlatformDriver) Update(ctx context.Context, ref interfaces.ResourceR func (d *AppPlatformDriver) Delete(ctx context.Context, ref interfaces.ResourceRef) error { _, err := d.client.Delete(ctx, ref.ProviderID) if err != nil { - return fmt.Errorf("app platform delete %q: %w", ref.Name, err) + return fmt.Errorf("app platform delete %q: %w", ref.Name, WrapGodoError(err)) } return nil } @@ -219,7 +219,7 @@ func appHealthResult(app *godo.App) *interfaces.HealthResult { func (d *AppPlatformDriver) Scale(ctx context.Context, ref interfaces.ResourceRef, replicas int) (*interfaces.ResourceOutput, error) { app, _, err := d.client.Get(ctx, ref.ProviderID) if err != nil { - return nil, fmt.Errorf("app platform scale read %q: %w", ref.Name, err) + return nil, fmt.Errorf("app platform scale read %q: %w", ref.Name, WrapGodoError(err)) } spec := app.Spec for _, svc := range spec.Services { @@ -227,7 +227,7 @@ func (d *AppPlatformDriver) Scale(ctx context.Context, ref interfaces.ResourceRe } updated, _, err := d.client.Update(ctx, ref.ProviderID, &godo.AppUpdateRequest{Spec: spec}) if err != nil { - return nil, fmt.Errorf("app platform scale update %q: %w", ref.Name, err) + return nil, fmt.Errorf("app platform scale update %q: %w", ref.Name, WrapGodoError(err)) } return appOutput(updated), nil } diff --git a/internal/drivers/cache.go b/internal/drivers/cache.go index 7275778..f9df40e 100644 --- a/internal/drivers/cache.go +++ b/internal/drivers/cache.go @@ -50,7 +50,7 @@ func (d *CacheDriver) Create(ctx context.Context, spec interfaces.ResourceSpec) db, _, err := d.client.Create(ctx, req) if err != nil { - return nil, fmt.Errorf("cache create %q: %w", spec.Name, err) + return nil, fmt.Errorf("cache create %q: %w", spec.Name, WrapGodoError(err)) } return cacheOutput(db), nil } @@ -58,7 +58,7 @@ func (d *CacheDriver) Create(ctx context.Context, spec interfaces.ResourceSpec) func (d *CacheDriver) Read(ctx context.Context, ref interfaces.ResourceRef) (*interfaces.ResourceOutput, error) { db, _, err := d.client.Get(ctx, ref.ProviderID) if err != nil { - return nil, fmt.Errorf("cache read %q: %w", ref.Name, err) + return nil, fmt.Errorf("cache read %q: %w", ref.Name, WrapGodoError(err)) } return cacheOutput(db), nil } @@ -71,7 +71,7 @@ func (d *CacheDriver) Update(ctx context.Context, ref interfaces.ResourceRef, sp NumNodes: numNodes, }) if err != nil { - return nil, fmt.Errorf("cache update %q: %w", ref.Name, err) + return nil, fmt.Errorf("cache update %q: %w", ref.Name, WrapGodoError(err)) } return d.Read(ctx, ref) } @@ -79,7 +79,7 @@ func (d *CacheDriver) Update(ctx context.Context, ref interfaces.ResourceRef, sp func (d *CacheDriver) Delete(ctx context.Context, ref interfaces.ResourceRef) error { _, err := d.client.Delete(ctx, ref.ProviderID) if err != nil { - return fmt.Errorf("cache delete %q: %w", ref.Name, err) + return fmt.Errorf("cache delete %q: %w", ref.Name, WrapGodoError(err)) } return nil } @@ -109,14 +109,14 @@ func (d *CacheDriver) HealthCheck(ctx context.Context, ref interfaces.ResourceRe func (d *CacheDriver) Scale(ctx context.Context, ref interfaces.ResourceRef, replicas int) (*interfaces.ResourceOutput, error) { db, _, err := d.client.Get(ctx, ref.ProviderID) if err != nil { - return nil, fmt.Errorf("cache scale read %q: %w", ref.Name, err) + return nil, fmt.Errorf("cache scale read %q: %w", ref.Name, WrapGodoError(err)) } _, err = d.client.Resize(ctx, ref.ProviderID, &godo.DatabaseResizeRequest{ SizeSlug: db.SizeSlug, NumNodes: replicas, }) if err != nil { - return nil, fmt.Errorf("cache scale %q: %w", ref.Name, err) + return nil, fmt.Errorf("cache scale %q: %w", ref.Name, WrapGodoError(err)) } return d.Read(ctx, ref) } diff --git a/internal/drivers/certificate.go b/internal/drivers/certificate.go index fed43b7..e564a59 100644 --- a/internal/drivers/certificate.go +++ b/internal/drivers/certificate.go @@ -49,7 +49,7 @@ func (d *CertificateDriver) Create(ctx context.Context, spec interfaces.Resource cert, _, err := d.client.Create(ctx, req) if err != nil { - return nil, fmt.Errorf("certificate create %q: %w", spec.Name, err) + return nil, fmt.Errorf("certificate create %q: %w", spec.Name, WrapGodoError(err)) } return certOutput(cert, spec.Name), nil } @@ -57,7 +57,7 @@ func (d *CertificateDriver) Create(ctx context.Context, spec interfaces.Resource func (d *CertificateDriver) Read(ctx context.Context, ref interfaces.ResourceRef) (*interfaces.ResourceOutput, error) { cert, _, err := d.client.Get(ctx, ref.ProviderID) if err != nil { - return nil, fmt.Errorf("certificate read %q: %w", ref.Name, err) + return nil, fmt.Errorf("certificate read %q: %w", ref.Name, WrapGodoError(err)) } return certOutput(cert, ref.Name), nil } @@ -73,7 +73,7 @@ func (d *CertificateDriver) Update(ctx context.Context, ref interfaces.ResourceR func (d *CertificateDriver) Delete(ctx context.Context, ref interfaces.ResourceRef) error { _, err := d.client.Delete(ctx, ref.ProviderID) if err != nil { - return fmt.Errorf("certificate delete %q: %w", ref.Name, err) + return fmt.Errorf("certificate delete %q: %w", ref.Name, WrapGodoError(err)) } return nil } diff --git a/internal/drivers/database.go b/internal/drivers/database.go index 3c37d2e..dd0b3f9 100644 --- a/internal/drivers/database.go +++ b/internal/drivers/database.go @@ -51,7 +51,7 @@ func (d *DatabaseDriver) Create(ctx context.Context, spec interfaces.ResourceSpe db, _, err := d.client.Create(ctx, req) if err != nil { - return nil, fmt.Errorf("database create %q: %w", spec.Name, err) + return nil, fmt.Errorf("database create %q: %w", spec.Name, WrapGodoError(err)) } return dbOutput(db), nil } @@ -59,7 +59,7 @@ func (d *DatabaseDriver) Create(ctx context.Context, spec interfaces.ResourceSpe func (d *DatabaseDriver) Read(ctx context.Context, ref interfaces.ResourceRef) (*interfaces.ResourceOutput, error) { db, _, err := d.client.Get(ctx, ref.ProviderID) if err != nil { - return nil, fmt.Errorf("database read %q: %w", ref.Name, err) + return nil, fmt.Errorf("database read %q: %w", ref.Name, WrapGodoError(err)) } return dbOutput(db), nil } @@ -72,7 +72,7 @@ func (d *DatabaseDriver) Update(ctx context.Context, ref interfaces.ResourceRef, NumNodes: numNodes, }) if err != nil { - return nil, fmt.Errorf("database update %q: %w", ref.Name, err) + return nil, fmt.Errorf("database update %q: %w", ref.Name, WrapGodoError(err)) } return d.Read(ctx, ref) } @@ -80,7 +80,7 @@ func (d *DatabaseDriver) Update(ctx context.Context, ref interfaces.ResourceRef, func (d *DatabaseDriver) Delete(ctx context.Context, ref interfaces.ResourceRef) error { _, err := d.client.Delete(ctx, ref.ProviderID) if err != nil { - return fmt.Errorf("database delete %q: %w", ref.Name, err) + return fmt.Errorf("database delete %q: %w", ref.Name, WrapGodoError(err)) } return nil } @@ -110,14 +110,14 @@ func (d *DatabaseDriver) HealthCheck(ctx context.Context, ref interfaces.Resourc func (d *DatabaseDriver) Scale(ctx context.Context, ref interfaces.ResourceRef, replicas int) (*interfaces.ResourceOutput, error) { db, _, err := d.client.Get(ctx, ref.ProviderID) if err != nil { - return nil, fmt.Errorf("database scale read %q: %w", ref.Name, err) + return nil, fmt.Errorf("database scale read %q: %w", ref.Name, WrapGodoError(err)) } _, err = d.client.Resize(ctx, ref.ProviderID, &godo.DatabaseResizeRequest{ SizeSlug: db.SizeSlug, NumNodes: replicas, }) if err != nil { - return nil, fmt.Errorf("database scale %q: %w", ref.Name, err) + return nil, fmt.Errorf("database scale %q: %w", ref.Name, WrapGodoError(err)) } return d.Read(ctx, ref) } diff --git a/internal/drivers/dns.go b/internal/drivers/dns.go index 68f096d..7370728 100644 --- a/internal/drivers/dns.go +++ b/internal/drivers/dns.go @@ -48,7 +48,7 @@ func (d *DNSDriver) Create(ctx context.Context, spec interfaces.ResourceSpec) (* if err != nil { dom, _, err = d.client.Create(ctx, &godo.DomainCreateRequest{Name: domain}) if err != nil { - return nil, fmt.Errorf("dns create domain %q: %w", domain, err) + return nil, fmt.Errorf("dns create domain %q: %w", domain, WrapGodoError(err)) } } @@ -63,7 +63,7 @@ func (d *DNSDriver) Read(ctx context.Context, ref interfaces.ResourceRef) (*inte domain := ref.ProviderID dom, _, err := d.client.Get(ctx, domain) if err != nil { - return nil, fmt.Errorf("dns read %q: %w", ref.Name, err) + return nil, fmt.Errorf("dns read %q: %w", ref.Name, WrapGodoError(err)) } return dnsOutput(dom, ref.Name), nil } @@ -79,7 +79,7 @@ func (d *DNSDriver) Update(ctx context.Context, ref interfaces.ResourceRef, spec func (d *DNSDriver) Delete(ctx context.Context, ref interfaces.ResourceRef) error { _, err := d.client.Delete(ctx, ref.ProviderID) if err != nil { - return fmt.Errorf("dns delete %q: %w", ref.Name, err) + return fmt.Errorf("dns delete %q: %w", ref.Name, WrapGodoError(err)) } return nil } @@ -112,7 +112,7 @@ func (d *DNSDriver) upsertRecords(ctx context.Context, domain string, config map existing, _, err := d.client.Records(ctx, domain, nil) if err != nil { - return fmt.Errorf("dns list records %q: %w", domain, err) + return fmt.Errorf("dns list records %q: %w", domain, WrapGodoError(err)) } existingByName := make(map[string]godo.DomainRecord) @@ -141,11 +141,11 @@ func (d *DNSDriver) upsertRecords(ctx context.Context, domain string, config map key := strings.ToLower(rType) + ":" + rName if existing, found := existingByName[key]; found { if _, _, err := d.client.EditRecord(ctx, domain, existing.ID, editReq); err != nil { - return fmt.Errorf("dns update record %q %s/%s: %w", domain, rType, rName, err) + return fmt.Errorf("dns update record %q %s/%s: %w", domain, rType, rName, WrapGodoError(err)) } } else { if _, _, err := d.client.CreateRecord(ctx, domain, editReq); err != nil { - return fmt.Errorf("dns create record %q %s/%s: %w", domain, rType, rName, err) + return fmt.Errorf("dns create record %q %s/%s: %w", domain, rType, rName, WrapGodoError(err)) } } } diff --git a/internal/drivers/droplet.go b/internal/drivers/droplet.go index 0b14741..d1e7276 100644 --- a/internal/drivers/droplet.go +++ b/internal/drivers/droplet.go @@ -45,7 +45,7 @@ func (d *DropletDriver) Create(ctx context.Context, spec interfaces.ResourceSpec droplet, _, err := d.client.Create(ctx, req) if err != nil { - return nil, fmt.Errorf("droplet create %q: %w", spec.Name, err) + return nil, fmt.Errorf("droplet create %q: %w", spec.Name, WrapGodoError(err)) } return dropletOutput(droplet), nil } @@ -54,7 +54,7 @@ func (d *DropletDriver) Read(ctx context.Context, ref interfaces.ResourceRef) (* id := providerIDToInt(ref.ProviderID) droplet, _, err := d.client.Get(ctx, id) if err != nil { - return nil, fmt.Errorf("droplet read %q: %w", ref.Name, err) + return nil, fmt.Errorf("droplet read %q: %w", ref.Name, WrapGodoError(err)) } return dropletOutput(droplet), nil } @@ -67,7 +67,7 @@ func (d *DropletDriver) Delete(ctx context.Context, ref interfaces.ResourceRef) id := providerIDToInt(ref.ProviderID) _, err := d.client.Delete(ctx, id) if err != nil { - return fmt.Errorf("droplet delete %q: %w", ref.Name, err) + return fmt.Errorf("droplet delete %q: %w", ref.Name, WrapGodoError(err)) } return nil } diff --git a/internal/drivers/errors.go b/internal/drivers/errors.go new file mode 100644 index 0000000..bbb01ca --- /dev/null +++ b/internal/drivers/errors.go @@ -0,0 +1,53 @@ +package drivers + +import ( + "fmt" + "net/http" + + "github.com/GoCodeAlone/workflow/interfaces" + "github.com/digitalocean/godo" +) + +// WrapGodoError inspects err for a *godo.ErrorResponse and maps its HTTP status +// code to the matching interfaces.Err* sentinel. The returned error wraps the +// sentinel so callers can use errors.Is for classification while still having +// access to the original DO API message via err.Error(). +// +// If err is nil or not a *godo.ErrorResponse, it is returned unchanged. +func WrapGodoError(err error) error { + if err == nil { + return nil + } + gErr, ok := err.(*godo.ErrorResponse) + if !ok || gErr.Response == nil { + return err + } + sentinel := sentinelForStatus(gErr.Response.StatusCode) + if sentinel == nil { + return err + } + return fmt.Errorf("%w: %v", sentinel, err) +} + +// sentinelForStatus maps an HTTP status code to its interfaces sentinel. +// Returns nil for codes that have no sentinel mapping (pass-through). +func sentinelForStatus(code int) error { + switch { + case code == http.StatusUnauthorized: + return interfaces.ErrUnauthorized + case code == http.StatusForbidden: + return interfaces.ErrForbidden + case code == http.StatusNotFound || code == http.StatusMethodNotAllowed: + return interfaces.ErrResourceNotFound + case code == http.StatusConflict: + return interfaces.ErrResourceAlreadyExists + case code == http.StatusBadRequest || code == http.StatusUnprocessableEntity: + return interfaces.ErrValidation + case code == http.StatusTooManyRequests: + return interfaces.ErrRateLimited + case code >= 500 && code <= 599: + return interfaces.ErrTransient + default: + return nil + } +} diff --git a/internal/drivers/errors_test.go b/internal/drivers/errors_test.go new file mode 100644 index 0000000..cfba007 --- /dev/null +++ b/internal/drivers/errors_test.go @@ -0,0 +1,76 @@ +package drivers_test + +import ( + "errors" + "net/http" + "testing" + + "github.com/GoCodeAlone/workflow-plugin-digitalocean/internal/drivers" + "github.com/GoCodeAlone/workflow/interfaces" + "github.com/digitalocean/godo" +) + +func godoErr(statusCode int) error { + return &godo.ErrorResponse{ + Response: &http.Response{StatusCode: statusCode}, + Message: http.StatusText(statusCode), + } +} + +func TestWrapGodoError_StatusCodeMapping(t *testing.T) { + cases := []struct { + code int + sentinel error + }{ + {http.StatusUnauthorized, interfaces.ErrUnauthorized}, // 401 + {http.StatusForbidden, interfaces.ErrForbidden}, // 403 + {http.StatusNotFound, interfaces.ErrResourceNotFound}, // 404 + {http.StatusMethodNotAllowed, interfaces.ErrResourceNotFound}, // 405 + {http.StatusConflict, interfaces.ErrResourceAlreadyExists}, // 409 + {http.StatusUnprocessableEntity, interfaces.ErrValidation}, // 422 + {http.StatusTooManyRequests, interfaces.ErrRateLimited}, // 429 + {http.StatusBadRequest, interfaces.ErrValidation}, // 400 + {http.StatusInternalServerError, interfaces.ErrTransient}, // 500 + {http.StatusBadGateway, interfaces.ErrTransient}, // 502 + {http.StatusServiceUnavailable, interfaces.ErrTransient}, // 503 + {http.StatusGatewayTimeout, interfaces.ErrTransient}, // 504 + } + + for _, tc := range cases { + err := drivers.WrapGodoError(godoErr(tc.code)) + if err == nil { + t.Errorf("status %d: expected non-nil error", tc.code) + continue + } + if !errors.Is(err, tc.sentinel) { + t.Errorf("status %d: errors.Is(%v) = false, want sentinel %v", tc.code, err, tc.sentinel) + } + } +} + +func TestWrapGodoError_PassthroughNonGodo(t *testing.T) { + orig := errors.New("some other error") + wrapped := drivers.WrapGodoError(orig) + if wrapped != orig { + t.Errorf("non-godo error should be returned as-is, got %v", wrapped) + } +} + +func TestWrapGodoError_NilPassthrough(t *testing.T) { + if drivers.WrapGodoError(nil) != nil { + t.Error("nil should remain nil") + } +} + +func TestWrapGodoError_OriginalMessagePreserved(t *testing.T) { + orig := godoErr(http.StatusNotFound) + err := drivers.WrapGodoError(orig) + // The original godo error message must still be accessible in the error chain. + if !errors.Is(err, interfaces.ErrResourceNotFound) { + t.Errorf("sentinel not in chain: %v", err) + } + // Original error string must appear somewhere in the output. + if err.Error() == "" { + t.Error("wrapped error should not be empty") + } +} diff --git a/internal/drivers/firewall.go b/internal/drivers/firewall.go index db94c3e..b6932ee 100644 --- a/internal/drivers/firewall.go +++ b/internal/drivers/firewall.go @@ -35,7 +35,7 @@ func (d *FirewallDriver) Create(ctx context.Context, spec interfaces.ResourceSpe req := firewallRequest(spec) fw, _, err := d.client.Create(ctx, req) if err != nil { - return nil, fmt.Errorf("firewall create %q: %w", spec.Name, err) + return nil, fmt.Errorf("firewall create %q: %w", spec.Name, WrapGodoError(err)) } return fwOutput(fw), nil } @@ -43,7 +43,7 @@ func (d *FirewallDriver) Create(ctx context.Context, spec interfaces.ResourceSpe func (d *FirewallDriver) Read(ctx context.Context, ref interfaces.ResourceRef) (*interfaces.ResourceOutput, error) { fw, _, err := d.client.Get(ctx, ref.ProviderID) if err != nil { - return nil, fmt.Errorf("firewall read %q: %w", ref.Name, err) + return nil, fmt.Errorf("firewall read %q: %w", ref.Name, WrapGodoError(err)) } return fwOutput(fw), nil } @@ -52,7 +52,7 @@ func (d *FirewallDriver) Update(ctx context.Context, ref interfaces.ResourceRef, req := firewallRequest(spec) fw, _, err := d.client.Update(ctx, ref.ProviderID, req) if err != nil { - return nil, fmt.Errorf("firewall update %q: %w", ref.Name, err) + return nil, fmt.Errorf("firewall update %q: %w", ref.Name, WrapGodoError(err)) } return fwOutput(fw), nil } @@ -60,7 +60,7 @@ func (d *FirewallDriver) Update(ctx context.Context, ref interfaces.ResourceRef, func (d *FirewallDriver) Delete(ctx context.Context, ref interfaces.ResourceRef) error { _, err := d.client.Delete(ctx, ref.ProviderID) if err != nil { - return fmt.Errorf("firewall delete %q: %w", ref.Name, err) + return fmt.Errorf("firewall delete %q: %w", ref.Name, WrapGodoError(err)) } return nil } diff --git a/internal/drivers/kubernetes.go b/internal/drivers/kubernetes.go index 6891ab4..caa3cc6 100644 --- a/internal/drivers/kubernetes.go +++ b/internal/drivers/kubernetes.go @@ -54,7 +54,7 @@ func (d *KubernetesDriver) Create(ctx context.Context, spec interfaces.ResourceS cluster, _, err := d.client.Create(ctx, req) if err != nil { - return nil, fmt.Errorf("kubernetes create %q: %w", spec.Name, err) + return nil, fmt.Errorf("kubernetes create %q: %w", spec.Name, WrapGodoError(err)) } return k8sOutput(cluster), nil } @@ -62,7 +62,7 @@ func (d *KubernetesDriver) Create(ctx context.Context, spec interfaces.ResourceS func (d *KubernetesDriver) Read(ctx context.Context, ref interfaces.ResourceRef) (*interfaces.ResourceOutput, error) { cluster, _, err := d.client.Get(ctx, ref.ProviderID) if err != nil { - return nil, fmt.Errorf("kubernetes read %q: %w", ref.Name, err) + return nil, fmt.Errorf("kubernetes read %q: %w", ref.Name, WrapGodoError(err)) } return k8sOutput(cluster), nil } @@ -73,7 +73,7 @@ func (d *KubernetesDriver) Update(ctx context.Context, ref interfaces.ResourceRe } cluster, _, err := d.client.Update(ctx, ref.ProviderID, req) if err != nil { - return nil, fmt.Errorf("kubernetes update %q: %w", ref.Name, err) + return nil, fmt.Errorf("kubernetes update %q: %w", ref.Name, WrapGodoError(err)) } return k8sOutput(cluster), nil } @@ -81,7 +81,7 @@ func (d *KubernetesDriver) Update(ctx context.Context, ref interfaces.ResourceRe func (d *KubernetesDriver) Delete(ctx context.Context, ref interfaces.ResourceRef) error { _, err := d.client.Delete(ctx, ref.ProviderID) if err != nil { - return fmt.Errorf("kubernetes delete %q: %w", ref.Name, err) + return fmt.Errorf("kubernetes delete %q: %w", ref.Name, WrapGodoError(err)) } return nil } @@ -111,7 +111,7 @@ func (d *KubernetesDriver) HealthCheck(ctx context.Context, ref interfaces.Resou func (d *KubernetesDriver) Scale(ctx context.Context, ref interfaces.ResourceRef, replicas int) (*interfaces.ResourceOutput, error) { cluster, _, err := d.client.Get(ctx, ref.ProviderID) if err != nil { - return nil, fmt.Errorf("kubernetes scale read %q: %w", ref.Name, err) + return nil, fmt.Errorf("kubernetes scale read %q: %w", ref.Name, WrapGodoError(err)) } if len(cluster.NodePools) == 0 { return nil, fmt.Errorf("kubernetes scale %q: no node pools found", ref.Name) @@ -122,7 +122,7 @@ func (d *KubernetesDriver) Scale(ctx context.Context, ref interfaces.ResourceRef Count: &count, }) if err != nil { - return nil, fmt.Errorf("kubernetes scale %q pool %q: %w", ref.Name, pool.ID, err) + return nil, fmt.Errorf("kubernetes scale %q pool %q: %w", ref.Name, pool.ID, WrapGodoError(err)) } return d.Read(ctx, ref) } diff --git a/internal/drivers/load_balancer.go b/internal/drivers/load_balancer.go index 57b73c1..73d35f3 100644 --- a/internal/drivers/load_balancer.go +++ b/internal/drivers/load_balancer.go @@ -52,7 +52,7 @@ func (d *LoadBalancerDriver) Create(ctx context.Context, spec interfaces.Resourc lb, _, err := d.client.Create(ctx, req) if err != nil { - return nil, fmt.Errorf("load balancer create %q: %w", spec.Name, err) + return nil, fmt.Errorf("load balancer create %q: %w", spec.Name, WrapGodoError(err)) } return lbOutput(lb), nil } @@ -60,7 +60,7 @@ func (d *LoadBalancerDriver) Create(ctx context.Context, spec interfaces.Resourc func (d *LoadBalancerDriver) Read(ctx context.Context, ref interfaces.ResourceRef) (*interfaces.ResourceOutput, error) { lb, _, err := d.client.Get(ctx, ref.ProviderID) if err != nil { - return nil, fmt.Errorf("load balancer read %q: %w", ref.Name, err) + return nil, fmt.Errorf("load balancer read %q: %w", ref.Name, WrapGodoError(err)) } return lbOutput(lb), nil } @@ -85,7 +85,7 @@ func (d *LoadBalancerDriver) Update(ctx context.Context, ref interfaces.Resource lb, _, err := d.client.Update(ctx, ref.ProviderID, req) if err != nil { - return nil, fmt.Errorf("load balancer update %q: %w", ref.Name, err) + return nil, fmt.Errorf("load balancer update %q: %w", ref.Name, WrapGodoError(err)) } return lbOutput(lb), nil } @@ -93,7 +93,7 @@ func (d *LoadBalancerDriver) Update(ctx context.Context, ref interfaces.Resource func (d *LoadBalancerDriver) Delete(ctx context.Context, ref interfaces.ResourceRef) error { _, err := d.client.Delete(ctx, ref.ProviderID) if err != nil { - return fmt.Errorf("load balancer delete %q: %w", ref.Name, err) + return fmt.Errorf("load balancer delete %q: %w", ref.Name, WrapGodoError(err)) } return nil } diff --git a/internal/drivers/registry.go b/internal/drivers/registry.go index fef5057..233935c 100644 --- a/internal/drivers/registry.go +++ b/internal/drivers/registry.go @@ -41,7 +41,7 @@ func (d *RegistryDriver) Create(ctx context.Context, spec interfaces.ResourceSpe Region: region, }) if err != nil { - return nil, fmt.Errorf("registry create %q: %w", spec.Name, err) + return nil, fmt.Errorf("registry create %q: %w", spec.Name, WrapGodoError(err)) } return registryOutput(reg, spec.Name), nil } @@ -49,7 +49,7 @@ func (d *RegistryDriver) Create(ctx context.Context, spec interfaces.ResourceSpe func (d *RegistryDriver) Read(ctx context.Context, ref interfaces.ResourceRef) (*interfaces.ResourceOutput, error) { reg, _, err := d.client.Get(ctx) if err != nil { - return nil, fmt.Errorf("registry read %q: %w", ref.Name, err) + return nil, fmt.Errorf("registry read %q: %w", ref.Name, WrapGodoError(err)) } return registryOutput(reg, ref.Name), nil } @@ -58,7 +58,7 @@ func (d *RegistryDriver) Update(ctx context.Context, _ interfaces.ResourceRef, s // DOCR does not support in-place updates; return current state. reg, _, err := d.client.Get(ctx) if err != nil { - return nil, fmt.Errorf("registry update %q: %w", spec.Name, err) + return nil, fmt.Errorf("registry update %q: %w", spec.Name, WrapGodoError(err)) } return registryOutput(reg, spec.Name), nil } @@ -66,7 +66,7 @@ func (d *RegistryDriver) Update(ctx context.Context, _ interfaces.ResourceRef, s func (d *RegistryDriver) Delete(ctx context.Context, ref interfaces.ResourceRef) error { _, err := d.client.Delete(ctx) if err != nil { - return fmt.Errorf("registry delete %q: %w", ref.Name, err) + return fmt.Errorf("registry delete %q: %w", ref.Name, WrapGodoError(err)) } return nil } diff --git a/internal/drivers/vpc.go b/internal/drivers/vpc.go index d0a61fe..233db0c 100644 --- a/internal/drivers/vpc.go +++ b/internal/drivers/vpc.go @@ -44,7 +44,7 @@ func (d *VPCDriver) Create(ctx context.Context, spec interfaces.ResourceSpec) (* vpc, _, err := d.client.Create(ctx, req) if err != nil { - return nil, fmt.Errorf("vpc create %q: %w", spec.Name, err) + return nil, fmt.Errorf("vpc create %q: %w", spec.Name, WrapGodoError(err)) } return vpcOutput(vpc), nil } @@ -52,7 +52,7 @@ func (d *VPCDriver) Create(ctx context.Context, spec interfaces.ResourceSpec) (* func (d *VPCDriver) Read(ctx context.Context, ref interfaces.ResourceRef) (*interfaces.ResourceOutput, error) { vpc, _, err := d.client.Get(ctx, ref.ProviderID) if err != nil { - return nil, fmt.Errorf("vpc read %q: %w", ref.Name, err) + return nil, fmt.Errorf("vpc read %q: %w", ref.Name, WrapGodoError(err)) } return vpcOutput(vpc), nil } @@ -63,7 +63,7 @@ func (d *VPCDriver) Update(ctx context.Context, ref interfaces.ResourceRef, spec } vpc, _, err := d.client.Update(ctx, ref.ProviderID, req) if err != nil { - return nil, fmt.Errorf("vpc update %q: %w", ref.Name, err) + return nil, fmt.Errorf("vpc update %q: %w", ref.Name, WrapGodoError(err)) } return vpcOutput(vpc), nil } @@ -71,7 +71,7 @@ func (d *VPCDriver) Update(ctx context.Context, ref interfaces.ResourceRef, spec func (d *VPCDriver) Delete(ctx context.Context, ref interfaces.ResourceRef) error { _, err := d.client.Delete(ctx, ref.ProviderID) if err != nil { - return fmt.Errorf("vpc delete %q: %w", ref.Name, err) + return fmt.Errorf("vpc delete %q: %w", ref.Name, WrapGodoError(err)) } return nil }