-
Notifications
You must be signed in to change notification settings - Fork 545
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.Object
s
#2631
Changes from all commits
3ecf78e
84f4879
0267f1f
41826d4
bac0c82
ee0c997
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
) |
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" | ||
) |
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) | ||
) |
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), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} |
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) | ||
} |
There was a problem hiding this comment.
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:
is fine, but
barks at you that they're not the same type?
Bah
There was a problem hiding this comment.
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.