// 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/v1alpha1" ) // 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 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, } val, err := b.Read() if rw.FunctionConfig == nil { rw.FunctionConfig = b.FunctionConfig } rw.Results = b.Results if !rw.NoWrap { rw.WrappingAPIVersion = b.WrappingAPIVersion rw.WrappingKind = b.WrappingKind } return val, errors.Wrap(err) } func (rw *ByteReadWriter) Write(nodes []*yaml.RNode) error { return ByteWriter{ Writer: rw.Writer, KeepReaderAnnotations: rw.KeepReaderAnnotations, Style: rw.Style, FunctionConfig: rw.FunctionConfig, Results: rw.Results, WrappingAPIVersion: rw.WrappingAPIVersion, WrappingKind: rw.WrappingKind, }.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, 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 } 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++ } 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.Wrap(err) } 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) if r.SetAnnotations == nil { r.SetAnnotations = map[string]string{} } if !r.OmitReaderAnnotations { r.SetAnnotations[kioutil.IndexAnnotation] = fmt.Sprintf("%d", index) if r.PreserveSeqIndent { // derive and add the seqindent annotation seqIndentStyle := yaml.DeriveSeqIndentStyle(originalYAML) if seqIndentStyle != "" { r.SetAnnotations[kioutil.SeqIndentAnnotation] = fmt.Sprintf("%s", 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 yaml.NewRNode(node), nil }