...

Source file src/sigs.k8s.io/kustomize/kyaml/fn/framework/matchers.go

Documentation: sigs.k8s.io/kustomize/kyaml/fn/framework

     1  // Copyright 2021 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package framework
     5  
     6  import (
     7  	"bytes"
     8  	"strings"
     9  	"text/template"
    10  
    11  	"sigs.k8s.io/kustomize/kyaml/errors"
    12  	"sigs.k8s.io/kustomize/kyaml/kio"
    13  	"sigs.k8s.io/kustomize/kyaml/sets"
    14  	"sigs.k8s.io/kustomize/kyaml/yaml"
    15  )
    16  
    17  // ResourceMatcher is implemented by types designed for use in or as selectors.
    18  type ResourceMatcher interface {
    19  	// kio.Filter applies the matcher to multiple resources.
    20  	// This makes individual matchers usable as selectors directly.
    21  	kio.Filter
    22  	// Match returns true if the given resource matches the matcher's configuration.
    23  	Match(node *yaml.RNode) bool
    24  }
    25  
    26  // ResourceMatcherFunc converts a compliant function into a ResourceMatcher
    27  type ResourceMatcherFunc func(node *yaml.RNode) bool
    28  
    29  // Match runs the ResourceMatcherFunc on the given node.
    30  func (m ResourceMatcherFunc) Match(node *yaml.RNode) bool {
    31  	return m(node)
    32  }
    33  
    34  // Filter applies ResourceMatcherFunc to a list of items, returning only those that match.
    35  func (m ResourceMatcherFunc) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
    36  	// MatchAll or MatchAny doesn't really matter here since there is only one matcher (m).
    37  	return MatchAll(m).Filter(items)
    38  }
    39  
    40  // ResourceTemplateMatcher is implemented by ResourceMatcher types that accept text templates as
    41  // part of their configuration.
    42  type ResourceTemplateMatcher interface {
    43  	// ResourceMatcher makes matchers usable in or as selectors.
    44  	ResourceMatcher
    45  	// DefaultTemplateData is used to pass default template values down a chain of matchers.
    46  	DefaultTemplateData(interface{})
    47  	// InitTemplates is used to render the templates in selectors that support
    48  	// ResourceTemplateMatcher. The selector should call this exactly once per filter
    49  	// operation, before beginning match comparisons.
    50  	InitTemplates() error
    51  }
    52  
    53  // ContainerNameMatcher returns a function that returns true if the "name" field
    54  // of the provided container node matches one of the given container names.
    55  // If no names are provided, the function always returns true.
    56  // Note that this is not a ResourceMatcher, since the node it matches against must be
    57  // container-level (e.g. "name", "env" and "image" would be top level fields).
    58  func ContainerNameMatcher(names ...string) func(node *yaml.RNode) bool {
    59  	namesSet := sets.String{}
    60  	namesSet.Insert(names...)
    61  	return func(node *yaml.RNode) bool {
    62  		if len(namesSet) == 0 {
    63  			return true
    64  		}
    65  		f := node.Field("name")
    66  		if f == nil {
    67  			return false
    68  		}
    69  		return namesSet.Has(yaml.GetValue(f.Value))
    70  	}
    71  }
    72  
    73  // NameMatcher matches resources whose metadata.name is equal to one of the provided values.
    74  // e.g. `NameMatcher("foo", "bar")` matches if `metadata.name` is either "foo" or "bar".
    75  //
    76  // NameMatcher supports templating.
    77  // e.g. `NameMatcher("{{.AppName}}")` will match `metadata.name` "foo" if TemplateData is
    78  // `struct{ AppName string }{ AppName: "foo" }`
    79  func NameMatcher(names ...string) ResourceTemplateMatcher {
    80  	return &TemplatedMetaSliceMatcher{
    81  		Templates: names,
    82  		MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool {
    83  			return names.Has(meta.Name)
    84  		},
    85  	}
    86  }
    87  
    88  // NamespaceMatcher matches resources whose metadata.namespace is equal to one of the provided values.
    89  // e.g. `NamespaceMatcher("foo", "bar")` matches if `metadata.namespace` is either "foo" or "bar".
    90  //
    91  // NamespaceMatcher supports templating.
    92  // e.g. `NamespaceMatcher("{{.AppName}}")` will match `metadata.namespace` "foo" if TemplateData is
    93  // `struct{ AppName string }{ AppName: "foo" }`
    94  func NamespaceMatcher(names ...string) ResourceTemplateMatcher {
    95  	return &TemplatedMetaSliceMatcher{
    96  		Templates: names,
    97  		MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool {
    98  			return names.Has(meta.Namespace)
    99  		},
   100  	}
   101  }
   102  
   103  // KindMatcher matches resources whose kind is equal to one of the provided values.
   104  // e.g. `KindMatcher("foo", "bar")` matches if `kind` is either "foo" or "bar".
   105  //
   106  // KindMatcher supports templating.
   107  // e.g. `KindMatcher("{{.TargetKind}}")` will match `kind` "foo" if TemplateData is
   108  // `struct{ TargetKind string }{ TargetKind: "foo" }`
   109  func KindMatcher(names ...string) ResourceTemplateMatcher {
   110  	return &TemplatedMetaSliceMatcher{
   111  		Templates: names,
   112  		MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool {
   113  			return names.Has(meta.Kind)
   114  		},
   115  	}
   116  }
   117  
   118  // APIVersionMatcher matches resources whose kind is equal to one of the provided values.
   119  // e.g. `APIVersionMatcher("foo/v1", "bar/v1")` matches if `apiVersion` is either "foo/v1" or
   120  // "bar/v1".
   121  //
   122  // APIVersionMatcher supports templating.
   123  // e.g. `APIVersionMatcher("{{.TargetAPI}}")` will match `apiVersion` "foo/v1" if TemplateData is
   124  // `struct{ TargetAPI string }{ TargetAPI: "foo/v1" }`
   125  func APIVersionMatcher(names ...string) ResourceTemplateMatcher {
   126  	return &TemplatedMetaSliceMatcher{
   127  		Templates: names,
   128  		MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool {
   129  			return names.Has(meta.APIVersion)
   130  		},
   131  	}
   132  }
   133  
   134  // GVKMatcher matches resources whose API group, version and kind match one of the provided values.
   135  // e.g. `GVKMatcher("foo/v1/Widget", "bar/v1/App")` matches if `apiVersion` concatenated with `kind`
   136  // is either "foo/v1/Widget" or "bar/v1/App".
   137  //
   138  // GVKMatcher supports templating.
   139  // e.g. `GVKMatcher("{{.TargetAPI}}")` will match "foo/v1/Widget" if TemplateData is
   140  // `struct{ TargetAPI string }{ TargetAPI: "foo/v1/Widget" }`
   141  func GVKMatcher(names ...string) ResourceTemplateMatcher {
   142  	return &TemplatedMetaSliceMatcher{
   143  		Templates: names,
   144  		MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool {
   145  			gvk := strings.Join([]string{meta.APIVersion, meta.Kind}, "/")
   146  			return names.Has(gvk)
   147  		},
   148  	}
   149  }
   150  
   151  // TemplatedMetaSliceMatcher is a utility type for constructing matchers that compare resource
   152  // metadata to a slice of (possibly templated) strings.
   153  type TemplatedMetaSliceMatcher struct {
   154  	// Templates is the list of possibly templated strings to compare to.
   155  	Templates []string
   156  	// values is the set of final (possibly rendered) strings to compare to.
   157  	values sets.String
   158  	// TemplateData is the data to use in template rendering.
   159  	// Rendering will not take place if it is nil when InitTemplates is called.
   160  	TemplateData interface{}
   161  	// MetaMatcher is a function that returns true if the given resource metadata matches at
   162  	// least one of the given names.
   163  	// The matcher implemented using TemplatedMetaSliceMatcher can compare names to any meta field.
   164  	MetaMatcher func(names sets.String, meta yaml.ResourceMeta) bool
   165  }
   166  
   167  // Match parses the resource node's metadata and delegates matching logic to the provided
   168  // MetaMatcher func. This allows ResourceMatchers build with TemplatedMetaSliceMatcher to match
   169  // against any field in resource metadata.
   170  func (m *TemplatedMetaSliceMatcher) Match(node *yaml.RNode) bool {
   171  	var err error
   172  	meta, err := node.GetMeta()
   173  	if err != nil {
   174  		return false
   175  	}
   176  	return m.MetaMatcher(m.values, meta)
   177  }
   178  
   179  // Filter applies the matcher to a list of items, returning only those that match.
   180  func (m *TemplatedMetaSliceMatcher) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
   181  	// AndSelector or OrSelector doesn't really matter here since there is only one matcher (m).
   182  	s := AndSelector{Matchers: []ResourceMatcher{m}, TemplateData: m.TemplateData}
   183  	return s.Filter(items)
   184  }
   185  
   186  // DefaultTemplateData sets TemplateData to the provided default values if it has not already
   187  // been set.
   188  func (m *TemplatedMetaSliceMatcher) DefaultTemplateData(data interface{}) {
   189  	if m.TemplateData == nil {
   190  		m.TemplateData = data
   191  	}
   192  }
   193  
   194  // InitTemplates is used to render any templates the selector's list of strings may contain
   195  // before the selector is applied. It should be called exactly once per filter
   196  // operation, before beginning match comparisons.
   197  func (m *TemplatedMetaSliceMatcher) InitTemplates() error {
   198  	values, err := templatizeSlice(m.Templates, m.TemplateData)
   199  	if err != nil {
   200  		return errors.Wrap(err)
   201  	}
   202  	m.values = sets.String{}
   203  	m.values.Insert(values...)
   204  	return nil
   205  }
   206  
   207  var _ ResourceTemplateMatcher = &TemplatedMetaSliceMatcher{}
   208  
   209  // LabelMatcher matches resources that are labelled with all of the provided key-value pairs.
   210  // e.g. `LabelMatcher(map[string]string{"app": "foo", "env": "prod"})` matches resources labelled
   211  // app=foo AND env=prod.
   212  //
   213  // LabelMatcher supports templating.
   214  // e.g. `LabelMatcher(map[string]string{"app": "{{ .AppName}}"})` will match label app=foo if
   215  // TemplateData is `struct{ AppName string }{ AppName: "foo" }`
   216  func LabelMatcher(labels map[string]string) ResourceTemplateMatcher {
   217  	return &TemplatedMetaMapMatcher{
   218  		Templates: labels,
   219  		MetaMatcher: func(labels map[string]string, meta yaml.ResourceMeta) bool {
   220  			return compareMaps(labels, meta.Labels)
   221  		},
   222  	}
   223  }
   224  
   225  func compareMaps(desired, actual map[string]string) bool {
   226  	for k := range desired {
   227  		// actual either doesn't have the key or has the wrong value for it
   228  		if actual[k] != desired[k] {
   229  			return false
   230  		}
   231  	}
   232  	return true
   233  }
   234  
   235  // AnnotationMatcher matches resources that are annotated with all of the provided key-value pairs.
   236  // e.g. `AnnotationMatcher(map[string]string{"app": "foo", "env": "prod"})` matches resources
   237  // annotated app=foo AND env=prod.
   238  //
   239  // AnnotationMatcher supports templating.
   240  // e.g. `AnnotationMatcher(map[string]string{"app": "{{ .AppName}}"})` will match label app=foo if
   241  // TemplateData is `struct{ AppName string }{ AppName: "foo" }`
   242  func AnnotationMatcher(ann map[string]string) ResourceTemplateMatcher {
   243  	return &TemplatedMetaMapMatcher{
   244  		Templates: ann,
   245  		MetaMatcher: func(ann map[string]string, meta yaml.ResourceMeta) bool {
   246  			return compareMaps(ann, meta.Annotations)
   247  		},
   248  	}
   249  }
   250  
   251  // TemplatedMetaMapMatcher is a utility type for constructing matchers that compare resource
   252  // metadata to a map of (possibly templated) key-value pairs.
   253  type TemplatedMetaMapMatcher struct {
   254  	// Templates is the list of possibly templated strings to compare to.
   255  	Templates map[string]string
   256  	// values is the map of final (possibly rendered) strings to compare to.
   257  	values map[string]string
   258  	// TemplateData is the data to use in template rendering.
   259  	// Rendering will not take place if it is nil when InitTemplates is called.
   260  	TemplateData interface{}
   261  	// MetaMatcher is a function that returns true if the given resource metadata matches at
   262  	// least one of the given names.
   263  	// The matcher implemented using TemplatedMetaSliceMatcher can compare names to any meta field.
   264  	MetaMatcher func(names map[string]string, meta yaml.ResourceMeta) bool
   265  }
   266  
   267  // Match parses the resource node's metadata and delegates matching logic to the provided
   268  // MetaMatcher func. This allows ResourceMatchers build with TemplatedMetaMapMatcher to match
   269  // against any field in resource metadata.
   270  func (m *TemplatedMetaMapMatcher) Match(node *yaml.RNode) bool {
   271  	var err error
   272  	meta, err := node.GetMeta()
   273  	if err != nil {
   274  		return false
   275  	}
   276  
   277  	return m.MetaMatcher(m.values, meta)
   278  }
   279  
   280  // DefaultTemplateData sets TemplateData to the provided default values if it has not already
   281  // been set.
   282  func (m *TemplatedMetaMapMatcher) DefaultTemplateData(data interface{}) {
   283  	if m.TemplateData == nil {
   284  		m.TemplateData = data
   285  	}
   286  }
   287  
   288  // InitTemplates is used to render any templates the selector's key-value pairs may contain
   289  // before the selector is applied. It should be called exactly once per filter
   290  // operation, before beginning match comparisons.
   291  func (m *TemplatedMetaMapMatcher) InitTemplates() error {
   292  	var err error
   293  	m.values, err = templatizeMap(m.Templates, m.TemplateData)
   294  	return errors.Wrap(err)
   295  }
   296  
   297  // Filter applies the matcher to a list of items, returning only those that match.
   298  func (m *TemplatedMetaMapMatcher) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
   299  	// AndSelector or OrSelector doesn't really matter here since there is only one matcher (m).
   300  	s := AndSelector{Matchers: []ResourceMatcher{m}, TemplateData: m.TemplateData}
   301  	return s.Filter(items)
   302  }
   303  
   304  var _ ResourceTemplateMatcher = &TemplatedMetaMapMatcher{}
   305  
   306  func templatizeSlice(values []string, data interface{}) ([]string, error) {
   307  	if data == nil {
   308  		return values, nil
   309  	}
   310  	var err error
   311  	results := make([]string, len(values))
   312  	for i := range values {
   313  		results[i], err = templatize(values[i], data)
   314  		if err != nil {
   315  			return nil, errors.WrapPrefixf(err, "unable to render template %s", values[i])
   316  		}
   317  	}
   318  	return results, nil
   319  }
   320  
   321  func templatizeMap(values map[string]string, data interface{}) (map[string]string, error) {
   322  	if data == nil {
   323  		return values, nil
   324  	}
   325  	var err error
   326  	results := make(map[string]string, len(values))
   327  
   328  	for k := range values {
   329  		results[k], err = templatize(values[k], data)
   330  		if err != nil {
   331  			return nil, errors.WrapPrefixf(err, "unable to render template for %s=%s", k, values[k])
   332  		}
   333  	}
   334  	return results, nil
   335  }
   336  
   337  // templatize renders the value as a template, using the provided data
   338  func templatize(value string, data interface{}) (string, error) {
   339  	t, err := template.New("kinds").Parse(value)
   340  	if err != nil {
   341  		return "", errors.Wrap(err)
   342  	}
   343  	var b bytes.Buffer
   344  	err = t.Execute(&b, data)
   345  	if err != nil {
   346  		return "", errors.Wrap(err)
   347  	}
   348  	return b.String(), nil
   349  }
   350  

View as plain text