[refactor]: Internalize loader api

This PR intends to move the loader api to
internal. Only the necessary methods which
are needed for the api have been put into
`pkg/loader.go`.

Signed-off-by: Varsha Prasad Narsing <varshaprasad96@gmail.com>
This commit is contained in:
Varsha Prasad Narsing
2023-06-06 17:26:34 -04:00
parent e3b9afcfaa
commit 7911b2c001
31 changed files with 109 additions and 97 deletions

View File

@@ -0,0 +1,11 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package loader
import "sigs.k8s.io/kustomize/kyaml/errors"
var (
ErrHTTP = errors.Errorf("HTTP Error")
ErrRtNotDir = errors.Errorf("must build at directory")
)

View File

@@ -0,0 +1,334 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package loader
import (
"fmt"
"io"
"log"
"net/http"
"net/url"
"path/filepath"
"strings"
"sigs.k8s.io/kustomize/api/ifc"
"sigs.k8s.io/kustomize/api/internal/git"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/filesys"
)
// IsRemoteFile returns whether path has a url scheme that kustomize allows for
// remote files. See https://github.com/kubernetes-sigs/kustomize/blob/master/examples/remoteBuild.md
func IsRemoteFile(path string) bool {
u, err := url.Parse(path)
return err == nil && (u.Scheme == "http" || u.Scheme == "https")
}
// 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
}
// Repo returns the absolute path to the repo that contains Root if this fileLoader was created from a url
// or the empty string otherwise.
func (fl *FileLoader) Repo() string {
if fl.repoSpec != nil {
return fl.repoSpec.Dir.String()
}
return ""
}
// 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 := filesys.ConfirmDir(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 },
}
}
// 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, errors.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 := filesys.ConfirmDir(fl.fSys, fl.root.Join(path))
if err != nil {
return nil, errors.WrapPrefixf(err, ErrRtNotDir.Error())
}
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)
}
// Path in repo can contain symlinks that exit repo. We can only
// check for this after cloning repo.
if !root.HasPrefix(repoSpec.CloneDir()) {
_ = cleaner()
return nil, fmt.Errorf("%q refers to directory outside of repo %q", repoSpec.AbsPath(),
repoSpec.CloneDir())
}
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 IsRemoteFile(path) {
return fl.httpClientGetContent(path)
}
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)
}
func (fl *FileLoader) httpClientGetContent(path string) ([]byte, error) {
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, errors.Wrap(err)
}
defer resp.Body.Close()
// response unsuccessful
if resp.StatusCode < 200 || resp.StatusCode > 299 {
_, err = git.NewRepoSpecFromURL(path)
if err == nil {
return nil, errors.Errorf("URL is a git repository")
}
return nil, fmt.Errorf("%w: status code %d (%s)", ErrHTTP, resp.StatusCode, http.StatusText(resp.StatusCode))
}
content, err := io.ReadAll(resp.Body)
return content, errors.Wrap(err)
}
// Cleanup runs the cleaner.
func (fl *FileLoader) Cleanup() error {
return fl.cleaner()
}

View File

@@ -0,0 +1,678 @@
/// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package loader
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"path"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/require"
"sigs.k8s.io/kustomize/api/ifc"
"sigs.k8s.io/kustomize/api/internal/git"
"sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/kustomize/kyaml/filesys"
)
func TestIsRemoteFile(t *testing.T) {
cases := map[string]struct {
url string
valid bool
}{
"https file": {
"https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/examples/helloWorld/configMap.yaml",
true,
},
"malformed https": {
// TODO(annasong): Maybe we want to fix this. Needs more research.
"https:/raw.githubusercontent.com/kubernetes-sigs/kustomize/master/examples/helloWorld/configMap.yaml",
true,
},
"https dir": {
"https://github.com/kubernetes-sigs/kustomize//examples/helloWorld/",
true,
},
"no scheme": {
"github.com/kubernetes-sigs/kustomize//examples/helloWorld/",
false,
},
"ssh": {
"ssh://git@github.com/kubernetes-sigs/kustomize.git",
false,
},
"local": {
"pod.yaml",
false,
},
}
for name, test := range cases {
test := test
t.Run(name, func(t *testing.T) {
require.Equal(t, test.valid, IsRemoteFile(test.url))
})
}
}
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 NewLoaderOrDie(
RestrictionRootOnly, MakeFakeFs(testCases), filesys.Separator)
}
func TestLoaderLoad(t *testing.T) {
require := require.New(t)
l1 := makeLoader()
repo := l1.Repo()
require.Empty(repo)
require.Equal("/", l1.Root())
for _, x := range testCases {
b, err := l1.Load(x.path)
require.NoError(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")
require.NoError(err)
repo = l2.Repo()
require.Empty(repo)
require.Equal("/foo/project", l2.Root())
for _, x := range testCases {
b, err := l2.Load(strings.TrimPrefix(x.path, "foo/project/"))
require.NoError(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
require.NoError(err)
require.Equal("/foo/project", l2.Root())
}
func TestLoaderNewSubDir(t *testing.T) {
require := require.New(t)
l1, err := makeLoader().New("foo/project")
require.NoError(err)
l2, err := l1.New("subdir1")
require.NoError(err)
require.Equal("/foo/project/subdir1", l2.Root())
x := testCases[1]
b, err := l2.Load("fileB.yaml")
require.NoError(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) {
require := require.New(t)
l1, err := makeLoader().New("foo/project/subdir1")
require.NoError(err)
require.Equal("/foo/project/subdir1", l1.Root())
// Cannot cd into a file.
l2, err := l1.New("fileB.yaml")
require.Error(err)
// It's not okay to stay at the same place.
l2, err = l1.New(filesys.SelfDir)
require.Error(err)
// It's not okay to go up and back down into same place.
l2, err = l1.New("../subdir1")
require.Error(err)
// It's not okay to go up via a relative path.
l2, err = l1.New("..")
require.Error(err)
// It's not okay to go up via an absolute path.
l2, err = l1.New("/foo/project")
require.Error(err)
// It's not okay to go to the root.
l2, err = l1.New("/")
require.Error(err)
// It's okay to go up and down to a sibling.
l2, err = l1.New("../subdir2")
require.NoError(err)
require.Equal("/foo/project/subdir2", l2.Root())
x := testCases[2]
b, err := l2.Load("fileC.yaml")
require.NoError(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")
require.Error(err)
}
func TestNewEmptyLoader(t *testing.T) {
_, err := makeLoader().New("")
require.Error(t, err)
}
func TestNewRemoteLoaderDoesNotExist(t *testing.T) {
_, err := makeLoader().New("https://example.com/org/repo")
require.ErrorContains(t, err, "fetch")
}
func TestLoaderLocalScheme(t *testing.T) {
// It is unlikely but possible for a reference with a url scheme to
// actually refer to a local file or directory.
t.Run("file", func(t *testing.T) {
fSys, dir := setupOnDisk(t)
parts := []string{
"ssh:",
"resource.yaml",
}
require.NoError(t, fSys.Mkdir(dir.Join(parts[0])))
const content = "resource config"
require.NoError(t, fSys.WriteFile(
dir.Join(filepath.Join(parts...)),
[]byte(content),
))
actualContent, err := NewLoaderOrDie(RestrictionRootOnly,
fSys,
dir.String(),
).Load(strings.Join(parts, "//"))
require.NoError(t, err)
require.Equal(t, content, string(actualContent))
})
t.Run("directory", func(t *testing.T) {
fSys, dir := setupOnDisk(t)
parts := []string{
"https:",
"root",
}
require.NoError(t, fSys.MkdirAll(dir.Join(filepath.Join(parts...))))
ldr, err := NewLoaderOrDie(RestrictionRootOnly,
fSys,
dir.String(),
).New(strings.Join(parts, "//"))
require.NoError(t, err)
require.Empty(t, ldr.Repo())
})
}
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(t *testing.T) (string, filesys.FileSystem) {
t.Helper()
fSys, tmpDir := setupOnDisk(t)
dir := tmpDir.String()
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
}
// Make sure everything works when loading files
// in or below the loader root.
func doSanityChecksAndDropIntoBase(
t *testing.T, l ifc.Loader) ifc.Loader {
t.Helper()
require := require.New(t)
data, err := l.Load(path.Join("base", "okayData"))
require.NoError(err)
require.Equal(contentOk, string(data))
data, err = l.Load("exteriorData")
require.NoError(err)
require.Equal(contentExteriorData, string(data))
// Drop in.
l, err = l.New("base")
require.NoError(err)
// Reading okayData works.
data, err = l.Load("okayData")
require.NoError(err)
require.Equal(contentOk, string(data))
// Reading local symlink to okayData works.
data, err = l.Load("symLinkToOkayData")
require.NoError(err)
require.Equal(contentOk, string(data))
return l
}
func TestRestrictionRootOnlyInRealLoader(t *testing.T) {
require := require.New(t)
dir, fSys := commonSetupForLoaderRestrictionTest(t)
var l ifc.Loader
l = NewLoaderOrDie(RestrictionRootOnly, fSys, dir)
l = doSanityChecksAndDropIntoBase(t, l)
// Reading symlink to exteriorData fails.
_, err := l.Load("symLinkToExteriorData")
require.Error(err)
require.Contains(err.Error(), "is not in or below")
// Attempt to read "up" fails, though earlier we were
// able to read this file when root was "..".
_, err = l.Load("../exteriorData")
require.Error(err)
require.Contains(err.Error(), "is not in or below")
}
func TestRestrictionNoneInRealLoader(t *testing.T) {
dir, fSys := commonSetupForLoaderRestrictionTest(t)
var l ifc.Loader
l = NewLoaderOrDie(RestrictionNone, fSys, dir)
l = doSanityChecksAndDropIntoBase(t, l)
// Reading symlink to exteriorData works.
_, err := l.Load("symLinkToExteriorData")
require.NoError(t, err)
// Attempt to read "up" works.
_, err = l.Load("../exteriorData")
require.NoError(t, 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 += 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) {
require := require.New(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+"/"+
konfig.DefaultKustomizationFileName(),
[]byte(`
whatever
`))
repoSpec, err := git.NewRepoSpecFromURL(url)
require.NoError(err)
l, err := newLoaderAtGitClone(
repoSpec, fSys, nil,
git.DoNothingCloner(filesys.ConfirmedDir(coRoot)))
require.NoError(err)
repo := l.Repo()
require.Equal(coRoot, repo)
require.Equal(coRoot+"/"+pathInRepo, l.Root())
_, err = l.New(url)
require.Error(err)
_, err = l.New(rootURL + "/" + "foo")
require.Error(err)
pathInRepo = "foo/overlay"
fSys.MkdirAll(coRoot + "/" + pathInRepo)
url = rootURL + "/" + pathInRepo
l2, err := l.New(url)
require.NoError(err)
repo = l2.Repo()
require.Equal(coRoot, repo)
require.Equal(coRoot+"/"+pathInRepo, l2.Root())
}
func TestLoaderDisallowsLocalBaseFromRemoteOverlay(t *testing.T) {
require := require.New(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")
require.Equal(cloneRoot+"/foo/overlay", l1.Root())
l2, err := l1.New("../base")
require.NoError(nil)
require.Equal(cloneRoot+"/foo/base", l2.Root())
l3, err := l2.New("../../../highBase")
require.NoError(err)
require.Equal(topDir+"/highBase", 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")
require.NoError(err)
l1, err = newLoaderAtGitClone(
repoSpec, fSys, nil,
git.DoNothingCloner(filesys.ConfirmedDir(cloneRoot)))
require.NoError(err)
require.Equal(cloneRoot+"/foo/overlay", l1.Root())
// This is okay.
l2, err = l1.New("../base")
require.NoError(err)
repo := l2.Repo()
require.Empty(repo)
require.Equal(cloneRoot+"/foo/base", l2.Root())
// This is not okay.
_, err = l2.New("../../../highBase")
require.Error(err)
require.Contains(err.Error(),
"base '/whatever/highBase' is outside '/whatever/someClone'")
}
func TestLoaderDisallowsRemoteBaseExitRepo(t *testing.T) {
fSys, dir := setupOnDisk(t)
repo := dir.Join("repo")
require.NoError(t, fSys.Mkdir(repo))
base := filepath.Join(repo, "base")
require.NoError(t, os.Symlink(dir.String(), base))
repoSpec, err := git.NewRepoSpecFromURL("https://github.com/org/repo/base")
require.NoError(t, err)
_, err = newLoaderAtGitClone(repoSpec, fSys, nil, git.DoNothingCloner(filesys.ConfirmedDir(repo)))
require.Error(t, err)
require.Contains(t, err.Error(), fmt.Sprintf("%q refers to directory outside of repo %q", base, repo))
}
func TestLocalLoaderReferencingGitBase(t *testing.T) {
require := require.New(t)
topDir := "/whatever"
cloneRoot := topDir + "/someClone"
fSys := filesys.MakeFsInMemory()
fSys.MkdirAll(topDir)
fSys.MkdirAll(cloneRoot + "/foo/base")
l1 := newLoaderAtConfirmedDir(
RestrictionRootOnly, filesys.ConfirmedDir(topDir), fSys, nil,
git.DoNothingCloner(filesys.ConfirmedDir(cloneRoot)))
require.Equal(topDir, l1.Root())
l2, err := l1.New("github.com/someOrg/someRepo/foo/base")
require.NoError(err)
repo := l2.Repo()
require.Equal(cloneRoot, repo)
require.Equal(cloneRoot+"/foo/base", l2.Root())
}
func TestRepoDirectCycleDetection(t *testing.T) {
require := require.New(t)
topDir := "/cycles"
cloneRoot := topDir + "/someClone"
fSys := filesys.MakeFsInMemory()
fSys.MkdirAll(topDir)
fSys.MkdirAll(cloneRoot)
l1 := newLoaderAtConfirmedDir(
RestrictionRootOnly, filesys.ConfirmedDir(topDir), fSys, nil,
git.DoNothingCloner(filesys.ConfirmedDir(cloneRoot)))
p1 := "github.com/someOrg/someRepo/foo"
rs1, err := git.NewRepoSpecFromURL(p1)
require.NoError(err)
l1.repoSpec = rs1
_, err = l1.New(p1)
require.Error(err)
require.Contains(err.Error(), "cycle detected")
}
func TestRepoIndirectCycleDetection(t *testing.T) {
require := require.New(t)
topDir := "/cycles"
cloneRoot := topDir + "/someClone"
fSys := filesys.MakeFsInMemory()
fSys.MkdirAll(topDir)
fSys.MkdirAll(cloneRoot)
l0 := newLoaderAtConfirmedDir(
RestrictionRootOnly, filesys.ConfirmedDir(topDir), fSys, nil,
git.DoNothingCloner(filesys.ConfirmedDir(cloneRoot)))
p1 := "github.com/someOrg/someRepo1"
p2 := "github.com/someOrg/someRepo2"
l1, err := l0.New(p1)
require.NoError(err)
l2, err := l1.New(p2)
require.NoError(err)
_, err = l2.New(p1)
require.Error(err)
require.Contains(err.Error(), "cycle detected")
}
// Inspired by https://hassansin.github.io/Unit-Testing-http-client-in-Go
type fakeRoundTripper func(req *http.Request) *http.Response
func (f fakeRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req), nil
}
func makeFakeHTTPClient(fn fakeRoundTripper) *http.Client {
return &http.Client{
Transport: fn,
}
}
// TestLoaderHTTP test http file loader
func TestLoaderHTTP(t *testing.T) {
require := require.New(t)
var testCasesFile = []testData{
{
path: "http/file.yaml",
expectedContent: "file content",
},
}
l1 := NewLoaderOrDie(
RestrictionRootOnly, MakeFakeFs(testCasesFile), filesys.Separator)
require.Equal("/", l1.Root())
for _, x := range testCasesFile {
b, err := l1.Load(x.path)
require.NoError(err)
if !reflect.DeepEqual([]byte(x.expectedContent), b) {
t.Fatalf("in load expected %s, but got %s", x.expectedContent, b)
}
}
var testCasesHTTP = []testData{
{
path: "http://example.com/resource.yaml",
expectedContent: "http content",
},
{
path: "https://example.com/resource.yaml",
expectedContent: "https content",
},
}
for _, x := range testCasesHTTP {
hc := makeFakeHTTPClient(func(req *http.Request) *http.Response {
u := req.URL.String()
require.Equal(x.path, u)
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBufferString(x.expectedContent)),
Header: make(http.Header),
}
})
l2 := l1
l2.http = hc
b, err := l2.Load(x.path)
require.NoError(err)
if !reflect.DeepEqual([]byte(x.expectedContent), b) {
t.Fatalf("in load expected %s, but got %s", x.expectedContent, b)
}
}
var testCaseUnsupported = []testData{
{
path: "httpsnotreal://example.com/resource.yaml",
expectedContent: "invalid",
},
}
for _, x := range testCaseUnsupported {
hc := makeFakeHTTPClient(func(req *http.Request) *http.Response {
t.Fatalf("unexpected request to URL %s", req.URL.String())
return nil
})
l2 := l1
l2.http = hc
_, err := l2.Load(x.path)
require.Error(err)
}
}
// setupOnDisk sets up a file system on disk and directory that is cleaned after
// test completion.
// TODO(annasong): Move all loader tests that require real file system into
// api/krusty.
func setupOnDisk(t *testing.T) (filesys.FileSystem, filesys.ConfirmedDir) {
t.Helper()
fSys := filesys.MakeFsOnDisk()
dir, err := filesys.NewTmpConfirmedDir()
require.NoError(t, err)
t.Cleanup(func() {
_ = fSys.RemoveAll(dir.String())
})
return fSys, dir
}

View File

@@ -0,0 +1,35 @@
// 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/api/ifc"
"sigs.k8s.io/kustomize/api/internal/git"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/filesys"
)
// 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 := filesys.ConfirmDir(fSys, target)
if err != nil {
return nil, errors.WrapPrefixf(err, ErrRtNotDir.Error())
}
return newLoaderAtConfirmedDir(
lr, root, fSys, nil, git.ClonerUsingGitExec), nil
}

View File

@@ -0,0 +1,35 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package loader
import (
"fmt"
"sigs.k8s.io/kustomize/kyaml/filesys"
)
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 resolve to 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
}

View File

@@ -0,0 +1,67 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package loader
import (
"path/filepath"
"strings"
"testing"
"sigs.k8s.io/kustomize/kyaml/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(
filesys.Separator + filepath.Join("tmp", "foo"))
path := filepath.Join(string(root), "whatever", "beans")
fSys.Create(path)
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 = filepath.Join(
string(root), "whatever", "..", "..", "foo", "whatever", "beans")
p, err = RestrictionRootOnly(fSys, root, path)
if err != nil {
t.Fatal(err)
}
path = filepath.Join(
string(root), "whatever", "beans")
if p != path {
t.Fatalf("expected '%s', got '%s'", path, p)
}
// Illegal; file exists but is out of bounds.
path = filepath.Join(filesys.Separator+"tmp", "illegal")
fSys.Create(path)
_, 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)
}
}