k3s/vendor/sigs.k8s.io/kustomize/api/loader/fileloader.go

337 lines
8.9 KiB
Go
Raw Normal View History

// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
2019-08-30 18:33:25 +00:00
package loader
import (
"fmt"
"io/ioutil"
2019-08-30 18:33:25 +00:00
"log"
"net/http"
"net/url"
2019-08-30 18:33:25 +00:00
"path/filepath"
"strings"
"sigs.k8s.io/kustomize/api/filesys"
"sigs.k8s.io/kustomize/api/ifc"
"sigs.k8s.io/kustomize/api/internal/git"
2019-08-30 18:33:25 +00:00
)
// fileLoader is a kustomization's interface to files.
//
// The directory in which a kustomization file sits
// is referred to below as the kustomization's _root_.
2019-08-30 18:33:25 +00:00
//
// An instance of fileLoader has an immutable root,
// and offers a `New` method returning a new loader
// with a new root.
//
// A kustomization file refers to two kinds of files:
//
// * supplemental data paths
//
// `Load` is used to visit these paths.
//
// These paths refer to resources, patches,
// data for ConfigMaps and Secrets, etc.
2019-08-30 18:33:25 +00:00
//
// The loadRestrictor may disallow certain paths
// or classes of paths.
2019-08-30 18:33:25 +00:00
//
// * bases (other kustomizations)
2019-08-30 18:33:25 +00:00
//
// `New` is used to load bases.
//
// A base can be either a remote git repo URL, or
// a directory specified relative to the current
// root. In the former case, the repo is locally
// cloned, and the new loader is rooted on a path
// in that clone.
//
// As loaders create new loaders, a root history
// is established, and used to disallow:
//
// - A base that is a repository that, in turn,
// specifies a base repository seen previously
// in the loading stack (a cycle).
//
// - An overlay depending on a base positioned at
// or above it. I.e. '../foo' is OK, but '.',
// '..', '../..', etc. are disallowed. Allowing
// such a base has no advantages and encourages
// cycles, particularly if some future change
// were to introduce globbing to file
// specifications in the kustomization file.
//
// These restrictions assure that kustomizations
// are self-contained and relocatable, and impose
// some safety when relying on remote kustomizations,
// e.g. a remotely loaded ConfigMap generator specified
// to read from /etc/passwd will fail.
2019-08-30 18:33:25 +00:00
//
type fileLoader struct {
// Loader that spawned this loader.
// Used to avoid cycles.
referrer *fileLoader
2019-08-30 18:33:25 +00:00
// An absolute, cleaned path to a directory.
// The Load function will read non-absolute
// paths relative to this directory.
root filesys.ConfirmedDir
// Restricts behavior of Load function.
loadRestrictor LoadRestrictorFunc
2019-08-30 18:33:25 +00:00
// If this is non-nil, the files were
// obtained from the given repository.
repoSpec *git.RepoSpec
2019-08-30 18:33:25 +00:00
// File system utilities.
fSys filesys.FileSystem
// Used to load from HTTP
http *http.Client
2019-08-30 18:33:25 +00:00
// Used to clone repositories.
cloner git.Cloner
2019-08-30 18:33:25 +00:00
// Used to clean up, as needed.
cleaner func() error
}
// NewFileLoaderAtCwd returns a loader that loads from PWD.
// A convenience for kustomize edit commands.
func NewFileLoaderAtCwd(fSys filesys.FileSystem) *fileLoader {
return newLoaderOrDie(
RestrictionRootOnly, fSys, filesys.SelfDir)
2019-08-30 18:33:25 +00:00
}
// NewFileLoaderAtRoot returns a loader that loads from "/".
// A convenience for tests.
func NewFileLoaderAtRoot(fSys filesys.FileSystem) *fileLoader {
return newLoaderOrDie(
RestrictionRootOnly, fSys, filesys.Separator)
2019-08-30 18:33:25 +00:00
}
// Root returns the absolute path that is prepended to any
// relative paths used in Load.
func (fl *fileLoader) Root() string {
return fl.root.String()
2019-08-30 18:33:25 +00:00
}
func newLoaderOrDie(
lr LoadRestrictorFunc,
fSys filesys.FileSystem, path string) *fileLoader {
2019-08-30 18:33:25 +00:00
root, err := demandDirectoryRoot(fSys, path)
if err != nil {
log.Fatalf("unable to make loader at '%s'; %v", path, err)
}
return newLoaderAtConfirmedDir(
lr, root, fSys, nil, git.ClonerUsingGitExec)
2019-08-30 18:33:25 +00:00
}
// newLoaderAtConfirmedDir returns a new fileLoader with given root.
func newLoaderAtConfirmedDir(
lr LoadRestrictorFunc,
root filesys.ConfirmedDir, fSys filesys.FileSystem,
2019-08-30 18:33:25 +00:00
referrer *fileLoader, cloner git.Cloner) *fileLoader {
return &fileLoader{
loadRestrictor: lr,
root: root,
referrer: referrer,
fSys: fSys,
cloner: cloner,
cleaner: func() error { return nil },
2019-08-30 18:33:25 +00:00
}
}
// Assure that the given path is in fact a directory.
func demandDirectoryRoot(
fSys filesys.FileSystem, path string) (filesys.ConfirmedDir, error) {
2019-08-30 18:33:25 +00:00
if path == "" {
return "", fmt.Errorf(
"loader root cannot be empty")
}
d, f, err := fSys.CleanedAbs(path)
if err != nil {
return "", err
2019-08-30 18:33:25 +00:00
}
if f != "" {
return "", fmt.Errorf(
"got file '%s', but '%s' must be a directory to be a root",
f, path)
}
return d, nil
}
// New returns a new Loader, rooted relative to current loader,
// or rooted in a temp directory holding a git repo clone.
func (fl *fileLoader) New(path string) (ifc.Loader, error) {
2019-08-30 18:33:25 +00:00
if path == "" {
return nil, fmt.Errorf("new root cannot be empty")
}
2019-08-30 18:33:25 +00:00
repoSpec, err := git.NewRepoSpecFromUrl(path)
if err == nil {
// Treat this as git repo clone request.
if err = fl.errIfRepoCycle(repoSpec); err != nil {
2019-08-30 18:33:25 +00:00
return nil, err
}
return newLoaderAtGitClone(
repoSpec, fl.fSys, fl, fl.cloner)
2019-08-30 18:33:25 +00:00
}
2019-08-30 18:33:25 +00:00
if filepath.IsAbs(path) {
return nil, fmt.Errorf("new root '%s' cannot be absolute", path)
}
root, err := demandDirectoryRoot(fl.fSys, fl.root.Join(path))
2019-08-30 18:33:25 +00:00
if err != nil {
return nil, err
}
if err = fl.errIfGitContainmentViolation(root); err != nil {
2019-08-30 18:33:25 +00:00
return nil, err
}
if err = fl.errIfArgEqualOrHigher(root); err != nil {
2019-08-30 18:33:25 +00:00
return nil, err
}
return newLoaderAtConfirmedDir(
fl.loadRestrictor, root, fl.fSys, fl, fl.cloner), nil
2019-08-30 18:33:25 +00:00
}
// newLoaderAtGitClone returns a new Loader pinned to a temporary
// directory holding a cloned git repo.
func newLoaderAtGitClone(
repoSpec *git.RepoSpec, fSys filesys.FileSystem,
2019-08-30 18:33:25 +00:00
referrer *fileLoader, cloner git.Cloner) (ifc.Loader, error) {
cleaner := repoSpec.Cleaner(fSys)
2019-08-30 18:33:25 +00:00
err := cloner(repoSpec)
if err != nil {
cleaner()
2019-08-30 18:33:25 +00:00
return nil, err
}
root, f, err := fSys.CleanedAbs(repoSpec.AbsPath())
if err != nil {
cleaner()
2019-08-30 18:33:25 +00:00
return nil, err
}
// We don't know that the path requested in repoSpec
// is a directory until we actually clone it and look
// inside. That just happened, hence the error check
// is here.
if f != "" {
cleaner()
2019-08-30 18:33:25 +00:00
return nil, fmt.Errorf(
"'%s' refers to file '%s'; expecting directory",
repoSpec.AbsPath(), f)
}
return &fileLoader{
// Clones never allowed to escape root.
loadRestrictor: RestrictionRootOnly,
root: root,
referrer: referrer,
repoSpec: repoSpec,
fSys: fSys,
cloner: cloner,
cleaner: cleaner,
2019-08-30 18:33:25 +00:00
}, nil
}
func (fl *fileLoader) errIfGitContainmentViolation(
base filesys.ConfirmedDir) error {
containingRepo := fl.containingRepo()
2019-08-30 18:33:25 +00:00
if containingRepo == nil {
return nil
}
if !base.HasPrefix(containingRepo.CloneDir()) {
return fmt.Errorf(
"security; bases in kustomizations found in "+
"cloned git repos must be within the repo, "+
"but base '%s' is outside '%s'",
base, containingRepo.CloneDir())
}
return nil
}
// Looks back through referrers for a git repo, returning nil
// if none found.
func (fl *fileLoader) containingRepo() *git.RepoSpec {
if fl.repoSpec != nil {
return fl.repoSpec
2019-08-30 18:33:25 +00:00
}
if fl.referrer == nil {
2019-08-30 18:33:25 +00:00
return nil
}
return fl.referrer.containingRepo()
2019-08-30 18:33:25 +00:00
}
// errIfArgEqualOrHigher tests whether the argument,
// is equal to or above the root of any ancestor.
func (fl *fileLoader) errIfArgEqualOrHigher(
candidateRoot filesys.ConfirmedDir) error {
if fl.root.HasPrefix(candidateRoot) {
2019-08-30 18:33:25 +00:00
return fmt.Errorf(
"cycle detected: candidate root '%s' contains visited root '%s'",
candidateRoot, fl.root)
2019-08-30 18:33:25 +00:00
}
if fl.referrer == nil {
2019-08-30 18:33:25 +00:00
return nil
}
return fl.referrer.errIfArgEqualOrHigher(candidateRoot)
2019-08-30 18:33:25 +00:00
}
// TODO(monopole): Distinguish branches?
// I.e. Allow a distinction between git URI with
// path foo and tag bar and a git URI with the same
// path but a different tag?
func (fl *fileLoader) errIfRepoCycle(newRepoSpec *git.RepoSpec) error {
2019-08-30 18:33:25 +00:00
// TODO(monopole): Use parsed data instead of Raw().
if fl.repoSpec != nil &&
strings.HasPrefix(fl.repoSpec.Raw(), newRepoSpec.Raw()) {
2019-08-30 18:33:25 +00:00
return fmt.Errorf(
"cycle detected: URI '%s' referenced by previous URI '%s'",
newRepoSpec.Raw(), fl.repoSpec.Raw())
2019-08-30 18:33:25 +00:00
}
if fl.referrer == nil {
2019-08-30 18:33:25 +00:00
return nil
}
return fl.referrer.errIfRepoCycle(newRepoSpec)
2019-08-30 18:33:25 +00:00
}
// Load returns the content of file at the given path,
// else an error. Relative paths are taken relative
// to the root.
func (fl *fileLoader) Load(path string) ([]byte, error) {
if u, err := url.Parse(path); err == nil && (u.Scheme == "http" || u.Scheme == "https") {
var hc *http.Client
if fl.http != nil {
hc = fl.http
} else {
hc = &http.Client{}
}
resp, err := hc.Get(path)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
2019-08-30 18:33:25 +00:00
}
if !filepath.IsAbs(path) {
path = fl.root.Join(path)
}
path, err := fl.loadRestrictor(fl.fSys, fl.root, path)
2019-08-30 18:33:25 +00:00
if err != nil {
return nil, err
}
return fl.fSys.ReadFile(path)
2019-08-30 18:33:25 +00:00
}
// Cleanup runs the cleaner.
func (fl *fileLoader) Cleanup() error {
return fl.cleaner()
2019-08-30 18:33:25 +00:00
}