forked from opencost/opencost
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create a filter implementation for K8s
runtime.Object
s (opencost#2631)
* 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
1 parent
811388d
commit b55dbfb
Showing
9 changed files
with
509 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.