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
72 changes: 66 additions & 6 deletions cmd/multi_service_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,74 @@ func applyCommonCoverage(recs []common.Recommendation, coverage float64) []commo

// determineServicesToProcess returns the list of services to process based on flags.
func determineServicesToProcess(cfg Config) ([]common.ServiceType, error) {
if cfg.AllServices {
return getAllServices(), nil
var services []common.ServiceType
switch {
case cfg.AllServices:
services = getAllServices()
case len(cfg.Services) > 0:
parsed, err := parseServices(cfg.Services)
if err != nil {
return nil, err
}
services = parsed
default:
// Default to RDS only for backward compatibility
return []common.ServiceType{common.ServiceRDS}, nil
}
return filterSavingsPlanTypes(services, cfg.IncludeSPTypes, cfg.ExcludeSPTypes)
}

// spTypeServices maps the --include-sp-types / --exclude-sp-types names
// (matched case-insensitively) to the per-plan-type service slugs.
var spTypeServices = map[string]common.ServiceType{
"compute": common.ServiceSavingsPlansCompute,
"ec2instance": common.ServiceSavingsPlansEC2Instance,
"sagemaker": common.ServiceSavingsPlansSageMaker,
"database": common.ServiceSavingsPlansDatabase,
}

// spTypeSet resolves --include-sp-types / --exclude-sp-types names to services.
func spTypeSet(flag string, names []string) (map[common.ServiceType]bool, error) {
set := make(map[common.ServiceType]bool, len(names))
for _, name := range names {
service, ok := spTypeServices[strings.ToLower(strings.TrimSpace(name))]
if !ok {
return nil, fmt.Errorf("unknown Savings Plan type %q in %s; valid types: Compute, EC2Instance, SageMaker, Database", name, flag)
}
set[service] = true
}
if len(cfg.Services) > 0 {
return parseServices(cfg.Services)
return set, nil
}

// filterSavingsPlanTypes drops the Savings Plan services that
// --include-sp-types / --exclude-sp-types rule out, so only the requested
// plan types are fetched. Each per-plan-type service queries exactly one plan
// type and the provider ignores the include/exclude lists for it, so the
// narrowing has to happen on the service list. Non-SP services pass through.
func filterSavingsPlanTypes(services []common.ServiceType, include, exclude []string) ([]common.ServiceType, error) {
includeSet, err := spTypeSet("--include-sp-types", include)
if err != nil {
return nil, err
}
excludeSet, err := spTypeSet("--exclude-sp-types", exclude)
if err != nil {
return nil, err
}
kept := make([]common.ServiceType, 0, len(services))
for _, service := range services {
if common.IsSavingsPlan(service) && !spTypeWanted(service, includeSet, excludeSet) {
continue
}
kept = append(kept, service)
}
return kept, nil
}

func spTypeWanted(service common.ServiceType, includeSet, excludeSet map[common.ServiceType]bool) bool {
if len(includeSet) > 0 && !includeSet[service] {
return false
}
// Default to RDS only for backward compatibility
return []common.ServiceType{common.ServiceRDS}, nil
return !excludeSet[service]
}

// printRunMode prints the current run mode (dry run or purchase).
Expand Down
37 changes: 37 additions & 0 deletions cmd/multi_service_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
orgtypes "github.com/aws/aws-sdk-go-v2/service/organizations/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

func TestGetAllAWSRegions(t *testing.T) {
Expand Down Expand Up @@ -857,3 +858,39 @@ func TestDetermineServicesToProcess_DefaultAndInvalid(t *testing.T) {
assert.ErrorContains(t, err, "elasticahe")
assert.Nil(t, result)
}

func TestDetermineServicesToProcess_SavingsPlanTypeFilters(t *testing.T) {
allSP := []common.ServiceType{
common.ServiceSavingsPlansCompute, common.ServiceSavingsPlansEC2Instance,
common.ServiceSavingsPlansSageMaker, common.ServiceSavingsPlansDatabase,
}
tests := []struct {
name string
cfg Config
want []common.ServiceType
wantErr string
}{
{"no filter keeps all four", Config{Services: []string{"savingsplans"}}, allSP, ""},
{"include narrows, case-insensitive", Config{Services: []string{"savingsplans"}, IncludeSPTypes: []string{"EC2Instance"}},
[]common.ServiceType{common.ServiceSavingsPlansEC2Instance}, ""},
{"exclude drops", Config{Services: []string{"savingsplans"}, ExcludeSPTypes: []string{"sagemaker", "Database"}},
[]common.ServiceType{common.ServiceSavingsPlansCompute, common.ServiceSavingsPlansEC2Instance}, ""},
{"include then exclude", Config{Services: []string{"savingsplans"}, IncludeSPTypes: []string{"compute", "ec2instance"}, ExcludeSPTypes: []string{"compute"}},
[]common.ServiceType{common.ServiceSavingsPlansEC2Instance}, ""},
{"non-SP services untouched", Config{Services: []string{"rds", "savingsplans"}, IncludeSPTypes: []string{"compute"}},
[]common.ServiceType{common.ServiceRDS, common.ServiceSavingsPlansCompute}, ""},
{"unknown include fails loud", Config{Services: []string{"savingsplans"}, IncludeSPTypes: []string{"ec2"}}, nil, `unknown Savings Plan type "ec2"`},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := determineServicesToProcess(tc.cfg)
if tc.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tc.want, got)
})
}
}
Loading