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

Merge pull request #1210 from kubecost/mmd/old-filter-syntax-to-filters-v2-internal

Add support for translating current filters (filterNamespaces=) to the new AllocationFilter types
Michael Dresser 4 лет назад
Родитель
Сommit
6ddf06ef1b

+ 65 - 4
pkg/kubecost/allocationfilter.go

@@ -1,6 +1,10 @@
 package kubecost
 
-import "github.com/kubecost/cost-model/pkg/log"
+import (
+	"strings"
+
+	"github.com/kubecost/cost-model/pkg/log"
+)
 
 // FilterField is an enum that represents Allocation-specific fields that can be
 // filtered on (namespace, label, etc.)
@@ -35,9 +39,29 @@ 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    FilterOp = "equals"
-	FilterNotEquals          = "notequals"
-	FilterContains           = "contains"
+	// 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
@@ -189,6 +213,43 @@ func (filter AllocationFilterCondition) Matches(a *Allocation) bool {
 		} 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

+ 60 - 0
pkg/kubecost/allocationfilter_test.go

@@ -27,6 +27,36 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 
 			expected: true,
 		},
+		{
+			name: "ClusterID StartsWith -> true",
+			a: &Allocation{
+				Properties: &AllocationProperties{
+					Cluster: "cluster-one",
+				},
+			},
+			filter: AllocationFilterCondition{
+				Field: FilterClusterID,
+				Op:    FilterStartsWith,
+				Value: "cluster",
+			},
+
+			expected: true,
+		},
+		{
+			name: "ClusterID StartsWith -> false",
+			a: &Allocation{
+				Properties: &AllocationProperties{
+					Cluster: "k8s-one",
+				},
+			},
+			filter: AllocationFilterCondition{
+				Field: FilterClusterID,
+				Op:    FilterStartsWith,
+				Value: "cluster",
+			},
+
+			expected: false,
+		},
 		{
 			name: "Node Equals -> true",
 			a: &Allocation{
@@ -351,6 +381,36 @@ func Test_AllocationFilterCondition_Matches(t *testing.T) {
 
 			expected: false,
 		},
+		{
+			name: `services containsprefix -> true`,
+			a: &Allocation{
+				Properties: &AllocationProperties{
+					Services: []string{"serv1", "serv2"},
+				},
+			},
+			filter: AllocationFilterCondition{
+				Field: FilterServices,
+				Op:    FilterContainsPrefix,
+				Value: "serv",
+			},
+
+			expected: true,
+		},
+		{
+			name: `services containsprefix -> false`,
+			a: &Allocation{
+				Properties: &AllocationProperties{
+					Services: []string{"foo", "bar"},
+				},
+			},
+			filter: AllocationFilterCondition{
+				Field: FilterServices,
+				Op:    FilterContainsPrefix,
+				Value: "serv",
+			},
+
+			expected: false,
+		},
 		{
 			name: `services contains unallocated -> false`,
 			a: &Allocation{

+ 339 - 0
pkg/util/filterutil/allocationfilters.go

@@ -0,0 +1,339 @@
+package filterutil
+
+import (
+	"strings"
+
+	"github.com/kubecost/cost-model/pkg/costmodel/clusters"
+	"github.com/kubecost/cost-model/pkg/kubecost"
+	"github.com/kubecost/cost-model/pkg/log"
+	"github.com/kubecost/cost-model/pkg/prom"
+	"github.com/kubecost/cost-model/pkg/util/httputil"
+)
+
+// parseWildcardEnd checks if the given filter value is wildcarded, meaning
+// it ends in "*". If it does, it removes the suffix and returns the cleaned
+// string and true. Otherwise, it returns the same filter and false.
+//
+// parseWildcardEnd("kube*") = "kube", true
+// parseWildcardEnd("kube") = "kube", false
+func parseWildcardEnd(rawFilterValue string) (string, bool) {
+	return strings.TrimSuffix(rawFilterValue, "*"), strings.HasSuffix(rawFilterValue, "*")
+}
+
+// AllocationFilterFromParamsV1 takes a set of HTTP query parameters and
+// converts them to an AllocationFilter, which is a structured in-Go
+// representation of a set of filters.
+//
+// The HTTP query parameters are the "v1" filters attached to the Allocation
+// API: "filterNamespaces=", "filterNodes=", etc.
+//
+// It takes an optional LabelConfig, which if provided enables "label-mapped"
+// filters like "filterDepartments".
+//
+// It takes an optional ClusterMap, which if provided enables cluster name
+// filtering. This turns all `filterClusters=foo` arguments into the equivalent
+// of `clusterID = "foo" OR clusterName = "foo"`.
+func AllocationFilterFromParamsV1(
+	qp httputil.QueryParams,
+	labelConfig *kubecost.LabelConfig,
+	clusterMap clusters.ClusterMap,
+) kubecost.AllocationFilter {
+
+	filter := kubecost.AllocationFilterAnd{
+		Filters: []kubecost.AllocationFilter{},
+	}
+
+	// ClusterMap does not provide a cluster name -> cluster ID mapping in the
+	// interface, probably because there could be multiple IDs with the same
+	// name. However, V1 filter logic demands that the parameters to
+	// filterClusters= be checked against both cluster ID AND cluster name.
+	//
+	// To support expected filterClusters= behavior, we construct a mapping
+	// of cluster name -> cluster IDs (could be multiple IDs for the same name)
+	// so that we can create AllocationFilters that use only ClusterIDEquals.
+	//
+	//
+	// AllocationFilter intentionally does not support cluster name filters
+	// because those should be considered presentation-layer only.
+	clusterNameToIDs := map[string][]string{}
+	if clusterMap != nil {
+		cMap := clusterMap.AsMap()
+		for _, info := range cMap {
+			if info == nil {
+				continue
+			}
+
+			if _, ok := clusterNameToIDs[info.Name]; ok {
+				clusterNameToIDs[info.Name] = append(clusterNameToIDs[info.Name], info.ID)
+			} else {
+				clusterNameToIDs[info.Name] = []string{info.ID}
+			}
+		}
+	}
+
+	// The proliferation of > 0 guards in the function is to avoid constructing
+	// empty filter structs. While it is functionally equivalent to add empty
+	// filter structs (they evaluate to true always) there could be overhead
+	// when calling Matches() repeatedly for no purpose.
+
+	if filterClusters := qp.GetList("filterClusters", ","); len(filterClusters) > 0 {
+		clustersOr := kubecost.AllocationFilterOr{
+			Filters: []kubecost.AllocationFilter{},
+		}
+
+		if idFilters := filterV1SingleValueFromList(filterClusters, kubecost.FilterClusterID); len(idFilters.Filters) > 0 {
+			clustersOr.Filters = append(clustersOr.Filters, idFilters)
+		}
+		for _, rawFilterValue := range filterClusters {
+			clusterNameFilter, wildcard := parseWildcardEnd(rawFilterValue)
+
+			clusterIDsToFilter := []string{}
+			for clusterName := range clusterNameToIDs {
+				if wildcard && strings.HasPrefix(clusterName, clusterNameFilter) {
+					clusterIDsToFilter = append(clusterIDsToFilter, clusterNameToIDs[clusterName]...)
+				} else if !wildcard && clusterName == clusterNameFilter {
+					clusterIDsToFilter = append(clusterIDsToFilter, clusterNameToIDs[clusterName]...)
+				}
+			}
+
+			for _, clusterID := range clusterIDsToFilter {
+				clustersOr.Filters = append(clustersOr.Filters,
+					kubecost.AllocationFilterCondition{
+						Field: kubecost.FilterClusterID,
+						Op:    kubecost.FilterEquals,
+						Value: clusterID,
+					},
+				)
+			}
+		}
+		filter.Filters = append(filter.Filters, clustersOr)
+	}
+
+	if raw := qp.GetList("filterNodes", ","); len(raw) > 0 {
+		filter.Filters = append(filter.Filters, filterV1SingleValueFromList(raw, kubecost.FilterNode))
+	}
+
+	if raw := qp.GetList("filterNamespaces", ","); len(raw) > 0 {
+		filter.Filters = append(filter.Filters, filterV1SingleValueFromList(raw, kubecost.FilterNamespace))
+	}
+
+	if raw := qp.GetList("filterControllerKinds", ","); len(raw) > 0 {
+		filter.Filters = append(filter.Filters, filterV1SingleValueFromList(raw, kubecost.FilterControllerKind))
+	}
+
+	// filterControllers= accepts controllerkind:controllername filters, e.g.
+	// "deployment:kubecost-cost-analyzer"
+	//
+	// Thus, we have to make a custom OR filter for this condition.
+	if filterControllers := qp.GetList("filterControllers", ","); len(filterControllers) > 0 {
+		controllersOr := kubecost.AllocationFilterOr{
+			Filters: []kubecost.AllocationFilter{},
+		}
+		for _, rawFilterValue := range filterControllers {
+			split := strings.Split(rawFilterValue, ":")
+			if len(split) == 1 {
+				filterValue, wildcard := parseWildcardEnd(split[0])
+				subFilter := kubecost.AllocationFilterCondition{
+					Field: kubecost.FilterControllerName,
+					Op:    kubecost.FilterEquals,
+					Value: filterValue,
+				}
+
+				if wildcard {
+					subFilter.Op = kubecost.FilterStartsWith
+				}
+				controllersOr.Filters = append(controllersOr.Filters, subFilter)
+			} else if len(split) == 2 {
+				kindFilterVal := split[0]
+				nameFilterVal, wildcard := parseWildcardEnd(split[1])
+
+				kindFilter := kubecost.AllocationFilterCondition{
+					Field: kubecost.FilterControllerKind,
+					Op:    kubecost.FilterEquals,
+					Value: kindFilterVal,
+				}
+				nameFilter := kubecost.AllocationFilterCondition{
+					Field: kubecost.FilterControllerName,
+					Op:    kubecost.FilterEquals,
+					Value: nameFilterVal,
+				}
+
+				if wildcard {
+					nameFilter.Op = kubecost.FilterStartsWith
+				}
+
+				// The controller name AND the controller kind must match
+				multiFilter := kubecost.AllocationFilterAnd{
+					Filters: []kubecost.AllocationFilter{kindFilter, nameFilter},
+				}
+				controllersOr.Filters = append(controllersOr.Filters, multiFilter)
+			} else {
+				log.Warnf("illegal filter for controller: %s", rawFilterValue)
+			}
+		}
+		if len(controllersOr.Filters) > 0 {
+			filter.Filters = append(filter.Filters, controllersOr)
+		}
+	}
+
+	if raw := qp.GetList("filterPods", ","); len(raw) > 0 {
+		filter.Filters = append(filter.Filters, filterV1SingleValueFromList(raw, kubecost.FilterPod))
+	}
+
+	if raw := qp.GetList("filterContainers", ","); len(raw) > 0 {
+		filter.Filters = append(filter.Filters, filterV1SingleValueFromList(raw, kubecost.FilterContainer))
+	}
+
+	// Label-mapped queries require a label config to be present.
+	if labelConfig != nil {
+		if raw := qp.GetList("filterDepartments", ","); len(raw) > 0 {
+			filter.Filters = append(filter.Filters, filterV1LabelMappedFromList(raw, labelConfig.DepartmentLabel))
+		}
+		if raw := qp.GetList("filterEnvironments", ","); len(raw) > 0 {
+			filter.Filters = append(filter.Filters, filterV1LabelMappedFromList(raw, labelConfig.EnvironmentLabel))
+		}
+		if raw := qp.GetList("filterOwners", ","); len(raw) > 0 {
+			filter.Filters = append(filter.Filters, filterV1LabelMappedFromList(raw, labelConfig.OwnerLabel))
+		}
+		if raw := qp.GetList("filterProducts", ","); len(raw) > 0 {
+			filter.Filters = append(filter.Filters, filterV1LabelMappedFromList(raw, labelConfig.ProductLabel))
+		}
+		if raw := qp.GetList("filterTeams", ","); len(raw) > 0 {
+			filter.Filters = append(filter.Filters, filterV1LabelMappedFromList(raw, labelConfig.TeamLabel))
+		}
+	} else {
+		log.Debugf("No label config is available. Not creating filters for label-mapped 'fields'.")
+	}
+
+	if raw := qp.GetList("filterAnnotations", ","); len(raw) > 0 {
+		filter.Filters = append(filter.Filters, filterV1DoubleValueFromList(raw, kubecost.FilterAnnotation))
+	}
+
+	if raw := qp.GetList("filterLabels", ","); len(raw) > 0 {
+		filter.Filters = append(filter.Filters, filterV1DoubleValueFromList(raw, kubecost.FilterLabel))
+	}
+
+	if filterServices := qp.GetList("filterServices", ","); len(filterServices) > 0 {
+		// filterServices= is the only filter that uses the "contains" operator.
+		servicesFilter := kubecost.AllocationFilterOr{
+			Filters: []kubecost.AllocationFilter{},
+		}
+		for _, filterValue := range filterServices {
+			// TODO: wildcard support
+			filterValue, wildcard := parseWildcardEnd(filterValue)
+			subFilter := kubecost.AllocationFilterCondition{
+				Field: kubecost.FilterServices,
+				Op:    kubecost.FilterContains,
+				Value: filterValue,
+			}
+			if wildcard {
+				subFilter.Op = kubecost.FilterContainsPrefix
+			}
+			servicesFilter.Filters = append(servicesFilter.Filters, subFilter)
+		}
+		filter.Filters = append(filter.Filters, servicesFilter)
+	}
+
+	return filter
+}
+
+// filterV1SingleValueFromList creates an OR of equality filters for a given
+// filter field.
+//
+// The v1 query language (e.g. "filterNamespaces=XYZ,ABC") uses OR within
+// a field (e.g. namespace = XYZ OR namespace = ABC)
+func filterV1SingleValueFromList(rawFilterValues []string, filterField kubecost.FilterField) kubecost.AllocationFilterOr {
+	filter := kubecost.AllocationFilterOr{
+		Filters: []kubecost.AllocationFilter{},
+	}
+
+	for _, filterValue := range rawFilterValues {
+		filterValue = strings.TrimSpace(filterValue)
+		filterValue, wildcard := parseWildcardEnd(filterValue)
+
+		subFilter := kubecost.AllocationFilterCondition{
+			Field: filterField,
+			// All v1 filters are equality comparisons
+			Op:    kubecost.FilterEquals,
+			Value: filterValue,
+		}
+
+		if wildcard {
+			subFilter.Op = kubecost.FilterStartsWith
+		}
+
+		filter.Filters = append(filter.Filters, subFilter)
+	}
+
+	return filter
+}
+
+// filterV1LabelMappedFromList is like filterV1SingleValueFromList but is
+// explicitly for a label because "label-mapped" filters (like filterTeams=)
+// are actually label filters with a fixed label key.
+func filterV1LabelMappedFromList(rawFilterValues []string, labelName string) kubecost.AllocationFilterOr {
+	filter := kubecost.AllocationFilterOr{
+		Filters: []kubecost.AllocationFilter{},
+	}
+
+	for _, filterValue := range rawFilterValues {
+		filterValue = strings.TrimSpace(filterValue)
+		filterValue, wildcard := parseWildcardEnd(filterValue)
+
+		subFilter := kubecost.AllocationFilterCondition{
+			Field: kubecost.FilterLabel,
+			// All v1 filters are equality comparisons
+			Op:    kubecost.FilterEquals,
+			Key:   labelName,
+			Value: filterValue,
+		}
+
+		if wildcard {
+			subFilter.Op = kubecost.FilterStartsWith
+		}
+
+		filter.Filters = append(filter.Filters, subFilter)
+	}
+
+	return filter
+}
+
+// filterV1DoubleValueFromList creates an OR of key:value equality filters for
+// colon-split filter values.
+//
+// The v1 query language (e.g. "filterLabels=app:foo,l2:bar") uses OR within
+// a field (e.g. label[app] = foo OR label[l2] = bar)
+func filterV1DoubleValueFromList(rawFilterValuesUnsplit []string, filterField kubecost.FilterField) kubecost.AllocationFilterOr {
+	filter := kubecost.AllocationFilterOr{
+		Filters: []kubecost.AllocationFilter{},
+	}
+
+	for _, unsplit := range rawFilterValuesUnsplit {
+		if unsplit != "" {
+			split := strings.Split(unsplit, ":")
+			if len(split) != 2 {
+				log.Warnf("illegal key/value filter (ignoring): %s", unsplit)
+				continue
+			}
+			key := prom.SanitizeLabelName(strings.TrimSpace(split[0]))
+			val := strings.TrimSpace(split[1])
+			val, wildcard := parseWildcardEnd(val)
+
+			subFilter := kubecost.AllocationFilterCondition{
+				Field: filterField,
+				// All v1 filters are equality comparisons
+				Op:    kubecost.FilterEquals,
+				Key:   key,
+				Value: val,
+			}
+
+			if wildcard {
+				subFilter.Op = kubecost.FilterStartsWith
+			}
+
+			filter.Filters = append(filter.Filters, subFilter)
+		}
+	}
+
+	return filter
+}

+ 668 - 0
pkg/util/filterutil/allocationfilters_test.go

@@ -0,0 +1,668 @@
+package filterutil
+
+import (
+	"testing"
+
+	"github.com/kubecost/cost-model/pkg/costmodel/clusters"
+	"github.com/kubecost/cost-model/pkg/kubecost"
+	"github.com/kubecost/cost-model/pkg/util/mapper"
+)
+
+type mockClusterMap struct {
+	m map[string]*clusters.ClusterInfo
+}
+
+func (mcp mockClusterMap) GetClusterIDs() []string {
+	panic("unimplemented")
+}
+
+func (mcp mockClusterMap) AsMap() map[string]*clusters.ClusterInfo {
+	return mcp.m
+}
+
+func (mcp mockClusterMap) InfoFor(clusterID string) *clusters.ClusterInfo {
+	panic("unimplemented")
+}
+
+func (mcp mockClusterMap) NameFor(clusterID string) string {
+	panic("unimplemented")
+}
+func (mcp mockClusterMap) NameIDFor(clusterID string) string {
+	panic("unimplemented")
+}
+func (mcp mockClusterMap) SplitNameID(nameID string) (string, string) {
+	panic("unimplemented")
+}
+
+func (mcp mockClusterMap) StopRefresh() {}
+
+func allocGenerator(props kubecost.AllocationProperties) kubecost.Allocation {
+	a := kubecost.Allocation{
+		Properties: &props,
+	}
+
+	a.Name = a.Properties.String()
+	return a
+}
+
+func TestFiltersFromParamsV1(t *testing.T) {
+	// TODO: __unallocated__ case?
+	cases := []struct {
+		name           string
+		qp             map[string]string
+		shouldMatch    []kubecost.Allocation
+		shouldNotMatch []kubecost.Allocation
+	}{
+		{
+			name: "single cluster ID",
+			qp: map[string]string{
+				"filterClusters": "cluster-one",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Cluster: "cluster-one",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Cluster: "foo",
+				}),
+			},
+		},
+		{
+			name: "wildcard cluster ID",
+			qp: map[string]string{
+				"filterClusters": "cluster*",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Cluster: "cluster-one",
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					Cluster: "cluster-two",
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					Cluster: "cluster",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Cluster: "foo",
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					Cluster: "cluste",
+				}),
+			},
+		},
+		{
+			name: "single cluster name",
+			qp: map[string]string{
+				"filterClusters": "cluster ABC",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Cluster: "mapped-cluster-ID-ABC",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Cluster: "cluster-one",
+				}),
+			},
+		},
+		{
+			name: "wildcard cluster name",
+			qp: map[string]string{
+				"filterClusters": "cluster A*",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Cluster: "mapped-cluster-ID-ABC",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Cluster: "cluster-one",
+				}),
+			},
+		},
+		{
+			name: "single node",
+			qp: map[string]string{
+				"filterNodes": "node-123-abc",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Node: "node-123-abc",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Node: "node-456-def",
+				}),
+			},
+		},
+		{
+			name: "wildcard node",
+			qp: map[string]string{
+				"filterNodes": "node-1*",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Node: "node-123-abc",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Node: "node-456-def",
+				}),
+			},
+		},
+		{
+			name: "single namespace",
+			qp: map[string]string{
+				"filterNamespaces": "kubecost",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Namespace: "kubecost",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Namespace: "kubecost2",
+				}),
+			},
+		},
+		{
+			name: "wildcard namespace",
+			qp: map[string]string{
+				"filterNamespaces": "kube*",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Namespace: "kubecost",
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					Namespace: "kube-system",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Namespace: "kub",
+				}),
+			},
+		},
+		{
+			name: "single controller kind",
+			qp: map[string]string{
+				"filterControllerKinds": "deployment",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					ControllerKind: "deployment",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					ControllerKind: "daemonset",
+				}),
+			},
+		},
+		{
+			name: "wildcard controller kind",
+			qp: map[string]string{
+				"filterControllerKinds": "depl*",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					ControllerKind: "deployment",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					ControllerKind: "daemonset",
+				}),
+			},
+		},
+		{
+			name: "single controller name",
+			qp: map[string]string{
+				"filterControllers": "kubecost-cost-analyzer",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Controller: "kubecost-cost-analyzer",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Controller: "kube-proxy",
+				}),
+			},
+		},
+		{
+			name: "wildcard controller name",
+			qp: map[string]string{
+				"filterControllers": "kubecost-*",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Controller: "kubecost-cost-analyzer",
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					Controller: "kubecost-frontend",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Controller: "kube-proxy",
+				}),
+			},
+		},
+		{
+			name: "single controller kind:name combo",
+			qp: map[string]string{
+				"filterControllers": "deployment:kubecost-cost-analyzer",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					ControllerKind: "deployment",
+					Controller:     "kubecost-cost-analyzer",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					ControllerKind: "daemonset",
+					Controller:     "kubecost-cost-analyzer",
+				}),
+			},
+		},
+		{
+			name: "wildcard controller kind:name combo",
+			qp: map[string]string{
+				"filterControllers": "deployment:kubecost*",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					ControllerKind: "deployment",
+					Controller:     "kubecost-cost-analyzer",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					ControllerKind: "daemonset",
+					Controller:     "kubecost-cost-analyzer",
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					ControllerKind: "deployment",
+					Controller:     "kube-system",
+				}),
+			},
+		},
+		{
+			name: "single pod",
+			qp: map[string]string{
+				"filterPods": "pod-123-abc",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Pod: "pod-123-abc",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Pod: "pod-456-def",
+				}),
+			},
+		},
+		{
+			name: "wildcard pod",
+			qp: map[string]string{
+				"filterPods": "pod-1*",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Pod: "pod-123-abc",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Pod: "pod-456-def",
+				}),
+			},
+		},
+		{
+			name: "single container",
+			qp: map[string]string{
+				"filterContainers": "container-123-abc",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Container: "container-123-abc",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Container: "container-456-def",
+				}),
+			},
+		},
+		{
+			name: "wildcard container",
+			qp: map[string]string{
+				"filterContainers": "container-1*",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Container: "container-123-abc",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Container: "container-456-def",
+				}),
+			},
+		},
+		{
+			name: "single department",
+			qp: map[string]string{
+				"filterDepartments": "pa-1",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"internal-product-umbrella": "pa-1",
+					},
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"internal-product-umbrella": "ps-N",
+					},
+				}),
+			},
+		},
+		{
+			name: "wildcard department",
+			qp: map[string]string{
+				"filterDepartments": "pa*",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"internal-product-umbrella": "pa-1",
+					},
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"internal-product-umbrella": "ps-N",
+					},
+				}),
+			},
+		},
+		{
+			name: "single label",
+			qp: map[string]string{
+				"filterLabels": "app:cost-analyzer",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"app": "cost-analyzer",
+					},
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"app": "foo",
+					},
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"foo": "bar",
+					},
+				}),
+			},
+		},
+		{
+			name: "wildcard label",
+			qp: map[string]string{
+				"filterLabels": "app:cost-*",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"app": "cost-analyzer",
+					},
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"app": "foo",
+					},
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"foo": "bar",
+					},
+				}),
+			},
+		},
+		{
+			name: "single annotation",
+			qp: map[string]string{
+				"filterAnnotations": "app:cost-analyzer",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Annotations: map[string]string{
+						"app": "cost-analyzer",
+					},
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Annotations: map[string]string{
+						"app": "foo",
+					},
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					Annotations: map[string]string{
+						"foo": "bar",
+					},
+				}),
+			},
+		},
+		{
+			name: "wildcard annotation",
+			qp: map[string]string{
+				"filterAnnotations": "app:cost-*",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Annotations: map[string]string{
+						"app": "cost-analyzer",
+					},
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Annotations: map[string]string{
+						"app": "foo",
+					},
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					Annotations: map[string]string{
+						"foo": "bar",
+					},
+				}),
+			},
+		},
+		{
+			name: "single service",
+			qp: map[string]string{
+				"filterServices": "serv1",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Services: []string{"serv1"},
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{}),
+				allocGenerator(kubecost.AllocationProperties{
+					Services: []string{"serv2"},
+				}),
+			},
+		},
+		{
+			name: "multi service",
+			qp: map[string]string{
+				"filterServices": "serv1,serv3",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Services: []string{"serv1"},
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					Services: []string{"serv2", "serv3"},
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{}),
+				allocGenerator(kubecost.AllocationProperties{
+					Services: []string{"serv2"},
+				}),
+			},
+		},
+		{
+			name: "wildcard service",
+			qp: map[string]string{
+				"filterServices": "serv*",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Services: []string{"serv1"},
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					Services: []string{"serv2"},
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{}),
+				allocGenerator(kubecost.AllocationProperties{
+					Services: []string{"foo"},
+				}),
+			},
+		},
+		{
+			name: "multi: namespaces, labels",
+			qp: map[string]string{
+				"filterNamespaces": "kube-system,kubecost",
+				"filterLabels":     "app:cost-analyzer,app:kube-proxy,foo:bar",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Namespace: "kubecost",
+					Labels: map[string]string{
+						"app": "cost-analyzer",
+					},
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					Namespace: "kubecost",
+					Labels: map[string]string{
+						"foo": "bar",
+					},
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					Namespace: "kube-system",
+					Labels: map[string]string{
+						"app": "kube-proxy",
+					},
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Namespace: "kubecost",
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					Namespace: "kubecost",
+					Labels: map[string]string{
+						"app": "something",
+					},
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"app": "foo",
+					},
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"foo": "bar",
+					},
+				}),
+			},
+		},
+		{
+			name: "cluster name OR cluster ID",
+			qp: map[string]string{
+				"filterClusters": "cluster ABC,cluster-one",
+			},
+			shouldMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Cluster: "mapped-cluster-ID-ABC",
+				}),
+				allocGenerator(kubecost.AllocationProperties{
+					Cluster: "cluster-one",
+				}),
+			},
+			shouldNotMatch: []kubecost.Allocation{
+				allocGenerator(kubecost.AllocationProperties{
+					Cluster: "cluster",
+				}),
+			},
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			// Convert map[string]string representation to the mapper
+			// library type
+			qpMap := mapper.NewMap()
+			for k, v := range c.qp {
+				qpMap.Set(k, v)
+			}
+			qpMapper := mapper.NewMapper(qpMap)
+
+			labelConfig := kubecost.LabelConfig{}
+			labelConfig.DepartmentLabel = "internal-product-umbrella"
+
+			clustersMap := mockClusterMap{
+				m: map[string]*clusters.ClusterInfo{
+					"mapped-cluster-ID-1": {
+						ID:   "mapped-cluster-ID-ABC",
+						Name: "cluster ABC",
+					},
+				},
+			}
+
+			filter := AllocationFilterFromParamsV1(qpMapper, &labelConfig, clustersMap)
+			for _, alloc := range c.shouldMatch {
+				if !filter.Matches(&alloc) {
+					t.Errorf("should have matched: %s", alloc.Name)
+				}
+			}
+			for _, alloc := range c.shouldNotMatch {
+				if filter.Matches(&alloc) {
+					t.Errorf("incorrectly matched: %s", alloc.Name)
+				}
+			}
+		})
+	}
+}