Documentation
¶
Overview ¶
Package commitmentopts discovers which (term, payment) commitment tuples each AWS commitment-capable service actually sells, persists the result, and exposes it to the API layer so the frontend can hide impossible combinations instead of hardcoding them.
The package never blocks a save on missing data: if no probe has been persisted yet (or the probe failed), Service.Validate falls back to permissive-true and the frontend's hardcoded rules still gate the UI.
Index ¶
- Variables
- type AccountLister
- type AzureSPProber
- type AzureSPValidateAPI
- type BuildConfigFn
- type Combo
- type EC2DescribeOfferings
- type EC2Prober
- type ElastiCacheDescribeOfferings
- type ElastiCacheProber
- type MemoryDBDescribeOfferings
- type MemoryDBProber
- type OpenSearchDescribeOfferings
- type OpenSearchProber
- type Options
- type PostgresStore
- type Prober
- type RDSDescribeOfferings
- type RDSProber
- type RedshiftDescribeOfferings
- type RedshiftProber
- type SavingsPlansDescribeOfferings
- type SavingsPlansProber
- type Service
- type Store
Constants ¶
This section is empty.
Variables ¶
var ErrNoData = errors.New("commitment options unavailable")
ErrNoData signals "we don't have cached commitment options and can't produce them right now" (no AWS account connected, or a probe failed). Callers MUST NOT treat this as a hard error — the API handler maps it to `{"status":"unavailable"}` and Validate maps it to permissive-true.
Functions ¶
This section is empty.
Types ¶
type AccountLister ¶
type AccountLister interface {
ListCloudAccounts(ctx context.Context, filter config.CloudAccountFilter) ([]config.CloudAccount, error)
}
AccountLister is the narrow subset of config.StoreInterface that Service needs. Defining it here (rather than depending on the full interface) keeps tests small and avoids a circular import when the API layer wires up the real store.
type AzureSPProber ¶
type AzureSPProber struct {
// NewClient builds an AzureSPValidateAPI from a credential. Override
// in tests to return a mock.
NewClient func(cred azcore.TokenCredential) (AzureSPValidateAPI, error)
}
AzureSPProber probes the Azure Billing Benefits ValidatePurchase endpoint for each candidate (term, payment) combination and returns those the API accepts as live Combos.
Design notes:
- Azure has no "list offerings" catalog endpoint comparable to AWS Describe*Offerings. ValidatePurchase is the closest live signal that a given (term, billingPlan) configuration is currently accepted.
- The probe uses a zero-valued (placeholder) subscription and the minimum non-zero hourly commitment so no real resource or billing impact occurs.
- A 422/invalid response for a given combo means it is not available; any other error (network, auth, 5xx) is treated as a probe failure and bubbled up so the Service can apply its all-or-nothing policy.
func DefaultAzureProbers ¶
func DefaultAzureProbers() []*AzureSPProber
DefaultAzureProbers returns one prober instance for the Azure Savings Plans service. The Service wires these up by default for Azure probe runs.
func (*AzureSPProber) ProbeAzure ¶
func (p *AzureSPProber) ProbeAzure(ctx context.Context, cred azcore.TokenCredential) ([]Combo, error)
ProbeAzure probes the Azure ValidatePurchase endpoint for each candidate (term, billingPlan) combo and returns Combos for those the API accepts.
The method signature differs from the AWS Prober interface because Azure authentication uses azcore.TokenCredential rather than aws.Config. Callers use this method directly; Service.probeAndPersistAzure wires it up.
func (*AzureSPProber) Service ¶
func (p *AzureSPProber) Service() string
Service returns "savingsplans".
type AzureSPValidateAPI ¶
type AzureSPValidateAPI interface {
ValidatePurchase(
ctx context.Context,
body armbillingbenefits.SavingsPlanPurchaseValidateRequest,
options *armbillingbenefits.RPClientValidatePurchaseOptions,
) (armbillingbenefits.RPClientValidatePurchaseResponse, error)
}
AzureSPValidateAPI is the minimal Azure Billing Benefits surface the probe needs. It matches the ValidatePurchase method on *armbillingbenefits.RPClient so tests can substitute a mock without importing the concrete SDK client.
NOTE: providers/azure/services/savingsplans.RPValidateAPI defines the same one-method interface. The duplication is intentional: importing the provider package from internal/commitmentopts would create a circular dependency. The interface is tiny enough that duplicating it here is the cleanest option.
type BuildConfigFn ¶
BuildConfigFn resolves an aws.Config for a given CloudAccount. The real wiring uses credentials.ResolveAWSCredentialProvider; tests substitute a stub that returns a zero aws.Config.
type Combo ¶
Combo is one (provider, service, term, payment) tuple harvested from the reserved-offerings APIs. TermYears is 1 or 3; Payment is one of "all-upfront", "partial-upfront", "no-upfront". Other durations and legacy utilization-style payment options are dropped during normalization.
type EC2DescribeOfferings ¶
type EC2DescribeOfferings interface {
DescribeReservedInstancesOfferings(ctx context.Context, params *ec2.DescribeReservedInstancesOfferingsInput, optFns ...func(*ec2.Options)) (*ec2.DescribeReservedInstancesOfferingsOutput, error)
}
EC2DescribeOfferings is the minimal EC2 surface we use.
type EC2Prober ¶
type EC2Prober struct {
NewClient func(cfg aws.Config) EC2DescribeOfferings
}
EC2Prober probes ec2:DescribeReservedInstancesOfferings.
type ElastiCacheDescribeOfferings ¶
type ElastiCacheDescribeOfferings interface {
DescribeReservedCacheNodesOfferings(ctx context.Context, params *elasticache.DescribeReservedCacheNodesOfferingsInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeReservedCacheNodesOfferingsOutput, error)
}
ElastiCacheDescribeOfferings is the minimal ElastiCache surface we use.
type ElastiCacheProber ¶
type ElastiCacheProber struct {
NewClient func(cfg aws.Config) ElastiCacheDescribeOfferings
}
ElastiCacheProber probes elasticache:DescribeReservedCacheNodesOfferings.
func (*ElastiCacheProber) Service ¶
func (p *ElastiCacheProber) Service() string
Service returns "elasticache".
type MemoryDBDescribeOfferings ¶
type MemoryDBDescribeOfferings interface {
DescribeReservedNodesOfferings(ctx context.Context, params *memorydb.DescribeReservedNodesOfferingsInput, optFns ...func(*memorydb.Options)) (*memorydb.DescribeReservedNodesOfferingsOutput, error)
}
MemoryDBDescribeOfferings is the minimal MemoryDB surface we use.
type MemoryDBProber ¶
type MemoryDBProber struct {
NewClient func(cfg aws.Config) MemoryDBDescribeOfferings
}
MemoryDBProber probes memorydb:DescribeReservedNodesOfferings.
func (*MemoryDBProber) Service ¶
func (p *MemoryDBProber) Service() string
Service returns "memorydb".
type OpenSearchDescribeOfferings ¶
type OpenSearchDescribeOfferings interface {
DescribeReservedInstanceOfferings(ctx context.Context, params *opensearch.DescribeReservedInstanceOfferingsInput, optFns ...func(*opensearch.Options)) (*opensearch.DescribeReservedInstanceOfferingsOutput, error)
}
OpenSearchDescribeOfferings is the minimal OpenSearch surface we use. The OpenSearch API has no per-instance-type filter on this endpoint, so the probe filters client-side after fetching.
type OpenSearchProber ¶
type OpenSearchProber struct {
NewClient func(cfg aws.Config) OpenSearchDescribeOfferings
}
OpenSearchProber probes opensearch:DescribeReservedInstanceOfferings.
func (*OpenSearchProber) Service ¶
func (p *OpenSearchProber) Service() string
Service returns "opensearch".
type Options ¶
Options groups valid combos by provider and service. Shape:
Options["aws"]["rds"] = []Combo{...}
Only AWS is ever populated today — Azure and GCP stay hardcoded in the frontend because their commitment APIs don't have an equivalent probe.
type PostgresStore ¶
type PostgresStore struct {
// contains filtered or unexported fields
}
PostgresStore is the Postgres-backed commitmentopts.Store implementation.
func NewPostgresStore ¶
func NewPostgresStore(db *database.Connection) *PostgresStore
NewPostgresStore returns a store backed by the given connection. The connection must already have the 000039 migration applied.
func (*PostgresStore) Get ¶
Get returns every persisted combo grouped by provider and service, plus a boolean that is true iff a probe run row exists. The combos map may be empty even when the boolean is true — that legitimately means "we probed and nothing matched our normalizer".
func (*PostgresStore) HasData ¶
func (s *PostgresStore) HasData(ctx context.Context) (bool, error)
HasData reports whether a probe run row exists.
func (*PostgresStore) Save ¶
Save persists the probe run row and every combo transactionally. If a run row already exists (a second process raced us to persist), Save is a no-op — the singleton PK means only the first writer wins. Combo inserts use ON CONFLICT DO NOTHING so re-running the probe after a manual row clear and partial repopulation doesn't fail.
type Prober ¶
type Prober interface {
// Service returns the canonical service name used as the Options key
// and persisted in commitment_options_combos.service.
Service() string
// Probe issues paginated Describe*Offerings calls against a canonical
// small instance type, normalizes the results, and returns unique
// Combos. Errors bubble up — the orchestrating Service treats ANY
// probe failure as "don't persist" (all-or-nothing).
Probe(ctx context.Context, cfg aws.Config) ([]Combo, error)
}
Prober probes a single AWS service's reserved-offerings API and returns every unique (term, payment) tuple it exposes. Implementations live in probe.go, one per service.
func DefaultProbers ¶
func DefaultProbers() []Prober
DefaultProbers returns one prober instance per commitment-capable service. The Service wires these up by default; tests override via a custom slice.
type RDSDescribeOfferings ¶
type RDSDescribeOfferings interface {
DescribeReservedDBInstancesOfferings(ctx context.Context, params *rds.DescribeReservedDBInstancesOfferingsInput, optFns ...func(*rds.Options)) (*rds.DescribeReservedDBInstancesOfferingsOutput, error)
}
RDSDescribeOfferings is the minimal RDS surface the probe needs. It matches the method signature on the generated client so tests can substitute a mock without dragging in the full RDSAPI interface.
type RDSProber ¶
type RDSProber struct {
// NewClient builds a client from the probe's aws.Config. Override in
// tests to return a mock.
NewClient func(cfg aws.Config) RDSDescribeOfferings
}
RDSProber probes rds:DescribeReservedDBInstancesOfferings.
type RedshiftDescribeOfferings ¶
type RedshiftDescribeOfferings interface {
DescribeReservedNodeOfferings(ctx context.Context, params *redshift.DescribeReservedNodeOfferingsInput, optFns ...func(*redshift.Options)) (*redshift.DescribeReservedNodeOfferingsOutput, error)
}
RedshiftDescribeOfferings is the minimal Redshift surface we use. The API has no NodeType filter on DescribeReservedNodeOfferings so the probe filters client-side.
type RedshiftProber ¶
type RedshiftProber struct {
NewClient func(cfg aws.Config) RedshiftDescribeOfferings
}
RedshiftProber probes redshift:DescribeReservedNodeOfferings.
func (*RedshiftProber) Service ¶
func (p *RedshiftProber) Service() string
Service returns "redshift".
type SavingsPlansDescribeOfferings ¶
type SavingsPlansDescribeOfferings interface {
DescribeSavingsPlansOfferings(ctx context.Context, params *savingsplans.DescribeSavingsPlansOfferingsInput, optFns ...func(*savingsplans.Options)) (*savingsplans.DescribeSavingsPlansOfferingsOutput, error)
}
SavingsPlansDescribeOfferings is the minimal Savings Plans surface the probe needs. It matches the relevant method on the generated client so tests can substitute a mock without dragging in the full SavingsPlansAPI interface.
type SavingsPlansProber ¶
type SavingsPlansProber struct {
// NewClient builds a client from the probe's aws.Config. Override in
// tests to return a mock.
NewClient func(cfg aws.Config) SavingsPlansDescribeOfferings
}
SavingsPlansProber probes savingsplans:DescribeSavingsPlansOfferings once per plan type (Compute, EC2Instance, SageMaker, Database) and emits per-product service keys. The probe runs all four plan types in sequence and returns all combos; Service.probeAndPersist fans them out concurrently alongside the RI probers. Any plan type that returns zero offerings for a given account is silently skipped — the frontend falls back to its hardcoded rules for that key.
func (*SavingsPlansProber) Probe ¶
Probe calls DescribeSavingsPlansOfferings for each of the four plan types and returns combos keyed with the per-product service slug. Empty results for a plan type are silently dropped so the caller never stores a zero-combo entry that would incorrectly suppress the fallback.
func (*SavingsPlansProber) Service ¶
func (p *SavingsPlansProber) Service() string
Service returns "savings-plans" — a sentinel used only for error wrapping. Each plan type emits its own per-product service key via Probe; the umbrella name is never stored.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service orchestrates probe-and-cache of AWS commitment options.
Concurrency model:
- Get is DB-first. The DB lookup is lock-free.
- On a cache miss, Get acquires mu, re-checks the DB under the lock (to absorb a concurrent writer), then probes. The lock is per- process; the singleton PK on commitment_options_probe_runs handles cross-process races.
- Probes run concurrently under errgroup. Any probe error aborts the whole attempt and NOTHING is persisted — we never want a partial snapshot to set the "cache warm" sentinel.
func New ¶
func New(store Store, accounts AccountLister, buildConfig BuildConfigFn, probers []Prober) *Service
New returns a Service wired to the given dependencies. Pass DefaultProbers() for the real probe set; tests inject stubs.
func (*Service) Get ¶
Get returns cached Options if they exist, otherwise probes. On any failure (no AWS account connected, probe error) it returns ErrNoData without persisting; callers map that to the unavailable status.
func (*Service) Validate ¶
func (s *Service) Validate(ctx context.Context, provider, service string, term int, payment string) (bool, error)
Validate reports whether (provider, service, term, payment) is a legal combination according to the cached probe data.
Fallback behavior: if no probe data exists (the server has never successfully probed) Validate returns true so saves aren't blocked when we can't verify. The frontend's hardcoded rules are the user-facing gate; this check is belt-and-braces.
type Store ¶
type Store interface {
// Get returns the cached Options and a boolean indicating whether a
// probe run row exists. The boolean is the authoritative "cache warm"
// signal — an empty combos slice plus a present run row means "we
// probed, AWS returned nothing matching our normalizer", and callers
// should NOT re-probe.
Get(ctx context.Context) (Options, bool, error)
// Save writes the probe run row and all combos atomically. If a run
// row already exists (concurrent writer won the race), Save is a
// no-op rather than an error.
Save(ctx context.Context, combos []Combo, sourceAccountID string) error
// HasData reports whether a probe run row exists. Used as a cheap
// pre-check before acquiring the prober lock.
HasData(ctx context.Context) (bool, error)
}
Store persists probe results. Implementations must treat the combos table as idempotent (ON CONFLICT DO NOTHING) so concurrent writers don't trip each other.