...

Source file src/sigs.k8s.io/kustomize/kyaml/kio/kioutil/kioutil.go

Documentation: sigs.k8s.io/kustomize/kyaml/kio/kioutil

     1  // Copyright 2019 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package kioutil
     5  
     6  import (
     7  	"fmt"
     8  	"path"
     9  	"sort"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"sigs.k8s.io/kustomize/kyaml/errors"
    14  	"sigs.k8s.io/kustomize/kyaml/yaml"
    15  )
    16  
    17  type AnnotationKey = string
    18  
    19  const (
    20  	// internalPrefix is the prefix given to internal annotations that are used
    21  	// internally by the orchestrator
    22  	internalPrefix string = "internal.config.kubernetes.io/"
    23  
    24  	// IndexAnnotation records the index of a specific resource in a file or input stream.
    25  	IndexAnnotation AnnotationKey = internalPrefix + "index"
    26  
    27  	// PathAnnotation records the path to the file the Resource was read from
    28  	PathAnnotation AnnotationKey = internalPrefix + "path"
    29  
    30  	// SeqIndentAnnotation records the sequence nodes indentation of the input resource
    31  	SeqIndentAnnotation AnnotationKey = internalPrefix + "seqindent"
    32  
    33  	// IdAnnotation records the id of the resource to map inputs to outputs
    34  	IdAnnotation AnnotationKey = internalPrefix + "id"
    35  
    36  	// Deprecated: Use IndexAnnotation instead.
    37  	LegacyIndexAnnotation AnnotationKey = "config.kubernetes.io/index"
    38  
    39  	// Deprecated: use PathAnnotation instead.
    40  	LegacyPathAnnotation AnnotationKey = "config.kubernetes.io/path"
    41  
    42  	// Deprecated: use IdAnnotation instead.
    43  	LegacyIdAnnotation = "config.k8s.io/id"
    44  
    45  	// InternalAnnotationsMigrationResourceIDAnnotation is used to uniquely identify
    46  	// resources during round trip to and from a function execution. We will use it
    47  	// to track the internal annotations and reconcile them if needed.
    48  	InternalAnnotationsMigrationResourceIDAnnotation = internalPrefix + "annotations-migration-resource-id"
    49  )
    50  
    51  func GetFileAnnotations(rn *yaml.RNode) (string, string, error) {
    52  	rm, _ := rn.GetMeta()
    53  	annotations := rm.Annotations
    54  	path, found := annotations[PathAnnotation]
    55  	if !found {
    56  		path = annotations[LegacyPathAnnotation]
    57  	}
    58  	index, found := annotations[IndexAnnotation]
    59  	if !found {
    60  		index = annotations[LegacyIndexAnnotation]
    61  	}
    62  	return path, index, nil
    63  }
    64  
    65  func GetIdAnnotation(rn *yaml.RNode) string {
    66  	rm, _ := rn.GetMeta()
    67  	annotations := rm.Annotations
    68  	id, found := annotations[IdAnnotation]
    69  	if !found {
    70  		id = annotations[LegacyIdAnnotation]
    71  	}
    72  	return id
    73  }
    74  
    75  func CopyLegacyAnnotations(rn *yaml.RNode) error {
    76  	meta, err := rn.GetMeta()
    77  	if err != nil {
    78  		if err == yaml.ErrMissingMetadata {
    79  			// resource has no metadata, this should be a no-op
    80  			return nil
    81  		}
    82  		return err
    83  	}
    84  	if err := copyAnnotations(meta, rn, LegacyPathAnnotation, PathAnnotation); err != nil {
    85  		return err
    86  	}
    87  	if err := copyAnnotations(meta, rn, LegacyIndexAnnotation, IndexAnnotation); err != nil {
    88  		return err
    89  	}
    90  	if err := copyAnnotations(meta, rn, LegacyIdAnnotation, IdAnnotation); err != nil {
    91  		return err
    92  	}
    93  	return nil
    94  }
    95  
    96  func copyAnnotations(meta yaml.ResourceMeta, rn *yaml.RNode, legacyKey string, newKey string) error {
    97  	newValue := meta.Annotations[newKey]
    98  	legacyValue := meta.Annotations[legacyKey]
    99  	if newValue != "" {
   100  		if legacyValue == "" {
   101  			if err := rn.PipeE(yaml.SetAnnotation(legacyKey, newValue)); err != nil {
   102  				return err
   103  			}
   104  		}
   105  	} else {
   106  		if legacyValue != "" {
   107  			if err := rn.PipeE(yaml.SetAnnotation(newKey, legacyValue)); err != nil {
   108  				return err
   109  			}
   110  		}
   111  	}
   112  	return nil
   113  }
   114  
   115  // ErrorIfMissingAnnotation validates the provided annotations are present on the given resources
   116  func ErrorIfMissingAnnotation(nodes []*yaml.RNode, keys ...AnnotationKey) error {
   117  	for _, key := range keys {
   118  		for _, node := range nodes {
   119  			val, err := node.Pipe(yaml.GetAnnotation(key))
   120  			if err != nil {
   121  				return errors.Wrap(err)
   122  			}
   123  			if val == nil {
   124  				return errors.Errorf("missing annotation %s", key)
   125  			}
   126  		}
   127  	}
   128  	return nil
   129  }
   130  
   131  // CreatePathAnnotationValue creates a default path annotation value for a Resource.
   132  // The path prefix will be dir.
   133  func CreatePathAnnotationValue(dir string, m yaml.ResourceMeta) string {
   134  	filename := fmt.Sprintf("%s_%s.yaml", strings.ToLower(m.Kind), m.Name)
   135  	return path.Join(dir, m.Namespace, filename)
   136  }
   137  
   138  // DefaultPathAndIndexAnnotation sets a default path or index value on any nodes missing the
   139  // annotation
   140  func DefaultPathAndIndexAnnotation(dir string, nodes []*yaml.RNode) error {
   141  	counts := map[string]int{}
   142  
   143  	// check each node for the path annotation
   144  	for i := range nodes {
   145  		if err := CopyLegacyAnnotations(nodes[i]); err != nil {
   146  			return err
   147  		}
   148  		m, err := nodes[i].GetMeta()
   149  		if err != nil {
   150  			return err
   151  		}
   152  
   153  		// calculate the max index in each file in case we are appending
   154  		if p, found := m.Annotations[PathAnnotation]; found {
   155  			// record the max indexes into each file
   156  			if i, found := m.Annotations[IndexAnnotation]; found {
   157  				index, _ := strconv.Atoi(i)
   158  				if index > counts[p] {
   159  					counts[p] = index
   160  				}
   161  			}
   162  
   163  			// has the path annotation already -- do nothing
   164  			continue
   165  		}
   166  
   167  		// set a path annotation on the Resource
   168  		path := CreatePathAnnotationValue(dir, m)
   169  		if err := nodes[i].PipeE(yaml.SetAnnotation(PathAnnotation, path)); err != nil {
   170  			return err
   171  		}
   172  		if err := nodes[i].PipeE(yaml.SetAnnotation(LegacyPathAnnotation, path)); err != nil {
   173  			return err
   174  		}
   175  	}
   176  
   177  	// set the index annotations
   178  	for i := range nodes {
   179  		m, err := nodes[i].GetMeta()
   180  		if err != nil {
   181  			return err
   182  		}
   183  
   184  		if _, found := m.Annotations[IndexAnnotation]; found {
   185  			continue
   186  		}
   187  
   188  		p := m.Annotations[PathAnnotation]
   189  
   190  		// set an index annotation on the Resource
   191  		c := counts[p]
   192  		counts[p] = c + 1
   193  		if err := nodes[i].PipeE(
   194  			yaml.SetAnnotation(IndexAnnotation, fmt.Sprintf("%d", c))); err != nil {
   195  			return err
   196  		}
   197  		if err := nodes[i].PipeE(
   198  			yaml.SetAnnotation(LegacyIndexAnnotation, fmt.Sprintf("%d", c))); err != nil {
   199  			return err
   200  		}
   201  	}
   202  	return nil
   203  }
   204  
   205  // DefaultPathAnnotation sets a default path annotation on any Reources
   206  // missing it.
   207  func DefaultPathAnnotation(dir string, nodes []*yaml.RNode) error {
   208  	// check each node for the path annotation
   209  	for i := range nodes {
   210  		if err := CopyLegacyAnnotations(nodes[i]); err != nil {
   211  			return err
   212  		}
   213  		m, err := nodes[i].GetMeta()
   214  		if err != nil {
   215  			return err
   216  		}
   217  
   218  		if _, found := m.Annotations[PathAnnotation]; found {
   219  			// has the path annotation already -- do nothing
   220  			continue
   221  		}
   222  
   223  		// set a path annotation on the Resource
   224  		path := CreatePathAnnotationValue(dir, m)
   225  		if err := nodes[i].PipeE(yaml.SetAnnotation(PathAnnotation, path)); err != nil {
   226  			return err
   227  		}
   228  		if err := nodes[i].PipeE(yaml.SetAnnotation(LegacyPathAnnotation, path)); err != nil {
   229  			return err
   230  		}
   231  	}
   232  	return nil
   233  }
   234  
   235  // Map invokes fn for each element in nodes.
   236  func Map(nodes []*yaml.RNode, fn func(*yaml.RNode) (*yaml.RNode, error)) ([]*yaml.RNode, error) {
   237  	var returnNodes []*yaml.RNode
   238  	for i := range nodes {
   239  		n, err := fn(nodes[i])
   240  		if err != nil {
   241  			return nil, errors.Wrap(err)
   242  		}
   243  		if n != nil {
   244  			returnNodes = append(returnNodes, n)
   245  		}
   246  	}
   247  	return returnNodes, nil
   248  }
   249  
   250  func MapMeta(nodes []*yaml.RNode, fn func(*yaml.RNode, yaml.ResourceMeta) (*yaml.RNode, error)) (
   251  	[]*yaml.RNode, error) {
   252  	var returnNodes []*yaml.RNode
   253  	for i := range nodes {
   254  		meta, err := nodes[i].GetMeta()
   255  		if err != nil {
   256  			return nil, errors.Wrap(err)
   257  		}
   258  		n, err := fn(nodes[i], meta)
   259  		if err != nil {
   260  			return nil, errors.Wrap(err)
   261  		}
   262  		if n != nil {
   263  			returnNodes = append(returnNodes, n)
   264  		}
   265  	}
   266  	return returnNodes, nil
   267  }
   268  
   269  // SortNodes sorts nodes in place:
   270  // - by PathAnnotation annotation
   271  // - by IndexAnnotation annotation
   272  func SortNodes(nodes []*yaml.RNode) error {
   273  	var err error
   274  	// use stable sort to keep ordering of equal elements
   275  	sort.SliceStable(nodes, func(i, j int) bool {
   276  		if err != nil {
   277  			return false
   278  		}
   279  		if err := CopyLegacyAnnotations(nodes[i]); err != nil {
   280  			return false
   281  		}
   282  		if err := CopyLegacyAnnotations(nodes[j]); err != nil {
   283  			return false
   284  		}
   285  		var iMeta, jMeta yaml.ResourceMeta
   286  		if iMeta, _ = nodes[i].GetMeta(); err != nil {
   287  			return false
   288  		}
   289  		if jMeta, _ = nodes[j].GetMeta(); err != nil {
   290  			return false
   291  		}
   292  
   293  		iValue := iMeta.Annotations[PathAnnotation]
   294  		jValue := jMeta.Annotations[PathAnnotation]
   295  		if iValue != jValue {
   296  			return iValue < jValue
   297  		}
   298  
   299  		iValue = iMeta.Annotations[IndexAnnotation]
   300  		jValue = jMeta.Annotations[IndexAnnotation]
   301  
   302  		// put resource config without an index first
   303  		if iValue == jValue {
   304  			return false
   305  		}
   306  		if iValue == "" {
   307  			return true
   308  		}
   309  		if jValue == "" {
   310  			return false
   311  		}
   312  
   313  		// sort by index
   314  		var iIndex, jIndex int
   315  		iIndex, err = strconv.Atoi(iValue)
   316  		if err != nil {
   317  			err = fmt.Errorf("unable to parse config.kubernetes.io/index %s :%v", iValue, err)
   318  			return false
   319  		}
   320  		jIndex, err = strconv.Atoi(jValue)
   321  		if err != nil {
   322  			err = fmt.Errorf("unable to parse config.kubernetes.io/index %s :%v", jValue, err)
   323  			return false
   324  		}
   325  		if iIndex != jIndex {
   326  			return iIndex < jIndex
   327  		}
   328  
   329  		// elements are equal
   330  		return false
   331  	})
   332  	return errors.Wrap(err)
   333  }
   334  
   335  // CopyInternalAnnotations copies the annotations that begin with the prefix
   336  // `internal.config.kubernetes.io` from the source RNode to the destination RNode.
   337  // It takes a parameter exclusions, which is a list of annotation keys to ignore.
   338  func CopyInternalAnnotations(src *yaml.RNode, dst *yaml.RNode, exclusions ...AnnotationKey) error {
   339  	srcAnnotations := GetInternalAnnotations(src)
   340  	for k, v := range srcAnnotations {
   341  		if stringSliceContains(exclusions, k) {
   342  			continue
   343  		}
   344  		if err := dst.PipeE(yaml.SetAnnotation(k, v)); err != nil {
   345  			return err
   346  		}
   347  	}
   348  	return nil
   349  }
   350  
   351  // ConfirmInternalAnnotationUnchanged compares the annotations of the RNodes that begin with the prefix
   352  // `internal.config.kubernetes.io`, throwing an error if they differ. It takes a parameter exclusions,
   353  // which is a list of annotation keys to ignore.
   354  func ConfirmInternalAnnotationUnchanged(r1 *yaml.RNode, r2 *yaml.RNode, exclusions ...AnnotationKey) error {
   355  	r1Annotations := GetInternalAnnotations(r1)
   356  	r2Annotations := GetInternalAnnotations(r2)
   357  
   358  	// this is a map to prevent duplicates
   359  	diffAnnos := make(map[string]bool)
   360  
   361  	for k, v1 := range r1Annotations {
   362  		if stringSliceContains(exclusions, k) {
   363  			continue
   364  		}
   365  		if v2, ok := r2Annotations[k]; !ok || v1 != v2 {
   366  			diffAnnos[k] = true
   367  		}
   368  	}
   369  
   370  	for k, v2 := range r2Annotations {
   371  		if stringSliceContains(exclusions, k) {
   372  			continue
   373  		}
   374  		if v1, ok := r1Annotations[k]; !ok || v2 != v1 {
   375  			diffAnnos[k] = true
   376  		}
   377  	}
   378  
   379  	if len(diffAnnos) > 0 {
   380  		keys := make([]string, 0, len(diffAnnos))
   381  		for k := range diffAnnos {
   382  			keys = append(keys, k)
   383  		}
   384  		sort.Strings(keys)
   385  
   386  		errorString := "internal annotations differ: "
   387  		for _, key := range keys {
   388  			errorString = errorString + key + ", "
   389  		}
   390  		return errors.Errorf(errorString[0 : len(errorString)-2])
   391  	}
   392  
   393  	return nil
   394  }
   395  
   396  // GetInternalAnnotations returns a map of all the annotations of the provided
   397  // RNode that satisfies one of the following: 1) begin with the prefix
   398  // `internal.config.kubernetes.io` 2) is one of `config.kubernetes.io/path`,
   399  // `config.kubernetes.io/index` and `config.k8s.io/id`.
   400  func GetInternalAnnotations(rn *yaml.RNode) map[string]string {
   401  	meta, _ := rn.GetMeta()
   402  	annotations := meta.Annotations
   403  	result := make(map[string]string)
   404  	for k, v := range annotations {
   405  		if strings.HasPrefix(k, internalPrefix) || k == LegacyPathAnnotation || k == LegacyIndexAnnotation || k == LegacyIdAnnotation {
   406  			result[k] = v
   407  		}
   408  	}
   409  	return result
   410  }
   411  
   412  // stringSliceContains returns true if the slice has the string.
   413  func stringSliceContains(slice []string, str string) bool {
   414  	for _, s := range slice {
   415  		if s == str {
   416  			return true
   417  		}
   418  	}
   419  	return false
   420  }
   421  

View as plain text