...

Source file src/sigs.k8s.io/kustomize/api/internal/loader/fileloader.go

Documentation: sigs.k8s.io/kustomize/api/internal/loader

     1  // Copyright 2019 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package loader
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"log"
    10  	"net/http"
    11  	"net/url"
    12  	"path/filepath"
    13  	"strings"
    14  
    15  	"sigs.k8s.io/kustomize/api/ifc"
    16  	"sigs.k8s.io/kustomize/api/internal/git"
    17  	"sigs.k8s.io/kustomize/kyaml/errors"
    18  	"sigs.k8s.io/kustomize/kyaml/filesys"
    19  )
    20  
    21  // IsRemoteFile returns whether path has a url scheme that kustomize allows for
    22  // remote files. See https://github.com/kubernetes-sigs/kustomize/blob/master/examples/remoteBuild.md
    23  func IsRemoteFile(path string) bool {
    24  	u, err := url.Parse(path)
    25  	return err == nil && (u.Scheme == "http" || u.Scheme == "https")
    26  }
    27  
    28  // FileLoader is a kustomization's interface to files.
    29  //
    30  // The directory in which a kustomization file sits
    31  // is referred to below as the kustomization's _root_.
    32  //
    33  // An instance of fileLoader has an immutable root,
    34  // and offers a `New` method returning a new loader
    35  // with a new root.
    36  //
    37  // A kustomization file refers to two kinds of files:
    38  //
    39  // * supplemental data paths
    40  //
    41  //	`Load` is used to visit these paths.
    42  //
    43  //	These paths refer to resources, patches,
    44  //	data for ConfigMaps and Secrets, etc.
    45  //
    46  //	The loadRestrictor may disallow certain paths
    47  //	or classes of paths.
    48  //
    49  // * bases (other kustomizations)
    50  //
    51  //	`New` is used to load bases.
    52  //
    53  //	A base can be either a remote git repo URL, or
    54  //	a directory specified relative to the current
    55  //	root. In the former case, the repo is locally
    56  //	cloned, and the new loader is rooted on a path
    57  //	in that clone.
    58  //
    59  //	As loaders create new loaders, a root history
    60  //	is established, and used to disallow:
    61  //
    62  //	- A base that is a repository that, in turn,
    63  //	  specifies a base repository seen previously
    64  //	  in the loading stack (a cycle).
    65  //
    66  //	- An overlay depending on a base positioned at
    67  //	  or above it.  I.e. '../foo' is OK, but '.',
    68  //	  '..', '../..', etc. are disallowed.  Allowing
    69  //	  such a base has no advantages and encourages
    70  //	  cycles, particularly if some future change
    71  //	  were to introduce globbing to file
    72  //	  specifications in the kustomization file.
    73  //
    74  // These restrictions assure that kustomizations
    75  // are self-contained and relocatable, and impose
    76  // some safety when relying on remote kustomizations,
    77  // e.g. a remotely loaded ConfigMap generator specified
    78  // to read from /etc/passwd will fail.
    79  type FileLoader struct {
    80  	// Loader that spawned this loader.
    81  	// Used to avoid cycles.
    82  	referrer *FileLoader
    83  
    84  	// An absolute, cleaned path to a directory.
    85  	// The Load function will read non-absolute
    86  	// paths relative to this directory.
    87  	root filesys.ConfirmedDir
    88  
    89  	// Restricts behavior of Load function.
    90  	loadRestrictor LoadRestrictorFunc
    91  
    92  	// If this is non-nil, the files were
    93  	// obtained from the given repository.
    94  	repoSpec *git.RepoSpec
    95  
    96  	// File system utilities.
    97  	fSys filesys.FileSystem
    98  
    99  	// Used to load from HTTP
   100  	http *http.Client
   101  
   102  	// Used to clone repositories.
   103  	cloner git.Cloner
   104  
   105  	// Used to clean up, as needed.
   106  	cleaner func() error
   107  }
   108  
   109  // Repo returns the absolute path to the repo that contains Root if this fileLoader was created from a url
   110  // or the empty string otherwise.
   111  func (fl *FileLoader) Repo() string {
   112  	if fl.repoSpec != nil {
   113  		return fl.repoSpec.Dir.String()
   114  	}
   115  	return ""
   116  }
   117  
   118  // Root returns the absolute path that is prepended to any
   119  // relative paths used in Load.
   120  func (fl *FileLoader) Root() string {
   121  	return fl.root.String()
   122  }
   123  
   124  func NewLoaderOrDie(
   125  	lr LoadRestrictorFunc,
   126  	fSys filesys.FileSystem, path string) *FileLoader {
   127  	root, err := filesys.ConfirmDir(fSys, path)
   128  	if err != nil {
   129  		log.Fatalf("unable to make loader at '%s'; %v", path, err)
   130  	}
   131  	return newLoaderAtConfirmedDir(
   132  		lr, root, fSys, nil, git.ClonerUsingGitExec)
   133  }
   134  
   135  // newLoaderAtConfirmedDir returns a new FileLoader with given root.
   136  func newLoaderAtConfirmedDir(
   137  	lr LoadRestrictorFunc,
   138  	root filesys.ConfirmedDir, fSys filesys.FileSystem,
   139  	referrer *FileLoader, cloner git.Cloner) *FileLoader {
   140  	return &FileLoader{
   141  		loadRestrictor: lr,
   142  		root:           root,
   143  		referrer:       referrer,
   144  		fSys:           fSys,
   145  		cloner:         cloner,
   146  		cleaner:        func() error { return nil },
   147  	}
   148  }
   149  
   150  // New returns a new Loader, rooted relative to current loader,
   151  // or rooted in a temp directory holding a git repo clone.
   152  func (fl *FileLoader) New(path string) (ifc.Loader, error) {
   153  	if path == "" {
   154  		return nil, errors.Errorf("new root cannot be empty")
   155  	}
   156  
   157  	repoSpec, err := git.NewRepoSpecFromURL(path)
   158  	if err == nil {
   159  		// Treat this as git repo clone request.
   160  		if err = fl.errIfRepoCycle(repoSpec); err != nil {
   161  			return nil, err
   162  		}
   163  		return newLoaderAtGitClone(
   164  			repoSpec, fl.fSys, fl, fl.cloner)
   165  	}
   166  
   167  	if filepath.IsAbs(path) {
   168  		return nil, fmt.Errorf("new root '%s' cannot be absolute", path)
   169  	}
   170  	root, err := filesys.ConfirmDir(fl.fSys, fl.root.Join(path))
   171  	if err != nil {
   172  		return nil, errors.WrapPrefixf(err, ErrRtNotDir.Error())
   173  	}
   174  	if err = fl.errIfGitContainmentViolation(root); err != nil {
   175  		return nil, err
   176  	}
   177  	if err = fl.errIfArgEqualOrHigher(root); err != nil {
   178  		return nil, err
   179  	}
   180  	return newLoaderAtConfirmedDir(
   181  		fl.loadRestrictor, root, fl.fSys, fl, fl.cloner), nil
   182  }
   183  
   184  // newLoaderAtGitClone returns a new Loader pinned to a temporary
   185  // directory holding a cloned git repo.
   186  func newLoaderAtGitClone(
   187  	repoSpec *git.RepoSpec, fSys filesys.FileSystem,
   188  	referrer *FileLoader, cloner git.Cloner) (ifc.Loader, error) {
   189  	cleaner := repoSpec.Cleaner(fSys)
   190  	err := cloner(repoSpec)
   191  	if err != nil {
   192  		cleaner()
   193  		return nil, err
   194  	}
   195  	root, f, err := fSys.CleanedAbs(repoSpec.AbsPath())
   196  	if err != nil {
   197  		cleaner()
   198  		return nil, err
   199  	}
   200  	// We don't know that the path requested in repoSpec
   201  	// is a directory until we actually clone it and look
   202  	// inside.  That just happened, hence the error check
   203  	// is here.
   204  	if f != "" {
   205  		cleaner()
   206  		return nil, fmt.Errorf(
   207  			"'%s' refers to file '%s'; expecting directory",
   208  			repoSpec.AbsPath(), f)
   209  	}
   210  	// Path in repo can contain symlinks that exit repo. We can only
   211  	// check for this after cloning repo.
   212  	if !root.HasPrefix(repoSpec.CloneDir()) {
   213  		_ = cleaner()
   214  		return nil, fmt.Errorf("%q refers to directory outside of repo %q", repoSpec.AbsPath(),
   215  			repoSpec.CloneDir())
   216  	}
   217  	return &FileLoader{
   218  		// Clones never allowed to escape root.
   219  		loadRestrictor: RestrictionRootOnly,
   220  		root:           root,
   221  		referrer:       referrer,
   222  		repoSpec:       repoSpec,
   223  		fSys:           fSys,
   224  		cloner:         cloner,
   225  		cleaner:        cleaner,
   226  	}, nil
   227  }
   228  
   229  func (fl *FileLoader) errIfGitContainmentViolation(
   230  	base filesys.ConfirmedDir) error {
   231  	containingRepo := fl.containingRepo()
   232  	if containingRepo == nil {
   233  		return nil
   234  	}
   235  	if !base.HasPrefix(containingRepo.CloneDir()) {
   236  		return fmt.Errorf(
   237  			"security; bases in kustomizations found in "+
   238  				"cloned git repos must be within the repo, "+
   239  				"but base '%s' is outside '%s'",
   240  			base, containingRepo.CloneDir())
   241  	}
   242  	return nil
   243  }
   244  
   245  // Looks back through referrers for a git repo, returning nil
   246  // if none found.
   247  func (fl *FileLoader) containingRepo() *git.RepoSpec {
   248  	if fl.repoSpec != nil {
   249  		return fl.repoSpec
   250  	}
   251  	if fl.referrer == nil {
   252  		return nil
   253  	}
   254  	return fl.referrer.containingRepo()
   255  }
   256  
   257  // errIfArgEqualOrHigher tests whether the argument,
   258  // is equal to or above the root of any ancestor.
   259  func (fl *FileLoader) errIfArgEqualOrHigher(
   260  	candidateRoot filesys.ConfirmedDir) error {
   261  	if fl.root.HasPrefix(candidateRoot) {
   262  		return fmt.Errorf(
   263  			"cycle detected: candidate root '%s' contains visited root '%s'",
   264  			candidateRoot, fl.root)
   265  	}
   266  	if fl.referrer == nil {
   267  		return nil
   268  	}
   269  	return fl.referrer.errIfArgEqualOrHigher(candidateRoot)
   270  }
   271  
   272  // TODO(monopole): Distinguish branches?
   273  // I.e. Allow a distinction between git URI with
   274  // path foo and tag bar and a git URI with the same
   275  // path but a different tag?
   276  func (fl *FileLoader) errIfRepoCycle(newRepoSpec *git.RepoSpec) error {
   277  	// TODO(monopole): Use parsed data instead of Raw().
   278  	if fl.repoSpec != nil &&
   279  		strings.HasPrefix(fl.repoSpec.Raw(), newRepoSpec.Raw()) {
   280  		return fmt.Errorf(
   281  			"cycle detected: URI '%s' referenced by previous URI '%s'",
   282  			newRepoSpec.Raw(), fl.repoSpec.Raw())
   283  	}
   284  	if fl.referrer == nil {
   285  		return nil
   286  	}
   287  	return fl.referrer.errIfRepoCycle(newRepoSpec)
   288  }
   289  
   290  // Load returns the content of file at the given path,
   291  // else an error. Relative paths are taken relative
   292  // to the root.
   293  func (fl *FileLoader) Load(path string) ([]byte, error) {
   294  	if IsRemoteFile(path) {
   295  		return fl.httpClientGetContent(path)
   296  	}
   297  	if !filepath.IsAbs(path) {
   298  		path = fl.root.Join(path)
   299  	}
   300  	path, err := fl.loadRestrictor(fl.fSys, fl.root, path)
   301  	if err != nil {
   302  		return nil, err
   303  	}
   304  	return fl.fSys.ReadFile(path)
   305  }
   306  
   307  func (fl *FileLoader) httpClientGetContent(path string) ([]byte, error) {
   308  	var hc *http.Client
   309  	if fl.http != nil {
   310  		hc = fl.http
   311  	} else {
   312  		hc = &http.Client{}
   313  	}
   314  	resp, err := hc.Get(path)
   315  	if err != nil {
   316  		return nil, errors.Wrap(err)
   317  	}
   318  	defer resp.Body.Close()
   319  	// response unsuccessful
   320  	if resp.StatusCode < 200 || resp.StatusCode > 299 {
   321  		_, err = git.NewRepoSpecFromURL(path)
   322  		if err == nil {
   323  			return nil, errors.Errorf("URL is a git repository")
   324  		}
   325  		return nil, fmt.Errorf("%w: status code %d (%s)", ErrHTTP, resp.StatusCode, http.StatusText(resp.StatusCode))
   326  	}
   327  	content, err := io.ReadAll(resp.Body)
   328  	return content, errors.Wrap(err)
   329  }
   330  
   331  // Cleanup runs the cleaner.
   332  func (fl *FileLoader) Cleanup() error {
   333  	return fl.cleaner()
   334  }
   335  

View as plain text