Skip to content

fix(aws-savingsplans): apply offering filters, validate commitment/tags, paginate describes - #1538

Open
Satyam-Trivedi-ZS wants to merge 1 commit into
stackshy:developmentfrom
Satyam-Trivedi-ZS:fix/aws-savingsplans-parity-1515
Open

Satyam-Trivedi-ZS wants to merge 1 commit into
stackshy:developmentfrom
Satyam-Trivedi-ZS:fix/aws-savingsplans-parity-1515

Conversation

@Satyam-Trivedi-ZS

Copy link
Copy Markdown
Collaborator

Summary

Part of #1515. This closes five of the seven open AWS Savings Plans parity gaps: DescribeSavingsPlansOfferings productType and filters, commitment range validation, pagination for DescribeSavingsPlans and DescribeSavingsPlansOfferings, and empty tag keys. One item is left as unverifiable and one is skipped. Reasons for both are below.

Item Status
(Medium) DescribeSavingsPlansOfferings productType decoded but never applied Fixed
(Medium) DescribeSavingsPlansOfferings filters (instanceFamily / region) decoded but never applied Fixed. The other documented request lists (paymentOptions, durations, currencies, descriptions, serviceCodes, usageTypes, operations) were not decoded at all, so they are now applied too.
(Medium) CreateSavingsPlan accepts zero/negative commitment Fixed
(Low) DescribeSavingsPlans/Offerings maxResults/nextToken ignored Fixed
(Low) CreateSavingsPlan clientToken reused with different params returns the original Unverifiable, not changed. The API reference only says the token is "a unique, case-sensitive identifier that you provide to ensure the idempotency of the request". It does not document what happens when a token is reused with different parameters, and the error list (ResourceNotFound / Validation / InternalServer / ServiceQuotaExceeded) has no IdempotentParameterMismatch. Any error code we picked would be a guess.
(Low) TagResource accepts empty tag key Fixed (TagResource and CreateSavingsPlan tags)
(Low) Cost Explorer GetSavingsPlansCoverage/Utilization not implemented Skipped. cloudemu has a Cost Explorer handler (server/aws/costexplorer), but it only serves GetCostAndUsage, GetCostForecast, GetDimensionValues and GetTags. Coverage and utilization need covered-vs-on-demand usage and amortization across services. That is a separate cross-service feature (the "billing-parity step 3" mentioned in handler.go), and adding it would mean inventing numbers. The store already exposes cost.Commitments for when that handler lands.

Changes

All changes are in server/aws/savingsplans/. Savings Plans has no portable driver or provider package; its state lives in the handler-owned store (see the package doc). So the business rules went into the store, validation and filter files, and the handler only decodes and encodes.

  • offerings.go (new): offeringFilter covers offeringIds, planTypes, paymentOptions, durations, currencies, descriptions, serviceCodes, usageTypes, operations, productType and filters[]. Dimensions are ANDed together; values inside one list are ORed. The filters[] names (region, instanceFamily) are the same enum as SavingsPlanOfferingPropertyKey, so an offering matches a filter when it has that property with one of the requested values. Offerings that span regions (Compute, SageMaker) have no region or instanceFamily property, so they don't match those filters.
  • Offerings now return properties. The seeded EC2Instance offering reports region=<serve region> and instanceFamily=m5, as in the real SavingsPlanOffering.properties field.
  • validation.go (new):
    • validateCommitment rejects with ValidationException anything outside [0.001, 1000000], NaN/Inf, or more than 5 decimal places.
    • validateTags rejects a key that is empty or longer than 128 characters, or a value longer than 256 characters, with ValidationException.
    • paginate is an offset-token pager. It reuses wire.EncodeOffset/DecodeOffset, whose base64 tokens fit the documented ^[A-Za-z0-9/=\+]+$ pattern. An omitted maxResults returns everything, as before. A negative value or one above 1000 is a ValidationException, and so is an undecodable nextToken. nextToken is left out on the last page ("null when there are no more results").
  • handler.go: describeSavingsPlans and describeOfferings now paginate. describeOfferings builds the filter and calls store.describeOfferings.
  • store.go: create/tag call the validators. Offerings carry a region. ListActive now preallocates its slice, which clears the package's one pre-existing prealloc finding; its behavior is unchanged.
  • There are no driver interface or operation changes, so docs/coverage does not change. There is no new persisted state: the offering catalog is static and reseeded at New.

AWS docs

Provider Coverage

  • AWS
  • Azure (n/a: AWS-only service)
  • GCP (n/a)
  • OCI (n/a)

Checklist

  • All tests pass (go test ./...)
  • Linter passes (golangci-lint run --timeout=9m ./...): no new findings in the touched package. Repo-wide lint on development already reports findings in unrelated code (CI lint is report-only). golangci-lint run ./server/aws/savingsplans/... gives 0 issues on this branch, against 1 pre-existing prealloc on development, which this PR fixes.
  • Every provider the change applies to implements the same behavior
  • Integration tests added to cloudemu_test.go (n/a: wire-only service, covered by SDK tests)
  • Unit tests added (validation_test.go, store level)
  • Regenerated docs: not needed, no operation or interface change

Test Plan

Unit tests (server/aws/savingsplans/validation_test.go):

  • TestCreateRejectsOutOfRangeCommitment
  • TestTagsRejectInvalidKeys
  • TestPaginate
  • TestDescribeOfferingsFilters

Wire tests with the real aws-sdk-go-v2 savingsplans client (server/aws/savingsplans/parity_sdk_test.go):

  • TestCreateSavingsPlanRejectsInvalidCommitment
  • TestTagResourceRejectsEmptyKey
  • TestDescribeOfferingsAppliesProductTypeAndFilters
  • TestDescribeSavingsPlansPagination
  • TestDescribeOfferingsPagination

All five SDK tests fail on development and pass with this change. I checked this by restoring the original handler/store/types files.

Also ran:

  • go build ./...
  • go vet ./server/aws/savingsplans/
  • go test -p 2 ./... (523 packages ok, 0 failures)
  • golangci-lint run ./server/aws/savingsplans/...: 0 issues on this branch, against 1 pre-existing (prealloc in ListActive) on development. No new findings.

Real-user test: cloudemu serve -providers aws -aws-port 4605 driven with AWS CLI v1 (botocore 1.42), --endpoint-url http://127.0.0.1:4605 --region us-east-1:

$ aws savingsplans describe-savings-plans-offerings --product-type SageMaker --query 'searchResults[].offeringId'
["sp-offering-sagemaker-1yr-no"]
$ aws savingsplans describe-savings-plans-offerings --product-type Fargate --query 'searchResults[].offeringId'
["sp-offering-compute-1yr-no", "sp-offering-compute-3yr-all"]
$ aws savingsplans describe-savings-plans-offerings --filters name=instanceFamily,values=m5 --query 'searchResults[].[offeringId,properties]'
[["sp-offering-ec2-1yr-partial", [{"name":"region","value":"us-east-1"},{"name":"instanceFamily","value":"m5"}]]]
$ aws savingsplans describe-savings-plans-offerings --filters name=region,values=eu-west-1 --query 'searchResults[].offeringId'
[]
$ aws savingsplans describe-savings-plans-offerings --payment-options "All Upfront" --query 'searchResults[].offeringId'
["sp-offering-compute-3yr-all"]
$ aws savingsplans describe-savings-plans-offerings --max-results 3 --query '[length(searchResults), nextToken]'
[3, "Mw=="]
$ aws savingsplans describe-savings-plans-offerings --max-results 3 --next-token Mw== --query '[length(searchResults), nextToken]'
[1, null]
$ aws savingsplans create-savings-plan --savings-plan-offering-id sp-offering-compute-1yr-no --commitment 0
An error occurred (ValidationException) when calling the CreateSavingsPlan operation: commitment must be between 0.001 and 1000000, got 0
$ ... --commitment -1          -> ValidationException (out of range)
$ ... --commitment 1.123456    -> ValidationException (more than 5 digits after the decimal point)
$ ... --commitment 2000000     -> ValidationException (out of range)
$ ... --commitment 1 --tags =v -> ValidationException: tag key must not be empty
$ ... --commitment 1.5   (x3)  -> savingsPlanId returned
$ aws savingsplans describe-savings-plans --max-results 2 --query '[length(savingsPlans), nextToken]'
[2, "Mg=="]
$ aws savingsplans describe-savings-plans --max-results 2 --next-token Mg== --query '[length(savingsPlans), nextToken]'
[1, null]
$ aws savingsplans describe-savings-plans --next-token 'garbage!'    -> ValidationException: invalid nextToken: garbage!
$ aws savingsplans describe-savings-plans --max-results 1001         -> ValidationException: maxResults must be between 1 and 1000
$ aws savingsplans tag-resource --resource-arn <arn> --tags =v       -> ValidationException: tag key must not be empty
$ aws savingsplans tag-resource --resource-arn <arn> --tags env=prod -> ok
$ aws savingsplans list-tags-for-resource --resource-arn <arn>       -> {"tags": {"env": "prod"}}

Terraform has no Savings Plans resource to exercise here (and is not installed), so it was not run.

Related Issues

Part of #1515

🤖 Generated with Claude Code

…gs, paginate describes

- DescribeSavingsPlansOfferings now applies productType, filters[]
  (region / instanceFamily, matched against the offering properties) and
  the other documented request lists (paymentOptions, durations,
  currencies, descriptions, serviceCodes, usageTypes, operations), and
  returns offering properties.
- CreateSavingsPlan rejects a commitment outside [0.001, 1000000] or with
  more than five decimal places (ValidationException).
- TagResource / CreateSavingsPlan reject an empty or >128-char tag key and
  a >256-char value (ValidationException).
- DescribeSavingsPlans / DescribeSavingsPlansOfferings honor maxResults /
  nextToken with offset tokens; bad tokens or out-of-range maxResults are
  ValidationException.

Part of stackshy#1515

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant