Przeglądaj źródła

Add NodeLabels filtering to allocation (#3238)

Signed-off-by: Niko Kovacevic <nikovacevic@gmail.com>
Niko Kovacevic 9 miesięcy temu
rodzic
commit
c34823186d

+ 1 - 0
core/pkg/filter/allocation/fields.go

@@ -23,6 +23,7 @@ const (
 	FieldServices       AllocationField = AllocationField(fieldstrings.FieldServices)
 	FieldLabel          AllocationField = AllocationField(fieldstrings.FieldLabel)
 	FieldAnnotation     AllocationField = AllocationField(fieldstrings.FieldAnnotation)
+	FieldNodeLabel      AllocationField = AllocationField(fieldstrings.FieldNodeLabel)
 )
 
 // AllocationAlias represents an alias field type for allocations.

+ 1 - 0
core/pkg/filter/allocation/parser.go

@@ -21,6 +21,7 @@ var allocationFilterFields []*ast.Field = []*ast.Field{
 	ast.NewSliceField(FieldServices),
 	ast.NewMapField(FieldLabel),
 	ast.NewMapField(FieldAnnotation),
+	ast.NewMapField(FieldNodeLabel),
 }
 
 // fieldMap is a lazily loaded mapping from AllocationField to ast.Field

+ 3 - 0
core/pkg/filter/fieldstrings/fieldstrings.go

@@ -15,6 +15,7 @@ const (
 	FieldServices       string = "services"
 	FieldLabel          string = "label"
 	FieldAnnotation     string = "annotation"
+	FieldNodeLabel      string = "nodeLabel"
 
 	FieldName       string = "name"
 	FieldType       string = "assetType"
@@ -24,6 +25,8 @@ const (
 	FieldAccount    string = "account"
 	FieldService    string = "service"
 
+	FieldNodeType string = "nodeType"
+
 	FieldInvoiceEntityID   string = "invoiceEntityID"
 	FieldInvoiceEntityName string = "invoiceEntityName"
 	FieldAccountID         string = "accountID"

+ 20 - 0
core/pkg/filter/node/fields.go

@@ -0,0 +1,20 @@
+package node
+
+import (
+	"github.com/opencost/opencost/core/pkg/filter/fieldstrings"
+)
+
+// NodeField is an enum that represents Asset-specific fields that can be
+// filtered on (namespace, label, etc.)
+type NodeField string
+
+// If you add a NodeField, make sure to update field maps to return the correct
+// Asset value does not enforce exhaustive pattern matching on "enum" types.
+const (
+	FieldProviderID NodeField = NodeField(fieldstrings.FieldProviderID)
+	FieldName       NodeField = NodeField(fieldstrings.FieldName)
+	FieldNodeType   NodeField = NodeField(fieldstrings.FieldNodeType)
+	FieldClusterID  NodeField = NodeField(fieldstrings.FieldClusterID)
+	FieldProvider   NodeField = NodeField(fieldstrings.FieldProvider)
+	FieldLabel      NodeField = NodeField(fieldstrings.FieldLabel)
+)

+ 41 - 0
core/pkg/filter/node/parser.go

@@ -0,0 +1,41 @@
+package node
+
+import "github.com/opencost/opencost/core/pkg/filter/ast"
+
+// a slice of all the node field instances the lexer should recognize as
+// valid left-hand comparators
+var nodeFilterFields []*ast.Field = []*ast.Field{
+	ast.NewField(FieldName),
+	ast.NewField(FieldNodeType),
+	ast.NewField(FieldClusterID),
+	ast.NewField(FieldProvider),
+	ast.NewField(FieldProviderID),
+	ast.NewMapField(FieldLabel),
+}
+
+// fieldMap is a lazily loaded mapping from NodeField to ast.Field
+var fieldMap map[NodeField]*ast.Field
+
+func init() {
+	fieldMap = make(map[NodeField]*ast.Field, len(nodeFilterFields))
+	for _, f := range nodeFilterFields {
+		ff := *f
+		fieldMap[NodeField(ff.Name)] = &ff
+	}
+}
+
+// DefaultFieldByName returns only default node filter fields by name.
+func DefaultFieldByName(field NodeField) *ast.Field {
+	if af, ok := fieldMap[field]; ok {
+		afcopy := *af
+		return &afcopy
+	}
+
+	return nil
+}
+
+// NewNodeFilterParser creates a new `ast.FilterParser` implementation
+// which uses node specific fields
+func NewNodeFilterParser() ast.FilterParser {
+	return ast.NewFilterParser(nodeFilterFields)
+}

+ 48 - 0
core/pkg/filter/node/parser_test.go

@@ -0,0 +1,48 @@
+package node
+
+import (
+	"testing"
+
+	"github.com/opencost/opencost/core/pkg/filter/ast"
+)
+
+func TestDefaultFieldByName(t *testing.T) {
+	var nodeField NodeField
+	var astf *ast.Field
+
+	nodeField = FieldName
+	astf = DefaultFieldByName(nodeField)
+	if astf.Name != "name" {
+		t.Errorf("expected %s; received %s", "name", astf.Name)
+	}
+
+	nodeField = FieldNodeType
+	astf = DefaultFieldByName(nodeField)
+	if astf.Name != "nodeType" {
+		t.Errorf("expected %s; received %s", "nodeType", astf.Name)
+	}
+
+	nodeField = FieldClusterID
+	astf = DefaultFieldByName(nodeField)
+	if astf.Name != "cluster" {
+		t.Errorf("expected %s; received %s", "cluster", astf.Name)
+	}
+
+	nodeField = FieldProvider
+	astf = DefaultFieldByName(nodeField)
+	if astf.Name != "provider" {
+		t.Errorf("expected %s; received %s", "provider", astf.Name)
+	}
+
+	nodeField = FieldProviderID
+	astf = DefaultFieldByName(nodeField)
+	if astf.Name != "providerID" {
+		t.Errorf("expected %s; received %s", "providerID", astf.Name)
+	}
+
+	nodeField = FieldLabel
+	astf = DefaultFieldByName(nodeField)
+	if astf.Name != "label" {
+		t.Errorf("expected %s; received %s", "label", astf.Name)
+	}
+}

+ 92 - 0
core/pkg/opencost/nodematcher.go

@@ -0,0 +1,92 @@
+package opencost
+
+import (
+	"fmt"
+
+	"github.com/opencost/opencost/core/pkg/filter/ast"
+	"github.com/opencost/opencost/core/pkg/filter/matcher"
+	nfilter "github.com/opencost/opencost/core/pkg/filter/node"
+	"github.com/opencost/opencost/core/pkg/filter/transform"
+)
+
+// NodeMatcher is a matcher implementation for Node instances,
+// compiled using the matcher.MatchCompiler.
+type NodeMatcher matcher.Matcher[*Node]
+
+// NewNodeMatchCompiler creates a new instance of a
+// matcher.MatchCompiler[Node] which can be used to compile filter.Filter
+// ASTs into matcher.Matcher[Node] implementations.
+//
+// If the label config is nil, the compiler will fail to compile alias filters
+// if any are present in the AST.
+func NewNodeMatchCompiler() *matcher.MatchCompiler[*Node] {
+	passes := []transform.CompilerPass{}
+
+	passes = append(passes,
+		transform.PrometheusKeySanitizePass(),
+		transform.UnallocatedReplacementPass(),
+	)
+	return matcher.NewMatchCompiler(
+		nodeFieldMap,
+		nodeSliceFieldMap,
+		nodeMapFieldMap,
+		passes...,
+	)
+}
+
+// Maps fields from an asset to a string value based on an identifier
+func nodeFieldMap(n *Node, identifier ast.Identifier) (string, error) {
+	if identifier.Field == nil {
+		return "", fmt.Errorf("cannot map field from identifier with nil field")
+	}
+	if n == nil {
+		return "", fmt.Errorf("cannot map field for nil Node")
+	}
+
+	// Check special fields before defaulting to properties-based fields
+	switch nfilter.NodeField(identifier.Field.Name) {
+	case nfilter.FieldLabel:
+		labels := n.GetLabels()
+		if labels == nil {
+			return "", nil
+		}
+		return labels[identifier.Key], nil
+	}
+
+	props := n.GetProperties()
+	if props == nil {
+		return "", fmt.Errorf("cannot map field for Node with nil props")
+	}
+
+	switch nfilter.NodeField(identifier.Field.Name) {
+	case nfilter.FieldName:
+		return props.Name, nil
+	case nfilter.FieldNodeType:
+		return n.NodeType, nil
+	case nfilter.FieldClusterID:
+		return props.Cluster, nil
+	case nfilter.FieldProvider:
+		return props.Provider, nil
+	case nfilter.FieldProviderID:
+		return props.ProviderID, nil
+	}
+
+	return "", fmt.Errorf("Failed to find string identifier on Node: %s", identifier.Field.Name)
+}
+
+// Maps slice fields from an asset to a []string value based on an identifier
+func nodeSliceFieldMap(n *Node, identifier ast.Identifier) ([]string, error) {
+	return nil, fmt.Errorf("Nodes have no slice fields")
+}
+
+// Maps map fields from an Node to a map[string]string value based on an identifier
+func nodeMapFieldMap(n *Node, identifier ast.Identifier) (map[string]string, error) {
+	if n == nil {
+		return nil, fmt.Errorf("cannot get map field for nil Node")
+	}
+	switch nfilter.NodeField(identifier.Field.Name) {
+	case nfilter.FieldLabel:
+		return n.GetLabels(), nil
+	}
+	return nil, fmt.Errorf("Failed to find map[string]string identifier on Node: %s", identifier.Field.Name)
+}

+ 147 - 0
core/pkg/opencost/nodematcher_test.go

@@ -0,0 +1,147 @@
+package opencost
+
+import (
+	"testing"
+
+	"github.com/opencost/opencost/core/pkg/filter/ast"
+	"github.com/opencost/opencost/core/pkg/filter/node"
+)
+
+func TestNodeMatcher(t *testing.T) {
+	var n *Node
+	var id ast.Identifier
+	var act string
+	var actMap map[string]string
+	var err error
+
+	n = &Node{
+		Properties: &AssetProperties{
+			Cluster:    "cluster",
+			ProviderID: "providerid",
+			Provider:   "provider",
+			Name:       "name",
+		},
+		Labels: AssetLabels{
+			"nodegroup": "ng",
+			"os":        "linux",
+		},
+		NodeType: "nodetype",
+	}
+
+	// test nodeFieldMap
+	id = ast.Identifier{
+		Field: &ast.Field{Name: string(node.FieldProviderID)},
+	}
+	act, err = nodeFieldMap(nil, id)
+	if act != "" || err == nil {
+		t.Errorf("expected error for nil node")
+	}
+
+	act, err = nodeFieldMap(n, id)
+	if err != nil {
+		t.Errorf("unexpected error for non-nil node")
+	}
+	if act != "providerid" {
+		t.Errorf("expected %s; received %s", "providerid", act)
+	}
+
+	id = ast.Identifier{
+		Field: &ast.Field{Name: string(node.FieldClusterID)},
+	}
+	act, err = nodeFieldMap(nil, id)
+	if act != "" || err == nil {
+		t.Errorf("expected error for nil node")
+	}
+	act, err = nodeFieldMap(n, id)
+	if err != nil {
+		t.Errorf("unexpected error for non-nil node")
+	}
+	if act != "cluster" {
+		t.Errorf("expected %s; received %s", "cluster", act)
+	}
+
+	id = ast.Identifier{
+		Field: &ast.Field{Name: string(node.FieldName)},
+	}
+	act, err = nodeFieldMap(nil, id)
+	if act != "" || err == nil {
+		t.Errorf("expected error for nil node")
+	}
+	act, err = nodeFieldMap(n, id)
+	if err != nil {
+		t.Errorf("unexpected error for non-nil node")
+	}
+	if act != "name" {
+		t.Errorf("expected %s; received %s", "name", act)
+	}
+
+	id = ast.Identifier{
+		Field: &ast.Field{Name: string(node.FieldNodeType)},
+	}
+	act, err = nodeFieldMap(nil, id)
+	if act != "" || err == nil {
+		t.Errorf("expected error for nil node")
+	}
+	act, err = nodeFieldMap(n, id)
+	if err != nil {
+		t.Errorf("unexpected error for non-nil node")
+	}
+	if act != "nodetype" {
+		t.Errorf("expected %s; received %s", "nodetype", act)
+	}
+
+	id = ast.Identifier{
+		Field: &ast.Field{Name: string(node.FieldProvider)},
+	}
+	act, err = nodeFieldMap(nil, id)
+	if act != "" || err == nil {
+		t.Errorf("expected error for nil node")
+	}
+	act, err = nodeFieldMap(n, id)
+	if err != nil {
+		t.Errorf("unexpected error for non-nil node")
+	}
+	if act != "provider" {
+		t.Errorf("expected %s; received %s", "provider", act)
+	}
+
+	id = ast.Identifier{
+		Field: &ast.Field{Name: string(node.FieldLabel)},
+		Key:   "nodegroup",
+	}
+	act, err = nodeFieldMap(nil, id)
+	if act != "" || err == nil {
+		t.Errorf("expected error for nil node")
+	}
+	act, err = nodeFieldMap(n, id)
+	if err != nil {
+		t.Errorf("unexpected error for non-nil node")
+	}
+	if act != "ng" {
+		t.Errorf("expected %s; received %s", "ng", act)
+	}
+
+	// test nodeSliceFieldMap
+	id = ast.Identifier{}
+	_, err = nodeSliceFieldMap(nil, id)
+	if err == nil {
+		t.Errorf("expected error for slice")
+	}
+	_, err = nodeSliceFieldMap(n, id)
+	if err == nil {
+		t.Errorf("expected error for slice")
+	}
+
+	// test nodeMapFieldMap
+	id = ast.Identifier{
+		Field: &ast.Field{Name: string(node.FieldLabel)},
+	}
+	actMap, err = nodeMapFieldMap(nil, id)
+	if err == nil {
+		t.Errorf("expected error for nil node")
+	}
+	actMap, err = nodeMapFieldMap(n, id)
+	if len(actMap) != 2 || err != nil {
+		t.Errorf("unexpected error for map")
+	}
+}