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

Initial HTTP filter -> AllocationFilter functions

Michael Dresser 4 лет назад
Родитель
Сommit
a3841d7d74
2 измененных файлов с 504 добавлено и 0 удалено
  1. 224 0
      pkg/util/filterutil/allocationfilters.go
  2. 280 0
      pkg/util/filterutil/allocationfilters_test.go

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

@@ -0,0 +1,224 @@
+package filterutil
+
+import (
+	"strings"
+
+	"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"
+)
+
+func FiltersFromParamsV1(qp httputil.QueryParams) kubecost.AllocationFilter {
+	if qp.Get("filter", "") != "" {
+		// TODO: short-circuit to a query language parser if the filter= param is
+		// present.
+	}
+
+	// TODO: wildcard handling
+
+	filter := kubecost.AllocationFilterAnd{
+		Filters: []kubecost.AllocationFilter{},
+	}
+
+	// TODO: remove comment
+	// The following is adapted from KCM's original pkg/allocation/filters.go
+
+	// Load Label Config
+	// Pull a LabelConfig from the app configuration, or default if
+	// configuration is unavailable.
+	// labelConfig := kubecost.NewLabelConfig()
+
+	// TODO: label config from analyzer in OSS?
+	// cfg, err := config.GetAnalyzerConfig()
+	// if err != nil {
+	// 	log.Warnf("AnalyzerConfig is nil")
+	// } else {
+	// 	labelConfig = cfg.LabelConfig()
+	// }
+
+	filterClusters := qp.GetList("filterClusters", ",")
+	filter.Filters = append(filter.Filters,
+		filterV1SingleValueFromList(filterClusters, kubecost.FilterClusterID),
+	)
+	// TODO: OR by cluster name
+	// Cluster Map doesn't seem to have a name -> ID mapping,
+	// only an ID (from the allocation) -> name mapping
+
+	// generate a filter func for each node filter, and OR the results
+	filterNodes := qp.GetList("filterNodes", ",")
+	filter.Filters = append(filter.Filters,
+		filterV1SingleValueFromList(filterNodes, kubecost.FilterNode),
+	)
+
+	// generate a filter func for each namespace filter, and OR the results
+	filterNamespaces := qp.GetList("filterNamespaces", ",")
+	filter.Filters = append(filter.Filters,
+		filterV1SingleValueFromList(filterNamespaces, kubecost.FilterNamespace),
+	)
+
+	filterControllerKinds := qp.GetList("filterControllerKinds", ",")
+	filter.Filters = append(filter.Filters,
+		filterV1SingleValueFromList(filterControllerKinds, kubecost.FilterControllerKind),
+	)
+
+	filterControllers := qp.GetList("filterControllers", ",")
+	filter.Filters = append(filter.Filters,
+		filterV1SingleValueFromList(filterControllers, kubecost.FilterControllerName),
+	)
+	// TODO: controllerkind:controllername filter e.g. "deployment:kubecost"
+
+	filterPods := qp.GetList("filterPods", ",")
+	filter.Filters = append(filter.Filters,
+		filterV1SingleValueFromList(filterPods, kubecost.FilterPod),
+	)
+
+	filterContainers := qp.GetList("filterContainers", ",")
+	filter.Filters = append(filter.Filters,
+		filterV1SingleValueFromList(filterContainers, kubecost.FilterContainer),
+	)
+
+	// TODO: label mapping special things
+	// filterDepartments := qp.GetList("filterDepartments", ",")
+	// if len(filterDepartments) > 0 {
+	// 	subFilter := kubecost.AllocationFilterOr{
+	// 		Filters: []kubecost.AllocationFilter{},
+	// 	}
+
+	// 	for _, filter := range filterDepartments {
+	// 		ffs = append(ffs, GetDepartmentFilterFunc(labelConfig, filter))
+	// 	}
+	// 	filter.Filters = append(filter.Filters, subFilter)
+	// }
+
+	// filterEnvironments := qp.GetList("filterEnvironments", ",")
+	// if len(filterEnvironments) > 0 {
+	// 	subFilter := kubecost.AllocationFilterOr{
+	// 		Filters: []kubecost.AllocationFilter{},
+	// 	}
+
+	// 	for _, filter := range filterEnvironments {
+	// 		ffs = append(ffs, GetEnvironmentFilterFunc(labelConfig, filter))
+	// 	}
+	// 	filter.Filters = append(filter.Filters, subFilter)
+	// }
+
+	// filterOwners := qp.GetList("filterOwners", ",")
+	// if len(filterOwners) > 0 {
+	// 	subFilter := kubecost.AllocationFilterOr{
+	// 		Filters: []kubecost.AllocationFilter{},
+	// 	}
+
+	// 	for _, filter := range filterOwners {
+	// 		ffs = append(ffs, GetOwnerFilterFunc(labelConfig, filter))
+	// 	}
+	// 	filter.Filters = append(filter.Filters, subFilter)
+	// }
+
+	// filterProducts := qp.GetList("filterProducts", ",")
+	// if len(filterProducts) > 0 {
+	// 	subFilter := kubecost.AllocationFilterOr{
+	// 		Filters: []kubecost.AllocationFilter{},
+	// 	}
+
+	// 	for _, filter := range filterProducts {
+	// 		ffs = append(ffs, GetProductFilterFunc(labelConfig, filter))
+	// 	}
+	// 	filter.Filters = append(filter.Filters, subFilter)
+	// }
+
+	// filterTeams := qp.GetList("filterTeams", ",")
+	// if len(filterTeams) > 0 {
+	// 	subFilter := kubecost.AllocationFilterOr{
+	// 		Filters: []kubecost.AllocationFilter{},
+	// 	}
+
+	// 	for _, filter := range filterTeams {
+	// 		ffs = append(ffs, GetTeamFilterFunc(labelConfig, filter))
+	// 	}
+	// 	filter.Filters = append(filter.Filters, subFilter)
+	// }
+
+	filterAnnotations := qp.GetList("filterAnnotations", ",")
+	filter.Filters = append(filter.Filters,
+		filterV1DoubleValueFromList(filterAnnotations, kubecost.FilterAnnotation),
+	)
+
+	filterLabels := qp.GetList("filterLabels", ",")
+	filter.Filters = append(filter.Filters,
+		filterV1DoubleValueFromList(filterLabels, kubecost.FilterLabel),
+	)
+
+	// TODO: filter service condition
+	// filterServices := qp.GetList("filterServices", ",")
+	// if len(filterServices) > 0 {
+	// 	subFilter := kubecost.AllocationFilterOr{
+	// 		Filters: []kubecost.AllocationFilter{},
+	// 	}
+
+	// 	for _, filter := range filterServices {
+	// 		ffs = append(ffs, GetServiceFilterFunc(filter))
+	// 	}
+	// 	filter.Filters = append(filter.Filters, subFilter)
+	// }
+
+	return filter
+}
+
+// TODO: comment
+// We don't need the filter op because all filter V1 comparisons are equality
+func filterV1SingleValueFromList(rawFilterValues []string, filterField kubecost.FilterField) kubecost.AllocationFilter {
+	// The v1 query language (e.g. "filterNamespaces=XYZ,ABC") uses or within
+	// a field (e.g. namespace = XYZ OR namespace = ABC)
+	filter := kubecost.AllocationFilterOr{
+		Filters: []kubecost.AllocationFilter{},
+	}
+
+	for _, filterValue := range rawFilterValues {
+		filterValue = strings.TrimSpace(filterValue)
+
+		filter.Filters = append(filter.Filters,
+			kubecost.AllocationFilterCondition{
+				Field: filterField,
+				Op:    kubecost.FilterEquals,
+				Value: filterValue,
+			},
+		)
+	}
+
+	return filter
+}
+
+// TODO: comment
+// We don't need the filter op because all filter V1 comparisons are equality
+func filterV1DoubleValueFromList(rawFilterValuesUnsplit []string, filterField kubecost.FilterField) kubecost.AllocationFilter {
+
+	// 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)
+	filter := kubecost.AllocationFilterOr{
+		Filters: []kubecost.AllocationFilter{},
+	}
+
+	for _, unsplit := range rawFilterValuesUnsplit {
+		if unsplit != "" {
+			split := strings.Split(unsplit, ":")
+			if len(split) != 2 {
+				log.Warningf("illegal key/value filter (ignoring): %s", unsplit)
+				continue
+			}
+			key := prom.SanitizeLabelName(strings.TrimSpace(split[0]))
+			val := strings.TrimSpace(split[1])
+
+			filter.Filters = append(filter.Filters,
+				kubecost.AllocationFilterCondition{
+					Field: filterField,
+					Op:    kubecost.FilterEquals,
+					Key:   key,
+					Value: val,
+				},
+			)
+		}
+	}
+
+	return filter
+}

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

@@ -0,0 +1,280 @@
+package filterutil
+
+import (
+	"testing"
+
+	"github.com/kubecost/cost-model/pkg/kubecost"
+	"github.com/kubecost/cost-model/pkg/util/mapper"
+)
+
+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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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",
+					},
+				}),
+			},
+		},
+	}
+
+	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)
+
+			filter := FiltersFromParamsV1(qpMapper)
+			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)
+				}
+			}
+		})
+	}
+}