Просмотр исходного кода

Merge pull request #1273 from kubecost/mmd/allocation-filter-flatten()

Add AllocationFilter.Flattened() for simplifying AllocationFilter expressions
Michael Dresser 3 лет назад
Родитель
Сommit
97c57edf2f
2 измененных файлов с 218 добавлено и 0 удалено
  1. 74 0
      pkg/kubecost/allocationfilter.go
  2. 144 0
      pkg/kubecost/allocationfilter_test.go

+ 74 - 0
pkg/kubecost/allocationfilter.go

@@ -86,6 +86,19 @@ type AllocationFilter interface {
 	// matches a filter.
 	Matches(a *Allocation) bool
 
+	// Flattened converts a filter into a minimal form, removing unnecessary
+	// intermediate objects, like single-element or zero-element AND and OR
+	// conditions.
+	//
+	// It returns nil if the filter is filtering nothing.
+	//
+	// Example:
+	// (and (or (namespaceequals "kubecost")) (or)) ->
+	// (namespaceequals "kubecost")
+	//
+	// (and (or)) -> nil
+	Flattened() AllocationFilter
+
 	String() string
 }
 
@@ -115,6 +128,11 @@ func (afc AllocationFilterCondition) String() string {
 	return fmt.Sprintf(`(%s %s[%s] "%s")`, afc.Op, afc.Field, afc.Key, afc.Value)
 }
 
+// Flattened returns itself because you cannot flatten a base condition further
+func (filter AllocationFilterCondition) Flattened() AllocationFilter {
+	return filter
+}
+
 // AllocationFilterOr is a set of filters that should be evaluated as a logical
 // OR.
 type AllocationFilterOr struct {
@@ -131,6 +149,42 @@ func (af AllocationFilterOr) String() string {
 	return s
 }
 
+// flattened returns a new slice of filters after flattening.
+func flattened(filters []AllocationFilter) []AllocationFilter {
+	var flattenedFilters []AllocationFilter
+	for _, innerFilter := range filters {
+		if innerFilter == nil {
+			continue
+		}
+		flattenedInner := innerFilter.Flattened()
+		if flattenedInner != nil {
+			flattenedFilters = append(flattenedFilters, flattenedInner)
+		}
+	}
+
+	return flattenedFilters
+}
+
+// Flattened converts a filter into a minimal form, removing unnecessary
+// intermediate objects
+//
+// Flattened returns:
+// - nil if filter contains no filters
+// - the inner filter if filter contains one filter
+// - an equivalent AllocationFilterOr if filter contains more than one filter
+func (filter AllocationFilterOr) Flattened() AllocationFilter {
+	flattenedFilters := flattened(filter.Filters)
+	if len(flattenedFilters) == 0 {
+		return nil
+	}
+
+	if len(flattenedFilters) == 1 {
+		return flattenedFilters[0]
+	}
+
+	return AllocationFilterOr{Filters: flattenedFilters}
+}
+
 // AllocationFilterOr is a set of filters that should be evaluated as a logical
 // AND.
 type AllocationFilterAnd struct {
@@ -147,6 +201,26 @@ func (af AllocationFilterAnd) String() string {
 	return s
 }
 
+// Flattened converts a filter into a minimal form, removing unnecessary
+// intermediate objects
+//
+// Flattened returns:
+// - nil if filter contains no filters
+// - the inner filter if filter contains one filter
+// - an equivalent AllocationFilterAnd if filter contains more than one filter
+func (filter AllocationFilterAnd) Flattened() AllocationFilter {
+	flattenedFilters := flattened(filter.Filters)
+	if len(flattenedFilters) == 0 {
+		return nil
+	}
+
+	if len(flattenedFilters) == 1 {
+		return flattenedFilters[0]
+	}
+
+	return AllocationFilterAnd{Filters: flattenedFilters}
+}
+
 func (filter AllocationFilterCondition) Matches(a *Allocation) bool {
 	if a == nil {
 		return false

+ 144 - 0
pkg/kubecost/allocationfilter_test.go

@@ -1,6 +1,7 @@
 package kubecost
 
 import (
+	"reflect"
 	"testing"
 )
 
@@ -821,3 +822,146 @@ func Test_AllocationFilterOr_Matches(t *testing.T) {
 		}
 	}
 }
+
+func Test_AllocationFilter_Flattened(t *testing.T) {
+	cases := []struct {
+		name string
+
+		input    AllocationFilter
+		expected AllocationFilter
+	}{
+		{
+			name: "AllocationFilterCondition",
+			input: AllocationFilterCondition{
+				Field: FilterNamespace,
+				Op:    FilterEquals,
+			},
+			expected: AllocationFilterCondition{
+				Field: FilterNamespace,
+				Op:    FilterEquals,
+			},
+		},
+		{
+			name:     "empty AllocationFilterAnd (nil)",
+			input:    AllocationFilterAnd{},
+			expected: nil,
+		},
+		{
+			name:     "empty AllocationFilterAnd (len 0)",
+			input:    AllocationFilterAnd{Filters: []AllocationFilter{}},
+			expected: nil,
+		},
+		{
+			name:     "empty AllocationFilterOr (nil)",
+			input:    AllocationFilterOr{},
+			expected: nil,
+		},
+		{
+			name:     "empty AllocationFilterOr (len 0)",
+			input:    AllocationFilterOr{Filters: []AllocationFilter{}},
+			expected: nil,
+		},
+		{
+			name: "single-element AllocationFilterAnd",
+			input: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterCondition{
+					Field: FilterNamespace,
+					Op:    FilterEquals,
+				},
+			}},
+
+			expected: AllocationFilterCondition{
+				Field: FilterNamespace,
+				Op:    FilterEquals,
+			},
+		},
+		{
+			name: "single-element AllocationFilterOr",
+			input: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterCondition{
+					Field: FilterNamespace,
+					Op:    FilterEquals,
+				},
+			}},
+
+			expected: AllocationFilterCondition{
+				Field: FilterNamespace,
+				Op:    FilterEquals,
+			},
+		},
+		{
+			name: "multi-element AllocationFilterAnd",
+			input: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterCondition{
+					Field: FilterNamespace,
+					Op:    FilterEquals,
+				},
+				AllocationFilterCondition{
+					Field: FilterClusterID,
+					Op:    FilterNotEquals,
+				},
+				AllocationFilterCondition{
+					Field: FilterServices,
+					Op:    FilterContains,
+				},
+			}},
+
+			expected: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterCondition{
+					Field: FilterNamespace,
+					Op:    FilterEquals,
+				},
+				AllocationFilterCondition{
+					Field: FilterClusterID,
+					Op:    FilterNotEquals,
+				},
+				AllocationFilterCondition{
+					Field: FilterServices,
+					Op:    FilterContains,
+				},
+			}},
+		},
+		{
+			name: "multi-element AllocationFilterOr",
+			input: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterCondition{
+					Field: FilterNamespace,
+					Op:    FilterEquals,
+				},
+				AllocationFilterCondition{
+					Field: FilterClusterID,
+					Op:    FilterNotEquals,
+				},
+				AllocationFilterCondition{
+					Field: FilterServices,
+					Op:    FilterContains,
+				},
+			}},
+
+			expected: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterCondition{
+					Field: FilterNamespace,
+					Op:    FilterEquals,
+				},
+				AllocationFilterCondition{
+					Field: FilterClusterID,
+					Op:    FilterNotEquals,
+				},
+				AllocationFilterCondition{
+					Field: FilterServices,
+					Op:    FilterContains,
+				},
+			}},
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			result := c.input.Flattened()
+
+			if !reflect.DeepEqual(result, c.expected) {
+				t.Errorf("Expected: '%s'. Got '%s'.", c.expected, result)
+			}
+		})
+	}
+}