Skip to content
Open
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
78 changes: 53 additions & 25 deletions server/aws/savingsplans/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,54 +152,82 @@ func (h *Handler) describeSavingsPlans(w http.ResponseWriter, r *http.Request) {
}

f := newPlanFilter(req.SavingsPlanIDs, req.SavingsPlanArns, req.States, req.Filters)
plans := h.store.describe(f)

plans, next, err := paginate(h.store.describe(f), req.MaxResults, req.NextToken)
if err != nil {
writeErr(w, err)

return
}

out := make([]map[string]any, 0, len(plans))
for _, p := range plans {
out = append(out, planToWire(p))
}

wire.WriteJSON(w, map[string]any{"savingsPlans": out})
writePage(w, "savingsPlans", out, next)
}

func (h *Handler) describeOfferings(w http.ResponseWriter, r *http.Request) {
var req struct {
OfferingIDs []string `json:"offeringIds"`
PlanTypes []string `json:"planTypes"`
Filters []wireFilter `json:"filters"`
ProductType string `json:"productType"`
MaxResults int `json:"maxResults"`
NextToken string `json:"nextToken"`
OfferingIDs []string `json:"offeringIds"`
PaymentOptions []string `json:"paymentOptions"`
ProductType string `json:"productType"`
PlanTypes []string `json:"planTypes"`
Durations []int64 `json:"durations"`
Currencies []string `json:"currencies"`
Descriptions []string `json:"descriptions"`
ServiceCodes []string `json:"serviceCodes"`
UsageTypes []string `json:"usageTypes"`
Operations []string `json:"operations"`
Filters []wireFilter `json:"filters"`
MaxResults int `json:"maxResults"`
NextToken string `json:"nextToken"`
}

if !wire.DecodeJSON(w, r, &req) {
return
}

ids := toSet(req.OfferingIDs)
types := toSet(req.PlanTypes)
f := &offeringFilter{
ids: toSet(req.OfferingIDs),
planTypes: toSet(req.PlanTypes),
paymentOptions: toSet(req.PaymentOptions),
durations: toSet(int64sToStrings(req.Durations)),
currencies: toSet(req.Currencies),
descriptions: toSet(req.Descriptions),
serviceCodes: toSet(req.ServiceCodes),
usageTypes: toSet(req.UsageTypes),
operations: toSet(req.Operations),
productType: req.ProductType,
attrs: req.Filters,
}

out := make([]map[string]any, 0, len(h.store.offerings))
offerings, next, err := paginate(h.store.describeOfferings(f), req.MaxResults, req.NextToken)
if err != nil {
writeErr(w, err)

for i := range h.store.offerings {
o := &h.store.offerings[i]
return
}

if len(ids) > 0 {
if _, ok := ids[o.id]; !ok {
continue
}
}
out := make([]map[string]any, 0, len(offerings))
for _, o := range offerings {
out = append(out, offeringToWire(o))
}

if len(types) > 0 {
if _, ok := types[o.planType]; !ok {
continue
}
}
writePage(w, "searchResults", out, next)
}

out = append(out, offeringToWire(o))
// writePage writes a describe response: the result list under key, plus
// nextToken only when more results remain (AWS documents it as null on the last
// page).
func writePage(w http.ResponseWriter, key string, items []map[string]any, next string) {
resp := map[string]any{key: items}
if next != "" {
resp["nextToken"] = next
}

wire.WriteJSON(w, map[string]any{"searchResults": out})
wire.WriteJSON(w, resp)
}

func (h *Handler) describeOfferingRates(w http.ResponseWriter, r *http.Request) {
Expand Down
123 changes: 123 additions & 0 deletions server/aws/savingsplans/offerings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package savingsplans

import "strconv"

// Offering filter / property names (SavingsPlanOfferingFilterAttribute and
// SavingsPlanOfferingPropertyKey share the same enum: region | instanceFamily).
// https://docs.aws.amazon.com/savingsplans/latest/APIReference/API_SavingsPlanOfferingFilterElement.html
const (
offeringPropRegion = "region"
offeringPropInstanceFamily = "instanceFamily"
)

// offeringFilter is the parsed DescribeSavingsPlansOfferings predicate. Every
// populated dimension must match (AND across dimensions, OR within a list).
// https://docs.aws.amazon.com/savingsplans/latest/APIReference/API_DescribeSavingsPlansOfferings.html
type offeringFilter struct {
ids map[string]struct{}
planTypes map[string]struct{}
paymentOptions map[string]struct{}
durations map[string]struct{}
currencies map[string]struct{}
descriptions map[string]struct{}
serviceCodes map[string]struct{}
usageTypes map[string]struct{}
operations map[string]struct{}
productType string
attrs []wireFilter
}

// matches reports whether offering o satisfies the filter.
func (f *offeringFilter) matches(o *offering) bool {
dims := []struct {
set map[string]struct{}
got string
}{
{f.ids, o.id},
{f.planTypes, o.planType},
{f.paymentOptions, o.paymentOption},
{f.durations, strconv.FormatInt(o.durationSecs, 10)},
{f.currencies, currencyUSD},
{f.descriptions, o.description},
{f.serviceCodes, o.serviceCode},
{f.usageTypes, o.usageType},
{f.operations, o.operation},
}

for _, d := range dims {
if !inSet(d.set, d.got) {
return false
}
}

if f.productType != "" && !containsValue(o.productTypes, f.productType) {
return false
}

for _, attr := range f.attrs {
if !offeringAttrMatches(o, attr) {
return false
}
}

return true
}

// offeringAttrMatches applies one filters[] element. The filter names are the
// offering property keys, so an offering matches when it carries that property
// with one of the requested values; an offering without the property (e.g. a
// region-spanning Compute offering under a region filter) does not match. An
// unknown filter name is tolerated (matches), like DescribeSavingsPlans.
func offeringAttrMatches(o *offering, attr wireFilter) bool {
switch attr.Name {
case offeringPropRegion, offeringPropInstanceFamily:
got, ok := offeringProperties(o)[attr.Name]

return ok && containsValue(attr.Values, got)
default:
return true
}
}

// offeringProperties returns the SavingsPlanOfferingProperty set for o: region
// and instanceFamily for a region/family-scoped EC2Instance offering, none for
// region-spanning offerings.
func offeringProperties(o *offering) map[string]string {
props := map[string]string{}

if o.region != "" {
props[offeringPropRegion] = o.region
}

if o.ec2Family != "" {
props[offeringPropInstanceFamily] = o.ec2Family
}

return props
}

// inSet reports whether v is in set; an empty set means the dimension is
// unfiltered.
func inSet(set map[string]struct{}, v string) bool {
if len(set) == 0 {
return true
}

_, ok := set[v]

return ok
}

// int64sToStrings renders durations so they share the string-set matcher.
func int64sToStrings(in []int64) []string {
if len(in) == 0 {
return nil
}

out := make([]string, len(in))
for i, v := range in {
out[i] = strconv.FormatInt(v, 10)
}

return out
}
Loading
Loading