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 }