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  }