commitmentopts

package
v0.0.0-...-333b13f Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2026 License: OSL-3.0 Imports: 23 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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

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

type BuildConfigFn func(ctx context.Context, account *config.CloudAccount) (aws.Config, error)

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

type Combo struct {
	Provider  string
	Service   string
	Payment   string
	TermYears int
}

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.

func (*EC2Prober) Probe

func (p *EC2Prober) Probe(ctx context.Context, cfg aws.Config) ([]Combo, error)

Probe returns the combos for t3.micro. IncludeMarketplace is explicitly false so we only see AWS-native (standard/convertible) offerings — the Marketplace resale market has arbitrary durations that would pollute normalization.

func (*EC2Prober) Service

func (p *EC2Prober) Service() string

Service returns "ec2".

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) Probe

func (p *ElastiCacheProber) Probe(ctx context.Context, cfg aws.Config) ([]Combo, error)

Probe returns the combos for cache.t3.micro.

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) Probe

func (p *MemoryDBProber) Probe(ctx context.Context, cfg aws.Config) ([]Combo, error)

Probe returns the combos for db.r6g.large.

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) Probe

func (p *OpenSearchProber) Probe(ctx context.Context, cfg aws.Config) ([]Combo, error)

Probe returns the combos for t3.small.search.

func (*OpenSearchProber) Service

func (p *OpenSearchProber) Service() string

Service returns "opensearch".

type Options

type Options map[string]map[string][]Combo

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

func (s *PostgresStore) Get(ctx context.Context) (Options, bool, error)

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

func (s *PostgresStore) Save(ctx context.Context, combos []Combo, sourceAccountID string) error

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.

func (*RDSProber) Probe

func (p *RDSProber) Probe(ctx context.Context, cfg aws.Config) ([]Combo, error)

Probe returns the normalized (term, payment) combos RDS currently sells against db.t3.micro.

func (*RDSProber) Service

func (p *RDSProber) Service() string

Service returns "rds".

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) Probe

func (p *RedshiftProber) Probe(ctx context.Context, cfg aws.Config) ([]Combo, error)

Probe returns the combos for dc2.large.

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

func (p *SavingsPlansProber) Probe(ctx context.Context, cfg aws.Config) ([]Combo, error)

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

func (s *Service) Get(ctx context.Context) (Options, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL