mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-30 09:51:23 +00:00
Make resource, resmap public.
This commit is contained in:
312
api/loader/fileloader.go
Normal file
312
api/loader/fileloader.go
Normal file
@@ -0,0 +1,312 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package loader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/filesys"
|
||||
"sigs.k8s.io/kustomize/v3/api/ifc"
|
||||
"sigs.k8s.io/kustomize/v3/pkg/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 clone repositories.
|
||||
cloner git.Cloner
|
||||
|
||||
// Used to clean up, as needed.
|
||||
cleaner func() error
|
||||
}
|
||||
|
||||
const CWD = "."
|
||||
|
||||
// NewFileLoaderAtCwd returns a loader that loads from ".".
|
||||
// A convenience for kustomize edit commands.
|
||||
func NewFileLoaderAtCwd(fSys filesys.FileSystem) *fileLoader {
|
||||
return newLoaderOrDie(
|
||||
RestrictionRootOnly, fSys, CWD)
|
||||
}
|
||||
|
||||
// NewFileLoaderAtRoot returns a loader that loads from "/".
|
||||
// A convenience for tests.
|
||||
func NewFileLoaderAtRoot(fSys filesys.FileSystem) *fileLoader {
|
||||
return newLoaderOrDie(
|
||||
RestrictionRootOnly, fSys, string(filepath.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 "", fmt.Errorf(
|
||||
"absolute path error in '%s' : %v", path, 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 !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()
|
||||
}
|
||||
594
api/loader/fileloader_test.go
Normal file
594
api/loader/fileloader_test.go
Normal file
@@ -0,0 +1,594 @@
|
||||
/*
|
||||
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 (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/filesys"
|
||||
"sigs.k8s.io/kustomize/v3/api/ifc"
|
||||
"sigs.k8s.io/kustomize/v3/pkg/git"
|
||||
"sigs.k8s.io/kustomize/v3/pkg/pgmconfig"
|
||||
)
|
||||
|
||||
type testData struct {
|
||||
path string
|
||||
expectedContent string
|
||||
}
|
||||
|
||||
var testCases = []testData{
|
||||
{
|
||||
path: "foo/project/fileA.yaml",
|
||||
expectedContent: "fileA content",
|
||||
},
|
||||
{
|
||||
path: "foo/project/subdir1/fileB.yaml",
|
||||
expectedContent: "fileB content",
|
||||
},
|
||||
{
|
||||
path: "foo/project/subdir2/fileC.yaml",
|
||||
expectedContent: "fileC content",
|
||||
},
|
||||
{
|
||||
path: "foo/project/fileD.yaml",
|
||||
expectedContent: "fileD content",
|
||||
},
|
||||
}
|
||||
|
||||
func MakeFakeFs(td []testData) filesys.FileSystem {
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
for _, x := range td {
|
||||
fSys.WriteFile("/"+x.path, []byte(x.expectedContent))
|
||||
}
|
||||
return fSys
|
||||
}
|
||||
|
||||
func makeLoader() *fileLoader {
|
||||
return NewFileLoaderAtRoot(MakeFakeFs(testCases))
|
||||
|
||||
}
|
||||
func TestLoaderLoad(t *testing.T) {
|
||||
l1 := makeLoader()
|
||||
if "/" != l1.Root() {
|
||||
t.Fatalf("incorrect root: '%s'\n", l1.Root())
|
||||
}
|
||||
for _, x := range testCases {
|
||||
b, err := l1.Load(x.path)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected load error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual([]byte(x.expectedContent), b) {
|
||||
t.Fatalf("in load expected %s, but got %s", x.expectedContent, b)
|
||||
}
|
||||
}
|
||||
l2, err := l1.New("foo/project")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if "/foo/project" != l2.Root() {
|
||||
t.Fatalf("incorrect root: %s\n", l2.Root())
|
||||
}
|
||||
for _, x := range testCases {
|
||||
b, err := l2.Load(strings.TrimPrefix(x.path, "foo/project/"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected load error %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual([]byte(x.expectedContent), b) {
|
||||
t.Fatalf("in load expected %s, but got %s", x.expectedContent, b)
|
||||
}
|
||||
}
|
||||
l2, err = l1.New("foo/project/") // Assure trailing slash stripped
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if "/foo/project" != l2.Root() {
|
||||
t.Fatalf("incorrect root: %s\n", l2.Root())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoaderNewSubDir(t *testing.T) {
|
||||
l1, err := makeLoader().New("foo/project")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
l2, err := l1.New("subdir1")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if "/foo/project/subdir1" != l2.Root() {
|
||||
t.Fatalf("incorrect root: %s\n", l2.Root())
|
||||
}
|
||||
x := testCases[1]
|
||||
b, err := l2.Load("fileB.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected load error %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual([]byte(x.expectedContent), b) {
|
||||
t.Fatalf("in load expected %s, but got %s", x.expectedContent, b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoaderBadRelative(t *testing.T) {
|
||||
l1, err := makeLoader().New("foo/project/subdir1")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if "/foo/project/subdir1" != l1.Root() {
|
||||
t.Fatalf("incorrect root: %s\n", l1.Root())
|
||||
}
|
||||
|
||||
// Cannot cd into a file.
|
||||
l2, err := l1.New("fileB.yaml")
|
||||
if err == nil {
|
||||
t.Fatalf("expected err, but got root %s", l2.Root())
|
||||
}
|
||||
|
||||
// It's not okay to stay at the same place.
|
||||
l2, err = l1.New(".")
|
||||
if err == nil {
|
||||
t.Fatalf("expected err, but got root %s", l2.Root())
|
||||
}
|
||||
|
||||
// It's not okay to go up and back down into same place.
|
||||
l2, err = l1.New("../subdir1")
|
||||
if err == nil {
|
||||
t.Fatalf("expected err, but got root %s", l2.Root())
|
||||
}
|
||||
|
||||
// It's not okay to go up via a relative path.
|
||||
l2, err = l1.New("..")
|
||||
if err == nil {
|
||||
t.Fatalf("expected err, but got root %s", l2.Root())
|
||||
}
|
||||
|
||||
// It's not okay to go up via an absolute path.
|
||||
l2, err = l1.New("/foo/project")
|
||||
if err == nil {
|
||||
t.Fatalf("expected err, but got root %s", l2.Root())
|
||||
}
|
||||
|
||||
// It's not okay to go to the root.
|
||||
l2, err = l1.New("/")
|
||||
if err == nil {
|
||||
t.Fatalf("expected err, but got root %s", l2.Root())
|
||||
}
|
||||
|
||||
// It's okay to go up and down to a sibling.
|
||||
l2, err = l1.New("../subdir2")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected new error %v", err)
|
||||
}
|
||||
if "/foo/project/subdir2" != l2.Root() {
|
||||
t.Fatalf("incorrect root: %s\n", l2.Root())
|
||||
}
|
||||
x := testCases[2]
|
||||
b, err := l2.Load("fileC.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected load error %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual([]byte(x.expectedContent), b) {
|
||||
t.Fatalf("in load expected %s, but got %s", x.expectedContent, b)
|
||||
}
|
||||
|
||||
// It's not OK to go over to a previously visited directory.
|
||||
// Must disallow going back and forth in a cycle.
|
||||
l1, err = l2.New("../subdir1")
|
||||
if err == nil {
|
||||
t.Fatalf("expected err, but got root %s", l1.Root())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoaderMisc(t *testing.T) {
|
||||
l := makeLoader()
|
||||
_, err := l.New("")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error for empty root location not returned")
|
||||
}
|
||||
_, err = l.New("https://google.com/project")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error")
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
contentOk = "hi there, i'm OK data"
|
||||
contentExteriorData = "i am data from outside the root"
|
||||
)
|
||||
|
||||
// Create a structure like this
|
||||
//
|
||||
// /tmp/kustomize-test-random
|
||||
// ├── base
|
||||
// │ ├── okayData
|
||||
// │ ├── symLinkToOkayData -> okayData
|
||||
// │ └── symLinkToExteriorData -> ../exteriorData
|
||||
// └── exteriorData
|
||||
//
|
||||
func commonSetupForLoaderRestrictionTest() (string, filesys.FileSystem, error) {
|
||||
dir, err := ioutil.TempDir("", "kustomize-test-")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
fSys := filesys.MakeFsOnDisk()
|
||||
fSys.Mkdir(filepath.Join(dir, "base"))
|
||||
|
||||
fSys.WriteFile(
|
||||
filepath.Join(dir, "base", "okayData"), []byte(contentOk))
|
||||
|
||||
fSys.WriteFile(
|
||||
filepath.Join(dir, "exteriorData"), []byte(contentExteriorData))
|
||||
|
||||
os.Symlink(
|
||||
filepath.Join(dir, "base", "okayData"),
|
||||
filepath.Join(dir, "base", "symLinkToOkayData"))
|
||||
os.Symlink(
|
||||
filepath.Join(dir, "exteriorData"),
|
||||
filepath.Join(dir, "base", "symLinkToExteriorData"))
|
||||
return dir, fSys, nil
|
||||
}
|
||||
|
||||
// Make sure everything works when loading files
|
||||
// in or below the loader root.
|
||||
func doSanityChecksAndDropIntoBase(
|
||||
t *testing.T, l ifc.Loader) ifc.Loader {
|
||||
data, err := l.Load(path.Join("base", "okayData"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if string(data) != contentOk {
|
||||
t.Fatalf("unexpected content: %v", data)
|
||||
}
|
||||
data, err = l.Load("exteriorData")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if string(data) != contentExteriorData {
|
||||
t.Fatalf("unexpected content: %v", data)
|
||||
}
|
||||
|
||||
// Drop in.
|
||||
l, err = l.New("base")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Reading okayData works.
|
||||
data, err = l.Load("okayData")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if string(data) != contentOk {
|
||||
t.Fatalf("unexpected content: %v", data)
|
||||
}
|
||||
|
||||
// Reading local symlink to okayData works.
|
||||
data, err = l.Load("symLinkToOkayData")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if string(data) != contentOk {
|
||||
t.Fatalf("unexpected content: %v", data)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func TestRestrictionRootOnlyInRealLoader(t *testing.T) {
|
||||
dir, fSys, err := commonSetupForLoaderRestrictionTest()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
var l ifc.Loader
|
||||
|
||||
l = newLoaderOrDie(RestrictionRootOnly, fSys, dir)
|
||||
|
||||
l = doSanityChecksAndDropIntoBase(t, l)
|
||||
|
||||
// Reading symlink to exteriorData fails.
|
||||
_, err = l.Load("symLinkToExteriorData")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "is not in or below") {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
|
||||
// Attempt to read "up" fails, though earlier we were
|
||||
// able to read this file when root was "..".
|
||||
_, err = l.Load("../exteriorData")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "is not in or below") {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestrictionNoneInRealLoader(t *testing.T) {
|
||||
dir, fSys, err := commonSetupForLoaderRestrictionTest()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
var l ifc.Loader
|
||||
|
||||
l = newLoaderOrDie(RestrictionNone, fSys, dir)
|
||||
|
||||
l = doSanityChecksAndDropIntoBase(t, l)
|
||||
|
||||
// Reading symlink to exteriorData works.
|
||||
_, err = l.Load("symLinkToExteriorData")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Attempt to read "up" works.
|
||||
_, err = l.Load("../exteriorData")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func splitOnNthSlash(v string, n int) (string, string) {
|
||||
left := ""
|
||||
for i := 0; i < n; i++ {
|
||||
k := strings.Index(v, "/")
|
||||
if k < 0 {
|
||||
break
|
||||
}
|
||||
left = left + v[:k+1]
|
||||
v = v[k+1:]
|
||||
}
|
||||
return left[:len(left)-1], v
|
||||
}
|
||||
|
||||
func TestSplit(t *testing.T) {
|
||||
p := "a/b/c/d/e/f/g"
|
||||
if left, right := splitOnNthSlash(p, 2); left != "a/b" || right != "c/d/e/f/g" {
|
||||
t.Fatalf("got left='%s', right='%s'", left, right)
|
||||
}
|
||||
if left, right := splitOnNthSlash(p, 3); left != "a/b/c" || right != "d/e/f/g" {
|
||||
t.Fatalf("got left='%s', right='%s'", left, right)
|
||||
}
|
||||
if left, right := splitOnNthSlash(p, 6); left != "a/b/c/d/e/f" || right != "g" {
|
||||
t.Fatalf("got left='%s', right='%s'", left, right)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLoaderAtGitClone(t *testing.T) {
|
||||
rootUrl := "github.com/someOrg/someRepo"
|
||||
pathInRepo := "foo/base"
|
||||
url := rootUrl + "/" + pathInRepo
|
||||
coRoot := "/tmp"
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys.MkdirAll(coRoot)
|
||||
fSys.MkdirAll(coRoot + "/" + pathInRepo)
|
||||
fSys.WriteFile(
|
||||
coRoot+"/"+pathInRepo+"/"+
|
||||
pgmconfig.DefaultKustomizationFileName(),
|
||||
[]byte(`
|
||||
whatever
|
||||
`))
|
||||
|
||||
repoSpec, err := git.NewRepoSpecFromUrl(url)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
l, err := newLoaderAtGitClone(
|
||||
repoSpec, fSys, nil,
|
||||
git.DoNothingCloner(filesys.ConfirmedDir(coRoot)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if coRoot+"/"+pathInRepo != l.Root() {
|
||||
t.Fatalf("expected root '%s', got '%s'\n",
|
||||
coRoot+"/"+pathInRepo, l.Root())
|
||||
}
|
||||
if _, err = l.New(url); err == nil {
|
||||
t.Fatalf("expected cycle error 1")
|
||||
}
|
||||
if _, err = l.New(rootUrl + "/" + "foo"); err == nil {
|
||||
t.Fatalf("expected cycle error 2")
|
||||
}
|
||||
|
||||
pathInRepo = "foo/overlay"
|
||||
fSys.MkdirAll(coRoot + "/" + pathInRepo)
|
||||
url = rootUrl + "/" + pathInRepo
|
||||
l2, err := l.New(url)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if coRoot+"/"+pathInRepo != l2.Root() {
|
||||
t.Fatalf("expected root '%s', got '%s'\n",
|
||||
coRoot+"/"+pathInRepo, l2.Root())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoaderDisallowsLocalBaseFromRemoteOverlay(t *testing.T) {
|
||||
// Define an overlay-base structure in the file system.
|
||||
topDir := "/whatever"
|
||||
cloneRoot := topDir + "/someClone"
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys.MkdirAll(topDir + "/highBase")
|
||||
fSys.MkdirAll(cloneRoot + "/foo/base")
|
||||
fSys.MkdirAll(cloneRoot + "/foo/overlay")
|
||||
|
||||
var l1 ifc.Loader
|
||||
|
||||
// Establish that a local overlay can navigate
|
||||
// to the local bases.
|
||||
l1 = newLoaderOrDie(
|
||||
RestrictionRootOnly, fSys, cloneRoot+"/foo/overlay")
|
||||
if l1.Root() != cloneRoot+"/foo/overlay" {
|
||||
t.Fatalf("unexpected root %s", l1.Root())
|
||||
}
|
||||
l2, err := l1.New("../base")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if l2.Root() != cloneRoot+"/foo/base" {
|
||||
t.Fatalf("unexpected root %s", l2.Root())
|
||||
}
|
||||
l3, err := l2.New("../../../highBase")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if l3.Root() != topDir+"/highBase" {
|
||||
t.Fatalf("unexpected root %s", l3.Root())
|
||||
}
|
||||
|
||||
// Establish that a Kustomization found in cloned
|
||||
// repo can reach (non-remote) bases inside the clone
|
||||
// but cannot reach a (non-remote) base outside the
|
||||
// clone but legitimately on the local file system.
|
||||
// This is to avoid a surprising interaction between
|
||||
// a remote K and local files. The remote K would be
|
||||
// non-functional on its own since by definition it
|
||||
// would refer to a non-remote base file that didn't
|
||||
// exist in its own repository, so presumably the
|
||||
// remote K would be deliberately designed to phish
|
||||
// for local K's.
|
||||
repoSpec, err := git.NewRepoSpecFromUrl(
|
||||
"github.com/someOrg/someRepo/foo/overlay")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
l1, err = newLoaderAtGitClone(
|
||||
repoSpec, fSys, nil,
|
||||
git.DoNothingCloner(filesys.ConfirmedDir(cloneRoot)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if l1.Root() != cloneRoot+"/foo/overlay" {
|
||||
t.Fatalf("unexpected root %s", l1.Root())
|
||||
}
|
||||
// This is okay.
|
||||
l2, err = l1.New("../base")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if l2.Root() != cloneRoot+"/foo/base" {
|
||||
t.Fatalf("unexpected root %s", l2.Root())
|
||||
}
|
||||
// This is not okay.
|
||||
l3, err = l2.New("../../../highBase")
|
||||
if err == nil {
|
||||
t.Fatalf("expected err")
|
||||
}
|
||||
if !strings.Contains(err.Error(),
|
||||
"base '/whatever/highBase' is outside '/whatever/someClone'") {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalLoaderReferencingGitBase(t *testing.T) {
|
||||
topDir := "/whatever"
|
||||
cloneRoot := topDir + "/someClone"
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys.MkdirAll(topDir)
|
||||
fSys.MkdirAll(cloneRoot + "/foo/base")
|
||||
|
||||
root, err := demandDirectoryRoot(fSys, topDir)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
l1 := newLoaderAtConfirmedDir(
|
||||
RestrictionRootOnly, root, fSys, nil,
|
||||
git.DoNothingCloner(filesys.ConfirmedDir(cloneRoot)))
|
||||
if l1.Root() != topDir {
|
||||
t.Fatalf("unexpected root %s", l1.Root())
|
||||
}
|
||||
l2, err := l1.New("github.com/someOrg/someRepo/foo/base")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if l2.Root() != cloneRoot+"/foo/base" {
|
||||
t.Fatalf("unexpected root %s", l2.Root())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoDirectCycleDetection(t *testing.T) {
|
||||
topDir := "/cycles"
|
||||
cloneRoot := topDir + "/someClone"
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys.MkdirAll(topDir)
|
||||
fSys.MkdirAll(cloneRoot)
|
||||
|
||||
root, err := demandDirectoryRoot(fSys, topDir)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
l1 := newLoaderAtConfirmedDir(
|
||||
RestrictionRootOnly, root, fSys, nil,
|
||||
git.DoNothingCloner(filesys.ConfirmedDir(cloneRoot)))
|
||||
p1 := "github.com/someOrg/someRepo/foo"
|
||||
rs1, err := git.NewRepoSpecFromUrl(p1)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
l1.repoSpec = rs1
|
||||
_, err = l1.New(p1)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cycle detected") {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoIndirectCycleDetection(t *testing.T) {
|
||||
topDir := "/cycles"
|
||||
cloneRoot := topDir + "/someClone"
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys.MkdirAll(topDir)
|
||||
fSys.MkdirAll(cloneRoot)
|
||||
|
||||
root, err := demandDirectoryRoot(fSys, topDir)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
l0 := newLoaderAtConfirmedDir(
|
||||
RestrictionRootOnly, root, fSys, nil,
|
||||
git.DoNothingCloner(filesys.ConfirmedDir(cloneRoot)))
|
||||
|
||||
p1 := "github.com/someOrg/someRepo1"
|
||||
p2 := "github.com/someOrg/someRepo2"
|
||||
|
||||
l1, err := l0.New(p1)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
l2, err := l1.New(p2)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
_, err = l2.New(p1)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cycle detected") {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
}
|
||||
34
api/loader/loader.go
Normal file
34
api/loader/loader.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package loader has a data loading interface and various implementations.
|
||||
package loader
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/v3/api/filesys"
|
||||
"sigs.k8s.io/kustomize/v3/api/ifc"
|
||||
"sigs.k8s.io/kustomize/v3/pkg/git"
|
||||
)
|
||||
|
||||
// NewLoader returns a Loader pointed at the given target.
|
||||
// If the target is remote, the loader will be restricted
|
||||
// to the root and below only. If the target is local, the
|
||||
// loader will have the restrictions passed in. Regardless,
|
||||
// if a local target attempts to transitively load remote bases,
|
||||
// the remote bases will all be root-only restricted.
|
||||
func NewLoader(
|
||||
lr LoadRestrictorFunc,
|
||||
target string, fSys filesys.FileSystem) (ifc.Loader, error) {
|
||||
repoSpec, err := git.NewRepoSpecFromUrl(target)
|
||||
if err == nil {
|
||||
// The target qualifies as a remote git target.
|
||||
return newLoaderAtGitClone(
|
||||
repoSpec, fSys, nil, git.ClonerUsingGitExec)
|
||||
}
|
||||
root, err := demandDirectoryRoot(fSys, target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newLoaderAtConfirmedDir(
|
||||
lr, root, fSys, nil, git.ClonerUsingGitExec), nil
|
||||
}
|
||||
76
api/loader/loadrestrictions.go
Normal file
76
api/loader/loadrestrictions.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package loader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"sigs.k8s.io/kustomize/v3/api/filesys"
|
||||
)
|
||||
|
||||
//go:generate stringer -type=loadRestrictions
|
||||
type loadRestrictions int
|
||||
|
||||
const (
|
||||
unknown loadRestrictions = iota
|
||||
rootOnly
|
||||
none
|
||||
)
|
||||
|
||||
const (
|
||||
flagName = "load_restrictor"
|
||||
)
|
||||
|
||||
var (
|
||||
flagValue = rootOnly.String()
|
||||
flagHelp = "if set to '" + none.String() +
|
||||
"', local kustomizations may load files from outside their root. " +
|
||||
"This does, however, break the relocatability of the kustomization."
|
||||
)
|
||||
|
||||
func AddFlagLoadRestrictor(set *pflag.FlagSet) {
|
||||
set.StringVar(
|
||||
&flagValue, flagName,
|
||||
rootOnly.String(), flagHelp)
|
||||
}
|
||||
|
||||
func ValidateFlagLoadRestrictor() (LoadRestrictorFunc, error) {
|
||||
switch flagValue {
|
||||
case rootOnly.String():
|
||||
return RestrictionRootOnly, nil
|
||||
case none.String():
|
||||
return RestrictionNone, nil
|
||||
default:
|
||||
return nil, fmt.Errorf(
|
||||
"illegal flag value --%s %s; legal values: %v",
|
||||
flagName, flagValue,
|
||||
[]string{rootOnly.String(), none.String()})
|
||||
}
|
||||
}
|
||||
|
||||
type LoadRestrictorFunc func(
|
||||
filesys.FileSystem, filesys.ConfirmedDir, string) (string, error)
|
||||
|
||||
func RestrictionRootOnly(
|
||||
fSys filesys.FileSystem, root filesys.ConfirmedDir, path string) (string, error) {
|
||||
d, f, err := fSys.CleanedAbs(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if f == "" {
|
||||
return "", fmt.Errorf("'%s' must be a file", path)
|
||||
}
|
||||
if !d.HasPrefix(root) {
|
||||
return "", fmt.Errorf(
|
||||
"security; file '%s' is not in or below '%s'",
|
||||
path, root)
|
||||
}
|
||||
return d.Join(f), nil
|
||||
}
|
||||
|
||||
func RestrictionNone(
|
||||
_ filesys.FileSystem, _ filesys.ConfirmedDir, path string) (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
41
api/loader/loadrestrictions_string.go
Normal file
41
api/loader/loadrestrictions_string.go
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2019 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.
|
||||
*/
|
||||
|
||||
// Code generated by "stringer -type=loadRestrictions"; DO NOT EDIT.
|
||||
|
||||
package loader
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[unknown-0]
|
||||
_ = x[rootOnly-1]
|
||||
_ = x[none-2]
|
||||
}
|
||||
|
||||
const _loadRestrictions_name = "unknownrootOnlynone"
|
||||
|
||||
var _loadRestrictions_index = [...]uint8{0, 7, 15, 19}
|
||||
|
||||
func (i loadRestrictions) String() string {
|
||||
if i < 0 || i >= loadRestrictions(len(_loadRestrictions_index)-1) {
|
||||
return "loadRestrictions(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _loadRestrictions_name[_loadRestrictions_index[i]:_loadRestrictions_index[i+1]]
|
||||
}
|
||||
74
api/loader/loadrestrictions_test.go
Normal file
74
api/loader/loadrestrictions_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
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 (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/filesys"
|
||||
)
|
||||
|
||||
func TestRestrictionNone(t *testing.T) {
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
root := filesys.ConfirmedDir("irrelevant")
|
||||
path := "whatever"
|
||||
p, err := RestrictionNone(fSys, root, path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p != path {
|
||||
t.Fatalf("expected '%s', got '%s'", path, p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestrictionRootOnly(t *testing.T) {
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
root := filesys.ConfirmedDir("/tmp/foo")
|
||||
|
||||
path := "/tmp/foo/whatever/beans"
|
||||
p, err := RestrictionRootOnly(fSys, root, path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p != path {
|
||||
t.Fatalf("expected '%s', got '%s'", path, p)
|
||||
}
|
||||
|
||||
// Legal.
|
||||
path = "/tmp/foo/whatever/../../foo/whatever"
|
||||
p, err = RestrictionRootOnly(fSys, root, path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path = "/tmp/foo/whatever"
|
||||
if p != path {
|
||||
t.Fatalf("expected '%s', got '%s'", path, p)
|
||||
}
|
||||
|
||||
// Illegal.
|
||||
path = "/tmp/illegal"
|
||||
_, err = RestrictionRootOnly(fSys, root, path)
|
||||
if err == nil {
|
||||
t.Fatal("should have an error")
|
||||
}
|
||||
if !strings.Contains(
|
||||
err.Error(),
|
||||
"file '/tmp/illegal' is not in or below '/tmp/foo'") {
|
||||
t.Fatalf("unexpected err: %s", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user