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
195 changes: 195 additions & 0 deletions internal/archera/recommendation_match.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package archera

import (
"errors"
"math/big"
"strings"
"unicode"

"github.com/google/uuid"
)

const (
awsSPYearSeconds int64 = 31536000
awsSPThreeYearSeconds int64 = 94608000
)

type RecommendationIdentity struct {
Provider, SourceAccountID, Service, PlanType, OfferingID string
Region, TopLevelRegion, InstanceFamily, Payment string
NativeDurationSeconds int64
HourlyCommitment *big.Rat
}

type LineItemIdentity struct {
ID, PlanID, OfferID, Provider, SourceAccountID, ProviderOfferingID string
Type, PlanType, Region, InstanceFamily, Payment string
DurationSeconds, SelectedCommitment *big.Rat
}

type ComparisonIdentity struct {
Region *string
IsCurrent bool
LineItemID, CurrentOfferID, Provider, CurrentType, CurrentPayment string
CurrentSelectedAmount *big.Rat
}

type MatchStatus string

const (
MatchUnavailable MatchStatus = "unavailable"
ResourceAssociated MatchStatus = "resource_associated"
)

type RecommendationMatch struct {
Status MatchStatus
LineItemID, Reason, AmountProvenance, EconomicStatus string
}

func MatchRecommendation(planID string, rec *RecommendationIdentity, rows []ComparisonIdentity, items []LineItemIdentity) (RecommendationMatch, error) {
unavailable := RecommendationMatch{Status: MatchUnavailable, Reason: "identity_unproven"}
if !validRecommendation(rec) {
return unavailable, nil
}
index, err := comparisonIndex(planID, rows, items)
if err != nil {
return unavailable, err
}
if index == nil {
return unavailable, nil
}
var selected *LineItemIdentity
for i := range items {
if !sameResource(rec, &items[i]) {
continue
}
if selected != nil {
unavailable.Reason = "ambiguous_line_items"
return unavailable, nil
}
selected = &items[i]
}
if selected == nil {
return unavailable, nil
}
row := index[strings.ToLower(selected.ID)]
if !sameCurrentBasis(rec, row) {
unavailable.Reason = "comparison_basis_unproven"
return unavailable, nil
}
result := RecommendationMatch{
Status: ResourceAssociated, LineItemID: row.LineItemID,
AmountProvenance: "stored_decimal_only", EconomicStatus: "unavailable_source_precision",
}
return result, nil
}

func validRecommendation(r *RecommendationIdentity) bool {
if r == nil {
return false
}
if r.Provider != "aws" || r.Service != "savings-plans-ec2instance" || r.PlanType != "EC2Instance" {
return false
}
if r.NativeDurationSeconds != awsSPYearSeconds && r.NativeDurationSeconds != awsSPThreeYearSeconds {
return false
}
return validDimensions(r) && validPayment(r.Payment) && sameAmount(r.HourlyCommitment, r.HourlyCommitment)
}

func validDimensions(r *RecommendationIdentity) bool {
for _, value := range []string{r.SourceAccountID, r.OfferingID, r.Region, r.InstanceFamily} {
if value == "" {
return false
}
}
return usableSource(r.SourceAccountID) && (r.TopLevelRegion == "" || r.TopLevelRegion == r.Region)
}

func validPayment(payment string) bool {
switch payment {
case "no_upfront", "partial_upfront", "all_upfront":
return true
default:
return false
}
}

func sameAmount(a, b *big.Rat) bool {
return a != nil && b != nil && a.Sign() > 0 && b.Sign() > 0 && a.Cmp(b) == 0
}

type resourceIdentity struct {
provider, source, offering, plan, region, family, payment string
}

func sameResource(r *RecommendationIdentity, item *LineItemIdentity) bool {
want := resourceIdentity{r.Provider, r.SourceAccountID, r.OfferingID, r.PlanType, r.Region, r.InstanceFamily, r.Payment}
got := resourceIdentity{item.Provider, item.SourceAccountID, item.ProviderOfferingID, item.PlanType, item.Region, item.InstanceFamily, item.Payment}
return want == got && item.Type == "savings_plan" && item.DurationSeconds != nil &&
item.DurationSeconds.Cmp(new(big.Rat).SetInt64(r.NativeDurationSeconds)) == 0 &&
sameAmount(r.HourlyCommitment, item.SelectedCommitment)
}

func sameCurrentBasis(r *RecommendationIdentity, row *ComparisonIdentity) bool {
return row != nil && row.IsCurrent && row.Region != nil && *row.Region == r.Region && row.Provider == r.Provider && row.CurrentType == "savings_plan" &&
row.CurrentPayment == r.Payment && sameAmount(r.HourlyCommitment, row.CurrentSelectedAmount)
}

func comparisonIndex(planID string, rows []ComparisonIdentity, items []LineItemIdentity) (map[string]*ComparisonIdentity, error) {
if !validMatchUUID(planID) {
return nil, nil
}
lineItems := make(map[string]*LineItemIdentity, len(items))
for i := range items {
if !validLineReference(planID, &items[i]) {
return nil, nil
}
id := strings.ToLower(items[i].ID)
if lineItems[id] != nil {
return nil, errors.New("duplicate Archera line-item reference")
}
lineItems[id] = &items[i]
}
index := make(map[string]*ComparisonIdentity, len(rows))
for i := range rows {
if !validComparisonReference(&rows[i], lineItems) {
return nil, nil
}
id := strings.ToLower(rows[i].LineItemID)
if index[id] != nil {
return nil, errors.New("duplicate Archera comparison reference")
}
index[id] = &rows[i]
}
return index, nil
}

func usableSource(source string) bool {
for _, r := range source {
if unicode.IsSpace(r) || unicode.IsControl(r) || unicode.In(r, unicode.Cf) {
return false
}
}
return true
}

func validMatchUUID(id string) bool {
if len(id) != 36 {
return false
}
_, err := uuid.Parse(id)
return err == nil
}

func validLineReference(planID string, item *LineItemIdentity) bool {
return validMatchUUID(item.ID) && validMatchUUID(item.OfferID) && validMatchUUID(item.PlanID) && strings.EqualFold(planID, item.PlanID)
}

func validComparisonReference(row *ComparisonIdentity, items map[string]*LineItemIdentity) bool {
if !validMatchUUID(row.LineItemID) || !validMatchUUID(row.CurrentOfferID) {
return false
}
item := items[strings.ToLower(row.LineItemID)]
return item != nil && strings.EqualFold(row.CurrentOfferID, item.OfferID)
}
190 changes: 190 additions & 0 deletions internal/archera/recommendation_match_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package archera

import (
"encoding/json"
"math/big"
"strings"
"testing"

"github.com/stretchr/testify/require"
)

const (
matchPlan = "baadf00d-ffff-4aba-8ada-abcdefabcdef"
matchItem = "caadf00d-ffff-4aba-8ada-abcdefabcdef"
matchOffer = "daadf00d-ffff-4aba-8ada-abcdefabcdef"
)

func matchFixture() (*RecommendationIdentity, []ComparisonIdentity, []LineItemIdentity) {
r := &RecommendationIdentity{
Provider: "aws", SourceAccountID: "source-account-a", Service: "savings-plans-ec2instance",
PlanType: "EC2Instance", OfferingID: "provider-offer-a", Region: "us-east-1",
InstanceFamily: "m5", Payment: "no_upfront", NativeDurationSeconds: 31536000,
HourlyCommitment: big.NewRat(1, 2),
}
region := r.Region
row := ComparisonIdentity{Region: &region, IsCurrent: true,
LineItemID: matchItem, CurrentOfferID: matchOffer, Provider: "aws",
CurrentType: "savings_plan", CurrentPayment: "no_upfront", CurrentSelectedAmount: big.NewRat(1, 2),
}
item := LineItemIdentity{
ID: matchItem, PlanID: matchPlan, OfferID: matchOffer, Provider: "aws",
SourceAccountID: r.SourceAccountID, ProviderOfferingID: r.OfferingID, Type: "savings_plan",
PlanType: r.PlanType, Region: r.Region, InstanceFamily: r.InstanceFamily, Payment: r.Payment,
DurationSeconds: big.NewRat(31536000, 1), SelectedCommitment: big.NewRat(1, 2),
}
return r, []ComparisonIdentity{row}, []LineItemIdentity{item}
}

func TestMatchRecommendationAssociatesOnlyProvenResourceIdentity(t *testing.T) {
rec, rows, items := matchFixture()
items[0].ID = strings.ToUpper(matchItem)
items[0].PlanID = strings.ToUpper(matchPlan)
items[0].OfferID = strings.ToUpper(matchOffer)
before, marshalErr := json.Marshal([]any{rec, rows, items})
require.NoError(t, marshalErr)
result, err := MatchRecommendation(matchPlan, rec, rows, items)
require.NoError(t, err)
require.Equal(t, ResourceAssociated, result.Status)
require.Equal(t, matchItem, result.LineItemID)
require.Equal(t, "stored_decimal_only", result.AmountProvenance)
require.Equal(t, "unavailable_source_precision", result.EconomicStatus)
require.Equal(t, "1/2", rec.HourlyCommitment.RatString())
require.Equal(t, "1/2", items[0].SelectedCommitment.RatString())
after, marshalErr := json.Marshal([]any{rec, rows, items})
require.NoError(t, marshalErr)
require.Equal(t, before, after)
rec.NativeDurationSeconds = 94608000
items[0].DurationSeconds = big.NewRat(94608000, 1)
result, err = MatchRecommendation(matchPlan, rec, rows, items)
require.NoError(t, err)
require.Equal(t, ResourceAssociated, result.Status)
}

func TestMatchRecommendationRefusesUnprovenAssociation(t *testing.T) {
tests := []struct {
name string
change func(*RecommendationIdentity, *ComparisonIdentity, *LineItemIdentity)
}{
{"unusable_source", func(r *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) {
r.SourceAccountID = "source\naccount"
i.SourceAccountID = r.SourceAccountID
}},
{"unknown_source", func(r *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) {
r.SourceAccountID = ""
i.SourceAccountID = ""
}},
{"collector_is_not_source", func(_ *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) {
i.SourceAccountID = "collector-account"
}},
{"source_case", func(_ *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) {
i.SourceAccountID = "SOURCE-account-a"
}},
{"provider", func(_ *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) { i.Provider = "gcp" }},
{"offering", func(_ *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) {
i.ProviderOfferingID = "other-offer"
}},
{"missing_offering", func(r *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) {
r.OfferingID = ""
i.ProviderOfferingID = ""
}},
{"unsupported_service", func(r *RecommendationIdentity, _ *ComparisonIdentity, _ *LineItemIdentity) {
r.Service = "savings-plans-compute"
}},
{"invented_type", func(_ *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) {
i.Type = "aws/savingsplan/EC2Instance"
}},
{"plan_type", func(_ *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) { i.PlanType = "Compute" }},
{"family", func(_ *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) { i.InstanceFamily = "m6" }},
{"missing_details_region", func(r *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) {
r.Region = ""
i.Region = ""
}},
{"contradictory_top_region", func(r *RecommendationIdentity, _ *ComparisonIdentity, _ *LineItemIdentity) {
r.TopLevelRegion = "us-west-2"
}},
{"native_duration", func(_ *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) {
i.DurationSeconds = big.NewRat(94608000, 1)
}},
{"payment", func(_ *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) { i.Payment = "all_upfront" }},
{"amount_mismatch", func(_ *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) {
i.SelectedCommitment = big.NewRat(1, 1)
}},
{"missing_hourly", func(r *RecommendationIdentity, _ *ComparisonIdentity, _ *LineItemIdentity) { r.HourlyCommitment = nil }},
{"zero_hourly", func(r *RecommendationIdentity, _ *ComparisonIdentity, _ *LineItemIdentity) {
r.HourlyCommitment = new(big.Rat)
}},
{"current_amount", func(_ *RecommendationIdentity, c *ComparisonIdentity, _ *LineItemIdentity) {
c.CurrentSelectedAmount = big.NewRat(2, 1)
}},
{"offering_case", func(_ *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) {
i.ProviderOfferingID = "PROVIDER-offer-a"
}},
{"missing_family", func(r *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) {
r.InstanceFamily = ""
i.InstanceFamily = ""
}},
{"unsupported_native_term", func(r *RecommendationIdentity, _ *ComparisonIdentity, i *LineItemIdentity) {
r.NativeDurationSeconds = 123
i.DurationSeconds = big.NewRat(123, 1)
}},
{"current_provider", func(_ *RecommendationIdentity, c *ComparisonIdentity, _ *LineItemIdentity) { c.Provider = "gcp" }},
{"current_unit_type", func(_ *RecommendationIdentity, c *ComparisonIdentity, _ *LineItemIdentity) { c.CurrentType = "ri" }},
{"current_flag", func(_ *RecommendationIdentity, c *ComparisonIdentity, _ *LineItemIdentity) { c.IsCurrent = false }},
{"current_region", func(_ *RecommendationIdentity, c *ComparisonIdentity, i *LineItemIdentity) {
c.Region = &i.InstanceFamily
}},
{"missing_current_region", func(_ *RecommendationIdentity, c *ComparisonIdentity, _ *LineItemIdentity) { c.Region = nil }},
{"current_offer", func(_ *RecommendationIdentity, c *ComparisonIdentity, _ *LineItemIdentity) {
c.CurrentOfferID = "faadf00d-ffff-4aba-8ada-abcdefabcdef"
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r, rows, items := matchFixture()
tt.change(r, &rows[0], &items[0])
got, err := MatchRecommendation(matchPlan, r, rows, items)
require.NoError(t, err)
require.Equal(t, MatchUnavailable, got.Status)
require.Empty(t, got.LineItemID)
})
}
}

func TestMatchRecommendationRejectsAmbiguousVendorRows(t *testing.T) {
r, rows, items := matchFixture()
rows = append(rows, rows[0])
_, err := MatchRecommendation(matchPlan, r, rows, items)
require.Error(t, err)
r, rows, items = matchFixture()
items = append(items, items[0])
items[1].ID = "eaadf00d-ffff-4aba-8ada-abcdefabcdef"
rows = append(rows, rows[0])
rows[1].LineItemID = items[1].ID
got, err := MatchRecommendation(matchPlan, r, rows, items)
require.NoError(t, err)
require.Equal(t, MatchUnavailable, got.Status)
require.Empty(t, got.LineItemID)
}

func TestMatchRecommendationRejectsEqualMalformedUUIDReferences(t *testing.T) {
for _, bad := range []string{"", "not-a-uuid", strings.ReplaceAll(matchItem, "-", ""), "zzzzzzzz-zzzz-4zzz-8zzz-zzzzzzzzzzzz"} {
for _, field := range []string{"plan", "item", "offer"} {
t.Run(field+"_"+bad, func(t *testing.T) {
r, rows, items := matchFixture()
plan := matchPlan
switch field {
case "plan":
plan, items[0].PlanID = bad, bad
case "item":
rows[0].LineItemID, items[0].ID = bad, bad
case "offer":
rows[0].CurrentOfferID, items[0].OfferID = bad, bad
}
got, err := MatchRecommendation(plan, r, rows, items)
require.NoError(t, err)
require.Equal(t, MatchUnavailable, got.Status)
})
}
}
}
Loading