diff --git a/CHANGELOG.md b/CHANGELOG.md index 430f2b3ca..012dad5a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). - `archera-comparison` command: a read-only, default-off comparison of an Archera commitment plan (table or JSON). See `docs/cli/archera-comparison.md` (#2156). +- `--ri-account-scope` flag (`linked` default, `payer`) for Reserved Instance + recommendations, mirroring `--sp-account-scope` (#2181). ### Fixed diff --git a/cmd/main.go b/cmd/main.go index c78d8debe..e9ccbd136 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -49,6 +49,7 @@ type Config struct { PaymentOption string CSVOutput string SPAccountScope string + RIAccountScope string // RecLookbackPeriod controls the LookbackPeriodInDays passed to // GetReservationPurchaseRecommendation. Valid values: "7d", "30d", "60d" // (recommendations.DefaultRecLookbackPeriod is the shared default). @@ -143,6 +144,7 @@ func init() { rootCmd.Flags().StringSliceVar(&toolCfg.IncludeSPTypes, "include-sp-types", []string{}, "Only include these Savings Plan types (comma-separated: Compute, EC2Instance, SageMaker, Database)") rootCmd.Flags().StringSliceVar(&toolCfg.ExcludeSPTypes, "exclude-sp-types", []string{}, "Exclude these Savings Plan types (comma-separated: Compute, EC2Instance, SageMaker, Database)") + rootCmd.Flags().StringVar(&toolCfg.RIAccountScope, "ri-account-scope", "linked", "Cost Explorer account scope for Reserved Instance recommendations: linked or payer (payer applies to a management account)") rootCmd.Flags().StringVar(&toolCfg.SPAccountScope, "sp-account-scope", "linked", "Cost Explorer account scope for Savings Plan recommendations: linked or payer (payer applies to a management account)") // Purchase pipeline flags diff --git a/cmd/multi_service_helpers.go b/cmd/multi_service_helpers.go index 4a107384f..84e0f2a6b 100644 --- a/cmd/multi_service_helpers.go +++ b/cmd/multi_service_helpers.go @@ -484,6 +484,7 @@ func fetchRecommendationsForRegion( IncludeSPTypes: cfg.IncludeSPTypes, ExcludeSPTypes: cfg.ExcludeSPTypes, SPAccountScope: cfg.SPAccountScope, + RIAccountScope: cfg.RIAccountScope, } // For Savings Plans, region is only the endpoint the account-level call is // signed in (spAPIRegion), not a filter. The provider folds params.Region diff --git a/cmd/ri_account_scope_test.go b/cmd/ri_account_scope_test.go new file mode 100644 index 000000000..9b07c3cca --- /dev/null +++ b/cmd/ri_account_scope_test.go @@ -0,0 +1,48 @@ +package main + +import ( + "context" + "testing" + + "github.com/LeanerCloud/cloud-commitments-go/pkg/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestValidateRIAccountScope(t *testing.T) { + orig := toolCfg.RIAccountScope + t.Cleanup(func() { toolCfg.RIAccountScope = orig }) + for scope, wantErr := range map[string]bool{"": false, "linked": false, "payer": false, "PAYER": false, "org": true} { + toolCfg.RIAccountScope = scope + err := validateRIAccountScope() + if wantErr { + require.Error(t, err, scope) + assert.Contains(t, err.Error(), "invalid ri-account-scope") + } else { + require.NoError(t, err, scope) + } + } +} + +func TestRIAccountScopeFlagDefault(t *testing.T) { + f := rootCmd.Flags().Lookup("ri-account-scope") + require.NotNil(t, f) + assert.Equal(t, "linked", f.DefValue) +} + +func TestFetchRecommendationsForRegion_PassesRIAccountScope(t *testing.T) { + captureAppLog(t) + client := &MockRecommendationsClient{} + var got *common.RecommendationParams + client.On("GetRecommendations", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { got = args.Get(1).(*common.RecommendationParams) }). + Return([]common.Recommendation{}, nil) + + _, err := fetchRecommendationsForRegion(context.Background(), client, common.ServiceRDS, "us-east-1", + Config{TermYears: 1, RIAccountScope: "payer"}) + require.NoError(t, err) + + require.NotNil(t, got) + assert.Equal(t, "payer", got.RIAccountScope) +} diff --git a/cmd/validators.go b/cmd/validators.go index 66ce09e23..abc337d38 100644 --- a/cmd/validators.go +++ b/cmd/validators.go @@ -43,7 +43,10 @@ func validateFlags(cmd *cobra.Command, args []string) error { return err } - return validateSPAccountScope() + if err := validateSPAccountScope(); err != nil { + return err + } + return validateRIAccountScope() } // validateSPAccountScope validates --sp-account-scope. Empty means linked. @@ -55,6 +58,15 @@ func validateSPAccountScope() error { return fmt.Errorf("invalid sp-account-scope %q: must be linked or payer", toolCfg.SPAccountScope) } +// validateRIAccountScope validates --ri-account-scope. Empty means linked. +func validateRIAccountScope() error { + switch strings.ToLower(toolCfg.RIAccountScope) { + case "", "linked", "payer": + return nil + } + return fmt.Errorf("invalid ri-account-scope %q: must be linked or payer", toolCfg.RIAccountScope) +} + func validateServices(cmd *cobra.Command) error { if cmd != nil && cmd.Flags().Changed("services") && len(toolCfg.Services) == 0 { return fmt.Errorf("--services must contain at least one service") diff --git a/docs/cli/README.md b/docs/cli/README.md index 93583b87a..2dafc6cff 100644 --- a/docs/cli/README.md +++ b/docs/cli/README.md @@ -75,6 +75,7 @@ All flags belong to the root command unless noted otherwise. | `--exclude-accounts` | (none) | Exclude recommendations for these accounts, matched exactly (case-insensitive) on account name or ID. A value that only partly matches account names stops the run. | | `--include-sp-types` | (all) | Only include these Savings Plan types: `Compute`, `EC2Instance`, `SageMaker`, `Database`. | | `--exclude-sp-types` | (none) | Exclude these Savings Plan types. | +| `--ri-account-scope` | `linked` | Cost Explorer account scope for Reserved Instance recommendations: `linked` or `payer`. A management account may only get recommendations at `payer` scope. | | `--sp-account-scope` | `linked` | Cost Explorer account scope for Savings Plan recommendations: `linked` or `payer`. A management account may only get recommendations at `payer` scope. | | `--include-extended-support` | `false` | By default, instances running engine versions in AWS Extended Support are excluded because Extended Support surcharges can erase RI savings. Pass this flag to include them. |