diff --git a/v1/providers/sfcomputev2/instance.go b/v1/providers/sfcomputev2/instance.go index f3c1076..5e34f0d 100644 --- a/v1/providers/sfcomputev2/instance.go +++ b/v1/providers/sfcomputev2/instance.go @@ -12,6 +12,7 @@ import ( "github.com/alecthomas/units" "github.com/brevdev/cloud/internal/errors" v1 "github.com/brevdev/cloud/v1" + "github.com/sfcompute/sfc-go/models/apierrors" "github.com/sfcompute/sfc-go/models/components" "github.com/sfcompute/sfc-go/models/operations" "github.com/sfcompute/sfc-go/optionalnullable" @@ -183,7 +184,7 @@ func (c *SFCClientV2) TerminateInstance(ctx context.Context, id v1.CloudProvider _, err := c.client.Instances.TerminateInstance(ctx, string(id)) if err != nil { - return errors.WrapAndTrace(err) + return normalizeTerminateInstanceError(err) } c.logger.Debug(ctx, "sfcv2: TerminateInstance end", @@ -193,6 +194,16 @@ func (c *SFCClientV2) TerminateInstance(ctx context.Context, id v1.CloudProvider return nil } +func normalizeTerminateInstanceError(err error) error { + var notFoundErr *apierrors.NotFoundError + if errors.As(err, ¬FoundErr) { + // Termination is idempotent: a missing instance is already in the + // requested terminal state. Do not retain the provider response body. + return errors.WrapAndTrace(v1.ErrInstanceNotFound) + } + return errors.WrapAndTrace(err) +} + func (c *SFCClientV2) getSSHInfo(ctx context.Context, id string, status components.InstanceStatus) (*components.InstanceSSHInfo, error) { if status != components.InstanceStatusRunning { return nil, nil diff --git a/v1/providers/sfcomputev2/instance_test.go b/v1/providers/sfcomputev2/instance_test.go new file mode 100644 index 0000000..5293388 --- /dev/null +++ b/v1/providers/sfcomputev2/instance_test.go @@ -0,0 +1,33 @@ +package v2 + +import ( + "errors" + "fmt" + "testing" + + v1 "github.com/brevdev/cloud/v1" + "github.com/sfcompute/sfc-go/models/apierrors" + "github.com/stretchr/testify/require" +) + +func TestNormalizeTerminateInstanceErrorTreatsNotFoundAsAlreadyGone(t *testing.T) { + t.Parallel() + + providerErr := fmt.Errorf("release instance: %w", &apierrors.NotFoundError{}) + + err := normalizeTerminateInstanceError(providerErr) + + require.ErrorIs(t, err, v1.ErrInstanceNotFound) + var notFoundErr *apierrors.NotFoundError + require.False(t, errors.As(err, ¬FoundErr)) +} + +func TestNormalizeTerminateInstanceErrorPreservesRetryableFailure(t *testing.T) { + t.Parallel() + + providerErr := errors.New("provider rate limited") + + err := normalizeTerminateInstanceError(providerErr) + + require.ErrorIs(t, err, providerErr) +}