Skip to content

Commit

Permalink
Create a filter implementation for K8s runtime.Objects (opencost#2631)
Browse files Browse the repository at this point in the history
* Initial K8s Object filter matcher

Signed-off-by: Michael Dresser <michaelmdresser@gmail.com>

* More unit tests for K8s object matcher

Signed-off-by: Michael Dresser <michaelmdresser@gmail.com>

* Switch filter fields to use common const strings

Signed-off-by: Michael Dresser <michaelmdresser@gmail.com>

* Improve error message on unsupported field

Signed-off-by: Michael Dresser <michaelmdresser@gmail.com>

* Switch to K8sObject-specific parser

Signed-off-by: Michael Dresser <michaelmdresser@gmail.com>

* Register K8sObjectField in ops package

Signed-off-by: Michael Dresser <michaelmdresser@gmail.com>

---------

Signed-off-by: Michael Dresser <michaelmdresser@gmail.com>
  • Loading branch information
michaelmdresser authored Mar 13, 2024
1 parent 811388d commit b55dbfb
Show file tree
Hide file tree
Showing 9 changed files with 509 additions and 38 deletions.
36 changes: 20 additions & 16 deletions core/pkg/filter/allocation/fields.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package allocation

import (
"github.com/opencost/opencost/core/pkg/filter/fieldstrings"
)

// AllocationField is an enum that represents Allocation-specific fields that can be
// filtered on (namespace, label, etc.)
type AllocationField string
Expand All @@ -8,17 +12,17 @@ type AllocationField string
// Allocation value
// does not enforce exhaustive pattern matching on "enum" types.
const (
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"
FieldClusterID AllocationField = AllocationField(fieldstrings.FieldClusterID)
FieldNode AllocationField = AllocationField(fieldstrings.FieldNode)
FieldNamespace AllocationField = AllocationField(fieldstrings.FieldNamespace)
FieldControllerKind AllocationField = AllocationField(fieldstrings.FieldControllerKind)
FieldControllerName AllocationField = AllocationField(fieldstrings.FieldControllerName)
FieldPod AllocationField = AllocationField(fieldstrings.FieldPod)
FieldContainer AllocationField = AllocationField(fieldstrings.FieldContainer)
FieldProvider AllocationField = AllocationField(fieldstrings.FieldProvider)
FieldServices AllocationField = AllocationField(fieldstrings.FieldServices)
FieldLabel AllocationField = AllocationField(fieldstrings.FieldLabel)
FieldAnnotation AllocationField = AllocationField(fieldstrings.FieldAnnotation)
)

// AllocationAlias represents an alias field type for allocations.
Expand All @@ -30,9 +34,9 @@ const (
type AllocationAlias string

const (
AliasDepartment AllocationAlias = "department"
AliasEnvironment AllocationAlias = "environment"
AliasOwner AllocationAlias = "owner"
AliasProduct AllocationAlias = "product"
AliasTeam AllocationAlias = "team"
AliasDepartment AllocationAlias = AllocationAlias(fieldstrings.AliasDepartment)
AliasEnvironment AllocationAlias = AllocationAlias(fieldstrings.AliasEnvironment)
AliasOwner AllocationAlias = AllocationAlias(fieldstrings.AliasOwner)
AliasProduct AllocationAlias = AllocationAlias(fieldstrings.AliasProduct)
AliasTeam AllocationAlias = AllocationAlias(fieldstrings.AliasTeam)
)
34 changes: 19 additions & 15 deletions core/pkg/filter/asset/fields.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
package asset

import (
"github.com/opencost/opencost/core/pkg/filter/fieldstrings"
)

// AssetField is an enum that represents Asset-specific fields that can be
// filtered on (namespace, label, etc.)
type AssetField string

// If you add a AssetField, make sure to update field maps to return the correct
// Asset value does not enforce exhaustive pattern matching on "enum" types.
const (
FieldName AssetField = "name"
FieldType AssetField = "assetType"
FieldCategory AssetField = "category"
FieldClusterID AssetField = "cluster"
FieldProject AssetField = "project"
FieldProvider AssetField = "provider"
FieldProviderID AssetField = "providerID"
FieldAccount AssetField = "account"
FieldService AssetField = "service"
FieldLabel AssetField = "label"
FieldName AssetField = AssetField(fieldstrings.FieldName)
FieldType AssetField = AssetField(fieldstrings.FieldType)
FieldCategory AssetField = AssetField(fieldstrings.FieldCategory)
FieldClusterID AssetField = AssetField(fieldstrings.FieldClusterID)
FieldProject AssetField = AssetField(fieldstrings.FieldProject)
FieldProvider AssetField = AssetField(fieldstrings.FieldProvider)
FieldProviderID AssetField = AssetField(fieldstrings.FieldProviderID)
FieldAccount AssetField = AssetField(fieldstrings.FieldAccount)
FieldService AssetField = AssetField(fieldstrings.FieldService)
FieldLabel AssetField = AssetField(fieldstrings.FieldLabel)
)

// AssetAlias represents an alias field type for assets.
Expand All @@ -27,9 +31,9 @@ const (
type AssetAlias string

const (
DepartmentProp AssetAlias = "department"
EnvironmentProp AssetAlias = "environment"
OwnerProp AssetAlias = "owner"
ProductProp AssetAlias = "product"
TeamProp AssetAlias = "team"
DepartmentProp AssetAlias = AssetAlias(fieldstrings.AliasDepartment)
EnvironmentProp AssetAlias = AssetAlias(fieldstrings.AliasEnvironment)
OwnerProp AssetAlias = AssetAlias(fieldstrings.AliasOwner)
ProductProp AssetAlias = AssetAlias(fieldstrings.AliasProduct)
TeamProp AssetAlias = AssetAlias(fieldstrings.AliasTeam)
)
18 changes: 11 additions & 7 deletions core/pkg/filter/cloudcost/fields.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package cloudcost

import (
"github.com/opencost/opencost/core/pkg/filter/fieldstrings"
)

// CloudCostField is an enum that represents CloudCost specific fields that can be filtered
type CloudCostField string

const (
FieldInvoiceEntityID CloudCostField = "invoiceEntityID"
FieldAccountID CloudCostField = "accountID"
FieldProvider CloudCostField = "provider"
FieldProviderID CloudCostField = "providerID"
FieldCategory CloudCostField = "category"
FieldService CloudCostField = "service"
FieldLabel CloudCostField = "label"
FieldInvoiceEntityID CloudCostField = CloudCostField(fieldstrings.FieldInvoiceEntityID)
FieldAccountID CloudCostField = CloudCostField(fieldstrings.FieldAccountID)
FieldProvider CloudCostField = CloudCostField(fieldstrings.FieldProvider)
FieldProviderID CloudCostField = CloudCostField(fieldstrings.FieldProviderID)
FieldCategory CloudCostField = CloudCostField(fieldstrings.FieldCategory)
FieldService CloudCostField = CloudCostField(fieldstrings.FieldService)
FieldLabel CloudCostField = CloudCostField(fieldstrings.FieldLabel)
)
35 changes: 35 additions & 0 deletions core/pkg/filter/fieldstrings/fieldstrings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package fieldstrings

// These strings are the central source of filter fields across all types of
// filters. Many filter types share fields; defining common consts means that
// there should be no drift between types.
const (
FieldClusterID string = "cluster"
FieldNode string = "node"
FieldNamespace string = "namespace"
FieldControllerKind string = "controllerKind"
FieldControllerName string = "controllerName"
FieldPod string = "pod"
FieldContainer string = "container"
FieldProvider string = "provider"
FieldServices string = "services"
FieldLabel string = "label"
FieldAnnotation string = "annotation"

FieldName string = "name"
FieldType string = "assetType"
FieldCategory string = "category"
FieldProject string = "project"
FieldProviderID string = "providerID"
FieldAccount string = "account"
FieldService string = "service"

FieldInvoiceEntityID string = "invoiceEntityID"
FieldAccountID string = "accountID"

AliasDepartment string = "department"
AliasEnvironment string = "environment"
AliasOwner string = "owner"
AliasProduct string = "product"
AliasTeam string = "team"
)
18 changes: 18 additions & 0 deletions core/pkg/filter/k8sobject/fields.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package k8sobject

import (
"github.com/opencost/opencost/core/pkg/filter/fieldstrings"
)

// K8sObjectField is an enum that represents K8sObject-specific fields that can
// be filtered on.
type K8sObjectField string

const (
FieldNamespace K8sObjectField = K8sObjectField(fieldstrings.FieldNamespace)
FieldControllerKind K8sObjectField = K8sObjectField(fieldstrings.FieldControllerKind)
FieldControllerName K8sObjectField = K8sObjectField(fieldstrings.FieldControllerName)
FieldPod K8sObjectField = K8sObjectField(fieldstrings.FieldPod)
FieldLabel K8sObjectField = K8sObjectField(fieldstrings.FieldLabel)
FieldAnnotation K8sObjectField = K8sObjectField(fieldstrings.FieldAnnotation)
)
43 changes: 43 additions & 0 deletions core/pkg/filter/k8sobject/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package k8sobject

import (
"github.com/opencost/opencost/core/pkg/filter/ast"
)

// a slice of all the allocation field instances the lexer should recognize as
// valid left-hand comparators
var k8sObjectFilterFields []*ast.Field = []*ast.Field{
ast.NewField(FieldNamespace),
ast.NewField(FieldControllerName, ast.FieldAttributeNilable),
ast.NewField(FieldControllerKind, ast.FieldAttributeNilable),
ast.NewField(FieldPod),
ast.NewMapField(FieldLabel),
ast.NewMapField(FieldAnnotation),
}

// fieldMap is a lazily loaded mapping from AllocationField to ast.Field
var fieldMap map[K8sObjectField]*ast.Field

func init() {
fieldMap = make(map[K8sObjectField]*ast.Field, len(k8sObjectFilterFields))
for _, f := range k8sObjectFilterFields {
ff := *f
fieldMap[K8sObjectField(ff.Name)] = &ff
}
}

// DefaultFieldByName returns only default allocation filter fields by name.
func DefaultFieldByName(field K8sObjectField) *ast.Field {
if af, ok := fieldMap[field]; ok {
afcopy := *af
return &afcopy
}

return nil
}

// NewK8sObjectFilterParser creates a new `ast.FilterParser` implementation for
// K8s runtime.Objects.
func NewK8sObjectFilterParser() ast.FilterParser {
return ast.NewFilterParser(k8sObjectFilterFields)
}
2 changes: 2 additions & 0 deletions core/pkg/filter/ops/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/opencost/opencost/core/pkg/filter/asset"
"github.com/opencost/opencost/core/pkg/filter/ast"
"github.com/opencost/opencost/core/pkg/filter/cloudcost"
"github.com/opencost/opencost/core/pkg/filter/k8sobject"
"github.com/opencost/opencost/core/pkg/util/typeutil"
)

Expand All @@ -29,6 +30,7 @@ var defaultFieldByType = map[string]any{
typeutil.TypeOf[allocation.AllocationField](): allocation.DefaultFieldByName,
typeutil.TypeOf[asset.AssetField](): asset.DefaultFieldByName,
typeutil.TypeOf[cloudcost.CloudCostField](): cloudcost.DefaultFieldByName,
typeutil.TypeOf[k8sobject.K8sObjectField](): k8sobject.DefaultFieldByName,
// typeutil.TypeOf[containerstats.ContainerStatsField](): containerstats.DefaultFieldByName,
}

Expand Down
139 changes: 139 additions & 0 deletions core/pkg/opencost/k8sobjectmatcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package opencost

import (
"fmt"

"github.com/opencost/opencost/core/pkg/filter/ast"
kfilter "github.com/opencost/opencost/core/pkg/filter/k8sobject"
"github.com/opencost/opencost/core/pkg/filter/matcher"
"github.com/opencost/opencost/core/pkg/filter/transform"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)

// K8sObjectMatcher is a matcher implementation for Kubernetes runtime.Object
// instances, compiled using the matcher.MatchCompiler.
type K8sObjectMatcher matcher.Matcher[runtime.Object]

// NewK8sObjectMatchCompiler creates a new instance of a
// matcher.MatchCompiler[runtime.Object] which can be used to compile
// filter.Filter ASTs into matcher.Matcher[runtime.Object] implementations.
//
// If the label config is nil, the compiler will fail to compile alias filters
// if any are present in the AST.
func NewK8sObjectMatchCompiler() *matcher.MatchCompiler[runtime.Object] {
passes := []transform.CompilerPass{}

return matcher.NewMatchCompiler(
k8sObjectFieldMap,
k8sObjectSliceFieldMap,
k8sObjectMapFieldMap,
passes...,
)
}

func objectMetaFromObject(o runtime.Object) (metav1.ObjectMeta, error) {
switch v := o.(type) {
case *appsv1.Deployment:
return v.ObjectMeta, nil
case *appsv1.StatefulSet:
return v.ObjectMeta, nil
case *appsv1.DaemonSet:
return v.ObjectMeta, nil
case *corev1.Pod:
return v.ObjectMeta, nil
case *batchv1.CronJob:
return v.ObjectMeta, nil
}

return metav1.ObjectMeta{}, fmt.Errorf("currently-unsupported runtime.Object type for filtering: %T", o)
}

// Maps fields from an allocation to a string value based on an identifier
func k8sObjectFieldMap(o runtime.Object, identifier ast.Identifier) (string, error) {
if identifier.Field == nil {
return "", fmt.Errorf("cannot map field from identifier with nil field")
}

m, err := objectMetaFromObject(o)
if err != nil {
return "", fmt.Errorf("retrieving object meta: %w", err)
}
var controllerKind string
var controllerName string
var pod string

switch v := o.(type) {
case *appsv1.Deployment:
controllerKind = "deployment"
controllerName = v.Name
case *appsv1.StatefulSet:
controllerKind = "statefulset"
controllerName = v.Name
case *appsv1.DaemonSet:
controllerKind = "daemonset"
controllerName = v.Name
case *corev1.Pod:
pod = v.Name
if len(v.OwnerReferences) == 0 {
controllerKind = "pod"
controllerName = v.Name
}
case *batchv1.CronJob:
controllerKind = "cronjob"
controllerName = v.Name
default:
return "", fmt.Errorf("currently-unsupported runtime.Object type for filtering: %T", o)
}

// For now, we will just do our best to implement Allocation fields because
// most k8s-based queries are on Allocation data. The other we will
// eventually want to support is Asset, but I'm not sure that I have time
// for that right now.
field := kfilter.K8sObjectField(identifier.Field.Name)
switch field {
case kfilter.FieldNamespace:
return m.Namespace, nil
case kfilter.FieldControllerName:
return controllerName, nil
case kfilter.FieldControllerKind:
return controllerKind, nil
case kfilter.FieldPod:
return pod, nil
case kfilter.FieldLabel:
if m.Labels != nil {
return m.Labels[identifier.Key], nil
}
return "", nil
case kfilter.FieldAnnotation:
if m.Annotations != nil {
return m.Annotations[identifier.Key], nil
}
return "", nil
}

return "", fmt.Errorf("Failed to find string identifier on K8sObject: %s (consider adding support if this is an expected field)", identifier.Field.Name)
}

// Maps slice fields from an allocation to a []string value based on an identifier
func k8sObjectSliceFieldMap(o runtime.Object, identifier ast.Identifier) ([]string, error) {
return nil, fmt.Errorf("K8sObject filters current have no supported []string identifiers")
}

// Maps map fields from an allocation to a map[string]string value based on an identifier
func k8sObjectMapFieldMap(o runtime.Object, identifier ast.Identifier) (map[string]string, error) {
m, err := objectMetaFromObject(o)
if err != nil {
return nil, fmt.Errorf("retrieving object meta: %w", err)
}
switch kfilter.K8sObjectField(identifier.Field.Name) {
case kfilter.FieldLabel:
return m.Labels, nil
case kfilter.FieldAnnotation:
return m.Annotations, nil
}
return nil, fmt.Errorf("Failed to find map[string]string identifier on K8sObject: %s", identifier.Field.Name)
}
Loading

0 comments on commit b55dbfb

Please sign in to comment.