github.com/SamarSidharth/kpt@v0.0.0-20231122062228-c7d747ae3ace/internal/util/update/resource-merge.go (about) 1 // Copyright 2019 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 update 16 17 import ( 18 "bytes" 19 "fmt" 20 "os" 21 "path/filepath" 22 "strings" 23 24 "github.com/GoogleContainerTools/kpt/internal/errors" 25 "github.com/GoogleContainerTools/kpt/internal/pkg" 26 "github.com/GoogleContainerTools/kpt/internal/types" 27 pkgdiff "github.com/GoogleContainerTools/kpt/internal/util/diff" 28 "github.com/GoogleContainerTools/kpt/internal/util/merge" 29 "github.com/GoogleContainerTools/kpt/internal/util/pkgutil" 30 kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" 31 "github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil" 32 "sigs.k8s.io/kustomize/kyaml/copyutil" 33 "sigs.k8s.io/kustomize/kyaml/kio" 34 "sigs.k8s.io/kustomize/kyaml/sets" 35 ) 36 37 // ResourceMergeUpdater updates a package by fetching the original and updated source 38 // packages, and performing a 3-way merge of the Resources. 39 type ResourceMergeUpdater struct{} 40 41 func (u ResourceMergeUpdater) Update(options Options) error { 42 const op errors.Op = "update.Update" 43 if !options.IsRoot { 44 hasChanges, err := PkgHasUpdatedUpstream(options.LocalPath, options.OriginPath) 45 if err != nil { 46 return errors.E(op, types.UniquePath(options.LocalPath), err) 47 } 48 49 // If the upstream information in local has changed from origin, it 50 // means the user had updated the package independently and we don't 51 // want to override it. 52 if hasChanges { 53 return nil 54 } 55 } 56 57 // Find all subpackages in local, upstream and original. They are sorted 58 // in increasing order based on the depth of the subpackage relative to the 59 // root package. 60 subPkgPaths, err := pkgutil.FindSubpackagesForPaths(pkg.Local, true, 61 options.LocalPath, options.UpdatedPath, options.OriginPath) 62 if err != nil { 63 return errors.E(op, types.UniquePath(options.LocalPath), err) 64 } 65 66 // Update each package and subpackage. Parent package is updated before 67 // subpackages to make sure auto-setters can work correctly. 68 for _, subPkgPath := range append([]string{"."}, subPkgPaths...) { 69 isRootPkg := false 70 if subPkgPath == "." && options.IsRoot { 71 isRootPkg = true 72 } 73 localSubPkgPath := filepath.Join(options.LocalPath, subPkgPath) 74 updatedSubPkgPath := filepath.Join(options.UpdatedPath, subPkgPath) 75 originalSubPkgPath := filepath.Join(options.OriginPath, subPkgPath) 76 77 err := u.updatePackage(subPkgPath, localSubPkgPath, updatedSubPkgPath, originalSubPkgPath, isRootPkg) 78 if err != nil { 79 return errors.E(op, types.UniquePath(localSubPkgPath), err) 80 } 81 } 82 return nil 83 } 84 85 // updatePackage updates the package in the location specified by localPath 86 // using the provided paths to the updated version of the package and the 87 // original version of the package. 88 func (u ResourceMergeUpdater) updatePackage(subPkgPath, localPath, updatedPath, originalPath string, isRootPkg bool) error { 89 const op errors.Op = "update.updatePackage" 90 localExists, err := pkgutil.Exists(localPath) 91 if err != nil { 92 return errors.E(op, types.UniquePath(localPath), err) 93 } 94 95 updatedExists, err := pkgutil.Exists(updatedPath) 96 if err != nil { 97 return errors.E(op, types.UniquePath(localPath), err) 98 } 99 100 originalExists, err := pkgutil.Exists(originalPath) 101 if err != nil { 102 return errors.E(op, types.UniquePath(localPath), err) 103 } 104 105 switch { 106 // Check if subpackage has been added both in upstream and in local 107 case !originalExists && localExists && updatedExists: 108 return errors.E(op, types.UniquePath(localPath), 109 fmt.Errorf("subpackage %q added in both upstream and local", subPkgPath)) 110 // Package added in upstream 111 case !originalExists && !localExists && updatedExists: 112 if err := pkgutil.CopyPackage(updatedPath, localPath, !isRootPkg, pkg.None); err != nil { 113 return errors.E(op, types.UniquePath(localPath), err) 114 } 115 // Package added locally 116 case !originalExists && localExists && !updatedExists: 117 break // No action needed. 118 // Package deleted from both upstream and local 119 case originalExists && !localExists && !updatedExists: 120 break // No action needed. 121 // Package deleted from local 122 case originalExists && !localExists && updatedExists: 123 break // In this case we assume the user knows what they are doing, so 124 // we don't re-add the updated package from upstream. 125 // Package deleted from upstream 126 case originalExists && localExists && !updatedExists: 127 // Check the diff. If there are local changes, we keep the subpackage. 128 diff, err := pkgdiff.PkgDiff(originalPath, localPath) 129 if err != nil { 130 return errors.E(op, types.UniquePath(localPath), err) 131 } 132 if diff.Len() == 0 { 133 if err := os.RemoveAll(localPath); err != nil { 134 return errors.E(op, types.UniquePath(localPath), err) 135 } 136 } 137 default: 138 if err := u.mergePackage(localPath, updatedPath, originalPath, subPkgPath, isRootPkg); err != nil { 139 return errors.E(op, types.UniquePath(localPath), err) 140 } 141 } 142 return nil 143 } 144 145 // mergePackage merge a package. It does a 3-way merge by using the provided 146 // paths to the local, updated and original versions of the package. 147 func (u ResourceMergeUpdater) mergePackage(localPath, updatedPath, originalPath, _ string, isRootPkg bool) error { 148 const op errors.Op = "update.mergePackage" 149 if err := kptfileutil.UpdateKptfile(localPath, updatedPath, originalPath, !isRootPkg); err != nil { 150 return errors.E(op, types.UniquePath(localPath), err) 151 } 152 153 // merge the Resources: original + updated + dest => dest 154 err := merge.Merge3{ 155 OriginalPath: originalPath, 156 UpdatedPath: updatedPath, 157 DestPath: localPath, 158 // TODO: Write a test to ensure this is set 159 MergeOnPath: true, 160 IncludeSubPackages: false, 161 }.Merge() 162 if err != nil { 163 return errors.E(op, types.UniquePath(localPath), err) 164 } 165 166 if err := ReplaceNonKRMFiles(updatedPath, originalPath, localPath); err != nil { 167 return errors.E(op, types.UniquePath(localPath), err) 168 } 169 return nil 170 } 171 172 // replaceNonKRMFiles replaces the non KRM files in localDir with the corresponding files in updatedDir, 173 // it also deletes non KRM files and sub dirs which are present in localDir and not in updatedDir 174 func ReplaceNonKRMFiles(updatedDir, originalDir, localDir string) error { 175 const op errors.Op = "update.ReplaceNonKRMFiles" 176 updatedSubDirs, updatedFiles, err := getSubDirsAndNonKrmFiles(updatedDir) 177 if err != nil { 178 return errors.E(op, types.UniquePath(localDir), err) 179 } 180 181 originalSubDirs, originalFiles, err := getSubDirsAndNonKrmFiles(originalDir) 182 if err != nil { 183 return errors.E(op, types.UniquePath(localDir), err) 184 } 185 186 localSubDirs, localFiles, err := getSubDirsAndNonKrmFiles(localDir) 187 if err != nil { 188 return errors.E(op, types.UniquePath(localDir), err) 189 } 190 191 // identify all non KRM files modified locally, to leave them untouched 192 locallyModifiedFiles := sets.String{} 193 for _, file := range localFiles.List() { 194 if !originalFiles.Has(file) { 195 // new local file has been added 196 locallyModifiedFiles.Insert(file) 197 continue 198 } 199 same, err := compareFiles(filepath.Join(originalDir, file), filepath.Join(localDir, file)) 200 if err != nil { 201 return errors.E(op, types.UniquePath(localDir), err) 202 } 203 if !same { 204 // local file has been modified 205 locallyModifiedFiles.Insert(file) 206 continue 207 } 208 209 // remove the file from local if it is not modified and is deleted from updated upstream 210 if !updatedFiles.Has(file) { 211 if err = os.Remove(filepath.Join(localDir, file)); err != nil { 212 return errors.E(op, types.UniquePath(localDir), err) 213 } 214 } 215 } 216 217 // make sure local has all sub-dirs present in updated 218 for _, dir := range updatedSubDirs.List() { 219 if err = os.MkdirAll(filepath.Join(localDir, dir), 0700); err != nil { 220 return errors.E(op, types.UniquePath(localDir), err) 221 } 222 } 223 224 // replace all non KRM files in local with the ones in updated 225 for _, file := range updatedFiles.List() { 226 if locallyModifiedFiles.Has(file) { 227 // skip syncing locally modified files 228 continue 229 } 230 err = copyutil.SyncFile(filepath.Join(updatedDir, file), filepath.Join(localDir, file)) 231 if err != nil { 232 return errors.E(op, types.UniquePath(localDir), err) 233 } 234 } 235 236 // delete all the empty dirs in local which are not in updated 237 for _, dir := range localSubDirs.List() { 238 if !updatedSubDirs.Has(dir) && originalSubDirs.Has(dir) { 239 // removes only empty dirs 240 os.Remove(filepath.Join(localDir, dir)) 241 } 242 } 243 244 return nil 245 } 246 247 // getSubDirsAndNonKrmFiles returns the list of all non git sub dirs and, non git+non KRM files 248 // in the root directory 249 func getSubDirsAndNonKrmFiles(root string) (sets.String, sets.String, error) { 250 const op errors.Op = "update.getSubDirsAndNonKrmFiles" 251 files := sets.String{} 252 dirs := sets.String{} 253 err := pkgutil.WalkPackage(root, func(path string, info os.FileInfo, err error) error { 254 if err != nil { 255 return errors.E(op, errors.IO, err) 256 } 257 258 if info.IsDir() { 259 path = strings.TrimPrefix(path, root) 260 if len(path) > 0 { 261 dirs.Insert(path) 262 } 263 return nil 264 } 265 isKrm, err := isKrmFile(path) 266 if err != nil { 267 return errors.E(op, err) 268 } 269 if !isKrm { 270 path = strings.TrimPrefix(path, root) 271 if len(path) > 0 && !strings.Contains(path, ".git") { 272 files.Insert(path) 273 } 274 } 275 return nil 276 }) 277 if err != nil { 278 return nil, nil, errors.E(op, err) 279 } 280 return dirs, files, nil 281 } 282 283 var krmFilesGlob = append([]string{kptfilev1.KptFileName}, kio.DefaultMatch...) 284 285 // isKrmFile checks if the file pointed to by the path is a yaml file (including 286 // the Kptfile). 287 func isKrmFile(path string) (bool, error) { 288 const op errors.Op = "update.isKrmFile" 289 for _, g := range krmFilesGlob { 290 if match, err := filepath.Match(g, filepath.Base(path)); err != nil { 291 return false, errors.E(op, err) 292 } else if match { 293 return true, nil 294 } 295 } 296 return false, nil 297 } 298 299 // compareFiles returns true if src file content is equal to dst file content 300 func compareFiles(src, dst string) (bool, error) { 301 const op errors.Op = "update.compareFiles" 302 b1, err := os.ReadFile(src) 303 if err != nil { 304 return false, errors.E(op, errors.IO, err) 305 } 306 b2, err := os.ReadFile(dst) 307 if err != nil { 308 return false, errors.E(op, errors.IO, err) 309 } 310 if bytes.Equal(b1, b2) { 311 return true, nil 312 } 313 return false, nil 314 }