diff --git a/providers/aws/go.mod b/providers/aws/go.mod index faff0db..5ff1a58 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-20261012192523-3838efd56668 + github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261013182117-cdcdc7fbcc32 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 d8cae52..0598ad2 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-20261012192523-3838efd56668 h1:/m3hxkSHGd8AJlhBaPTttRDwtwC1ycYH7IbRkRKR9bM= -github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261012192523-3838efd56668/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261013182117-cdcdc7fbcc32 h1:IxqJVVAA26JAS9BtKvNFjn2zPN/LuzPFP5y4O417JS8= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261013182117-cdcdc7fbcc32/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/recommendations/collection_sp.go b/providers/aws/recommendations/collection_sp.go index faa904a..96ed4b2 100644 --- a/providers/aws/recommendations/collection_sp.go +++ b/providers/aws/recommendations/collection_sp.go @@ -3,6 +3,7 @@ package recommendations import ( "context" "fmt" + "strings" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/costexplorer" @@ -32,11 +33,15 @@ func (c *Client) getSavingsPlansRecommendations(ctx context.Context, params *com if err != nil { return nil, fmt.Errorf("invalid lookback period for Savings Plans recommendation: %w", err) } + scope, err := convertSavingsPlansAccountScope(params.SPAccountScope) + if err != nil { + return nil, err + } results := make([]serviceResult, 0, len(planTypes)) for _, planType := range planTypes { input := &costexplorer.GetSavingsPlansPurchaseRecommendationInput{ SavingsPlansType: planType, PaymentOption: payment, TermInYears: term, - LookbackPeriodInDays: lookback, AccountScope: types.AccountScopeLinked, + LookbackPeriodInDays: lookback, AccountScope: scope, } recs, fetchErr := c.fetchSPAllPages(ctx, input, params, planType) if canceled := collectionCancellation(fetchErr); canceled != nil { @@ -53,6 +58,19 @@ func (c *Client) getSavingsPlansRecommendations(ctx context.Context, params *com return mergeServiceResults(results...) } +// convertSavingsPlansAccountScope maps RecommendationParams.SPAccountScope to +// the Cost Explorer scope. Empty is linked, the long-standing default; an +// unknown value is an error rather than a silent fallback. +func convertSavingsPlansAccountScope(s string) (types.AccountScope, error) { + switch strings.ToLower(s) { + case "", "linked": + return types.AccountScopeLinked, nil + case "payer": + return types.AccountScopePayer, nil + } + return "", fmt.Errorf("invalid Savings Plans account scope %q: want linked or payer", s) +} + func (c *Client) fetchSPAllPages( ctx context.Context, input *costexplorer.GetSavingsPlansPurchaseRecommendationInput, diff --git a/providers/aws/recommendations/parser_sp_account_scope_test.go b/providers/aws/recommendations/parser_sp_account_scope_test.go new file mode 100644 index 0000000..fe07294 --- /dev/null +++ b/providers/aws/recommendations/parser_sp_account_scope_test.go @@ -0,0 +1,38 @@ +package recommendations + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/LeanerCloud/cloud-commitments-go/pkg/common" +) + +func TestSPAccountScope(t *testing.T) { + t.Parallel() + for _, tc := range []struct{ in, want string }{ + {"", "LINKED"}, {"linked", "LINKED"}, {"payer", "PAYER"}, {"PAYER", "PAYER"}, + } { + t.Run(tc.in, func(t *testing.T) { + t.Parallel() + fixture := &spSDKFixture{respond: func(spSDKRequest) (map[string]any, error) { return spSDKPage(""), nil }} + params := spSDKParams(common.ServiceSavingsPlansCompute) + params.SPAccountScope = tc.in + _, err := spSDKClient(fixture).GetRecommendations(context.Background(), params) + require.NoError(t, err) + require.Len(t, fixture.requests, 1) + require.Equal(t, tc.want, fixture.requests[0].AccountScope) + }) + } +} + +func TestSPAccountScopeRejectsUnknown(t *testing.T) { + t.Parallel() + fixture := &spSDKFixture{respond: func(spSDKRequest) (map[string]any, error) { return spSDKPage(""), nil }} + params := spSDKParams(common.ServiceSavingsPlansCompute) + params.SPAccountScope = "org" + _, err := spSDKClient(fixture).GetRecommendations(context.Background(), params) + require.ErrorContains(t, err, "invalid Savings Plans account scope") + require.Empty(t, fixture.requests) +}