Selaa lähdekoodia

add support for assets to use configured labels

Signed-off-by: saweber <saweber@gmail.com>
saweber 3 vuotta sitten
vanhempi
sitoutus
2d81355cab

+ 54 - 19
pkg/kubecost/asset.go

@@ -254,9 +254,13 @@ func AssetToExternalAllocation(asset Asset, aggregateBy []string, labelConfig *L
 // values will key by only those values.
 // Valid values of `aggregateBy` elements are strings which are an `AssetProperty`, and strings prefixed
 // with `"label:"`.
-func key(a Asset, aggregateBy []string) (string, error) {
+func key(a Asset, aggregateBy []string, labelConfig *LabelConfig) (string, error) {
 	var buffer strings.Builder
 
+	if labelConfig == nil {
+		labelConfig = NewLabelConfig()
+	}
+
 	if aggregateBy == nil {
 		aggregateBy = []string{
 			string(AssetProviderProp),
@@ -292,6 +296,16 @@ func key(a Asset, aggregateBy []string) (string, error) {
 			key = a.GetProperties().ProviderID
 		case s == string(AssetNameProp):
 			key = a.GetProperties().Name
+		case s == string(AssetDepartmentProp):
+			key = getKeyFromLabelConfig(a, labelConfig, labelConfig.DepartmentExternalLabel)
+		case s == string(AssetEnvironmentProp):
+			key = getKeyFromLabelConfig(a, labelConfig, labelConfig.EnvironmentExternalLabel)
+		case s == string(AssetOwnerProp):
+			key = getKeyFromLabelConfig(a, labelConfig, labelConfig.OwnerExternalLabel)
+		case s == string(AssetProductProp):
+			key = getKeyFromLabelConfig(a, labelConfig, labelConfig.ProductExternalLabel)
+		case s == string(AssetTeamProp):
+			key = getKeyFromLabelConfig(a, labelConfig, labelConfig.TeamExternalLabel)
 		case strings.HasPrefix(s, "label:"):
 			if labelKey := strings.TrimPrefix(s, "label:"); labelKey != "" {
 				labelVal := a.GetLabels()[labelKey]
@@ -320,8 +334,26 @@ func key(a Asset, aggregateBy []string) (string, error) {
 	return buffer.String(), nil
 }
 
+func getKeyFromLabelConfig(a Asset, labelConfig *LabelConfig, label string) string {
+	labels := a.GetLabels()
+	if labels == nil {
+		return UnallocatedSuffix
+	} else {
+		key := UnallocatedSuffix
+		labelNames := strings.Split(label, ",")
+		for _, labelName := range labelNames {
+			name := labelConfig.Sanitize(labelName)
+			if labelValue, ok := labels[name]; ok {
+				key = labelValue
+				break
+			}
+		}
+		return key
+	}
+}
+
 func GetAssetKey(a Asset, aggregateBy []string) (string, error) {
-	return key(a, aggregateBy)
+	return key(a, aggregateBy, nil)
 }
 
 func toString(a Asset) string {
@@ -2675,7 +2707,7 @@ func NewAssetSet(start, end time.Time, assets ...Asset) *AssetSet {
 	}
 
 	for _, a := range assets {
-		as.Insert(a)
+		as.Insert(a, nil)
 	}
 
 	return as
@@ -2718,7 +2750,7 @@ func (as *AssetSet) AggregateBy(aggregateBy []string, opts *AssetAggregationOpti
 			}
 		}
 		if insert {
-			err := aggSet.Insert(sa)
+			err := aggSet.Insert(sa, opts.LabelConfig)
 			if err != nil {
 				return err
 			}
@@ -2737,7 +2769,7 @@ func (as *AssetSet) AggregateBy(aggregateBy []string, opts *AssetAggregationOpti
 	// Insert each asset into the new set, which will be keyed by the `aggregateBy`
 	// on aggSet, resulting in aggregation.
 	for _, asset := range as.Assets {
-		err := aggSet.Insert(asset)
+		err := aggSet.Insert(asset, opts.LabelConfig)
 		if err != nil {
 			return err
 		}
@@ -2849,13 +2881,13 @@ func (as *AssetSet) End() time.Time {
 // FindMatch attempts to find a match in the AssetSet for the given Asset on
 // the provided Properties and labels. If a match is not found, FindMatch
 // returns nil and a Not Found error.
-func (as *AssetSet) FindMatch(query Asset, aggregateBy []string) (Asset, error) {
-	matchKey, err := key(query, aggregateBy)
+func (as *AssetSet) FindMatch(query Asset, aggregateBy []string, labelConfig *LabelConfig) (Asset, error) {
+	matchKey, err := key(query, aggregateBy, labelConfig)
 	if err != nil {
 		return nil, err
 	}
 	for _, asset := range as.Assets {
-		if k, err := key(asset, aggregateBy); err != nil {
+		if k, err := key(asset, aggregateBy, labelConfig); err != nil {
 			return nil, err
 		} else if k == matchKey {
 			return asset, nil
@@ -2873,7 +2905,7 @@ func (as *AssetSet) FindMatch(query Asset, aggregateBy []string) (Asset, error)
 func (as *AssetSet) ReconciliationMatch(query Asset) (Asset, bool, error) {
 	// Full match means matching on (Category, ProviderID)
 	fullMatchProps := []string{string(AssetCategoryProp), string(AssetProviderIDProp)}
-	fullMatchKey, err := key(query, fullMatchProps)
+	fullMatchKey, err := key(query, fullMatchProps, nil)
 
 	// This should never happen because we are using enumerated Properties,
 	// but the check is here in case that changes
@@ -2883,7 +2915,7 @@ func (as *AssetSet) ReconciliationMatch(query Asset) (Asset, bool, error) {
 
 	// Partial match means matching only on (ProviderID)
 	providerIDMatchProps := []string{string(AssetProviderIDProp)}
-	providerIDMatchKey, err := key(query, providerIDMatchProps)
+	providerIDMatchKey, err := key(query, providerIDMatchProps, nil)
 
 	// This should never happen because we are using enumerated Properties,
 	// but the check is here in case that changes
@@ -2897,13 +2929,13 @@ func (as *AssetSet) ReconciliationMatch(query Asset) (Asset, bool, error) {
 		if asset.Type() == CloudAssetType {
 			continue
 		}
-		if k, err := key(asset, fullMatchProps); err != nil {
+		if k, err := key(asset, fullMatchProps, nil); err != nil {
 			return nil, false, err
 		} else if k == fullMatchKey {
 			log.DedupedInfof(10, "Asset ETL: Reconciliation[rcnw]: ReconcileRange Match: %s", fullMatchKey)
 			return asset, true, nil
 		}
-		if k, err := key(asset, providerIDMatchProps); err != nil {
+		if k, err := key(asset, providerIDMatchProps, nil); err != nil {
 			return nil, false, err
 		} else if k == providerIDMatchKey {
 			// Found a partial match. Save it until after all other options
@@ -2975,7 +3007,7 @@ func (as *AssetSet) Get(key string) (Asset, bool) {
 // Insert inserts the given Asset into the AssetSet, using the AssetSet's
 // configured Properties to determine the key under which the Asset will
 // be inserted.
-func (as *AssetSet) Insert(asset Asset) error {
+func (as *AssetSet) Insert(asset Asset, labelConfig *LabelConfig) error {
 	if as == nil {
 		return fmt.Errorf("cannot Insert into nil AssetSet")
 	}
@@ -2984,8 +3016,10 @@ func (as *AssetSet) Insert(asset Asset) error {
 		as.Assets = map[string]Asset{}
 	}
 
+	// need a label config
+
 	// Determine key into which to Insert the Asset.
-	k, err := key(asset, as.AggregationKeys)
+	k, err := key(asset, as.AggregationKeys, labelConfig)
 	if err != nil {
 		return err
 	}
@@ -3038,14 +3072,14 @@ func (as *AssetSet) Resolution() time.Duration {
 	return as.Window.Duration()
 }
 
-func (as *AssetSet) Set(asset Asset, aggregateBy []string) error {
+func (as *AssetSet) Set(asset Asset, aggregateBy []string, labelConfig *LabelConfig) error {
 	if as.IsEmpty() {
 		as.Assets = map[string]Asset{}
 	}
 
 	// Expand the window to match the AssetSet, then set it
 	asset.ExpandWindow(as.Window)
-	k, err := key(asset, aggregateBy)
+	k, err := key(asset, aggregateBy, labelConfig)
 	if err != nil {
 		return err
 	}
@@ -3113,14 +3147,14 @@ func (as *AssetSet) accumulate(that *AssetSet) (*AssetSet, error) {
 	acc.AggregationKeys = as.AggregationKeys
 
 	for _, asset := range as.Assets {
-		err := acc.Insert(asset)
+		err := acc.Insert(asset, nil)
 		if err != nil {
 			return nil, err
 		}
 	}
 
 	for _, asset := range that.Assets {
-		err := acc.Insert(asset)
+		err := acc.Insert(asset, nil)
 		if err != nil {
 			return nil, err
 		}
@@ -3240,6 +3274,7 @@ func (asr *AssetSetRange) NewAccumulation() (*AssetSet, error) {
 type AssetAggregationOptions struct {
 	SharedHourlyCosts map[string]float64
 	FilterFuncs       []AssetMatchFunc
+	LabelConfig       *LabelConfig
 }
 
 func (asr *AssetSetRange) AggregateBy(aggregateBy []string, opts *AssetAggregationOptions) error {
@@ -3326,7 +3361,7 @@ func (asr *AssetSetRange) InsertRange(that *AssetSetRange) error {
 
 		// Insert each Asset from the given set
 		for _, asset := range thatAS.Assets {
-			err = as.Insert(asset)
+			err = as.Insert(asset, nil)
 			if err != nil {
 				err = fmt.Errorf("error inserting asset: %s", err)
 				continue

+ 5 - 5
pkg/kubecost/asset_test.go

@@ -786,7 +786,7 @@ func TestAssetSet_FindMatch(t *testing.T) {
 	// Assert success of a simple match of Type and ProviderID
 	as = GenerateMockAssetSet(startYesterday)
 	query = NewNode("", "", "gcp-node3", s, e, w)
-	match, err = as.FindMatch(query, []string{string(AssetTypeProp), string(AssetProviderIDProp)})
+	match, err = as.FindMatch(query, []string{string(AssetTypeProp), string(AssetProviderIDProp)}, nil)
 	if err != nil {
 		t.Fatalf("AssetSet.FindMatch: unexpected error: %s", err)
 	}
@@ -794,7 +794,7 @@ func TestAssetSet_FindMatch(t *testing.T) {
 	// Assert error of a simple non-match of Type and ProviderID
 	as = GenerateMockAssetSet(startYesterday)
 	query = NewNode("", "", "aws-node3", s, e, w)
-	match, err = as.FindMatch(query, []string{string(AssetTypeProp), string(AssetProviderIDProp)})
+	match, err = as.FindMatch(query, []string{string(AssetTypeProp), string(AssetProviderIDProp)}, nil)
 	if err == nil {
 		t.Fatalf("AssetSet.FindMatch: expected error (no match); found %s", match)
 	}
@@ -802,7 +802,7 @@ func TestAssetSet_FindMatch(t *testing.T) {
 	// Assert error of matching ProviderID, but not Type
 	as = GenerateMockAssetSet(startYesterday)
 	query = NewCloud(ComputeCategory, "gcp-node3", s, e, w)
-	match, err = as.FindMatch(query, []string{string(AssetTypeProp), string(AssetProviderIDProp)})
+	match, err = as.FindMatch(query, []string{string(AssetTypeProp), string(AssetProviderIDProp)}, nil)
 	if err == nil {
 		t.Fatalf("AssetSet.FindMatch: expected error (no match); found %s", match)
 	}
@@ -833,8 +833,8 @@ func TestAssetSet_InsertMatchingWindow(t *testing.T) {
 	a2.Window = NewClosedWindow(a2WindowStart, a2WindowEnd)
 
 	as := NewAssetSet(setStart, setEnd)
-	as.Insert(a1)
-	as.Insert(a2)
+	as.Insert(a1, nil)
+	as.Insert(a2, nil)
 
 	if as.Length() != 2 {
 		t.Errorf("AS length got %d, expected %d", as.Length(), 2)

+ 25 - 0
pkg/kubecost/assetprops.go

@@ -41,6 +41,21 @@ const (
 
 	// AssetTypeProp describes the type of the Asset
 	AssetTypeProp AssetProperty = "type"
+
+	// AssetDepartmentProp describes the department of the Asset
+	AssetDepartmentProp AssetProperty = "department"
+
+	// AssetEnvironmentProp describes the environment of the Asset
+	AssetEnvironmentProp AssetProperty = "environment"
+
+	// AssetOwnerProp describes the owner of the Asset
+	AssetOwnerProp AssetProperty = "owner"
+
+	// AssetProductProp describes the product of the Asset
+	AssetProductProp AssetProperty = "product"
+
+	// AssetTeamProp describes the team of the Asset
+	AssetTeamProp AssetProperty = "team"
 )
 
 // ParseAssetProperty attempts to parse a string into an AssetProperty
@@ -64,6 +79,16 @@ func ParseAssetProperty(text string) (AssetProperty, error) {
 		return AssetServiceProp, nil
 	case "type":
 		return AssetTypeProp, nil
+	case "department":
+		return AssetDepartmentProp, nil
+	case "environment":
+		return AssetEnvironmentProp, nil
+	case "owner":
+		return AssetOwnerProp, nil
+	case "product":
+		return AssetProductProp, nil
+	case "team":
+		return AssetTeamProp, nil
 	}
 	return AssetNilProp, fmt.Errorf("invalid asset property: %s", text)
 }

+ 6 - 6
pkg/kubecost/diff_test.go

@@ -16,20 +16,20 @@ func TestDiff(t *testing.T) {
 	node1.CPUCost = 10
 	node1b := node1.Clone().(*Node)
 	node1b.CPUCost = 20
-	node1Key, _ := key(node1, nil)
+	node1Key, _ := key(node1, nil, nil)
 	node2 := NewNode("node2", "cluster1", "123abc", start, end, window1)
 	node2.CPUCost = 100
 	node2b := node2.Clone().(*Node)
 	node2b.CPUCost = 105
-	node2Key, _ := key(node2, nil)
+	node2Key, _ := key(node2, nil, nil)
 	node3 := NewNode("node3", "cluster1", "123abc", start, end, window1)
-	node3Key, _ := key(node3, nil)
+	node3Key, _ := key(node3, nil, nil)
 	node4 := NewNode("node4", "cluster1", "123abc", start, end, window1)
-	node4Key, _ := key(node4, nil)
+	node4Key, _ := key(node4, nil, nil)
 	disk1 := NewDisk("disk1", "cluster1", "123abc", start, end, window1)
-	disk1Key, _ := key(disk1, nil)
+	disk1Key, _ := key(disk1, nil, nil)
 	disk2 := NewDisk("disk2", "cluster1", "123abc", start, end, window1)
-	disk2Key, _ := key(disk2, nil)
+	disk2Key, _ := key(disk2, nil, nil)
 
 	cases := map[string]struct {
 		inputAssetsBefore []Asset

+ 2 - 0
pkg/kubecost/query.go

@@ -67,6 +67,7 @@ type AssetQueryOptions struct {
 	IncludeCloud            bool
 	SharedHourlyCosts       map[string]float64
 	Step                    time.Duration
+	LabelConfig             *LabelConfig
 }
 
 // CloudUsageQueryOptions define optional parameters for querying a Store
@@ -76,6 +77,7 @@ type CloudUsageQueryOptions struct {
 	Compute      bool
 	FilterFuncs  []CloudUsageMatchFunc
 	FilterValues CloudUsageFilter
+	LabelConfig  *LabelConfig
 }
 
 type CloudUsageFilter struct {