mirror of
https://github.com/k3s-io/k3s.git
synced 2024-06-07 19:41:36 +00:00
336 lines
8.9 KiB
Go
336 lines
8.9 KiB
Go
// Copyright 2019 The Kubernetes Authors.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package loader
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"sigs.k8s.io/kustomize/api/filesys"
|
|
"sigs.k8s.io/kustomize/api/ifc"
|
|
"sigs.k8s.io/kustomize/api/internal/git"
|
|
)
|
|
|
|
// 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_.
|
|
//
|
|
// 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.
|
|
//
|
|
// The loadRestrictor may disallow certain paths
|
|
// or classes of paths.
|
|
//
|
|
// * bases (other kustomizations)
|
|
//
|
|
// `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.
|
|
//
|
|
type fileLoader struct {
|
|
// Loader that spawned this loader.
|
|
// Used to avoid cycles.
|
|
referrer *fileLoader
|
|
|
|
// 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
|
|
|
|
// If this is non-nil, the files were
|
|
// obtained from the given repository.
|
|
repoSpec *git.RepoSpec
|
|
|
|
// File system utilities.
|
|
fSys filesys.FileSystem
|
|
|
|
// Used to load from HTTP
|
|
http *http.Client
|
|
|
|
// Used to clone repositories.
|
|
cloner git.Cloner
|
|
|
|
// 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)
|
|
}
|
|
|
|
// NewFileLoaderAtRoot returns a loader that loads from "/".
|
|
// A convenience for tests.
|
|
func NewFileLoaderAtRoot(fSys filesys.FileSystem) *fileLoader {
|
|
return newLoaderOrDie(
|
|
RestrictionRootOnly, fSys, filesys.Separator)
|
|
}
|
|
|
|
// Root returns the absolute path that is prepended to any
|
|
// relative paths used in Load.
|
|
func (fl *fileLoader) Root() string {
|
|
return fl.root.String()
|
|
}
|
|
|
|
func newLoaderOrDie(
|
|
lr LoadRestrictorFunc,
|
|
fSys filesys.FileSystem, path string) *fileLoader {
|
|
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)
|
|
}
|
|
|
|
// newLoaderAtConfirmedDir returns a new fileLoader with given root.
|
|
func newLoaderAtConfirmedDir(
|
|
lr LoadRestrictorFunc,
|
|
root filesys.ConfirmedDir, fSys filesys.FileSystem,
|
|
referrer *fileLoader, cloner git.Cloner) *fileLoader {
|
|
return &fileLoader{
|
|
loadRestrictor: lr,
|
|
root: root,
|
|
referrer: referrer,
|
|
fSys: fSys,
|
|
cloner: cloner,
|
|
cleaner: func() error { return nil },
|
|
}
|
|
}
|
|
|
|
// Assure that the given path is in fact a directory.
|
|
func demandDirectoryRoot(
|
|
fSys filesys.FileSystem, path string) (filesys.ConfirmedDir, error) {
|
|
if path == "" {
|
|
return "", fmt.Errorf(
|
|
"loader root cannot be empty")
|
|
}
|
|
d, f, err := fSys.CleanedAbs(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
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) {
|
|
if path == "" {
|
|
return nil, fmt.Errorf("new root cannot be empty")
|
|
}
|
|
|
|
repoSpec, err := git.NewRepoSpecFromUrl(path)
|
|
if err == nil {
|
|
// Treat this as git repo clone request.
|
|
if err = fl.errIfRepoCycle(repoSpec); err != nil {
|
|
return nil, err
|
|
}
|
|
return newLoaderAtGitClone(
|
|
repoSpec, fl.fSys, fl, fl.cloner)
|
|
}
|
|
|
|
if filepath.IsAbs(path) {
|
|
return nil, fmt.Errorf("new root '%s' cannot be absolute", path)
|
|
}
|
|
root, err := demandDirectoryRoot(fl.fSys, fl.root.Join(path))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = fl.errIfGitContainmentViolation(root); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = fl.errIfArgEqualOrHigher(root); err != nil {
|
|
return nil, err
|
|
}
|
|
return newLoaderAtConfirmedDir(
|
|
fl.loadRestrictor, root, fl.fSys, fl, fl.cloner), nil
|
|
}
|
|
|
|
// newLoaderAtGitClone returns a new Loader pinned to a temporary
|
|
// directory holding a cloned git repo.
|
|
func newLoaderAtGitClone(
|
|
repoSpec *git.RepoSpec, fSys filesys.FileSystem,
|
|
referrer *fileLoader, cloner git.Cloner) (ifc.Loader, error) {
|
|
cleaner := repoSpec.Cleaner(fSys)
|
|
err := cloner(repoSpec)
|
|
if err != nil {
|
|
cleaner()
|
|
return nil, err
|
|
}
|
|
root, f, err := fSys.CleanedAbs(repoSpec.AbsPath())
|
|
if err != nil {
|
|
cleaner()
|
|
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()
|
|
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,
|
|
}, nil
|
|
}
|
|
|
|
func (fl *fileLoader) errIfGitContainmentViolation(
|
|
base filesys.ConfirmedDir) error {
|
|
containingRepo := fl.containingRepo()
|
|
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
|
|
}
|
|
if fl.referrer == nil {
|
|
return nil
|
|
}
|
|
return fl.referrer.containingRepo()
|
|
}
|
|
|
|
// 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) {
|
|
return fmt.Errorf(
|
|
"cycle detected: candidate root '%s' contains visited root '%s'",
|
|
candidateRoot, fl.root)
|
|
}
|
|
if fl.referrer == nil {
|
|
return nil
|
|
}
|
|
return fl.referrer.errIfArgEqualOrHigher(candidateRoot)
|
|
}
|
|
|
|
// 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 {
|
|
// TODO(monopole): Use parsed data instead of Raw().
|
|
if fl.repoSpec != nil &&
|
|
strings.HasPrefix(fl.repoSpec.Raw(), newRepoSpec.Raw()) {
|
|
return fmt.Errorf(
|
|
"cycle detected: URI '%s' referenced by previous URI '%s'",
|
|
newRepoSpec.Raw(), fl.repoSpec.Raw())
|
|
}
|
|
if fl.referrer == nil {
|
|
return nil
|
|
}
|
|
return fl.referrer.errIfRepoCycle(newRepoSpec)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
if !filepath.IsAbs(path) {
|
|
path = fl.root.Join(path)
|
|
}
|
|
path, err := fl.loadRestrictor(fl.fSys, fl.root, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return fl.fSys.ReadFile(path)
|
|
}
|
|
|
|
// Cleanup runs the cleaner.
|
|
func (fl *fileLoader) Cleanup() error {
|
|
return fl.cleaner()
|
|
}
|