pkg/filter/filter.go (263 lines of code) (raw):
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package filter contains methods for selecting and filtering lists of
// components and objects.
package filter
import (
"strings"
bundle "github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/apis/bundle/v1alpha1"
"github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/converter"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// Filter filters the components and objects to produce a new set of components.
type Filter struct{}
// NewFilter creates a new Filter.
func NewFilter() *Filter {
return &Filter{}
}
// Options for filtering bundles. By default, if any of the options match, then
// the relevant component or object is removed. If InvertMatch is set, then the
// objects are kept instead of removed.
type Options struct {
// Kinds represent the Kinds to filter on. Can either be unqualified ("Deployment")
// or qualified ("apps/v1beta1,Pod"). Qualified kinds are often called
// GroupVersionKind in the Kubernetes Schema.
Kinds []string
// Names represent the names to filter on. For objects, this is the
// metadata.name field. For components, this is the ComponentName.
Names []string
// Annotations contain key/value pairs to filter on. An empty string value matches
// all annotation-values for a particular key.
Annotations map[string]string
// Labels contain key/value pairs to filter on. An empty string value matches
// all label-values for a particular key.
Labels map[string]string
// Namespaces to filter on.
Namespaces []string
// InvertMatch indicates wether to return the opposite match.
InvertMatch bool
}
// OptionsFromObjectSelector creates an Options Object from a
func OptionsFromObjectSelector(sel *bundle.ObjectSelector) *Options {
// TODO(kashomon): Should probably make a copy.
if sel == nil {
return nil
}
opts := &Options{
Kinds: sel.Kinds,
Names: sel.Names,
Annotations: sel.Annotations,
Labels: sel.Labels,
Namespaces: sel.Namespaces,
}
if sel.InvertMatch != nil {
opts.InvertMatch = *sel.InvertMatch
}
return opts
}
// FilterComponents removes components based on the ObjectMeta properties of
// the components, returning a new cluster bundle with just filtered
// components. Filtering for components doesn't take into account the
// properties of the object-children of the components. This is the opposite
// matching from SelectComponents.
func (f *Filter) FilterComponents(data []*bundle.Component, o *Options) []*bundle.Component {
_, notMatched := f.PartitionComponents(data, o)
return notMatched
}
// SelectComponents picks components based on the ObjectMeta properties of the
// components, returning a new cluster bundle with just filtered components.
// Filtering for components doesn't take into account the properties of the
// object-children of the components. This performs the opposte matching from
// FilterComponents.
func (f *Filter) SelectComponents(data []*bundle.Component, o *Options) []*bundle.Component {
matched, _ := f.PartitionComponents(data, o)
return matched
}
// PartitionComponents splits the components into matched and not matched sets.
// PartitionComponents ignores the InvertMatch option, since both matched and
// unmatched objects are returned. Thus, the options to partition are always
// treated as options for matching objects.
func (f *Filter) PartitionComponents(data []*bundle.Component, o *Options) ([]*bundle.Component, []*bundle.Component) {
var newData []*bundle.Component
for _, cp := range data {
newData = append(newData, cp.DeepCopy())
}
data = newData
// nil options should not imply any change.
if o == nil {
return data, nil
}
var matched []*bundle.Component
var notMatched []*bundle.Component
for _, c := range data {
if MatchesComponent(c, o) {
matched = append(matched, c)
} else {
notMatched = append(notMatched, c)
}
}
return matched, notMatched
}
// FilterObjects removes objects based on the ObjectMeta properties of the objects,
// returning a new list with just filtered objects. This performs the opposite match from SelectObjects.
func (f *Filter) FilterObjects(data []*unstructured.Unstructured, o *Options) []*unstructured.Unstructured {
_, notMatched := f.PartitionObjects(data, o)
return notMatched
}
// SelectObjects picks objects based on the ObjectMeta properties of the objects,
// returning a new list with just filtered objects.
func (f *Filter) SelectObjects(data []*unstructured.Unstructured, o *Options) []*unstructured.Unstructured {
matched, _ := f.PartitionObjects(data, o)
return matched
}
// PartitionObjects splits the objects into matched and not matched sets.
// PartitionObjects ignores the KeepOnly option, since both matched and
// unmatched objects are returned. Thus, the options to partition are always
// treated as options for matching objects.
func (f *Filter) PartitionObjects(data []*unstructured.Unstructured, o *Options) ([]*unstructured.Unstructured, []*unstructured.Unstructured) {
var newData []*unstructured.Unstructured
for _, oj := range data {
newData = append(newData, oj.DeepCopy())
}
data = newData
// nil options should not imply any change.
if o == nil {
return data, nil
}
var matched []*unstructured.Unstructured
var notMatched []*unstructured.Unstructured
for _, cp := range data {
if MatchesObject(cp, o) {
matched = append(matched, cp)
} else {
notMatched = append(notMatched, cp)
}
}
return matched, notMatched
}
// objectData contains data about the object being filtered.
type objectData struct {
// APIVersion of the object.
apiVersion string
// Kind of the object.
kind string
// name of the object. For unstructured objects, this is is metadata.name.
// For components, this is ComponentName
name string
// meta is the ObjectMeta for an object.
meta *metav1.ObjectMeta
}
// objectDataFromComponent returns ObjectData created from a component.
func objectDataFromComponent(c *bundle.Component) *objectData {
return &objectData{
apiVersion: c.APIVersion,
kind: c.Kind,
name: c.Spec.ComponentName,
meta: c.ObjectMeta.DeepCopy(),
}
}
// newObjectData returns ObjectData created from an unstructured Object.
func newObjectData(uns *unstructured.Unstructured) *objectData {
return &objectData{
apiVersion: uns.GetAPIVersion(),
kind: uns.GetKind(),
name: uns.GetName(),
meta: converter.FromUnstructured(uns).ExtractObjectMeta(),
}
}
// MatchesComponent returns true if the conditions match a Component.
func MatchesComponent(c *bundle.Component, o *Options) bool {
return matches(objectDataFromComponent(c), o)
}
// MatchesObject returns true if the conditions match an object.
func MatchesObject(obj *unstructured.Unstructured, o *Options) bool {
return matches(newObjectData(obj), o)
}
// Matches returns whether an object matches the given. The match functions
// does an AND of ORS. In otherwords:
//
// (name1 OR name2 OR name3) AND
// (kind2 OR kind2 OR kind3) AND etc.
func matches(d *objectData, o *Options) bool {
if o == nil {
return true
}
matchesKinds := true
if len(o.Kinds) > 0 {
matchesKinds = false
for _, optk := range o.Kinds {
objKind := d.kind
if strings.ContainsRune(optk, ',') {
// Assume this is a Qualified Kind match of the form
// "apps/v1beta1,Deployment". Commas shouldn't be normally in a kind.
objKind = d.apiVersion + "," + d.kind
}
if optk == objKind {
matchesKinds = true
break
}
}
}
matchesNS := true
if len(o.Namespaces) > 0 {
matchesNS = false
for _, optn := range o.Namespaces {
if optn == d.meta.Namespace {
matchesNS = true
break
}
}
}
matchesNames := true
if len(o.Names) > 0 {
matchesNames = false
for _, optn := range o.Names {
if optn == d.meta.GetName() {
matchesNames = true
break
}
}
}
matchesAnnot := true
if len(o.Annotations) > 0 {
matchesAnnot = false
for key, v := range o.Annotations {
if val, ok := d.meta.Annotations[key]; ok && val == v {
matchesAnnot = true
break
}
}
}
matchesLabels := true
if len(o.Labels) > 0 {
matchesLabels = false
for key, v := range o.Labels {
if val, ok := d.meta.Labels[key]; ok && val == v {
matchesLabels = true
break
}
}
}
matches := matchesKinds && matchesNS && matchesNames && matchesAnnot && matchesLabels
if o.InvertMatch {
return !matches
}
return matches
}
// ComponentPredicate is a func that returns true for components that match
// criteria and false otherwise.
type ComponentPredicate func(*bundle.Component) bool
// Select takes a list of components and a predicate and returns the
// components that match the predicate.
func Select(components []*bundle.Component, predicate ComponentPredicate) []*bundle.Component {
var out []*bundle.Component
for _, component := range components {
if predicate(component) {
out = append(out, component)
}
}
return out
}
// ComponentFieldMatchIn takes a []string and see if the field matches one of
// those.
func ComponentFieldMatchIn(matchList []string, fieldGetter func(*bundle.Component) string) ComponentPredicate {
return func(component *bundle.Component) bool {
fv := fieldGetter(component)
for _, match := range matchList {
if fv == match {
return true
}
}
return false
}
}
// ObjectFieldMatchIn takes a []string and sees if any object in the component
// matches one of those.
func ObjectFieldMatchIn(matchList []string, objectGetter func(*unstructured.Unstructured) string) ComponentPredicate {
return func(component *bundle.Component) bool {
for _, obj := range component.Spec.Objects {
ov := objectGetter(obj)
for _, match := range matchList {
if ov == match {
return true
}
}
}
return false
}
}
// And returns true if every ComponentPredicate function returns true and
// returns false otherwise.
func And(componentPredicates ...ComponentPredicate) ComponentPredicate {
return func(component *bundle.Component) bool {
for _, predicate := range componentPredicates {
if !predicate(component) {
return false
}
}
return true
}
}
// Or returns true if any ComponentPredicate function returns true and
// returns false otherwise.
func Or(componentPredicates ...ComponentPredicate) ComponentPredicate {
return func(component *bundle.Component) bool {
for _, predicate := range componentPredicates {
if predicate(component) {
return true
}
}
return false
}
}
// Not returns a ComponentPredicate that negates it's result.
func Not(predicate ComponentPredicate) ComponentPredicate {
return func(component *bundle.Component) bool {
return !predicate(component)
}
}
// SelectObjects returns components with only the objects that match the
// predicate.
func SelectObjects(components []*bundle.Component, predicate ComponentPredicate) []*bundle.Component {
var out []*bundle.Component
for _, component := range components {
temp := component.DeepCopy()
var selectedObjects []*unstructured.Unstructured
for _, object := range component.Spec.Objects {
temp.Spec.Objects = []*unstructured.Unstructured{object}
if predicate(temp) {
selectedObjects = append(selectedObjects, object)
}
}
if len(selectedObjects) == 0 {
continue
}
newComp := component.DeepCopy()
newComp.Spec.Objects = selectedObjects
out = append(out, newComp)
}
return out
}