Bläddra i källkod

Merge pull request #1359 from opencost/mmd/allocationfilter-equals

Implement .Equals() for the AllocationFilter interface
Michael Dresser 3 år sedan
förälder
incheckning
7c59ce636e
2 ändrade filer med 745 tillägg och 0 borttagningar
  1. 94 0
      pkg/kubecost/allocationfilter.go
  2. 651 0
      pkg/kubecost/allocationfilter_test.go

+ 94 - 0
pkg/kubecost/allocationfilter.go

@@ -2,6 +2,7 @@ package kubecost
 
 import (
 	"fmt"
+	"sort"
 	"strings"
 
 	"github.com/opencost/opencost/pkg/log"
@@ -100,6 +101,10 @@ type AllocationFilter interface {
 	Flattened() AllocationFilter
 
 	String() string
+
+	// Equals returns true if the two AllocationFilters are logically
+	// equivalent.
+	Equals(AllocationFilter) bool
 }
 
 // AllocationFilterCondition is the lowest-level type of filter. It represents
@@ -130,9 +135,17 @@ func (afc AllocationFilterCondition) String() string {
 
 // Flattened returns itself because you cannot flatten a base condition further
 func (filter AllocationFilterCondition) Flattened() AllocationFilter {
+
 	return filter
 }
 
+func (left AllocationFilterCondition) Equals(right AllocationFilter) bool {
+	if rightAFC, ok := right.(AllocationFilterCondition); ok {
+		return left == rightAFC
+	}
+	return false
+}
+
 // AllocationFilterOr is a set of filters that should be evaluated as a logical
 // OR.
 type AllocationFilterOr struct {
@@ -185,6 +198,44 @@ func (filter AllocationFilterOr) Flattened() AllocationFilter {
 	return AllocationFilterOr{Filters: flattenedFilters}
 }
 
+func (filter AllocationFilterOr) sort() {
+	for _, inner := range filter.Filters {
+		if and, ok := inner.(AllocationFilterAnd); ok {
+			and.sort()
+		} else if or, ok := inner.(AllocationFilterOr); ok {
+			or.sort()
+		}
+	}
+
+	// While a slight hack, we can rely on the string serialization of the
+	// inner filters to get a sortable representation.
+	sort.SliceStable(filter.Filters, func(i, j int) bool {
+		return filter.Filters[i].String() < filter.Filters[j].String()
+	})
+}
+
+func (left AllocationFilterOr) Equals(right AllocationFilter) bool {
+	// The type cast takes care of right == nil as well
+	rightOr, ok := right.(AllocationFilterOr)
+	if !ok {
+		return false
+	}
+
+	if len(left.Filters) != len(rightOr.Filters) {
+		return false
+	}
+
+	left.sort()
+	rightOr.sort()
+
+	for i := range left.Filters {
+		if !left.Filters[i].Equals(rightOr.Filters[i]) {
+			return false
+		}
+	}
+	return true
+}
+
 // AllocationFilterOr is a set of filters that should be evaluated as a logical
 // AND.
 type AllocationFilterAnd struct {
@@ -221,6 +272,44 @@ func (filter AllocationFilterAnd) Flattened() AllocationFilter {
 	return AllocationFilterAnd{Filters: flattenedFilters}
 }
 
+func (filter AllocationFilterAnd) sort() {
+	for _, inner := range filter.Filters {
+		if and, ok := inner.(AllocationFilterAnd); ok {
+			and.sort()
+		} else if or, ok := inner.(AllocationFilterOr); ok {
+			or.sort()
+		}
+	}
+
+	// While a slight hack, we can rely on the string serialization of the
+	// inner filters.
+	sort.SliceStable(filter.Filters, func(i, j int) bool {
+		return filter.Filters[i].String() < filter.Filters[j].String()
+	})
+}
+
+func (left AllocationFilterAnd) Equals(right AllocationFilter) bool {
+	// The type cast takes care of right == nil as well
+	rightAnd, ok := right.(AllocationFilterAnd)
+	if !ok {
+		return false
+	}
+
+	if len(left.Filters) != len(rightAnd.Filters) {
+		return false
+	}
+
+	left.sort()
+	rightAnd.sort()
+
+	for i := range left.Filters {
+		if !left.Filters[i].Equals(rightAnd.Filters[i]) {
+			return false
+		}
+	}
+	return true
+}
+
 func (filter AllocationFilterCondition) Matches(a *Allocation) bool {
 	if a == nil {
 		return false
@@ -428,3 +517,8 @@ func (afn AllocationFilterNone) String() string { return "(none)" }
 func (afn AllocationFilterNone) Flattened() AllocationFilter { return afn }
 
 func (afn AllocationFilterNone) Matches(a *Allocation) bool { return false }
+
+func (left AllocationFilterNone) Equals(right AllocationFilter) bool {
+	_, ok := right.(AllocationFilterNone)
+	return ok
+}

+ 651 - 0
pkg/kubecost/allocationfilter_test.go

@@ -1,6 +1,7 @@
 package kubecost
 
 import (
+	"fmt"
 	"reflect"
 	"testing"
 )
@@ -1130,3 +1131,653 @@ func Test_AllocationFilter_Flattened(t *testing.T) {
 		})
 	}
 }
+
+func Test_AllocationFilter_Equals(t *testing.T) {
+	cases := []struct {
+		left     AllocationFilter
+		right    AllocationFilter
+		expected bool
+	}{
+		// AFC
+		{
+			left:     AllocationFilterCondition{},
+			right:    AllocationFilterCondition{},
+			expected: true,
+		},
+		{
+			left: AllocationFilterCondition{
+				Field: FilterNamespace,
+				Op:    FilterStartsWith,
+				Value: "kubecost-abc",
+			},
+			right: AllocationFilterCondition{
+				Field: FilterNamespace,
+				Op:    FilterStartsWith,
+				Value: "kubecost-abc",
+			},
+			expected: true,
+		},
+		{
+			left: AllocationFilterCondition{
+				Field: FilterLabel,
+				Op:    FilterEquals,
+				Key:   "app",
+				Value: "kubecost-abc",
+			},
+			right: AllocationFilterCondition{
+				Field: FilterLabel,
+				Op:    FilterEquals,
+				Key:   "app",
+				Value: "kubecost-abc",
+			},
+			expected: true,
+		},
+		{
+			left: AllocationFilterCondition{
+				Field: FilterLabel,
+				Op:    FilterEquals,
+				Key:   "app",
+				Value: "kubecost-abc",
+			},
+			right: AllocationFilterCondition{
+				Field: FilterLabel,
+				Op:    FilterEquals,
+				Value: "kubecost-abc",
+			},
+			expected: false,
+		},
+		{
+			left: AllocationFilterCondition{
+				Field: FilterLabel,
+				Op:    FilterEquals,
+				Value: "kubecost-abc",
+			},
+			right: AllocationFilterCondition{
+				Field: FilterLabel,
+				Op:    FilterEquals,
+				Key:   "app",
+				Value: "kubecost-abc",
+			},
+			expected: false,
+		},
+		{
+			left: AllocationFilterCondition{
+				Field: FilterNamespace,
+				Op:    FilterStartsWith,
+				Value: "kubecost-abc",
+			},
+			right: AllocationFilterCondition{
+				Field: FilterNamespace,
+				Op:    FilterStartsWith,
+				Value: "kubecost-abcd",
+			},
+			expected: false,
+		},
+		// OR
+		// EMPTY
+		{
+			left:     AllocationFilterOr{},
+			right:    nil,
+			expected: false,
+		},
+		{
+			left:     AllocationFilterOr{Filters: []AllocationFilter{}},
+			right:    nil,
+			expected: false,
+		},
+
+		{
+			left:     AllocationFilterOr{},
+			right:    AllocationFilterOr{},
+			expected: true,
+		},
+		{
+			left:     AllocationFilterOr{},
+			right:    AllocationFilterOr{Filters: []AllocationFilter{}},
+			expected: true,
+		},
+
+		{
+			left:     AllocationFilterOr{Filters: []AllocationFilter{}},
+			right:    AllocationFilterOr{},
+			expected: true,
+		},
+		{
+			left:     AllocationFilterOr{Filters: []AllocationFilter{}},
+			right:    AllocationFilterOr{Filters: []AllocationFilter{}},
+			expected: true,
+		},
+		// FILLED
+		{
+			left: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			expected: true,
+		},
+		{
+			left: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterNone{},
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+					},
+				},
+			}},
+			expected: true,
+		},
+		{
+			left: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+			}},
+			expected: true,
+		},
+		{
+			left: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns3",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			expected: false,
+		},
+		{
+			left: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterOr{Filters: []AllocationFilter{
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterAnd{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			expected: false,
+		},
+		// AND
+		// EMPTY
+		{
+			left:     AllocationFilterAnd{},
+			right:    nil,
+			expected: false,
+		},
+		{
+			left:     AllocationFilterAnd{Filters: []AllocationFilter{}},
+			right:    nil,
+			expected: false,
+		},
+
+		{
+			left:     AllocationFilterAnd{},
+			right:    AllocationFilterAnd{},
+			expected: true,
+		},
+		{
+			left:     AllocationFilterAnd{},
+			right:    AllocationFilterAnd{Filters: []AllocationFilter{}},
+			expected: true,
+		},
+
+		{
+			left:     AllocationFilterAnd{Filters: []AllocationFilter{}},
+			right:    AllocationFilterAnd{},
+			expected: true,
+		},
+		{
+			left:     AllocationFilterAnd{Filters: []AllocationFilter{}},
+			right:    AllocationFilterAnd{Filters: []AllocationFilter{}},
+			expected: true,
+		},
+		// FILLED
+		{
+			left: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			expected: true,
+		},
+		{
+			left: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterNone{},
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+					},
+				},
+			}},
+			expected: true,
+		},
+		{
+			left: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+			}},
+			expected: true,
+		},
+		{
+			left: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns3",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			expected: false,
+		},
+		{
+			left: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterNone{},
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			right: AllocationFilterAnd{Filters: []AllocationFilter{
+				AllocationFilterCondition{
+					Field: FilterLabel,
+					Op:    FilterStartsWith,
+					Key:   "xyz",
+					Value: "kubecost",
+				},
+				AllocationFilterOr{
+					Filters: []AllocationFilter{
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns1",
+						},
+						AllocationFilterCondition{
+							Field: FilterNamespace,
+							Op:    FilterEquals,
+							Value: "ns2",
+						},
+					},
+				},
+			}},
+			expected: false,
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(fmt.Sprintf("'%s' = '%s'", c.left, c.right), func(t *testing.T) {
+			if c.left.Equals(c.right) != c.expected {
+				t.Fatalf("Expected: %t", c.expected)
+			}
+		})
+	}
+}