exchange

package
v0.0.0-...-79642c4 Latest Latest
Warning

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

Go to latest
Published: Jul 12, 2026 License: OSL-3.0 Imports: 14 Imported by: 0

Documentation

Overview

Package exchange provides AWS Convertible Reserved Instance exchange operations. It wraps the EC2 GetReservedInstancesExchangeQuote and AcceptReservedInstancesExchangeQuote APIs with input validation and spend-cap guardrails.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NormalizationFactorForSize

func NormalizationFactorForSize(size string) float64

NormalizationFactorForSize returns the normalization factor for a given instance size. Returns 0 if the size is unknown.

func ParseDecimalRat

func ParseDecimalRat(s string) (*big.Rat, error)

ParseDecimalRat parses AWS decimal strings like "123.45" or "-0.018000" into big.Rat.

Types

type AutoExchangeResult

type AutoExchangeResult struct {
	Mode      string
	Completed []ExchangeOutcome
	Pending   []ExchangeOutcome
	Failed    []ExchangeOutcome
	Skipped   []SkippedRecommendation
}

AutoExchangeResult contains the outcome of an auto exchange run.

func RunAutoExchange

func RunAutoExchange(ctx context.Context, params RunAutoExchangeParams) (*AutoExchangeResult, error)

RunAutoExchange orchestrates automated RI exchanges.

type EC2ExchangeAPI

type EC2ExchangeAPI interface {
	GetReservedInstancesExchangeQuote(ctx context.Context, params *ec2.GetReservedInstancesExchangeQuoteInput, optFns ...func(*ec2.Options)) (*ec2.GetReservedInstancesExchangeQuoteOutput, error)
	AcceptReservedInstancesExchangeQuote(ctx context.Context, params *ec2.AcceptReservedInstancesExchangeQuoteInput, optFns ...func(*ec2.Options)) (*ec2.AcceptReservedInstancesExchangeQuoteOutput, error)
}

EC2ExchangeAPI defines the EC2 API methods used by exchange operations. Satisfied by *ec2.Client; accept this interface to enable testing without real AWS credentials.

type ExchangeClient

type ExchangeClient struct {
	// contains filtered or unexported fields
}

ExchangeClient wraps an EC2ExchangeAPI for dependency-injected exchange operations. Use NewExchangeClient to construct one.

func NewExchangeClient

func NewExchangeClient(cfg sdkaws.Config) *ExchangeClient

NewExchangeClient creates an ExchangeClient from an AWS config.

func NewExchangeClientFromAPI

func NewExchangeClientFromAPI(api EC2ExchangeAPI) *ExchangeClient

NewExchangeClientFromAPI creates an ExchangeClient from an existing EC2ExchangeAPI implementation (useful for testing).

func (*ExchangeClient) Execute

Execute performs a convertible RI exchange with a spend-cap guardrail using the injected EC2 client.

func (*ExchangeClient) GetQuote

GetQuote retrieves an exchange quote using the injected EC2 client.

type ExchangeClientInterface

type ExchangeClientInterface interface {
	GetQuote(ctx context.Context, req ExchangeQuoteRequest) (*ExchangeQuoteSummary, error)
	Execute(ctx context.Context, req ExchangeExecuteRequest) (string, *ExchangeQuoteSummary, error)
}

ExchangeClientInterface abstracts the ExchangeClient for testability.

type ExchangeExecuteRequest

type ExchangeExecuteRequest struct {
	Region          string
	ExpectedAccount string // optional safety check
	ReservedIDs     []string

	// Targets is the preferred path for multi-target exchanges. When
	// non-empty, TargetOfferingID / TargetCount are ignored.
	Targets []TargetConfig

	// Legacy single-target alias. Prefer Targets for new code.
	TargetOfferingID string
	TargetCount      int32

	// Guardrail: require PaymentDue <= MaxPaymentDueUSD to execute.
	// AWS returns a single aggregated PaymentDue across all targets,
	// so for multi-target requests this guardrail naturally becomes a
	// total cap rather than a per-target cap.
	// If nil, execution is refused.
	MaxPaymentDueUSD *big.Rat
}

ExchangeExecuteRequest holds parameters for executing an exchange.

type ExchangeMode

type ExchangeMode string

ExchangeMode constrains the originating code path of an exchange record so `saveFailedRecord` (and any future caller) can't silently leak a typo into `ExchangeRecord.Mode`. The storage field stays `string` for serialization stability — this is a call-site discipline, not a schema change.

const (
	ExchangeModeAuto   ExchangeMode = "auto"
	ExchangeModeManual ExchangeMode = "manual"
)

type ExchangeOutcome

type ExchangeOutcome struct {
	RecordID           string
	ApprovalToken      string
	SourceRIID         string
	SourceInstanceType string
	TargetInstanceType string
	TargetOfferingID   string
	TargetCount        int32
	PaymentDue         string
	ExchangeID         string
	UtilizationPct     float64
	Error              string
}

ExchangeOutcome captures the result of a single exchange attempt.

type ExchangeQuoteRequest

type ExchangeQuoteRequest struct {
	Region          string
	ExpectedAccount string // optional safety check
	ReservedIDs     []string

	// Targets is the preferred path for multi-target exchanges. When
	// non-empty, TargetOfferingID / TargetCount are ignored.
	Targets []TargetConfig

	// TargetOfferingID + TargetCount are the legacy single-target
	// fields, retained so pre-existing callers (HTTP handlers, sanity
	// tests, serialized PurchasePlans) don't need a flag-day change.
	// New code should populate Targets instead.
	TargetOfferingID string
	TargetCount      int32

	// DryRun here uses the AWS API DryRun parameter (permission check).
	// The quote call itself never performs an exchange.
	DryRun bool
}

ExchangeQuoteRequest holds parameters for requesting an exchange quote.

type ExchangeQuoteSummary

type ExchangeQuoteSummary struct {
	IsValidExchange         bool
	ValidationFailureReason string
	CurrencyCode            string

	PaymentDueRaw    string   // as returned by AWS (string)
	PaymentDueUSD    *big.Rat `json:"-"`                         // internal use only, not serializable
	PaymentDueUSDStr string   `json:"payment_due_usd,omitempty"` // parsed decimal for JSON consumers

	OutputReservedInstancesExp string // formatted date string (YYYY-MM-DD), empty if not set

	// Rollups (strings in AWS response)
	SourceHourlyPriceRaw      string
	SourceRemainingUpfrontRaw string
	SourceRemainingTotalRaw   string
	TargetHourlyPriceRaw      string
	TargetRemainingUpfrontRaw string
	TargetRemainingTotalRaw   string
}

ExchangeQuoteSummary is a small, stable summary we can log/guard on.

func ExecuteExchange

func ExecuteExchange(ctx context.Context, req ExchangeExecuteRequest) (exchangeID string, quote *ExchangeQuoteSummary, err error)

ExecuteExchange performs a convertible RI exchange with a spend-cap guardrail. This is a convenience wrapper that creates its own AWS client from default config.

func GetExchangeQuote

func GetExchangeQuote(ctx context.Context, req ExchangeQuoteRequest) (*ExchangeQuoteSummary, error)

GetExchangeQuote retrieves an exchange quote from the EC2 API.

type ExchangeRecord

type ExchangeRecord struct {
	ID                 string
	AccountID          string
	ExchangeID         string
	Region             string
	SourceRIIDs        []string
	SourceInstanceType string
	SourceCount        int
	TargetOfferingID   string
	TargetInstanceType string
	TargetCount        int
	PaymentDue         string
	Status             string
	ApprovalToken      string
	Error              string
	Mode               string
	CreatedAt          time.Time
	UpdatedAt          time.Time
	CompletedAt        *time.Time
	ExpiresAt          *time.Time
}

ExchangeRecord is a lightweight record type for the auto exchange logic. It mirrors config.RIExchangeRecord but lives in pkg/exchange to avoid cross-module imports (pkg/ is a separate Go module from internal/).

type LookupOfferingFunc

type LookupOfferingFunc func(ctx context.Context, instanceType, productDesc, tenancy, scope string, duration int64) (string, error)

LookupOfferingFunc looks up a target offering ID for a given instance type and RI metadata.

type OfferingOption

type OfferingOption struct {
	InstanceType         string  `json:"instance_type"`
	OfferingID           string  `json:"offering_id"`
	EffectiveMonthlyCost float64 `json:"effective_monthly_cost"`
	// NormalizationFactor is the AWS normalization factor for the
	// offering's instance size — needed by the local
	// passesDollarUnitsCheck pre-filter that gates which cross-family
	// alternatives surface in the UI. Zero on missing means the
	// alternative cannot be safely compared and the pre-filter
	// excludes it.
	NormalizationFactor float64 `json:"normalization_factor,omitempty"`
	// CurrencyCode is the ISO-4217 currency the EffectiveMonthlyCost is
	// denominated in (typically "USD"). The pre-filter requires source
	// and target currencies to match; empty on either side falls back to
	// "skip the currency guard" so today's USD-only fixtures stay green.
	CurrencyCode string `json:"currency_code,omitempty"`
	// TermSeconds is the offering's commitment term in seconds
	// (31_536_000 for 1y, 94_608_000 for 3y) — same unit as
	// RIInfo.TermSeconds and the AWS SDK's ReservedInstance.Duration.
	// AnalyzeReshapingWithRecs rejects term-mismatched alternatives
	// before building AlternativeTargets so a 3y RI never surfaces as
	// an alternative to a 1y source commitment (and vice versa). Zero
	// on either side falls back to "skip the term guard" for callers
	// or fixtures that don't populate it.
	TermSeconds int64 `json:"term_seconds,omitempty"`
	// SavingsAbs is the absolute monthly savings (in CurrencyCode) that
	// AWS Cost Explorer reported for this recommendation. Used by
	// compositeScore to estimate savings % and derive a confidence
	// signal. Zero means "not supplied" — the score component is omitted
	// rather than coerced to 0 (which would falsely rank this offering
	// last on the savings axis).
	SavingsAbs *float64 `json:"savings_abs,omitempty"`
	// RecommendationCount is the number of instances AWS Cost Explorer
	// included in the recommendation. Used together with SavingsAbs to
	// derive a confidence signal (large fleet + large savings = high
	// confidence). Zero means "not supplied"; the confidence signal is
	// skipped rather than treated as a 0-instance fleet.
	RecommendationCount int `json:"recommendation_count,omitempty"`
}

OfferingOption is a single Convertible RI offering exposed to the reshape layer: the AWS offering ID, the instance type it provisions, and the effective monthly cost (amortised fixed price + recurring hourly charges × 730 hours). Used both as the return shape of PurchaseRecLookup (what the recommendations-store closure returns) and as the element type of ReshapeRecommendation.AlternativeTargets (what reshape emits to the HTTP handler). Kept in pkg/exchange so pkg/exchange stays AWS-free and providers/aws/services/ec2 imports the type rather than defining its own.

type PurchaseRecLookup

type PurchaseRecLookup func(ctx context.Context, region, currencyCode string) ([]OfferingOption, error)

PurchaseRecLookup is the signature of the closure that resolves a region + currency to the cached AWS Cost Explorer purchase recommendations available there. Used by AnalyzeReshapingWithRecs to generate cross-family alternatives without per-recommendation AWS API calls — the recommendations table is already populated by the scheduler tick. Implementations read from store.ListStoredRecommendations and map each RecommendationRecord to an OfferingOption (effective monthly cost amortised from the upfront and monthly fields).

region is the AWS region the reshape page is viewing; currencyCode is the source RIs' currency, propagated onto each returned OfferingOption.CurrencyCode so the dollar-units pre-filter can refuse cross-currency comparisons cleanly.

type RIExchangeConfig

type RIExchangeConfig struct {
	Mode                     string
	UtilizationThreshold     float64
	MaxPaymentPerExchangeUSD float64
	MaxPaymentDailyUSD       float64
	LookbackDays             int
}

RIExchangeConfig holds the runtime configuration for auto exchange.

type RIExchangeStore

type RIExchangeStore interface {
	SaveRIExchangeRecord(ctx context.Context, record *ExchangeRecord) error
	CancelAllPendingExchanges(ctx context.Context) (int64, error)
	GetStaleProcessingExchanges(ctx context.Context, olderThan time.Duration) ([]ExchangeRecord, error)
	GetRIExchangeDailySpend(ctx context.Context, date time.Time) (string, error)
	CompleteRIExchange(ctx context.Context, id string, exchangeID string) error
	FailRIExchange(ctx context.Context, id string, errorMsg string) error
}

RIExchangeStore is the subset of store operations needed by RunAutoExchange.

type RIInfo

type RIInfo struct {
	ID                  string
	InstanceType        string
	InstanceCount       int32
	OfferingClass       string  // must be "convertible" — standard RIs cannot be exchanged
	NormalizationFactor float64 // AWS normalization factor for the instance size
	// MonthlyCost is the effective per-instance per-month price (amortised
	// upfront + recurring charges, AWS-canonical 730 hours per month).
	// Used by the local passesDollarUnitsCheck pre-filter that gates
	// cross-family alternatives. Zero when the caller didn't compute it
	// (e.g. older callers that only need primary-target analysis); the
	// pre-filter treats zero as "skip the check" so existing behaviour
	// is preserved.
	MonthlyCost float64
	// CurrencyCode is the ISO-4217 currency the prices are denominated
	// in (typically "USD"). Used by the cross-family check to refuse
	// comparisons across currencies. Empty matches the same "skip"
	// semantics as MonthlyCost == 0.
	CurrencyCode string
	// TermSeconds is the RI commitment term in seconds (31_536_000 for
	// 1y, 94_608_000 for 3y) — the same unit the AWS SDK uses on the
	// ReservedInstance.Duration field and the unit ec2.ConvertibleRI.Duration
	// already carries. Used by the cross-family alternatives pass to
	// reject term-mismatched offerings (e.g. surfacing a 3y RI as an
	// alternative to a 1y commitment is wrong because the customer's
	// existing exchange anchor is the 1y term). Zero means "unknown" —
	// the term filter is skipped for that rec to preserve today's
	// behaviour for older callers that don't populate it.
	TermSeconds int64
}

RIInfo describes a Reserved Instance for reshape analysis. Callers should pre-filter to convertible RIs only.

type RIMetadataInfo

type RIMetadataInfo struct {
	ProductDescription string
	InstanceTenancy    string
	Scope              string
	Duration           int64
}

RIMetadataInfo holds the offering metadata for a specific RI.

type ReshapeRecommendation

type ReshapeRecommendation struct {
	SourceRIID          string           `json:"source_ri_id"`
	SourceInstanceType  string           `json:"source_instance_type"`
	SourceCount         int32            `json:"source_count"`
	TargetInstanceType  string           `json:"target_instance_type"`
	TargetCount         int32            `json:"target_count"`
	AlternativeTargets  []OfferingOption `json:"alternative_targets,omitempty"`
	UtilizationPercent  float64          `json:"utilization_percent"`
	NormalizedUsed      float64          `json:"normalized_used"`
	NormalizedPurchased float64          `json:"normalized_purchased"`
	Reason              string           `json:"reason"`
}

ReshapeRecommendation describes a suggested exchange for an underutilized RI.

AlternativeTargets lists cross-family options enriched with real offering IDs and monthly cost from cached AWS Cost Explorer purchase recommendations (see AnalyzeReshapingWithRecs). This is advisory data for the UI to surface alongside the primary target; the auto-exchange pipeline still acts on TargetInstanceType only so existing automated behaviour is unchanged. Empty when the base AnalyzeReshaping is used directly (auto.go) or when no cached recommendations exist for the region.

func AnalyzeReshaping

func AnalyzeReshaping(ris []RIInfo, utilization []UtilizationInfo, threshold float64) []ReshapeRecommendation

AnalyzeReshaping identifies underutilized convertible RIs and suggests optimal exchange targets using AWS normalization factors.

threshold is a percentage (0–100) below which an RI is considered underutilized. For example, threshold=95 means RIs with <95% utilization get recommendations.

Recommendations are emitted with empty AlternativeTargets — alternatives are populated only by AnalyzeReshapingWithRecs, which pairs each rec against the cached AWS Cost Explorer recommendations.

func AnalyzeReshapingWithRecs

func AnalyzeReshapingWithRecs(
	ctx context.Context,
	ris []RIInfo,
	utilization []UtilizationInfo,
	threshold float64,
	region, currencyCode string,
	lookup PurchaseRecLookup,
) []ReshapeRecommendation

AnalyzeReshapingWithRecs is AnalyzeReshaping plus cross-family alternatives sourced from the cached AWS Cost Explorer purchase recommendations table. It calls the base analyzer, then makes a SINGLE region-scoped lookup and pairs each rec against the returned offerings — no per-recommendation AWS API call, no hand-curated peer-family allowlist.

Filtering rules per rec:

  • The source family (parsed from rec.SourceInstanceType) is excluded so the alternatives slice carries only cross-family options.
  • The primary target (rec.TargetInstanceType) is also excluded — it is already surfaced as the primary suggestion.
  • passesDollarUnitsCheck gates each surviving offering against the source RI's NF / MonthlyCost / CurrencyCode so the UI doesn't show options that would be rejected at AWS exchange time. When the source RI lacks pricing (MonthlyCost == 0) the gate is skipped for that rec to preserve today's behaviour for callers that don't supply pricing.

Missing cache, lookup error, or empty-region response: rec ships with empty AlternativeTargets — the dashboard's primary reshape suggestion stays intact and the UX matches "AWS hasn't recommended anything for this region yet". auto.go keeps calling AnalyzeReshaping (no alternatives needed); only the HTTP reshape handler calls this enriched version.

type RunAutoExchangeParams

type RunAutoExchangeParams struct {
	Store          RIExchangeStore
	ExchangeClient ExchangeClientInterface
	LookupOffering LookupOfferingFunc
	RIs            []RIInfo
	Utilization    []UtilizationInfo
	Config         RIExchangeConfig
	AccountID      string
	Region         string
	DashboardURL   string

	// RIMetadata maps RI ID to its metadata (product description, tenancy, scope, duration).
	RIMetadata map[string]RIMetadataInfo
}

RunAutoExchangeParams holds all dependencies for RunAutoExchange.

type SkippedRecommendation

type SkippedRecommendation struct {
	SourceRIID         string
	SourceInstanceType string
	Reason             string
}

SkippedRecommendation captures a recommendation that was not processed.

type TargetConfig

type TargetConfig struct {
	OfferingID string
	Count      int32
}

TargetConfig is a single target offering in an exchange: a Convertible RI offering to buy and how many of it. AWS accepts multiple targets per exchange (AcceptReservedInstancesExchangeQuote is all-or-nothing across the whole TargetConfigurations slice), which lets callers redistribute RI value across several shapes in one atomic operation.

type UtilizationInfo

type UtilizationInfo struct {
	RIID               string
	UtilizationPercent float64 // 0.0–100.0
}

UtilizationInfo provides utilization data for a specific RI.

Jump to

Keyboard shortcuts

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