mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-10 16:42:51 +00:00
This commit creates a new version of the alpha configuration functions framework. Goals include: - Make it easy to build multi-version APIs with the framework (not previously facilitated at all). - Simplify the framework's APIs where redundant configuration options exist (leaving the most powerful, replacing others with helpers to maintain usability they provided). - Make the Framework's APIs more consistent (e.g. between the various template types, usage of kio.Filter, field names) - Decouple responsibilities (e.g. command creation, resource list processing, generation of templating functions). - Make the framework even more powerfully pluggable (e.g. any kio.Filter can be a selector, and the selector the framework provides is itself a filter built from reusable abstractions). - Improve documentation. - Make container patches merge fields (notably list fields like `env`) correctly.
350 lines
13 KiB
Go
350 lines
13 KiB
Go
// Copyright 2021 The Kubernetes Authors.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package framework
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"sigs.k8s.io/kustomize/kyaml/errors"
|
|
"sigs.k8s.io/kustomize/kyaml/kio"
|
|
"sigs.k8s.io/kustomize/kyaml/sets"
|
|
"sigs.k8s.io/kustomize/kyaml/yaml"
|
|
)
|
|
|
|
// ResourceMatcher is implemented by types designed for use in or as selectors.
|
|
type ResourceMatcher interface {
|
|
// kio.Filter applies the matcher to multiple resources.
|
|
// This makes individual matchers usable as selectors directly.
|
|
kio.Filter
|
|
// Match returns true if the given resource matches the matcher's configuration.
|
|
Match(node *yaml.RNode) bool
|
|
}
|
|
|
|
// ResourceMatcherFunc converts a compliant function into a ResourceMatcher
|
|
type ResourceMatcherFunc func(node *yaml.RNode) bool
|
|
|
|
// Match runs the ResourceMatcherFunc on the given node.
|
|
func (m ResourceMatcherFunc) Match(node *yaml.RNode) bool {
|
|
return m(node)
|
|
}
|
|
|
|
// Filter applies ResourceMatcherFunc to a list of items, returning only those that match.
|
|
func (m ResourceMatcherFunc) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
|
|
// MatchAll or MatchAny doesn't really matter here since there is only one matcher (m).
|
|
return MatchAll(m).Filter(items)
|
|
}
|
|
|
|
// ResourceTemplateMatcher is implemented by ResourceMatcher types that accept text templates as
|
|
// part of their configuration.
|
|
type ResourceTemplateMatcher interface {
|
|
// ResourceMatcher makes matchers usable in or as selectors.
|
|
ResourceMatcher
|
|
// DefaultTemplateData is used to pass default template values down a chain of matchers.
|
|
DefaultTemplateData(interface{})
|
|
// InitTemplates is used to render the templates in selectors that support
|
|
// ResourceTemplateMatcher. The selector should call this exactly once per filter
|
|
// operation, before beginning match comparisons.
|
|
InitTemplates() error
|
|
}
|
|
|
|
// ContainerNameMatcher returns a function that returns true if the "name" field
|
|
// of the provided container node matches one of the given container names.
|
|
// If no names are provided, the function always returns true.
|
|
// Note that this is not a ResourceMatcher, since the node it matches against must be
|
|
// container-level (e.g. "name", "env" and "image" would be top level fields).
|
|
func ContainerNameMatcher(names ...string) func(node *yaml.RNode) bool {
|
|
namesSet := sets.String{}
|
|
namesSet.Insert(names...)
|
|
return func(node *yaml.RNode) bool {
|
|
if len(namesSet) == 0 {
|
|
return true
|
|
}
|
|
f := node.Field("name")
|
|
if f == nil {
|
|
return false
|
|
}
|
|
return namesSet.Has(yaml.GetValue(f.Value))
|
|
}
|
|
}
|
|
|
|
// NameMatcher matches resources whose metadata.name is equal to one of the provided values.
|
|
// e.g. `NameMatcher("foo", "bar")` matches if `metadata.name` is either "foo" or "bar".
|
|
//
|
|
// NameMatcher supports templating.
|
|
// e.g. `NameMatcher("{{.AppName}}")` will match `metadata.name` "foo" if TemplateData is
|
|
// `struct{ AppName string }{ AppName: "foo" }`
|
|
func NameMatcher(names ...string) ResourceTemplateMatcher {
|
|
return &TemplatedMetaSliceMatcher{
|
|
Templates: names,
|
|
MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool {
|
|
return names.Has(meta.Name)
|
|
},
|
|
}
|
|
}
|
|
|
|
// NamespaceMatcher matches resources whose metadata.namespace is equal to one of the provided values.
|
|
// e.g. `NamespaceMatcher("foo", "bar")` matches if `metadata.namespace` is either "foo" or "bar".
|
|
//
|
|
// NamespaceMatcher supports templating.
|
|
// e.g. `NamespaceMatcher("{{.AppName}}")` will match `metadata.namespace` "foo" if TemplateData is
|
|
// `struct{ AppName string }{ AppName: "foo" }`
|
|
func NamespaceMatcher(names ...string) ResourceTemplateMatcher {
|
|
return &TemplatedMetaSliceMatcher{
|
|
Templates: names,
|
|
MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool {
|
|
return names.Has(meta.Namespace)
|
|
},
|
|
}
|
|
}
|
|
|
|
// KindMatcher matches resources whose kind is equal to one of the provided values.
|
|
// e.g. `KindMatcher("foo", "bar")` matches if `kind` is either "foo" or "bar".
|
|
//
|
|
// KindMatcher supports templating.
|
|
// e.g. `KindMatcher("{{.TargetKind}}")` will match `kind` "foo" if TemplateData is
|
|
// `struct{ TargetKind string }{ TargetKind: "foo" }`
|
|
func KindMatcher(names ...string) ResourceTemplateMatcher {
|
|
return &TemplatedMetaSliceMatcher{
|
|
Templates: names,
|
|
MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool {
|
|
return names.Has(meta.Kind)
|
|
},
|
|
}
|
|
}
|
|
|
|
// APIVersionMatcher matches resources whose kind is equal to one of the provided values.
|
|
// e.g. `APIVersionMatcher("foo/v1", "bar/v1")` matches if `apiVersion` is either "foo/v1" or
|
|
// "bar/v1".
|
|
//
|
|
// APIVersionMatcher supports templating.
|
|
// e.g. `APIVersionMatcher("{{.TargetAPI}}")` will match `apiVersion` "foo/v1" if TemplateData is
|
|
// `struct{ TargetAPI string }{ TargetAPI: "foo/v1" }`
|
|
func APIVersionMatcher(names ...string) ResourceTemplateMatcher {
|
|
return &TemplatedMetaSliceMatcher{
|
|
Templates: names,
|
|
MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool {
|
|
return names.Has(meta.APIVersion)
|
|
},
|
|
}
|
|
}
|
|
|
|
// GVKMatcher matches resources whose API group, version and kind match one of the provided values.
|
|
// e.g. `GVKMatcher("foo/v1/Widget", "bar/v1/App")` matches if `apiVersion` concatenated with `kind`
|
|
// is either "foo/v1/Widget" or "bar/v1/App".
|
|
//
|
|
// GVKMatcher supports templating.
|
|
// e.g. `GVKMatcher("{{.TargetAPI}}")` will match "foo/v1/Widget" if TemplateData is
|
|
// `struct{ TargetAPI string }{ TargetAPI: "foo/v1/Widget" }`
|
|
func GVKMatcher(names ...string) ResourceTemplateMatcher {
|
|
return &TemplatedMetaSliceMatcher{
|
|
Templates: names,
|
|
MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool {
|
|
gvk := strings.Join([]string{meta.APIVersion, meta.Kind}, "/")
|
|
return names.Has(gvk)
|
|
},
|
|
}
|
|
}
|
|
|
|
// TemplatedMetaSliceMatcher is a utility type for constructing matchers that compare resource
|
|
// metadata to a slice of (possibly templated) strings.
|
|
type TemplatedMetaSliceMatcher struct {
|
|
// Templates is the list of possibly templated strings to compare to.
|
|
Templates []string
|
|
// values is the set of final (possibly rendered) strings to compare to.
|
|
values sets.String
|
|
// TemplateData is the data to use in template rendering.
|
|
// Rendering will not take place if it is nil when InitTemplates is called.
|
|
TemplateData interface{}
|
|
// MetaMatcher is a function that returns true if the given resource metadata matches at
|
|
// least one of the given names.
|
|
// The matcher implemented using TemplatedMetaSliceMatcher can compare names to any meta field.
|
|
MetaMatcher func(names sets.String, meta yaml.ResourceMeta) bool
|
|
}
|
|
|
|
// Match parses the resource node's metadata and delegates matching logic to the provided
|
|
// MetaMatcher func. This allows ResourceMatchers build with TemplatedMetaSliceMatcher to match
|
|
// against any field in resource metadata.
|
|
func (m *TemplatedMetaSliceMatcher) Match(node *yaml.RNode) bool {
|
|
var err error
|
|
meta, err := node.GetMeta()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return m.MetaMatcher(m.values, meta)
|
|
}
|
|
|
|
// Filter applies the matcher to a list of items, returning only those that match.
|
|
func (m *TemplatedMetaSliceMatcher) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
|
|
// AndSelector or OrSelector doesn't really matter here since there is only one matcher (m).
|
|
s := AndSelector{Matchers: []ResourceMatcher{m}, TemplateData: m.TemplateData}
|
|
return s.Filter(items)
|
|
}
|
|
|
|
// DefaultTemplateData sets TemplateData to the provided default values if it has not already
|
|
// been set.
|
|
func (m *TemplatedMetaSliceMatcher) DefaultTemplateData(data interface{}) {
|
|
if m.TemplateData == nil {
|
|
m.TemplateData = data
|
|
}
|
|
}
|
|
|
|
// InitTemplates is used to render any templates the selector's list of strings may contain
|
|
// before the selector is applied. It should be called exactly once per filter
|
|
// operation, before beginning match comparisons.
|
|
func (m *TemplatedMetaSliceMatcher) InitTemplates() error {
|
|
values, err := templatizeSlice(m.Templates, m.TemplateData)
|
|
if err != nil {
|
|
return errors.Wrap(err)
|
|
}
|
|
m.values = sets.String{}
|
|
m.values.Insert(values...)
|
|
return nil
|
|
}
|
|
|
|
var _ ResourceTemplateMatcher = &TemplatedMetaSliceMatcher{}
|
|
|
|
// LabelMatcher matches resources that are labelled with all of the provided key-value pairs.
|
|
// e.g. `LabelMatcher(map[string]string{"app": "foo", "env": "prod"})` matches resources labelled
|
|
// app=foo AND env=prod.
|
|
//
|
|
// LabelMatcher supports templating.
|
|
// e.g. `LabelMatcher(map[string]string{"app": "{{ .AppName}}"})` will match label app=foo if
|
|
// TemplateData is `struct{ AppName string }{ AppName: "foo" }`
|
|
func LabelMatcher(labels map[string]string) ResourceTemplateMatcher {
|
|
return &TemplatedMetaMapMatcher{
|
|
Templates: labels,
|
|
MetaMatcher: func(labels map[string]string, meta yaml.ResourceMeta) bool {
|
|
return compareMaps(labels, meta.Labels)
|
|
},
|
|
}
|
|
}
|
|
|
|
func compareMaps(desired, actual map[string]string) bool {
|
|
for k := range desired {
|
|
// actual either doesn't have the key or has the wrong value for it
|
|
if actual[k] != desired[k] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// AnnotationMatcher matches resources that are annotated with all of the provided key-value pairs.
|
|
// e.g. `AnnotationMatcher(map[string]string{"app": "foo", "env": "prod"})` matches resources
|
|
// annotated app=foo AND env=prod.
|
|
//
|
|
// AnnotationMatcher supports templating.
|
|
// e.g. `AnnotationMatcher(map[string]string{"app": "{{ .AppName}}"})` will match label app=foo if
|
|
// TemplateData is `struct{ AppName string }{ AppName: "foo" }`
|
|
func AnnotationMatcher(ann map[string]string) ResourceTemplateMatcher {
|
|
return &TemplatedMetaMapMatcher{
|
|
Templates: ann,
|
|
MetaMatcher: func(ann map[string]string, meta yaml.ResourceMeta) bool {
|
|
return compareMaps(ann, meta.Annotations)
|
|
},
|
|
}
|
|
}
|
|
|
|
// TemplatedMetaMapMatcher is a utility type for constructing matchers that compare resource
|
|
// metadata to a map of (possibly templated) key-value pairs.
|
|
type TemplatedMetaMapMatcher struct {
|
|
// Templates is the list of possibly templated strings to compare to.
|
|
Templates map[string]string
|
|
// values is the map of final (possibly rendered) strings to compare to.
|
|
values map[string]string
|
|
// TemplateData is the data to use in template rendering.
|
|
// Rendering will not take place if it is nil when InitTemplates is called.
|
|
TemplateData interface{}
|
|
// MetaMatcher is a function that returns true if the given resource metadata matches at
|
|
// least one of the given names.
|
|
// The matcher implemented using TemplatedMetaSliceMatcher can compare names to any meta field.
|
|
MetaMatcher func(names map[string]string, meta yaml.ResourceMeta) bool
|
|
}
|
|
|
|
// Match parses the resource node's metadata and delegates matching logic to the provided
|
|
// MetaMatcher func. This allows ResourceMatchers build with TemplatedMetaMapMatcher to match
|
|
// against any field in resource metadata.
|
|
func (m *TemplatedMetaMapMatcher) Match(node *yaml.RNode) bool {
|
|
var err error
|
|
meta, err := node.GetMeta()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return m.MetaMatcher(m.values, meta)
|
|
}
|
|
|
|
// DefaultTemplateData sets TemplateData to the provided default values if it has not already
|
|
// been set.
|
|
func (m *TemplatedMetaMapMatcher) DefaultTemplateData(data interface{}) {
|
|
if m.TemplateData == nil {
|
|
m.TemplateData = data
|
|
}
|
|
}
|
|
|
|
// InitTemplates is used to render any templates the selector's key-value pairs may contain
|
|
// before the selector is applied. It should be called exactly once per filter
|
|
// operation, before beginning match comparisons.
|
|
func (m *TemplatedMetaMapMatcher) InitTemplates() error {
|
|
var err error
|
|
m.values, err = templatizeMap(m.Templates, m.TemplateData)
|
|
return errors.Wrap(err)
|
|
}
|
|
|
|
// Filter applies the matcher to a list of items, returning only those that match.
|
|
func (m *TemplatedMetaMapMatcher) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
|
|
// AndSelector or OrSelector doesn't really matter here since there is only one matcher (m).
|
|
s := AndSelector{Matchers: []ResourceMatcher{m}, TemplateData: m.TemplateData}
|
|
return s.Filter(items)
|
|
}
|
|
|
|
var _ ResourceTemplateMatcher = &TemplatedMetaMapMatcher{}
|
|
|
|
func templatizeSlice(values []string, data interface{}) ([]string, error) {
|
|
if data == nil {
|
|
return values, nil
|
|
}
|
|
var err error
|
|
results := make([]string, len(values))
|
|
for i := range values {
|
|
results[i], err = templatize(values[i], data)
|
|
if err != nil {
|
|
return nil, errors.WrapPrefixf(err, "unable to render template %s", values[i])
|
|
}
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func templatizeMap(values map[string]string, data interface{}) (map[string]string, error) {
|
|
if data == nil {
|
|
return values, nil
|
|
}
|
|
var err error
|
|
results := make(map[string]string, len(values))
|
|
|
|
for k := range values {
|
|
results[k], err = templatize(values[k], data)
|
|
if err != nil {
|
|
return nil, errors.WrapPrefixf(err, "unable to render template for %s=%s", k, values[k])
|
|
}
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// templatize renders the value as a template, using the provided data
|
|
func templatize(value string, data interface{}) (string, error) {
|
|
t, err := template.New("kinds").Parse(value)
|
|
if err != nil {
|
|
return "", errors.Wrap(err)
|
|
}
|
|
var b bytes.Buffer
|
|
err = t.Execute(&b, data)
|
|
if err != nil {
|
|
return "", errors.Wrap(err)
|
|
}
|
|
return b.String(), nil
|
|
}
|