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

     1  // Copyright 2020 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 pkgutil
    16  
    17  import (
    18  	"io"
    19  	"os"
    20  	"path/filepath"
    21  	"sort"
    22  	"strings"
    23  
    24  	"github.com/GoogleContainerTools/kpt/internal/pkg"
    25  	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
    26  	"github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil"
    27  	"sigs.k8s.io/kustomize/kyaml/copyutil"
    28  	"sigs.k8s.io/kustomize/kyaml/filesys"
    29  	"sigs.k8s.io/kustomize/kyaml/kio"
    30  	"sigs.k8s.io/kustomize/kyaml/kio/filters"
    31  )
    32  
    33  // WalkPackage walks the package defined at src and provides a callback for
    34  // every folder and file. Any subpackages and the .git folder are excluded.
    35  func WalkPackage(src string, c func(string, os.FileInfo, error) error) error {
    36  	excludedDirs := make(map[string]bool)
    37  	return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
    38  		if err != nil {
    39  			return c(path, info, err)
    40  		}
    41  		// don't copy the .git dir
    42  		if path != src {
    43  			rel := strings.TrimPrefix(path, src)
    44  			if copyutil.IsDotGitFolder(rel) {
    45  				return nil
    46  			}
    47  		}
    48  
    49  		for dir := range excludedDirs {
    50  			if strings.HasPrefix(path, dir) {
    51  				return nil
    52  			}
    53  		}
    54  
    55  		if info.IsDir() {
    56  			_, err := os.Stat(filepath.Join(path, kptfilev1.KptFileName))
    57  			if err != nil && !os.IsNotExist(err) {
    58  				return c(path, info, err)
    59  			}
    60  			if err == nil && path != src {
    61  				excludedDirs[path] = true
    62  				return nil
    63  			}
    64  		}
    65  		return c(path, info, err)
    66  	})
    67  }
    68  
    69  // CopyPackage copies the content of a single package from src to dst. If includeSubpackages
    70  // is true, it will copy resources belonging to any subpackages.
    71  func CopyPackage(src, dst string, copyRootKptfile bool, matcher pkg.SubpackageMatcher) error {
    72  	subpackagesToCopy, err := pkg.Subpackages(filesys.FileSystemOrOnDisk{}, src, matcher, true)
    73  	if err != nil {
    74  		return err
    75  	}
    76  
    77  	err = WalkPackage(src, func(path string, info os.FileInfo, err error) error {
    78  		if err != nil {
    79  			return err
    80  		}
    81  
    82  		// path is an absolute path, rather than a path relative to src.
    83  		// e.g. if src is /path/to/package, then path might be /path/to/package/and/sub/dir
    84  		// we need the path relative to src `and/sub/dir` when we are copying the files to dest.
    85  		copyTo := strings.TrimPrefix(path, src)
    86  		if copyTo == "/Kptfile" {
    87  			_, err := os.Stat(filepath.Join(dst, copyTo))
    88  			if err == nil {
    89  				return nil
    90  			}
    91  			if !os.IsNotExist(err) {
    92  				return err
    93  			}
    94  		}
    95  
    96  		// make directories that don't exist
    97  		if info.IsDir() {
    98  			return os.MkdirAll(filepath.Join(dst, copyTo), info.Mode())
    99  		}
   100  
   101  		if path == filepath.Join(src, kptfilev1.KptFileName) && !copyRootKptfile {
   102  			return nil
   103  		}
   104  
   105  		// copy file by reading and writing it
   106  		b, err := os.ReadFile(filepath.Join(src, copyTo))
   107  		if err != nil {
   108  			return err
   109  		}
   110  		err = os.WriteFile(filepath.Join(dst, copyTo), b, info.Mode())
   111  		if err != nil {
   112  			return err
   113  		}
   114  
   115  		return nil
   116  	})
   117  
   118  	if err != nil {
   119  		return err
   120  	}
   121  
   122  	for _, subpackage := range subpackagesToCopy {
   123  		subpackageSrc := filepath.Join(src, subpackage)
   124  		// subpackageDest := filepath.Join(dst, strings.TrimPrefix(subpackage, src))
   125  		err = filepath.Walk(subpackageSrc, func(path string, info os.FileInfo, err error) error {
   126  			if err != nil {
   127  				return err
   128  			}
   129  			// don't copy the .git dir
   130  			if path != src {
   131  				rel := strings.TrimPrefix(path, subpackageSrc)
   132  				if copyutil.IsDotGitFolder(rel) {
   133  					return nil
   134  				}
   135  			}
   136  
   137  			copyTo := strings.TrimPrefix(path, src)
   138  			if info.IsDir() {
   139  				return os.MkdirAll(filepath.Join(dst, copyTo), info.Mode())
   140  			}
   141  
   142  			if copyTo == "/Kptfile" {
   143  				_, err := os.Stat(filepath.Join(dst, copyTo))
   144  				if err == nil {
   145  					return nil
   146  				}
   147  				if !os.IsNotExist(err) {
   148  					return err
   149  				}
   150  			}
   151  
   152  			// copy file by reading and writing it
   153  			b, err := os.ReadFile(filepath.Join(src, copyTo))
   154  			if err != nil {
   155  				return err
   156  			}
   157  			err = os.WriteFile(filepath.Join(dst, copyTo), b, info.Mode())
   158  			if err != nil {
   159  				return err
   160  			}
   161  
   162  			return nil
   163  		})
   164  		if err != nil {
   165  			return err
   166  		}
   167  	}
   168  
   169  	return nil
   170  }
   171  
   172  func RemovePackageContent(path string, removeRootKptfile bool) error {
   173  	// Walk the package (while ignoring subpackages) and delete all files.
   174  	// We capture the paths to any subdirectories in the package so we
   175  	// can handle those later. We can't do it while walking the package
   176  	// since we don't want to end up deleting directories that might
   177  	// contain a nested subpackage.
   178  	var dirs []string
   179  	if err := WalkPackage(path, func(p string, info os.FileInfo, err error) error {
   180  		if err != nil {
   181  			if os.IsNotExist(err) {
   182  				return nil
   183  			}
   184  			return err
   185  		}
   186  
   187  		if info.IsDir() {
   188  			if p != path {
   189  				dirs = append(dirs, p)
   190  			}
   191  			return nil
   192  		}
   193  
   194  		if p == filepath.Join(path, kptfilev1.KptFileName) && !removeRootKptfile {
   195  			return nil
   196  		}
   197  
   198  		return os.Remove(p)
   199  	}); err != nil {
   200  		return err
   201  	}
   202  
   203  	// Delete any of the directories in the package that are
   204  	// empty. We start with the most deeply nested directories
   205  	// so we can just check every directory for files/directories.
   206  	sort.Slice(dirs, SubPkgFirstSorter(dirs))
   207  	for _, p := range dirs {
   208  		f, err := os.Open(p)
   209  		if err != nil {
   210  			return err
   211  		}
   212  		// List up to one file or folder in the directory.
   213  		_, err = f.Readdirnames(1)
   214  		if err != nil && err != io.EOF {
   215  			return err
   216  		}
   217  		// If the returned error is EOF, it means the folder
   218  		// was empty and we can remove it.
   219  		if err == io.EOF {
   220  			err = os.RemoveAll(p)
   221  			if err != nil {
   222  				return err
   223  			}
   224  		}
   225  	}
   226  	return nil
   227  }
   228  
   229  // RootPkgFirstSorter returns a "less" function that can be used with the
   230  // sort.Slice function to correctly sort package paths so parent packages
   231  // are always before subpackages.
   232  func RootPkgFirstSorter(paths []string) func(i, j int) bool {
   233  	return func(i, j int) bool {
   234  		iPath := paths[i]
   235  		jPath := paths[j]
   236  		if iPath == "." {
   237  			return true
   238  		}
   239  		if jPath == "." {
   240  			return false
   241  		}
   242  		// First sort based on the number of segments.
   243  		iSegmentCount := len(filepath.SplitList(iPath))
   244  		jSegmentCount := len(filepath.SplitList(jPath))
   245  		if jSegmentCount != iSegmentCount {
   246  			return iSegmentCount < jSegmentCount
   247  		}
   248  		// If two paths are at the same depth, just sort lexicographically.
   249  		return iPath < jPath
   250  	}
   251  }
   252  
   253  // SubPkgFirstSorter returns a "less" function that can be used with the
   254  // sort.Slice function to correctly sort package paths so subpackages are
   255  // always before parent packages.
   256  func SubPkgFirstSorter(paths []string) func(i, j int) bool {
   257  	sorter := RootPkgFirstSorter(paths)
   258  	return func(i, j int) bool {
   259  		return !sorter(i, j)
   260  	}
   261  }
   262  
   263  // FindSubpackagesForPaths traverses the provided package paths
   264  // and finds all subpackages using the provided pkgLocatorFunc
   265  func FindSubpackagesForPaths(matcher pkg.SubpackageMatcher, recurse bool, pkgPaths ...string) ([]string, error) {
   266  	uniquePaths := make(map[string]bool)
   267  	for _, path := range pkgPaths {
   268  		paths, err := pkg.Subpackages(filesys.FileSystemOrOnDisk{}, path, matcher, recurse)
   269  		if err != nil {
   270  			return []string{}, err
   271  		}
   272  		for _, p := range paths {
   273  			uniquePaths[p] = true
   274  		}
   275  	}
   276  	paths := []string{}
   277  	for p := range uniquePaths {
   278  		paths = append(paths, p)
   279  	}
   280  	sort.Slice(paths, RootPkgFirstSorter(paths))
   281  	return paths, nil
   282  }
   283  
   284  // FormatPackage formats resources and meta-resources in the package and all its subpackages
   285  func FormatPackage(pkgPath string) {
   286  	inout := &kio.LocalPackageReadWriter{
   287  		PackagePath:       pkgPath,
   288  		MatchFilesGlob:    append(kio.DefaultMatch, kptfilev1.KptFileName),
   289  		PreserveSeqIndent: true,
   290  		WrapBareSeqNode:   true,
   291  	}
   292  	f := &filters.FormatFilter{
   293  		UseSchema: true,
   294  	}
   295  	err := kio.Pipeline{
   296  		Inputs:  []kio.Reader{inout},
   297  		Filters: []kio.Filter{f},
   298  		Outputs: []kio.Writer{inout},
   299  	}.Execute()
   300  	if err != nil {
   301  		// do not throw error if formatting fails
   302  		return
   303  	}
   304  	err = RoundTripKptfilesInPkg(pkgPath)
   305  	if err != nil {
   306  		// do not throw error if formatting fails
   307  		return
   308  	}
   309  }
   310  
   311  // RoundTripKptfilesInPkg reads and writes all Kptfiles in the package including
   312  // subpackages. This is used to format Kptfiles in the order of go structures
   313  // TODO: phanimarupaka remove this method after addressing https://github.com/GoogleContainerTools/kpt/issues/2052
   314  func RoundTripKptfilesInPkg(pkgPath string) error {
   315  	paths, err := pkg.Subpackages(filesys.FileSystemOrOnDisk{}, pkgPath, pkg.All, true)
   316  	if err != nil {
   317  		return err
   318  	}
   319  
   320  	var pkgsPaths []string
   321  	for _, path := range paths {
   322  		// join pkgPath as the paths are relative to pkgPath
   323  		pkgsPaths = append(pkgsPaths, filepath.Join(pkgPath, path))
   324  	}
   325  	// include root package as well
   326  	pkgsPaths = append(pkgsPaths, pkgPath)
   327  
   328  	for _, pkgPath := range pkgsPaths {
   329  		kf, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, pkgPath)
   330  		if err != nil {
   331  			// do not throw error if formatting fails
   332  			return err
   333  		}
   334  		err = kptfileutil.WriteFile(pkgPath, kf)
   335  		if err != nil {
   336  			// do not throw error if formatting fails
   337  			return err
   338  		}
   339  	}
   340  	return nil
   341  }
   342  
   343  // Exists returns true if a file or directory exists on the provided path,
   344  // and false otherwise.
   345  func Exists(path string) (bool, error) {
   346  	_, err := os.Stat(path)
   347  	if err != nil && !os.IsNotExist(err) {
   348  		return false, err
   349  	}
   350  	return !os.IsNotExist(err), nil
   351  }