Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions providers/aws/internal/offeringprice/offeringprice.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,27 @@ const hourlyFrequency = "Hourly"

// Charge is one recurring charge of an offering.
type Charge struct {
Amount float64
Frequency string
Amount float64
}

// Input is the price data of one offering.
type Input struct {
// Service names the AWS service in error messages and term parsing.
Service string

// Term is the requested term (rec.Term); it must match the offering.
Term string

Charges []Charge

// FixedPrice is the upfront price per reservation.
FixedPrice float64

// UsagePrice is the legacy per-hour price per reservation. It is used
// only when the recurring charges sum to zero.
UsagePrice float64
Charges []Charge

// DurationSeconds is the offering's own duration; 0 means missing.
DurationSeconds int64
}
Expand Down
8 changes: 4 additions & 4 deletions providers/aws/internal/offeringprice/offeringprice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ func TestPrice(t *testing.T) {
err string
}{
{"all upfront", Input{Term: "1yr", FixedPrice: 1000, DurationSeconds: one}, 1000, 8760, 0, ""},
{"hourly charge, usage 0", Input{Term: "1yr", Charges: []Charge{{0.05, "Hourly"}}, DurationSeconds: one}, 438, 8760, 0.05, ""},
{"hourly charge, usage 0", Input{Term: "1yr", Charges: []Charge{{Amount: 0.05, Frequency: "Hourly"}}, DurationSeconds: one}, 438, 8760, 0.05, ""},
{"usage only, no charges", Input{Term: "1yr", UsagePrice: 0.05, DurationSeconds: one}, 438, 8760, 0.05, ""},
{"partial 3yr", Input{Term: "3yr", FixedPrice: 500, Charges: []Charge{{0.03, "Hourly"}}, DurationSeconds: three}, 500 + 0.03*26280, 26280, 0.03, ""},
{"both nonzero", Input{Term: "1yr", UsagePrice: 0.05, Charges: []Charge{{0.05, "Hourly"}}, DurationSeconds: one}, 0, 0, 0, "refusing to guess"},
{"non hourly", Input{Term: "1yr", Charges: []Charge{{1, "Monthly"}}, DurationSeconds: one}, 0, 0, 0, "only \"Hourly\""},
{"partial 3yr", Input{Term: "3yr", FixedPrice: 500, Charges: []Charge{{Amount: 0.03, Frequency: "Hourly"}}, DurationSeconds: three}, 500 + 0.03*26280, 26280, 0.03, ""},
{"both nonzero", Input{Term: "1yr", UsagePrice: 0.05, Charges: []Charge{{Amount: 0.05, Frequency: "Hourly"}}, DurationSeconds: one}, 0, 0, 0, "refusing to guess"},
{"non hourly", Input{Term: "1yr", Charges: []Charge{{Amount: 1, Frequency: "Monthly"}}, DurationSeconds: one}, 0, 0, 0, "only \"Hourly\""},
{"missing duration", Input{Term: "1yr"}, 0, 0, 0, "no duration"},
{"term mismatch", Input{Term: "3yr", DurationSeconds: one}, 0, 0, 0, "does not match"},
{"negative fixed", Input{Term: "1yr", FixedPrice: -1, DurationSeconds: one}, 0, 0, 0, "invalid FixedPrice"},
Expand Down
16 changes: 8 additions & 8 deletions providers/aws/internal/purchasecfg/purchasetest/purchasetest.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,18 @@ func RunEmptyToken(t *testing.T, call Call, alreadyExists error) {

lost := &net.OpError{Op: "read", Err: errors.New("connection reset")}
for _, tc := range []struct {
name, token string
apiErr error
name, token string
emptyResp bool
unknown bool
}{
{"transport error", "", lost, false, true},
{"5xx", "", RespErr(http.StatusInternalServerError, &smithy.GenericAPIError{Code: "InternalFailure"}), false, true},
{"empty response", "", nil, true, true},
{"400 AlreadyExists", "", RespErr(http.StatusBadRequest, alreadyExists), false, false},
{"throttle", "", RespErr(http.StatusServiceUnavailable, &smithy.GenericAPIError{Code: "Throttling"}), false, false},
{"token: transport error", "exec-token", lost, false, false},
{"token: empty response", "exec-token", nil, true, false},
{apiErr: lost, name: "transport error", token: "", emptyResp: false, unknown: true},
{apiErr: RespErr(http.StatusInternalServerError, &smithy.GenericAPIError{Code: "InternalFailure"}), name: "5xx", token: "", emptyResp: false, unknown: true},
{apiErr: nil, name: "empty response", token: "", emptyResp: true, unknown: true},
{apiErr: RespErr(http.StatusBadRequest, alreadyExists), name: "400 AlreadyExists", token: "", emptyResp: false, unknown: false},
{apiErr: RespErr(http.StatusServiceUnavailable, &smithy.GenericAPIError{Code: "Throttling"}), name: "throttle", token: "", emptyResp: false, unknown: false},
{apiErr: lost, name: "token: transport error", token: "exec-token", emptyResp: false, unknown: false},
{apiErr: nil, name: "token: empty response", token: "exec-token", emptyResp: true, unknown: false},
} {
t.Run(tc.name, func(t *testing.T) {
_, result, err := call(t, tc.token, tc.apiErr, tc.emptyResp)
Expand Down
8 changes: 4 additions & 4 deletions providers/aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,16 @@ func (r *realOrganizationsPaginator) NextPage(ctx context.Context, optFns ...fun

// Provider implements the provider.Provider interface for AWS.
type Provider struct {
cfg aws.Config
cfgOnce sync.Once
cfgErr error // non-nil if config loading failed
profile string
region string
configLoader ConfigLoader
stsClient STSClient
ec2Client EC2Client
orgPaginator OrganizationsPaginator
credentialsProvider aws.CredentialsProvider // optional override for per-account execution
profile string
region string
cfg aws.Config
cfgOnce sync.Once
}

// NewAWSProvider creates a new AWS provider instance.
Expand Down
8 changes: 4 additions & 4 deletions providers/aws/recommendations/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,14 @@ type Client struct {

region string

// recLookbackPeriod is forwarded to GetReservationPurchaseRecommendation
// as LookbackPeriodInDays. Defaults to "7d" when empty.
recLookbackPeriod string

// skuCatalog caches the per-instance-type vCPU/memory catalog, fetched
// lazily once per Client lifetime via sync.Once (one DescribeInstanceTypes
// fan-out per scheduler tick).
skuCatalog skuCatalog

// recLookbackPeriod is forwarded to GetReservationPurchaseRecommendation
// as LookbackPeriodInDays. Defaults to "7d" when empty.
recLookbackPeriod string
}

// NewClient creates a new recommendations client.
Expand Down
2 changes: 1 addition & 1 deletion providers/aws/recommendations/collection_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ func requireUSD(code string) error {
// IncompleteRecommendationsError accompanies survivors of an incomplete collection.
// Each cause represents one rejected detail or one failed collection scope.
type IncompleteRecommendationsError struct {
Causes []error
FailedDetails int
FailedScopes int
Causes []error
}

func (e *IncompleteRecommendationsError) Error() string {
Expand Down
14 changes: 7 additions & 7 deletions providers/aws/recommendations/parser_sp.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,16 +176,16 @@ type spNumbers struct {
func parseSPNumbers(detail *types.SavingsPlansPurchaseRecommendationDetail) (spNumbers, error) {
var n spNumbers
for _, f := range []struct {
name string
src *string
dst *float64
name string
}{
{"HourlyCommitmentToPurchase", detail.HourlyCommitmentToPurchase, &n.hourlyCommitment},
{"EstimatedMonthlySavingsAmount", detail.EstimatedMonthlySavingsAmount, &n.monthlySavings},
{"UpfrontCost", detail.UpfrontCost, &n.upfrontCost},
{"EstimatedSavingsPercentage", detail.EstimatedSavingsPercentage, &n.savingsPercent},
{"EstimatedAverageUtilization", detail.EstimatedAverageUtilization, &n.recommendedUtilization},
{"CurrentAverageHourlyOnDemandSpend", detail.CurrentAverageHourlyOnDemandSpend, &n.hourlyOnDemand},
{src: detail.HourlyCommitmentToPurchase, dst: &n.hourlyCommitment, name: "HourlyCommitmentToPurchase"},
{src: detail.EstimatedMonthlySavingsAmount, dst: &n.monthlySavings, name: "EstimatedMonthlySavingsAmount"},
{src: detail.UpfrontCost, dst: &n.upfrontCost, name: "UpfrontCost"},
{src: detail.EstimatedSavingsPercentage, dst: &n.savingsPercent, name: "EstimatedSavingsPercentage"},
{src: detail.EstimatedAverageUtilization, dst: &n.recommendedUtilization, name: "EstimatedAverageUtilization"},
{src: detail.CurrentAverageHourlyOnDemandSpend, dst: &n.hourlyOnDemand, name: "CurrentAverageHourlyOnDemandSpend"},
} {
v, err := parseOptionalFloat(f.name, f.src)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion providers/aws/recommendations/usage_history.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,9 @@ type regionKey struct{ service, region string }
// regionBucket holds the distinct resource types requested for one regionKey
// (in first-seen order) and the recommendation indices for each type.
type regionBucket struct {
recsByType map[string][]int
key regionKey
types []string
recsByType map[string][]int
}

// groupRecsByRegion builds an ordered list of (service, region) buckets, each
Expand Down
8 changes: 4 additions & 4 deletions providers/aws/recommendations/utilization.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,13 @@ func aggregateUtilizationDetail(agg map[string]*riAccumulator, detail types.Rese
// parseUtilizationHours strictly parses the three required hours fields.
func parseUtilizationHours(u *types.ReservationAggregates) (purchased, actual, unused float64, err error) {
for _, f := range []struct {
name string
src *string
dst *float64
name string
}{
{"PurchasedHours", u.PurchasedHours, &purchased},
{"TotalActualHours", u.TotalActualHours, &actual},
{"UnusedHours", u.UnusedHours, &unused},
{src: u.PurchasedHours, dst: &purchased, name: "PurchasedHours"},
{src: u.TotalActualHours, dst: &actual, name: "TotalActualHours"},
{src: u.UnusedHours, dst: &unused, name: "UnusedHours"},
} {
if f.src == nil {
return 0, 0, 0, fmt.Errorf("%s is missing while other utilization hours are present", f.name)
Expand Down
42 changes: 23 additions & 19 deletions providers/aws/services/ec2/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,9 +385,9 @@ type ec2OfferingQuery struct {
tenancy types.Tenancy
scope types.Scope
availabilityZone string // set only for zonal scope
duration int64
wantOfferingType types.OfferingTypeValues
offeringClass types.OfferingClassType
duration int64
}

// resolveOfferingClassType maps the GlobalConfig string value to the AWS SDK
Expand Down Expand Up @@ -761,32 +761,36 @@ func (c *Client) getDurationValue(term string) (int64, error) {

// ConvertibleRI represents an active convertible Reserved Instance.
type ConvertibleRI struct {
ReservedInstanceID string `json:"reserved_instance_id"`
InstanceType string `json:"instance_type"`
AvailabilityZone string `json:"availability_zone"`
InstanceCount int32 `json:"instance_count"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
OfferingType string `json:"offering_type"`
FixedPrice float64 `json:"fixed_price"`
UsagePrice float64 `json:"usage_price"`
State string `json:"state"`
NormalizationFactor float64 `json:"normalization_factor"`
ProductDescription string `json:"product_description"`
InstanceTenancy string `json:"instance_tenancy"`
Scope string `json:"scope"`
Duration int64 `json:"duration"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
ReservedInstanceID string `json:"reserved_instance_id"`
InstanceType string `json:"instance_type"`
AvailabilityZone string `json:"availability_zone"`
OfferingType string `json:"offering_type"`
State string `json:"state"`
ProductDescription string `json:"product_description"`
InstanceTenancy string `json:"instance_tenancy"`
Scope string `json:"scope"`

// CurrencyCode is the ISO-4217 currency that FixedPrice / UsagePrice
// are denominated in (typically "USD"). Plumbed through to
// exchange.RIInfo so the dollar-units pre-filter on cross-family
// alternatives can refuse comparisons across currencies.
CurrencyCode string `json:"currency_code,omitempty"`

FixedPrice float64 `json:"fixed_price"`
UsagePrice float64 `json:"usage_price"`
NormalizationFactor float64 `json:"normalization_factor"`
Duration int64 `json:"duration"`

// RecurringHourlyAmount is the per-hour recurring charge (sum across
// the SDK's RecurringCharges slice) — this is non-zero for
// no-upfront and partial-upfront RIs where some of the cost is
// billed hourly rather than as the upfront FixedPrice. Used by the
// exchange-package dollar-units calculation.
RecurringHourlyAmount float64 `json:"recurring_hourly_amount,omitempty"`

InstanceCount int32 `json:"instance_count"`
}

// ListConvertibleReservedInstances returns all active convertible RIs in the region.
Expand Down Expand Up @@ -971,11 +975,11 @@ type TargetOffering struct {
InstanceType string `json:"instance_type"`
OfferingType string `json:"offering_type"`
ProductDescription string `json:"product_description"`
CurrencyCode string `json:"currency_code"`
Scope string `json:"scope"`
Duration int64 `json:"duration"`
FixedPrice float64 `json:"fixed_price"`
UsagePrice float64 `json:"usage_price"`
CurrencyCode string `json:"currency_code"`
Scope string `json:"scope"`
NormalizationFactor float64 `json:"normalization_factor"`
}

Expand All @@ -986,8 +990,8 @@ type ListTargetOfferingsParams struct {
Tenancy string
Scope string
AvailabilityZone string
Duration int64
OfferingType string
Duration int64
}

// maxTargetOfferingPages caps the pagination walk for ListTargetOfferings.
Expand Down
20 changes: 10 additions & 10 deletions providers/aws/services/ec2/offering_match.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ type offeringWant struct {
productDesc string
tenancy types.Tenancy
scope types.Scope
duration int64
class types.OfferingClassType
offeringType types.OfferingTypeValues
az string
duration int64
}

// want maps the lookup query onto the fields offeringMismatch re-checks.
Expand All @@ -44,31 +44,31 @@ const vpcProductSuffix = " (Amazon VPC)"
// field is a mismatch); instance type, scope and product are compared only
// when AWS populated the offering field.
var offeringChecks = []struct {
reason string
mismatch func(o types.ReservedInstancesOffering, w offeringWant) bool
reason string
}{
{"zone", func(o types.ReservedInstancesOffering, w offeringWant) bool {
{reason: "zone", mismatch: func(o types.ReservedInstancesOffering, w offeringWant) bool {
return !offeringInZone(o, w.az)
}},
{"class", func(o types.ReservedInstancesOffering, w offeringWant) bool {
{reason: "class", mismatch: func(o types.ReservedInstancesOffering, w offeringWant) bool {
return w.class != "" && o.OfferingClass != w.class
}},
{"tenancy", func(o types.ReservedInstancesOffering, w offeringWant) bool {
{reason: "tenancy", mismatch: func(o types.ReservedInstancesOffering, w offeringWant) bool {
return w.tenancy != "" && o.InstanceTenancy != w.tenancy
}},
{"duration", func(o types.ReservedInstancesOffering, w offeringWant) bool {
{reason: "duration", mismatch: func(o types.ReservedInstancesOffering, w offeringWant) bool {
return w.duration != 0 && aws.ToInt64(o.Duration) != w.duration
}},
{"offering-type", func(o types.ReservedInstancesOffering, w offeringWant) bool {
{reason: "offering-type", mismatch: func(o types.ReservedInstancesOffering, w offeringWant) bool {
return w.offeringType != "" && o.OfferingType != w.offeringType
}},
{"instance-type", func(o types.ReservedInstancesOffering, w offeringWant) bool {
{reason: "instance-type", mismatch: func(o types.ReservedInstancesOffering, w offeringWant) bool {
return w.instanceType != "" && o.InstanceType != "" && string(o.InstanceType) != w.instanceType
}},
{"scope", func(o types.ReservedInstancesOffering, w offeringWant) bool {
{reason: "scope", mismatch: func(o types.ReservedInstancesOffering, w offeringWant) bool {
return w.scope != "" && o.Scope != "" && o.Scope != w.scope
}},
{"product", func(o types.ReservedInstancesOffering, w offeringWant) bool {
{reason: "product", mismatch: func(o types.ReservedInstancesOffering, w offeringWant) bool {
got, want := string(o.ProductDescription), w.productDesc
if got == "" || want == "" {
return false
Expand Down
6 changes: 3 additions & 3 deletions providers/aws/services/rds/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,8 @@ func (c *Client) findOfferingID(ctx context.Context, rec common.Recommendation,

// rdsOfferingPageResult holds the outcome of a single DescribeReservedDBInstancesOfferings page.
type rdsOfferingPageResult struct {
id string // non-empty when an exact match was found on this page
marker *string // pagination cursor for the next page; nil when exhausted
id string // non-empty when an exact match was found on this page
}

// fetchRDSOfferingPage calls DescribeReservedDBInstancesOfferings for one page and
Expand Down Expand Up @@ -519,9 +519,9 @@ func (c *Client) offeringDuration(term string) (duration string, seconds int64,
type rdsOfferingKey struct {
class string
engine string
multiAZ bool
duration int64
offeringType string
duration int64
multiAZ bool
}

func (k rdsOfferingKey) matches(o *types.ReservedDBInstancesOffering) bool {
Expand Down
Loading