working commit
This commit is contained in:
+349
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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] }
|
||||
Reference in New Issue
Block a user