Start api directory, which will become a module.

This commit is contained in:
Jeffrey Regan
2019-10-17 11:36:34 -07:00
parent 180429774a
commit e5c8b5ec8f
226 changed files with 623 additions and 689 deletions

View File

@@ -0,0 +1,79 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filesys
import (
"io/ioutil"
"path/filepath"
"strings"
)
// ConfirmedDir is a clean, absolute, delinkified path
// that was confirmed to point to an existing directory.
type ConfirmedDir string
// NewTmpConfirmedDir returns a temporary dir, else error.
// The directory is cleaned, no symlinks, etc. so it's
// returned as a ConfirmedDir.
func NewTmpConfirmedDir() (ConfirmedDir, error) {
n, err := ioutil.TempDir("", "kustomize-")
if err != nil {
return "", err
}
// In MacOs `ioutil.TempDir` creates a directory
// with root in the `/var` folder, which is in turn
// a symlinked path to `/private/var`.
// Function `filepath.EvalSymlinks`is used to
// resolve the real absolute path.
deLinked, err := filepath.EvalSymlinks(n)
return ConfirmedDir(deLinked), err
}
// HasPrefix returns true if the directory argument
// is a prefix of self (d) from the point of view of
// a file system.
//
// I.e., it's true if the argument equals or contains
// self (d) in a file path sense.
//
// HasPrefix emulates the semantics of strings.HasPrefix
// such that the following are true:
//
// strings.HasPrefix("foobar", "foobar")
// strings.HasPrefix("foobar", "foo")
// strings.HasPrefix("foobar", "")
//
// d := fSys.ConfirmDir("/foo/bar")
// d.HasPrefix("/foo/bar")
// d.HasPrefix("/foo")
// d.HasPrefix("/")
//
// Not contacting a file system here to check for
// actual path existence.
//
// This is tested on linux, but will have trouble
// on other operating systems.
// TODO(monopole) Refactor when #golang/go/18358 closes.
// See also:
// https://github.com/golang/go/issues/18358
// https://github.com/golang/dep/issues/296
// https://github.com/golang/dep/blob/master/internal/fs/fs.go#L33
// https://codereview.appspot.com/5712045
func (d ConfirmedDir) HasPrefix(path ConfirmedDir) bool {
if path.String() == string(filepath.Separator) || path == d {
return true
}
return strings.HasPrefix(
string(d),
string(path)+string(filepath.Separator))
}
func (d ConfirmedDir) Join(path string) string {
return filepath.Join(string(d), path)
}
func (d ConfirmedDir) String() string {
return string(d)
}

View File

@@ -0,0 +1,109 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filesys_test
import (
"path/filepath"
"testing"
. "sigs.k8s.io/kustomize/v3/api/filesys"
)
func TestJoin(t *testing.T) {
fSys := MakeFsInMemory()
err := fSys.Mkdir("/foo")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
d, f, err := fSys.CleanedAbs("/foo")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if f != "" {
t.Fatalf("unexpected file: %v", f)
}
if d.Join("bar") != "/foo/bar" {
t.Fatalf("expected join %s", d.Join("bar"))
}
}
func TestHasPrefix_Slash(t *testing.T) {
fSys := MakeFsInMemory()
d, f, err := fSys.CleanedAbs("/")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if f != "" {
t.Fatalf("unexpected file: %v", f)
}
if d.HasPrefix("/hey") {
t.Fatalf("should be false")
}
if !d.HasPrefix("/") {
t.Fatalf("/ should have the prefix /")
}
}
func TestHasPrefix_SlashFoo(t *testing.T) {
fSys := MakeFsInMemory()
err := fSys.Mkdir("/foo")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
d, _, err := fSys.CleanedAbs("/foo")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if d.HasPrefix("/fo") {
t.Fatalf("/foo does not have path prefix /fo")
}
if d.HasPrefix("/fod") {
t.Fatalf("/foo does not have path prefix /fod")
}
if !d.HasPrefix("/foo") {
t.Fatalf("/foo should have prefix /foo")
}
}
func TestHasPrefix_SlashFooBar(t *testing.T) {
fSys := MakeFsInMemory()
err := fSys.MkdirAll("/foo/bar")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
d, _, err := fSys.CleanedAbs("/foo/bar")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if d.HasPrefix("/fo") {
t.Fatalf("/foo/bar does not have path prefix /fo")
}
if d.HasPrefix("/foobar") {
t.Fatalf("/foo/bar does not have path prefix /foobar")
}
if !d.HasPrefix("/foo/bar") {
t.Fatalf("/foo/bar should have prefix /foo/bar")
}
if !d.HasPrefix("/foo") {
t.Fatalf("/foo/bar should have prefix /foo")
}
if !d.HasPrefix("/") {
t.Fatalf("/foo/bar should have prefix /")
}
}
func TestNewTempConfirmDir(t *testing.T) {
tmp, err := NewTmpConfirmedDir()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
delinked, err := filepath.EvalSymlinks(string(tmp))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(tmp) != delinked {
t.Fatalf("unexpected path containing symlinks")
}
}

41
api/filesys/file.go Normal file
View File

@@ -0,0 +1,41 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filesys
import (
"io"
"os"
"time"
)
var _ os.FileInfo = &fileInfo{}
// fileInfo implements os.FileInfo for a fileInMemory instance.
type fileInfo struct {
*fileInMemory
}
// Name returns the name of the file
func (fi *fileInfo) Name() string { return fi.name }
// Size returns the size of the file
func (fi *fileInfo) Size() int64 { return int64(len(fi.content)) }
// Mode returns the file mode
func (fi *fileInfo) Mode() os.FileMode { return 0777 }
// ModTime returns the modification time
func (fi *fileInfo) ModTime() time.Time { return time.Time{} }
// IsDir returns if it is a directory
func (fi *fileInfo) IsDir() bool { return fi.dir }
// Sys should return underlying data source, but it now returns nil
func (fi *fileInfo) Sys() interface{} { return nil }
// File groups the basic os.File methods.
type File interface {
io.ReadWriteCloser
Stat() (os.FileInfo, error)
}

View File

@@ -0,0 +1,56 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filesys
import (
"bytes"
"os"
)
var _ File = &fileInMemory{}
// fileInMemory implements File in-memory for tests.
type fileInMemory struct {
name string
content []byte
dir bool
open bool
}
// makeDir makes a fake directory.
func makeDir(name string) *fileInMemory {
return &fileInMemory{name: name, dir: true}
}
// Close marks the fake file closed.
func (f *fileInMemory) Close() error {
f.open = false
return nil
}
// Read never fails, and doesn't mutate p.
func (f *fileInMemory) Read(p []byte) (n int, err error) {
return len(p), nil
}
// Write saves the contents of the argument to memory.
func (f *fileInMemory) Write(p []byte) (n int, err error) {
f.content = p
return len(p), nil
}
// ContentMatches returns true if v matches fake file's content.
func (f *fileInMemory) ContentMatches(v []byte) bool {
return bytes.Equal(v, f.content)
}
// GetContent the content of a fake file.
func (f *fileInMemory) GetContent() []byte {
return f.content
}
// Stat returns nil.
func (f *fileInMemory) Stat() (os.FileInfo, error) {
return nil, nil
}

27
api/filesys/fileondisk.go Normal file
View File

@@ -0,0 +1,27 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filesys
import (
"os"
)
var _ File = &fileOnDisk{}
// fileOnDisk implements File using the local filesystem.
type fileOnDisk struct {
file *os.File
}
// Close closes a file.
func (f *fileOnDisk) Close() error { return f.file.Close() }
// Read reads a file's content.
func (f *fileOnDisk) Read(p []byte) (n int, err error) { return f.file.Read(p) }
// Write writes bytes to a file
func (f *fileOnDisk) Write(p []byte) (n int, err error) { return f.file.Write(p) }
// Stat returns an interface which has all the information regarding the file.
func (f *fileOnDisk) Stat() (os.FileInfo, error) { return f.file.Stat() }

41
api/filesys/filesystem.go Normal file
View File

@@ -0,0 +1,41 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package filesys provides a file system abstraction layer.
package filesys
import (
"path/filepath"
)
// FileSystem groups basic os filesystem methods.
type FileSystem interface {
// Create a file.
Create(name string) (File, error)
// MkDir makes a directory.
Mkdir(path string) error
// MkDir makes a directory path, creating intervening directories.
MkdirAll(path string) error
// RemoveAll removes path and any children it contains.
RemoveAll(path string) error
// Open opens the named file for reading.
Open(path string) (File, error)
// IsDir returns true if the path is a directory.
IsDir(path string) bool
// CleanedAbs converts the given path into a
// directory and a file name, where the directory
// is represented as a ConfirmedDir and all that implies.
// If the entire path is a directory, the file component
// is an empty string.
CleanedAbs(path string) (ConfirmedDir, string, error)
// Exists is true if the path exists in the file system.
Exists(path string) bool
// Glob returns the list of matching files
Glob(pattern string) ([]string, error)
// ReadFile returns the contents of the file at the given path.
ReadFile(path string) ([]byte, error)
// WriteFile writes the data to a file at the given path.
WriteFile(path string, data []byte) error
// Walk walks the file system with the given WalkFunc.
Walk(path string, walkFn filepath.WalkFunc) error
}

223
api/filesys/fsinmemory.go Normal file
View File

@@ -0,0 +1,223 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filesys
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
var _ FileSystem = &fsInMemory{}
// fsInMemory implements FileSystem using a in-memory filesystem
// primarily for use in tests.
type fsInMemory struct {
m map[string]*fileInMemory
}
// MakeFsInMemory returns an instance of fsInMemory with no files in it.
func MakeFsInMemory() FileSystem {
result := &fsInMemory{m: map[string]*fileInMemory{}}
result.Mkdir(separator)
return result
}
const (
separator = string(filepath.Separator)
doubleSep = separator + separator
)
// Create assures a fake file appears in the in-memory file system.
func (fs *fsInMemory) Create(name string) (File, error) {
f := &fileInMemory{}
f.open = true
fs.m[name] = f
return fs.m[name], nil
}
// Mkdir assures a fake directory appears in the in-memory file system.
func (fs *fsInMemory) Mkdir(name string) error {
fs.m[name] = makeDir(name)
return nil
}
// MkdirAll delegates to Mkdir
func (fs *fsInMemory) MkdirAll(name string) error {
return fs.Mkdir(name)
}
// RemoveAll presumably does rm -r on a path.
// There's no error.
func (fs *fsInMemory) RemoveAll(name string) error {
var toRemove []string
for k := range fs.m {
if strings.HasPrefix(k, name) {
toRemove = append(toRemove, k)
}
}
for _, k := range toRemove {
delete(fs.m, k)
}
return nil
}
// Open returns a fake file in the open state.
func (fs *fsInMemory) Open(name string) (File, error) {
if _, found := fs.m[name]; !found {
return nil, fmt.Errorf("file %q cannot be opened", name)
}
return fs.m[name], nil
}
// CleanedAbs cannot fail.
func (fs *fsInMemory) CleanedAbs(path string) (ConfirmedDir, string, error) {
if fs.IsDir(path) {
return ConfirmedDir(path), "", nil
}
d := filepath.Dir(path)
if d == path {
return ConfirmedDir(d), "", nil
}
return ConfirmedDir(d), filepath.Base(path), nil
}
// Exists returns true if file is known.
func (fs *fsInMemory) Exists(name string) bool {
_, found := fs.m[name]
return found
}
// Glob returns the list of matching files
func (fs *fsInMemory) Glob(pattern string) ([]string, error) {
var result []string
for p := range fs.m {
if fs.pathMatch(p, pattern) {
result = append(result, p)
}
}
sort.Strings(result)
return result, nil
}
// IsDir returns true if the file exists and is a directory.
func (fs *fsInMemory) IsDir(name string) bool {
f, found := fs.m[name]
if found && f.dir {
return true
}
if !strings.HasSuffix(name, separator) {
name = name + separator
}
for k := range fs.m {
if strings.HasPrefix(k, name) {
return true
}
}
return false
}
// ReadFile always returns an empty bytes and error depending on content of m.
func (fs *fsInMemory) ReadFile(name string) ([]byte, error) {
if ff, found := fs.m[name]; found {
return ff.content, nil
}
return nil, fmt.Errorf("cannot read file %q", name)
}
// WriteFile always succeeds and does nothing.
func (fs *fsInMemory) WriteFile(name string, c []byte) error {
ff := &fileInMemory{}
ff.Write(c)
fs.m[name] = ff
return nil
}
// Walk implements filepath.Walk using the fake filesystem.
func (fs *fsInMemory) Walk(path string, walkFn filepath.WalkFunc) error {
info, err := fs.lstat(path)
if err != nil {
err = walkFn(path, info, err)
} else {
err = fs.walk(path, info, walkFn)
}
if err == filepath.SkipDir {
return nil
}
return err
}
func (fs *fsInMemory) pathMatch(path, pattern string) bool {
match, _ := filepath.Match(pattern, path)
return match
}
func (fs *fsInMemory) lstat(path string) (*fileInfo, error) {
f, found := fs.m[path]
if !found {
return nil, os.ErrNotExist
}
return &fileInfo{f}, nil
}
func (fs *fsInMemory) join(elem ...string) string {
for i, e := range elem {
if e != "" {
return strings.Replace(
strings.Join(elem[i:], separator), doubleSep, separator, -1)
}
}
return ""
}
func (fs *fsInMemory) readDirNames(path string) []string {
var names []string
if !strings.HasSuffix(path, separator) {
path += separator
}
pathSegments := strings.Count(path, separator)
for name := range fs.m {
if name == path {
continue
}
if strings.Count(name, separator) > pathSegments {
continue
}
if strings.HasPrefix(name, path) {
names = append(names, filepath.Base(name))
}
}
sort.Strings(names)
return names
}
func (fs *fsInMemory) walk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
if !info.IsDir() {
return walkFn(path, info, nil)
}
names := fs.readDirNames(path)
if err := walkFn(path, info, nil); err != nil {
return err
}
for _, name := range names {
filename := fs.join(path, name)
fileInfo, err := fs.lstat(filename)
if err != nil {
if err := walkFn(filename, fileInfo, os.ErrNotExist); err != nil && err != filepath.SkipDir {
return err
}
} else {
err = fs.walk(filename, fileInfo, walkFn)
if err != nil {
if !fileInfo.IsDir() || err != filepath.SkipDir {
return err
}
}
}
}
return nil
}

View File

@@ -0,0 +1,149 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filesys_test
import (
"bytes"
"reflect"
"testing"
. "sigs.k8s.io/kustomize/v3/api/filesys"
)
func TestExists(t *testing.T) {
fSys := MakeFsInMemory()
if fSys.Exists("foo") {
t.Fatalf("expected no foo")
}
fSys.Mkdir("/")
if !fSys.IsDir("/") {
t.Fatalf("expected dir at /")
}
}
func TestIsDir(t *testing.T) {
fSys := MakeFsInMemory()
expectedName := "my-dir"
err := fSys.Mkdir(expectedName)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
shouldExist(t, fSys, expectedName)
if !fSys.IsDir(expectedName) {
t.Fatalf(expectedName + " should be a dir")
}
}
func shouldExist(t *testing.T, fSys FileSystem, name string) {
if !fSys.Exists(name) {
t.Fatalf(name + " should exist")
}
}
func shouldNotExist(t *testing.T, fSys FileSystem, name string) {
if fSys.Exists(name) {
t.Fatalf(name + " should not exist")
}
}
func TestRemoveAll(t *testing.T) {
fSys := MakeFsInMemory()
fSys.WriteFile("/foo/project/file.yaml", []byte("Unused"))
fSys.WriteFile("/foo/project/subdir/file.yaml", []byte("Unused"))
fSys.WriteFile("/foo/apple/subdir/file.yaml", []byte("Unused"))
shouldExist(t, fSys, "/foo/project/file.yaml")
shouldExist(t, fSys, "/foo/project/subdir/file.yaml")
shouldExist(t, fSys, "/foo/apple/subdir/file.yaml")
err := fSys.RemoveAll("/foo/project")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
shouldNotExist(t, fSys, "/foo/project/file.yaml")
shouldNotExist(t, fSys, "/foo/project/subdir/file.yaml")
shouldExist(t, fSys, "/foo/apple/subdir/file.yaml")
}
func TestIsDirDeeper(t *testing.T) {
fSys := MakeFsInMemory()
fSys.WriteFile("/foo/project/file.yaml", []byte("Unused"))
fSys.WriteFile("/foo/project/subdir/file.yaml", []byte("Unused"))
if !fSys.IsDir("/") {
t.Fatalf("/ should be a dir")
}
if !fSys.IsDir("/foo") {
t.Fatalf("/foo should be a dir")
}
if !fSys.IsDir("/foo/project") {
t.Fatalf("/foo/project should be a dir")
}
if fSys.IsDir("/fo") {
t.Fatalf("/fo should not be a dir")
}
if fSys.IsDir("/x") {
t.Fatalf("/x should not be a dir")
}
}
func TestCreate(t *testing.T) {
fSys := MakeFsInMemory()
f, err := fSys.Create("foo")
if f == nil {
t.Fatalf("expected file")
}
if err != nil {
t.Fatalf("unexpected error")
}
shouldExist(t, fSys, "foo")
}
func TestReadFile(t *testing.T) {
fSys := MakeFsInMemory()
f, err := fSys.Create("foo")
if f == nil {
t.Fatalf("expected file")
}
if err != nil {
t.Fatalf("unexpected error")
}
content, err := fSys.ReadFile("foo")
if len(content) != 0 {
t.Fatalf("expected no content")
}
if err != nil {
t.Fatalf("expected no error")
}
}
func TestWriteFile(t *testing.T) {
fSys := MakeFsInMemory()
c := []byte("heybuddy")
err := fSys.WriteFile("foo", c)
if err != nil {
t.Fatalf("expected no error")
}
content, err := fSys.ReadFile("foo")
if err != nil {
t.Fatalf("expected read to work: %v", err)
}
if bytes.Compare(c, content) != 0 {
t.Fatalf("incorrect content: %v", content)
}
}
func TestGlob(t *testing.T) {
fSys := MakeFsInMemory()
fSys.Create("dir/foo")
fSys.Create("dir/bar")
files, err := fSys.Glob("dir/*")
if err != nil {
t.Fatalf("expected no error")
}
expected := []string{
"dir/bar",
"dir/foo",
}
if !reflect.DeepEqual(files, expected) {
t.Fatalf("incorrect files found by glob: %v", files)
}
}

114
api/filesys/fsondisk.go Normal file
View File

@@ -0,0 +1,114 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filesys
import (
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
)
var _ FileSystem = fsOnDisk{}
// fsOnDisk implements FileSystem using the local filesystem.
type fsOnDisk struct{}
// MakeFsOnDisk makes an instance of fsOnDisk.
func MakeFsOnDisk() FileSystem {
return fsOnDisk{}
}
// Create delegates to os.Create.
func (fsOnDisk) Create(name string) (File, error) { return os.Create(name) }
// Mkdir delegates to os.Mkdir.
func (fsOnDisk) Mkdir(name string) error {
return os.Mkdir(name, 0777|os.ModeDir)
}
// MkdirAll delegates to os.MkdirAll.
func (fsOnDisk) MkdirAll(name string) error {
return os.MkdirAll(name, 0777|os.ModeDir)
}
// RemoveAll delegates to os.RemoveAll.
func (fsOnDisk) RemoveAll(name string) error {
return os.RemoveAll(name)
}
// Open delegates to os.Open.
func (fsOnDisk) Open(name string) (File, error) { return os.Open(name) }
// CleanedAbs converts the given path into a
// directory and a file name, where the directory
// is represented as a ConfirmedDir and all that implies.
// If the entire path is a directory, the file component
// is an empty string.
func (x fsOnDisk) CleanedAbs(
path string) (ConfirmedDir, string, error) {
absRoot, err := filepath.Abs(path)
if err != nil {
return "", "", fmt.Errorf(
"abs path error on '%s' : %v", path, err)
}
deLinked, err := filepath.EvalSymlinks(absRoot)
if err != nil {
return "", "", fmt.Errorf(
"evalsymlink failure on '%s' : %v", path, err)
}
if x.IsDir(deLinked) {
return ConfirmedDir(deLinked), "", nil
}
d := filepath.Dir(deLinked)
if !x.IsDir(d) {
// Programmer/assumption error.
log.Fatalf("first part of '%s' not a directory", deLinked)
}
if d == deLinked {
// Programmer/assumption error.
log.Fatalf("d '%s' should be a subset of deLinked", d)
}
f := filepath.Base(deLinked)
if filepath.Join(d, f) != deLinked {
// Programmer/assumption error.
log.Fatalf("these should be equal: '%s', '%s'",
filepath.Join(d, f), deLinked)
}
return ConfirmedDir(d), f, nil
}
// Exists returns true if os.Stat succeeds.
func (fsOnDisk) Exists(name string) bool {
_, err := os.Stat(name)
return err == nil
}
// Glob returns the list of matching files
func (fsOnDisk) Glob(pattern string) ([]string, error) {
return filepath.Glob(pattern)
}
// IsDir delegates to os.Stat and FileInfo.IsDir
func (fsOnDisk) IsDir(name string) bool {
info, err := os.Stat(name)
if err != nil {
return false
}
return info.IsDir()
}
// ReadFile delegates to ioutil.ReadFile.
func (fsOnDisk) ReadFile(name string) ([]byte, error) { return ioutil.ReadFile(name) }
// WriteFile delegates to ioutil.WriteFile with read/write permissions.
func (fsOnDisk) WriteFile(name string, c []byte) error {
return ioutil.WriteFile(name, c, 0666)
}
// Walk delegates to filepath.Walk.
func (fsOnDisk) Walk(path string, walkFn filepath.WalkFunc) error {
return filepath.Walk(path, walkFn)
}

View File

@@ -0,0 +1,165 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filesys_test
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"reflect"
"testing"
. "sigs.k8s.io/kustomize/v3/api/filesys"
)
func makeTestDir(t *testing.T) (FileSystem, string) {
fSys := MakeFsOnDisk()
td, err := ioutil.TempDir("", "kustomize_testing_dir")
if err != nil {
t.Fatalf("unexpected error %s", err)
}
testDir, err := filepath.EvalSymlinks(td)
if err != nil {
t.Fatalf("unexpected error %s", err)
}
if !fSys.Exists(testDir) {
t.Fatalf("expected existence")
}
if !fSys.IsDir(testDir) {
t.Fatalf("expected directory")
}
return fSys, testDir
}
func TestCleanedAbs_1(t *testing.T) {
fSys, testDir := makeTestDir(t)
defer os.RemoveAll(testDir)
d, f, err := fSys.CleanedAbs("")
if err != nil {
t.Fatalf("unexpected err=%v", err)
}
wd, err := os.Getwd()
if err != nil {
t.Fatalf("unexpected err=%v", err)
}
if d.String() != wd {
t.Fatalf("unexpected d=%s", d)
}
if f != "" {
t.Fatalf("unexpected f=%s", f)
}
}
func TestCleanedAbs_2(t *testing.T) {
fSys, testDir := makeTestDir(t)
defer os.RemoveAll(testDir)
d, f, err := fSys.CleanedAbs("/")
if err != nil {
t.Fatalf("unexpected err=%v", err)
}
if d != "/" {
t.Fatalf("unexpected d=%s", d)
}
if f != "" {
t.Fatalf("unexpected f=%s", f)
}
}
func TestCleanedAbs_3(t *testing.T) {
fSys, testDir := makeTestDir(t)
defer os.RemoveAll(testDir)
err := fSys.WriteFile(
filepath.Join(testDir, "foo"), []byte(`foo`))
if err != nil {
t.Fatalf("unexpected err=%v", err)
}
d, f, err := fSys.CleanedAbs(filepath.Join(testDir, "foo"))
if err != nil {
t.Fatalf("unexpected err=%v", err)
}
if d.String() != testDir {
t.Fatalf("unexpected d=%s", d)
}
if f != "foo" {
t.Fatalf("unexpected f=%s", f)
}
}
func TestCleanedAbs_4(t *testing.T) {
fSys, testDir := makeTestDir(t)
defer os.RemoveAll(testDir)
err := fSys.MkdirAll(filepath.Join(testDir, "d1", "d2"))
if err != nil {
t.Fatalf("unexpected err=%v", err)
}
err = fSys.WriteFile(
filepath.Join(testDir, "d1", "d2", "bar"),
[]byte(`bar`))
if err != nil {
t.Fatalf("unexpected err=%v", err)
}
d, f, err := fSys.CleanedAbs(
filepath.Join(testDir, "d1", "d2"))
if err != nil {
t.Fatalf("unexpected err=%v", err)
}
if d.String() != filepath.Join(testDir, "d1", "d2") {
t.Fatalf("unexpected d=%s", d)
}
if f != "" {
t.Fatalf("unexpected f=%s", f)
}
d, f, err = fSys.CleanedAbs(
filepath.Join(testDir, "d1", "d2", "bar"))
if err != nil {
t.Fatalf("unexpected err=%v", err)
}
if d.String() != filepath.Join(testDir, "d1", "d2") {
t.Fatalf("unexpected d=%s", d)
}
if f != "bar" {
t.Fatalf("unexpected f=%s", f)
}
}
func TestReadFilesRealFS(t *testing.T) {
fSys, testDir := makeTestDir(t)
defer os.RemoveAll(testDir)
err := fSys.WriteFile(path.Join(testDir, "foo"), []byte(`foo`))
if err != nil {
t.Fatalf("unexpected error %s", err)
}
if !fSys.Exists(path.Join(testDir, "foo")) {
t.Fatalf("expected foo")
}
if fSys.IsDir(path.Join(testDir, "foo")) {
t.Fatalf("expected foo not to be a directory")
}
err = fSys.WriteFile(path.Join(testDir, "bar"), []byte(`bar`))
if err != nil {
t.Fatalf("unexpected error %s", err)
}
files, err := fSys.Glob(path.Join("testDir", "*"))
expected := []string{
path.Join(testDir, "bar"),
path.Join(testDir, "foo"),
}
if err != nil {
t.Fatalf("expected no error")
}
if reflect.DeepEqual(files, expected) {
t.Fatalf("incorrect files found by glob: %v", files)
}
}

12
api/filesys/rpath.go Normal file
View File

@@ -0,0 +1,12 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filesys
import "path/filepath"
// RootedPath returns a rooted path, e.g. "/foo/bar" as
// opposed to "foo/bar".
func RootedPath(elem ...string) string {
return separator + filepath.Join(elem...)
}

52
api/hasher/hasher.go Normal file
View File

@@ -0,0 +1,52 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package hasher
import (
"crypto/sha256"
"encoding/json"
"fmt"
"sort"
)
// SortArrayAndComputeHash sorts a string array and
// returns a hash for it
func SortArrayAndComputeHash(s []string) (string, error) {
sort.Strings(s)
data, err := json.Marshal(s)
if err != nil {
return "", err
}
return Encode(Hash(string(data)))
}
// Copied from https://github.com/kubernetes/kubernetes
// /blob/master/pkg/kubectl/util/hash/hash.go
func Encode(hex string) (string, error) {
if len(hex) < 10 {
return "", fmt.Errorf(
"input length must be at least 10")
}
enc := []rune(hex[:10])
for i := range enc {
switch enc[i] {
case '0':
enc[i] = 'g'
case '1':
enc[i] = 'h'
case '3':
enc[i] = 'k'
case 'a':
enc[i] = 'm'
case 'e':
enc[i] = 't'
}
}
return string(enc), nil
}
// Hash returns the hex form of the sha256 of the argument.
func Hash(data string) string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(data)))
}

41
api/hasher/hasher_test.go Normal file
View File

@@ -0,0 +1,41 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package hasher_test
import (
"testing"
. "sigs.k8s.io/kustomize/v3/api/hasher"
)
func TestSortArrayAndComputeHash(t *testing.T) {
array1 := []string{"a", "b", "c", "d"}
array2 := []string{"c", "b", "d", "a"}
h1, err := SortArrayAndComputeHash(array1)
if err != nil {
t.Errorf("unexpected error %v", err)
}
if h1 == "" {
t.Errorf("failed to hash %v", array1)
}
h2, err := SortArrayAndComputeHash(array2)
if err != nil {
t.Errorf("unexpected error %v", err)
}
if h2 == "" {
t.Errorf("failed to hash %v", array2)
}
if h1 != h2 {
t.Errorf("hash is not consistent with reordered list: %s %s", h1, h2)
}
}
func TestHash(t *testing.T) {
// hash the empty string to be sure that sha256 is being used
expect := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
sum := Hash("")
if expect != sum {
t.Errorf("expected hash %q but got %q", expect, sum)
}
}

View File

@@ -0,0 +1,21 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// A dummy main to help with releasing the kustomize API module.
package main
import (
"fmt"
"os"
"sigs.k8s.io/kustomize/v3/api/provenance"
)
// TODO: delete this when we find a better way to generate release notes.
func main() {
fmt.Println(`
This 'main' exists only to make goreleaser create release notes for the API.
See https://github.com/goreleaser/goreleaser/issues/981
and https://github.com/kubernetes-sigs/kustomize/tree/master/releasing`)
provenance.GetProvenance().Print(os.Stdout, false)
}

View File

@@ -0,0 +1,260 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kusttest_test
import (
"fmt"
"path/filepath"
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/internal/loadertest"
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
"sigs.k8s.io/kustomize/v3/k8sdeps/transformer"
"sigs.k8s.io/kustomize/v3/pkg/loader"
"sigs.k8s.io/kustomize/v3/pkg/pgmconfig"
"sigs.k8s.io/kustomize/v3/pkg/plugins"
"sigs.k8s.io/kustomize/v3/pkg/resmap"
"sigs.k8s.io/kustomize/v3/pkg/resource"
"sigs.k8s.io/kustomize/v3/pkg/target"
"sigs.k8s.io/kustomize/v3/pkg/transformers/config/defaultconfig"
"sigs.k8s.io/kustomize/v3/pkg/validators"
)
// KustTestHarness is an environment for running a kustomize build,
// aka a run of MakeCustomizedResMap. It holds a file loader
// presumably primed with an in-memory file system, a plugin
// loader, factories to make what it needs, etc.
type KustTestHarness struct {
t *testing.T
rf *resmap.Factory
ldr loadertest.FakeLoader
pl *plugins.Loader
}
func NewKustTestHarness(t *testing.T, path string) *KustTestHarness {
return NewKustTestHarnessFull(
t, path, loader.RestrictionRootOnly, plugins.DefaultPluginConfig())
}
func NewKustTestHarnessAllowPlugins(t *testing.T, path string) *KustTestHarness {
return NewKustTestHarnessFull(
t, path, loader.RestrictionRootOnly, plugins.ActivePluginConfig())
}
func NewKustTestHarnessNoLoadRestrictor(t *testing.T, path string) *KustTestHarness {
return NewKustTestHarnessFull(
t, path, loader.RestrictionNone, plugins.DefaultPluginConfig())
}
func NewKustTestHarnessFull(
t *testing.T, path string,
lr loader.LoadRestrictorFunc, pc *types.PluginConfig) *KustTestHarness {
rf := resmap.NewFactory(resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl()), transformer.NewFactoryImpl())
return &KustTestHarness{
t: t,
rf: rf,
ldr: loadertest.NewFakeLoaderWithRestrictor(lr, path),
pl: plugins.NewLoader(pc, rf)}
}
func (th *KustTestHarness) MakeKustTarget() *target.KustTarget {
kt, err := target.NewKustTarget(
th.ldr, validators.MakeFakeValidator(), th.rf,
transformer.NewFactoryImpl(), th.pl)
if err != nil {
th.t.Fatalf("Unexpected construction error %v", err)
}
return kt
}
func (th *KustTestHarness) WriteF(dir string, content string) {
err := th.ldr.AddFile(dir, []byte(content))
if err != nil {
th.t.Fatalf("failed write to %s; %v", dir, err)
}
}
func (th *KustTestHarness) WriteK(dir string, content string) {
th.WriteF(
filepath.Join(
dir,
pgmconfig.DefaultKustomizationFileName()), `
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
`+content)
}
func (th *KustTestHarness) RF() *resource.Factory {
return th.rf.RF()
}
func (th *KustTestHarness) FromMap(m map[string]interface{}) *resource.Resource {
return th.rf.RF().FromMap(m)
}
func (th *KustTestHarness) FromMapAndOption(m map[string]interface{}, args *types.GeneratorArgs, option *types.GeneratorOptions) *resource.Resource {
return th.rf.RF().FromMapAndOption(m, args, option)
}
func (th *KustTestHarness) WriteDefaultConfigs(fName string) {
m := defaultconfig.GetDefaultFieldSpecsAsMap()
var content []byte
for _, tCfg := range m {
content = append(content, []byte(tCfg)...)
}
err := th.ldr.AddFile(fName, content)
if err != nil {
th.t.Fatalf("unable to add file %s", fName)
}
}
func (th *KustTestHarness) LoadAndRunGenerator(
config string) resmap.ResMap {
res, err := th.rf.RF().FromBytes([]byte(config))
if err != nil {
th.t.Fatalf("Err: %v", err)
}
g, err := th.pl.LoadGenerator(
th.ldr, validators.MakeFakeValidator(), res)
if err != nil {
th.t.Fatalf("Err: %v", err)
}
rm, err := g.Generate()
if err != nil {
th.t.Fatalf("Err: %v", err)
}
return rm
}
func (th *KustTestHarness) LoadAndRunTransformer(
config, input string) resmap.ResMap {
resMap, err := th.RunTransformer(config, input)
if err != nil {
th.t.Fatalf("Err: %v", err)
}
return resMap
}
func (th *KustTestHarness) ErrorFromLoadAndRunTransformer(
config, input string) error {
_, err := th.RunTransformer(config, input)
return err
}
func (th *KustTestHarness) RunTransformer(
config, input string) (resmap.ResMap, error) {
resMap, err := th.rf.NewResMapFromBytes([]byte(input))
if err != nil {
th.t.Fatalf("Err: %v", err)
}
return th.RunTransformerFromResMap(config, resMap)
}
func (th *KustTestHarness) RunTransformerFromResMap(
config string, resMap resmap.ResMap) (resmap.ResMap, error) {
transConfig, err := th.rf.RF().FromBytes([]byte(config))
if err != nil {
th.t.Fatalf("Err: %v", err)
}
g, err := th.pl.LoadTransformer(
th.ldr, validators.MakeFakeValidator(), transConfig)
if err != nil {
return nil, err
}
err = g.Transform(resMap)
return resMap, err
}
func tabToSpace(input string) string {
var result []string
for _, i := range input {
if i == 9 {
result = append(result, " ")
} else {
result = append(result, string(i))
}
}
return strings.Join(result, "")
}
func convertToArray(x string) ([]string, int) {
a := strings.Split(strings.TrimSuffix(x, "\n"), "\n")
maxLen := 0
for i, v := range a {
z := tabToSpace(v)
if len(z) > maxLen {
maxLen = len(z)
}
a[i] = z
}
return a, maxLen
}
func hint(a, b string) string {
if a == b {
return " "
}
return "X"
}
func (th *KustTestHarness) AssertActualEqualsExpected(
m resmap.ResMap, expected string) {
th.AssertActualEqualsExpectedWithTweak(m, nil, expected)
}
func (th *KustTestHarness) AssertActualEqualsExpectedWithTweak(
m resmap.ResMap, tweaker func([]byte) []byte, expected string) {
if m == nil {
th.t.Fatalf("Map should not be nil.")
}
// Ignore leading linefeed in expected value
// to ease readability of tests.
if len(expected) > 0 && expected[0] == 10 {
expected = expected[1:]
}
actual, err := m.AsYaml()
if err != nil {
th.t.Fatalf("Unexpected err: %v", err)
}
if tweaker != nil {
actual = tweaker(actual)
}
if string(actual) != expected {
th.reportDiffAndFail(actual, expected)
}
}
// Pretty printing of file differences.
func (th *KustTestHarness) reportDiffAndFail(actual []byte, expected string) {
sE, maxLen := convertToArray(expected)
sA, _ := convertToArray(string(actual))
fmt.Println("===== ACTUAL BEGIN ========================================")
fmt.Print(string(actual))
fmt.Println("===== ACTUAL END ==========================================")
format := fmt.Sprintf("%%s %%-%ds %%s\n", maxLen+4)
limit := 0
if len(sE) < len(sA) {
limit = len(sE)
} else {
limit = len(sA)
}
fmt.Printf(format, " ", "EXPECTED", "ACTUAL")
fmt.Printf(format, " ", "--------", "------")
for i := 0; i < limit; i++ {
fmt.Printf(format, hint(sE[i], sA[i]), sE[i], sA[i])
}
if len(sE) < len(sA) {
for i := len(sE); i < len(sA); i++ {
fmt.Printf(format, "X", "", sA[i])
}
} else {
for i := len(sA); i < len(sE); i++ {
fmt.Printf(format, "X", sE[i], "")
}
}
th.t.Fatalf("Expected not equal to actual")
}

View File

@@ -0,0 +1,112 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kusttest_test
import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/pkg/pgmconfig"
"sigs.k8s.io/kustomize/v3/pkg/plugins"
)
// PluginTestEnv manages the plugin test environment.
// It sets/resets XDG_CONFIG_HOME, makes/removes a temp objRoot,
// manages a plugin compiler, etc.
type PluginTestEnv struct {
t *testing.T
compiler *plugins.Compiler
workDir string
oldXdg string
wasSet bool
}
func NewPluginTestEnv(t *testing.T) *PluginTestEnv {
return &PluginTestEnv{t: t}
}
func (x *PluginTestEnv) Set() *PluginTestEnv {
x.createWorkDir()
x.compiler = x.makeCompiler()
x.setEnv()
return x
}
func (x *PluginTestEnv) Reset() {
x.resetEnv()
x.removeWorkDir()
}
func (x *PluginTestEnv) BuildGoPlugin(g, v, k string) {
err := x.compiler.Compile(g, v, k)
if err != nil {
x.t.Errorf("compile failed: %v", err)
}
}
func (x *PluginTestEnv) BuildExecPlugin(g, v, k string) {
lowK := strings.ToLower(k)
obj := filepath.Join(x.compiler.ObjRoot(), g, v, lowK, k)
src := filepath.Join(x.compiler.SrcRoot(), g, v, lowK, k)
if err := os.MkdirAll(filepath.Dir(obj), 0755); err != nil {
x.t.Errorf("error making directory: %s", filepath.Dir(obj))
}
cmd := exec.Command("cp", src, obj)
cmd.Env = os.Environ()
if err := cmd.Run(); err != nil {
x.t.Errorf("error copying %s to %s: %v", src, obj, err)
}
}
func (x *PluginTestEnv) makeCompiler() *plugins.Compiler {
// The plugin loader wants to find object code under
// $XDG_CONFIG_HOME/kustomize/plugins
// and the compiler writes object code to
// $objRoot
// so set things up accordingly.
objRoot := filepath.Join(
x.workDir, pgmconfig.ProgramName, pgmconfig.PluginRoot)
err := os.MkdirAll(objRoot, os.ModePerm)
if err != nil {
x.t.Error(err)
}
srcRoot, err := plugins.DefaultSrcRoot()
if err != nil {
x.t.Error(err)
}
return plugins.NewCompiler(srcRoot, objRoot)
}
func (x *PluginTestEnv) createWorkDir() {
var err error
x.workDir, err = ioutil.TempDir("", "kustomize-plugin-tests")
if err != nil {
x.t.Errorf("failed to make work dir: %v", err)
}
}
func (x *PluginTestEnv) removeWorkDir() {
err := os.RemoveAll(x.workDir)
if err != nil {
x.t.Errorf(
"removing work dir: %s %v", x.workDir, err)
}
}
func (x *PluginTestEnv) setEnv() {
x.oldXdg, x.wasSet = os.LookupEnv(pgmconfig.XdgConfigHome)
os.Setenv(pgmconfig.XdgConfigHome, x.workDir)
}
func (x *PluginTestEnv) resetEnv() {
if x.wasSet {
os.Setenv(pgmconfig.XdgConfigHome, x.oldXdg)
} else {
os.Unsetenv(pgmconfig.XdgConfigHome)
}
}

213
api/kv/kv.go Normal file
View File

@@ -0,0 +1,213 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kv
import (
"bufio"
"bytes"
"fmt"
"os"
"path"
"strings"
"unicode"
"unicode/utf8"
"github.com/pkg/errors"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/pkg/ifc"
)
var utf8bom = []byte{0xEF, 0xBB, 0xBF}
// loader reads and validates KV pairs.
type loader struct {
// Used to read the filesystem.
ldr ifc.Loader
// Used to validate various k8s data fields.
validator ifc.Validator
}
func NewLoader(ldr ifc.Loader, v ifc.Validator) ifc.KvLoader {
return &loader{ldr: ldr, validator: v}
}
func (kvl *loader) Validator() ifc.Validator {
return kvl.validator
}
func (kvl *loader) Load(
args types.KvPairSources) (all []types.Pair, err error) {
pairs, err := kvl.keyValuesFromEnvFiles(args.EnvSources)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf(
"env source files: %v",
args.EnvSources))
}
all = append(all, pairs...)
pairs, err = keyValuesFromLiteralSources(args.LiteralSources)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf(
"literal sources %v", args.LiteralSources))
}
all = append(all, pairs...)
pairs, err = kvl.keyValuesFromFileSources(args.FileSources)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf(
"file sources: %v", args.FileSources))
}
return append(all, pairs...), nil
}
func keyValuesFromLiteralSources(sources []string) ([]types.Pair, error) {
var kvs []types.Pair
for _, s := range sources {
k, v, err := parseLiteralSource(s)
if err != nil {
return nil, err
}
kvs = append(kvs, types.Pair{Key: k, Value: v})
}
return kvs, nil
}
func (kvl *loader) keyValuesFromFileSources(sources []string) ([]types.Pair, error) {
var kvs []types.Pair
for _, s := range sources {
k, fPath, err := parseFileSource(s)
if err != nil {
return nil, err
}
content, err := kvl.ldr.Load(fPath)
if err != nil {
return nil, err
}
kvs = append(kvs, types.Pair{Key: k, Value: string(content)})
}
return kvs, nil
}
func (kvl *loader) keyValuesFromEnvFiles(paths []string) ([]types.Pair, error) {
var kvs []types.Pair
for _, p := range paths {
content, err := kvl.ldr.Load(p)
if err != nil {
return nil, err
}
more, err := kvl.keyValuesFromLines(content)
if err != nil {
return nil, err
}
kvs = append(kvs, more...)
}
return kvs, nil
}
// keyValuesFromLines parses given content in to a list of key-value pairs.
func (kvl *loader) keyValuesFromLines(content []byte) ([]types.Pair, error) {
var kvs []types.Pair
scanner := bufio.NewScanner(bytes.NewReader(content))
currentLine := 0
for scanner.Scan() {
// Process the current line, retrieving a key/value pair if
// possible.
scannedBytes := scanner.Bytes()
kv, err := kvl.keyValuesFromLine(scannedBytes, currentLine)
if err != nil {
return nil, err
}
currentLine++
if len(kv.Key) == 0 {
// no key means line was empty or a comment
continue
}
kvs = append(kvs, kv)
}
return kvs, nil
}
// KeyValuesFromLine returns a kv with blank key if the line is empty or a comment.
// The value will be retrieved from the environment if necessary.
func (kvl *loader) keyValuesFromLine(line []byte, currentLine int) (types.Pair, error) {
kv := types.Pair{}
if !utf8.Valid(line) {
return kv, fmt.Errorf("line %d has invalid utf8 bytes : %v", line, string(line))
}
// We trim UTF8 BOM from the first line of the file but no others
if currentLine == 0 {
line = bytes.TrimPrefix(line, utf8bom)
}
// trim the line from all leading whitespace first
line = bytes.TrimLeftFunc(line, unicode.IsSpace)
// If the line is empty or a comment, we return a blank key/value pair.
if len(line) == 0 || line[0] == '#' {
return kv, nil
}
data := strings.SplitN(string(line), "=", 2)
key := data[0]
if err := kvl.validator.IsEnvVarName(key); err != nil {
return kv, err
}
if len(data) == 2 {
kv.Value = data[1]
} else {
// No value (no `=` in the line) is a signal to obtain the value
// from the environment.
kv.Value = os.Getenv(key)
}
kv.Key = key
return kv, nil
}
// ParseFileSource parses the source given.
//
// Acceptable formats include:
// 1. source-path: the basename will become the key name
// 2. source-name=source-path: the source-name will become the key name and
// source-path is the path to the key file.
//
// Key names cannot include '='.
func parseFileSource(source string) (keyName, filePath string, err error) {
numSeparators := strings.Count(source, "=")
switch {
case numSeparators == 0:
return path.Base(source), source, nil
case numSeparators == 1 && strings.HasPrefix(source, "="):
return "", "", fmt.Errorf("key name for file path %v missing", strings.TrimPrefix(source, "="))
case numSeparators == 1 && strings.HasSuffix(source, "="):
return "", "", fmt.Errorf("file path for key name %v missing", strings.TrimSuffix(source, "="))
case numSeparators > 1:
return "", "", errors.New("key names or file paths cannot contain '='")
default:
components := strings.Split(source, "=")
return components[0], components[1], nil
}
}
// ParseLiteralSource parses the source key=val pair into its component pieces.
// This functionality is distinguished from strings.SplitN(source, "=", 2) since
// it returns an error in the case of empty keys, values, or a missing equals sign.
func parseLiteralSource(source string) (keyName, value string, err error) {
// leading equal is invalid
if strings.Index(source, "=") == 0 {
return "", "", fmt.Errorf("invalid literal source %v, expected key=value", source)
}
// split after the first equal (so values can have the = character)
items := strings.SplitN(source, "=", 2)
if len(items) != 2 {
return "", "", fmt.Errorf("invalid literal source %v, expected key=value", source)
}
return items[0], strings.Trim(items[1], "\"'"), nil
}

97
api/kv/kv_test.go Normal file
View File

@@ -0,0 +1,97 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kv
import (
"reflect"
"testing"
"sigs.k8s.io/kustomize/v3/api/filesys"
"sigs.k8s.io/kustomize/v3/api/types"
ldr "sigs.k8s.io/kustomize/v3/pkg/loader"
"sigs.k8s.io/kustomize/v3/pkg/validators"
)
func makeKvLoader(fSys filesys.FileSystem) *loader {
return &loader{
ldr: ldr.NewFileLoaderAtRoot(fSys),
validator: validators.MakeFakeValidator()}
}
func TestKeyValuesFromLines(t *testing.T) {
tests := []struct {
desc string
content string
expectedPairs []types.Pair
expectedErr bool
}{
{
desc: "valid kv content parse",
content: `
k1=v1
k2=v2
`,
expectedPairs: []types.Pair{
{Key: "k1", Value: "v1"},
{Key: "k2", Value: "v2"},
},
expectedErr: false,
},
{
desc: "content with comments",
content: `
k1=v1
#k2=v2
`,
expectedPairs: []types.Pair{
{Key: "k1", Value: "v1"},
},
expectedErr: false,
},
// TODO: add negative testcases
}
kvl := makeKvLoader(filesys.MakeFsInMemory())
for _, test := range tests {
pairs, err := kvl.keyValuesFromLines([]byte(test.content))
if test.expectedErr && err == nil {
t.Fatalf("%s should not return error", test.desc)
}
if !reflect.DeepEqual(pairs, test.expectedPairs) {
t.Errorf("%s should succeed, got:%v exptected:%v", test.desc, pairs, test.expectedPairs)
}
}
}
func TestKeyValuesFromFileSources(t *testing.T) {
tests := []struct {
description string
sources []string
expected []types.Pair
}{
{
description: "create kvs from file sources",
sources: []string{"files/app-init.ini"},
expected: []types.Pair{
{
Key: "app-init.ini",
Value: "FOO=bar",
},
},
},
}
fSys := filesys.MakeFsInMemory()
fSys.WriteFile("/files/app-init.ini", []byte("FOO=bar"))
kvl := makeKvLoader(fSys)
for _, tc := range tests {
kvs, err := kvl.keyValuesFromFileSources(tc.sources)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(kvs, tc.expected) {
t.Fatalf("in testcase: %q updated:\n%#v\ndoesn't match expected:\n%#v\n", tc.description, kvs, tc.expected)
}
}
}

View File

@@ -0,0 +1,54 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package provenance
import (
"fmt"
"io"
"runtime"
)
var (
version = "unknown"
// sha1 from git, output of $(git rev-parse HEAD)
gitCommit = "$Format:%H$"
// build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ')
buildDate = "1970-01-01T00:00:00Z"
goos = runtime.GOOS
goarch = runtime.GOARCH
)
// Provenance holds information about the build of an executable.
type Provenance struct {
// Version of the kustomize binary.
Version string `json:"version"`
// GitCommit is a git commit
GitCommit string `json:"gitCommit"`
// BuildDate is date of the build.
BuildDate string `json:"buildDate"`
// GoOs holds OS name.
GoOs string `json:"goOs"`
// GoArch holds architecture name.
GoArch string `json:"goArch"`
}
// GetProvenance returns an instance of Provenance.
func GetProvenance() Provenance {
return Provenance{
version,
gitCommit,
buildDate,
goos,
goarch,
}
}
// Print prints provenance info.
func (v Provenance) Print(w io.Writer, short bool) {
if short {
fmt.Fprintf(w, "%s\n", v.Version)
} else {
fmt.Fprintf(w, "Version: %+v\n", v)
}
}

200
api/resid/gvk.go Normal file
View File

@@ -0,0 +1,200 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package resid
import (
"strings"
)
// Gvk identifies a Kubernetes API type.
// https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
type Gvk struct {
Group string `json:"group,omitempty" yaml:"group,omitempty"`
Version string `json:"version,omitempty" yaml:"version,omitempty"`
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
}
// FromKind makes a Gvk with only the kind specified.
func FromKind(k string) Gvk {
return Gvk{
Kind: k,
}
}
// GvkFromString makes a Gvk with a string,
// which is constructed by String() function
func GvkFromString(s string) Gvk {
values := strings.Split(s, fieldSep)
g := values[0]
if g == noGroup {
g = ""
}
v := values[1]
if v == noVersion {
v = ""
}
k := values[2]
if k == noKind {
k = ""
}
return Gvk{
Group: g,
Version: v,
Kind: k,
}
}
// Values that are brief but meaningful in logs.
const (
noGroup = "~G"
noVersion = "~V"
noKind = "~K"
fieldSep = "_"
)
// String returns a string representation of the GVK.
func (x Gvk) String() string {
g := x.Group
if g == "" {
g = noGroup
}
v := x.Version
if v == "" {
v = noVersion
}
k := x.Kind
if k == "" {
k = noKind
}
return strings.Join([]string{g, v, k}, fieldSep)
}
// Equals returns true if the Gvk's have equal fields.
func (x Gvk) Equals(o Gvk) bool {
return x.Group == o.Group && x.Version == o.Version && x.Kind == o.Kind
}
// An attempt to order things to help k8s, e.g.
// a Service should come before things that refer to it.
// Namespace should be first.
// In some cases order just specified to provide determinism.
var orderFirst = []string{
"Namespace",
"ResourceQuota",
"StorageClass",
"CustomResourceDefinition",
"MutatingWebhookConfiguration",
"ServiceAccount",
"PodSecurityPolicy",
"Role",
"ClusterRole",
"RoleBinding",
"ClusterRoleBinding",
"ConfigMap",
"Secret",
"Service",
"LimitRange",
"PriorityClass",
"Deployment",
"StatefulSet",
"CronJob",
"PodDisruptionBudget",
}
var orderLast = []string{
"ValidatingWebhookConfiguration",
}
var typeOrders = func() map[string]int {
m := map[string]int{}
for i, n := range orderFirst {
m[n] = -len(orderFirst) + i
}
for i, n := range orderLast {
m[n] = 1 + i
}
return m
}()
// IsLessThan returns true if self is less than the argument.
func (x Gvk) IsLessThan(o Gvk) bool {
indexI := typeOrders[x.Kind]
indexJ := typeOrders[o.Kind]
if indexI != indexJ {
return indexI < indexJ
}
return x.String() < o.String()
}
// IsSelected returns true if `selector` selects `x`; otherwise, false.
// If `selector` and `x` are the same, return true.
// If `selector` is nil, it is considered a wildcard match, returning true.
// If selector fields are empty, they are considered wildcards matching
// anything in the corresponding fields, e.g.
//
// this item:
// <Group: "extensions", Version: "v1beta1", Kind: "Deployment">
//
// is selected by
// <Group: "", Version: "", Kind: "Deployment">
//
// but rejected by
// <Group: "apps", Version: "", Kind: "Deployment">
//
func (x Gvk) IsSelected(selector *Gvk) bool {
if selector == nil {
return true
}
if len(selector.Group) > 0 {
if x.Group != selector.Group {
return false
}
}
if len(selector.Version) > 0 {
if x.Version != selector.Version {
return false
}
}
if len(selector.Kind) > 0 {
if x.Kind != selector.Kind {
return false
}
}
return true
}
var notNamespaceableKinds = []string{
"APIService",
"CSIDriver",
"CSINode",
"CertificateSigningRequest",
"ClusterRole",
"ClusterRoleBinding",
"ComponentStatus",
"CustomResourceDefinition",
"MutatingWebhookConfiguration",
"Namespace",
"Node",
"PersistentVolume",
"PodSecurityPolicy",
"PodSecurityPolicy",
"PriorityClass",
"RuntimeClass",
"SelfSubjectAccessReview",
"SelfSubjectRulesReview",
"StorageClass",
"SubjectAccessReview",
"TokenReview",
"ValidatingWebhookConfiguration",
"VolumeAttachment",
}
// IsNamespaceableKind returns true if x is a namespaceable Gvk
// Implements https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/#not-all-objects-are-in-a-namespace
func (x Gvk) IsNamespaceableKind() bool {
for _, k := range notNamespaceableKinds {
if k == x.Kind {
return false
}
}
return true
}

218
api/resid/gvk_test.go Normal file
View File

@@ -0,0 +1,218 @@
/*
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 resid
import (
"testing"
)
var equalsTests = []struct {
x1 Gvk
x2 Gvk
}{
{Gvk{Group: "a", Version: "b", Kind: "c"},
Gvk{Group: "a", Version: "b", Kind: "c"}},
{Gvk{Version: "b", Kind: "c"},
Gvk{Version: "b", Kind: "c"}},
{Gvk{Kind: "c"},
Gvk{Kind: "c"}},
}
func TestEquals(t *testing.T) {
for _, hey := range equalsTests {
if !hey.x1.Equals(hey.x2) {
t.Fatalf("%v should equal %v", hey.x1, hey.x2)
}
}
}
var lessThanTests = []struct {
x1 Gvk
x2 Gvk
}{
{Gvk{Group: "a", Version: "b", Kind: "CustomResourceDefinition"},
Gvk{Group: "a", Version: "b", Kind: "RoleBinding"}},
{Gvk{Group: "a", Version: "b", Kind: "Namespace"},
Gvk{Group: "a", Version: "b", Kind: "ClusterRole"}},
{Gvk{Group: "a", Version: "b", Kind: "a"},
Gvk{Group: "a", Version: "b", Kind: "b"}},
{Gvk{Group: "a", Version: "b", Kind: "Namespace"},
Gvk{Group: "a", Version: "c", Kind: "Namespace"}},
{Gvk{Group: "a", Version: "c", Kind: "Namespace"},
Gvk{Group: "b", Version: "c", Kind: "Namespace"}},
{Gvk{Group: "b", Version: "c", Kind: "Namespace"},
Gvk{Group: "a", Version: "c", Kind: "ClusterRole"}},
{Gvk{Group: "a", Version: "c", Kind: "Namespace"},
Gvk{Group: "a", Version: "b", Kind: "ClusterRole"}},
{Gvk{Group: "a", Version: "d", Kind: "Namespace"},
Gvk{Group: "b", Version: "c", Kind: "Namespace"}},
{Gvk{Group: "a", Version: "b", Kind: orderFirst[len(orderFirst)-1]},
Gvk{Group: "a", Version: "b", Kind: orderLast[0]}},
{Gvk{Group: "a", Version: "b", Kind: orderFirst[len(orderFirst)-1]},
Gvk{Group: "a", Version: "b", Kind: "CustomKindX"}},
{Gvk{Group: "a", Version: "b", Kind: "CustomKindX"},
Gvk{Group: "a", Version: "b", Kind: orderLast[0]}},
{Gvk{Group: "a", Version: "b", Kind: "CustomKindA"},
Gvk{Group: "a", Version: "b", Kind: "CustomKindB"}},
{Gvk{Group: "a", Version: "b", Kind: "CustomKindX"},
Gvk{Group: "a", Version: "b", Kind: "ValidatingWebhookConfiguration"}},
{Gvk{Group: "a", Version: "b", Kind: "APIService"},
Gvk{Group: "a", Version: "b", Kind: "ValidatingWebhookConfiguration"}},
{Gvk{Group: "a", Version: "b", Kind: "Service"},
Gvk{Group: "a", Version: "b", Kind: "APIService"}},
}
func TestIsLessThan1(t *testing.T) {
for _, hey := range lessThanTests {
if !hey.x1.IsLessThan(hey.x2) {
t.Fatalf("%v should be less than %v", hey.x1, hey.x2)
}
if hey.x2.IsLessThan(hey.x1) {
t.Fatalf("%v should not be less than %v", hey.x2, hey.x1)
}
}
}
var stringTests = []struct {
x Gvk
s string
}{
{Gvk{}, "~G_~V_~K"},
{Gvk{Kind: "k"}, "~G_~V_k"},
{Gvk{Version: "v"}, "~G_v_~K"},
{Gvk{Version: "v", Kind: "k"}, "~G_v_k"},
{Gvk{Group: "g"}, "g_~V_~K"},
{Gvk{Group: "g", Kind: "k"}, "g_~V_k"},
{Gvk{Group: "g", Version: "v"}, "g_v_~K"},
{Gvk{Group: "g", Version: "v", Kind: "k"}, "g_v_k"},
}
func TestString(t *testing.T) {
for _, hey := range stringTests {
if hey.x.String() != hey.s {
t.Fatalf("bad string for %v '%s'", hey.x, hey.s)
}
}
}
func TestSelectByGVK(t *testing.T) {
type testCase struct {
description string
in Gvk
filter *Gvk
expected bool
}
testCases := []testCase{
{
description: "nil filter",
in: Gvk{},
filter: nil,
expected: true,
},
{
description: "gvk matches",
in: Gvk{
Group: "group1",
Version: "version1",
Kind: "kind1",
},
filter: &Gvk{
Group: "group1",
Version: "version1",
Kind: "kind1",
},
expected: true,
},
{
description: "group doesn't matches",
in: Gvk{
Group: "group1",
Version: "version1",
Kind: "kind1",
},
filter: &Gvk{
Group: "group2",
Version: "version1",
Kind: "kind1",
},
expected: false,
},
{
description: "version doesn't matches",
in: Gvk{
Group: "group1",
Version: "version1",
Kind: "kind1",
},
filter: &Gvk{
Group: "group1",
Version: "version2",
Kind: "kind1",
},
expected: false,
},
{
description: "kind doesn't matches",
in: Gvk{
Group: "group1",
Version: "version1",
Kind: "kind1",
},
filter: &Gvk{
Group: "group1",
Version: "version1",
Kind: "kind2",
},
expected: false,
},
{
description: "no version in filter",
in: Gvk{
Group: "group1",
Version: "version1",
Kind: "kind1",
},
filter: &Gvk{
Group: "group1",
Version: "",
Kind: "kind1",
},
expected: true,
},
{
description: "only kind is set in filter",
in: Gvk{
Group: "group1",
Version: "version1",
Kind: "kind1",
},
filter: &Gvk{
Group: "",
Version: "",
Kind: "kind1",
},
expected: true,
},
}
for _, tc := range testCases {
filtered := tc.in.IsSelected(tc.filter)
if filtered != tc.expected {
t.Fatalf("unexpected filter result for test case: %v", tc.description)
}
}
}

127
api/resid/resid.go Normal file
View File

@@ -0,0 +1,127 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package resid
import (
"strings"
)
// ResId is an identifier of a k8s resource object.
type ResId struct {
// Gvk of the resource.
Gvk `json:",inline,omitempty" yaml:",inline,omitempty"`
// Name of the resource before transformation.
Name string `json:"name,omitempty" yaml:"name,omitempty"`
// Namespace the resource belongs to.
// An untransformed resource has no namespace.
// A fully transformed resource has the namespace
// from the top most overlay.
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
}
// NewResIdWithNamespace creates new ResId
// in a given namespace.
func NewResIdWithNamespace(k Gvk, n, ns string) ResId {
return ResId{Gvk: k, Name: n, Namespace: ns}
}
// NewResId creates new ResId.
func NewResId(k Gvk, n string) ResId {
return ResId{Gvk: k, Name: n}
}
// NewResIdKindOnly creates a new ResId.
func NewResIdKindOnly(k string, n string) ResId {
return ResId{Gvk: FromKind(k), Name: n}
}
const (
noNamespace = "~X"
noName = "~N"
separator = "|"
TotallyNotANamespace = "_non_namespaceable_"
DefaultNamespace = "default"
)
// String of ResId based on GVK, name and prefix
func (id ResId) String() string {
ns := id.Namespace
if ns == "" {
ns = noNamespace
}
nm := id.Name
if nm == "" {
nm = noName
}
return strings.Join(
[]string{id.Gvk.String(), ns, nm}, separator)
}
func FromString(s string) ResId {
values := strings.Split(s, separator)
g := GvkFromString(values[0])
ns := values[1]
if ns == noNamespace {
ns = ""
}
nm := values[2]
if nm == noName {
nm = ""
}
return ResId{
Gvk: g,
Namespace: ns,
Name: nm,
}
}
// GvknString of ResId based on GVK and name
func (id ResId) GvknString() string {
return id.Gvk.String() + separator + id.Name
}
// GvknEquals returns true if the other id matches
// Group/Version/Kind/name.
func (id ResId) GvknEquals(o ResId) bool {
return id.Name == o.Name && id.Gvk.Equals(o.Gvk)
}
// Equals returns true if the other id matches
// namespace/Group/Version/Kind/name.
func (id ResId) Equals(o ResId) bool {
return id.IsNsEquals(o) && id.GvknEquals(o)
}
// IsNsEquals returns true if the id is in
// the same effective namespace.
func (id ResId) IsNsEquals(o ResId) bool {
return id.EffectiveNamespace() == o.EffectiveNamespace()
}
// IsInDefaultNs returns true if id is a namespaceable
// ResId and the Namespace is either not set or set
// to DefaultNamespace.
func (id ResId) IsInDefaultNs() bool {
return id.IsNamespaceableKind() && id.isPutativelyDefaultNs()
}
func (id ResId) isPutativelyDefaultNs() bool {
return id.Namespace == "" || id.Namespace == DefaultNamespace
}
// EffectiveNamespace returns a non-ambiguous, non-empty
// namespace for use in reporting and equality tests.
func (id ResId) EffectiveNamespace() string {
// The order of these checks matters.
if !id.IsNamespaceableKind() {
return TotallyNotANamespace
}
if id.isPutativelyDefaultNs() {
return DefaultNamespace
}
return id.Namespace
}

423
api/resid/resid_test.go Normal file
View File

@@ -0,0 +1,423 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package resid
import (
"testing"
)
var resIdStringTests = []struct {
x ResId
s string
}{
{
ResId{
Namespace: "ns",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
"g_v_k|ns|nm",
},
{
ResId{
Namespace: "ns",
Gvk: Gvk{Version: "v", Kind: "k"},
Name: "nm",
},
"~G_v_k|ns|nm",
},
{
ResId{
Namespace: "ns",
Gvk: Gvk{Kind: "k"},
Name: "nm",
},
"~G_~V_k|ns|nm",
},
{
ResId{
Namespace: "ns",
Gvk: Gvk{},
Name: "nm",
},
"~G_~V_~K|ns|nm",
},
{
ResId{
Gvk: Gvk{},
Name: "nm",
},
"~G_~V_~K|~X|nm",
},
{
ResId{
Gvk: Gvk{},
Name: "nm",
},
"~G_~V_~K|~X|nm",
},
{
ResId{
Gvk: Gvk{},
},
"~G_~V_~K|~X|~N",
},
{
ResId{
Gvk: Gvk{},
},
"~G_~V_~K|~X|~N",
},
{
ResId{},
"~G_~V_~K|~X|~N",
},
}
func TestResIdString(t *testing.T) {
for _, hey := range resIdStringTests {
if hey.x.String() != hey.s {
t.Fatalf("Actual: %v, Expected: '%s'", hey.x, hey.s)
}
}
}
var gvknStringTests = []struct {
x ResId
s string
}{
{
ResId{
Namespace: "ns",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
"g_v_k|nm",
},
{
ResId{
Namespace: "ns",
Gvk: Gvk{Version: "v", Kind: "k"},
Name: "nm",
},
"~G_v_k|nm",
},
{
ResId{
Namespace: "ns",
Gvk: Gvk{Kind: "k"},
Name: "nm",
},
"~G_~V_k|nm",
},
{
ResId{
Namespace: "ns",
Gvk: Gvk{},
Name: "nm",
},
"~G_~V_~K|nm",
},
{
ResId{
Gvk: Gvk{},
Name: "nm",
},
"~G_~V_~K|nm",
},
{
ResId{
Gvk: Gvk{},
Name: "nm",
},
"~G_~V_~K|nm",
},
{
ResId{
Gvk: Gvk{},
},
"~G_~V_~K|",
},
{
ResId{
Gvk: Gvk{},
},
"~G_~V_~K|",
},
{
ResId{},
"~G_~V_~K|",
},
}
func TestGvknString(t *testing.T) {
for _, hey := range gvknStringTests {
if hey.x.GvknString() != hey.s {
t.Fatalf("Actual: %s, Expected: '%s'", hey.x.GvknString(), hey.s)
}
}
}
func TestResIdEquals(t *testing.T) {
var GvknEqualsTest = []struct {
id1 ResId
id2 ResId
gVknResult bool
nsEquals bool
equals bool
}{
{
id1: ResId{
Namespace: "X",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
id2: ResId{
Namespace: "X",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
gVknResult: true,
nsEquals: true,
equals: true,
},
{
id1: ResId{
Namespace: "X",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
id2: ResId{
Namespace: "Z",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
gVknResult: true,
nsEquals: false,
equals: false,
},
{
id1: ResId{
Namespace: "X",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
id2: ResId{
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
gVknResult: true,
nsEquals: false,
equals: false,
},
{
id1: ResId{
Namespace: "X",
Gvk: Gvk{Version: "v", Kind: "k"},
Name: "nm",
},
id2: ResId{
Namespace: "Z",
Gvk: Gvk{Version: "v", Kind: "k"},
Name: "nm",
},
gVknResult: true,
nsEquals: false,
equals: false,
},
{
id1: ResId{
Namespace: "X",
Gvk: Gvk{Kind: "k"},
Name: "nm",
},
id2: ResId{
Namespace: "Z",
Gvk: Gvk{Kind: "k"},
Name: "nm",
},
gVknResult: true,
nsEquals: false,
equals: false,
},
{
id1: ResId{
Gvk: Gvk{Kind: "k"},
Name: "nm",
},
id2: ResId{
Gvk: Gvk{Kind: "k"},
Name: "nm2",
},
gVknResult: false,
nsEquals: true,
equals: false,
},
{
id1: ResId{
Gvk: Gvk{Kind: "k"},
Name: "nm",
},
id2: ResId{
Gvk: Gvk{Kind: "Node"},
Name: "nm",
},
gVknResult: false,
nsEquals: false,
equals: false,
},
{
id1: ResId{
Gvk: Gvk{Kind: "Node"},
Name: "nm1",
},
id2: ResId{
Gvk: Gvk{Kind: "Node"},
Name: "nm2",
},
gVknResult: false,
nsEquals: true,
equals: false,
},
{
id1: ResId{
Namespace: "default",
Gvk: Gvk{Kind: "k"},
Name: "nm1",
},
id2: ResId{
Gvk: Gvk{Kind: "k"},
Name: "nm2",
},
gVknResult: false,
nsEquals: true,
equals: false,
},
{
id1: ResId{
Namespace: "X",
Name: "nm",
},
id2: ResId{
Namespace: "Z",
Name: "nm",
},
gVknResult: true,
nsEquals: false,
equals: false,
},
}
for _, tst := range GvknEqualsTest {
if tst.id1.GvknEquals(tst.id2) != tst.gVknResult {
t.Fatalf("GvknEquals(\n%v,\n%v\n) should be %v",
tst.id1, tst.id2, tst.gVknResult)
}
if tst.id1.IsNsEquals(tst.id2) != tst.nsEquals {
t.Fatalf("IsNsEquals(\n%v,\n%v\n) should be %v",
tst.id1, tst.id2, tst.equals)
}
if tst.id1.Equals(tst.id2) != tst.equals {
t.Fatalf("Equals(\n%v,\n%v\n) should be %v",
tst.id1, tst.id2, tst.equals)
}
}
}
var ids = []ResId{
{
Namespace: "ns",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
{
Namespace: "ns",
Gvk: Gvk{Version: "v", Kind: "k"},
Name: "nm",
},
{
Namespace: "ns",
Gvk: Gvk{Kind: "k"},
Name: "nm",
},
{
Namespace: "ns",
Gvk: Gvk{},
Name: "nm",
},
{
Gvk: Gvk{},
Name: "nm",
},
{
Gvk: Gvk{},
Name: "nm",
},
{
Gvk: Gvk{},
},
}
func TestFromString(t *testing.T) {
for _, id := range ids {
newId := FromString(id.String())
if newId != id {
t.Fatalf("Actual: %v, Expected: '%s'", newId, id)
}
}
}
func TestEffectiveNamespace(t *testing.T) {
var test = []struct {
id ResId
expected string
}{
{
id: ResId{
Gvk: Gvk{Group: "g", Version: "v", Kind: "Node"},
Name: "nm",
},
expected: TotallyNotANamespace,
},
{
id: ResId{
Namespace: "foo",
Gvk: Gvk{Group: "g", Version: "v", Kind: "Node"},
Name: "nm",
},
expected: TotallyNotANamespace,
},
{
id: ResId{
Namespace: "foo",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
expected: "foo",
},
{
id: ResId{
Namespace: "",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
expected: DefaultNamespace,
},
{
id: ResId{
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
expected: DefaultNamespace,
},
}
for _, tst := range test {
if actual := tst.id.EffectiveNamespace(); actual != tst.expected {
t.Fatalf("EffectiveNamespace was %s, expected %s",
actual, tst.expected)
}
}
}

View File

@@ -0,0 +1,10 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
// ConfigMapArgs contains the metadata of how to generate a configmap.
type ConfigMapArgs struct {
// GeneratorArgs for the configmap.
GeneratorArgs `json:",inline,omitempty" yaml:",inline,omitempty"`
}

9
api/types/doc.go Normal file
View File

@@ -0,0 +1,9 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package types holds the definition of the kustomization struct and
// supporting structs. It's the k8s API conformant object that describes
// a set of generation and transformation operations to create and/or
// modify k8s resources.
// A kustomization file is a serialization of this struct.
package types

51
api/types/fix.go Normal file
View File

@@ -0,0 +1,51 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
import (
"log"
"regexp"
"sigs.k8s.io/yaml"
)
// FixKustomizationPreUnmarshalling modies the raw data
// before marshalling - e.g. changes old field names to
// new field names.
func FixKustomizationPreUnmarshalling(data []byte) []byte {
deprecateFieldsMap := map[string]string{
"imageTags:": "images:",
}
for oldname, newname := range deprecateFieldsMap {
pattern := regexp.MustCompile(oldname)
data = pattern.ReplaceAll(data, []byte(newname))
}
if useLegacyPatch(data) {
pattern := regexp.MustCompile("patches:")
data = pattern.ReplaceAll(data, []byte("patchesStrategicMerge:"))
}
return data
}
func useLegacyPatch(data []byte) bool {
found := false
var object map[string]interface{}
err := yaml.Unmarshal(data, &object)
if err != nil {
log.Fatalf("invalid content from %s\n", string(data))
}
if rawPatches, ok := object["patches"]; ok {
patches, ok := rawPatches.([]interface{})
if !ok {
log.Fatalf("invalid patches from %v\n", rawPatches)
}
for _, p := range patches {
_, ok := p.(string)
if ok {
found = true
}
}
}
return found
}

View File

@@ -0,0 +1,12 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
//go:generate stringer -type=GarbagePolicy
type GarbagePolicy int
const (
GarbageIgnore GarbagePolicy = iota + 1
GarbageCollect
)

View File

@@ -0,0 +1,25 @@
// Code generated by "stringer -type=GarbagePolicy"; DO NOT EDIT.
package types
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[GarbageIgnore-1]
_ = x[GarbageCollect-2]
}
const _GarbagePolicy_name = "GarbageIgnoreGarbageCollect"
var _GarbagePolicy_index = [...]uint8{0, 13, 27}
func (i GarbagePolicy) String() string {
i -= 1
if i < 0 || i >= GarbagePolicy(len(_GarbagePolicy_index)-1) {
return "GarbagePolicy(" + strconv.FormatInt(int64(i+1), 10) + ")"
}
return _GarbagePolicy_name[_GarbagePolicy_index[i]:_GarbagePolicy_index[i+1]]
}

51
api/types/genargs.go Normal file
View File

@@ -0,0 +1,51 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
import (
"strconv"
"strings"
)
// GenArgs contains both GeneratorArgs and GeneratorOptions.
type GenArgs struct {
args *GeneratorArgs
opts *GeneratorOptions
}
// NewGenArgs returns a new object of GenArgs
func NewGenArgs(args *GeneratorArgs, opts *GeneratorOptions) *GenArgs {
return &GenArgs{
args: args,
opts: opts,
}
}
func (g *GenArgs) String() string {
if g == nil {
return "{nilGenArgs}"
}
return "{" +
strings.Join([]string{
"nsfx:" + strconv.FormatBool(g.NeedsHashSuffix()),
"beh:" + g.Behavior().String()},
",") +
"}"
}
// NeedsHashSuffix returns true if the hash suffix is needed.
// It is needed when the two conditions are both met
// 1) GenArgs is not nil
// 2) DisableNameSuffixHash in GeneratorOptions is not set to true
func (g *GenArgs) NeedsHashSuffix() bool {
return g.args != nil && (g.opts == nil || g.opts.DisableNameSuffixHash == false)
}
// Behavior returns Behavior field of GeneratorArgs
func (g *GenArgs) Behavior() GenerationBehavior {
if g.args == nil {
return BehaviorUnspecified
}
return NewGenerationBehavior(g.args.Behavior)
}

37
api/types/genargs_test.go Normal file
View File

@@ -0,0 +1,37 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types_test
import (
"testing"
. "sigs.k8s.io/kustomize/v3/api/types"
)
func TestGenArgs_String(t *testing.T) {
tests := []struct {
ga *GenArgs
expected string
}{
{
ga: nil,
expected: "{nilGenArgs}",
},
{
ga: &GenArgs{},
expected: "{nsfx:false,beh:unspecified}",
},
{
ga: NewGenArgs(
&GeneratorArgs{Behavior: "merge"},
&GeneratorOptions{DisableNameSuffixHash: false}),
expected: "{nsfx:true,beh:merge}",
},
}
for _, test := range tests {
if test.ga.String() != test.expected {
t.Fatalf("Expected '%s', got '%s'", test.expected, test.ga.String())
}
}
}

View File

@@ -0,0 +1,46 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
// GenerationBehavior specifies generation behavior of configmaps, secrets and maybe other resources.
type GenerationBehavior int
const (
// BehaviorUnspecified is an Unspecified behavior; typically treated as a Create.
BehaviorUnspecified GenerationBehavior = iota
// BehaviorCreate makes a new resource.
BehaviorCreate
// BehaviorReplace replaces a resource.
BehaviorReplace
// BehaviorMerge attempts to merge a new resource with an existing resource.
BehaviorMerge
)
// String converts a GenerationBehavior to a string.
func (b GenerationBehavior) String() string {
switch b {
case BehaviorReplace:
return "replace"
case BehaviorMerge:
return "merge"
case BehaviorCreate:
return "create"
default:
return "unspecified"
}
}
// NewGenerationBehavior converts a string to a GenerationBehavior.
func NewGenerationBehavior(s string) GenerationBehavior {
switch s {
case "replace":
return BehaviorReplace
case "merge":
return BehaviorMerge
case "create":
return BehaviorCreate
default:
return BehaviorUnspecified
}
}

View File

@@ -0,0 +1,24 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
// GeneratorArgs contains arguments common to ConfigMap and Secret generators.
type GeneratorArgs struct {
// Namespace for the configmap, optional
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
// Name - actually the partial name - of the generated resource.
// The full name ends up being something like
// NamePrefix + this.Name + hash(content of generated resource).
Name string `json:"name,omitempty" yaml:"name,omitempty"`
// Behavior of generated resource, must be one of:
// 'create': create a new one
// 'replace': replace the existing one
// 'merge': merge with the existing one
Behavior string `json:"behavior,omitempty" yaml:"behavior,omitempty"`
// KvPairSources for the generator.
KvPairSources `json:",inline,omitempty" yaml:",inline,omitempty"`
}

View File

@@ -0,0 +1,18 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
// GeneratorOptions modify behavior of all ConfigMap and Secret generators.
type GeneratorOptions struct {
// Labels to add to all generated resources.
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
// Annotations to add to all generated resources.
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
// DisableNameSuffixHash if true disables the default behavior of adding a
// suffix to the names of generated resources that is a hash of the
// resource contents.
DisableNameSuffixHash bool `json:"disableNameSuffixHash,omitempty" yaml:"disableNameSuffixHash,omitempty"`
}

21
api/types/image.go Normal file
View File

@@ -0,0 +1,21 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
// Image contains an image name, a new name, a new tag or digest,
// which will replace the original name and tag.
type Image struct {
// Name is a tag-less image name.
Name string `json:"name,omitempty" yaml:"name,omitempty"`
// NewName is the value used to replace the original name.
NewName string `json:"newName,omitempty" yaml:"newName,omitempty"`
// NewTag is the value used to replace the original tag.
NewTag string `json:"newTag,omitempty" yaml:"newTag,omitempty"`
// Digest is the value used to replace the original image tag.
// If digest is present NewTag value is ignored.
Digest string `json:"digest,omitempty" yaml:"digest,omitempty"`
}

16
api/types/inventory.go Normal file
View File

@@ -0,0 +1,16 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
// Inventory records all objects touched in a build operation.
type Inventory struct {
Type string `json:"type,omitempty" yaml:"type,omitempty"`
ConfigMap NameArgs `json:"configMap,omitempty" yaml:"configMap,omitempty"`
}
// NameArgs holds both namespace and name.
type NameArgs struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
}

151
api/types/kustomization.go Normal file
View File

@@ -0,0 +1,151 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
const (
KustomizationVersion = "kustomize.config.k8s.io/v1beta1"
KustomizationKind = "Kustomization"
)
// Kustomization holds the information needed to generate customized k8s api resources.
type Kustomization struct {
TypeMeta `json:",inline" yaml:",inline"`
//
// Operators - what kustomize can do.
//
// NamePrefix will prefix the names of all resources mentioned in the kustomization
// file including generated configmaps and secrets.
NamePrefix string `json:"namePrefix,omitempty" yaml:"namePrefix,omitempty"`
// NameSuffix will suffix the names of all resources mentioned in the kustomization
// file including generated configmaps and secrets.
NameSuffix string `json:"nameSuffix,omitempty" yaml:"nameSuffix,omitempty"`
// Namespace to add to all objects.
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
// CommonLabels to add to all objects and selectors.
CommonLabels map[string]string `json:"commonLabels,omitempty" yaml:"commonLabels,omitempty"`
// CommonAnnotations to add to all objects.
CommonAnnotations map[string]string `json:"commonAnnotations,omitempty" yaml:"commonAnnotations,omitempty"`
// PatchesStrategicMerge specifies the relative path to a file
// containing a strategic merge patch. Format documented at
// https://github.com/kubernetes/community/blob/master/contributors/devel/strategic-merge-patch.md
// URLs and globs are not supported.
PatchesStrategicMerge []PatchStrategicMerge `json:"patchesStrategicMerge,omitempty" yaml:"patchesStrategicMerge,omitempty"`
// JSONPatches is a list of JSONPatch for applying JSON patch.
// Format documented at https://tools.ietf.org/html/rfc6902
// and http://jsonpatch.com
PatchesJson6902 []PatchJson6902 `json:"patchesJson6902,omitempty" yaml:"patchesJson6902,omitempty"`
// Patches is a list of patches, where each one can be either a
// Strategic Merge Patch or a JSON patch.
// Each patch can be applied to multiple target objects.
Patches []Patch `json:"patches,omitempty" yaml:"patches,omitempty"`
// Images is a list of (image name, new name, new tag or digest)
// for changing image names, tags or digests. This can also be achieved with a
// patch, but this operator is simpler to specify.
Images []Image `json:"images,omitempty" yaml:"images,omitempty"`
// Replicas is a list of {resourcename, count} that allows for simpler replica
// specification. This can also be done with a patch.
Replicas []Replica `json:"replicas,omitempty" yaml:"replicas,omitempty"`
// Vars allow things modified by kustomize to be injected into a
// kubernetes object specification. A var is a name (e.g. FOO) associated
// with a field in a specific resource instance. The field must
// contain a value of type string/bool/int/float, and defaults to the name field
// of the instance. Any appearance of "$(FOO)" in the object
// spec will be replaced at kustomize build time, after the final
// value of the specified field has been determined.
Vars []Var `json:"vars,omitempty" yaml:"vars,omitempty"`
//
// Operands - what kustomize operates on.
//
// Resources specifies relative paths to files holding YAML representations
// of kubernetes API objects, or specifcations of other kustomizations
// via relative paths, absolute paths, or URLs.
Resources []string `json:"resources,omitempty" yaml:"resources,omitempty"`
// Crds specifies relative paths to Custom Resource Definition files.
// This allows custom resources to be recognized as operands, making
// it possible to add them to the Resources list.
// CRDs themselves are not modified.
Crds []string `json:"crds,omitempty" yaml:"crds,omitempty"`
// Deprecated.
// Anything that would have been specified here should
// be specified in the Resources field instead.
Bases []string `json:"bases,omitempty" yaml:"bases,omitempty"`
//
// Generators (operators that create operands)
//
// ConfigMapGenerator is a list of configmaps to generate from
// local data (one configMap per list item).
// The resulting resource is a normal operand, subject to
// name prefixing, patching, etc. By default, the name of
// the map will have a suffix hash generated from its contents.
ConfigMapGenerator []ConfigMapArgs `json:"configMapGenerator,omitempty" yaml:"configMapGenerator,omitempty"`
// SecretGenerator is a list of secrets to generate from
// local data (one secret per list item).
// The resulting resource is a normal operand, subject to
// name prefixing, patching, etc. By default, the name of
// the map will have a suffix hash generated from its contents.
SecretGenerator []SecretArgs `json:"secretGenerator,omitempty" yaml:"secretGenerator,omitempty"`
// GeneratorOptions modify behavior of all ConfigMap and Secret generators.
GeneratorOptions *GeneratorOptions `json:"generatorOptions,omitempty" yaml:"generatorOptions,omitempty"`
// Configurations is a list of transformer configuration files
Configurations []string `json:"configurations,omitempty" yaml:"configurations,omitempty"`
// Generators is a list of files containing custom generators
Generators []string `json:"generators,omitempty" yaml:"generators,omitempty"`
// Transformers is a list of files containing transformers
Transformers []string `json:"transformers,omitempty" yaml:"transformers,omitempty"`
// Inventory appends an object that contains the record
// of all other objects, which can be used in apply, prune and delete
Inventory *Inventory `json:"inventory,omitempty" yaml:"inventory,omitempty"`
}
// FixKustomizationPostUnmarshalling fixes things
// like empty fields that should not be empty, or
// moving content of deprecated fields to newer
// fields.
func (k *Kustomization) FixKustomizationPostUnmarshalling() {
if k.APIVersion == "" {
k.APIVersion = KustomizationVersion
}
if k.Kind == "" {
k.Kind = KustomizationKind
}
for _, b := range k.Bases {
k.Resources = append(k.Resources, b)
}
k.Bases = nil
}
func (k *Kustomization) EnforceFields() []string {
var errs []string
if k.APIVersion != "" && k.APIVersion != KustomizationVersion {
errs = append(errs, "apiVersion should be "+KustomizationVersion)
}
if k.Kind != "" && k.Kind != KustomizationKind {
errs = append(errs, "kind should be "+KustomizationKind)
}
return errs
}

View File

@@ -0,0 +1,31 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
// KvPairSources defines places to obtain key value pairs.
type KvPairSources struct {
// LiteralSources is a list of literal
// pair sources. Each literal source should
// be a key and literal value, e.g. `key=value`
LiteralSources []string `json:"literals,omitempty" yaml:"literals,omitempty"`
// FileSources is a list of file "sources" to
// use in creating a list of key, value pairs.
// A source takes the form: [{key}=]{path}
// If the "key=" part is missing, the key is the
// path's basename. If they "key=" part is present,
// it becomes the key (replacing the basename).
// In either case, the value is the file contents.
// Specifying a directory will iterate each named
// file in the directory whose basename is a
// valid configmap key.
FileSources []string `json:"files,omitempty" yaml:"files,omitempty"`
// EnvSources is a list of file paths.
// The contents of each file should be one
// key=value pair per line, e.g. a Docker
// or npm ".env" file or a ".ini" file
// (wikipedia.org/wiki/INI_file)
EnvSources []string `json:"envs,omitempty" yaml:"envs,omitempty"`
}

11
api/types/objectmeta.go Normal file
View File

@@ -0,0 +1,11 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
// ObjectMeta partially copies apimachinery/pkg/apis/meta/v1.ObjectMeta
// No need for a direct dependence; the fields are stable.
type ObjectMeta struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
}

10
api/types/pair.go Normal file
View File

@@ -0,0 +1,10 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
// Pair is a key value pair.
type Pair struct {
Key string
Value string
}

19
api/types/patch.go Normal file
View File

@@ -0,0 +1,19 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
// Patch represent either a Strategic Merge Patch or a JSON patch
// and its targets.
// The content of the patch can either be from a file
// or from an inline string.
type Patch struct {
// Path is a relative file path to the patch file.
Path string `json:"path,omitempty" yaml:"path,omitempty"`
// Patch is the content of a patch.
Patch string `json:"patch,omitempty" yaml:"patch,omitempty"`
// Target points to the resources that the patch is applied to
Target *Selector `json:"target,omitempty" yaml:"target,omitempty"`
}

View File

@@ -0,0 +1,21 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
// PatchJson6902 represents a json patch for an object
// with format documented https://tools.ietf.org/html/rfc6902.
type PatchJson6902 struct {
// PatchTarget refers to a Kubernetes object that the json patch will be
// applied to. It must refer to a Kubernetes resource under the
// purview of this kustomization. PatchTarget should use the
// raw name of the object (the name specified in its YAML,
// before addition of a namePrefix and a nameSuffix).
Target *PatchTarget `json:"target" yaml:"target"`
// relative file path for a json patch file inside a kustomization
Path string `json:"path,omitempty" yaml:"path,omitempty"`
// inline patch string
Patch string `json:"patch,omitempty" yaml:"patch,omitempty"`
}

View File

@@ -0,0 +1,9 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
// PatchStrategicMerge represents a relative path to a
// stategic merge patch with the format
// https://github.com/kubernetes/community/blob/master/contributors/devel/strategic-merge-patch.md
type PatchStrategicMerge string

15
api/types/patchtarget.go Normal file
View File

@@ -0,0 +1,15 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
import (
"sigs.k8s.io/kustomize/v3/api/resid"
)
// PatchTarget represents the kubernetes object that the patch is applied to
type PatchTarget struct {
resid.Gvk `json:",inline,omitempty" yaml:",inline,omitempty"`
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
Name string `json:"name" yaml:"name"`
}

16
api/types/pluginconfig.go Normal file
View File

@@ -0,0 +1,16 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
// PluginConfig holds plugin configuration.
type PluginConfig struct {
// DirectoryPath is an absolute path to a
// directory containing kustomize plugins.
// This directory may contain subdirectories
// further categorizing plugins.
DirectoryPath string
// Enabled is true if plugins are enabled.
Enabled bool
}

16
api/types/replica.go Normal file
View File

@@ -0,0 +1,16 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
// Replica specifies a modification to a replica config.
// The number of replicas of a resource whose name matches will be set to count.
// This struct is used by the ReplicaCountTransform, and is meant to supplement
// the existing patch functionality with a simpler syntax for replica configuration.
type Replica struct {
// The name of the resource to change the replica count
Name string `json:"name,omitempty" yaml:"name,omitempty"`
// The number of replicas required.
Count int64 `json:"count" yaml:"count"`
}

19
api/types/secretargs.go Normal file
View File

@@ -0,0 +1,19 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
// SecretArgs contains the metadata of how to generate a secret.
type SecretArgs struct {
// GeneratorArgs for the secret.
GeneratorArgs `json:",inline,omitempty" yaml:",inline,omitempty"`
// Type of the secret.
//
// This is the same field as the secret type field in v1/Secret:
// It can be "Opaque" (default), or "kubernetes.io/tls".
//
// If type is "kubernetes.io/tls", then "literals" or "files" must have exactly two
// keys: "tls.key" and "tls.crt"
Type string `json:"type,omitempty" yaml:"type,omitempty"`
}

27
api/types/selector.go Normal file
View File

@@ -0,0 +1,27 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
import (
"sigs.k8s.io/kustomize/v3/api/resid"
)
// Selector specifies a set of resources.
// Any resource that matches intersection of all conditions
// is included in this set.
type Selector struct {
resid.Gvk `json:",inline,omitempty" yaml:",inline,omitempty"`
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
// AnnotationSelector is a string that follows the label selection expression
// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api
// It matches with the resource annotations.
AnnotationSelector string `json:"annotationSelector,omitempty" yaml:"annotationSelector,omitempty"`
// LabelSelector is a string that follows the label selection expression
// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api
// It matches with the resource labels.
LabelSelector string `json:"labelSelector,omitempty" yaml:"labelSelector,omitempty"`
}

11
api/types/typemeta.go Normal file
View File

@@ -0,0 +1,11 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
// TypeMeta partially copies apimachinery/pkg/apis/meta/v1.TypeMeta
// No need for a direct dependence; the fields are stable.
type TypeMeta struct {
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"`
}

213
api/types/var.go Normal file
View File

@@ -0,0 +1,213 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
import (
"fmt"
"reflect"
"sort"
"strings"
"sigs.k8s.io/kustomize/v3/api/resid"
)
const defaultFieldPath = "metadata.name"
// Var represents a variable whose value will be sourced
// from a field in a Kubernetes object.
type Var struct {
// Value of identifier name e.g. FOO used in container args, annotations
// Appears in pod template as $(FOO)
Name string `json:"name" yaml:"name"`
// ObjRef must refer to a Kubernetes resource under the
// purview of this kustomization. ObjRef should use the
// raw name of the object (the name specified in its YAML,
// before addition of a namePrefix and a nameSuffix).
ObjRef Target `json:"objref" yaml:"objref"`
// FieldRef refers to the field of the object referred to by
// ObjRef whose value will be extracted for use in
// replacing $(FOO).
// If unspecified, this defaults to fieldPath: $defaultFieldPath
FieldRef FieldSelector `json:"fieldref,omitempty" yaml:"fieldref,omitempty"`
}
// Target refers to a kubernetes object by Group, Version, Kind and Name
// gvk.Gvk contains Group, Version and Kind
// APIVersion is added to keep the backward compatibility of using ObjectReference
// for Var.ObjRef
type Target struct {
APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"`
resid.Gvk `json:",inline,omitempty" yaml:",inline,omitempty"`
Name string `json:"name" yaml:"name"`
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
}
// GVK returns the Gvk object in Target
func (t *Target) GVK() resid.Gvk {
if t.APIVersion == "" {
return t.Gvk
}
versions := strings.Split(t.APIVersion, "/")
if len(versions) == 2 {
t.Group = versions[0]
t.Version = versions[1]
}
if len(versions) == 1 {
t.Version = versions[0]
}
return t.Gvk
}
// FieldSelector contains the fieldPath to an object field.
// This struct is added to keep the backward compatibility of using ObjectFieldSelector
// for Var.FieldRef
type FieldSelector struct {
FieldPath string `json:"fieldPath,omitempty" yaml:"fieldPath,omitempty"`
}
// defaulting sets reference to field used by default.
func (v *Var) Defaulting() {
if v.FieldRef.FieldPath == "" {
v.FieldRef.FieldPath = defaultFieldPath
}
v.ObjRef.GVK()
}
// DeepEqual returns true if var a and b are Equals.
// Note 1: The objects are unchanged by the VarEqual
// Note 2: Should be normalize be FieldPath before doing
// the DeepEqual. spec.a[b] is supposed to be the same
// as spec.a.b
func (v Var) DeepEqual(other Var) bool {
v.Defaulting()
other.Defaulting()
return reflect.DeepEqual(v, other)
}
// VarSet is a set of Vars where no var.Name is repeated.
type VarSet struct {
set map[string]Var
}
// NewVarSet returns an initialized VarSet
func NewVarSet() VarSet {
return VarSet{set: map[string]Var{}}
}
// AsSlice returns the vars as a slice.
func (vs *VarSet) AsSlice() []Var {
s := make([]Var, len(vs.set))
i := 0
for _, v := range vs.set {
s[i] = v
i++
}
sort.Sort(byName(s))
return s
}
// Copy returns a copy of the var set.
func (vs *VarSet) Copy() VarSet {
newSet := make(map[string]Var, len(vs.set))
for k, v := range vs.set {
newSet[k] = v
}
return VarSet{set: newSet}
}
// MergeSet absorbs other vars with error on name collision.
func (vs *VarSet) MergeSet(incoming VarSet) error {
for _, incomingVar := range incoming.set {
if err := vs.Merge(incomingVar); err != nil {
return err
}
}
return nil
}
// MergeSlice absorbs a Var slice with error on name collision.
// Empty fields in incoming vars are defaulted.
func (vs *VarSet) MergeSlice(incoming []Var) error {
for _, v := range incoming {
if err := vs.Merge(v); err != nil {
return err
}
}
return nil
}
// Merge absorbs another Var with error on name collision.
// Empty fields in incoming Var is defaulted.
func (vs *VarSet) Merge(v Var) error {
if vs.Contains(v) {
return fmt.Errorf(
"var '%s' already encountered", v.Name)
}
v.Defaulting()
vs.set[v.Name] = v
return nil
}
// AbsorbSet absorbs other vars with error on (name,value) collision.
func (vs *VarSet) AbsorbSet(incoming VarSet) error {
for _, v := range incoming.set {
if err := vs.Absorb(v); err != nil {
return err
}
}
return nil
}
// AbsorbSlice absorbs a Var slice with error on (name,value) collision.
// Empty fields in incoming vars are defaulted.
func (vs *VarSet) AbsorbSlice(incoming []Var) error {
for _, v := range incoming {
if err := vs.Absorb(v); err != nil {
return err
}
}
return nil
}
// Absorb absorbs another Var with error on (name,value) collision.
// Empty fields in incoming Var is defaulted.
func (vs *VarSet) Absorb(v Var) error {
conflicting := vs.Get(v.Name)
if conflicting == nil {
// no conflict. The var is valid.
v.Defaulting()
vs.set[v.Name] = v
return nil
}
if !reflect.DeepEqual(v, *conflicting) {
// two vars with the same name are pointing at two
// different resources.
return fmt.Errorf(
"var '%s' already encountered", v.Name)
}
return nil
}
// Contains is true if the set has the other var.
func (vs *VarSet) Contains(other Var) bool {
return vs.Get(other.Name) != nil
}
// Get returns the var with the given name, else nil.
func (vs *VarSet) Get(name string) *Var {
if v, found := vs.set[name]; found {
return &v
}
return nil
}
// byName is a sort interface which sorts Vars by name alphabetically
type byName []Var
func (v byName) Len() int { return len(v) }
func (v byName) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
func (v byName) Less(i, j int) bool { return v[i].Name < v[j].Name }

171
api/types/var_test.go Normal file
View File

@@ -0,0 +1,171 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package types
import (
"reflect"
"strings"
"testing"
"gopkg.in/yaml.v2"
"sigs.k8s.io/kustomize/v3/api/resid"
)
func TestGVK(t *testing.T) {
type testcase struct {
data string
expected resid.Gvk
}
testcases := []testcase{
{
data: `
apiVersion: v1
kind: Secret
name: my-secret
`,
expected: resid.Gvk{Group: "", Version: "v1", Kind: "Secret"},
},
{
data: `
apiVersion: myapps/v1
kind: MyKind
name: my-kind
`,
expected: resid.Gvk{Group: "myapps", Version: "v1", Kind: "MyKind"},
},
{
data: `
version: v2
kind: MyKind
name: my-kind
`,
expected: resid.Gvk{Version: "v2", Kind: "MyKind"},
},
}
for _, tc := range testcases {
var targ Target
err := yaml.Unmarshal([]byte(tc.data), &targ)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
if !reflect.DeepEqual(targ.GVK(), tc.expected) {
t.Fatalf("Expected %v, but got %v", tc.expected, targ.GVK())
}
}
}
func TestDefaulting(t *testing.T) {
v := &Var{
Name: "SOME_VARIABLE_NAME",
ObjRef: Target{
Gvk: resid.Gvk{
Version: "v1",
Kind: "Secret",
},
Name: "my-secret",
},
}
v.Defaulting()
if v.FieldRef.FieldPath != defaultFieldPath {
t.Fatalf("expected %s, got %v",
defaultFieldPath, v.FieldRef.FieldPath)
}
}
func TestVarSet(t *testing.T) {
set := NewVarSet()
vars := []Var{
{
Name: "SHELLVARS",
ObjRef: Target{
APIVersion: "v7",
Gvk: resid.Gvk{Kind: "ConfigMap"},
Name: "bash"},
},
{
Name: "BACKEND",
ObjRef: Target{
APIVersion: "v7",
Gvk: resid.Gvk{Kind: "Deployment"},
Name: "myTiredBackend"},
},
{
Name: "AWARD",
ObjRef: Target{
APIVersion: "v7",
Gvk: resid.Gvk{Kind: "Service"},
Name: "nobelPrize"},
FieldRef: FieldSelector{FieldPath: "some.arbitrary.path"},
},
}
err := set.MergeSlice(vars)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
for _, v := range vars {
if !set.Contains(v) {
t.Fatalf("set %v should contain var %v", set.AsSlice(), v)
}
}
set2 := NewVarSet()
err = set2.MergeSet(set)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
err = set2.MergeSlice(vars)
if err == nil {
t.Fatalf("expected err")
}
if !strings.Contains(err.Error(), "var 'SHELLVARS' already encountered") {
t.Fatalf("unexpected err: %v", err)
}
v := set2.Get("BACKEND")
if v == nil {
t.Fatalf("expected var")
}
// Confirm defaulting.
if v.FieldRef.FieldPath != defaultFieldPath {
t.Fatalf("unexpected field path: %v", v.FieldRef.FieldPath)
}
// Confirm sorting.
names := set2.AsSlice()
if names[0].Name != "AWARD" ||
names[1].Name != "BACKEND" ||
names[2].Name != "SHELLVARS" {
t.Fatalf("unexpected order in : %v", names)
}
}
func TestVarSetCopy(t *testing.T) {
set1 := NewVarSet()
vars := []Var{
{Name: "First"},
{Name: "Second"},
{Name: "Third"},
}
err := set1.MergeSlice(vars)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
// Confirm copying
set2 := set1.Copy()
for _, varInSet1 := range set1.AsSlice() {
if v := set2.Get(varInSet1.Name); v == nil {
t.Fatalf("set %v should contain a Var named %s", set2.AsSlice(), varInSet1)
} else if !set2.Contains(*v) {
t.Fatalf("set %v should contain %v", set2.AsSlice(), v)
}
}
// Confirm that the copy is deep
w := Var{Name: "Only in set2"}
set2.Merge(w)
if !set2.Contains(w) {
t.Fatalf("set %v should contain %v", set2.AsSlice(), w)
}
if set1.Contains(w) {
t.Fatalf("set %v should not contain %v", set1.AsSlice(), w)
}
}