Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
### Added

- **`common.ErrRefusedBeforePurchase`:** a sentinel for a purchase refused before
the call sent any purchase request, detectable with `errors.Is`.
the call sent any purchase request, detectable with `errors.Is`. The AWS EC2, RDS,
OpenSearch, Redshift and MemoryDB approved-upfront guard refusals wrap it.
- **Azure `NewPartialSubscriptionFailure` constructor:** builds a
`PartialSubscriptionFailureError`, optionally with skip reasons readable
through `Skips()`, so code outside the module can construct one, for example
Expand Down
4 changes: 2 additions & 2 deletions providers/aws/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/LeanerCloud/cloud-commitments-go/providers/aws
go 1.26.10

require (
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261020191912-205cf99b8ef3
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261104063122-3efa339480ac
github.com/aws/aws-sdk-go-v2 v1.47.4
github.com/aws/aws-sdk-go-v2/config v1.33.10
github.com/aws/aws-sdk-go-v2/credentials v1.20.10
Expand Down Expand Up @@ -34,5 +34,5 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.5 // indirect
github.com/stretchr/objx v0.5.3 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/sys v0.49.0 // indirect
)
8 changes: 4 additions & 4 deletions providers/aws/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261020191912-205cf99b8ef3 h1:NBjYpYKzw1mRs77nRaCya+HcuJdV6SaPCJEbJVbza0U=
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261020191912-205cf99b8ef3/go.mod h1:awNjqN4KJHvFvb2wzKWrwXK954pkHtevPl0TtYkS2io=
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261104063122-3efa339480ac h1:EvEUCt1uH5oBtPcJJPt1mX6XYQWAKtL9QBSo9CfeaOw=
github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261104063122-3efa339480ac/go.mod h1:pseCFRgm1rzBKomktjw6E0ayxmVE7XfHHiOmew4ka+g=
github.com/aws/aws-sdk-go-v2 v1.47.4 h1:bTvk6UdSSSvaQsfLVj1JRDEEqj/PDRwGf1KanaRzau0=
github.com/aws/aws-sdk-go-v2 v1.47.4/go.mod h1:XkrjAZvr705Nytv6PkPUhoSB2W9+vVHBp5oAWpkh8Ak=
github.com/aws/aws-sdk-go-v2/config v1.33.10 h1:RRb0GghYqq2ikMXzfT4r9NO9lVhOld3FIhJJWDIdk+w=
Expand Down Expand Up @@ -54,5 +54,5 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
2 changes: 1 addition & 1 deletion providers/aws/services/ec2/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ func (c *Client) PurchaseCommitment(ctx context.Context, rec common.Recommendati
}

if err = checkApprovedUpfront(rec, offering); err != nil {
result.Error = err
result.Error = fmt.Errorf("%w: %w", common.ErrRefusedBeforePurchase, err)
Comment thread
cristim marked this conversation as resolved.
Resolved
return result, result.Error
}

Expand Down
1 change: 1 addition & 0 deletions providers/aws/services/ec2/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1724,6 +1724,7 @@ func TestPurchaseCommitment_ExistingCommitmentFlag(t *testing.T) {

result, err := client.PurchaseCommitment(context.Background(), rec, common.PurchaseOptions{IdempotencyToken: token})
require.Error(t, err)
assert.NotErrorIs(t, err, common.ErrRefusedBeforePurchase)
assert.False(t, result.Success)
assert.False(t, result.ExistingCommitment)
})
Expand Down
2 changes: 2 additions & 0 deletions providers/aws/services/ec2/purchase_price_guard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ func TestPurchaseCommitment_RefusesOfferingAboveApprovedUpfront(t *testing.T) {
m := offeringsMock(pricedOffering(600))
res, err := (&Client{client: m, region: "us-east-1"}).PurchaseCommitment(context.Background(), pricedRec(), common.PurchaseOptions{})
require.ErrorContains(t, err, "exceeds the approved upfront")
assert.ErrorIs(t, err, common.ErrRefusedBeforePurchase)
assert.ErrorIs(t, res.Error, common.ErrRefusedBeforePurchase)
assert.False(t, res.Success)
m.AssertNotCalled(t, "PurchaseReservedInstancesOffering", mock.Anything, mock.Anything)
}
Expand Down
2 changes: 1 addition & 1 deletion providers/aws/services/memorydb/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func (c *Client) PurchaseCommitment(ctx context.Context, rec common.Recommendati
// After the dedupe: a re-drive that already bought must not be refused for a
// later price rise (ec2/client.go does the same).
if err = c.checkApprovedUpfront(ctx, rec, offeringID); err != nil {
result.Error = err
result.Error = fmt.Errorf("%w: %w", common.ErrRefusedBeforePurchase, err)
return result, result.Error
}

Expand Down
2 changes: 2 additions & 0 deletions providers/aws/services/memorydb/purchase_guard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ func TestPurchaseGuard_OtherPurchaseErrorNotRecovered(t *testing.T) {
result, err := guardClient(m).PurchaseCommitment(context.Background(), mdbIdemRec(), common.PurchaseOptions{IdempotencyToken: common.DeriveIdempotencyToken("exec-other", 0)})

require.Error(t, err)
assert.NotErrorIs(t, err, common.ErrRefusedBeforePurchase)
assert.NotErrorIs(t, result.Error, common.ErrRefusedBeforePurchase)
assert.False(t, result.Success)
m.AssertNumberOfCalls(t, "DescribeReservedNodes", 1)
}
2 changes: 2 additions & 0 deletions providers/aws/services/memorydb/purchase_price_guard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ func TestPurchaseCommitment_RefusesOfferingAboveApprovedUpfront(t *testing.T) {
m := guardMock(525.01) // 2 x 525.01 = 1050.02 > ceiling 1050.00
res, err := buy(m, guardRec())
require.ErrorContains(t, err, "exceeds the approved upfront")
assert.ErrorIs(t, err, common.ErrRefusedBeforePurchase)
assert.ErrorIs(t, res.Error, common.ErrRefusedBeforePurchase)
assert.ErrorContains(t, err, "1050.02")
assert.ErrorContains(t, err, "1000.00")
assert.False(t, res.Success)
Expand Down
2 changes: 1 addition & 1 deletion providers/aws/services/opensearch/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ func (c *Client) PurchaseCommitment(ctx context.Context, rec common.Recommendati
// After the dedupe: a re-drive that already bought must not be refused for a
// later price rise (ec2/client.go does the same).
if err = c.checkApprovedUpfront(ctx, rec, offeringID, instanceCount); err != nil {
result.Error = err
result.Error = fmt.Errorf("%w: %w", common.ErrRefusedBeforePurchase, err)
return result, result.Error
}

Expand Down
2 changes: 2 additions & 0 deletions providers/aws/services/opensearch/purchase_guard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ func TestPurchaseGuard_OtherPurchaseErrorNotRecovered(t *testing.T) {
result, err := guardClient(m).PurchaseCommitment(context.Background(), osIdemRec(), common.PurchaseOptions{IdempotencyToken: common.DeriveIdempotencyToken("exec-other", 0)})

require.Error(t, err)
assert.NotErrorIs(t, err, common.ErrRefusedBeforePurchase)
assert.NotErrorIs(t, result.Error, common.ErrRefusedBeforePurchase)
assert.False(t, result.Success)
m.AssertNumberOfCalls(t, "DescribeReservedInstances", 1)
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ func TestPurchaseCommitment_RefusesOfferingAboveApprovedUpfront(t *testing.T) {
m := guardMock(525.01, "USD") // 2 x 525.01 = 1050.02 > ceiling 1050.00
res, err := buy(m, guardRec())
require.ErrorContains(t, err, "exceeds the approved upfront")
assert.ErrorIs(t, err, common.ErrRefusedBeforePurchase)
assert.ErrorIs(t, res.Error, common.ErrRefusedBeforePurchase)
assert.ErrorContains(t, err, "1050.02")
assert.ErrorContains(t, err, "1000.00")
assert.False(t, res.Success)
Expand Down
2 changes: 1 addition & 1 deletion providers/aws/services/rds/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func (c *Client) PurchaseCommitment(ctx context.Context, rec common.Recommendati
// After the dedupe: a re-drive that already bought must not be refused for a
// later price rise (ec2/client.go does the same).
if err = c.checkApprovedUpfront(ctx, rec, offeringID); err != nil {
result.Error = err
result.Error = fmt.Errorf("%w: %w", common.ErrRefusedBeforePurchase, err)
return result, result.Error
}

Expand Down
2 changes: 2 additions & 0 deletions providers/aws/services/rds/purchase_guard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ func TestPurchaseGuard_OtherPurchaseErrorNotRecovered(t *testing.T) {
result, err := guardClient(m).PurchaseCommitment(context.Background(), idempotencyTestRec(), common.PurchaseOptions{IdempotencyToken: common.DeriveIdempotencyToken("exec-other", 0)})

require.Error(t, err)
assert.NotErrorIs(t, err, common.ErrRefusedBeforePurchase)
assert.NotErrorIs(t, result.Error, common.ErrRefusedBeforePurchase)
assert.False(t, result.Success)
m.AssertNumberOfCalls(t, "DescribeReservedDBInstances", 1)
}
23 changes: 23 additions & 0 deletions providers/aws/services/rds/purchase_price_guard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ func TestPurchaseCommitment_RefusesOfferingAboveApprovedUpfront(t *testing.T) {
m := guardMock(525.01, "USD") // 2 x 525.01 = 1050.02 > ceiling 1050.00
res, err := buy(m, guardRec())
require.ErrorContains(t, err, "exceeds the approved upfront")
assert.ErrorIs(t, err, common.ErrRefusedBeforePurchase)
assert.ErrorIs(t, res.Error, common.ErrRefusedBeforePurchase)
assert.ErrorContains(t, err, "1050.02")
assert.ErrorContains(t, err, "1000.00")
assert.False(t, res.Success)
Expand Down Expand Up @@ -157,10 +159,31 @@ func TestPurchaseCommitment_RefusesWhenPriceReadFails(t *testing.T) {
res, err := buy(m, guardRec())
require.ErrorContains(t, err, "approved upfront check")
assert.ErrorContains(t, err, "throttled")
assert.ErrorIs(t, err, common.ErrRefusedBeforePurchase)
assert.ErrorIs(t, res.Error, common.ErrRefusedBeforePurchase)
assert.False(t, res.Success)
m.AssertNotCalled(t, "PurchaseReservedDBInstancesOffering", mock.Anything, mock.Anything)
}

// The guard passes (2 x 500 == approved 1000), so a purchase API failure is not
// a refusal before purchase.
func TestPurchaseCommitment_PurchaseFailureAfterGuardIsNotRefused(t *testing.T) {
t.Parallel()
m := &MockRDSClient{}
priced := exactOffering("offering-1")
priced.FixedPrice = aws.Float64(500)
priced.CurrencyCode = aws.String("USD")
m.On("DescribeReservedDBInstancesOfferings", mock.Anything, mock.Anything).
Return(&rds.DescribeReservedDBInstancesOfferingsOutput{ReservedDBInstancesOfferings: []types.ReservedDBInstancesOffering{priced}}, nil)
m.On("PurchaseReservedDBInstancesOffering", mock.Anything, mock.Anything).
Return((*rds.PurchaseReservedDBInstancesOfferingOutput)(nil), errors.New("boom"))
res, err := buy(m, guardRec())
require.Error(t, err)
assert.NotErrorIs(t, err, common.ErrRefusedBeforePurchase)
assert.NotErrorIs(t, res.Error, common.ErrRefusedBeforePurchase)
m.AssertCalled(t, "PurchaseReservedDBInstancesOffering", mock.Anything, mock.Anything)
}

func TestPurchaseCommitment_RefusesWhenPriceReadIsNotOneOffering(t *testing.T) {
t.Parallel()
two := []types.ReservedDBInstancesOffering{exactOffering("offering-1"), exactOffering("offering-1")}
Expand Down
2 changes: 1 addition & 1 deletion providers/aws/services/redshift/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ func (c *Client) PurchaseCommitment(ctx context.Context, rec common.Recommendati
// After the dedupe: a re-drive that already bought must not be refused for a
// later price rise (ec2/client.go does the same).
if err = c.checkApprovedUpfront(ctx, rec, offeringID); err != nil {
result.Error = err
result.Error = fmt.Errorf("%w: %w", common.ErrRefusedBeforePurchase, err)
return result, result.Error
}

Expand Down
1 change: 1 addition & 0 deletions providers/aws/services/redshift/dedupe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ func TestDedupe_RecoverAfterUnknownOutcome(t *testing.T) {
return
}
assert.ErrorIs(t, err, common.ErrOutcomeUnknown)
assert.NotErrorIs(t, err, common.ErrRefusedBeforePurchase)
assert.False(t, result.Success)
})
}
Expand Down
2 changes: 2 additions & 0 deletions providers/aws/services/redshift/purchase_price_guard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ func TestPurchaseCommitment_RefusesOfferingAboveApprovedUpfront(t *testing.T) {
m := guardMock(525.01, "USD") // 2 x 525.01 = 1050.02 > ceiling 1050.00
res, err := buy(m, guardRec())
require.ErrorContains(t, err, "exceeds the approved upfront")
assert.ErrorIs(t, err, common.ErrRefusedBeforePurchase)
assert.ErrorIs(t, res.Error, common.ErrRefusedBeforePurchase)
assert.ErrorContains(t, err, "1050.02")
assert.ErrorContains(t, err, "1000.00")
assert.False(t, res.Success)
Expand Down
Loading