| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 |
- package kubecost
- import (
- "strings"
- "github.com/kubecost/opencost/pkg/log"
- )
- // FilterField is an enum that represents Allocation-specific fields that can be
- // filtered on (namespace, label, etc.)
- type FilterField string
- // If you add a FilterField, MAKE SURE TO UPDATE ALL FILTER IMPLEMENTATIONS! Go
- // does not enforce exhaustive pattern matching on "enum" types.
- const (
- FilterClusterID FilterField = "clusterid"
- FilterNode = "node"
- FilterNamespace = "namespace"
- FilterControllerKind = "controllerkind"
- FilterControllerName = "controllername"
- FilterPod = "pod"
- FilterContainer = "container"
- // Filtering based on label aliases (team, department, etc.) should be a
- // responsibility of the query handler. By the time it reaches this
- // structured representation, we shouldn't have to be aware of what is
- // aliased to what.
- FilterLabel = "label"
- FilterAnnotation = "annotation"
- FilterServices = "services"
- )
- // FilterOp is an enum that represents operations that can be performed
- // when filtering (equality, inequality, etc.)
- type FilterOp string
- // If you add a FilterOp, MAKE SURE TO UPDATE ALL FILTER IMPLEMENTATIONS! Go
- // does not enforce exhaustive pattern matching on "enum" types.
- const (
- // FilterEquals is the equality operator
- // "kube-system" FilterEquals "kube-system" = true
- // "kube-syste" FilterEquals "kube-system" = false
- FilterEquals FilterOp = "equals"
- // FilterNotEquals is the inequality operator
- FilterNotEquals = "notequals"
- // FilterContains is an array/slice membership operator
- // ["a", "b", "c"] FilterContains "a" = true
- FilterContains = "contains"
- // FilterStartsWith matches strings with the given prefix.
- // "kube-system" StartsWith "kube" = true
- //
- // When comparing with a field represented by an array/slice, this is like
- // applying FilterContains to every element of the slice.
- FilterStartsWith = "startswith"
- // FilterContainsPrefix is like FilterContains, but using StartsWith instead
- // of Equals.
- // ["kube-system", "abc123"] ContainsPrefix ["kube"] = true
- FilterContainsPrefix = "containsprefix"
- )
- // AllocationFilter represents anything that can be used to filter an
- // Allocation.
- //
- // Implement this interface with caution. While it is generic, it
- // is intended to be introspectable so query handlers can perform various
- // optimizations. These optimizations include:
- // - Routing a query to the most optimal cache
- // - Querying backing data stores efficiently (e.g. translation to SQL)
- //
- // Custom implementations of this interface outside of this package should not
- // expect to receive these benefits. Passing a custom implementation to a
- // handler may in errors.
- type AllocationFilter interface {
- // Matches is the canonical in-Go function for determing if an Allocation
- // matches a filter.
- Matches(a *Allocation) bool
- }
- // AllocationFilterCondition is the lowest-level type of filter. It represents
- // the a filter operation (equality, inequality, etc.) on a field (namespace,
- // label, etc.).
- type AllocationFilterCondition struct {
- Field FilterField
- Op FilterOp
- // Key is for filters that require key-value pairs, like labels or
- // annotations.
- //
- // A filter of 'label[app]:"foo"' has Key="app" and Value="foo"
- Key string
- // Value is for _all_ filters. A filter of 'namespace:"kubecost"' has
- // Value="kubecost"
- Value string
- }
- // AllocationFilterOr is a set of filters that should be evaluated as a logical
- // OR.
- type AllocationFilterOr struct {
- Filters []AllocationFilter
- }
- // AllocationFilterOr is a set of filters that should be evaluated as a logical
- // AND.
- type AllocationFilterAnd struct {
- Filters []AllocationFilter
- }
- func (filter AllocationFilterCondition) Matches(a *Allocation) bool {
- if a == nil {
- return false
- }
- if a.Properties == nil {
- return false
- }
- // The Allocation's value for the field to compare
- // We use an interface{} so this can contain the services []string slice
- var valueToCompare interface{}
- // toCompareMissing will be true if the value to be compared is missing in
- // the Allocation. For example, if we're filtering based on the value of
- // the "app" label, but the Allocation doesn't have an "app" label, this
- // will become true. This lets us deal with != gracefully.
- toCompareMissing := false
- // This switch maps the filter.Field to the field to be compared in
- // a.Properties and sets valueToCompare from the value in a.Properties.
- switch filter.Field {
- case FilterClusterID:
- valueToCompare = a.Properties.Cluster
- case FilterNode:
- valueToCompare = a.Properties.Node
- case FilterNamespace:
- valueToCompare = a.Properties.Namespace
- case FilterControllerKind:
- valueToCompare = a.Properties.ControllerKind
- case FilterControllerName:
- valueToCompare = a.Properties.Controller
- case FilterPod:
- valueToCompare = a.Properties.Pod
- case FilterContainer:
- valueToCompare = a.Properties.Container
- // Comes from GetAnnotation/LabelFilterFunc in KCM
- case FilterLabel:
- val, ok := a.Properties.Labels[filter.Key]
- if !ok {
- toCompareMissing = true
- } else {
- valueToCompare = val
- }
- case FilterAnnotation:
- val, ok := a.Properties.Annotations[filter.Key]
- if !ok {
- toCompareMissing = true
- } else {
- valueToCompare = val
- }
- case FilterServices:
- valueToCompare = a.Properties.Services
- default:
- log.Errorf("Allocation Filter: Unhandled filter field. This is a filter implementation error and requires immediate patching. Field: %s", filter.Field)
- return false
- }
- switch filter.Op {
- case FilterEquals:
- if toCompareMissing {
- return false
- }
- // namespace:"__unallocated__" should match a.Properties.Namespace = ""
- if valueToCompare == "" {
- return filter.Value == UnallocatedSuffix
- }
- if valueToCompare == filter.Value {
- return true
- }
- case FilterNotEquals:
- if toCompareMissing {
- return true
- }
- // namespace!:"__unallocated__" should match
- // a.Properties.Namespace != ""
- if filter.Value == UnallocatedSuffix {
- return valueToCompare != ""
- }
- if valueToCompare != filter.Value {
- return true
- }
- case FilterContains:
- if stringSlice, ok := valueToCompare.([]string); ok {
- if len(stringSlice) == 0 {
- return filter.Value == UnallocatedSuffix
- }
- for _, s := range stringSlice {
- if s == filter.Value {
- return true
- }
- }
- } else {
- log.Warnf("Allocation Filter: invalid 'contains' call for non-list filter value")
- }
- case FilterStartsWith:
- if toCompareMissing {
- return false
- }
- // We don't need special __unallocated__ logic here because a query
- // asking for "__unallocated__" won't have a wildcard and unallocated
- // properties are the empty string.
- s, ok := valueToCompare.(string)
- if !ok {
- log.Warnf("Allocation Filter: invalid 'startswith' call for field with unsupported type")
- return false
- }
- return strings.HasPrefix(s, filter.Value)
- case FilterContainsPrefix:
- if toCompareMissing {
- return false
- }
- // We don't need special __unallocated__ logic here because a query
- // asking for "__unallocated__" won't have a wildcard and unallocated
- // properties are the empty string.
- values, ok := valueToCompare.([]string)
- if !ok {
- log.Warnf("Allocation Filter: invalid '%s' call for field with unsupported type", FilterContainsPrefix)
- return false
- }
- for _, s := range values {
- if strings.HasPrefix(s, filter.Value) {
- return true
- }
- }
- return false
- default:
- log.Errorf("Allocation Filter: Unhandled filter op. This is a filter implementation error and requires immediate patching. Op: %s", filter.Op)
- return false
- }
- return false
- }
- func (and AllocationFilterAnd) Matches(a *Allocation) bool {
- filters := and.Filters
- if len(filters) == 0 {
- return true
- }
- for _, filter := range filters {
- if !filter.Matches(a) {
- return false
- }
- }
- return true
- }
- func (or AllocationFilterOr) Matches(a *Allocation) bool {
- filters := or.Filters
- if len(filters) == 0 {
- return true
- }
- for _, filter := range filters {
- if filter.Matches(a) {
- return true
- }
- }
- return false
- }
|