/// Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package loader import ( "io/ioutil" "os" "path" "path/filepath" "reflect" "strings" "testing" "sigs.k8s.io/kustomize/api/filesys" "sigs.k8s.io/kustomize/api/git" "sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/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) } }