Files
kustomize/kyaml/fn/framework/patch.go
Katrina Verey 5c4b5b1bf0 Improvements to kyaml fn framework
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.
2021-03-03 08:27:19 -08:00

244 lines
7.6 KiB
Go

// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package framework
import (
"bytes"
"fmt"
"strings"
"text/template"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/yaml"
"sigs.k8s.io/kustomize/kyaml/yaml/merge2"
"sigs.k8s.io/kustomize/kyaml/yaml/walk"
)
// ResourcePatchTemplate applies a patch to a collection of resources
type ResourcePatchTemplate struct {
// Templates is a function that returns a list of templates to render into one or more patches.
Templates TemplatesFunc
// Selector targets the rendered patches to specific resources. If no Selector is provided,
// all resources will be patched.
//
// Although any Filter can be used, this framework provides several especially for Selector use:
// framework.Selector, framework.AndSelector, framework.OrSelector. You can also use any of the
// framework's ResourceMatchers here directly.
Selector kio.Filter
// TemplateData is the data to use when rendering the templates provided by the Templates field.
TemplateData interface{}
}
// DefaultTemplateData sets TemplateData to the provided default values if it has not already
// been set.
func (t *ResourcePatchTemplate) DefaultTemplateData(data interface{}) {
if t.TemplateData == nil {
t.TemplateData = data
}
}
// Filter applies the ResourcePatchTemplate to the appropriate resources in the input.
// First, it applies the Selector to identify target resources. Then, it renders the Templates
// into patches using TemplateData. Finally, it identifies applies the patch to each resource.
func (t ResourcePatchTemplate) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
var err error
target := items
if t.Selector != nil {
target, err = t.Selector.Filter(items)
if err != nil {
return nil, err
}
}
if len(target) == 0 {
// nothing to do
return items, nil
}
if err := t.apply(target); err != nil {
return nil, errors.Wrap(err)
}
return items, nil
}
func (t *ResourcePatchTemplate) apply(matches []*yaml.RNode) error {
templates, err := t.Templates()
if err != nil {
return errors.Wrap(err)
}
var patches []*yaml.RNode
for i := range templates {
newP, err := renderPatches(templates[i], t.TemplateData)
if err != nil {
return errors.Wrap(err)
}
patches = append(patches, newP...)
}
// apply the patches to the matching resources
for j := range matches {
for i := range patches {
matches[j], err = merge2.Merge(patches[i], matches[j], yaml.MergeOptions{})
if err != nil {
return errors.WrapPrefixf(err, "failed to apply templated patch")
}
}
}
return nil
}
// ContainerPatchTemplate defines a patch to be applied to containers
type ContainerPatchTemplate struct {
// Templates is a function that returns a list of templates to render into one or more
// patches that apply at the container level. For example, "name", "env" and "image" would be
// top-level fields in container patches.
Templates TemplatesFunc
// Selector targets the rendered patches to containers within specific resources.
// If no Selector is provided, all resources with containers will be patched (subject to
// ContainerMatcher, if provided).
//
// Although any Filter can be used, this framework provides several especially for Selector use:
// framework.Selector, framework.AndSelector, framework.OrSelector. You can also use any of the
// framework's ResourceMatchers here directly.
Selector kio.Filter
// TemplateData is the data to use when rendering the templates provided by the Templates field.
TemplateData interface{}
// ContainerMatcher targets the rendered patch to only those containers it matches.
// For example, it can be used with ContainerNameMatcher to patch only containers with
// specific names. If no ContainerMatcher is provided, all containers will be patched.
//
// The node passed to ContainerMatcher will be container-level, not a full resource node.
// For example, "name", "env" and "image" would be top level fields.
// To filter based on resource-level context, use the Selector field.
ContainerMatcher func(node *yaml.RNode) bool
}
// DefaultTemplateData sets TemplateData to the provided default values if it has not already
// been set.
func (cpt *ContainerPatchTemplate) DefaultTemplateData(data interface{}) {
if cpt.TemplateData == nil {
cpt.TemplateData = data
}
}
// Filter applies the ContainerPatchTemplate to the appropriate resources in the input.
// First, it applies the Selector to identify target resources. Then, it renders the Templates
// into patches using TemplateData. Finally, it identifies target containers and applies the
// patches.
func (cpt ContainerPatchTemplate) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
var err error
target := items
if cpt.Selector != nil {
target, err = cpt.Selector.Filter(items)
if err != nil {
return nil, err
}
}
if len(target) == 0 {
// nothing to do
return items, nil
}
if err := cpt.apply(target); err != nil {
return nil, err
}
return items, nil
}
// PatchContainers applies the patch to each matching container in each resource.
func (cpt ContainerPatchTemplate) apply(matches []*yaml.RNode) error {
templates, err := cpt.Templates()
if err != nil {
return errors.Wrap(err)
}
var patches []*yaml.RNode
for i := range templates {
newP, err := renderPatches(templates[i], cpt.TemplateData)
if err != nil {
return errors.Wrap(err)
}
patches = append(patches, newP...)
}
for i := range matches {
// TODO(knverey): Make this work for more Kinds and expose the helper for doing so.
containers, err := matches[i].Pipe(yaml.Lookup("spec", "template", "spec", "containers"))
if err != nil {
return errors.Wrap(err)
}
if containers == nil {
continue
}
err = containers.VisitElements(func(node *yaml.RNode) error {
if cpt.ContainerMatcher != nil && !cpt.ContainerMatcher(node) {
return nil
}
for j := range patches {
merger := walk.Walker{
Sources: []*yaml.RNode{node, patches[j]}, // dest, src
Visitor: merge2.Merger{},
MergeOptions: yaml.MergeOptions{},
Schema: openapi.SchemaForResourceType(yaml.TypeMeta{
APIVersion: "v1",
Kind: "Pod",
}).Lookup("spec", "containers").Elements(),
}
_, err = merger.Walk()
if err != nil {
return errors.WrapPrefixf(err, "failed to apply templated patch")
}
}
return nil
})
if err != nil {
return errors.Wrap(err)
}
}
return nil
}
func renderPatches(t *template.Template, data interface{}) ([]*yaml.RNode, error) {
// render the patches
var b bytes.Buffer
if err := t.Execute(&b, data); err != nil {
return nil, errors.WrapPrefixf(err, "failed to render patch template %v", t.DefinedTemplates())
}
// parse the patches into RNodes
var nodes []*yaml.RNode
for _, s := range strings.Split(b.String(), "\n---\n") {
s = strings.TrimSpace(s)
if s == "" {
continue
}
r := &kio.ByteReader{Reader: bytes.NewBufferString(s), OmitReaderAnnotations: true}
newNodes, err := r.Read()
if err != nil {
return nil, errors.WrapPrefixf(err,
"failed to parse rendered patch template into a resource:\n%s\n", addLineNumbers(s))
}
if err := yaml.ErrorIfAnyInvalidAndNonNull(yaml.MappingNode, newNodes...); err != nil {
return nil, errors.WrapPrefixf(err,
"failed to parse rendered patch template into a resource:\n%s\n", addLineNumbers(s))
}
nodes = append(nodes, newNodes...)
}
return nodes, nil
}
func addLineNumbers(s string) string {
lines := strings.Split(s, "\n")
for j := range lines {
lines[j] = fmt.Sprintf("%03d %s", j+1, lines[j])
}
return strings.Join(lines, "\n")
}