Prechádzať zdrojové kódy

add v2.1 filters to for cloud cost

Signed-off-by: Sean Holcomb <seanholcomb@gmail.com>
Sean Holcomb 2 rokov pred
rodič
commit
0db0c60814

+ 14 - 0
pkg/filter21/cloudcost/fields.go

@@ -0,0 +1,14 @@
+package cloudcost
+
+// CloudCostField is an enum that represents CloudCost specific fields that can be filtered
+type CloudCostField string
+
+const (
+	FieldInvoiceEntity CloudCostField = "invoiceEntity"
+	FieldAccount       CloudCostField = "account"
+	FieldProvider      CloudCostField = "provider"
+	FieldProviderID    CloudCostField = "providerID"
+	FieldCategory      CloudCostField = "category"
+	FieldService       CloudCostField = "service"
+	FieldLabel         CloudCostField = "label"
+)

+ 42 - 0
pkg/filter21/cloudcost/parser.go

@@ -0,0 +1,42 @@
+package cloudcost
+
+import "github.com/opencost/opencost/pkg/filter21/ast"
+
+// a slice of all the cloud costs field instances the lexer should recognize as
+// valid left-hand comparators
+var cloudCostFilterFields []*ast.Field = []*ast.Field{
+	ast.NewField(FieldInvoiceEntity),
+	ast.NewField(FieldAccount),
+	ast.NewField(FieldProvider),
+	ast.NewField(FieldProviderID),
+	ast.NewField(FieldCategory),
+	ast.NewField(FieldService),
+	ast.NewMapField(FieldLabel),
+}
+
+// fieldMap is a lazily loaded mapping from CloudAggregationField to ast.Field
+var fieldMap map[CloudCostField]*ast.Field
+
+// DefaultFieldByName returns only default cloud cost filter fields by name.
+func DefaultFieldByName(field CloudCostField) *ast.Field {
+	if fieldMap == nil {
+		fieldMap = make(map[CloudCostField]*ast.Field, len(cloudCostFilterFields))
+		for _, f := range cloudCostFilterFields {
+			ff := *f
+			fieldMap[CloudCostField(ff.Name)] = &ff
+		}
+	}
+
+	if af, ok := fieldMap[field]; ok {
+		afcopy := *af
+		return &afcopy
+	}
+
+	return nil
+}
+
+// NewCloudCostFilterParser creates a new `ast.FilterParser` implementation
+// which uses CloudCost specific fields
+func NewCloudCostFilterParser() ast.FilterParser {
+	return ast.NewFilterParser(cloudCostFilterFields)
+}

+ 33 - 0
pkg/kubecost/cloudcost.go

@@ -6,6 +6,8 @@ import (
 	"time"
 
 	"github.com/opencost/opencost/pkg/filter"
+	filter21 "github.com/opencost/opencost/pkg/filter21"
+	"github.com/opencost/opencost/pkg/filter21/ast"
 	"github.com/opencost/opencost/pkg/log"
 )
 
@@ -287,6 +289,37 @@ func (ccs *CloudCostSet) Filter(filters filter.Filter[*CloudCost]) *CloudCostSet
 	return result
 }
 
+func (ccs *CloudCostSet) Filter21(filters filter21.Filter) (*CloudCostSet, error) {
+	if ccs == nil {
+		return nil, nil
+	}
+
+	if filters == nil {
+		return ccs.Clone(), nil
+	}
+
+	compiler := NewCloudCostMatchCompiler()
+	var err error
+	matcher, err := compiler.Compile(filters)
+	if err != nil {
+		return ccs.Clone(), fmt.Errorf("compiling filter '%s': %w", ast.ToPreOrderShortString(filters), err)
+	}
+
+	if matcher == nil {
+		return ccs.Clone(), fmt.Errorf("unexpected nil filter")
+	}
+
+	result := ccs.cloneSet()
+
+	for _, cc := range ccs.CloudCosts {
+		if matcher.Matches(cc) {
+			result.Insert(cc.Clone())
+		}
+	}
+
+	return result, nil
+}
+
 // Insert adds a CloudCost to a CloudCostSet using its AggregationProperties and LabelConfig
 // to determine the key where it will be inserted
 func (ccs *CloudCostSet) Insert(cc *CloudCost) error {

+ 80 - 0
pkg/kubecost/cloudcostmatcher.go

@@ -0,0 +1,80 @@
+package kubecost
+
+import (
+	"fmt"
+
+	"github.com/opencost/opencost/pkg/filter21/ast"
+	ccfilter "github.com/opencost/opencost/pkg/filter21/cloudcost"
+	"github.com/opencost/opencost/pkg/filter21/matcher"
+	"github.com/opencost/opencost/pkg/filter21/transform"
+)
+
+// CloudCostMatcher is a matcher implementation for CloudCost instances,
+// compiled using the matcher.MatchCompiler for cloud costs.
+type CloudCostMatcher matcher.Matcher[*CloudCost]
+
+// NewCloudCostMatchCompiler creates a new instance of a
+// matcher.MatchCompiler[*CloudCost] which can be used to compile filter.Filter
+// ASTs into matcher.Matcher[*CloudCost] implementations.
+//
+// If storage interfaces every support querying natively by alias (e.g. if a
+// data store contained a "product" attribute on an CloudCost row), that should
+// be handled by a purpose-built AST compiler.
+func NewCloudCostMatchCompiler() *matcher.MatchCompiler[*CloudCost] {
+	passes := []transform.CompilerPass{}
+
+	passes = append(passes,
+		transform.UnallocatedReplacementPass(),
+	)
+	return matcher.NewMatchCompiler(
+		cloudCostFieldMap,
+		cloudCostSliceFieldMap,
+		cloudCostMapFieldMap,
+		passes...,
+	)
+}
+
+// Maps fields from a cloud cost to a string value based on an identifier
+func cloudCostFieldMap(cc *CloudCost, identifier ast.Identifier) (string, error) {
+	if cc == nil {
+		return "", fmt.Errorf("cannot map to nil cloud cost")
+	}
+	if cc.Properties == nil {
+		return "", fmt.Errorf("cannot map to nil properties")
+	}
+	if identifier.Field == nil {
+		return "", fmt.Errorf("cannot map field from identifier with nil field")
+	}
+	switch ccfilter.CloudCostField(identifier.Field.Name) {
+	case ccfilter.FieldInvoiceEntity:
+		return cc.Properties.InvoiceEntityID, nil
+	case ccfilter.FieldAccount:
+		return cc.Properties.AccountID, nil
+	case ccfilter.FieldProvider:
+		return cc.Properties.Provider, nil
+	case ccfilter.FieldProviderID:
+		return cc.Properties.ProviderID, nil
+	case ccfilter.FieldCategory:
+		return cc.Properties.Category, nil
+	case ccfilter.FieldService:
+		return cc.Properties.Service, nil
+	case ccfilter.FieldLabel:
+		return cc.Properties.Labels[identifier.Key], nil
+	}
+
+	return "", fmt.Errorf("Failed to find string identifier on CloudCost: %s", identifier.Field.Name)
+}
+
+// Maps slice fields from an asset to a []string value based on an identifier
+func cloudCostSliceFieldMap(cc *CloudCost, identifier ast.Identifier) ([]string, error) {
+	return nil, fmt.Errorf("Cloud Cost have no slice fields")
+}
+
+// Maps map fields from a cloud cost to a map[string]string value based on an identifier
+func cloudCostMapFieldMap(cc *CloudCost, identifier ast.Identifier) (map[string]string, error) {
+	switch ccfilter.CloudCostField(identifier.Field.Name) {
+	case ccfilter.FieldLabel:
+		return cc.Properties.Labels, nil
+	}
+	return nil, fmt.Errorf("Failed to find map[string]string identifier on CloudCost: %s", identifier.Field.Name)
+}