Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a filter implementation for K8s runtime.Objects #2631

Merged
merged 6 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Comment on lines +12 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did go force you to add the type cast when you referred to the fieldstrings constants?

ie:

const FieldNamespace K8sObjectField = "namespace"

is fine, but

const FieldNamespace K8sObjectField = fieldstrings.FieldNamespace

barks at you that they're not the same type?

Bah

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it barks; I'm glad it does, otherwise these enum-like things would be pretty useless.

)
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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wanted to point out (if you haven't noted field attributes yet) that ast.FieldAttributeNilable (or any attribute that might be needed) on a field is simply metadata that can be accessed in the compiler or code generator to help alleviate any special cases that may exist.

Copy link
Contributor

@mbolt35 mbolt35 Mar 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attributes are just bitflags (sorry, I think I'm showing my age), so they're not super expansive, but likely more space than we'll ever need. I think Nilable starts at the 5th bit, and we're using an int, so it's expandable to another 27 custom attributes 🤷

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh very neat, I just copied whatever Allocation was using for consistency; I think it also makes sense here.

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
Loading