Files
kustomize/pkg/loader/fileloader.go
2019-01-14 15:35:03 -08:00

221 lines
6.6 KiB
Go

/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package loader
import (
"fmt"
"log"
"path/filepath"
"strings"
"sigs.k8s.io/kustomize/pkg/fs"
"sigs.k8s.io/kustomize/pkg/ifc"
)
// fileLoader loads files, returning an array of bytes.
// It also enforces two kustomization requirements:
//
// 1) relocatable
//
// A kustomization and the resources, bases,
// patches, etc. that it depends on should be
// relocatable, so all path specifications
// must be relative, not absolute. The paths
// are taken relative to the location of the
// kusttomization file.
//
// 2) acyclic
//
// There should be no cycles in overlay to base
// relationships, including no cycles between
// git repositories.
//
// The loader has a notion of a current working directory
// (CWD), called 'root', that is independent of the CWD
// of the process. When `Load` is called with a file path
// argument, the load is done relative to this root,
// not relative to the process CWD.
//
// The loader offers a `New` method returning a new loader
// with a new root. The new root can be one of two things,
// a remote git repo URL, or a directory specified relative
// to the current root. In the former case, the remote
// repository is locally cloned, and the new loader is
// rooted on a path in that clone.
//
// Crucially, a root history is used to so that New fails
// if its argument either matches or is a parent of the
// current or any previously used root.
//
// This disallows:
//
// * A base that is a git repository that, in turn,
// specifies a base repository seen previously
// in the loading process (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 encourage cycles,
// particularly if some future change were to
// introduce globbing to file specifications in
// the kustomization file.
//
type fileLoader struct {
// Previously visited absolute directory paths.
// Tracked to avoid cycles.
// The last entry is the current root.
roots []string
// File system utilities.
fSys fs.FileSystem
// Used to clone repositories.
cloner gitCloner
// Used to clean up, as needed.
cleaner func() error
}
// NewFileLoaderAtCwd returns a loader that loads from ".".
func NewFileLoaderAtCwd(fSys fs.FileSystem) *fileLoader {
return newLoaderOrDie(fSys, ".")
}
// NewFileLoaderAtRoot returns a loader that loads from "/".
func NewFileLoaderAtRoot(fSys fs.FileSystem) *fileLoader {
return newLoaderOrDie(fSys, string(filepath.Separator))
}
// Root returns the absolute path that is prepended to any
// relative paths used in Load.
func (l *fileLoader) Root() string {
return l.roots[len(l.roots)-1]
}
func newLoaderOrDie(fSys fs.FileSystem, path string) *fileLoader {
l, err := newFileLoaderAt(
path, fSys, []string{}, simpleGitCloner)
if err != nil {
log.Fatalf("unable to make loader at '%s'; %v", path, err)
}
return l
}
// newFileLoaderAt returns a new fileLoader with given root.
func newFileLoaderAt(
root string, fSys fs.FileSystem,
roots []string, cloner gitCloner) (*fileLoader, error) {
if root == "" {
return nil, fmt.Errorf(
"loader root cannot be empty")
}
absRoot, err := filepath.Abs(root)
if err != nil {
return nil, fmt.Errorf(
"absolute path error in '%s' : %v", root, err)
}
if err := isPathEqualToOrAbove(absRoot, roots); err != nil {
return nil, err
}
if !fSys.IsDir(absRoot) {
return nil, fmt.Errorf(
"absolute root dir '%s' does not exist", absRoot)
}
return &fileLoader{
roots: append(roots, absRoot),
fSys: fSys,
cloner: cloner,
cleaner: func() error { return nil },
}, nil
}
// New returns a new Loader, rooted relative to current loader,
// or rooted in a temp directory holding a git repo clone.
func (l *fileLoader) New(path string) (ifc.Loader, error) {
if path == "" {
return nil, fmt.Errorf("new root cannot be empty")
}
if isRepoUrl(path) {
// This works well enough for purpose at hand to detect
// previously visited URLs and thus avoid cycles.
if err := isPathEqualToOrAbove(path, l.roots); err != nil {
return nil, err
}
return newGitLoader(path, l.fSys, l.roots, l.cloner)
}
if filepath.IsAbs(path) {
return nil, fmt.Errorf("new root '%s' cannot be absolute", path)
}
absRoot, err := filepath.Abs(filepath.Join(l.Root(), path))
if err != nil {
return nil, fmt.Errorf(
"problem joining '%s' to '%s': %v", l.Root(), path, err)
}
return newFileLoaderAt(absRoot, l.fSys, l.roots, l.cloner)
}
// newGitLoader returns a new Loader pinned to a temporary
// directory holding a cloned git repo.
func newGitLoader(
root string, fSys fs.FileSystem,
roots []string, cloner gitCloner) (ifc.Loader, error) {
tmpDirForRepo, pathInRepo, err := cloner(root)
if err != nil {
return nil, err
}
trueRoot := filepath.Join(tmpDirForRepo, pathInRepo)
if !fSys.IsDir(trueRoot) {
return nil, fmt.Errorf(
"something wrong cloning '%s'; unable to find '%s'",
root, trueRoot)
}
return &fileLoader{
roots: append(roots, root, trueRoot),
fSys: fSys,
cloner: cloner,
cleaner: func() error { return fSys.RemoveAll(tmpDirForRepo) },
}, nil
}
// isPathEqualToOrAbove tests whether the 1st argument,
// viewed as a path to a directory, is equal to or above
// any of the paths in the 2nd argument. It's assumed
// that all paths are cleaned, delinkified and absolute.
func isPathEqualToOrAbove(path string, roots []string) error {
terminated := path + string(filepath.Separator)
for _, r := range roots {
if r == path || strings.HasPrefix(r, terminated) {
return fmt.Errorf(
"cycle detected: new root '%s' contains previous root '%s'",
path, r)
}
}
return nil
}
// Load returns content of file at the given relative path.
func (l *fileLoader) Load(path string) ([]byte, error) {
if filepath.IsAbs(path) {
return nil, fmt.Errorf(
"must use relative path; '%s' is absolute", path)
}
return l.fSys.ReadFile(filepath.Join(l.Root(), path))
}
// Cleanup runs the cleaner.
func (l *fileLoader) Cleanup() error {
return l.cleaner()
}