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 ¶
- func NormalizationFactorForSize(size string) float64
- func ParseDecimalRat(s string) (*big.Rat, error)
- type AutoExchangeResult
- type EC2ExchangeAPI
- type ExchangeClient
- type ExchangeClientInterface
- type ExchangeExecuteRequest
- type ExchangeMode
- type ExchangeOutcome
- type ExchangeQuoteRequest
- type ExchangeQuoteSummary
- type ExchangeRecord
- type LookupOfferingFunc
- type OfferingOption
- type PurchaseRecLookup
- type RIExchangeConfig
- type RIExchangeStore
- type RIInfo
- type RIMetadataInfo
- type ReshapeRecommendation
- type RunAutoExchangeParams
- type SkippedRecommendation
- type TargetConfig
- type UtilizationInfo
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NormalizationFactorForSize ¶
NormalizationFactorForSize returns the normalization factor for a given instance size. Returns 0 if the size is unknown.
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 ¶
func (c *ExchangeClient) Execute(ctx context.Context, req ExchangeExecuteRequest) (string, *ExchangeQuoteSummary, error)
Execute performs a convertible RI exchange with a spend-cap guardrail using the injected EC2 client.
func (*ExchangeClient) GetQuote ¶
func (c *ExchangeClient) GetQuote(ctx context.Context, req ExchangeQuoteRequest) (*ExchangeQuoteSummary, error)
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 ¶
SkippedRecommendation captures a recommendation that was not processed.
type TargetConfig ¶
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 ¶
UtilizationInfo provides utilization data for a specific RI.