From b20c4202f4d9b14bd35cbd962ab9e5194e9e2b8b Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Sat, 10 Oct 2026 04:24:55 +0200 Subject: [PATCH] refactor(aws): use common.ErrOutcomeUnknown and drop the internal sentinel Pin pkg to 30bf3837 in providers/aws, azure, gcp and ci_cd_sanity_tests, point ClassifyPurchaseError, the EC2 and Redshift clients and their tests at the exported sentinel, and delete purchasecfg.ErrOutcomeUnknown. One sentinel, no alias (#301). Co-Authored-By: Claude Sonnet 5.5 --- ci_cd_sanity_tests/go.mod | 2 +- ci_cd_sanity_tests/go.sum | 4 ++-- providers/aws/go.mod | 2 +- providers/aws/go.sum | 4 ++-- providers/aws/internal/purchasecfg/outcome.go | 14 ++++---------- providers/aws/internal/purchasecfg/outcome_test.go | 3 ++- providers/aws/services/ec2/client.go | 2 +- providers/aws/services/ec2/purchase_retry_test.go | 5 ++--- providers/aws/services/redshift/client.go | 2 +- .../aws/services/redshift/purchase_retry_test.go | 5 ++--- providers/azure/go.mod | 2 +- providers/azure/go.sum | 4 ++-- providers/gcp/go.mod | 2 +- providers/gcp/go.sum | 4 ++-- 14 files changed, 24 insertions(+), 31 deletions(-) diff --git a/ci_cd_sanity_tests/go.mod b/ci_cd_sanity_tests/go.mod index 94b400c..c157d21 100644 --- a/ci_cd_sanity_tests/go.mod +++ b/ci_cd_sanity_tests/go.mod @@ -41,7 +41,7 @@ require ( require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 - github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009141026-a450912d1d4e + github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009181525-30bf383795db github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 ) diff --git a/ci_cd_sanity_tests/go.sum b/ci_cd_sanity_tests/go.sum index 41110d7..d13721f 100644 --- a/ci_cd_sanity_tests/go.sum +++ b/ci_cd_sanity_tests/go.sum @@ -20,8 +20,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009141026-a450912d1d4e h1:guLTNuzHesGLDTCvEjz91ClEpzN9PT8CHPWDnynRKww= -github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009141026-a450912d1d4e/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009181525-30bf383795db h1:d/eTZP31KewqE049NlQM3nZIUygRudHJ25Im2ibJk58= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009181525-30bf383795db/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/config v1.29.12 h1:Y/2a+jLPrPbHpFkpAAYkVEtJmxORlXoo5k2g1fa2sUo= diff --git a/providers/aws/go.mod b/providers/aws/go.mod index 20a8f78..2abe838 100644 --- a/providers/aws/go.mod +++ b/providers/aws/go.mod @@ -3,7 +3,7 @@ module github.com/LeanerCloud/cloud-commitments-go/providers/aws go 1.26.9 require ( - github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009141026-a450912d1d4e + github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009181525-30bf383795db github.com/aws/aws-sdk-go-v2 v1.41.5 github.com/aws/aws-sdk-go-v2/config v1.29.12 github.com/aws/aws-sdk-go-v2/credentials v1.17.65 diff --git a/providers/aws/go.sum b/providers/aws/go.sum index 8aab55c..46b71fd 100644 --- a/providers/aws/go.sum +++ b/providers/aws/go.sum @@ -1,5 +1,5 @@ -github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009141026-a450912d1d4e h1:guLTNuzHesGLDTCvEjz91ClEpzN9PT8CHPWDnynRKww= -github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009141026-a450912d1d4e/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009181525-30bf383795db h1:d/eTZP31KewqE049NlQM3nZIUygRudHJ25Im2ibJk58= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009181525-30bf383795db/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/config v1.29.12 h1:Y/2a+jLPrPbHpFkpAAYkVEtJmxORlXoo5k2g1fa2sUo= diff --git a/providers/aws/internal/purchasecfg/outcome.go b/providers/aws/internal/purchasecfg/outcome.go index 92cd62c..d167f7a 100644 --- a/providers/aws/internal/purchasecfg/outcome.go +++ b/providers/aws/internal/purchasecfg/outcome.go @@ -5,19 +5,13 @@ import ( "fmt" "net/http" + "github.com/LeanerCloud/cloud-commitments-go/pkg/common" "github.com/aws/aws-sdk-go-v2/aws/retry" awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" "github.com/aws/smithy-go" ) -// ErrOutcomeUnknown marks a purchase whose request may have reached AWS but -// whose result is not known: the commitment may already exist. Services whose -// purchase API has no idempotency token or caller-chosen ID (EC2, Redshift) -// must not be re-sent automatically after such an error; reconcile against -// DescribeReservedInstances / DescribeReservedNodes first. -var ErrOutcomeUnknown = errors.New("purchase outcome unknown") - -// ClassifyPurchaseError wraps err with ErrOutcomeUnknown unless AWS definitely +// ClassifyPurchaseError wraps err with common.ErrOutcomeUnknown unless AWS definitely // rejected the request before processing it. It errs toward "unknown": a false // unknown makes a human check, a false definite allows a double-buy. // @@ -29,7 +23,7 @@ var ErrOutcomeUnknown = errors.New("purchase outcome unknown") // cancellation, 5xx, a 200 whose body could not be deserialized, and pre-send // failures (credentials, DNS, endpoint), which are treated conservatively. func ClassifyPurchaseError(err error) error { - if err == nil || errors.Is(err, ErrOutcomeUnknown) { + if err == nil || errors.Is(err, common.ErrOutcomeUnknown) { return err } var apiErr smithy.APIError @@ -41,7 +35,7 @@ func ClassifyPurchaseError(err error) error { return err } } - return fmt.Errorf("%w: the request may have been sent and the commitment may exist; reconcile before retrying: %w", ErrOutcomeUnknown, err) + return fmt.Errorf("%w: the request may have been sent and the commitment may exist; reconcile before retrying: %w", common.ErrOutcomeUnknown, err) } // httpStatus returns the HTTP status carried by err, or 0 when there is none. diff --git a/providers/aws/internal/purchasecfg/outcome_test.go b/providers/aws/internal/purchasecfg/outcome_test.go index 685af17..216d360 100644 --- a/providers/aws/internal/purchasecfg/outcome_test.go +++ b/providers/aws/internal/purchasecfg/outcome_test.go @@ -7,6 +7,7 @@ import ( "net/http" "testing" + "github.com/LeanerCloud/cloud-commitments-go/pkg/common" "github.com/aws/aws-sdk-go-v2/aws/retry" awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" "github.com/aws/smithy-go" @@ -63,7 +64,7 @@ func TestClassifyPurchaseError(t *testing.T) { assert.NoError(t, got) return } - assert.Equal(t, tc.unknown, errors.Is(got, ErrOutcomeUnknown)) + assert.Equal(t, tc.unknown, errors.Is(got, common.ErrOutcomeUnknown)) assert.ErrorIs(t, got, tc.err, "the original error must stay in the chain") }) } diff --git a/providers/aws/services/ec2/client.go b/providers/aws/services/ec2/client.go index 36f40c4..3a2ed67 100644 --- a/providers/aws/services/ec2/client.go +++ b/providers/aws/services/ec2/client.go @@ -187,7 +187,7 @@ func (c *Client) PurchaseCommitment(ctx context.Context, rec common.Recommendati } } else { // A 200 without an ID: the buy most likely happened. - result.Error = fmt.Errorf("purchase response was empty: %w", purchasecfg.ErrOutcomeUnknown) + result.Error = fmt.Errorf("purchase response was empty: %w", common.ErrOutcomeUnknown) return result, result.Error } diff --git a/providers/aws/services/ec2/purchase_retry_test.go b/providers/aws/services/ec2/purchase_retry_test.go index 049547f..6d28791 100644 --- a/providers/aws/services/ec2/purchase_retry_test.go +++ b/providers/aws/services/ec2/purchase_retry_test.go @@ -10,7 +10,6 @@ import ( "testing" "github.com/LeanerCloud/cloud-commitments-go/pkg/common" - "github.com/LeanerCloud/cloud-commitments-go/providers/aws/internal/purchasecfg" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/ec2" @@ -70,7 +69,7 @@ func TestPurchaseCommitment_LostResponseIsNotRetriedBySDK(t *testing.T) { require.Error(t, err, "a purchase whose response was lost must surface as an error, not a success") assert.False(t, result.Success) - assert.ErrorIs(t, err, purchasecfg.ErrOutcomeUnknown, "a lost response must be reported as outcome-unknown, not as a definite failure") + assert.ErrorIs(t, err, common.ErrOutcomeUnknown, "a lost response must be reported as outcome-unknown, not as a definite failure") assert.EqualValues(t, 1, purchases.Load(), "the SDK must not re-send a non-idempotent EC2 purchase (no ClientToken)") } @@ -91,5 +90,5 @@ func TestPurchaseCommitment_EmptyResponseIsOutcomeUnknown(t *testing.T) { rec := common.Recommendation{ResourceType: "t3.micro", Count: 1, PaymentOption: "partial-upfront", Term: "3yr", Details: &common.ComputeDetails{Platform: "Linux/UNIX", Tenancy: "default", Scope: "Region"}} _, err := c.PurchaseCommitment(context.Background(), rec, common.PurchaseOptions{}) - assert.ErrorIs(t, err, purchasecfg.ErrOutcomeUnknown) + assert.ErrorIs(t, err, common.ErrOutcomeUnknown) } diff --git a/providers/aws/services/redshift/client.go b/providers/aws/services/redshift/client.go index 443d265..8d58025 100644 --- a/providers/aws/services/redshift/client.go +++ b/providers/aws/services/redshift/client.go @@ -212,7 +212,7 @@ func (c *Client) PurchaseCommitment(ctx context.Context, rec common.Recommendati } } else { // A 200 without a node: the buy most likely happened. - result.Error = fmt.Errorf("purchase response was empty: %w", purchasecfg.ErrOutcomeUnknown) + result.Error = fmt.Errorf("purchase response was empty: %w", common.ErrOutcomeUnknown) return result, result.Error } diff --git a/providers/aws/services/redshift/purchase_retry_test.go b/providers/aws/services/redshift/purchase_retry_test.go index 30f8d48..5e05f2b 100644 --- a/providers/aws/services/redshift/purchase_retry_test.go +++ b/providers/aws/services/redshift/purchase_retry_test.go @@ -10,7 +10,6 @@ import ( "testing" "github.com/LeanerCloud/cloud-commitments-go/pkg/common" - "github.com/LeanerCloud/cloud-commitments-go/providers/aws/internal/purchasecfg" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/redshift" @@ -69,7 +68,7 @@ func TestPurchaseCommitment_LostResponseIsNotRetriedBySDK(t *testing.T) { require.Error(t, err, "a purchase whose response was lost must surface as an error, not a success") assert.False(t, result.Success) - assert.ErrorIs(t, err, purchasecfg.ErrOutcomeUnknown, "a lost response must be reported as outcome-unknown, not as a definite failure") + assert.ErrorIs(t, err, common.ErrOutcomeUnknown, "a lost response must be reported as outcome-unknown, not as a definite failure") assert.EqualValues(t, 1, purchases.Load(), "the SDK must not re-send a non-idempotent Redshift purchase (no ClientToken)") } @@ -90,5 +89,5 @@ func TestPurchaseCommitment_EmptyResponseIsOutcomeUnknown(t *testing.T) { rec := common.Recommendation{ResourceType: "dc2.large", Count: 1, PaymentOption: "all-upfront", Term: "1yr", Details: common.DataWarehouseDetails{NodeType: "dc2.large", NumberOfNodes: 1}} _, err := c.PurchaseCommitment(context.Background(), rec, common.PurchaseOptions{}) - assert.ErrorIs(t, err, purchasecfg.ErrOutcomeUnknown) + assert.ErrorIs(t, err, common.ErrOutcomeUnknown) } diff --git a/providers/azure/go.mod b/providers/azure/go.mod index 3a358f0..fd32782 100644 --- a/providers/azure/go.mod +++ b/providers/azure/go.mod @@ -15,7 +15,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/search/armsearch v1.4.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.2.0 - github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009141026-a450912d1d4e + github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009181525-30bf383795db github.com/stretchr/testify v1.11.1 golang.org/x/sync v0.23.0 ) diff --git a/providers/azure/go.sum b/providers/azure/go.sum index 2da4831..0a259fa 100644 --- a/providers/azure/go.sum +++ b/providers/azure/go.sum @@ -38,8 +38,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009141026-a450912d1d4e h1:guLTNuzHesGLDTCvEjz91ClEpzN9PT8CHPWDnynRKww= -github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009141026-a450912d1d4e/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009181525-30bf383795db h1:d/eTZP31KewqE049NlQM3nZIUygRudHJ25Im2ibJk58= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009181525-30bf383795db/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= diff --git a/providers/gcp/go.mod b/providers/gcp/go.mod index 1b4706b..729bbf6 100644 --- a/providers/gcp/go.mod +++ b/providers/gcp/go.mod @@ -8,7 +8,7 @@ require ( cloud.google.com/go/redis v1.18.3 cloud.google.com/go/resourcemanager v1.10.7 cloud.google.com/go/storage v1.59.2 - github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009141026-a450912d1d4e + github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009181525-30bf383795db github.com/googleapis/gax-go/v2 v2.21.0 github.com/stretchr/testify v1.11.1 golang.org/x/oauth2 v0.36.0 diff --git a/providers/gcp/go.sum b/providers/gcp/go.sum index 7ad1ac6..d7927ce 100644 --- a/providers/gcp/go.sum +++ b/providers/gcp/go.sum @@ -36,8 +36,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= -github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009141026-a450912d1d4e h1:guLTNuzHesGLDTCvEjz91ClEpzN9PT8CHPWDnynRKww= -github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009141026-a450912d1d4e/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009181525-30bf383795db h1:d/eTZP31KewqE049NlQM3nZIUygRudHJ25Im2ibJk58= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261009181525-30bf383795db/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=