working commit

This commit is contained in:
2026-03-13 19:02:42 +02:00
parent bebbf79c7a
commit 5c1da77f4c
1329 changed files with 314708 additions and 39 deletions
+349
View File
@@ -0,0 +1,349 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kio
import (
"bytes"
"fmt"
"io"
"regexp"
"sort"
"strings"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
const (
ResourceListKind = "ResourceList"
ResourceListAPIVersion = "config.kubernetes.io/v1"
)
// ByteReadWriter reads from an input and writes to an output.
type ByteReadWriter struct {
// Reader is where ResourceNodes are decoded from.
Reader io.Reader
// Writer is where ResourceNodes are encoded.
Writer io.Writer
// OmitReaderAnnotations will configures Read to skip setting the config.kubernetes.io/index
// annotation on Resources as they are Read.
OmitReaderAnnotations bool
// KeepReaderAnnotations if set will keep the Reader specific annotations when writing
// the Resources, otherwise they will be cleared.
KeepReaderAnnotations bool
// PreserveSeqIndent if true adds kioutil.SeqIndentAnnotation to each resource
PreserveSeqIndent bool
// Style is a style that is set on the Resource Node Document.
Style yaml.Style
// WrapBareSeqNode wraps the bare sequence node document with map node,
// kyaml uses reader annotations to track resources, it is not possible to
// add them to bare sequence nodes, this option enables wrapping such bare
// sequence nodes into map node with key yaml.BareSeqNodeWrappingKey
// note that this wrapping is different and not related to ResourceList wrapping
WrapBareSeqNode bool
FunctionConfig *yaml.RNode
Results *yaml.RNode
NoWrap bool
WrappingAPIVersion string
WrappingKind string
}
func (rw *ByteReadWriter) Read() ([]*yaml.RNode, error) {
b := &ByteReader{
Reader: rw.Reader,
OmitReaderAnnotations: rw.OmitReaderAnnotations,
PreserveSeqIndent: rw.PreserveSeqIndent,
WrapBareSeqNode: rw.WrapBareSeqNode,
}
val, err := b.Read()
rw.Results = b.Results
if rw.FunctionConfig == nil {
rw.FunctionConfig = b.FunctionConfig
}
if !rw.NoWrap && rw.WrappingKind == "" {
rw.WrappingAPIVersion = b.WrappingAPIVersion
rw.WrappingKind = b.WrappingKind
}
return val, errors.Wrap(err)
}
func (rw *ByteReadWriter) Write(nodes []*yaml.RNode) error {
w := ByteWriter{
Writer: rw.Writer,
KeepReaderAnnotations: rw.KeepReaderAnnotations,
Style: rw.Style,
FunctionConfig: rw.FunctionConfig,
Results: rw.Results,
}
if !rw.NoWrap {
w.WrappingAPIVersion = rw.WrappingAPIVersion
w.WrappingKind = rw.WrappingKind
}
return w.Write(nodes)
}
// ParseAll reads all of the inputs into resources
func ParseAll(inputs ...string) ([]*yaml.RNode, error) {
return (&ByteReader{
Reader: bytes.NewBufferString(strings.Join(inputs, "\n---\n")),
}).Read()
}
// FromBytes reads from a byte slice.
func FromBytes(bs []byte) ([]*yaml.RNode, error) {
return (&ByteReader{
OmitReaderAnnotations: true,
AnchorsAweigh: true,
Reader: bytes.NewBuffer(bs),
}).Read()
}
// StringAll writes all of the resources to a string
func StringAll(resources []*yaml.RNode) (string, error) {
var b bytes.Buffer
err := (&ByteWriter{Writer: &b}).Write(resources)
return b.String(), err
}
// ByteReader decodes ResourceNodes from bytes.
// By default, Read will set the config.kubernetes.io/index annotation on each RNode as it
// is read so they can be written back in the same order.
type ByteReader struct {
// Reader is where ResourceNodes are decoded from.
Reader io.Reader
// OmitReaderAnnotations will configures Read to skip setting the config.kubernetes.io/index
// and internal.config.kubernetes.io/seqindent annotations on Resources as they are Read.
OmitReaderAnnotations bool
// PreserveSeqIndent if true adds kioutil.SeqIndentAnnotation to each resource
PreserveSeqIndent bool
// SetAnnotations is a map of caller specified annotations to set on resources as they are read
// These are independent of the annotations controlled by OmitReaderAnnotations
SetAnnotations map[string]string
FunctionConfig *yaml.RNode
Results *yaml.RNode
// DisableUnwrapping prevents Resources in Lists and ResourceLists from being unwrapped
DisableUnwrapping bool
// WrappingAPIVersion is set by Read(), and is the apiVersion of the object that
// the read objects were originally wrapped in.
WrappingAPIVersion string
// WrappingKind is set by Read(), and is the kind of the object that
// the read objects were originally wrapped in.
WrappingKind string
// WrapBareSeqNode wraps the bare sequence node document with map node,
// kyaml uses reader annotations to track resources, it is not possible to
// add them to bare sequence nodes, this option enables wrapping such bare
// sequence nodes into map node with key yaml.BareSeqNodeWrappingKey
// note that this wrapping is different and not related to ResourceList wrapping
WrapBareSeqNode bool
// AnchorsAweigh set to true attempts to replace all YAML anchor aliases
// with their definitions (anchor values) immediately after the read.
AnchorsAweigh bool
}
var _ Reader = &ByteReader{}
// splitDocuments returns a slice of all documents contained in a YAML string. Multiple documents can be divided by the
// YAML document separator (---). It allows for white space and comments to be after the separator on the same line,
// but will return an error if anything else is on the line.
func splitDocuments(s string) ([]string, error) {
docs := make([]string, 0)
if len(s) > 0 {
// The YAML document separator is any line that starts with ---
yamlSeparatorRegexp := regexp.MustCompile(`\n---.*\n`)
// Find all separators, check them for invalid content, and append each document to docs
separatorLocations := yamlSeparatorRegexp.FindAllStringIndex(s, -1)
prev := 0
for i := range separatorLocations {
loc := separatorLocations[i]
separator := s[loc[0]:loc[1]]
// If the next non-whitespace character on the line following the separator is not a comment, return an error
trimmedContentAfterSeparator := strings.TrimSpace(separator[4:])
if len(trimmedContentAfterSeparator) > 0 && trimmedContentAfterSeparator[0] != '#' {
return nil, errors.Errorf("invalid document separator: %s", strings.TrimSpace(separator))
}
docs = append(docs, s[prev:loc[0]])
prev = loc[1]
}
docs = append(docs, s[prev:])
}
return docs, nil
}
func (r *ByteReader) Read() ([]*yaml.RNode, error) {
if r.PreserveSeqIndent && r.OmitReaderAnnotations {
return nil, errors.Errorf(`"PreserveSeqIndent" option adds a reader annotation, please set "OmitReaderAnnotations" to false`)
}
output := ResourceNodeSlice{}
// by manually splitting resources -- otherwise the decoder will get the Resource
// boundaries wrong for header comments.
input := &bytes.Buffer{}
_, err := io.Copy(input, r.Reader)
if err != nil {
return nil, errors.Wrap(err)
}
// Replace the ending \r\n (line ending used in windows) with \n and then split it into multiple YAML documents
// if it contains document separators (---)
values, err := splitDocuments(strings.ReplaceAll(input.String(), "\r\n", "\n"))
if err != nil {
return nil, errors.Wrap(err)
}
index := 0
for i := range values {
// the Split used above will eat the tail '\n' from each resource. This may affect the
// literal string value since '\n' is meaningful in it.
if i != len(values)-1 {
values[i] += "\n"
}
decoder := yaml.NewDecoder(bytes.NewBufferString(values[i]))
node, err := r.decode(values[i], index, decoder)
if err == io.EOF {
continue
}
if err != nil {
return nil, errors.Wrap(err)
}
if yaml.IsMissingOrNull(node) {
// empty value
continue
}
// ok if no metadata -- assume not an InputList
meta, err := node.GetMeta()
if err != yaml.ErrMissingMetadata && err != nil {
return nil, errors.WrapPrefixf(err, "[%d]", i)
}
// the elements are wrapped in an InputList, unwrap them
// don't check apiVersion, we haven't standardized on the domain
if !r.DisableUnwrapping &&
len(values) == 1 && // Only unwrap if there is only 1 value
(meta.Kind == ResourceListKind || meta.Kind == "List") &&
(node.Field("items") != nil || node.Field("functionConfig") != nil) {
r.WrappingKind = meta.Kind
r.WrappingAPIVersion = meta.APIVersion
// unwrap the list
if fc := node.Field("functionConfig"); fc != nil {
r.FunctionConfig = fc.Value
}
if res := node.Field("results"); res != nil {
r.Results = res.Value
}
items := node.Field("items")
if items != nil {
for i := range items.Value.Content() {
// add items
output = append(output, yaml.NewRNode(items.Value.Content()[i]))
}
}
continue
}
// add the node to the list
output = append(output, node)
// increment the index annotation value
index++
}
if r.AnchorsAweigh {
for _, n := range output {
if err = n.DeAnchor(); err != nil {
return nil, err
}
}
}
return output, nil
}
func (r *ByteReader) decode(originalYAML string, index int, decoder *yaml.Decoder) (*yaml.RNode, error) {
node := &yaml.Node{}
err := decoder.Decode(node)
if err == io.EOF {
return nil, io.EOF
}
if err != nil {
return nil, errors.WrapPrefixf(err, "MalformedYAMLError")
}
if yaml.IsYNodeEmptyDoc(node) {
return nil, nil
}
// set annotations on the read Resources
// sort the annotations by key so the output Resources is consistent (otherwise the
// annotations will be in a random order)
n := yaml.NewRNode(node)
// check if it is a bare sequence node and wrap it with a yaml.BareSeqNodeWrappingKey
if r.WrapBareSeqNode && node.Kind == yaml.DocumentNode && len(node.Content) > 0 &&
node.Content[0] != nil && node.Content[0].Kind == yaml.SequenceNode {
wrappedNode := yaml.NewRNode(&yaml.Node{
Kind: yaml.MappingNode,
})
wrappedNode.PipeE(yaml.SetField(yaml.BareSeqNodeWrappingKey, n))
n = wrappedNode
}
if r.SetAnnotations == nil {
r.SetAnnotations = map[string]string{}
}
if !r.OmitReaderAnnotations {
err := kioutil.CopyLegacyAnnotations(n)
if err != nil {
return nil, err
}
r.SetAnnotations[kioutil.IndexAnnotation] = fmt.Sprintf("%d", index)
r.SetAnnotations[kioutil.LegacyIndexAnnotation] = fmt.Sprintf("%d", index)
if r.PreserveSeqIndent {
// derive and add the seqindent annotation
seqIndentStyle := yaml.DeriveSeqIndentStyle(originalYAML)
if seqIndentStyle != "" {
r.SetAnnotations[kioutil.SeqIndentAnnotation] = seqIndentStyle
}
}
}
var keys []string
for k := range r.SetAnnotations {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
_, err = n.Pipe(yaml.SetAnnotation(k, r.SetAnnotations[k]))
if err != nil {
return nil, errors.Wrap(err)
}
}
return n, nil
}
+198
View File
@@ -0,0 +1,198 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kio
import (
"encoding/json"
"io"
"path/filepath"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// ByteWriter writes ResourceNodes to bytes. Generally YAML encoding will be used but in the special
// case of writing a single, bare yaml.RNode that has a kioutil.PathAnnotation indicating that the
// target is a JSON file JSON encoding is used. See shouldJSONEncodeSingleBareNode below for more
// information.
type ByteWriter struct {
// Writer is where ResourceNodes are encoded.
Writer io.Writer
// KeepReaderAnnotations if set will keep the Reader specific annotations when writing
// the Resources, otherwise they will be cleared.
KeepReaderAnnotations bool
// ClearAnnotations is a list of annotations to clear when writing the Resources.
ClearAnnotations []string
// Style is a style that is set on the Resource Node Document.
Style yaml.Style
// FunctionConfig is the function config for an ResourceList. If non-nil
// wrap the results in an ResourceList.
FunctionConfig *yaml.RNode
Results *yaml.RNode
// WrappingKind if set will cause ByteWriter to wrap the Resources in
// an 'items' field in this kind. e.g. if WrappingKind is 'List',
// ByteWriter will wrap the Resources in a List .items field.
WrappingKind string
// WrappingAPIVersion is the apiVersion for WrappingKind
WrappingAPIVersion string
// Sort if set, will cause ByteWriter to sort the nodes before writing them.
Sort bool
}
var _ Writer = ByteWriter{}
func (w ByteWriter) Write(inputNodes []*yaml.RNode) error {
// Copy the nodes to prevent writer from mutating the original nodes.
nodes := copyRNodes(inputNodes)
if w.Sort {
if err := kioutil.SortNodes(nodes); err != nil {
return errors.Wrap(err)
}
}
// Even though we use the this value further down we must check this before removing annotations
jsonEncodeSingleBareNode := w.shouldJSONEncodeSingleBareNode(nodes)
// store seqindent annotation value for each node in order to set the encoder indentation
var seqIndentsForNodes []string
for i := range nodes {
seqIndentsForNodes = append(seqIndentsForNodes, nodes[i].GetAnnotations()[kioutil.SeqIndentAnnotation])
}
for i := range nodes {
// clean resources by removing annotations set by the Reader
if !w.KeepReaderAnnotations {
_, err := nodes[i].Pipe(yaml.ClearAnnotation(kioutil.IndexAnnotation))
if err != nil {
return errors.Wrap(err)
}
_, err = nodes[i].Pipe(yaml.ClearAnnotation(kioutil.LegacyIndexAnnotation))
if err != nil {
return errors.Wrap(err)
}
_, err = nodes[i].Pipe(yaml.ClearAnnotation(kioutil.SeqIndentAnnotation))
if err != nil {
return errors.Wrap(err)
}
}
for _, a := range w.ClearAnnotations {
_, err := nodes[i].Pipe(yaml.ClearAnnotation(a))
if err != nil {
return errors.Wrap(err)
}
}
if err := yaml.ClearEmptyAnnotations(nodes[i]); err != nil {
return err
}
if w.Style != 0 {
nodes[i].YNode().Style = w.Style
}
}
if jsonEncodeSingleBareNode {
encoder := json.NewEncoder(w.Writer)
encoder.SetIndent("", " ")
return errors.Wrap(encoder.Encode(nodes[0]))
}
encoder := yaml.NewEncoder(w.Writer)
defer encoder.Close()
// don't wrap the elements
if w.WrappingKind == "" {
for i := range nodes {
if seqIndentsForNodes[i] == string(yaml.WideSequenceStyle) {
encoder.DefaultSeqIndent()
} else {
encoder.CompactSeqIndent()
}
if err := encoder.Encode(upWrapBareSequenceNode(nodes[i].Document())); err != nil {
return errors.Wrap(err)
}
}
return nil
}
// wrap the elements in a list
items := &yaml.Node{Kind: yaml.SequenceNode}
list := &yaml.Node{
Kind: yaml.MappingNode,
Style: w.Style,
Content: []*yaml.Node{
{Kind: yaml.ScalarNode, Value: "apiVersion"},
{Kind: yaml.ScalarNode, Value: w.WrappingAPIVersion},
{Kind: yaml.ScalarNode, Value: "kind"},
{Kind: yaml.ScalarNode, Value: w.WrappingKind},
{Kind: yaml.ScalarNode, Value: "items"}, items,
}}
if w.FunctionConfig != nil {
list.Content = append(list.Content,
&yaml.Node{Kind: yaml.ScalarNode, Value: "functionConfig"},
w.FunctionConfig.YNode())
}
if w.Results != nil {
list.Content = append(list.Content,
&yaml.Node{Kind: yaml.ScalarNode, Value: "results"},
w.Results.YNode())
}
doc := &yaml.Node{
Kind: yaml.DocumentNode,
Content: []*yaml.Node{list}}
for i := range nodes {
items.Content = append(items.Content, nodes[i].YNode())
}
return encoder.Encode(doc)
}
func copyRNodes(in []*yaml.RNode) []*yaml.RNode {
out := make([]*yaml.RNode, len(in))
for i := range in {
out[i] = in[i].Copy()
}
return out
}
// shouldJSONEncodeSingleBareNode determines if nodes contain a single node that should not be
// wrapped and has a JSON file extension, which in turn means that the node should be JSON encoded.
// Note 1: this must be checked before any annotations to avoid losing information about the target
// filename extension.
// Note 2: JSON encoding should only be used for single, unwrapped nodes because multiple unwrapped
// nodes cannot be represented in JSON (no multi doc support). Furthermore, the typical use
// cases for wrapping nodes would likely not include later writing the whole wrapper to a
// .json file, i.e. there is no point risking any edge case information loss e.g. comments
// disappearing, that could come from JSON encoding the whole wrapper just to ensure that
// one (or all nodes) can be read as JSON.
func (w ByteWriter) shouldJSONEncodeSingleBareNode(nodes []*yaml.RNode) bool {
if w.WrappingKind == "" && len(nodes) == 1 {
if path, _, _ := kioutil.GetFileAnnotations(nodes[0]); path != "" {
filename := filepath.Base(path)
for _, glob := range JSONMatch {
if match, _ := filepath.Match(glob, filename); match {
return true
}
}
}
}
return false
}
// upWrapBareSequenceNode unwraps the bare sequence nodes wrapped by yaml.BareSeqNodeWrappingKey
func upWrapBareSequenceNode(node *yaml.Node) *yaml.Node {
rNode := yaml.NewRNode(node)
seqNode, err := rNode.Pipe(yaml.Lookup(yaml.BareSeqNodeWrappingKey))
if err == nil && !seqNode.IsNilOrEmpty() {
return seqNode.YNode()
}
return node
}
+35
View File
@@ -0,0 +1,35 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package kio contains libraries for reading and writing collections of Resources.
//
// Reading Resources
//
// Resources are Read using a kio.Reader function. Examples:
// [kio.LocalPackageReader{}, kio.ByteReader{}]
//
// Resources read using a LocalPackageReader will have annotations applied so they can be
// written back to the files they were read from.
//
// Modifying Resources
//
// Resources are modified using a kio.Filter. The kio.Filter accepts a collection of
// Resources as input, and returns a new collection as output.
// It is recommended to use the yaml package for manipulating individual Resources in
// the collection.
//
// Writing Resources
//
// Resources are Read using a kio.Reader function. Examples:
// [kio.LocalPackageWriter{}, kio.ByteWriter{}]
//
// ReadWriters
//
// It is preferred to use a ReadWriter when reading and writing from / to the same source.
//
// Building Pipelines
//
// The preferred way to transforms a collection of Resources is to use kio.Pipeline to Read,
// Modify and Write the collection of Resources. Pipeline will automatically sequentially
// invoke the Read, Modify, Write steps, returning and error immediately on any failure.
package kio
+105
View File
@@ -0,0 +1,105 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kio
import (
"errors"
"os"
"path/filepath"
"strings"
gitignore "github.com/monochromegane/go-gitignore"
"sigs.k8s.io/kustomize/kyaml/ext"
"sigs.k8s.io/kustomize/kyaml/filesys"
)
// ignoreFilesMatcher handles `.krmignore` files, which allows for ignoring
// files or folders in a package. The format of this file is a subset of the
// gitignore format, with recursive patterns (like a/**/c) not supported. If a
// file or folder matches any of the patterns in the .krmignore file for the
// package, it will be excluded.
//
// It works as follows:
//
// * It will look for .krmignore file in the top folder and on the top level
// of any subpackages. Subpackages are defined by the presence of a Krmfile
// in the folder.
// * `.krmignore` files only cover files and folders for the package in which
// it is defined. So ignore patterns defined in a parent package does not
// affect which files are ignored from a subpackage.
// * An ignore pattern can not ignore a subpackage. So even if the parent
// package contains a pattern that ignores the directory foo, if foo is a
// subpackage, it will still be included if the IncludeSubpackages property
// is set to true
type ignoreFilesMatcher struct {
matchers []matcher
fs filesys.FileSystemOrOnDisk
}
// readIgnoreFile checks whether there is a .krmignore file in the path, and
// if it is, reads it in and turns it into a matcher. If we can't find a file,
// we just add a matcher that match nothing.
func (i *ignoreFilesMatcher) readIgnoreFile(path string) error {
i.verifyPath(path)
f, err := i.fs.Open(filepath.Join(path, ext.IgnoreFileName()))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
i.matchers = append(i.matchers, matcher{
matcher: gitignore.DummyIgnoreMatcher(false),
basePath: path,
})
return nil
}
return err
}
defer f.Close()
i.matchers = append(i.matchers, matcher{
matcher: gitignore.NewGitIgnoreFromReader(path, f),
basePath: path,
})
return nil
}
// verifyPath checks whether the top matcher on the stack
// is correct for the provided filepath. Matchers are removed once
// we encounter a filepath that is not a subpath of the basepath for
// the matcher.
func (i *ignoreFilesMatcher) verifyPath(path string) {
for j := len(i.matchers) - 1; j >= 0; j-- {
matcher := i.matchers[j]
if strings.HasPrefix(path, matcher.basePath) || path == matcher.basePath {
i.matchers = i.matchers[:j+1]
return
}
}
}
// matchFile checks whether the file given by the provided path matches
// any of the patterns in the .krmignore file for the package.
func (i *ignoreFilesMatcher) matchFile(path string) bool {
if len(i.matchers) == 0 {
return false
}
i.verifyPath(filepath.Dir(path))
return i.matchers[len(i.matchers)-1].matcher.Match(path, false)
}
// matchDir checks whether the directory given by the provided path matches
// any of the patterns in the .krmignore file for the package.
func (i *ignoreFilesMatcher) matchDir(path string) bool {
if len(i.matchers) == 0 {
return false
}
i.verifyPath(path)
return i.matchers[len(i.matchers)-1].matcher.Match(path, true)
}
// matcher wraps the gitignore matcher and the path to the folder
// where the file was found.
type matcher struct {
matcher gitignore.IgnoreMatcher
basePath string
}
+447
View File
@@ -0,0 +1,447 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package kio contains low-level libraries for reading, modifying and writing
// Resource Configuration and packages.
package kio
import (
"fmt"
"strconv"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// Reader reads ResourceNodes. Analogous to io.Reader.
type Reader interface {
Read() ([]*yaml.RNode, error)
}
// ResourceNodeSlice is a collection of ResourceNodes.
// While ResourceNodeSlice has no inherent constraints on ordering or uniqueness, specific
// Readers, Filters or Writers may have constraints.
type ResourceNodeSlice []*yaml.RNode
var _ Reader = ResourceNodeSlice{}
func (o ResourceNodeSlice) Read() ([]*yaml.RNode, error) {
return o, nil
}
// Writer writes ResourceNodes. Analogous to io.Writer.
type Writer interface {
Write([]*yaml.RNode) error
}
// WriterFunc implements a Writer as a function.
type WriterFunc func([]*yaml.RNode) error
func (fn WriterFunc) Write(o []*yaml.RNode) error {
return fn(o)
}
// ReaderWriter implements both Reader and Writer interfaces
type ReaderWriter interface {
Reader
Writer
}
// Filter modifies a collection of Resource Configuration by returning the modified slice.
// When possible, Filters should be serializable to yaml so that they can be described
// as either data or code.
//
// Analogous to http://www.linfo.org/filters.html
type Filter interface {
Filter([]*yaml.RNode) ([]*yaml.RNode, error)
}
// TrackableFilter is an extension of Filter which is also capable of tracking
// which fields were mutated by the filter.
type TrackableFilter interface {
Filter
WithMutationTracker(func(key, value, tag string, node *yaml.RNode))
}
// FilterFunc implements a Filter as a function.
type FilterFunc func([]*yaml.RNode) ([]*yaml.RNode, error)
func (fn FilterFunc) Filter(o []*yaml.RNode) ([]*yaml.RNode, error) {
return fn(o)
}
// Pipeline reads Resource Configuration from a set of Inputs, applies some
// transformation filters, and writes the results to a set of Outputs.
//
// Analogous to http://www.linfo.org/pipes.html
type Pipeline struct {
// Inputs provide sources for Resource Configuration to be read.
Inputs []Reader `yaml:"inputs,omitempty"`
// Filters are transformations applied to the Resource Configuration.
// They are applied in the order they are specified.
// Analogous to http://www.linfo.org/filters.html
Filters []Filter `yaml:"filters,omitempty"`
// Outputs are where the transformed Resource Configuration is written.
Outputs []Writer `yaml:"outputs,omitempty"`
// ContinueOnEmptyResult configures what happens when a filter in the pipeline
// returns an empty result.
// If it is false (default), subsequent filters will be skipped and the result
// will be returned immediately. This is useful as an optimization when you
// know that subsequent filters will not alter the empty result.
// If it is true, the empty result will be provided as input to the next
// filter in the list. This is useful when subsequent functions in the
// pipeline may generate new resources.
ContinueOnEmptyResult bool `yaml:"continueOnEmptyResult,omitempty"`
}
// Execute executes each step in the sequence, returning immediately after encountering
// any error as part of the Pipeline.
func (p Pipeline) Execute() error {
return p.ExecuteWithCallback(nil)
}
// PipelineExecuteCallbackFunc defines a callback function that will be called each time a step in the pipeline succeeds.
type PipelineExecuteCallbackFunc = func(op Filter)
// ExecuteWithCallback executes each step in the sequence, returning immediately after encountering
// any error as part of the Pipeline. The callback will be called each time a step succeeds.
func (p Pipeline) ExecuteWithCallback(callback PipelineExecuteCallbackFunc) error {
var result []*yaml.RNode
// read from the inputs
for _, i := range p.Inputs {
nodes, err := i.Read()
if err != nil {
return errors.Wrap(err)
}
result = append(result, nodes...)
}
// apply operations
for i := range p.Filters {
// Not all RNodes passed through kio.Pipeline have metadata nor should
// they all be required to.
nodeAnnos, err := PreprocessResourcesForInternalAnnotationMigration(result)
if err != nil {
return err
}
op := p.Filters[i]
if callback != nil {
callback(op)
}
result, err = op.Filter(result)
// TODO (issue 2872): This len(result) == 0 should be removed and empty result list should be
// handled by outputs. However currently some writer like LocalPackageReadWriter
// will clear the output directory and which will cause unpredictable results
if len(result) == 0 && !p.ContinueOnEmptyResult || err != nil {
return errors.Wrap(err)
}
// If either the internal annotations for path, index, and id OR the legacy
// annotations for path, index, and id are changed, we have to update the other.
err = ReconcileInternalAnnotations(result, nodeAnnos)
if err != nil {
return err
}
}
// write to the outputs
for _, o := range p.Outputs {
if err := o.Write(result); err != nil {
return errors.Wrap(err)
}
}
return nil
}
// FilterAll runs the yaml.Filter against all inputs
func FilterAll(filter yaml.Filter) Filter {
return FilterFunc(func(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
for i := range nodes {
_, err := filter.Filter(nodes[i])
if err != nil {
return nil, errors.Wrap(err)
}
}
return nodes, nil
})
}
// PreprocessResourcesForInternalAnnotationMigration returns a mapping from id to all
// internal annotations, so that we can use it to reconcile the annotations
// later. This is necessary because currently both internal-prefixed annotations
// and legacy annotations are currently supported, and a change to one must be
// reflected in the other if needed.
func PreprocessResourcesForInternalAnnotationMigration(result []*yaml.RNode) (map[string]map[string]string, error) {
idToAnnosMap := make(map[string]map[string]string)
for i := range result {
idStr := strconv.Itoa(i)
err := result[i].PipeE(yaml.SetAnnotation(kioutil.InternalAnnotationsMigrationResourceIDAnnotation, idStr))
if err != nil {
return nil, err
}
idToAnnosMap[idStr] = kioutil.GetInternalAnnotations(result[i])
if err = kioutil.CopyLegacyAnnotations(result[i]); err != nil {
return nil, err
}
meta, _ := result[i].GetMeta()
if err = checkMismatchedAnnos(meta.Annotations); err != nil {
return nil, err
}
}
return idToAnnosMap, nil
}
func checkMismatchedAnnos(annotations map[string]string) error {
path := annotations[kioutil.PathAnnotation]
index := annotations[kioutil.IndexAnnotation]
id := annotations[kioutil.IdAnnotation]
legacyPath := annotations[kioutil.LegacyPathAnnotation]
legacyIndex := annotations[kioutil.LegacyIndexAnnotation]
legacyId := annotations[kioutil.LegacyIdAnnotation]
// if prior to running the functions, the legacy and internal annotations differ,
// throw an error as we cannot infer the user's intent.
if path != "" && legacyPath != "" && path != legacyPath {
return fmt.Errorf("resource input to function has mismatched legacy and internal path annotations")
}
if index != "" && legacyIndex != "" && index != legacyIndex {
return fmt.Errorf("resource input to function has mismatched legacy and internal index annotations")
}
if id != "" && legacyId != "" && id != legacyId {
return fmt.Errorf("resource input to function has mismatched legacy and internal id annotations")
}
return nil
}
type nodeAnnotations struct {
path string
index string
id string
}
// ReconcileInternalAnnotations reconciles the annotation format for path, index and id annotations.
// It will ensure the output annotation format matches the format in the input. e.g. if the input
// format uses the legacy format and the output will be converted to the legacy format if it's not.
func ReconcileInternalAnnotations(result []*yaml.RNode, nodeAnnosMap map[string]map[string]string) error {
useInternal, useLegacy, err := determineAnnotationsFormat(nodeAnnosMap)
if err != nil {
return err
}
for i := range result {
// if only one annotation is set, set the other.
err = missingInternalOrLegacyAnnotations(result[i])
if err != nil {
return err
}
// we must check to see if the function changed either the new internal annotations
// or the old legacy annotations. If one is changed, the change must be reflected
// in the other.
err = checkAnnotationsAltered(result[i], nodeAnnosMap)
if err != nil {
return err
}
// We invoke determineAnnotationsFormat to find out if the original annotations
// use the internal or (and) the legacy format. We format the resources to
// make them consistent with original format.
err = formatInternalAnnotations(result[i], useInternal, useLegacy)
if err != nil {
return err
}
// if the annotations are still somehow out of sync, throw an error
meta, _ := result[i].GetMeta()
err = checkMismatchedAnnos(meta.Annotations)
if err != nil {
return err
}
if _, err = result[i].Pipe(yaml.ClearAnnotation(kioutil.InternalAnnotationsMigrationResourceIDAnnotation)); err != nil {
return err
}
}
return nil
}
// determineAnnotationsFormat determines if the resources are using one of the internal and legacy annotation format or both of them.
func determineAnnotationsFormat(nodeAnnosMap map[string]map[string]string) (bool, bool, error) {
var useInternal, useLegacy bool
var err error
if len(nodeAnnosMap) == 0 {
return true, true, nil
}
var internal, legacy *bool
for _, annos := range nodeAnnosMap {
_, foundPath := annos[kioutil.PathAnnotation]
_, foundIndex := annos[kioutil.IndexAnnotation]
_, foundId := annos[kioutil.IdAnnotation]
_, foundLegacyPath := annos[kioutil.LegacyPathAnnotation]
_, foundLegacyIndex := annos[kioutil.LegacyIndexAnnotation]
_, foundLegacyId := annos[kioutil.LegacyIdAnnotation]
if !(foundPath || foundIndex || foundId || foundLegacyPath || foundLegacyIndex || foundLegacyId) {
continue
}
foundOneOf := foundPath || foundIndex || foundId
if internal == nil {
f := foundOneOf
internal = &f
}
if (foundOneOf && !*internal) || (!foundOneOf && *internal) {
err = fmt.Errorf("the annotation formatting in the input resources is not consistent")
return useInternal, useLegacy, err
}
foundOneOf = foundLegacyPath || foundLegacyIndex || foundLegacyId
if legacy == nil {
f := foundOneOf
legacy = &f
}
if (foundOneOf && !*legacy) || (!foundOneOf && *legacy) {
err = fmt.Errorf("the annotation formatting in the input resources is not consistent")
return useInternal, useLegacy, err
}
}
if internal != nil {
useInternal = *internal
}
if legacy != nil {
useLegacy = *legacy
}
return useInternal, useLegacy, err
}
func missingInternalOrLegacyAnnotations(rn *yaml.RNode) error {
if err := missingInternalOrLegacyAnnotation(rn, kioutil.PathAnnotation, kioutil.LegacyPathAnnotation); err != nil {
return err
}
if err := missingInternalOrLegacyAnnotation(rn, kioutil.IndexAnnotation, kioutil.LegacyIndexAnnotation); err != nil {
return err
}
if err := missingInternalOrLegacyAnnotation(rn, kioutil.IdAnnotation, kioutil.LegacyIdAnnotation); err != nil {
return err
}
return nil
}
func missingInternalOrLegacyAnnotation(rn *yaml.RNode, newKey string, legacyKey string) error {
meta, _ := rn.GetMeta()
annotations := meta.Annotations
value := annotations[newKey]
legacyValue := annotations[legacyKey]
if value == "" && legacyValue == "" {
// do nothing
return nil
}
if value == "" {
// new key is not set, copy from legacy key
if err := rn.PipeE(yaml.SetAnnotation(newKey, legacyValue)); err != nil {
return err
}
} else if legacyValue == "" {
// legacy key is not set, copy from new key
if err := rn.PipeE(yaml.SetAnnotation(legacyKey, value)); err != nil {
return err
}
}
return nil
}
func checkAnnotationsAltered(rn *yaml.RNode, nodeAnnosMap map[string]map[string]string) error {
meta, _ := rn.GetMeta()
annotations := meta.Annotations
// get the resource's current path, index, and ids from the new annotations
internal := nodeAnnotations{
path: annotations[kioutil.PathAnnotation],
index: annotations[kioutil.IndexAnnotation],
id: annotations[kioutil.IdAnnotation],
}
// get the resource's current path, index, and ids from the legacy annotations
legacy := nodeAnnotations{
path: annotations[kioutil.LegacyPathAnnotation],
index: annotations[kioutil.LegacyIndexAnnotation],
id: annotations[kioutil.LegacyIdAnnotation],
}
rid := annotations[kioutil.InternalAnnotationsMigrationResourceIDAnnotation]
originalAnnotations, found := nodeAnnosMap[rid]
if !found {
return nil
}
originalPath, found := originalAnnotations[kioutil.PathAnnotation]
if !found {
originalPath = originalAnnotations[kioutil.LegacyPathAnnotation]
}
if originalPath != "" {
switch {
case originalPath != internal.path && originalPath != legacy.path && internal.path != legacy.path:
return fmt.Errorf("resource input to function has mismatched legacy and internal path annotations")
case originalPath != internal.path:
if _, err := rn.Pipe(yaml.SetAnnotation(kioutil.LegacyPathAnnotation, internal.path)); err != nil {
return err
}
case originalPath != legacy.path:
if _, err := rn.Pipe(yaml.SetAnnotation(kioutil.PathAnnotation, legacy.path)); err != nil {
return err
}
}
}
originalIndex, found := originalAnnotations[kioutil.IndexAnnotation]
if !found {
originalIndex = originalAnnotations[kioutil.LegacyIndexAnnotation]
}
if originalIndex != "" {
switch {
case originalIndex != internal.index && originalIndex != legacy.index && internal.index != legacy.index:
return fmt.Errorf("resource input to function has mismatched legacy and internal index annotations")
case originalIndex != internal.index:
if _, err := rn.Pipe(yaml.SetAnnotation(kioutil.LegacyIndexAnnotation, internal.index)); err != nil {
return err
}
case originalIndex != legacy.index:
if _, err := rn.Pipe(yaml.SetAnnotation(kioutil.IndexAnnotation, legacy.index)); err != nil {
return err
}
}
}
return nil
}
func formatInternalAnnotations(rn *yaml.RNode, useInternal, useLegacy bool) error {
if !useInternal {
if err := rn.PipeE(yaml.ClearAnnotation(kioutil.IdAnnotation)); err != nil {
return err
}
if err := rn.PipeE(yaml.ClearAnnotation(kioutil.PathAnnotation)); err != nil {
return err
}
if err := rn.PipeE(yaml.ClearAnnotation(kioutil.IndexAnnotation)); err != nil {
return err
}
}
if !useLegacy {
if err := rn.PipeE(yaml.ClearAnnotation(kioutil.LegacyIdAnnotation)); err != nil {
return err
}
if err := rn.PipeE(yaml.ClearAnnotation(kioutil.LegacyPathAnnotation)); err != nil {
return err
}
if err := rn.PipeE(yaml.ClearAnnotation(kioutil.LegacyIndexAnnotation)); err != nil {
return err
}
}
return nil
}
+420
View File
@@ -0,0 +1,420 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kioutil
import (
"fmt"
"path"
"sort"
"strconv"
"strings"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
type AnnotationKey = string
const (
// internalPrefix is the prefix given to internal annotations that are used
// internally by the orchestrator
internalPrefix string = "internal.config.kubernetes.io/"
// IndexAnnotation records the index of a specific resource in a file or input stream.
IndexAnnotation AnnotationKey = internalPrefix + "index"
// PathAnnotation records the path to the file the Resource was read from
PathAnnotation AnnotationKey = internalPrefix + "path"
// SeqIndentAnnotation records the sequence nodes indentation of the input resource
SeqIndentAnnotation AnnotationKey = internalPrefix + "seqindent"
// IdAnnotation records the id of the resource to map inputs to outputs
IdAnnotation AnnotationKey = internalPrefix + "id"
// Deprecated: Use IndexAnnotation instead.
LegacyIndexAnnotation AnnotationKey = "config.kubernetes.io/index"
// Deprecated: use PathAnnotation instead.
LegacyPathAnnotation AnnotationKey = "config.kubernetes.io/path"
// Deprecated: use IdAnnotation instead.
LegacyIdAnnotation = "config.k8s.io/id"
// InternalAnnotationsMigrationResourceIDAnnotation is used to uniquely identify
// resources during round trip to and from a function execution. We will use it
// to track the internal annotations and reconcile them if needed.
InternalAnnotationsMigrationResourceIDAnnotation = internalPrefix + "annotations-migration-resource-id"
)
func GetFileAnnotations(rn *yaml.RNode) (string, string, error) {
rm, _ := rn.GetMeta()
annotations := rm.Annotations
path, found := annotations[PathAnnotation]
if !found {
path = annotations[LegacyPathAnnotation]
}
index, found := annotations[IndexAnnotation]
if !found {
index = annotations[LegacyIndexAnnotation]
}
return path, index, nil
}
func GetIdAnnotation(rn *yaml.RNode) string {
rm, _ := rn.GetMeta()
annotations := rm.Annotations
id, found := annotations[IdAnnotation]
if !found {
id = annotations[LegacyIdAnnotation]
}
return id
}
func CopyLegacyAnnotations(rn *yaml.RNode) error {
meta, err := rn.GetMeta()
if err != nil {
if err == yaml.ErrMissingMetadata {
// resource has no metadata, this should be a no-op
return nil
}
return err
}
if err := copyAnnotations(meta, rn, LegacyPathAnnotation, PathAnnotation); err != nil {
return err
}
if err := copyAnnotations(meta, rn, LegacyIndexAnnotation, IndexAnnotation); err != nil {
return err
}
if err := copyAnnotations(meta, rn, LegacyIdAnnotation, IdAnnotation); err != nil {
return err
}
return nil
}
func copyAnnotations(meta yaml.ResourceMeta, rn *yaml.RNode, legacyKey string, newKey string) error {
newValue := meta.Annotations[newKey]
legacyValue := meta.Annotations[legacyKey]
if newValue != "" {
if legacyValue == "" {
if err := rn.PipeE(yaml.SetAnnotation(legacyKey, newValue)); err != nil {
return err
}
}
} else {
if legacyValue != "" {
if err := rn.PipeE(yaml.SetAnnotation(newKey, legacyValue)); err != nil {
return err
}
}
}
return nil
}
// ErrorIfMissingAnnotation validates the provided annotations are present on the given resources
func ErrorIfMissingAnnotation(nodes []*yaml.RNode, keys ...AnnotationKey) error {
for _, key := range keys {
for _, node := range nodes {
val, err := node.Pipe(yaml.GetAnnotation(key))
if err != nil {
return errors.Wrap(err)
}
if val == nil {
return errors.Errorf("missing annotation %s", key)
}
}
}
return nil
}
// CreatePathAnnotationValue creates a default path annotation value for a Resource.
// The path prefix will be dir.
func CreatePathAnnotationValue(dir string, m yaml.ResourceMeta) string {
filename := fmt.Sprintf("%s_%s.yaml", strings.ToLower(m.Kind), m.Name)
return path.Join(dir, m.Namespace, filename)
}
// DefaultPathAndIndexAnnotation sets a default path or index value on any nodes missing the
// annotation
func DefaultPathAndIndexAnnotation(dir string, nodes []*yaml.RNode) error {
counts := map[string]int{}
// check each node for the path annotation
for i := range nodes {
if err := CopyLegacyAnnotations(nodes[i]); err != nil {
return err
}
m, err := nodes[i].GetMeta()
if err != nil {
return err
}
// calculate the max index in each file in case we are appending
if p, found := m.Annotations[PathAnnotation]; found {
// record the max indexes into each file
if i, found := m.Annotations[IndexAnnotation]; found {
index, _ := strconv.Atoi(i)
if index > counts[p] {
counts[p] = index
}
}
// has the path annotation already -- do nothing
continue
}
// set a path annotation on the Resource
path := CreatePathAnnotationValue(dir, m)
if err := nodes[i].PipeE(yaml.SetAnnotation(PathAnnotation, path)); err != nil {
return err
}
if err := nodes[i].PipeE(yaml.SetAnnotation(LegacyPathAnnotation, path)); err != nil {
return err
}
}
// set the index annotations
for i := range nodes {
m, err := nodes[i].GetMeta()
if err != nil {
return err
}
if _, found := m.Annotations[IndexAnnotation]; found {
continue
}
p := m.Annotations[PathAnnotation]
// set an index annotation on the Resource
c := counts[p]
counts[p] = c + 1
if err := nodes[i].PipeE(
yaml.SetAnnotation(IndexAnnotation, fmt.Sprintf("%d", c))); err != nil {
return err
}
if err := nodes[i].PipeE(
yaml.SetAnnotation(LegacyIndexAnnotation, fmt.Sprintf("%d", c))); err != nil {
return err
}
}
return nil
}
// DefaultPathAnnotation sets a default path annotation on any Reources
// missing it.
func DefaultPathAnnotation(dir string, nodes []*yaml.RNode) error {
// check each node for the path annotation
for i := range nodes {
if err := CopyLegacyAnnotations(nodes[i]); err != nil {
return err
}
m, err := nodes[i].GetMeta()
if err != nil {
return err
}
if _, found := m.Annotations[PathAnnotation]; found {
// has the path annotation already -- do nothing
continue
}
// set a path annotation on the Resource
path := CreatePathAnnotationValue(dir, m)
if err := nodes[i].PipeE(yaml.SetAnnotation(PathAnnotation, path)); err != nil {
return err
}
if err := nodes[i].PipeE(yaml.SetAnnotation(LegacyPathAnnotation, path)); err != nil {
return err
}
}
return nil
}
// Map invokes fn for each element in nodes.
func Map(nodes []*yaml.RNode, fn func(*yaml.RNode) (*yaml.RNode, error)) ([]*yaml.RNode, error) {
var returnNodes []*yaml.RNode
for i := range nodes {
n, err := fn(nodes[i])
if err != nil {
return nil, errors.Wrap(err)
}
if n != nil {
returnNodes = append(returnNodes, n)
}
}
return returnNodes, nil
}
func MapMeta(nodes []*yaml.RNode, fn func(*yaml.RNode, yaml.ResourceMeta) (*yaml.RNode, error)) (
[]*yaml.RNode, error) {
var returnNodes []*yaml.RNode
for i := range nodes {
meta, err := nodes[i].GetMeta()
if err != nil {
return nil, errors.Wrap(err)
}
n, err := fn(nodes[i], meta)
if err != nil {
return nil, errors.Wrap(err)
}
if n != nil {
returnNodes = append(returnNodes, n)
}
}
return returnNodes, nil
}
// SortNodes sorts nodes in place:
// - by PathAnnotation annotation
// - by IndexAnnotation annotation
func SortNodes(nodes []*yaml.RNode) error {
var err error
// use stable sort to keep ordering of equal elements
sort.SliceStable(nodes, func(i, j int) bool {
if err != nil {
return false
}
if err := CopyLegacyAnnotations(nodes[i]); err != nil {
return false
}
if err := CopyLegacyAnnotations(nodes[j]); err != nil {
return false
}
var iMeta, jMeta yaml.ResourceMeta
if iMeta, _ = nodes[i].GetMeta(); err != nil {
return false
}
if jMeta, _ = nodes[j].GetMeta(); err != nil {
return false
}
iValue := iMeta.Annotations[PathAnnotation]
jValue := jMeta.Annotations[PathAnnotation]
if iValue != jValue {
return iValue < jValue
}
iValue = iMeta.Annotations[IndexAnnotation]
jValue = jMeta.Annotations[IndexAnnotation]
// put resource config without an index first
if iValue == jValue {
return false
}
if iValue == "" {
return true
}
if jValue == "" {
return false
}
// sort by index
var iIndex, jIndex int
iIndex, err = strconv.Atoi(iValue)
if err != nil {
err = fmt.Errorf("unable to parse config.kubernetes.io/index %s :%v", iValue, err)
return false
}
jIndex, err = strconv.Atoi(jValue)
if err != nil {
err = fmt.Errorf("unable to parse config.kubernetes.io/index %s :%v", jValue, err)
return false
}
if iIndex != jIndex {
return iIndex < jIndex
}
// elements are equal
return false
})
return errors.Wrap(err)
}
// CopyInternalAnnotations copies the annotations that begin with the prefix
// `internal.config.kubernetes.io` from the source RNode to the destination RNode.
// It takes a parameter exclusions, which is a list of annotation keys to ignore.
func CopyInternalAnnotations(src *yaml.RNode, dst *yaml.RNode, exclusions ...AnnotationKey) error {
srcAnnotations := GetInternalAnnotations(src)
for k, v := range srcAnnotations {
if stringSliceContains(exclusions, k) {
continue
}
if err := dst.PipeE(yaml.SetAnnotation(k, v)); err != nil {
return err
}
}
return nil
}
// ConfirmInternalAnnotationUnchanged compares the annotations of the RNodes that begin with the prefix
// `internal.config.kubernetes.io`, throwing an error if they differ. It takes a parameter exclusions,
// which is a list of annotation keys to ignore.
func ConfirmInternalAnnotationUnchanged(r1 *yaml.RNode, r2 *yaml.RNode, exclusions ...AnnotationKey) error {
r1Annotations := GetInternalAnnotations(r1)
r2Annotations := GetInternalAnnotations(r2)
// this is a map to prevent duplicates
diffAnnos := make(map[string]bool)
for k, v1 := range r1Annotations {
if stringSliceContains(exclusions, k) {
continue
}
if v2, ok := r2Annotations[k]; !ok || v1 != v2 {
diffAnnos[k] = true
}
}
for k, v2 := range r2Annotations {
if stringSliceContains(exclusions, k) {
continue
}
if v1, ok := r1Annotations[k]; !ok || v2 != v1 {
diffAnnos[k] = true
}
}
if len(diffAnnos) > 0 {
keys := make([]string, 0, len(diffAnnos))
for k := range diffAnnos {
keys = append(keys, k)
}
sort.Strings(keys)
errorString := "internal annotations differ: "
for _, key := range keys {
errorString = errorString + key + ", "
}
return errors.Errorf("%s", errorString[0:len(errorString)-2])
}
return nil
}
// GetInternalAnnotations returns a map of all the annotations of the provided
// RNode that satisfies one of the following: 1) begin with the prefix
// `internal.config.kubernetes.io` 2) is one of `config.kubernetes.io/path`,
// `config.kubernetes.io/index` and `config.k8s.io/id`.
func GetInternalAnnotations(rn *yaml.RNode) map[string]string {
meta, _ := rn.GetMeta()
annotations := meta.Annotations
result := make(map[string]string)
for k, v := range annotations {
if strings.HasPrefix(k, internalPrefix) || k == LegacyPathAnnotation || k == LegacyIndexAnnotation || k == LegacyIdAnnotation {
result[k] = v
}
}
return result
}
// stringSliceContains returns true if the slice has the string.
func stringSliceContains(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}
return false
}
+360
View File
@@ -0,0 +1,360 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kio
import (
"fmt"
"os"
"path/filepath"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/filesys"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
"sigs.k8s.io/kustomize/kyaml/sets"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// requiredResourcePackageAnnotations are annotations that are required to write resources back to
// files.
var requiredResourcePackageAnnotations = []string{kioutil.IndexAnnotation, kioutil.PathAnnotation}
// PackageBuffer implements Reader and Writer, storing Resources in a local field.
type PackageBuffer struct {
Nodes []*yaml.RNode
}
func (r *PackageBuffer) Read() ([]*yaml.RNode, error) {
return r.Nodes, nil
}
func (r *PackageBuffer) Write(nodes []*yaml.RNode) error {
r.Nodes = nodes
return nil
}
// LocalPackageReadWriter reads and writes Resources from / to a local directory.
// When writing, LocalPackageReadWriter will delete files if all of the Resources from
// that file have been removed from the output.
type LocalPackageReadWriter struct {
Kind string `yaml:"kind,omitempty"`
KeepReaderAnnotations bool `yaml:"keepReaderAnnotations,omitempty"`
// PreserveSeqIndent if true adds kioutil.SeqIndentAnnotation to each resource
PreserveSeqIndent bool
// PackagePath is the path to the package directory.
PackagePath string `yaml:"path,omitempty"`
// PackageFileName is the name of file containing package metadata.
// It will be used to identify package.
PackageFileName string `yaml:"packageFileName,omitempty"`
// MatchFilesGlob configures Read to only read Resources from files matching any of the
// provided patterns.
// Defaults to ["*.yaml", "*.yml"] if empty. To match all files specify ["*"].
MatchFilesGlob []string `yaml:"matchFilesGlob,omitempty"`
// IncludeSubpackages will configure Read to read Resources from subpackages.
// Subpackages are identified by presence of PackageFileName.
IncludeSubpackages bool `yaml:"includeSubpackages,omitempty"`
// ErrorIfNonResources will configure Read to throw an error if yaml missing missing
// apiVersion or kind is read.
ErrorIfNonResources bool `yaml:"errorIfNonResources,omitempty"`
// OmitReaderAnnotations will cause the reader to skip annotating Resources with the file
// path and mode.
OmitReaderAnnotations bool `yaml:"omitReaderAnnotations,omitempty"`
// SetAnnotations are annotations to set on the Resources as they are read.
SetAnnotations map[string]string `yaml:"setAnnotations,omitempty"`
// NoDeleteFiles if set to true, LocalPackageReadWriter won't delete any files
NoDeleteFiles bool `yaml:"noDeleteFiles,omitempty"`
files sets.String
// FileSkipFunc is a function which returns true if reader should ignore
// the file
FileSkipFunc LocalPackageSkipFileFunc
// FileSystem can be used to mock the disk file system.
FileSystem filesys.FileSystemOrOnDisk
// WrapBareSeqNode wraps the bare sequence node document with map node,
// kyaml uses reader annotations to track resources, it is not possible to
// add them to bare sequence nodes, this option enables wrapping such bare
// sequence nodes into map node with key yaml.BareSeqNodeWrappingKey
// note that this wrapping is different and not related to ResourceList wrapping
WrapBareSeqNode bool
}
func (r *LocalPackageReadWriter) Read() ([]*yaml.RNode, error) {
nodes, err := LocalPackageReader{
PackagePath: r.PackagePath,
MatchFilesGlob: r.MatchFilesGlob,
IncludeSubpackages: r.IncludeSubpackages,
ErrorIfNonResources: r.ErrorIfNonResources,
SetAnnotations: r.SetAnnotations,
PackageFileName: r.PackageFileName,
FileSkipFunc: r.FileSkipFunc,
PreserveSeqIndent: r.PreserveSeqIndent,
FileSystem: r.FileSystem,
WrapBareSeqNode: r.WrapBareSeqNode,
}.Read()
if err != nil {
return nil, errors.Wrap(err)
}
// keep track of all the files
if !r.NoDeleteFiles {
r.files, err = r.getFiles(nodes)
if err != nil {
return nil, errors.Wrap(err)
}
}
return nodes, nil
}
func (r *LocalPackageReadWriter) Write(nodes []*yaml.RNode) error {
newFiles, err := r.getFiles(nodes)
if err != nil {
return errors.Wrap(err)
}
var clear []string
for k := range r.SetAnnotations {
clear = append(clear, k)
}
err = LocalPackageWriter{
PackagePath: r.PackagePath,
ClearAnnotations: clear,
KeepReaderAnnotations: r.KeepReaderAnnotations,
FileSystem: r.FileSystem,
}.Write(nodes)
if err != nil {
return errors.Wrap(err)
}
deleteFiles := r.files.Difference(newFiles)
for f := range deleteFiles {
if err = r.FileSystem.RemoveAll(filepath.Join(r.PackagePath, f)); err != nil {
return errors.Wrap(err)
}
}
return nil
}
func (r *LocalPackageReadWriter) getFiles(nodes []*yaml.RNode) (sets.String, error) {
val := sets.String{}
for _, n := range nodes {
path, _, err := kioutil.GetFileAnnotations(n)
if err != nil {
return nil, errors.Wrap(err)
}
val.Insert(path)
}
return val, nil
}
// LocalPackageSkipFileFunc is a function which returns true if the file
// in the package should be ignored by reader.
// relPath is an OS specific relative path
type LocalPackageSkipFileFunc func(relPath string) bool
// LocalPackageReader reads ResourceNodes from a local package.
type LocalPackageReader struct {
Kind string `yaml:"kind,omitempty"`
// PackagePath is the path to the package directory.
PackagePath string `yaml:"path,omitempty"`
// PackageFileName is the name of file containing package metadata.
// It will be used to identify package.
PackageFileName string `yaml:"packageFileName,omitempty"`
// MatchFilesGlob configures Read to only read Resources from files matching any of the
// provided patterns.
// Defaults to ["*.yaml", "*.yml"] if empty. To match all files specify ["*"].
MatchFilesGlob []string `yaml:"matchFilesGlob,omitempty"`
// IncludeSubpackages will configure Read to read Resources from subpackages.
// Subpackages are identified by presence of PackageFileName.
IncludeSubpackages bool `yaml:"includeSubpackages,omitempty"`
// ErrorIfNonResources will configure Read to throw an error if yaml missing missing
// apiVersion or kind is read.
ErrorIfNonResources bool `yaml:"errorIfNonResources,omitempty"`
// OmitReaderAnnotations will cause the reader to skip annotating Resources with the file
// path and mode.
OmitReaderAnnotations bool `yaml:"omitReaderAnnotations,omitempty"`
// SetAnnotations are annotations to set on the Resources as they are read.
SetAnnotations map[string]string `yaml:"setAnnotations,omitempty"`
// FileSkipFunc is a function which returns true if reader should ignore
// the file
FileSkipFunc LocalPackageSkipFileFunc
// PreserveSeqIndent if true adds kioutil.SeqIndentAnnotation to each resource
PreserveSeqIndent bool
// FileSystem can be used to mock the disk file system.
FileSystem filesys.FileSystemOrOnDisk
// WrapBareSeqNode wraps the bare sequence node document with map node,
// kyaml uses reader annotations to track resources, it is not possible to
// add them to bare sequence nodes, this option enables wrapping such bare
// sequence nodes into map node with key yaml.BareSeqNodeWrappingKey
// note that this wrapping is different and not related to ResourceList wrapping
WrapBareSeqNode bool
}
var _ Reader = LocalPackageReader{}
var DefaultMatch = []string{"*.yaml", "*.yml"}
var JSONMatch = []string{"*.json"}
var MatchAll = append(DefaultMatch, JSONMatch...)
// Read reads the Resources.
func (r LocalPackageReader) Read() ([]*yaml.RNode, error) {
if r.PackagePath == "" {
return nil, fmt.Errorf("must specify package path")
}
// use slash for path
r.PackagePath = filepath.ToSlash(r.PackagePath)
if len(r.MatchFilesGlob) == 0 {
r.MatchFilesGlob = DefaultMatch
}
var operand ResourceNodeSlice
var pathRelativeTo string
var err error
ignoreFilesMatcher := &ignoreFilesMatcher{
fs: r.FileSystem,
}
dir, file, err := r.FileSystem.CleanedAbs(r.PackagePath)
if err != nil {
return nil, errors.Wrap(err)
}
r.PackagePath = filepath.Join(string(dir), file)
err = r.FileSystem.Walk(r.PackagePath, func(
path string, info os.FileInfo, err error) error {
if err != nil {
return errors.Wrap(err)
}
// is this the user specified path?
if path == r.PackagePath {
if info.IsDir() {
// skip the root package directory, but check for a
// .krmignore file first.
pathRelativeTo = r.PackagePath
return ignoreFilesMatcher.readIgnoreFile(path)
}
// user specified path is a file rather than a directory.
// make its path relative to its parent so it can be written to another file.
pathRelativeTo = filepath.Dir(r.PackagePath)
}
// check if we should skip the directory or file
if info.IsDir() {
return r.shouldSkipDir(path, ignoreFilesMatcher)
}
// get the relative path to file within the package so we can write the files back out
// to another location.
relPath, err := filepath.Rel(pathRelativeTo, path)
if err != nil {
return errors.WrapPrefixf(err, "%s", pathRelativeTo)
}
if match, err := r.shouldSkipFile(path, relPath, ignoreFilesMatcher); err != nil {
return err
} else if match {
// skip this file
return nil
}
r.initReaderAnnotations(relPath, info)
nodes, err := r.readFile(path, info)
if err != nil {
return errors.WrapPrefixf(err, "%s", path)
}
operand = append(operand, nodes...)
return nil
})
return operand, err
}
// readFile reads the ResourceNodes from a file
func (r *LocalPackageReader) readFile(path string, _ os.FileInfo) ([]*yaml.RNode, error) {
f, err := r.FileSystem.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
rr := &ByteReader{
DisableUnwrapping: true,
Reader: f,
OmitReaderAnnotations: r.OmitReaderAnnotations,
SetAnnotations: r.SetAnnotations,
PreserveSeqIndent: r.PreserveSeqIndent,
WrapBareSeqNode: r.WrapBareSeqNode,
}
return rr.Read()
}
// shouldSkipFile returns true if the file should be skipped
func (r *LocalPackageReader) shouldSkipFile(path, relPath string, matcher *ignoreFilesMatcher) (bool, error) {
// check if the file is covered by a .krmignore file.
if matcher.matchFile(path) {
return true, nil
}
if r.FileSkipFunc != nil && r.FileSkipFunc(relPath) {
return true, nil
}
// check if the files are in scope
for _, g := range r.MatchFilesGlob {
if match, err := filepath.Match(g, filepath.Base(path)); err != nil {
return true, errors.Wrap(err)
} else if match {
return false, nil
}
}
return true, nil
}
// initReaderAnnotations adds the LocalPackageReader Annotations to r.SetAnnotations
func (r *LocalPackageReader) initReaderAnnotations(path string, _ os.FileInfo) {
if r.SetAnnotations == nil {
r.SetAnnotations = map[string]string{}
}
if !r.OmitReaderAnnotations {
r.SetAnnotations[kioutil.PathAnnotation] = path
r.SetAnnotations[kioutil.LegacyPathAnnotation] = path
}
}
// shouldSkipDir returns a filepath.SkipDir if the directory should be skipped
func (r *LocalPackageReader) shouldSkipDir(path string, matcher *ignoreFilesMatcher) error {
if matcher.matchDir(path) {
return filepath.SkipDir
}
if r.PackageFileName == "" {
return nil
}
// check if this is a subpackage
if !r.FileSystem.Exists(filepath.Join(path, r.PackageFileName)) {
return nil
}
if !r.IncludeSubpackages {
return filepath.SkipDir
}
return matcher.readIgnoreFile(path)
}
+150
View File
@@ -0,0 +1,150 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kio
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/filesys"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// LocalPackageWriter writes ResourceNodes to a filesystem
type LocalPackageWriter struct {
Kind string `yaml:"kind,omitempty"`
// PackagePath is the path to the package directory.
PackagePath string `yaml:"path,omitempty"`
// KeepReaderAnnotations if set will retain the annotations set by LocalPackageReader
KeepReaderAnnotations bool `yaml:"keepReaderAnnotations,omitempty"`
// ClearAnnotations will clear annotations before writing the resources
ClearAnnotations []string `yaml:"clearAnnotations,omitempty"`
// FileSystem can be used to mock the disk file system.
FileSystem filesys.FileSystemOrOnDisk
}
var _ Writer = LocalPackageWriter{}
func (r LocalPackageWriter) Write(nodes []*yaml.RNode) error {
// set the path and index annotations if they are missing
if err := kioutil.DefaultPathAndIndexAnnotation("", nodes); err != nil {
return err
}
if !r.FileSystem.Exists(r.PackagePath) {
return errors.WrapPrefixf(os.ErrNotExist, "could not write to %q", r.PackagePath)
}
if !r.FileSystem.IsDir(r.PackagePath) {
// if the user specified input isn't a directory, the package is the directory of the
// target
r.PackagePath = filepath.Dir(r.PackagePath)
}
// setup indexes for writing Resources back to files
if err := r.errorIfMissingRequiredAnnotation(nodes); err != nil {
return err
}
outputFiles, err := r.indexByFilePath(nodes)
if err != nil {
return err
}
for k := range outputFiles {
if err = kioutil.SortNodes(outputFiles[k]); err != nil {
return errors.Wrap(err)
}
}
if !r.KeepReaderAnnotations {
r.ClearAnnotations = append(r.ClearAnnotations, kioutil.PathAnnotation)
r.ClearAnnotations = append(r.ClearAnnotations, kioutil.LegacyPathAnnotation)
}
// validate outputs before writing any
for path := range outputFiles {
outputPath := filepath.Join(r.PackagePath, path)
if r.FileSystem.IsDir(outputPath) {
return fmt.Errorf("config.kubernetes.io/path cannot be a directory: %s", path)
}
err = r.FileSystem.MkdirAll(filepath.Dir(outputPath))
if err != nil {
return errors.Wrap(err)
}
}
// write files
buf := bytes.NewBuffer(nil)
for path := range outputFiles {
outputPath := filepath.Join(r.PackagePath, path)
err = r.FileSystem.MkdirAll(filepath.Dir(filepath.Join(r.PackagePath, path)))
if err != nil {
return errors.Wrap(err)
}
buf.Reset()
w := ByteWriter{
Writer: buf,
KeepReaderAnnotations: r.KeepReaderAnnotations,
ClearAnnotations: r.ClearAnnotations,
}
if err = w.Write(outputFiles[path]); err != nil {
return errors.Wrap(err)
}
if err := r.FileSystem.WriteFile(outputPath, buf.Bytes()); err != nil {
return errors.Wrap(err)
}
}
return nil
}
func (r LocalPackageWriter) errorIfMissingRequiredAnnotation(nodes []*yaml.RNode) error {
for i := range nodes {
for _, s := range requiredResourcePackageAnnotations {
key, err := nodes[i].Pipe(yaml.GetAnnotation(s))
if err != nil {
return errors.Wrap(err)
}
if key == nil || key.YNode() == nil || key.YNode().Value == "" {
return errors.Errorf(
"resources must be annotated with %s to be written to files", s)
}
}
}
return nil
}
func (r LocalPackageWriter) indexByFilePath(nodes []*yaml.RNode) (map[string][]*yaml.RNode, error) {
outputFiles := map[string][]*yaml.RNode{}
for i := range nodes {
// parse the file write path
node := nodes[i]
value, err := node.Pipe(yaml.GetAnnotation(kioutil.PathAnnotation))
if err != nil {
// this should never happen if errorIfMissingRequiredAnnotation was run
return nil, errors.Wrap(err)
}
path := value.YNode().Value
outputFiles[path] = append(outputFiles[path], node)
if filepath.IsAbs(path) {
return nil, errors.Errorf("package paths may not be absolute paths")
}
if strings.Contains(filepath.Clean(path), "..") {
return nil, fmt.Errorf("resource must be written under package %s: %s",
r.PackagePath, filepath.Clean(path))
}
}
return outputFiles, nil
}
+519
View File
@@ -0,0 +1,519 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kio
import (
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"github.com/xlab/treeprint"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
type TreeStructure string
const (
// TreeStructurePackage configures TreeWriter to generate the tree structure off of the
// Resources packages.
TreeStructurePackage TreeStructure = "directory"
// TreeStructureOwners configures TreeWriter to generate the tree structure off of the
// Resource owners.
TreeStructureGraph TreeStructure = "owners"
)
var GraphStructures = []string{string(TreeStructureGraph), string(TreeStructurePackage)}
// TreeWriter prints the package structured as a tree.
// TODO(pwittrock): test this package better. it is lower-risk since it is only
// used for printing rather than updating or editing.
type TreeWriter struct {
Writer io.Writer
Root string
Fields []TreeWriterField
Structure TreeStructure
OpenAPIFileName string
}
// TreeWriterField configures a Resource field to be included in the tree
type TreeWriterField struct {
yaml.PathMatcher
Name string
SubName string
}
func (p TreeWriter) packageStructure(nodes []*yaml.RNode) error {
for i := range nodes {
if err := kioutil.CopyLegacyAnnotations(nodes[i]); err != nil {
return err
}
}
indexByPackage := p.index(nodes)
// create the new tree
tree := treeprint.New()
tree.SetValue(p.Root)
// add each package to the tree
treeIndex := map[string]treeprint.Tree{}
keys := p.sort(indexByPackage)
for _, pkg := range keys {
// create a branch for this package -- search for the parent package and create
// the branch under it -- requires that the keys are sorted
branch := tree
for parent, subTree := range treeIndex {
if strings.HasPrefix(pkg, parent) {
// found a package whose path is a prefix to our own, use this
// package if a closer one isn't found
branch = subTree
// don't break, continue searching for more closely related ancestors
}
}
// create a new branch for the package
createOk := pkg != "." // special edge case logic for tree on current working dir
if createOk {
branch = branch.AddBranch(branchName(p.Root, pkg, p.OpenAPIFileName))
}
// cache the branch for this package
treeIndex[pkg] = branch
// print each resource in the package
for i := range indexByPackage[pkg] {
var err error
if _, err = p.doResource(indexByPackage[pkg][i], "", branch); err != nil {
return err
}
}
}
_, err := io.WriteString(p.Writer, tree.String())
return err
}
// branchName takes the root directory and relative path to the directory
// and returns the branch name
func branchName(root, dirRelPath, openAPIFileName string) string {
name := filepath.Base(dirRelPath)
_, err := os.Stat(filepath.Join(root, dirRelPath, openAPIFileName))
if !os.IsNotExist(err) {
// add Pkg: prefix indicating that it is a separate package as it has
// openAPIFile
return fmt.Sprintf("Pkg: %s", name)
}
return name
}
// Write writes the ascii tree to p.Writer
func (p TreeWriter) Write(nodes []*yaml.RNode) error {
switch p.Structure {
case TreeStructurePackage:
return p.packageStructure(nodes)
case TreeStructureGraph:
return p.graphStructure(nodes)
}
// If any resource has an owner reference, default to the graph structure. Otherwise, use package structure.
for _, node := range nodes {
if owners, _ := node.Pipe(yaml.Lookup("metadata", "ownerReferences")); owners != nil {
return p.graphStructure(nodes)
}
}
return p.packageStructure(nodes)
}
// node wraps a tree node, and any children nodes
type node struct {
p TreeWriter
*yaml.RNode
children []*node
}
func (a node) Len() int { return len(a.children) }
func (a node) Swap(i, j int) { a.children[i], a.children[j] = a.children[j], a.children[i] }
func (a node) Less(i, j int) bool {
return compareNodes(a.children[i].RNode, a.children[j].RNode)
}
// Tree adds this node to the root
func (a node) Tree(root treeprint.Tree) error {
sort.Sort(a)
branch := root
var err error
// generate a node for the Resource
if a.RNode != nil {
branch, err = a.p.doResource(a.RNode, "Resource", root)
if err != nil {
return err
}
}
// attach children to the branch
for _, n := range a.children {
if err := n.Tree(branch); err != nil {
return err
}
}
return nil
}
// graphStructure writes the tree using owners for structure
func (p TreeWriter) graphStructure(nodes []*yaml.RNode) error {
resourceToOwner := map[string]*node{}
root := &node{}
// index each of the nodes by their owner
for _, n := range nodes {
ownerVal, err := ownerToString(n)
if err != nil {
return err
}
var owner *node
if ownerVal == "" {
// no owner -- attach to the root
owner = root
} else {
// owner found -- attach to the owner
var found bool
owner, found = resourceToOwner[ownerVal]
if !found {
// initialize the owner if not found
resourceToOwner[ownerVal] = &node{p: p}
owner = resourceToOwner[ownerVal]
}
}
nodeVal, err := nodeToString(n)
if err != nil {
return err
}
val, found := resourceToOwner[nodeVal]
if !found {
// initialize the node if not found -- may have already been initialized if it
// is the owner of another node
resourceToOwner[nodeVal] = &node{p: p}
val = resourceToOwner[nodeVal]
}
val.RNode = n
owner.children = append(owner.children, val)
}
for k, v := range resourceToOwner {
if v.RNode == nil {
return fmt.Errorf(
"owner '%s' not found in input, but found as an owner of input objects", k)
}
}
// print the tree
tree := treeprint.New()
if err := root.Tree(tree); err != nil {
return err
}
_, err := io.WriteString(p.Writer, tree.String())
return err
}
// nodeToString generates a string to identify the node -- matches ownerToString format
func nodeToString(node *yaml.RNode) (string, error) {
meta, err := node.GetMeta()
if err != nil {
return "", err
}
return fmt.Sprintf("%s %s/%s", meta.Kind, meta.Namespace, meta.Name), nil
}
// ownerToString generate a string to identify the owner -- matches nodeToString format
func ownerToString(node *yaml.RNode) (string, error) {
meta, err := node.GetMeta()
if err != nil {
return "", err
}
namespace := meta.Namespace
owners, err := node.Pipe(yaml.Lookup("metadata", "ownerReferences"))
if err != nil {
return "", err
}
if owners == nil {
return "", nil
}
elements, err := owners.Elements()
if err != nil {
return "", err
}
if len(elements) == 0 {
return "", err
}
owner := elements[0]
var kind, name string
if value := owner.Field("kind"); !value.IsNilOrEmpty() {
kind = value.Value.YNode().Value
}
if value := owner.Field("name"); !value.IsNilOrEmpty() {
name = value.Value.YNode().Value
}
return fmt.Sprintf("%s %s/%s", kind, namespace, name), nil
}
// index indexes the Resources by their package
func (p TreeWriter) index(nodes []*yaml.RNode) map[string][]*yaml.RNode {
// index the ResourceNodes by package
indexByPackage := map[string][]*yaml.RNode{}
for i := range nodes {
meta, err := nodes[i].GetMeta()
if err != nil || meta.Kind == "" {
// not a resource
continue
}
pkg := filepath.Dir(meta.Annotations[kioutil.PathAnnotation])
indexByPackage[pkg] = append(indexByPackage[pkg], nodes[i])
}
return indexByPackage
}
func compareNodes(i, j *yaml.RNode) bool {
metai, _ := i.GetMeta()
metaj, _ := j.GetMeta()
pi := metai.Annotations[kioutil.PathAnnotation]
pj := metaj.Annotations[kioutil.PathAnnotation]
// compare file names
if filepath.Base(pi) != filepath.Base(pj) {
return filepath.Base(pi) < filepath.Base(pj)
}
// compare namespace
if metai.Namespace != metaj.Namespace {
return metai.Namespace < metaj.Namespace
}
// compare name
if metai.Name != metaj.Name {
return metai.Name < metaj.Name
}
// compare kind
if metai.Kind != metaj.Kind {
return metai.Kind < metaj.Kind
}
// compare apiVersion
if metai.APIVersion != metaj.APIVersion {
return metai.APIVersion < metaj.APIVersion
}
return true
}
// sort sorts the Resources in the index in display order and returns the ordered
// keys for the index
//
// Packages are sorted by package name
// Resources within a package are sorted by: [filename, namespace, name, kind, apiVersion]
func (p TreeWriter) sort(indexByPackage map[string][]*yaml.RNode) []string {
var keys []string
for k := range indexByPackage {
pkgNodes := indexByPackage[k]
sort.Slice(pkgNodes, func(i, j int) bool { return compareNodes(pkgNodes[i], pkgNodes[j]) })
keys = append(keys, k)
}
// return the package names sorted lexicographically
sort.Strings(keys)
return keys
}
func (p TreeWriter) doResource(leaf *yaml.RNode, metaString string, branch treeprint.Tree) (treeprint.Tree, error) {
meta, _ := leaf.GetMeta()
if metaString == "" {
path := meta.Annotations[kioutil.PathAnnotation]
path = filepath.Base(path)
metaString = path
}
value := fmt.Sprintf("%s %s", meta.Kind, meta.Name)
if len(meta.Namespace) > 0 {
value = fmt.Sprintf("%s %s/%s", meta.Kind, meta.Namespace, meta.Name)
}
fields, err := p.getFields(leaf)
if err != nil {
return nil, err
}
n := branch.AddMetaBranch(metaString, value)
for i := range fields {
field := fields[i]
// do leaf node
if len(field.matchingElementsAndFields) == 0 {
n.AddNode(fmt.Sprintf("%s: %s", field.name, field.value))
continue
}
// do nested nodes
b := n.AddBranch(field.name)
for j := range field.matchingElementsAndFields {
elem := field.matchingElementsAndFields[j]
b := b.AddBranch(elem.name)
for k := range elem.matchingElementsAndFields {
field := elem.matchingElementsAndFields[k]
b.AddNode(fmt.Sprintf("%s: %s", field.name, field.value))
}
}
}
return n, nil
}
// getFields looks up p.Fields from leaf and structures them into treeFields.
// TODO(pwittrock): simplify this function
func (p TreeWriter) getFields(leaf *yaml.RNode) (treeFields, error) {
fieldsByName := map[string]*treeField{}
// index nested and non-nested fields
for i := range p.Fields {
f := p.Fields[i]
seq, err := leaf.Pipe(&f)
if err != nil {
return nil, err
}
if seq == nil {
continue
}
if fieldsByName[f.Name] == nil {
fieldsByName[f.Name] = &treeField{name: f.Name}
}
// non-nested field -- add directly to the treeFields list
if f.SubName == "" {
// non-nested field -- only 1 element
val, err := yaml.String(seq.Content()[0], yaml.Trim, yaml.Flow)
if err != nil {
return nil, err
}
fieldsByName[f.Name].value = val
continue
}
// nested-field -- create a parent elem, and index by the 'match' value
if fieldsByName[f.Name].subFieldByMatch == nil {
fieldsByName[f.Name].subFieldByMatch = map[string]treeFields{}
}
index := fieldsByName[f.Name].subFieldByMatch
for j := range seq.Content() {
elem := seq.Content()[j]
matches := f.Matches[elem]
str, err := yaml.String(elem, yaml.Trim, yaml.Flow)
if err != nil {
return nil, err
}
// map the field by the name of the element
// index the subfields by the matching element so we can put all the fields for the
// same element under the same branch
matchKey := strings.Join(matches, "/")
index[matchKey] = append(index[matchKey], &treeField{name: f.SubName, value: str})
}
}
// iterate over collection of all queried fields in the Resource
for _, field := range fieldsByName {
// iterate over collection of elements under the field -- indexed by element name
for match, subFields := range field.subFieldByMatch {
// create a new element for this collection of fields
// note: we will convert name to an index later, but keep the match for sorting
elem := &treeField{name: match}
field.matchingElementsAndFields = append(field.matchingElementsAndFields, elem)
// iterate over collection of queried fields for the element
for i := range subFields {
// add to the list of fields for this element
elem.matchingElementsAndFields = append(elem.matchingElementsAndFields, subFields[i])
}
}
// clear this cached data
field.subFieldByMatch = nil
}
// put the fields in a list so they are ordered
fieldList := treeFields{}
for _, v := range fieldsByName {
fieldList = append(fieldList, v)
}
// sort the fields
sort.Sort(fieldList)
for i := range fieldList {
field := fieldList[i]
// sort the elements under this field
sort.Sort(field.matchingElementsAndFields)
for i := range field.matchingElementsAndFields {
element := field.matchingElementsAndFields[i]
// sort the elements under a list field by their name
sort.Sort(element.matchingElementsAndFields)
// set the name of the element to its index
element.name = fmt.Sprintf("%d", i)
}
}
return fieldList, nil
}
// treeField wraps a field node
type treeField struct {
// name is the name of the node
name string
// value is the value of the node -- may be empty
value string
// matchingElementsAndFields is a slice of fields that go under this as a branch
matchingElementsAndFields treeFields
// subFieldByMatch caches matchingElementsAndFields indexed by the name of the matching elem
subFieldByMatch map[string]treeFields
}
// treeFields wraps a slice of treeField so they can be sorted
type treeFields []*treeField
func (nodes treeFields) Len() int { return len(nodes) }
func (nodes treeFields) Less(i, j int) bool {
iIndex, iFound := yaml.FieldOrder[nodes[i].name]
jIndex, jFound := yaml.FieldOrder[nodes[j].name]
if iFound && jFound {
return iIndex < jIndex
}
if iFound {
return true
}
if jFound {
return false
}
if nodes[i].name != nodes[j].name {
return nodes[i].name < nodes[j].name
}
if nodes[i].value != nodes[j].value {
return nodes[i].value < nodes[j].value
}
return false
}
func (nodes treeFields) Swap(i, j int) { nodes[i], nodes[j] = nodes[j], nodes[i] }