Browse Source

Merge pull request #1985 from opencost/mmd/filter-v2.1-alias-compiler

Add Alias support to v2.1 Allocation filters
Michael Dresser 2 years ago
parent
commit
07087be8f7

+ 16 - 16
pkg/filter21/allocation/fields.go

@@ -8,17 +8,17 @@ type AllocationField string
 // Allocation value
 // does not enforce exhaustive pattern matching on "enum" types.
 const (
-	AllocationFieldClusterID      AllocationField = "cluster"
-	AllocationFieldNode           AllocationField = "node"
-	AllocationFieldNamespace      AllocationField = "namespace"
-	AllocationFieldControllerKind AllocationField = "controllerKind"
-	AllocationFieldControllerName AllocationField = "controllerName"
-	AllocationFieldPod            AllocationField = "pod"
-	AllocationFieldContainer      AllocationField = "container"
-	AllocationFieldProvider       AllocationField = "provider"
-	AllocationFieldServices       AllocationField = "services"
-	AllocationFieldLabel          AllocationField = "label"
-	AllocationFieldAnnotation     AllocationField = "annotation"
+	FieldClusterID      AllocationField = "cluster"
+	FieldNode           AllocationField = "node"
+	FieldNamespace      AllocationField = "namespace"
+	FieldControllerKind AllocationField = "controllerKind"
+	FieldControllerName AllocationField = "controllerName"
+	FieldPod            AllocationField = "pod"
+	FieldContainer      AllocationField = "container"
+	FieldProvider       AllocationField = "provider"
+	FieldServices       AllocationField = "services"
+	FieldLabel          AllocationField = "label"
+	FieldAnnotation     AllocationField = "annotation"
 )
 
 // AllocationAlias represents an alias field type for allocations.
@@ -29,9 +29,9 @@ const (
 type AllocationAlias string
 
 const (
-	AllocationAliasDepartment  AllocationAlias = "department"
-	AllocationAliasEnvironment AllocationAlias = "environment"
-	AllocationAliasOwner       AllocationAlias = "owner"
-	AllocationAliasProduct     AllocationAlias = "product"
-	AllocationAliasTeam        AllocationAlias = "team"
+	AliasDepartment  AllocationAlias = "department"
+	AliasEnvironment AllocationAlias = "environment"
+	AliasOwner       AllocationAlias = "owner"
+	AliasProduct     AllocationAlias = "product"
+	AliasTeam        AllocationAlias = "team"
 )

+ 16 - 16
pkg/filter21/allocation/parser.go

@@ -5,22 +5,22 @@ import "github.com/opencost/opencost/pkg/filter21/ast"
 // a slice of all the allocation field instances the lexer should recognize as
 // valid left-hand comparators
 var allocationFilterFields []*ast.Field = []*ast.Field{
-	ast.NewField(AllocationFieldClusterID),
-	ast.NewField(AllocationFieldNode),
-	ast.NewField(AllocationFieldNamespace),
-	ast.NewField(AllocationFieldControllerName),
-	ast.NewField(AllocationFieldControllerKind),
-	ast.NewField(AllocationFieldContainer),
-	ast.NewField(AllocationFieldPod),
-	ast.NewField(AllocationFieldProvider),
-	ast.NewAliasField(AllocationAliasDepartment),
-	ast.NewAliasField(AllocationAliasEnvironment),
-	ast.NewAliasField(AllocationAliasOwner),
-	ast.NewAliasField(AllocationAliasProduct),
-	ast.NewAliasField(AllocationAliasTeam),
-	ast.NewSliceField(AllocationFieldServices),
-	ast.NewMapField(AllocationFieldLabel),
-	ast.NewMapField(AllocationFieldAnnotation),
+	ast.NewField(FieldClusterID),
+	ast.NewField(FieldNode),
+	ast.NewField(FieldNamespace),
+	ast.NewField(FieldControllerName),
+	ast.NewField(FieldControllerKind),
+	ast.NewField(FieldContainer),
+	ast.NewField(FieldPod),
+	ast.NewField(FieldProvider),
+	ast.NewAliasField(AliasDepartment),
+	ast.NewAliasField(AliasEnvironment),
+	ast.NewAliasField(AliasOwner),
+	ast.NewAliasField(AliasProduct),
+	ast.NewAliasField(AliasTeam),
+	ast.NewSliceField(FieldServices),
+	ast.NewMapField(FieldLabel),
+	ast.NewMapField(FieldAnnotation),
 }
 
 // fieldMap is a lazily loaded mapping from AllocationField to ast.Field

+ 6 - 0
pkg/filter21/ast/tree.go

@@ -30,6 +30,12 @@ func (id *Identifier) Equal(ident Identifier) bool {
 
 // String returns the string representation for the Identifier
 func (id *Identifier) String() string {
+	if id == nil {
+		return "<nil>"
+	}
+	if id.Field == nil {
+		return "<nil field>"
+	}
 	s := id.Field.Name
 	if id.Key != "" {
 		s += "[" + id.Key + "]"

+ 43 - 4
pkg/filter21/ast/walker.go

@@ -29,10 +29,49 @@ const (
 	TraversalStateExit
 )
 
-// PreOrderTraversal accepts a root `FilterNode` and calls the f callback on each leaf node it traverses.
-// When entering "group" leaf nodes (leaf nodes which contain other leaf nodes), a TraversalStateEnter/Exit
-// will be includes to denote each depth. In short, the callback will be executed twice for each "group" op,
-// once before entering, and once bofore exiting.
+// TransformLeaves produces a new tree, leaving non-leaf nodes (e.g. And, Or)
+// intact and replacing leaf nodes (e.g. Equals, Contains) with the result of
+// calling leafTransformer(node).
+func TransformLeaves(node FilterNode, transformer func(FilterNode) FilterNode) FilterNode {
+	if node == nil {
+		return nil
+	}
+
+	// For group ops, we need to execute the callback with an Enter,
+	// recursively call traverse, then execute the callback with an Exit.
+	switch n := node.(type) {
+	case *NotOp:
+		return &NotOp{
+			Operand: TransformLeaves(n.Operand, transformer),
+		}
+	case *AndOp:
+		var newOperands []FilterNode
+		for _, o := range n.Operands {
+			newOperands = append(newOperands, TransformLeaves(o, transformer))
+		}
+		return &AndOp{
+			Operands: newOperands,
+		}
+	case *OrOp:
+		var newOperands []FilterNode
+		for _, o := range n.Operands {
+			newOperands = append(newOperands, TransformLeaves(o, transformer))
+		}
+		return &OrOp{
+			Operands: newOperands,
+		}
+
+	// Remaining nodes are assumed to be leaves
+	default:
+		return transformer(node)
+	}
+}
+
+// PreOrderTraversal accepts a root `FilterNode` and calls the f callback on
+// each leaf node it traverses. When entering "group" leaf nodes (leaf nodes
+// which contain other leaf nodes), a TraversalStateEnter/Exit will be includes
+// to denote each depth. In short, the callback will be executed twice for each
+// "group" op, once before entering, and once bofore exiting.
 func PreOrderTraversal(node FilterNode, f func(FilterNode, TraversalState)) {
 	if node == nil {
 		return

+ 52 - 0
pkg/filter21/ast/walker_test.go

@@ -0,0 +1,52 @@
+package ast
+
+import (
+	"fmt"
+)
+
+func ExampleTransformLeaves() {
+	originalTree := &AndOp{
+		Operands: []FilterNode{
+			&EqualOp{
+				Left: Identifier{
+					Field: &Field{
+						Name: "field1",
+					},
+					Key: "foo",
+				},
+				Right: "bar",
+			},
+
+			&EqualOp{
+				Left: Identifier{
+					Field: &Field{
+						Name: "field2",
+					},
+				},
+				Right: "bar",
+			},
+		},
+	}
+
+	// This transformer applies "Not" to all leaves
+	transformFunc := func(node FilterNode) FilterNode {
+		switch concrete := node.(type) {
+		case *AndOp, *OrOp, *NotOp:
+			panic("Leaf transformer should not be called on non-leaf nodes")
+		default:
+			return &NotOp{Operand: concrete}
+		}
+	}
+
+	newTree := TransformLeaves(originalTree, transformFunc)
+	fmt.Println(ToPreOrderString(newTree))
+	// Output:
+	// And {
+	//   Not {
+	//     Equals { Left: field1[foo], Right: bar }
+	//   }
+	//   Not {
+	//     Equals { Left: field2, Right: bar }
+	//   }
+	// }
+}

+ 7 - 1
pkg/filter21/matcher/compiler.go

@@ -1,6 +1,8 @@
 package matcher
 
 import (
+	"fmt"
+
 	"github.com/opencost/opencost/pkg/filter21/ast"
 	"github.com/opencost/opencost/pkg/filter21/transform"
 	"github.com/opencost/opencost/pkg/filter21/util"
@@ -50,7 +52,11 @@ func NewMatchCompiler[T any](
 // which can be used to match T instances dynamically.
 func (mc *MatchCompiler[T]) Compile(filter ast.FilterNode) (Matcher[T], error) {
 	// apply compiler passes on parsed ast
-	filter = transform.ApplyAll(filter, mc.passes)
+	var err error
+	filter, err = transform.ApplyAll(filter, mc.passes)
+	if err != nil {
+		return nil, fmt.Errorf("applying compiler passes: %w", err)
+	}
 
 	// if the root node is a void op, return an allpass
 	if _, ok := filter.(*ast.VoidOp); ok {

+ 15 - 15
pkg/filter21/ops/ops_test.go

@@ -14,12 +14,12 @@ func TestBasicOpsBuilder(t *testing.T) {
 
 	filterTree := ops.And(
 		ops.Or(
-			ops.Eq(allocation.AllocationFieldNamespace, "kubecost"),
-			ops.Eq(allocation.AllocationFieldClusterID, "cluster-one"),
+			ops.Eq(allocation.FieldNamespace, "kubecost"),
+			ops.Eq(allocation.FieldClusterID, "cluster-one"),
 		),
-		ops.NotContains(allocation.AllocationFieldServices, "service-a"),
-		ops.NotEq(ops.WithKey(allocation.AllocationFieldLabel, "app"), "cost-analyzer"),
-		ops.Contains(allocation.AllocationFieldLabel, "foo"),
+		ops.NotContains(allocation.FieldServices, "service-a"),
+		ops.NotEq(ops.WithKey(allocation.FieldLabel, "app"), "cost-analyzer"),
+		ops.Contains(allocation.FieldLabel, "foo"),
 	)
 
 	otherTree, err := parser.Parse(`
@@ -41,12 +41,12 @@ func TestBasicOpsBuilder(t *testing.T) {
 func TestLongFormComparison(t *testing.T) {
 	filterTree := ops.And(
 		ops.Or(
-			ops.Eq(allocation.AllocationFieldNamespace, "kubecost"),
-			ops.Eq(allocation.AllocationFieldClusterID, "cluster-one"),
+			ops.Eq(allocation.FieldNamespace, "kubecost"),
+			ops.Eq(allocation.FieldClusterID, "cluster-one"),
 		),
-		ops.NotContains(allocation.AllocationFieldServices, "service-a"),
-		ops.NotEq(ops.WithKey(allocation.AllocationFieldLabel, "app"), "cost-analyzer"),
-		ops.Contains(allocation.AllocationFieldLabel, "foo"),
+		ops.NotContains(allocation.FieldServices, "service-a"),
+		ops.NotEq(ops.WithKey(allocation.FieldLabel, "app"), "cost-analyzer"),
+		ops.Contains(allocation.FieldLabel, "foo"),
 	)
 
 	comparisonTree := &ast.AndOp{
@@ -55,14 +55,14 @@ func TestLongFormComparison(t *testing.T) {
 				Operands: []ast.FilterNode{
 					&ast.EqualOp{
 						Left: ast.Identifier{
-							Field: ast.NewField(allocation.AllocationFieldNamespace),
+							Field: ast.NewField(allocation.FieldNamespace),
 							Key:   "",
 						},
 						Right: "kubecost",
 					},
 					&ast.EqualOp{
 						Left: ast.Identifier{
-							Field: ast.NewField(allocation.AllocationFieldClusterID),
+							Field: ast.NewField(allocation.FieldClusterID),
 							Key:   "",
 						},
 						Right: "cluster-one",
@@ -72,7 +72,7 @@ func TestLongFormComparison(t *testing.T) {
 			&ast.NotOp{
 				Operand: &ast.ContainsOp{
 					Left: ast.Identifier{
-						Field: ast.NewSliceField(allocation.AllocationFieldServices),
+						Field: ast.NewSliceField(allocation.FieldServices),
 						Key:   "",
 					},
 					Right: "service-a",
@@ -81,7 +81,7 @@ func TestLongFormComparison(t *testing.T) {
 			&ast.NotOp{
 				Operand: &ast.EqualOp{
 					Left: ast.Identifier{
-						Field: ast.NewMapField(allocation.AllocationFieldLabel),
+						Field: ast.NewMapField(allocation.FieldLabel),
 						Key:   "app",
 					},
 					Right: "cost-analyzer",
@@ -89,7 +89,7 @@ func TestLongFormComparison(t *testing.T) {
 			},
 			&ast.ContainsOp{
 				Left: ast.Identifier{
-					Field: ast.NewMapField(allocation.AllocationFieldLabel),
+					Field: ast.NewMapField(allocation.FieldLabel),
 					Key:   "",
 				},
 				Right: "foo",

+ 22 - 11
pkg/filter21/transform/pass.go

@@ -1,29 +1,40 @@
 package transform
 
-import "github.com/opencost/opencost/pkg/filter21/ast"
+import (
+	"fmt"
+
+	"github.com/opencost/opencost/pkg/filter21/ast"
+)
 
 // CompilerPass is an interface which defines an implementation capable of
 // accepting an input AST and making optimizations or changes, and returning
 // a new (or the existing) AST.
 type CompilerPass interface {
 	// Exec executes the pass on the provided AST. This method may either return
-	// a new AST or the existing modified AST. Note that the parameter to this method
-	// may be changed directly.
-	Exec(filter ast.FilterNode) ast.FilterNode
+	// a new AST or the existing modified AST. Note that the parameter to this
+	// method may be changed directly.
+	Exec(filter ast.FilterNode) (ast.FilterNode, error)
 }
 
-// ApplyAll applies all the compiler passes serially and returns the resulting tree. This
-// method copies the passes AST before executing the compiler passes.
-func ApplyAll(filter ast.FilterNode, passes []CompilerPass) ast.FilterNode {
+// func CompilerPass(transformFunc func(ast.FilterNode) (ast.FilterNode, error)) (ast.FilterNode, error) {
+// }
+
+// ApplyAll applies all the compiler passes serially and returns the resulting
+// tree. This method copies the passes AST before executing the compiler passes.
+func ApplyAll(filter ast.FilterNode, passes []CompilerPass) (ast.FilterNode, error) {
 	// return the input filter if there are no passes to run
 	if len(passes) == 0 {
-		return filter
+		return filter, nil
 	}
 
 	// Clone the filter first, then apply the passes
 	var f ast.FilterNode = ast.Clone(filter)
-	for _, pass := range passes {
-		f = pass.Exec(f)
+	for i, pass := range passes {
+		var err error
+		f, err = pass.Exec(f)
+		if err != nil {
+			return nil, fmt.Errorf("compiler pass %d (%+v) failed: %w", i, pass, err)
+		}
 	}
-	return f
+	return f, nil
 }

+ 2 - 2
pkg/filter21/transform/promlabels.go

@@ -20,7 +20,7 @@ type promKeySanitizePass struct{}
 // Exec executes the pass on the provided AST. This method may either return
 // a new AST or modify and return the AST parameter. The parameter into this
 // method may be changed directly.
-func (pks *promKeySanitizePass) Exec(filter ast.FilterNode) ast.FilterNode {
+func (pks *promKeySanitizePass) Exec(filter ast.FilterNode) (ast.FilterNode, error) {
 	ast.PreOrderTraversal(filter, func(fn ast.FilterNode, ts ast.TraversalState) {
 		switch n := fn.(type) {
 		case *ast.EqualOp:
@@ -51,7 +51,7 @@ func (pks *promKeySanitizePass) Exec(filter ast.FilterNode) ast.FilterNode {
 			}
 		}
 	})
-	return filter
+	return filter, nil
 }
 
 // sanitizes the identifier

+ 2 - 2
pkg/filter21/transform/unallocated.go

@@ -17,7 +17,7 @@ type unallocReplacePass struct{}
 // Exec executes the pass on the provided AST. This method may either return
 // a new AST or modify and return the AST parameter. The parameter into this
 // method may be changed directly.
-func (pks *unallocReplacePass) Exec(filter ast.FilterNode) ast.FilterNode {
+func (pks *unallocReplacePass) Exec(filter ast.FilterNode) (ast.FilterNode, error) {
 	ast.PreOrderTraversal(filter, func(fn ast.FilterNode, ts ast.TraversalState) {
 		switch n := fn.(type) {
 		case *ast.EqualOp:
@@ -30,7 +30,7 @@ func (pks *unallocReplacePass) Exec(filter ast.FilterNode) ast.FilterNode {
 			n.Right = replaceUnallocated(n.Right)
 		}
 	})
-	return filter
+	return filter, nil
 }
 
 // replaces unallocated with empty string if valid

+ 2 - 2
pkg/kubecost/allocation.go

@@ -1137,7 +1137,7 @@ func (as *AllocationSet) AggregateBy(aggregateBy []string, options *AllocationAg
 	if options.Filter == nil {
 		filter = &matcher.AllPass[*Allocation]{}
 	} else {
-		compiler := NewAllocationMatchCompiler()
+		compiler := NewAllocationMatchCompiler(options.LabelConfig)
 		var err error
 		filter, err = compiler.Compile(options.Filter)
 		if err != nil {
@@ -1735,7 +1735,7 @@ func computeShareCoeffs(aggregateBy []string, options *AllocationAggregationOpti
 	if options.Filter == nil {
 		filter = &matcher.AllPass[*Allocation]{}
 	} else {
-		compiler := NewAllocationMatchCompiler()
+		compiler := NewAllocationMatchCompiler(options.LabelConfig)
 		var err error
 		filter, err = compiler.Compile(options.Filter)
 		if err != nil {

+ 7 - 7
pkg/kubecost/allocation_test.go

@@ -10,7 +10,7 @@ import (
 
 	"github.com/davecgh/go-spew/spew"
 	filter21 "github.com/opencost/opencost/pkg/filter21"
-	allocfilter "github.com/opencost/opencost/pkg/filter21/allocation"
+	afilter "github.com/opencost/opencost/pkg/filter21/allocation"
 	"github.com/opencost/opencost/pkg/filter21/ops"
 	"github.com/opencost/opencost/pkg/log"
 	"github.com/opencost/opencost/pkg/util"
@@ -18,8 +18,8 @@ import (
 	"github.com/opencost/opencost/pkg/util/timeutil"
 )
 
-var filterParser = allocfilter.NewAllocationFilterParser()
-var matcherCompiler = NewAllocationMatchCompiler()
+var filterParser = afilter.NewAllocationFilterParser()
+var matcherCompiler = NewAllocationMatchCompiler(nil)
 
 // useful for creating filters on the fly when testing. panics
 // on parse errors!
@@ -3210,7 +3210,7 @@ func Test_AggregateByService_UnmountedLBs(t *testing.T) {
 	set.Insert(idle)
 
 	set.AggregateBy([]string{AllocationServiceProp}, &AllocationAggregationOptions{
-		Filter: ops.Contains(allocfilter.AllocationFieldServices, "nginx-plus-nginx-ingress"),
+		Filter: ops.Contains(afilter.FieldServices, "nginx-plus-nginx-ingress"),
 	})
 
 	for _, alloc := range set.Allocations {
@@ -3438,7 +3438,7 @@ func Test_DetermineSharingName(t *testing.T) {
 }
 
 func TestIsFilterEmptyTrue(t *testing.T) {
-	compiler := NewAllocationMatchCompiler()
+	compiler := NewAllocationMatchCompiler(nil)
 	matcher, err := compiler.Compile(nil)
 	if err != nil {
 		t.Fatalf("compiling nil filter: %s", err)
@@ -3451,8 +3451,8 @@ func TestIsFilterEmptyTrue(t *testing.T) {
 }
 
 func TestIsFilterEmptyFalse(t *testing.T) {
-	compiler := NewAllocationMatchCompiler()
-	matcher, err := compiler.Compile(ops.Eq(allocfilter.AllocationFieldClusterID, "test"))
+	compiler := NewAllocationMatchCompiler(nil)
+	matcher, err := compiler.Compile(ops.Eq(afilter.FieldClusterID, "test"))
 	if err != nil {
 		t.Fatalf("compiling nil filter: %s", err)
 	}

+ 107 - 58
pkg/kubecost/allocationfilter_test.go

@@ -4,12 +4,20 @@ import (
 	"testing"
 
 	filter21 "github.com/opencost/opencost/pkg/filter21"
-	allocfilter "github.com/opencost/opencost/pkg/filter21/allocation"
+	afilter "github.com/opencost/opencost/pkg/filter21/allocation"
 	"github.com/opencost/opencost/pkg/filter21/ast"
 	"github.com/opencost/opencost/pkg/filter21/ops"
 )
 
 func Test_AllocationFilterCondition_Matches(t *testing.T) {
+	labelConfig := &LabelConfig{
+		DepartmentLabel:  "keydepartment",
+		EnvironmentLabel: "keyenvironment",
+		OwnerLabel:       "keyowner",
+		ProductLabel:     "keyproduct",
+		TeamLabel:        "keyteam",
+	}
+
 	cases := []struct {
 		name   string
 		a      *Allocation
@@ -24,7 +32,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Cluster: "cluster-one",
 				},
 			},
-			filter:   ops.Eq(allocfilter.AllocationFieldClusterID, "cluster-one"),
+			filter:   ops.Eq(afilter.FieldClusterID, "cluster-one"),
 			expected: true,
 		},
 		{
@@ -34,7 +42,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Cluster: "cluster-one",
 				},
 			},
-			filter:   ops.ContainsPrefix(allocfilter.AllocationFieldClusterID, "cluster"),
+			filter:   ops.ContainsPrefix(afilter.FieldClusterID, "cluster"),
 			expected: true,
 		},
 		{
@@ -44,7 +52,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Cluster: "k8s-one",
 				},
 			},
-			filter: ops.ContainsPrefix(allocfilter.AllocationFieldClusterID, "cluster"),
+			filter: ops.ContainsPrefix(afilter.FieldClusterID, "cluster"),
 
 			expected: false,
 		},
@@ -55,7 +63,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Cluster: "",
 				},
 			},
-			filter:   ops.ContainsPrefix(allocfilter.AllocationFieldClusterID, ""),
+			filter:   ops.ContainsPrefix(afilter.FieldClusterID, ""),
 			expected: true,
 		},
 		{
@@ -65,7 +73,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Cluster: "abc",
 				},
 			},
-			filter:   ops.ContainsPrefix(allocfilter.AllocationFieldClusterID, ""),
+			filter:   ops.ContainsPrefix(afilter.FieldClusterID, ""),
 			expected: true,
 		},
 		{
@@ -75,7 +83,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Node: "node123",
 				},
 			},
-			filter:   ops.Eq(allocfilter.AllocationFieldNode, "node123"),
+			filter:   ops.Eq(afilter.FieldNode, "node123"),
 			expected: true,
 		},
 		{
@@ -85,7 +93,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Namespace: "kube-system",
 				},
 			},
-			filter:   ops.NotEq(allocfilter.AllocationFieldNamespace, "kube-system"),
+			filter:   ops.NotEq(afilter.FieldNamespace, "kube-system"),
 			expected: false,
 		},
 		{
@@ -95,7 +103,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Namespace: "kube-system",
 				},
 			},
-			filter:   ops.NotEq(allocfilter.AllocationFieldNamespace, UnallocatedSuffix),
+			filter:   ops.NotEq(afilter.FieldNamespace, UnallocatedSuffix),
 			expected: true,
 		},
 		{
@@ -105,7 +113,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Namespace: "",
 				},
 			},
-			filter:   ops.NotEq(allocfilter.AllocationFieldNamespace, UnallocatedSuffix),
+			filter:   ops.NotEq(afilter.FieldNamespace, UnallocatedSuffix),
 			expected: false,
 		},
 		{
@@ -115,7 +123,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Namespace: "",
 				},
 			},
-			filter:   ops.Eq(allocfilter.AllocationFieldNamespace, UnallocatedSuffix),
+			filter:   ops.Eq(afilter.FieldNamespace, UnallocatedSuffix),
 			expected: true,
 		},
 		{
@@ -125,7 +133,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					ControllerKind: "deployment", // We generally store controller kinds as all lowercase
 				},
 			},
-			filter:   ops.Eq(allocfilter.AllocationFieldControllerKind, "deployment"),
+			filter:   ops.Eq(afilter.FieldControllerKind, "deployment"),
 			expected: true,
 		},
 		{
@@ -135,7 +143,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Controller: "kc-cost-analyzer",
 				},
 			},
-			filter:   ops.Eq(allocfilter.AllocationFieldControllerName, "kc-cost-analyzer"),
+			filter:   ops.Eq(afilter.FieldControllerName, "kc-cost-analyzer"),
 			expected: true,
 		},
 		{
@@ -145,7 +153,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Pod: "pod-123 UID-ABC",
 				},
 			},
-			filter:   ops.Eq(allocfilter.AllocationFieldPod, "pod-123 UID-ABC"),
+			filter:   ops.Eq(afilter.FieldPod, "pod-123 UID-ABC"),
 			expected: true,
 		},
 		{
@@ -155,7 +163,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Container: "cost-model",
 				},
 			},
-			filter:   ops.Eq(allocfilter.AllocationFieldContainer, "cost-model"),
+			filter:   ops.Eq(afilter.FieldContainer, "cost-model"),
 			expected: true,
 		},
 		{
@@ -167,7 +175,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					},
 				},
 			},
-			filter:   ops.Eq(ops.WithKey(allocfilter.AllocationFieldLabel, "app"), "foo"),
+			filter:   ops.Eq(ops.WithKey(afilter.FieldLabel, "app"), "foo"),
 			expected: true,
 		},
 		{
@@ -179,7 +187,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					},
 				},
 			},
-			filter:   ops.Eq(ops.WithKey(allocfilter.AllocationFieldLabel, "app"), "foo"),
+			filter:   ops.Eq(ops.WithKey(afilter.FieldLabel, "app"), "foo"),
 			expected: false,
 		},
 		{
@@ -191,7 +199,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					},
 				},
 			},
-			filter:   ops.Eq(ops.WithKey(allocfilter.AllocationFieldLabel, "app"), "foo"),
+			filter:   ops.Eq(ops.WithKey(afilter.FieldLabel, "app"), "foo"),
 			expected: false,
 		},
 		{
@@ -203,7 +211,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					},
 				},
 			},
-			filter:   ops.Eq(ops.WithKey(allocfilter.AllocationFieldLabel, "app"), UnallocatedSuffix),
+			filter:   ops.Eq(ops.WithKey(afilter.FieldLabel, "app"), UnallocatedSuffix),
 			expected: true,
 		},
 		{
@@ -215,7 +223,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					},
 				},
 			},
-			filter:   ops.Eq(ops.WithKey(allocfilter.AllocationFieldLabel, "app"), UnallocatedSuffix),
+			filter:   ops.Eq(ops.WithKey(afilter.FieldLabel, "app"), UnallocatedSuffix),
 			expected: false,
 		},
 		{
@@ -227,7 +235,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					},
 				},
 			},
-			filter:   ops.NotEq(ops.WithKey(allocfilter.AllocationFieldLabel, "app"), UnallocatedSuffix),
+			filter:   ops.NotEq(ops.WithKey(afilter.FieldLabel, "app"), UnallocatedSuffix),
 			expected: false,
 		},
 		{
@@ -239,7 +247,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					},
 				},
 			},
-			filter:   ops.NotEq(ops.WithKey(allocfilter.AllocationFieldLabel, "app"), UnallocatedSuffix),
+			filter:   ops.NotEq(ops.WithKey(afilter.FieldLabel, "app"), UnallocatedSuffix),
 			expected: true,
 		},
 		{
@@ -251,7 +259,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					},
 				},
 			},
-			filter:   ops.NotEq(ops.WithKey(allocfilter.AllocationFieldLabel, "app"), "foo"),
+			filter:   ops.NotEq(ops.WithKey(afilter.FieldLabel, "app"), "foo"),
 			expected: true,
 		},
 		{
@@ -263,7 +271,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					},
 				},
 			},
-			filter:   ops.Eq(ops.WithKey(allocfilter.AllocationFieldAnnotation, "prom_modified_name"), "testing123"),
+			filter:   ops.Eq(ops.WithKey(afilter.FieldAnnotation, "prom_modified_name"), "testing123"),
 			expected: true,
 		},
 		{
@@ -275,7 +283,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					},
 				},
 			},
-			filter:   ops.Eq(ops.WithKey(allocfilter.AllocationFieldAnnotation, "app"), "foo"),
+			filter:   ops.Eq(ops.WithKey(afilter.FieldAnnotation, "app"), "foo"),
 			expected: false,
 		},
 		{
@@ -287,7 +295,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					},
 				},
 			},
-			filter:   ops.Eq(ops.WithKey(allocfilter.AllocationFieldAnnotation, "app"), "foo"),
+			filter:   ops.Eq(ops.WithKey(afilter.FieldAnnotation, "app"), "foo"),
 			expected: false,
 		},
 		{
@@ -299,7 +307,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					},
 				},
 			},
-			filter:   ops.NotEq(ops.WithKey(allocfilter.AllocationFieldAnnotation, "app"), "foo"),
+			filter:   ops.NotEq(ops.WithKey(afilter.FieldAnnotation, "app"), "foo"),
 			expected: true,
 		},
 		{
@@ -309,7 +317,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Namespace: "",
 				},
 			},
-			filter:   ops.Eq(allocfilter.AllocationFieldNamespace, UnallocatedSuffix),
+			filter:   ops.Eq(afilter.FieldNamespace, UnallocatedSuffix),
 			expected: true,
 		},
 		{
@@ -319,7 +327,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Services: []string{"serv1", "serv2"},
 				},
 			},
-			filter:   ops.Contains(allocfilter.AllocationFieldServices, "serv2"),
+			filter:   ops.Contains(afilter.FieldServices, "serv2"),
 			expected: true,
 		},
 		{
@@ -329,7 +337,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Services: []string{"serv1", "serv2"},
 				},
 			},
-			filter:   ops.Contains(allocfilter.AllocationFieldServices, "serv3"),
+			filter:   ops.Contains(afilter.FieldServices, "serv3"),
 			expected: false,
 		},
 		{
@@ -339,7 +347,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Services: []string{"serv1", "serv2"},
 				},
 			},
-			filter:   ops.NotContains(allocfilter.AllocationFieldServices, "serv3"),
+			filter:   ops.NotContains(afilter.FieldServices, "serv3"),
 			expected: true,
 		},
 		{
@@ -349,7 +357,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Services: []string{"serv1", "serv2"},
 				},
 			},
-			filter:   ops.NotContains(allocfilter.AllocationFieldServices, "serv2"),
+			filter:   ops.NotContains(afilter.FieldServices, "serv2"),
 			expected: false,
 		},
 		{
@@ -359,7 +367,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Services: []string{"serv1", "serv2"},
 				},
 			},
-			filter:   ops.NotContains(allocfilter.AllocationFieldServices, UnallocatedSuffix),
+			filter:   ops.NotContains(afilter.FieldServices, UnallocatedSuffix),
 			expected: true,
 		},
 		{
@@ -369,7 +377,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Services: []string{},
 				},
 			},
-			filter:   ops.NotContains(allocfilter.AllocationFieldServices, UnallocatedSuffix),
+			filter:   ops.NotContains(afilter.FieldServices, UnallocatedSuffix),
 			expected: false,
 		},
 		{
@@ -379,7 +387,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Services: []string{"serv1", "serv2"},
 				},
 			},
-			filter:   ops.ContainsPrefix(allocfilter.AllocationFieldServices, "serv"),
+			filter:   ops.ContainsPrefix(afilter.FieldServices, "serv"),
 			expected: true,
 		},
 		{
@@ -389,7 +397,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Services: []string{"foo", "bar"},
 				},
 			},
-			filter:   ops.ContainsPrefix(allocfilter.AllocationFieldServices, "serv"),
+			filter:   ops.ContainsPrefix(afilter.FieldServices, "serv"),
 			expected: false,
 		},
 		{
@@ -399,7 +407,7 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Services: []string{"serv1", "serv2"},
 				},
 			},
-			filter:   ops.Contains(allocfilter.AllocationFieldServices, UnallocatedSuffix),
+			filter:   ops.Contains(afilter.FieldServices, UnallocatedSuffix),
 			expected: false,
 		},
 		{
@@ -409,13 +417,53 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 					Services: []string{},
 				},
 			},
-			filter:   ops.Contains(allocfilter.AllocationFieldServices, UnallocatedSuffix),
+			filter:   ops.Contains(afilter.FieldServices, UnallocatedSuffix),
+			expected: true,
+		},
+		{
+			name: `department equals -> true`,
+			a: &Allocation{
+				Properties: &AllocationProperties{
+					Labels: AllocationLabels{
+						"keydepartment": "foo",
+					},
+				},
+			},
+			// The ops package doesn't handle alias construction quite right,
+			// so we construct it more manually here
+			filter: &ast.EqualOp{
+				Left: ast.Identifier{
+					Field: ast.NewAliasField(afilter.AliasDepartment),
+				},
+				Right: "foo",
+			},
+			expected: true,
+		},
+		{
+			name: `product != unallocated -> true`,
+			a: &Allocation{
+				Properties: &AllocationProperties{
+					Annotations: AllocationAnnotations{
+						"keyproduct": "foo",
+					},
+				},
+			},
+			// The ops package doesn't handle alias construction quite right,
+			// so we construct it more manually here
+			filter: &ast.NotOp{
+				Operand: &ast.EqualOp{
+					Left: ast.Identifier{
+						Field: ast.NewAliasField(afilter.AliasDepartment),
+					},
+					Right: UnallocatedSuffix,
+				},
+			},
 			expected: true,
 		},
 	}
 
 	for _, c := range cases {
-		compiler := NewAllocationMatchCompiler()
+		compiler := NewAllocationMatchCompiler(labelConfig)
 		compiled, err := compiler.Compile(c.filter)
 		if err != nil {
 			t.Fatalf("err compiling filter '%s': %s", ast.ToPreOrderShortString(c.filter), err)
@@ -537,7 +585,7 @@ func Test_AllocationFilterContradiction_Matches(t *testing.T) {
 
 	for _, c := range cases {
 		filter := &ast.ContradictionOp{}
-		compiler := NewAllocationMatchCompiler()
+		compiler := NewAllocationMatchCompiler(nil)
 		compiled, err := compiler.Compile(filter)
 		if err != nil {
 			t.Fatalf("err compiling filter '%s': %s", ast.ToPreOrderShortString(filter), err)
@@ -549,6 +597,7 @@ func Test_AllocationFilterContradiction_Matches(t *testing.T) {
 		}
 	}
 }
+
 func Test_AllocationFilterAnd_Matches(t *testing.T) {
 	cases := []struct {
 		name   string
@@ -568,8 +617,8 @@ func Test_AllocationFilterAnd_Matches(t *testing.T) {
 				},
 			},
 			filter: ops.And(
-				ops.Eq(ops.WithKey(allocfilter.AllocationFieldLabel, "app"), "foo"),
-				ops.Eq(allocfilter.AllocationFieldNamespace, "kubecost"),
+				ops.Eq(ops.WithKey(afilter.FieldLabel, "app"), "foo"),
+				ops.Eq(afilter.FieldNamespace, "kubecost"),
 			),
 			expected: true,
 		},
@@ -584,8 +633,8 @@ func Test_AllocationFilterAnd_Matches(t *testing.T) {
 				},
 			},
 			filter: ops.And(
-				ops.Eq(ops.WithKey(allocfilter.AllocationFieldLabel, "app"), "foo"),
-				ops.Eq(allocfilter.AllocationFieldNamespace, "kubecost"),
+				ops.Eq(ops.WithKey(afilter.FieldLabel, "app"), "foo"),
+				ops.Eq(afilter.FieldNamespace, "kubecost"),
 			),
 			expected: false,
 		},
@@ -600,8 +649,8 @@ func Test_AllocationFilterAnd_Matches(t *testing.T) {
 				},
 			},
 			filter: ops.And(
-				ops.Eq(ops.WithKey(allocfilter.AllocationFieldLabel, "app"), "foo"),
-				ops.Eq(allocfilter.AllocationFieldNamespace, "kubecost"),
+				ops.Eq(ops.WithKey(afilter.FieldLabel, "app"), "foo"),
+				ops.Eq(afilter.FieldNamespace, "kubecost"),
 			),
 			expected: false,
 		},
@@ -616,8 +665,8 @@ func Test_AllocationFilterAnd_Matches(t *testing.T) {
 				},
 			},
 			filter: ops.And(
-				ops.Eq(ops.WithKey(allocfilter.AllocationFieldLabel, "app"), "foo"),
-				ops.Eq(allocfilter.AllocationFieldNamespace, "kubecost"),
+				ops.Eq(ops.WithKey(afilter.FieldLabel, "app"), "foo"),
+				ops.Eq(afilter.FieldNamespace, "kubecost"),
 			),
 			expected: false,
 		},
@@ -637,7 +686,7 @@ func Test_AllocationFilterAnd_Matches(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		compiler := NewAllocationMatchCompiler()
+		compiler := NewAllocationMatchCompiler(nil)
 		compiled, err := compiler.Compile(c.filter)
 		if err != nil {
 			t.Fatalf("err compiling filter '%s': %s", ast.ToPreOrderShortString(c.filter), err)
@@ -669,8 +718,8 @@ func Test_AllocationFilterOr_Matches(t *testing.T) {
 				},
 			},
 			filter: ops.Or(
-				ops.Eq(ops.WithKey(allocfilter.AllocationFieldLabel, "app"), "foo"),
-				ops.Eq(allocfilter.AllocationFieldNamespace, "kubecost"),
+				ops.Eq(ops.WithKey(afilter.FieldLabel, "app"), "foo"),
+				ops.Eq(afilter.FieldNamespace, "kubecost"),
 			),
 			expected: true,
 		},
@@ -685,8 +734,8 @@ func Test_AllocationFilterOr_Matches(t *testing.T) {
 				},
 			},
 			filter: ops.Or(
-				ops.Eq(ops.WithKey(allocfilter.AllocationFieldLabel, "app"), "foo"),
-				ops.Eq(allocfilter.AllocationFieldNamespace, "kubecost"),
+				ops.Eq(ops.WithKey(afilter.FieldLabel, "app"), "foo"),
+				ops.Eq(afilter.FieldNamespace, "kubecost"),
 			),
 			expected: true,
 		},
@@ -701,8 +750,8 @@ func Test_AllocationFilterOr_Matches(t *testing.T) {
 				},
 			},
 			filter: ops.Or(
-				ops.Eq(ops.WithKey(allocfilter.AllocationFieldLabel, "app"), "foo"),
-				ops.Eq(allocfilter.AllocationFieldNamespace, "kubecost"),
+				ops.Eq(ops.WithKey(afilter.FieldLabel, "app"), "foo"),
+				ops.Eq(afilter.FieldNamespace, "kubecost"),
 			),
 			expected: true,
 		},
@@ -717,15 +766,15 @@ func Test_AllocationFilterOr_Matches(t *testing.T) {
 				},
 			},
 			filter: ops.Or(
-				ops.Eq(ops.WithKey(allocfilter.AllocationFieldLabel, "app"), "foo"),
-				ops.Eq(allocfilter.AllocationFieldNamespace, "kubecost"),
+				ops.Eq(ops.WithKey(afilter.FieldLabel, "app"), "foo"),
+				ops.Eq(afilter.FieldNamespace, "kubecost"),
 			),
 			expected: false,
 		},
 	}
 
 	for _, c := range cases {
-		compiler := NewAllocationMatchCompiler()
+		compiler := NewAllocationMatchCompiler(nil)
 		compiled, err := compiler.Compile(c.filter)
 		if err != nil {
 			t.Fatalf("err compiling filter '%s': %s", ast.ToPreOrderShortString(c.filter), err)

+ 202 - 23
pkg/kubecost/allocationmatcher.go

@@ -3,9 +3,10 @@ package kubecost
 import (
 	"fmt"
 
-	allocationfilter "github.com/opencost/opencost/pkg/filter21/allocation"
+	afilter "github.com/opencost/opencost/pkg/filter21/allocation"
 	"github.com/opencost/opencost/pkg/filter21/ast"
 	"github.com/opencost/opencost/pkg/filter21/matcher"
+	"github.com/opencost/opencost/pkg/filter21/ops"
 	"github.com/opencost/opencost/pkg/filter21/transform"
 )
 
@@ -13,40 +14,61 @@ import (
 // compiled using the matcher.MatchCompiler for allocations.
 type AllocationMatcher matcher.Matcher[*Allocation]
 
-// NewAllocationMatchCompiler creates a new instance of a matcher.MatchCompiler[*Allocation]
-// which can be used to compile filter.Filter ASTs into matcher.Matcher[*Allocation]
-// implementations.
-func NewAllocationMatchCompiler() *matcher.MatchCompiler[*Allocation] {
+// NewAllocationMatchCompiler creates a new instance of a
+// matcher.MatchCompiler[*Allocation] which can be used to compile filter.Filter
+// ASTs into matcher.Matcher[*Allocation] implementations.
+//
+// If the label config is nil, the compiler will fail to compile alias filters
+// if any are present in the AST.
+//
+// If storage interfaces every support querying natively by alias (e.g. if a
+// data store contained a "product" attribute on an Allocation row), that should
+// be handled by a purpose-built AST compiler.
+func NewAllocationMatchCompiler(labelConfig *LabelConfig) *matcher.MatchCompiler[*Allocation] {
+	passes := []transform.CompilerPass{}
+
+	// The label config pass should be the first pass
+	if labelConfig != nil {
+		passes = append(passes, NewAliasPass(*labelConfig))
+	}
+
+	passes = append(passes,
+		transform.PrometheusKeySanitizePass(),
+		transform.UnallocatedReplacementPass(),
+	)
 	return matcher.NewMatchCompiler(
 		allocationFieldMap,
 		allocationSliceFieldMap,
 		allocationMapFieldMap,
-		transform.PrometheusKeySanitizePass(),
-		transform.UnallocatedReplacementPass())
+		passes...,
+	)
 }
 
 // Maps fields from an allocation to a string value based on an identifier
 func allocationFieldMap(a *Allocation, identifier ast.Identifier) (string, error) {
-	switch allocationfilter.AllocationField(identifier.Field.Name) {
-	case allocationfilter.AllocationFieldNamespace:
+	if identifier.Field == nil {
+		return "", fmt.Errorf("cannot map field from identifier with nil field")
+	}
+	switch afilter.AllocationField(identifier.Field.Name) {
+	case afilter.FieldNamespace:
 		return a.Properties.Namespace, nil
-	case allocationfilter.AllocationFieldNode:
+	case afilter.FieldNode:
 		return a.Properties.Node, nil
-	case allocationfilter.AllocationFieldClusterID:
+	case afilter.FieldClusterID:
 		return a.Properties.Cluster, nil
-	case allocationfilter.AllocationFieldControllerName:
+	case afilter.FieldControllerName:
 		return a.Properties.Controller, nil
-	case allocationfilter.AllocationFieldControllerKind:
+	case afilter.FieldControllerKind:
 		return a.Properties.ControllerKind, nil
-	case allocationfilter.AllocationFieldPod:
+	case afilter.FieldPod:
 		return a.Properties.Pod, nil
-	case allocationfilter.AllocationFieldContainer:
+	case afilter.FieldContainer:
 		return a.Properties.Container, nil
-	case allocationfilter.AllocationFieldProvider:
+	case afilter.FieldProvider:
 		return a.Properties.ProviderID, nil
-	case allocationfilter.AllocationFieldLabel:
+	case afilter.FieldLabel:
 		return a.Properties.Labels[identifier.Key], nil
-	case allocationfilter.AllocationFieldAnnotation:
+	case afilter.FieldAnnotation:
 		return a.Properties.Annotations[identifier.Key], nil
 	}
 
@@ -55,8 +77,8 @@ func allocationFieldMap(a *Allocation, identifier ast.Identifier) (string, error
 
 // Maps slice fields from an allocation to a []string value based on an identifier
 func allocationSliceFieldMap(a *Allocation, identifier ast.Identifier) ([]string, error) {
-	switch allocationfilter.AllocationField(identifier.Field.Name) {
-	case allocationfilter.AllocationFieldServices:
+	switch afilter.AllocationField(identifier.Field.Name) {
+	case afilter.FieldServices:
 		return a.Properties.Services, nil
 	}
 
@@ -65,11 +87,168 @@ func allocationSliceFieldMap(a *Allocation, identifier ast.Identifier) ([]string
 
 // Maps map fields from an allocation to a map[string]string value based on an identifier
 func allocationMapFieldMap(a *Allocation, identifier ast.Identifier) (map[string]string, error) {
-	switch allocationfilter.AllocationField(identifier.Field.Name) {
-	case allocationfilter.AllocationFieldLabel:
+	switch afilter.AllocationField(identifier.Field.Name) {
+	case afilter.FieldLabel:
 		return a.Properties.Labels, nil
-	case allocationfilter.AllocationFieldAnnotation:
+	case afilter.FieldAnnotation:
 		return a.Properties.Annotations, nil
 	}
 	return nil, fmt.Errorf("Failed to find map[string]string identifier on Allocation: %s", identifier.Field.Name)
 }
+
+// aliasPass implements the transform.CompilerPass interface, providing a pass
+// which converts alias nodes to logically-equivalent label/annotation filter
+// nodes based on the label config.
+type aliasPass struct {
+	Config              LabelConfig
+	AliasNameToAliasKey map[afilter.AllocationAlias]string
+}
+
+// NewAliasPass creates a compiler pass that converts alias nodes to
+// logically-equivalent label/annotation nodes based on the label config.
+//
+// Due to the special alias logic that combines label and annotation behavior
+// when filtering on alias, an alias filter is logically equivalent to the
+// following expression:
+//
+// (or
+//
+//	(and (contains labels <parseraliaskey>)
+//	     (<op> labels[<parseraliaskey>] <filtervalue>))
+//	(and (not (contains labels <parseraliaskey>))
+//	     (and (contains annotations departmentkey)
+//	          (<op> annotations[<parseraliaskey>] <filtervalue>))))
+func NewAliasPass(config LabelConfig) transform.CompilerPass {
+	aliasNameToAliasKey := map[afilter.AllocationAlias]string{
+		afilter.AliasDepartment:  config.DepartmentLabel,
+		afilter.AliasEnvironment: config.EnvironmentLabel,
+		afilter.AliasOwner:       config.OwnerLabel,
+		afilter.AliasProduct:     config.ProductLabel,
+		afilter.AliasTeam:        config.TeamLabel,
+	}
+
+	return &aliasPass{
+		Config:              config,
+		AliasNameToAliasKey: aliasNameToAliasKey,
+	}
+}
+
+// Exec implements the transform.CompilerPass interface for an alias pass.
+// See aliasPass struct documentation for an explanation.
+func (p *aliasPass) Exec(filter ast.FilterNode) (ast.FilterNode, error) {
+	if p.AliasNameToAliasKey == nil {
+		return nil, fmt.Errorf("cannot perform alias conversion with nil mapping of alias name -> key")
+	}
+
+	var transformErr error
+	leafTransformerFunc := func(node ast.FilterNode) ast.FilterNode {
+		if transformErr != nil {
+			return node
+		}
+
+		var field *ast.Field
+		var filterValue string
+		var filterOp ast.FilterOp
+
+		switch concrete := node.(type) {
+		// These ops are not alias ops, alias ops can only be base-level ops
+		// like =, !=, etc. No modification required here.
+		case *ast.AndOp, *ast.OrOp, *ast.NotOp, *ast.VoidOp, *ast.ContradictionOp:
+			return node
+
+		case *ast.EqualOp:
+			field = concrete.Left.Field
+			filterValue = concrete.Right
+			filterOp = ast.FilterOpEquals
+		case *ast.ContainsOp:
+			field = concrete.Left.Field
+			filterValue = concrete.Right
+			filterOp = ast.FilterOpContains
+		case *ast.ContainsPrefixOp:
+			field = concrete.Left.Field
+			filterValue = concrete.Right
+			filterOp = ast.FilterOpContainsPrefix
+		case *ast.ContainsSuffixOp:
+			field = concrete.Left.Field
+			filterValue = concrete.Right
+			filterOp = ast.FilterOpContainsSuffix
+		default:
+			transformErr = fmt.Errorf("unknown op '%s' during alias pass", concrete.Op())
+			return node
+		}
+
+		if field == nil {
+			return node
+		}
+		if !field.IsAlias() {
+			return node
+		}
+
+		filterFieldAlias := afilter.AllocationAlias(field.Name)
+		parserAliasKey, ok := p.AliasNameToAliasKey[filterFieldAlias]
+		if !ok {
+			transformErr = fmt.Errorf("unknown alias field '%s'", filterFieldAlias)
+			return node
+		}
+
+		newFilter, err := convertAliasFilterToLabelAnnotationFilter(parserAliasKey, filterValue, filterOp)
+		if err != nil {
+			transformErr = fmt.Errorf("performing alias conversion for node '%+v': %w", node, err)
+			return node
+		}
+
+		return newFilter
+	}
+
+	newFilter := ast.TransformLeaves(filter, leafTransformerFunc)
+
+	if transformErr != nil {
+		return nil, fmt.Errorf("alias pass transform: %w", transformErr)
+	}
+
+	return newFilter, nil
+}
+
+// convertAliasFilterToLabelAnnotationFilter constructs a new filter node using
+// only operations on labels and annotations that is logically equivalent to an
+// alias node from relevant data extracted from the original alias node.
+func convertAliasFilterToLabelAnnotationFilter(aliasKey string, filterValue string, op ast.FilterOp) (ast.FilterNode, error) {
+	labelKey := ops.WithKey(afilter.FieldLabel, aliasKey)
+	annotationKey := ops.WithKey(afilter.FieldAnnotation, aliasKey)
+
+	var labelOp ast.FilterNode
+	var annotationOp ast.FilterNode
+
+	// This should only need to implement conversion for base-level ops like
+	// equals, contains, etc.
+	switch op {
+	case ast.FilterOpEquals:
+		labelOp = ops.Eq(labelKey, filterValue)
+		annotationOp = ops.Eq(annotationKey, filterValue)
+	case ast.FilterOpContains:
+		labelOp = ops.Contains(labelKey, filterValue)
+		annotationOp = ops.Contains(annotationKey, filterValue)
+	case ast.FilterOpContainsPrefix:
+		labelOp = ops.ContainsPrefix(labelKey, filterValue)
+		annotationOp = ops.ContainsPrefix(annotationKey, filterValue)
+	case ast.FilterOpContainsSuffix:
+		labelOp = ops.ContainsSuffix(labelKey, filterValue)
+		annotationOp = ops.ContainsSuffix(annotationKey, filterValue)
+	default:
+		return nil, fmt.Errorf("unsupported op type '%s' for alias conversion", op)
+	}
+
+	return ops.Or(
+		ops.And(
+			ops.Contains(afilter.FieldLabel, aliasKey),
+			labelOp,
+		),
+		ops.And(
+			ops.Not(ops.Contains(afilter.FieldLabel, aliasKey)),
+			ops.And(
+				ops.Contains(afilter.FieldAnnotation, aliasKey),
+				annotationOp,
+			),
+		),
+	), nil
+}

+ 64 - 0
pkg/kubecost/allocationmatcher_test.go

@@ -0,0 +1,64 @@
+package kubecost
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	afilter "github.com/opencost/opencost/pkg/filter21/allocation"
+	"github.com/opencost/opencost/pkg/filter21/ast"
+	"github.com/opencost/opencost/pkg/filter21/ops"
+)
+
+func TestAliasPass(t *testing.T) {
+	labelConfig := &LabelConfig{
+		DepartmentLabel:  "keydepartment",
+		EnvironmentLabel: "keyenvironment",
+		OwnerLabel:       "keyowner",
+		ProductLabel:     "keyproduct",
+		TeamLabel:        "keyteam",
+	}
+
+	cases := []struct {
+		name     string
+		input    ast.FilterNode
+		expected ast.FilterNode
+	}{
+		{
+			name: "department equal",
+			input: &ast.EqualOp{
+				Left: ast.Identifier{
+					Field: ast.NewAliasField(afilter.AliasDepartment),
+				},
+				Right: "x",
+			},
+			expected: ops.Or(
+				ops.And(
+					ops.Contains(afilter.FieldLabel, "keydepartment"),
+					ops.Eq(ops.WithKey(afilter.FieldLabel, "keydepartment"), "x"),
+				),
+				ops.And(
+					ops.Not(ops.Contains(afilter.FieldLabel, "keydepartment")),
+					ops.And(
+						ops.Contains(afilter.FieldAnnotation, "keydepartment"),
+						ops.Eq(ops.WithKey(afilter.FieldAnnotation, "keydepartment"), "x"),
+					),
+				),
+			),
+		},
+	}
+
+	for _, c := range cases {
+		pass := NewAliasPass(*labelConfig)
+
+		t.Run(c.name, func(t *testing.T) {
+			result, err := pass.Exec(c.input)
+			if err != nil {
+				t.Fatalf("unexpected error: %s", err)
+			}
+
+			if diff := cmp.Diff(c.expected, result); len(diff) > 0 {
+				t.Errorf("diff: %s", diff)
+			}
+		})
+	}
+}

+ 1 - 1
pkg/kubecost/summaryallocation.go

@@ -546,7 +546,7 @@ func (sas *SummaryAllocationSet) AggregateBy(aggregateBy []string, options *Allo
 	if options.Filter == nil {
 		filter = &matcher.AllPass[*Allocation]{}
 	} else {
-		compiler := NewAllocationMatchCompiler()
+		compiler := NewAllocationMatchCompiler(options.LabelConfig)
 		var err error
 		filter, err = compiler.Compile(options.Filter)
 		if err != nil {

+ 22 - 22
pkg/util/filterutil/filterutil.go

@@ -10,7 +10,7 @@ import (
 	"github.com/opencost/opencost/pkg/util/typeutil"
 
 	filter "github.com/opencost/opencost/pkg/filter21"
-	allocationfilter "github.com/opencost/opencost/pkg/filter21/allocation"
+	afilter "github.com/opencost/opencost/pkg/filter21/allocation"
 	"github.com/opencost/opencost/pkg/filter21/ast"
 	// cloudfilter "github.com/opencost/opencost/pkg/filter/cloud"
 )
@@ -26,7 +26,7 @@ import (
 // funcs by Field type.
 var defaultFieldByType = map[string]any{
 	// typeutil.TypeOf[cloudfilter.CloudAggregationField](): cloudfilter.DefaultFieldByName,
-	typeutil.TypeOf[allocationfilter.AllocationField](): allocationfilter.DefaultFieldByName,
+	typeutil.TypeOf[afilter.AllocationField](): afilter.DefaultFieldByName,
 }
 
 // DefaultFieldByName looks up a specific T field instance by name and returns the default
@@ -169,7 +169,7 @@ func AllocationFilterFromParamsV1(
 		var ops []ast.FilterNode
 
 		// filter my cluster identifier
-		ops = push(ops, filterV1SingleValueFromList(params.Clusters, allocationfilter.AllocationFieldClusterID))
+		ops = push(ops, filterV1SingleValueFromList(params.Clusters, afilter.FieldClusterID))
 
 		for _, rawFilterValue := range params.Clusters {
 			clusterNameFilter, wildcard := parseWildcardEnd(rawFilterValue)
@@ -186,7 +186,7 @@ func AllocationFilterFromParamsV1(
 			for _, clusterID := range clusterIDsToFilter {
 				ops = append(ops, &ast.EqualOp{
 					Left: ast.Identifier{
-						Field: allocationfilter.DefaultFieldByName(allocationfilter.AllocationFieldClusterID),
+						Field: afilter.DefaultFieldByName(afilter.FieldClusterID),
 						Key:   "",
 					},
 					Right: clusterID,
@@ -200,15 +200,15 @@ func AllocationFilterFromParamsV1(
 	}
 
 	if len(params.Nodes) > 0 {
-		filterOps = push(filterOps, filterV1SingleValueFromList(params.Nodes, allocationfilter.AllocationFieldNode))
+		filterOps = push(filterOps, filterV1SingleValueFromList(params.Nodes, afilter.FieldNode))
 	}
 
 	if len(params.Namespaces) > 0 {
-		filterOps = push(filterOps, filterV1SingleValueFromList(params.Namespaces, allocationfilter.AllocationFieldNamespace))
+		filterOps = push(filterOps, filterV1SingleValueFromList(params.Namespaces, afilter.FieldNamespace))
 	}
 
 	if len(params.ControllerKinds) > 0 {
-		filterOps = push(filterOps, filterV1SingleValueFromList(params.ControllerKinds, allocationfilter.AllocationFieldControllerKind))
+		filterOps = push(filterOps, filterV1SingleValueFromList(params.ControllerKinds, afilter.FieldControllerKind))
 	}
 
 	// filterControllers= accepts controllerkind:controllername filters, e.g.
@@ -223,14 +223,14 @@ func AllocationFilterFromParamsV1(
 			if len(split) == 1 {
 				filterValue, wildcard := parseWildcardEnd(split[0])
 
-				subFilter := toEqualOp(allocationfilter.AllocationFieldControllerName, "", filterValue, wildcard)
+				subFilter := toEqualOp(afilter.FieldControllerName, "", filterValue, wildcard)
 				ops = append(ops, subFilter)
 			} else if len(split) == 2 {
 				kindFilterVal := split[0]
 				nameFilterVal, wildcard := parseWildcardEnd(split[1])
 
-				kindFilter := toEqualOp(allocationfilter.AllocationFieldControllerKind, "", kindFilterVal, false)
-				nameFilter := toEqualOp(allocationfilter.AllocationFieldControllerName, "", nameFilterVal, wildcard)
+				kindFilter := toEqualOp(afilter.FieldControllerKind, "", kindFilterVal, false)
+				nameFilter := toEqualOp(afilter.FieldControllerName, "", nameFilterVal, wildcard)
 
 				// The controller name AND the controller kind must match
 				ops = append(ops, &ast.AndOp{
@@ -248,11 +248,11 @@ func AllocationFilterFromParamsV1(
 	}
 
 	if len(params.Pods) > 0 {
-		filterOps = push(filterOps, filterV1SingleValueFromList(params.Pods, allocationfilter.AllocationFieldPod))
+		filterOps = push(filterOps, filterV1SingleValueFromList(params.Pods, afilter.FieldPod))
 	}
 
 	if len(params.Containers) > 0 {
-		filterOps = push(filterOps, filterV1SingleValueFromList(params.Containers, allocationfilter.AllocationFieldContainer))
+		filterOps = push(filterOps, filterV1SingleValueFromList(params.Containers, afilter.FieldContainer))
 	}
 
 	// Label-mapped queries require a label config to be present.
@@ -277,11 +277,11 @@ func AllocationFilterFromParamsV1(
 	}
 
 	if len(params.Annotations) > 0 {
-		filterOps = push(filterOps, filterV1DoubleValueFromList(params.Annotations, allocationfilter.AllocationFieldAnnotation))
+		filterOps = push(filterOps, filterV1DoubleValueFromList(params.Annotations, afilter.FieldAnnotation))
 	}
 
 	if len(params.Labels) > 0 {
-		filterOps = push(filterOps, filterV1DoubleValueFromList(params.Labels, allocationfilter.AllocationFieldLabel))
+		filterOps = push(filterOps, filterV1DoubleValueFromList(params.Labels, afilter.FieldLabel))
 	}
 
 	if len(params.Services) > 0 {
@@ -292,7 +292,7 @@ func AllocationFilterFromParamsV1(
 			// TODO: wildcard support
 			filterValue, wildcard := parseWildcardEnd(filterValue)
 
-			subFilter := toContainsOp(allocationfilter.AllocationFieldServices, "", filterValue, wildcard)
+			subFilter := toContainsOp(afilter.FieldServices, "", filterValue, wildcard)
 			ops = append(ops, subFilter)
 		}
 
@@ -351,7 +351,7 @@ func filterV1LabelAliasMappedFromList(rawFilterValues []string, labelName string
 //
 // The v1 query language (e.g. "filterLabels=app:foo,l2:bar") uses OR within
 // a field (e.g. label[app] = foo OR label[l2] = bar)
-func filterV1DoubleValueFromList(rawFilterValuesUnsplit []string, filterField allocationfilter.AllocationField) ast.FilterNode {
+func filterV1DoubleValueFromList(rawFilterValuesUnsplit []string, filterField afilter.AllocationField) ast.FilterNode {
 	var ops []ast.FilterNode
 
 	for _, unsplit := range rawFilterValuesUnsplit {
@@ -463,7 +463,7 @@ func toAllocationAliasOp(labelName string, filterValue string, wildcard bool) *a
 	// labels.Contains(labelName)
 	labelContainsKey := &ast.ContainsOp{
 		Left: ast.Identifier{
-			Field: allocationfilter.DefaultFieldByName(allocationfilter.AllocationFieldLabel),
+			Field: afilter.DefaultFieldByName(afilter.FieldLabel),
 			Key:   "",
 		},
 		Right: labelName,
@@ -472,7 +472,7 @@ func toAllocationAliasOp(labelName string, filterValue string, wildcard bool) *a
 	// annotations.Contains(labelName)
 	annotationContainsKey := &ast.ContainsOp{
 		Left: ast.Identifier{
-			Field: allocationfilter.DefaultFieldByName(allocationfilter.AllocationFieldAnnotation),
+			Field: afilter.DefaultFieldByName(afilter.FieldAnnotation),
 			Key:   "",
 		},
 		Right: labelName,
@@ -483,7 +483,7 @@ func toAllocationAliasOp(labelName string, filterValue string, wildcard bool) *a
 	if wildcard {
 		labelSubFilter = &ast.ContainsPrefixOp{
 			Left: ast.Identifier{
-				Field: allocationfilter.DefaultFieldByName(allocationfilter.AllocationFieldLabel),
+				Field: afilter.DefaultFieldByName(afilter.FieldLabel),
 				Key:   labelName,
 			},
 			Right: filterValue,
@@ -491,7 +491,7 @@ func toAllocationAliasOp(labelName string, filterValue string, wildcard bool) *a
 	} else {
 		labelSubFilter = &ast.EqualOp{
 			Left: ast.Identifier{
-				Field: allocationfilter.DefaultFieldByName(allocationfilter.AllocationFieldLabel),
+				Field: afilter.DefaultFieldByName(afilter.FieldLabel),
 				Key:   labelName,
 			},
 			Right: filterValue,
@@ -503,7 +503,7 @@ func toAllocationAliasOp(labelName string, filterValue string, wildcard bool) *a
 	if wildcard {
 		annotationSubFilter = &ast.ContainsPrefixOp{
 			Left: ast.Identifier{
-				Field: allocationfilter.DefaultFieldByName(allocationfilter.AllocationFieldAnnotation),
+				Field: afilter.DefaultFieldByName(afilter.FieldAnnotation),
 				Key:   labelName,
 			},
 			Right: filterValue,
@@ -511,7 +511,7 @@ func toAllocationAliasOp(labelName string, filterValue string, wildcard bool) *a
 	} else {
 		annotationSubFilter = &ast.EqualOp{
 			Left: ast.Identifier{
-				Field: allocationfilter.DefaultFieldByName(allocationfilter.AllocationFieldAnnotation),
+				Field: afilter.DefaultFieldByName(afilter.FieldAnnotation),
 				Key:   labelName,
 			},
 			Right: filterValue,

+ 1 - 1
pkg/util/filterutil/queryfilters_test.go

@@ -8,7 +8,7 @@ import (
 	"github.com/opencost/opencost/pkg/util/mapper"
 )
 
-var allocCompiler = kubecost.NewAllocationMatchCompiler()
+var allocCompiler = kubecost.NewAllocationMatchCompiler(nil)
 
 type mockClusterMap struct {
 	m map[string]*clusters.ClusterInfo