github.com/SamarSidharth/kpt@v0.0.0-20231122062228-c7d747ae3ace/internal/util/addmergecomment/addmergecomment.go (about)

     1  // Copyright 2021 The kpt Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package addmergecomment
    16  
    17  import (
    18  	"fmt"
    19  	"os"
    20  	"strings"
    21  
    22  	"github.com/GoogleContainerTools/kpt/internal/util/merge"
    23  	"sigs.k8s.io/kustomize/kyaml/copyutil"
    24  	"sigs.k8s.io/kustomize/kyaml/kio"
    25  	"sigs.k8s.io/kustomize/kyaml/resid"
    26  	kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
    27  )
    28  
    29  // TODO(yuwenma): Those const vars are defined in kpt-functions-sdk/go/fn v0.0.0-20220706221933-7181f451a663+
    30  // we cannot import go/fn directly because the porch/set-namespace uses an older go/fn version. Bumping kpt module alone fails
    31  // kpt CI.
    32  // We should update porch/set-namespace once https://github.com/GoogleContainerTools/kpt-functions-catalog/pull/885 is released.
    33  // and cleanup the const vars below
    34  const (
    35  	upstreamIdentifierFmt = "%s|%s|%s|%s"
    36  	upstreamIdentifier    = "internal.kpt.dev/upstream-identifier"
    37  	unknownNamespace      = "~C"
    38  	defaultNamespace      = "default"
    39  )
    40  
    41  // AddMergeComment adds merge comments with format "kpt-merge: namespace/name"
    42  // to all resources in the package
    43  type AddMergeComment struct{}
    44  
    45  // Process invokes AddMergeComment kyaml filter on the resources in input packages paths
    46  func Process(paths ...string) error {
    47  	for _, path := range paths {
    48  		inout := &kio.LocalPackageReadWriter{PackagePath: path, PreserveSeqIndent: true, WrapBareSeqNode: true}
    49  		amc := &AddMergeComment{}
    50  		err := kio.Pipeline{
    51  			Inputs:  []kio.Reader{inout},
    52  			Filters: []kio.Filter{kio.FilterAll(amc)},
    53  			Outputs: []kio.Writer{inout},
    54  		}.Execute()
    55  		if err != nil {
    56  			// this should be a best effort, do not error if this step fails
    57  			// https://github.com/GoogleContainerTools/kpt/issues/2559
    58  			return nil
    59  		}
    60  	}
    61  	return nil
    62  }
    63  
    64  // addUpstreamAnnotation adds internal.kpt.dev/upstream-identifier annotation to resource.
    65  // In a 3 level package chain (root -> branch -> deployable), the downstream package uses the upstream package meta GKNN
    66  // as its upstream origin, not the upstream its own origin. For example
    67  // root: No `upstreamIdentifier` annotation
    68  // branch: `upstream-identifier=rootGKNN`
    69  // deployable: `upstream-identifier=branchGKNN`
    70  // One known caveat is that upstream meta change can cause downstream origin mismatch. This potentially causes the 3-way merge
    71  // to fail in pkg update step.
    72  func addUpstreamAnnotation(object *kyaml.RNode, mergeComment string) error {
    73  	group, _ := resid.ParseGroupVersion(object.GetApiVersion())
    74  	var name, namespace string
    75  	if strings.Contains(mergeComment, merge.MergeCommentPrefix) {
    76  		nsAndName := merge.NsAndNameForMerge(mergeComment)
    77  		namespace = nsAndName[0]
    78  		name = nsAndName[1]
    79  	} else {
    80  		namespace = object.GetNamespace()
    81  		name = object.GetName()
    82  	}
    83  	// Convert namespace to follow the upstream identifier convention, where
    84  	// - empty string is treated as "default"
    85  	// - unknown custom resource or cluster scoped resource use placeholder "~C"
    86  	if namespace == "" {
    87  		namespace = defaultNamespace
    88  	} else if object.GetNamespace() == resid.TotallyNotANamespace {
    89  		namespace = unknownNamespace
    90  	}
    91  	upstreamIdentifierValue := fmt.Sprintf(upstreamIdentifierFmt, group, object.GetKind(), namespace, name)
    92  	return object.PipeE(kyaml.SetAnnotation(upstreamIdentifier, upstreamIdentifierValue))
    93  }
    94  
    95  // Filter implements kyaml.Filter
    96  // this filter adds merge comment with format "kpt-merge: namespace/name" to
    97  // the input resource, if the namespace field doesn't exist on the resource,
    98  // it uses "default" namespace
    99  func (amc *AddMergeComment) Filter(object *kyaml.RNode) (*kyaml.RNode, error) {
   100  	rm, err := object.GetMeta()
   101  	if err != nil {
   102  		// skip adding merge comment if no metadata
   103  		return object, nil
   104  	}
   105  	mf := object.Field(kyaml.MetadataField)
   106  	if object.GetName() == "" && object.GetNamespace() == "" && len(object.GetLabels()) == 0 {
   107  		// skip adding merge comment if empty metadata. Since the intermediate annotations always exist,
   108  		// mf.IsNilOrEmpty cannot tell whether it's empty meta or not.
   109  		// e.g. Empty meta with internal annotations.
   110  		//kind: MyKind
   111  		//spec:
   112  		//  replicas: 3
   113  		//metadata:
   114  		//  annotations:
   115  		//    config.kubernetes.io/index: '0'
   116  		//    config.kubernetes.io/path: 'k8s-cli-982798852.yaml'
   117  		//    internal.config.kubernetes.io/index: '0'
   118  		//    internal.config.kubernetes.io/path: 'k8s-cli-982798852.yaml'
   119  		//    internal.config.kubernetes.io/seqindent: 'compact'
   120  		//    internal.config.kubernetes.io/annotations-migration-resource-id: '0'
   121  		return object, nil
   122  	}
   123  
   124  	// Only add merge comment if merge comment does not present
   125  	if !strings.Contains(mf.Key.YNode().LineComment, merge.MergeCommentPrefix) {
   126  		mf.Key.YNode().LineComment = fmt.Sprintf("%s %s/%s", merge.MergeCommentPrefix, rm.Namespace, rm.Name)
   127  	}
   128  	// We will migrate kpt-merge comment to upstream-identifier annotation. As an intermediate stage, this filter
   129  	// preserves the mergeComment behavior to guarantee the backward compatibility.
   130  	if err := addUpstreamAnnotation(object, mf.Key.YNode().LineComment); err != nil {
   131  		return object, nil
   132  	}
   133  	return object, nil
   134  }
   135  
   136  // ProcessWithCleanup copies the input directory contents to
   137  // new temp directory and adds merge comment to the resources in directory
   138  // it also returns the cleanup function to clean the created temp directory
   139  func ProcessWithCleanup(path string) (string, func(), error) {
   140  	expected, err := os.MkdirTemp("", "")
   141  	if err != nil {
   142  		return "", nil, err
   143  	}
   144  	err = copyutil.CopyDir(path, expected)
   145  	if err != nil {
   146  		return "", nil, err
   147  	}
   148  
   149  	err = Process(expected)
   150  	if err != nil {
   151  		return "", nil, err
   152  	}
   153  
   154  	clean := func() {
   155  		os.RemoveAll(expected)
   156  	}
   157  
   158  	return expected, clean, nil
   159  }