internal/filters/filters.go (220 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package filters
import (
"fmt"
"strings"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/errors"
"k8s.io/utils/strings/slices"
)
var (
// ValidTypes are the valid types of Elastic resources that are supported by the filtering system.
ValidTypes = []string{"agent", "apm", "beat", "elasticsearch", "enterprisesearch", "kibana", "maps", "logstash"}
Empty = make(TypeFilters)
)
type Filters interface {
Empty() bool
Matches(lbls map[string]string) bool
Contains(name, typ string) bool
}
func Or(fs ...Filters) Filters {
return &or{fs: fs}
}
type or struct {
fs []Filters
}
// Contains implements Filters.
func (o *or) Contains(name string, typ string) bool {
if o.Empty() {
return true
}
for _, f := range o.fs {
if f.Contains(name, typ) {
return true
}
}
return false
}
// Empty implements Filters.
func (o *or) Empty() bool {
if len(o.fs) == 0 {
return true
}
for _, f := range o.fs {
if f.Empty() {
return true
}
}
return false
}
// Matches implements Filters.
func (o *or) Matches(lbls map[string]string) bool {
if o.Empty() {
return true
}
for _, f := range o.fs {
if f.Matches(lbls) {
return true
}
}
return false
}
var _ Filters = &or{}
// And creates filters that are the logical AND of all passed filters in fs.
func And(fs ...Filters) Filters {
return &and{fs: fs}
}
type and struct {
fs []Filters
}
// Contains implements Filters.
func (a *and) Contains(name string, typ string) bool {
for _, f := range a.fs {
if !f.Contains(name, typ) {
return false
}
}
return true
}
// Empty implements Filters.
func (a *and) Empty() bool {
for _, f := range a.fs {
if !f.Empty() {
return false
}
}
return true
}
// Matches implements Filters.
func (a *and) Matches(lbls map[string]string) bool {
for _, f := range a.fs {
if !f.Matches(lbls) {
return false
}
}
return true
}
var _ Filters = &and{}
// TypeFilters contains a Filter map for each Elastic type given in the filter "source".
type TypeFilters map[string][]TypeFilter
// Empty returns if there are no defined filters.
func (f TypeFilters) Empty() bool {
return len(f) == 0
}
// Matches will determine if the given labels matches any of the
// Filter's label selectors.
func (f TypeFilters) Matches(lbls map[string]string) bool {
// empty set of filters always matches.
if f.Empty() {
return true
}
for _, fs := range f {
for _, filter := range fs {
if filter.Selector.Matches(labels.Set(lbls)) {
return true
}
}
}
return false
}
// Contains will check if any of the filters of named type 'typ'
// contain a filter for an object named 'name'.
func (f TypeFilters) Contains(name, typ string) bool {
if f.Empty() {
return true // empty filter contains everything
}
var (
ok bool
typeFilters []TypeFilter
)
if typeFilters, ok = f[typ]; !ok {
return false
}
for _, filter := range typeFilters {
if filter.Name == name {
return true
}
}
return false
}
// TypeFilter contains a type + name (type = elasticsearch, name = my-cluster)
// and a label selector to easily determine if any queried resource's labels match
// a given filter.
type TypeFilter struct {
Type string
Name string
Selector labels.Selector
}
type LabelFilter []labels.Selector
// Contains implements Filters.
func (s LabelFilter) Contains(_, _ string) bool {
return false
}
// Empty implements Filters.
func (s LabelFilter) Empty() bool {
return len(s) == 0
}
// Matches implements Filters.
func (s LabelFilter) Matches(lbls map[string]string) bool {
if s.Empty() {
return true
}
for _, f := range s {
if f.Matches(labels.Set(lbls)) {
return true
}
}
return false
}
var _ Filters = &LabelFilter{}
// NewTypeFilter returns a new set of filters, given a slice of key=value pairs,
// parses and validates the given filters, and returns an error
// if the given key=value pairs are invalid.
//
// source example:
// []string{"elasticsearch=my-cluster", "kibana=my-kb"}
func NewTypeFilter(source []string) (Filters, error) {
return parse(source)
}
func NewLabelFilter(source []string) (Filters, error) {
if len(source) == 0 {
return Empty, nil
}
selectors, err := parseSelectors(source)
if err != nil {
return Empty, err
}
return LabelFilter(selectors), nil
}
// parse will validate the given source filters, and for each
// filter type, will create a Filter with a label selector,
// returning the set of Filters, and any errors encountered.
func parse(source []string) (Filters, error) {
if len(source) == 0 {
return Empty, nil
}
filters := make(TypeFilters)
var typ, name string
for _, fltr := range source {
filterSlice := strings.Split(fltr, "=")
if len(filterSlice) != 2 {
return filters, fmt.Errorf("invalid filter: %s", fltr)
}
typ, name = filterSlice[0], filterSlice[1]
if err := validateType(typ); err != nil {
return filters, err
}
if _, ok := filters[typ]; ok {
return filters, fmt.Errorf("invalid filter: %s: multiple filters for the same type (%s) are not supported", fltr, typ)
}
selector := buildSelectorForTypeFilter(typ, name)
filters[typ] = append(filters[typ], TypeFilter{
Name: name,
Type: typ,
Selector: selector,
})
}
return filters, nil
}
func parseSelectors(selectorSource []string) ([]labels.Selector, error) {
selectors := make([]labels.Selector, 0, len(selectorSource))
var errs []error
for _, s := range selectorSource {
parsed, err := labels.Parse(s)
if err != nil {
errs = append(errs, err)
continue
}
selectors = append(selectors, parsed)
}
if len(errs) > 0 {
return nil, errors.NewAggregate(errs)
}
return selectors, nil
}
func validateType(typ string) error {
if !slices.Contains(ValidTypes, typ) {
return fmt.Errorf("invalid filter type: %s, supported types: %v", typ, ValidTypes)
}
return nil
}
// buildSelectorForTypeFilter will create a label set for a given type+name pair,
// create a selector from that label set, and add label requirements
// to the selector for each key/value in the label set, returning the selector
// and any errors in processing.
//
// Having a labels.Selector on the Filter allows an easy match operation:
//
// Selector.Match(LabelSet)
//
// against any runtime object's labels.
func buildSelectorForTypeFilter(typ, name string) labels.Selector {
nameAttr := "name"
if typ == "elasticsearch" {
nameAttr = "cluster-name"
}
set := labels.Set{
"common.k8s.elastic.co/type": typ,
fmt.Sprintf("%s.k8s.elastic.co/%s", typ, nameAttr): name,
}
return labels.SelectorFromValidatedSet(set)
}