diff --git a/kyaml/doc.go b/kyaml/doc.go new file mode 100644 index 000000000..6dd895bc3 --- /dev/null +++ b/kyaml/doc.go @@ -0,0 +1,20 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +// Package kyaml contains libraries for reading and writing Kubernetes Resource configuration +// as yaml. +// +// Resources +// +// Individual Resources are manipulated using the yaml package. +// import ( +// "sigs.k8s.io/kustomize/kyaml/yaml" +// ) +// +// Collections of Resources +// +// Collections of Resources are manipulated using the kio package. +// import ( +// "sigs.k8s.io/kustomize/kyaml/kio" +// ) +package kyaml diff --git a/kyaml/kio/byteio_reader.go b/kyaml/kio/byteio_reader.go index 1f6503a2e..0875058fb 100644 --- a/kyaml/kio/byteio_reader.go +++ b/kyaml/kio/byteio_reader.go @@ -19,7 +19,7 @@ const ( ResourceListApiVersion = "kyaml.kustomize.dev/v1alpha1" ) -// ByteReadWriter reads from an input and writes to an output +// ByteReadWriter reads from an input and writes to an output. type ByteReadWriter struct { // Reader is where ResourceNodes are decoded from. Reader io.Reader diff --git a/kyaml/kio/doc.go b/kyaml/kio/doc.go new file mode 100644 index 000000000..b0fe4a4b1 --- /dev/null +++ b/kyaml/kio/doc.go @@ -0,0 +1,32 @@ +// 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 diff --git a/kyaml/kio/kio.go b/kyaml/kio/kio.go index 12608eae7..7b6c6dc67 100644 --- a/kyaml/kio/kio.go +++ b/kyaml/kio/kio.go @@ -30,22 +30,23 @@ 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) } -// GrepFilter modifies a collection of Resource Configuration by returning the modified slice. +// 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 -// declaratively as data. +// as either data or code. // // Analogous to http://www.linfo.org/filters.html type Filter interface { Filter([]*yaml.RNode) ([]*yaml.RNode, error) } -// FilterFunc can be used to implement GrepFilter by defining a function. +// 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) { @@ -53,7 +54,7 @@ func (fn FilterFunc) Filter(o []*yaml.RNode) ([]*yaml.RNode, error) { } // Pipeline reads Resource Configuration from a set of Inputs, applies some -// transformations, and writes the results to a set of Outputs. +// transformation filters, and writes the results to a set of Outputs. // // Analogous to http://www.linfo.org/pipes.html type Pipeline struct { @@ -69,7 +70,8 @@ type Pipeline struct { Outputs []Writer `yaml:"outputs,omitempty"` } -// Execute implements the Pipeline pipeline. +// Execute executes each step in the sequence, returning immediately after encountering +// any error as part of the Pipeline. func (p Pipeline) Execute() error { var result []*yaml.RNode diff --git a/kyaml/kio/pkgio_reader.go b/kyaml/kio/pkgio_reader.go index 594586d8f..f56b48339 100644 --- a/kyaml/kio/pkgio_reader.go +++ b/kyaml/kio/pkgio_reader.go @@ -17,6 +17,7 @@ import ( // 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 } @@ -30,6 +31,9 @@ func (r *PackageBuffer) Write(nodes []*yaml.RNode) error { 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"` diff --git a/kyaml/kio/tree.go b/kyaml/kio/tree.go index 1f2f76d12..14e3766b2 100644 --- a/kyaml/kio/tree.go +++ b/kyaml/kio/tree.go @@ -27,7 +27,7 @@ const ( TreeStructureGraph TreeStructure = "graph" ) -// TreeWriter prints the package structured as a tree +// 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 { diff --git a/kyaml/yaml/doc.go b/kyaml/yaml/doc.go index 1f57e499b..b58811cf8 100644 --- a/kyaml/yaml/doc.go +++ b/kyaml/yaml/doc.go @@ -1,12 +1,49 @@ // Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 -// Package yaml contains low-level libraries for manipulating individual Kubernetes Resource -// Configuration yaml. +// Package yaml contains libraries for manipulating individual Kubernetes Resource +// Configuration as yaml, keeping yaml structure and comments. // -// It exports the public pieces of "gopkg.in/yaml.v3", so can be used as a drop in replacement. +// Parsing Resources // -// This package should be used over sigs.k8s.io/yaml: -// - If retaining or modifying yaml comments, structure, formatting -// - If Resources should be round tripped without dropping fields +// Typically Resources will be initialized as collections through the kio package libraries. +// However it is possible to directly initialize Resources using Parse. +// resource, err := yaml.Parse("apiVersion: apps/v1\nkind: Deployment") +// +// Processing Resources +// +// Individual Resources are manipulated using the Pipe and PipeE to apply Filter functions +// to transform the Resource data. +// err := resource.PipeE(yaml.SetAnnotation("key", "value")) +// +// If multiple Filter functions are provided to Pipe or PipeE, each function is applied to +// the result of the last function -- e.g. yaml.Lookup(...), yaml.SetField(...) +// +// Field values may also be retrieved using Pipe. +// annotationValue, err := resource.Pipe(yaml.GetAnnotation("key")) +// +// See http://www.linfo.org/filters.html for a definition of filters. +// +// Common Filters +// +// There are a number of standard filter functions provided by the yaml package. +// +// Working with annotations: +// [AnnotationSetter{}, AnnotationGetter{}, AnnotationClearer{}] +// +// Working with fields by path: +// [PathMatcher{}, PathGetter{}] +// +// Working with individual fields on Maps and Objects: +// [FieldMatcher{}, FieldSetter{}, FieldGetter{}] +// +// Working with individual elements in Sequences: +// [ElementAppender{}, ElementSetter{}, ElementMatcher{}] +// +// Writing Filters +// +// Users may implement their own filter functions. When doing so, can be necessary to work with +// the RNode directly rather than through Pipe. RNode provides a number of functions for doing +// so. See: +// [GetMeta(), Fields(), Elements(), String()] package yaml diff --git a/kyaml/yaml/example_test.go b/kyaml/yaml/example_test.go index 136add0a1..5d4b28de3 100644 --- a/kyaml/yaml/example_test.go +++ b/kyaml/yaml/example_test.go @@ -839,3 +839,56 @@ spec: // - bar // } + +func ExampleRNode_Elements() { + resource, err := Parse(` +- name: foo + args: ['run.sh'] +- name: bar + args: ['run.sh'] +- name: baz + args: ['run.sh'] +`) + if err != nil { + log.Fatal(err) + } + elements, err := resource.Elements() + if err != nil { + log.Fatal(err) + } + for i, e := range elements { + fmt.Println(fmt.Sprintf("Element: %d", i)) + fmt.Println(e.MustString()) + } + // Output: + // Element: 0 + // name: foo + // args: ['run.sh'] + // + // Element: 1 + // name: bar + // args: ['run.sh'] + // + // Element: 2 + // name: baz + // args: ['run.sh'] + +} + +func ExampleRNode_ElementValues() { + resource, err := Parse(` +- name: foo + args: ['run.sh'] +- name: bar + args: ['run.sh'] +- name: baz + args: ['run.sh'] +`) + if err != nil { + log.Fatal(err) + } + + fmt.Println(resource.ElementValues("name")) + // Output: + // [foo bar baz] +} diff --git a/kyaml/yaml/filters.go b/kyaml/yaml/filters.go index 2fa76eb34..b47b4993c 100644 --- a/kyaml/yaml/filters.go +++ b/kyaml/yaml/filters.go @@ -10,6 +10,7 @@ import ( "strings" ) +// Filters is the list of serializable Pipeline Filters var Filters = map[string]func() Filter{ "AnnotationClearer": func() Filter { return &AnnotationClearer{} }, "AnnotationGetter": func() Filter { return &AnnotationGetter{} }, @@ -29,7 +30,9 @@ var Filters = map[string]func() Filter{ "TeePiper": func() Filter { return &TeePiper{} }, } -// YFilter wraps the GrepFilter interface so it can be unmarshalled into a struct. +// YFilter wraps the GrepFilter interface so the filter can be represented as +// data and can be unmarshalled into a struct from a yaml config file. +// This allows Pipelines to be expressed as data rather than code. type YFilter struct { Filter } diff --git a/kyaml/yaml/fns.go b/kyaml/yaml/fns.go index 392f9f487..c454e7917 100644 --- a/kyaml/yaml/fns.go +++ b/kyaml/yaml/fns.go @@ -303,10 +303,13 @@ func (f FieldMatcher) Filter(rn *RNode) (*RNode, error) { return nil, nil } +// Lookup returns a PathGetter to lookup a field by its path. func Lookup(path ...string) PathGetter { return PathGetter{Path: path} } +// Lookup returns a PathGetter to lookup a field by its path and create it if it doesn't already +// exist. func LookupCreate(kind yaml.Kind, path ...string) PathGetter { return PathGetter{Path: path, Create: kind} } diff --git a/kyaml/yaml/order.go b/kyaml/yaml/order.go index 157defc82..e0726f504 100644 --- a/kyaml/yaml/order.go +++ b/kyaml/yaml/order.go @@ -77,19 +77,19 @@ func (s set) Has(key string) bool { return found } -// whitelistedListSortKinds contains the set of kinds that are whitelisted +// WhitelistedListSortKinds contains the set of kinds that are whitelisted // for sorting list field elements var WhitelistedListSortKinds = newSet( "CronJob", "DaemonSet", "Deployment", "Job", "ReplicaSet", "StatefulSet", "ValidatingWebhookConfiguration") -// whitelistedListSortApis contains the set of apis that are whitelisted for +// WhitelistedListSortApis contains the set of apis that are whitelisted for // sorting list field elements var WhitelistedListSortApis = newSet( "apps/v1", "apps/v1beta1", "apps/v1beta2", "batch/v1", "batch/v1beta1", "extensions/v1beta1", "v1", "admissionregistration.k8s.io/v1beta1") -// whitelistedListSortFields contains json paths to list fields that should +// WhitelistedListSortFields contains json paths to list fields that should // be sorted, and the field they should be sorted by var WhitelistedListSortFields = map[string]string{ ".spec.template.spec.containers": "name", diff --git a/kyaml/yaml/types.go b/kyaml/yaml/types.go index 06f956fc4..d3ff92238 100644 --- a/kyaml/yaml/types.go +++ b/kyaml/yaml/types.go @@ -19,10 +19,12 @@ const ( NullNodeTag = "!!null" ) +// NullNode returns a RNode point represents a null; value func NullNode() *RNode { return NewRNode(&Node{Tag: NullNodeTag}) } +// IsMissingOrNull returns true if the RNode is nil or contains and explicitly null value. func IsMissingOrNull(node *RNode) bool { if node == nil || node.YNode() == nil || node.YNode().Tag == NullNodeTag { return true @@ -30,6 +32,8 @@ func IsMissingOrNull(node *RNode) bool { return false } +// IsEmpty returns true if the RNode is MissingOrNull, or is either a MappingNode with +// no fields, or a SequenceNode with no elements. func IsEmpty(node *RNode) bool { if node == nil || node.YNode() == nil || node.YNode().Tag == NullNodeTag { return true @@ -118,7 +122,7 @@ func MustParse(value string) *RNode { return v } -// NewScalarRNode returns a new Scalar *RNode containing the provided value. +// NewScalarRNode returns a new Scalar *RNode containing the provided scalar value. func NewScalarRNode(value string) *RNode { return &RNode{ value: &yaml.Node{ @@ -127,7 +131,7 @@ func NewScalarRNode(value string) *RNode { }} } -// NewListRNode returns a new List *RNode containing the provided value. +// NewListRNode returns a new List *RNode containing the provided scalar values. func NewListRNode(values ...string) *RNode { seq := &RNode{value: &yaml.Node{Kind: yaml.SequenceNode}} for _, v := range values { @@ -139,7 +143,7 @@ func NewListRNode(values ...string) *RNode { return seq } -// NewRNode returns a new *RNode containing the provided value. +// NewRNode returns a new RNode pointer containing the provided Node. func NewRNode(value *yaml.Node) *RNode { if value != nil { value.Style = 0 @@ -147,7 +151,9 @@ func NewRNode(value *yaml.Node) *RNode { return &RNode{value: value} } -// GrepFilter may modify or walk the RNode. +// Filter defines a function to manipulate an individual RNode such as by changing +// its values, or returning a field. +// // When possible, Filters should be serializable to yaml so that they can be described // declaratively as data. // @@ -212,30 +218,31 @@ func (m MapNodeSlice) Values() []*RNode { return values } -// ResourceMeta contains the metadata for a Resource. +// ResourceMeta contains the metadata for a both Resource Type and Resource. type ResourceMeta struct { + // ApiVersion is the apiVersion field of a Resource ApiVersion string `yaml:"apiVersion,omitempty"` - Kind string `yaml:"kind,omitempty"` + // Kind is the kind field of a Resource + Kind string `yaml:"kind,omitempty"` + // ObjectMeta is the metadata field of a Resource ObjectMeta `yaml:"metadata,omitempty"` } -func NewResourceMeta(name string, typeMeta ResourceMeta) ResourceMeta { - return ResourceMeta{ - Kind: typeMeta.Kind, - ApiVersion: typeMeta.ApiVersion, - ObjectMeta: ObjectMeta{Name: name}, - } -} - +// ObjectMeta contains metadata about a Resource type ObjectMeta struct { - Name string `yaml:"name,omitempty"` - Namespace string `yaml:"namespace,omitempty"` - Labels map[string]string `yaml:"labels,omitempty"` + // Name is the metadata.name field of a Resource + Name string `yaml:"name,omitempty"` + // Namespace is the metadata.namespace field of a Resource + Namespace string `yaml:"namespace,omitempty"` + // Labels is the metadata.labels field of a Resource + Labels map[string]string `yaml:"labels,omitempty"` + // Annotations is the metadata.annotations field of a Resource. Annotations map[string]string `yaml:"annotations,omitempty"` } var MissingMetaError = errors.New("missing Resource metadata") +// GetMeta returns the ResourceMeta for a RNode func (rn *RNode) GetMeta() (ResourceMeta, error) { m := ResourceMeta{} b := &bytes.Buffer{} @@ -309,7 +316,7 @@ func (rn *RNode) YNode() *yaml.Node { return rn.value } -// SetYNode sets the yaml.Node value. +// SetYNode sets the yaml.Node value on an RNode. func (rn *RNode) SetYNode(node *yaml.Node) { if rn.value == nil || node == nil { rn.value = node @@ -318,12 +325,13 @@ func (rn *RNode) SetYNode(node *yaml.Node) { *rn.value = *node } -// SetYNode sets the value on a Document. +// AppendToFieldPath appends a field name to the FieldPath. func (rn *RNode) AppendToFieldPath(parts ...string) { rn.fieldPath = append(rn.fieldPath, parts...) } -// FieldPath returns the field path from the object root to rn. Does not include list indexes. +// FieldPath returns the field path from the Resource root node, to rn. +// Does not include list indexes. func (rn *RNode) FieldPath() []string { return rn.fieldPath } @@ -333,6 +341,7 @@ const ( Flow = "Flow" ) +// String returns a string valuer for a Node, applying the supplied formatting options func String(node *yaml.Node, opts ...string) (string, error) { if node == nil { return "", nil @@ -358,7 +367,7 @@ func String(node *yaml.Node, opts ...string) (string, error) { return val, err } -// NewScalarRNode returns the yaml NewScalarRNode representation of the RNode value. +// String returns string representation of the RNode func (rn *RNode) String() (string, error) { if rn == nil { return "", nil @@ -366,6 +375,7 @@ func (rn *RNode) String() (string, error) { return String(rn.value) } +// MustString returns string representation of the RNode or panics if there is an error func (rn *RNode) MustString() string { s, err := rn.String() if err != nil { @@ -374,7 +384,7 @@ func (rn *RNode) MustString() string { return s } -// Content returns the value node's Content field. +// Content returns Node Content field. func (rn *RNode) Content() []*yaml.Node { if rn == nil { return nil @@ -382,8 +392,8 @@ func (rn *RNode) Content() []*yaml.Node { return rn.YNode().Content } -// Fields returns the list of fields for a ResourceNode containing a MappingNode -// value. +// Fields returns the list of field names for a MappingNode. +// Returns an error for non-MappingNodes. func (rn *RNode) Fields() ([]string, error) { if err := ErrorIfInvalid(rn, yaml.MappingNode); err != nil { return nil, err @@ -395,7 +405,8 @@ func (rn *RNode) Fields() ([]string, error) { return fields, nil } -// Field returns the fieldName, fieldValue pair for MappingNodes. Returns nil for non-MappingNodes. +// Field returns a fieldName, fieldValue pair for MappingNodes. +// Returns nil for non-MappingNodes. func (rn *RNode) Field(field string) *MapNode { if rn.YNode().Kind != yaml.MappingNode { return nil @@ -409,7 +420,8 @@ func (rn *RNode) Field(field string) *MapNode { return nil } -// VisitFields calls fn for each field in rn. +// VisitFields calls fn for each field in the RNode. +// Returns an error for non-MappingNodes. func (rn *RNode) VisitFields(fn func(node *MapNode) error) error { // get the list of srcFieldNames srcFieldNames, err := rn.Fields() @@ -426,8 +438,8 @@ func (rn *RNode) VisitFields(fn func(node *MapNode) error) error { return nil } -// Elements returns a list of elements for a ResourceNode containing a -// SequenceNode value. +// Elements returns the list of elements in the RNode. +// Returns an error for non-SequenceNodes. func (rn *RNode) Elements() ([]*RNode, error) { if err := ErrorIfInvalid(rn, yaml.SequenceNode); err != nil { return nil, err @@ -439,6 +451,9 @@ func (rn *RNode) Elements() ([]*RNode, error) { return elements, nil } +// ElementValues returns a list of all observed values for a given field name in a +// list of elements. +// Returns error for non-SequenceNodes. func (rn *RNode) ElementValues(key string) ([]string, error) { if err := ErrorIfInvalid(rn, yaml.SequenceNode); err != nil { return nil, err @@ -454,7 +469,7 @@ func (rn *RNode) ElementValues(key string) ([]string, error) { } // Element returns the element in the list which contains the field matching the value. -// Returns nil for non-SequenceNodes +// Returns nil for non-SequenceNodes or if no Element matches. func (rn *RNode) Element(key, value string) *RNode { if rn.YNode().Kind != yaml.SequenceNode { return nil @@ -466,7 +481,8 @@ func (rn *RNode) Element(key, value string) *RNode { return elem } -// VisitElements calls fn for each element in the list. +// VisitElements calls fn for each element in a SequenceNode. +// Returns an error for non-SequenceNodes func (rn *RNode) VisitElements(fn func(node *RNode) error) error { elements, err := rn.Elements() if err != nil { @@ -481,14 +497,17 @@ func (rn *RNode) VisitElements(fn func(node *RNode) error) error { return nil } -// AssociativeSequencePaths is a map of paths to sequences that have associative keys. +// AssociativeSequencePaths is a list of field names that may be used as associative keys +// when merge Resources. // The order sets the precedence of the merge keys -- if multiple keys are present -// in the list, then the FIRST key which ALL elements have is used as the -// associative key. +// in Resources in a list, then the FIRST key which ALL elements in the list have is used as the +// associative key for merging that list. var AssociativeSequenceKeys = []string{ "mountPath", "devicePath", "ip", "type", "topologyKey", "name", "containerPort", } +// IsAssociative returns true if all elements in the list contain an AssociativeSequenceKey +// as a field. func IsAssociative(nodes []*RNode) bool { for i := range nodes { node := nodes[i] @@ -502,13 +521,13 @@ func IsAssociative(nodes []*RNode) bool { return false } -// IsAssociative returns true if the RNode is for an associative list. +// IsAssociative returns true if the RNode contains an AssociativeSequenceKey as a field. func (rn *RNode) IsAssociative() bool { return rn.GetAssociativeKey() != "" } -// GetAssociativeKey returns the associative key used to merge the list, or "" if the -// list is not associative. +// GetAssociativeKey returns the AssociativeSequenceKey used to merge the elements in the +// SequenceNode, or "" if the list is not associative. func (rn *RNode) GetAssociativeKey() string { // look for any associative keys in the first element for _, key := range AssociativeSequenceKeys {