Make resource, resmap public.

This commit is contained in:
jregan
2019-10-20 10:01:06 -07:00
parent e2fd33c54a
commit 3af5a8afea
118 changed files with 288 additions and 288 deletions

View File

@@ -4,7 +4,7 @@
package builtinconfig
import (
"sigs.k8s.io/kustomize/v3/pkg/ifc"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/yaml"
)

View File

@@ -8,8 +8,8 @@ import (
"sort"
"sigs.k8s.io/kustomize/v3/api/builtinconfig/consts"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/pkg/ifc"
)
// TransformerConfig holds the data needed to perform transformations.

95
api/ifc/ifc.go Normal file
View File

@@ -0,0 +1,95 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package ifc holds miscellaneous interfaces used by kustomize.
package ifc
import (
"sigs.k8s.io/kustomize/v3/api/resid"
"sigs.k8s.io/kustomize/v3/api/types"
)
// Validator provides functions to validate annotations and labels
type Validator interface {
MakeAnnotationValidator() func(map[string]string) error
MakeAnnotationNameValidator() func([]string) error
MakeLabelValidator() func(map[string]string) error
MakeLabelNameValidator() func([]string) error
ValidateNamespace(string) []string
ErrIfInvalidKey(string) error
IsEnvVarName(k string) error
}
// KvLoader reads and validates KV pairs.
type KvLoader interface {
Validator() Validator
Load(args types.KvPairSources) (all []types.Pair, err error)
}
// Loader interface exposes methods to read bytes.
type Loader interface {
// Root returns the root location for this Loader.
Root() string
// New returns Loader located at newRoot.
New(newRoot string) (Loader, error)
// Load returns the bytes read from the location or an error.
Load(location string) ([]byte, error)
// Cleanup cleans the loader
Cleanup() error
}
// Kunstructured allows manipulation of k8s objects
// that do not have Golang structs.
type Kunstructured interface {
Map() map[string]interface{}
SetMap(map[string]interface{})
Copy() Kunstructured
GetFieldValue(string) (interface{}, error)
GetString(string) (string, error)
GetStringSlice(string) ([]string, error)
GetBool(path string) (bool, error)
GetFloat64(path string) (float64, error)
GetInt64(path string) (int64, error)
GetSlice(path string) ([]interface{}, error)
GetStringMap(path string) (map[string]string, error)
GetMap(path string) (map[string]interface{}, error)
MarshalJSON() ([]byte, error)
UnmarshalJSON([]byte) error
GetGvk() resid.Gvk
SetGvk(resid.Gvk)
GetKind() string
GetName() string
SetName(string)
SetNamespace(string)
GetLabels() map[string]string
SetLabels(map[string]string)
GetAnnotations() map[string]string
SetAnnotations(map[string]string)
MatchesLabelSelector(selector string) (bool, error)
MatchesAnnotationSelector(selector string) (bool, error)
Patch(Kunstructured) error
}
// KunstructuredFactory makes instances of Kunstructured.
type KunstructuredFactory interface {
SliceFromBytes([]byte) ([]Kunstructured, error)
FromMap(m map[string]interface{}) Kunstructured
Hasher() KunstructuredHasher
MakeConfigMap(
kvLdr KvLoader,
options *types.GeneratorOptions,
args *types.ConfigMapArgs) (Kunstructured, error)
MakeSecret(
kvLdr KvLoader,
options *types.GeneratorOptions,
args *types.SecretArgs) (Kunstructured, error)
}
// KunstructuredHasher returns a hash of the argument
// or an error.
type KunstructuredHasher interface {
Hash(Kunstructured) (string, error)
}
// See core.v1.SecretTypeOpaque
const SecretTypeOpaque = "Opaque"

View File

@@ -10,17 +10,17 @@ import (
"testing"
"sigs.k8s.io/kustomize/v3/api/builtinconfig/consts"
"sigs.k8s.io/kustomize/v3/api/loader"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/testutils/valtest"
"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/validators"
)
// KustTestHarness is an environment for running a kustomize build,
@@ -63,7 +63,7 @@ func NewKustTestHarnessFull(
func (th *KustTestHarness) MakeKustTarget() *target.KustTarget {
kt, err := target.NewKustTarget(
th.ldr, validators.MakeFakeValidator(), th.rf,
th.ldr, valtest_test.MakeFakeValidator(), th.rf,
transformer.NewFactoryImpl(), th.pl)
if err != nil {
th.t.Fatalf("Unexpected construction error %v", err)
@@ -119,7 +119,7 @@ func (th *KustTestHarness) LoadAndRunGenerator(
th.t.Fatalf("Err: %v", err)
}
g, err := th.pl.LoadGenerator(
th.ldr, validators.MakeFakeValidator(), res)
th.ldr, valtest_test.MakeFakeValidator(), res)
if err != nil {
th.t.Fatalf("Err: %v", err)
}
@@ -161,7 +161,7 @@ func (th *KustTestHarness) RunTransformerFromResMap(
th.t.Fatalf("Err: %v", err)
}
g, err := th.pl.LoadTransformer(
th.ldr, validators.MakeFakeValidator(), transConfig)
th.ldr, valtest_test.MakeFakeValidator(), transConfig)
if err != nil {
return nil, err
}

View File

@@ -14,8 +14,8 @@ import (
"unicode/utf8"
"github.com/pkg/errors"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/pkg/ifc"
)
var utf8bom = []byte{0xEF, 0xBB, 0xBF}

View File

@@ -8,15 +8,15 @@ import (
"testing"
"sigs.k8s.io/kustomize/v3/api/filesys"
ldr "sigs.k8s.io/kustomize/v3/api/loader"
"sigs.k8s.io/kustomize/v3/api/testutils/valtest"
"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()}
validator: valtest_test.MakeFakeValidator()}
}
func TestKeyValuesFromLines(t *testing.T) {

312
api/loader/fileloader.go Normal file
View File

@@ -0,0 +1,312 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package loader
import (
"fmt"
"log"
"path/filepath"
"strings"
"sigs.k8s.io/kustomize/v3/api/filesys"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/pkg/git"
)
// fileLoader is a kustomization's interface to files.
//
// The directory in which a kustomization file sits
// is referred to below as the kustomization's _root_.
//
// An instance of fileLoader has an immutable root,
// and offers a `New` method returning a new loader
// with a new root.
//
// A kustomization file refers to two kinds of files:
//
// * supplemental data paths
//
// `Load` is used to visit these paths.
//
// These paths refer to resources, patches,
// data for ConfigMaps and Secrets, etc.
//
// The loadRestrictor may disallow certain paths
// or classes of paths.
//
// * bases (other kustomizations)
//
// `New` is used to load bases.
//
// A base can be either a remote git repo URL, or
// a directory specified relative to the current
// root. In the former case, the repo is locally
// cloned, and the new loader is rooted on a path
// in that clone.
//
// As loaders create new loaders, a root history
// is established, and used to disallow:
//
// - A base that is a repository that, in turn,
// specifies a base repository seen previously
// in the loading stack (a cycle).
//
// - An overlay depending on a base positioned at
// or above it. I.e. '../foo' is OK, but '.',
// '..', '../..', etc. are disallowed. Allowing
// such a base has no advantages and encourages
// cycles, particularly if some future change
// were to introduce globbing to file
// specifications in the kustomization file.
//
// These restrictions assure that kustomizations
// are self-contained and relocatable, and impose
// some safety when relying on remote kustomizations,
// e.g. a remotely loaded ConfigMap generator specified
// to read from /etc/passwd will fail.
//
type fileLoader struct {
// Loader that spawned this loader.
// Used to avoid cycles.
referrer *fileLoader
// An absolute, cleaned path to a directory.
// The Load function will read non-absolute
// paths relative to this directory.
root filesys.ConfirmedDir
// Restricts behavior of Load function.
loadRestrictor LoadRestrictorFunc
// If this is non-nil, the files were
// obtained from the given repository.
repoSpec *git.RepoSpec
// File system utilities.
fSys filesys.FileSystem
// Used to clone repositories.
cloner git.Cloner
// Used to clean up, as needed.
cleaner func() error
}
const CWD = "."
// NewFileLoaderAtCwd returns a loader that loads from ".".
// A convenience for kustomize edit commands.
func NewFileLoaderAtCwd(fSys filesys.FileSystem) *fileLoader {
return newLoaderOrDie(
RestrictionRootOnly, fSys, CWD)
}
// NewFileLoaderAtRoot returns a loader that loads from "/".
// A convenience for tests.
func NewFileLoaderAtRoot(fSys filesys.FileSystem) *fileLoader {
return newLoaderOrDie(
RestrictionRootOnly, fSys, string(filepath.Separator))
}
// Root returns the absolute path that is prepended to any
// relative paths used in Load.
func (fl *fileLoader) Root() string {
return fl.root.String()
}
func newLoaderOrDie(
lr LoadRestrictorFunc,
fSys filesys.FileSystem, path string) *fileLoader {
root, err := demandDirectoryRoot(fSys, path)
if err != nil {
log.Fatalf("unable to make loader at '%s'; %v", path, err)
}
return newLoaderAtConfirmedDir(
lr, root, fSys, nil, git.ClonerUsingGitExec)
}
// newLoaderAtConfirmedDir returns a new fileLoader with given root.
func newLoaderAtConfirmedDir(
lr LoadRestrictorFunc,
root filesys.ConfirmedDir, fSys filesys.FileSystem,
referrer *fileLoader, cloner git.Cloner) *fileLoader {
return &fileLoader{
loadRestrictor: lr,
root: root,
referrer: referrer,
fSys: fSys,
cloner: cloner,
cleaner: func() error { return nil },
}
}
// Assure that the given path is in fact a directory.
func demandDirectoryRoot(
fSys filesys.FileSystem, path string) (filesys.ConfirmedDir, error) {
if path == "" {
return "", fmt.Errorf(
"loader root cannot be empty")
}
d, f, err := fSys.CleanedAbs(path)
if err != nil {
return "", fmt.Errorf(
"absolute path error in '%s' : %v", path, err)
}
if f != "" {
return "", fmt.Errorf(
"got file '%s', but '%s' must be a directory to be a root",
f, path)
}
return d, nil
}
// New returns a new Loader, rooted relative to current loader,
// or rooted in a temp directory holding a git repo clone.
func (fl *fileLoader) New(path string) (ifc.Loader, error) {
if path == "" {
return nil, fmt.Errorf("new root cannot be empty")
}
repoSpec, err := git.NewRepoSpecFromUrl(path)
if err == nil {
// Treat this as git repo clone request.
if err := fl.errIfRepoCycle(repoSpec); err != nil {
return nil, err
}
return newLoaderAtGitClone(
repoSpec, fl.fSys, fl, fl.cloner)
}
if filepath.IsAbs(path) {
return nil, fmt.Errorf("new root '%s' cannot be absolute", path)
}
root, err := demandDirectoryRoot(fl.fSys, fl.root.Join(path))
if err != nil {
return nil, err
}
if err := fl.errIfGitContainmentViolation(root); err != nil {
return nil, err
}
if err := fl.errIfArgEqualOrHigher(root); err != nil {
return nil, err
}
return newLoaderAtConfirmedDir(
fl.loadRestrictor, root, fl.fSys, fl, fl.cloner), nil
}
// newLoaderAtGitClone returns a new Loader pinned to a temporary
// directory holding a cloned git repo.
func newLoaderAtGitClone(
repoSpec *git.RepoSpec, fSys filesys.FileSystem,
referrer *fileLoader, cloner git.Cloner) (ifc.Loader, error) {
cleaner := repoSpec.Cleaner(fSys)
err := cloner(repoSpec)
if err != nil {
cleaner()
return nil, err
}
root, f, err := fSys.CleanedAbs(repoSpec.AbsPath())
if err != nil {
cleaner()
return nil, err
}
// We don't know that the path requested in repoSpec
// is a directory until we actually clone it and look
// inside. That just happened, hence the error check
// is here.
if f != "" {
cleaner()
return nil, fmt.Errorf(
"'%s' refers to file '%s'; expecting directory",
repoSpec.AbsPath(), f)
}
return &fileLoader{
// Clones never allowed to escape root.
loadRestrictor: RestrictionRootOnly,
root: root,
referrer: referrer,
repoSpec: repoSpec,
fSys: fSys,
cloner: cloner,
cleaner: cleaner,
}, nil
}
func (fl *fileLoader) errIfGitContainmentViolation(
base filesys.ConfirmedDir) error {
containingRepo := fl.containingRepo()
if containingRepo == nil {
return nil
}
if !base.HasPrefix(containingRepo.CloneDir()) {
return fmt.Errorf(
"security; bases in kustomizations found in "+
"cloned git repos must be within the repo, "+
"but base '%s' is outside '%s'",
base, containingRepo.CloneDir())
}
return nil
}
// Looks back through referrers for a git repo, returning nil
// if none found.
func (fl *fileLoader) containingRepo() *git.RepoSpec {
if fl.repoSpec != nil {
return fl.repoSpec
}
if fl.referrer == nil {
return nil
}
return fl.referrer.containingRepo()
}
// errIfArgEqualOrHigher tests whether the argument,
// is equal to or above the root of any ancestor.
func (fl *fileLoader) errIfArgEqualOrHigher(
candidateRoot filesys.ConfirmedDir) error {
if fl.root.HasPrefix(candidateRoot) {
return fmt.Errorf(
"cycle detected: candidate root '%s' contains visited root '%s'",
candidateRoot, fl.root)
}
if fl.referrer == nil {
return nil
}
return fl.referrer.errIfArgEqualOrHigher(candidateRoot)
}
// TODO(monopole): Distinguish branches?
// I.e. Allow a distinction between git URI with
// path foo and tag bar and a git URI with the same
// path but a different tag?
func (fl *fileLoader) errIfRepoCycle(newRepoSpec *git.RepoSpec) error {
// TODO(monopole): Use parsed data instead of Raw().
if fl.repoSpec != nil &&
strings.HasPrefix(fl.repoSpec.Raw(), newRepoSpec.Raw()) {
return fmt.Errorf(
"cycle detected: URI '%s' referenced by previous URI '%s'",
newRepoSpec.Raw(), fl.repoSpec.Raw())
}
if fl.referrer == nil {
return nil
}
return fl.referrer.errIfRepoCycle(newRepoSpec)
}
// Load returns the content of file at the given path,
// else an error. Relative paths are taken relative
// to the root.
func (fl *fileLoader) Load(path string) ([]byte, error) {
if !filepath.IsAbs(path) {
path = fl.root.Join(path)
}
path, err := fl.loadRestrictor(fl.fSys, fl.root, path)
if err != nil {
return nil, err
}
return fl.fSys.ReadFile(path)
}
// Cleanup runs the cleaner.
func (fl *fileLoader) Cleanup() error {
return fl.cleaner()
}

View File

@@ -0,0 +1,594 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package loader
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"reflect"
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/api/filesys"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/pkg/git"
"sigs.k8s.io/kustomize/v3/pkg/pgmconfig"
)
type testData struct {
path string
expectedContent string
}
var testCases = []testData{
{
path: "foo/project/fileA.yaml",
expectedContent: "fileA content",
},
{
path: "foo/project/subdir1/fileB.yaml",
expectedContent: "fileB content",
},
{
path: "foo/project/subdir2/fileC.yaml",
expectedContent: "fileC content",
},
{
path: "foo/project/fileD.yaml",
expectedContent: "fileD content",
},
}
func MakeFakeFs(td []testData) filesys.FileSystem {
fSys := filesys.MakeFsInMemory()
for _, x := range td {
fSys.WriteFile("/"+x.path, []byte(x.expectedContent))
}
return fSys
}
func makeLoader() *fileLoader {
return NewFileLoaderAtRoot(MakeFakeFs(testCases))
}
func TestLoaderLoad(t *testing.T) {
l1 := makeLoader()
if "/" != l1.Root() {
t.Fatalf("incorrect root: '%s'\n", l1.Root())
}
for _, x := range testCases {
b, err := l1.Load(x.path)
if err != nil {
t.Fatalf("unexpected load error: %v", err)
}
if !reflect.DeepEqual([]byte(x.expectedContent), b) {
t.Fatalf("in load expected %s, but got %s", x.expectedContent, b)
}
}
l2, err := l1.New("foo/project")
if err != nil {
t.Fatalf("unexpected err: %v\n", err)
}
if "/foo/project" != l2.Root() {
t.Fatalf("incorrect root: %s\n", l2.Root())
}
for _, x := range testCases {
b, err := l2.Load(strings.TrimPrefix(x.path, "foo/project/"))
if err != nil {
t.Fatalf("unexpected load error %v", err)
}
if !reflect.DeepEqual([]byte(x.expectedContent), b) {
t.Fatalf("in load expected %s, but got %s", x.expectedContent, b)
}
}
l2, err = l1.New("foo/project/") // Assure trailing slash stripped
if err != nil {
t.Fatalf("unexpected err: %v\n", err)
}
if "/foo/project" != l2.Root() {
t.Fatalf("incorrect root: %s\n", l2.Root())
}
}
func TestLoaderNewSubDir(t *testing.T) {
l1, err := makeLoader().New("foo/project")
if err != nil {
t.Fatalf("unexpected err: %v\n", err)
}
l2, err := l1.New("subdir1")
if err != nil {
t.Fatalf("unexpected err: %v\n", err)
}
if "/foo/project/subdir1" != l2.Root() {
t.Fatalf("incorrect root: %s\n", l2.Root())
}
x := testCases[1]
b, err := l2.Load("fileB.yaml")
if err != nil {
t.Fatalf("unexpected load error %v", err)
}
if !reflect.DeepEqual([]byte(x.expectedContent), b) {
t.Fatalf("in load expected %s, but got %s", x.expectedContent, b)
}
}
func TestLoaderBadRelative(t *testing.T) {
l1, err := makeLoader().New("foo/project/subdir1")
if err != nil {
t.Fatalf("unexpected err: %v\n", err)
}
if "/foo/project/subdir1" != l1.Root() {
t.Fatalf("incorrect root: %s\n", l1.Root())
}
// Cannot cd into a file.
l2, err := l1.New("fileB.yaml")
if err == nil {
t.Fatalf("expected err, but got root %s", l2.Root())
}
// It's not okay to stay at the same place.
l2, err = l1.New(".")
if err == nil {
t.Fatalf("expected err, but got root %s", l2.Root())
}
// It's not okay to go up and back down into same place.
l2, err = l1.New("../subdir1")
if err == nil {
t.Fatalf("expected err, but got root %s", l2.Root())
}
// It's not okay to go up via a relative path.
l2, err = l1.New("..")
if err == nil {
t.Fatalf("expected err, but got root %s", l2.Root())
}
// It's not okay to go up via an absolute path.
l2, err = l1.New("/foo/project")
if err == nil {
t.Fatalf("expected err, but got root %s", l2.Root())
}
// It's not okay to go to the root.
l2, err = l1.New("/")
if err == nil {
t.Fatalf("expected err, but got root %s", l2.Root())
}
// It's okay to go up and down to a sibling.
l2, err = l1.New("../subdir2")
if err != nil {
t.Fatalf("unexpected new error %v", err)
}
if "/foo/project/subdir2" != l2.Root() {
t.Fatalf("incorrect root: %s\n", l2.Root())
}
x := testCases[2]
b, err := l2.Load("fileC.yaml")
if err != nil {
t.Fatalf("unexpected load error %v", err)
}
if !reflect.DeepEqual([]byte(x.expectedContent), b) {
t.Fatalf("in load expected %s, but got %s", x.expectedContent, b)
}
// It's not OK to go over to a previously visited directory.
// Must disallow going back and forth in a cycle.
l1, err = l2.New("../subdir1")
if err == nil {
t.Fatalf("expected err, but got root %s", l1.Root())
}
}
func TestLoaderMisc(t *testing.T) {
l := makeLoader()
_, err := l.New("")
if err == nil {
t.Fatalf("Expected error for empty root location not returned")
}
_, err = l.New("https://google.com/project")
if err == nil {
t.Fatalf("Expected error")
}
}
const (
contentOk = "hi there, i'm OK data"
contentExteriorData = "i am data from outside the root"
)
// Create a structure like this
//
// /tmp/kustomize-test-random
// ├── base
// │ ├── okayData
// │ ├── symLinkToOkayData -> okayData
// │ └── symLinkToExteriorData -> ../exteriorData
// └── exteriorData
//
func commonSetupForLoaderRestrictionTest() (string, filesys.FileSystem, error) {
dir, err := ioutil.TempDir("", "kustomize-test-")
if err != nil {
return "", nil, err
}
fSys := filesys.MakeFsOnDisk()
fSys.Mkdir(filepath.Join(dir, "base"))
fSys.WriteFile(
filepath.Join(dir, "base", "okayData"), []byte(contentOk))
fSys.WriteFile(
filepath.Join(dir, "exteriorData"), []byte(contentExteriorData))
os.Symlink(
filepath.Join(dir, "base", "okayData"),
filepath.Join(dir, "base", "symLinkToOkayData"))
os.Symlink(
filepath.Join(dir, "exteriorData"),
filepath.Join(dir, "base", "symLinkToExteriorData"))
return dir, fSys, nil
}
// Make sure everything works when loading files
// in or below the loader root.
func doSanityChecksAndDropIntoBase(
t *testing.T, l ifc.Loader) ifc.Loader {
data, err := l.Load(path.Join("base", "okayData"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(data) != contentOk {
t.Fatalf("unexpected content: %v", data)
}
data, err = l.Load("exteriorData")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(data) != contentExteriorData {
t.Fatalf("unexpected content: %v", data)
}
// Drop in.
l, err = l.New("base")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Reading okayData works.
data, err = l.Load("okayData")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(data) != contentOk {
t.Fatalf("unexpected content: %v", data)
}
// Reading local symlink to okayData works.
data, err = l.Load("symLinkToOkayData")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(data) != contentOk {
t.Fatalf("unexpected content: %v", data)
}
return l
}
func TestRestrictionRootOnlyInRealLoader(t *testing.T) {
dir, fSys, err := commonSetupForLoaderRestrictionTest()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer os.RemoveAll(dir)
var l ifc.Loader
l = newLoaderOrDie(RestrictionRootOnly, fSys, dir)
l = doSanityChecksAndDropIntoBase(t, l)
// Reading symlink to exteriorData fails.
_, err = l.Load("symLinkToExteriorData")
if err == nil {
t.Fatalf("expected error")
}
if !strings.Contains(err.Error(), "is not in or below") {
t.Fatalf("unexpected err: %v", err)
}
// Attempt to read "up" fails, though earlier we were
// able to read this file when root was "..".
_, err = l.Load("../exteriorData")
if err == nil {
t.Fatalf("expected error")
}
if !strings.Contains(err.Error(), "is not in or below") {
t.Fatalf("unexpected err: %v", err)
}
}
func TestRestrictionNoneInRealLoader(t *testing.T) {
dir, fSys, err := commonSetupForLoaderRestrictionTest()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer os.RemoveAll(dir)
var l ifc.Loader
l = newLoaderOrDie(RestrictionNone, fSys, dir)
l = doSanityChecksAndDropIntoBase(t, l)
// Reading symlink to exteriorData works.
_, err = l.Load("symLinkToExteriorData")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Attempt to read "up" works.
_, err = l.Load("../exteriorData")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func splitOnNthSlash(v string, n int) (string, string) {
left := ""
for i := 0; i < n; i++ {
k := strings.Index(v, "/")
if k < 0 {
break
}
left = left + v[:k+1]
v = v[k+1:]
}
return left[:len(left)-1], v
}
func TestSplit(t *testing.T) {
p := "a/b/c/d/e/f/g"
if left, right := splitOnNthSlash(p, 2); left != "a/b" || right != "c/d/e/f/g" {
t.Fatalf("got left='%s', right='%s'", left, right)
}
if left, right := splitOnNthSlash(p, 3); left != "a/b/c" || right != "d/e/f/g" {
t.Fatalf("got left='%s', right='%s'", left, right)
}
if left, right := splitOnNthSlash(p, 6); left != "a/b/c/d/e/f" || right != "g" {
t.Fatalf("got left='%s', right='%s'", left, right)
}
}
func TestNewLoaderAtGitClone(t *testing.T) {
rootUrl := "github.com/someOrg/someRepo"
pathInRepo := "foo/base"
url := rootUrl + "/" + pathInRepo
coRoot := "/tmp"
fSys := filesys.MakeFsInMemory()
fSys.MkdirAll(coRoot)
fSys.MkdirAll(coRoot + "/" + pathInRepo)
fSys.WriteFile(
coRoot+"/"+pathInRepo+"/"+
pgmconfig.DefaultKustomizationFileName(),
[]byte(`
whatever
`))
repoSpec, err := git.NewRepoSpecFromUrl(url)
if err != nil {
t.Fatalf("unexpected err: %v\n", err)
}
l, err := newLoaderAtGitClone(
repoSpec, fSys, nil,
git.DoNothingCloner(filesys.ConfirmedDir(coRoot)))
if err != nil {
t.Fatalf("unexpected err: %v\n", err)
}
if coRoot+"/"+pathInRepo != l.Root() {
t.Fatalf("expected root '%s', got '%s'\n",
coRoot+"/"+pathInRepo, l.Root())
}
if _, err = l.New(url); err == nil {
t.Fatalf("expected cycle error 1")
}
if _, err = l.New(rootUrl + "/" + "foo"); err == nil {
t.Fatalf("expected cycle error 2")
}
pathInRepo = "foo/overlay"
fSys.MkdirAll(coRoot + "/" + pathInRepo)
url = rootUrl + "/" + pathInRepo
l2, err := l.New(url)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if coRoot+"/"+pathInRepo != l2.Root() {
t.Fatalf("expected root '%s', got '%s'\n",
coRoot+"/"+pathInRepo, l2.Root())
}
}
func TestLoaderDisallowsLocalBaseFromRemoteOverlay(t *testing.T) {
// Define an overlay-base structure in the file system.
topDir := "/whatever"
cloneRoot := topDir + "/someClone"
fSys := filesys.MakeFsInMemory()
fSys.MkdirAll(topDir + "/highBase")
fSys.MkdirAll(cloneRoot + "/foo/base")
fSys.MkdirAll(cloneRoot + "/foo/overlay")
var l1 ifc.Loader
// Establish that a local overlay can navigate
// to the local bases.
l1 = newLoaderOrDie(
RestrictionRootOnly, fSys, cloneRoot+"/foo/overlay")
if l1.Root() != cloneRoot+"/foo/overlay" {
t.Fatalf("unexpected root %s", l1.Root())
}
l2, err := l1.New("../base")
if err != nil {
t.Fatalf("unexpected err: %v\n", err)
}
if l2.Root() != cloneRoot+"/foo/base" {
t.Fatalf("unexpected root %s", l2.Root())
}
l3, err := l2.New("../../../highBase")
if err != nil {
t.Fatalf("unexpected err: %v\n", err)
}
if l3.Root() != topDir+"/highBase" {
t.Fatalf("unexpected root %s", l3.Root())
}
// Establish that a Kustomization found in cloned
// repo can reach (non-remote) bases inside the clone
// but cannot reach a (non-remote) base outside the
// clone but legitimately on the local file system.
// This is to avoid a surprising interaction between
// a remote K and local files. The remote K would be
// non-functional on its own since by definition it
// would refer to a non-remote base file that didn't
// exist in its own repository, so presumably the
// remote K would be deliberately designed to phish
// for local K's.
repoSpec, err := git.NewRepoSpecFromUrl(
"github.com/someOrg/someRepo/foo/overlay")
if err != nil {
t.Fatalf("unexpected err: %v\n", err)
}
l1, err = newLoaderAtGitClone(
repoSpec, fSys, nil,
git.DoNothingCloner(filesys.ConfirmedDir(cloneRoot)))
if err != nil {
t.Fatalf("unexpected err: %v\n", err)
}
if l1.Root() != cloneRoot+"/foo/overlay" {
t.Fatalf("unexpected root %s", l1.Root())
}
// This is okay.
l2, err = l1.New("../base")
if err != nil {
t.Fatalf("unexpected err: %v\n", err)
}
if l2.Root() != cloneRoot+"/foo/base" {
t.Fatalf("unexpected root %s", l2.Root())
}
// This is not okay.
l3, err = l2.New("../../../highBase")
if err == nil {
t.Fatalf("expected err")
}
if !strings.Contains(err.Error(),
"base '/whatever/highBase' is outside '/whatever/someClone'") {
t.Fatalf("unexpected err: %v", err)
}
}
func TestLocalLoaderReferencingGitBase(t *testing.T) {
topDir := "/whatever"
cloneRoot := topDir + "/someClone"
fSys := filesys.MakeFsInMemory()
fSys.MkdirAll(topDir)
fSys.MkdirAll(cloneRoot + "/foo/base")
root, err := demandDirectoryRoot(fSys, topDir)
if err != nil {
t.Fatalf("unexpected err: %v\n", err)
}
l1 := newLoaderAtConfirmedDir(
RestrictionRootOnly, root, fSys, nil,
git.DoNothingCloner(filesys.ConfirmedDir(cloneRoot)))
if l1.Root() != topDir {
t.Fatalf("unexpected root %s", l1.Root())
}
l2, err := l1.New("github.com/someOrg/someRepo/foo/base")
if err != nil {
t.Fatalf("unexpected err: %v\n", err)
}
if l2.Root() != cloneRoot+"/foo/base" {
t.Fatalf("unexpected root %s", l2.Root())
}
}
func TestRepoDirectCycleDetection(t *testing.T) {
topDir := "/cycles"
cloneRoot := topDir + "/someClone"
fSys := filesys.MakeFsInMemory()
fSys.MkdirAll(topDir)
fSys.MkdirAll(cloneRoot)
root, err := demandDirectoryRoot(fSys, topDir)
if err != nil {
t.Fatalf("unexpected err: %v\n", err)
}
l1 := newLoaderAtConfirmedDir(
RestrictionRootOnly, root, fSys, nil,
git.DoNothingCloner(filesys.ConfirmedDir(cloneRoot)))
p1 := "github.com/someOrg/someRepo/foo"
rs1, err := git.NewRepoSpecFromUrl(p1)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
l1.repoSpec = rs1
_, err = l1.New(p1)
if err == nil {
t.Fatalf("expected error")
}
if !strings.Contains(err.Error(), "cycle detected") {
t.Fatalf("unexpected err: %v", err)
}
}
func TestRepoIndirectCycleDetection(t *testing.T) {
topDir := "/cycles"
cloneRoot := topDir + "/someClone"
fSys := filesys.MakeFsInMemory()
fSys.MkdirAll(topDir)
fSys.MkdirAll(cloneRoot)
root, err := demandDirectoryRoot(fSys, topDir)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
l0 := newLoaderAtConfirmedDir(
RestrictionRootOnly, root, fSys, nil,
git.DoNothingCloner(filesys.ConfirmedDir(cloneRoot)))
p1 := "github.com/someOrg/someRepo1"
p2 := "github.com/someOrg/someRepo2"
l1, err := l0.New(p1)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
l2, err := l1.New(p2)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
_, err = l2.New(p1)
if err == nil {
t.Fatalf("expected error")
}
if !strings.Contains(err.Error(), "cycle detected") {
t.Fatalf("unexpected err: %v", err)
}
}

34
api/loader/loader.go Normal file
View File

@@ -0,0 +1,34 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package loader has a data loading interface and various implementations.
package loader
import (
"sigs.k8s.io/kustomize/v3/api/filesys"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/pkg/git"
)
// NewLoader returns a Loader pointed at the given target.
// If the target is remote, the loader will be restricted
// to the root and below only. If the target is local, the
// loader will have the restrictions passed in. Regardless,
// if a local target attempts to transitively load remote bases,
// the remote bases will all be root-only restricted.
func NewLoader(
lr LoadRestrictorFunc,
target string, fSys filesys.FileSystem) (ifc.Loader, error) {
repoSpec, err := git.NewRepoSpecFromUrl(target)
if err == nil {
// The target qualifies as a remote git target.
return newLoaderAtGitClone(
repoSpec, fSys, nil, git.ClonerUsingGitExec)
}
root, err := demandDirectoryRoot(fSys, target)
if err != nil {
return nil, err
}
return newLoaderAtConfirmedDir(
lr, root, fSys, nil, git.ClonerUsingGitExec), nil
}

View File

@@ -0,0 +1,76 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package loader
import (
"fmt"
"github.com/spf13/pflag"
"sigs.k8s.io/kustomize/v3/api/filesys"
)
//go:generate stringer -type=loadRestrictions
type loadRestrictions int
const (
unknown loadRestrictions = iota
rootOnly
none
)
const (
flagName = "load_restrictor"
)
var (
flagValue = rootOnly.String()
flagHelp = "if set to '" + none.String() +
"', local kustomizations may load files from outside their root. " +
"This does, however, break the relocatability of the kustomization."
)
func AddFlagLoadRestrictor(set *pflag.FlagSet) {
set.StringVar(
&flagValue, flagName,
rootOnly.String(), flagHelp)
}
func ValidateFlagLoadRestrictor() (LoadRestrictorFunc, error) {
switch flagValue {
case rootOnly.String():
return RestrictionRootOnly, nil
case none.String():
return RestrictionNone, nil
default:
return nil, fmt.Errorf(
"illegal flag value --%s %s; legal values: %v",
flagName, flagValue,
[]string{rootOnly.String(), none.String()})
}
}
type LoadRestrictorFunc func(
filesys.FileSystem, filesys.ConfirmedDir, string) (string, error)
func RestrictionRootOnly(
fSys filesys.FileSystem, root filesys.ConfirmedDir, path string) (string, error) {
d, f, err := fSys.CleanedAbs(path)
if err != nil {
return "", err
}
if f == "" {
return "", fmt.Errorf("'%s' must be a file", path)
}
if !d.HasPrefix(root) {
return "", fmt.Errorf(
"security; file '%s' is not in or below '%s'",
path, root)
}
return d.Join(f), nil
}
func RestrictionNone(
_ filesys.FileSystem, _ filesys.ConfirmedDir, path string) (string, error) {
return path, nil
}

View File

@@ -0,0 +1,41 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by "stringer -type=loadRestrictions"; DO NOT EDIT.
package loader
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[unknown-0]
_ = x[rootOnly-1]
_ = x[none-2]
}
const _loadRestrictions_name = "unknownrootOnlynone"
var _loadRestrictions_index = [...]uint8{0, 7, 15, 19}
func (i loadRestrictions) String() string {
if i < 0 || i >= loadRestrictions(len(_loadRestrictions_index)-1) {
return "loadRestrictions(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _loadRestrictions_name[_loadRestrictions_index[i]:_loadRestrictions_index[i+1]]
}

View File

@@ -0,0 +1,74 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package loader
import (
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/api/filesys"
)
func TestRestrictionNone(t *testing.T) {
fSys := filesys.MakeFsInMemory()
root := filesys.ConfirmedDir("irrelevant")
path := "whatever"
p, err := RestrictionNone(fSys, root, path)
if err != nil {
t.Fatal(err)
}
if p != path {
t.Fatalf("expected '%s', got '%s'", path, p)
}
}
func TestRestrictionRootOnly(t *testing.T) {
fSys := filesys.MakeFsInMemory()
root := filesys.ConfirmedDir("/tmp/foo")
path := "/tmp/foo/whatever/beans"
p, err := RestrictionRootOnly(fSys, root, path)
if err != nil {
t.Fatal(err)
}
if p != path {
t.Fatalf("expected '%s', got '%s'", path, p)
}
// Legal.
path = "/tmp/foo/whatever/../../foo/whatever"
p, err = RestrictionRootOnly(fSys, root, path)
if err != nil {
t.Fatal(err)
}
path = "/tmp/foo/whatever"
if p != path {
t.Fatalf("expected '%s', got '%s'", path, p)
}
// Illegal.
path = "/tmp/illegal"
_, err = RestrictionRootOnly(fSys, root, path)
if err == nil {
t.Fatal("should have an error")
}
if !strings.Contains(
err.Error(),
"file '/tmp/illegal' is not in or below '/tmp/foo'") {
t.Fatalf("unexpected err: %s", err)
}
}

136
api/resmap/factory.go Normal file
View File

@@ -0,0 +1,136 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package resmap
import (
"github.com/pkg/errors"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/internal/kusterr"
)
// Factory makes instances of ResMap.
type Factory struct {
resF *resource.Factory
tf PatchFactory
}
// NewFactory returns a new resmap.Factory.
func NewFactory(rf *resource.Factory, tf PatchFactory) *Factory {
return &Factory{resF: rf, tf: tf}
}
// RF returns a resource.Factory.
func (rmF *Factory) RF() *resource.Factory {
return rmF.resF
}
func New() ResMap {
return newOne()
}
// FromResource returns a ResMap with one entry.
func (rmF *Factory) FromResource(res *resource.Resource) ResMap {
m, err := newResMapFromResourceSlice([]*resource.Resource{res})
if err != nil {
panic(err)
}
return m
}
// FromFile returns a ResMap given a resource path.
func (rmF *Factory) FromFile(
loader ifc.Loader, path string) (ResMap, error) {
content, err := loader.Load(path)
if err != nil {
return nil, err
}
m, err := rmF.NewResMapFromBytes(content)
if err != nil {
return nil, kusterr.Handler(err, path)
}
return m, nil
}
// NewResMapFromBytes decodes a list of objects in byte array format.
func (rmF *Factory) NewResMapFromBytes(b []byte) (ResMap, error) {
resources, err := rmF.resF.SliceFromBytes(b)
if err != nil {
return nil, err
}
return newResMapFromResourceSlice(resources)
}
// NewResMapFromConfigMapArgs returns a Resource slice given
// a configmap metadata slice from kustomization file.
func (rmF *Factory) NewResMapFromConfigMapArgs(
kvLdr ifc.KvLoader,
options *types.GeneratorOptions,
argList []types.ConfigMapArgs) (ResMap, error) {
var resources []*resource.Resource
for _, args := range argList {
res, err := rmF.resF.MakeConfigMap(kvLdr, options, &args)
if err != nil {
return nil, errors.Wrap(err, "NewResMapFromConfigMapArgs")
}
resources = append(resources, res)
}
return newResMapFromResourceSlice(resources)
}
func (rmF *Factory) FromConfigMapArgs(
kvLdr ifc.KvLoader,
options *types.GeneratorOptions,
args types.ConfigMapArgs) (ResMap, error) {
res, err := rmF.resF.MakeConfigMap(kvLdr, options, &args)
if err != nil {
return nil, err
}
return rmF.FromResource(res), nil
}
// NewResMapFromSecretArgs takes a SecretArgs slice, generates
// secrets from each entry, and accumulates them in a ResMap.
func (rmF *Factory) NewResMapFromSecretArgs(
kvLdr ifc.KvLoader,
options *types.GeneratorOptions,
argsList []types.SecretArgs) (ResMap, error) {
var resources []*resource.Resource
for _, args := range argsList {
res, err := rmF.resF.MakeSecret(kvLdr, options, &args)
if err != nil {
return nil, errors.Wrap(err, "NewResMapFromSecretArgs")
}
resources = append(resources, res)
}
return newResMapFromResourceSlice(resources)
}
func (rmF *Factory) FromSecretArgs(
kvLdr ifc.KvLoader,
options *types.GeneratorOptions,
args types.SecretArgs) (ResMap, error) {
res, err := rmF.resF.MakeSecret(kvLdr, options, &args)
if err != nil {
return nil, err
}
return rmF.FromResource(res), nil
}
func (rmF *Factory) MergePatches(patches []*resource.Resource) (
ResMap, error) {
return rmF.tf.MergePatches(patches, rmF.resF)
}
func newResMapFromResourceSlice(resources []*resource.Resource) (ResMap, error) {
result := New()
for _, res := range resources {
err := result.Append(res)
if err != nil {
return nil, err
}
}
return result, nil
}

266
api/resmap/factory_test.go Normal file
View File

@@ -0,0 +1,266 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package resmap_test
import (
"encoding/base64"
"reflect"
"sigs.k8s.io/kustomize/v3/api/resid"
"testing"
"sigs.k8s.io/kustomize/v3/api/filesys"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/kv"
"sigs.k8s.io/kustomize/v3/api/loader"
. "sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/testutils/resmaptest"
"sigs.k8s.io/kustomize/v3/api/testutils/valtest"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/internal/loadertest"
)
func TestFromFile(t *testing.T) {
resourceStr := `apiVersion: apps/v1
kind: Deployment
metadata:
name: dply1
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dply2
---
# some comment
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dply2
namespace: test
---
`
l := loadertest.NewFakeLoader("/whatever/project")
if ferr := l.AddFile("/whatever/project/deployment.yaml", []byte(resourceStr)); ferr != nil {
t.Fatalf("Error adding fake file: %v\n", ferr)
}
expected := resmaptest_test.NewRmBuilder(t, rf).
Add(map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "dply1",
}}).
Add(map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "dply2",
}}).
Add(map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "dply2",
"namespace": "test",
}}).ResMap()
m, _ := rmF.FromFile(l, "deployment.yaml")
if m.Size() != 3 {
t.Fatalf("result should contain 3, but got %d", m.Size())
}
if err := expected.ErrorIfNotEqualLists(m); err != nil {
t.Fatalf("actual doesn't match expected: %v", err)
}
}
func TestFromBytes(t *testing.T) {
encoded := []byte(`apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
`)
expected := resmaptest_test.NewRmBuilder(t, rf).
Add(map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
}}).
Add(map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm2",
}}).ResMap()
m, err := rmF.NewResMapFromBytes(encoded)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(m, expected) {
t.Fatalf("%#v doesn't match expected %#v", m, expected)
}
}
var cmap = resid.Gvk{Version: "v1", Kind: "ConfigMap"}
func TestNewFromConfigMaps(t *testing.T) {
type testCase struct {
description string
input []types.ConfigMapArgs
filepath string
content string
expected ResMap
}
l := loadertest.NewFakeLoader("/whatever/project")
kvLdr := kv.NewLoader(l, valtest_test.MakeFakeValidator())
testCases := []testCase{
{
description: "construct config map from env",
input: []types.ConfigMapArgs{
{
GeneratorArgs: types.GeneratorArgs{
Name: "envConfigMap",
KvPairSources: types.KvPairSources{
EnvSources: []string{"app.env"},
},
},
},
},
filepath: "/whatever/project/app.env",
content: "DB_USERNAME=admin\nDB_PASSWORD=somepw",
expected: resmaptest_test.NewRmBuilder(t, rf).Add(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "envConfigMap",
},
"data": map[string]interface{}{
"DB_USERNAME": "admin",
"DB_PASSWORD": "somepw",
}}).ResMap(),
},
{
description: "construct config map from file",
input: []types.ConfigMapArgs{{
GeneratorArgs: types.GeneratorArgs{
Name: "fileConfigMap",
KvPairSources: types.KvPairSources{
FileSources: []string{"app-init.ini"},
},
},
},
},
filepath: "/whatever/project/app-init.ini",
content: "FOO=bar\nBAR=baz\n",
expected: resmaptest_test.NewRmBuilder(t, rf).Add(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "fileConfigMap",
},
"data": map[string]interface{}{
"app-init.ini": `FOO=bar
BAR=baz
`,
},
}).ResMap(),
},
{
description: "construct config map from literal",
input: []types.ConfigMapArgs{
{
GeneratorArgs: types.GeneratorArgs{
Name: "literalConfigMap",
KvPairSources: types.KvPairSources{
LiteralSources: []string{"a=x", "b=y", "c=\"Good Morning\"", "d=\"false\""},
},
},
},
},
expected: resmaptest_test.NewRmBuilder(t, rf).Add(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "literalConfigMap",
},
"data": map[string]interface{}{
"a": "x",
"b": "y",
"c": "Good Morning",
"d": "false",
},
}).ResMap(),
},
// TODO: add testcase for data coming from multiple sources like
// files/literal/env etc.
}
for _, tc := range testCases {
if fErr := l.AddFile(tc.filepath, []byte(tc.content)); fErr != nil {
t.Fatalf("Error adding fake file: %v\n", fErr)
}
r, err := rmF.NewResMapFromConfigMapArgs(kvLdr, nil, tc.input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err = tc.expected.ErrorIfNotEqualLists(r); err != nil {
t.Fatalf("testcase: %q, err: %v", tc.description, err)
}
}
}
func TestNewResMapFromSecretArgs(t *testing.T) {
secrets := []types.SecretArgs{
{
GeneratorArgs: types.GeneratorArgs{
Name: "apple",
KvPairSources: types.KvPairSources{
LiteralSources: []string{
"DB_USERNAME=admin",
"DB_PASSWORD=somepw",
},
},
},
Type: ifc.SecretTypeOpaque,
},
}
fSys := filesys.MakeFsInMemory()
fSys.Mkdir(".")
actual, err := rmF.NewResMapFromSecretArgs(
kv.NewLoader(
loader.NewFileLoaderAtRoot(fSys),
valtest_test.MakeFakeValidator()), nil, secrets)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := resmaptest_test.NewRmBuilder(t, rf).Add(
map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "apple",
},
"type": ifc.SecretTypeOpaque,
"data": map[string]interface{}{
"DB_USERNAME": base64.StdEncoding.EncodeToString([]byte("admin")),
"DB_PASSWORD": base64.StdEncoding.EncodeToString([]byte("somepw")),
},
}).ResMap()
if err = expected.ErrorIfNotEqualLists(actual); err != nil {
t.Fatalf("error: %s", err)
}
}

37
api/resmap/idslice.go Normal file
View File

@@ -0,0 +1,37 @@
/*
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 resmap
import (
"sort"
"sigs.k8s.io/kustomize/v3/api/resid"
)
// IdSlice implements the sort interface.
type IdSlice []resid.ResId
var _ sort.Interface = IdSlice{}
func (a IdSlice) Len() int { return len(a) }
func (a IdSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a IdSlice) Less(i, j int) bool {
if !a[i].Gvk.Equals(a[j].Gvk) {
return a[i].Gvk.IsLessThan(a[j].Gvk)
}
return a[i].String() < a[j].String()
}

View File

@@ -0,0 +1,52 @@
/*
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 resmap
import (
"reflect"
"sort"
"testing"
"sigs.k8s.io/kustomize/v3/api/resid"
)
func TestLess(t *testing.T) {
ids := IdSlice{
resid.NewResIdKindOnly("ConfigMap", "cm"),
resid.NewResIdKindOnly("Pod", "pod"),
resid.NewResIdKindOnly("Namespace", "ns1"),
resid.NewResIdKindOnly("Namespace", "ns2"),
resid.NewResIdKindOnly("Role", "ro"),
resid.NewResIdKindOnly("RoleBinding", "rb"),
resid.NewResIdKindOnly("CustomResourceDefinition", "crd"),
resid.NewResIdKindOnly("ServiceAccount", "sa"),
}
expected := IdSlice{
resid.NewResIdKindOnly("Namespace", "ns1"),
resid.NewResIdKindOnly("Namespace", "ns2"),
resid.NewResIdKindOnly("CustomResourceDefinition", "crd"),
resid.NewResIdKindOnly("ServiceAccount", "sa"),
resid.NewResIdKindOnly("Role", "ro"),
resid.NewResIdKindOnly("RoleBinding", "rb"),
resid.NewResIdKindOnly("ConfigMap", "cm"),
resid.NewResIdKindOnly("Pod", "pod"),
}
sort.Sort(ids)
if !reflect.DeepEqual(ids, expected) {
t.Fatalf("expected %+v but got %+v", expected, ids)
}
}

View File

@@ -0,0 +1,15 @@
/// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package patch holds miscellaneous interfaces used by kustomize.
package resmap
import (
"sigs.k8s.io/kustomize/v3/api/resource"
)
// PatchFactory makes transformers that require k8sdeps.
type PatchFactory interface {
MergePatches(patches []*resource.Resource,
rf *resource.Factory) (ResMap, error)
}

779
api/resmap/resmap.go Normal file
View File

@@ -0,0 +1,779 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package resmap implements a map from ResId to Resource that
// tracks all resources in a kustomization.
package resmap
import (
"bytes"
"fmt"
"regexp"
"github.com/pkg/errors"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/resid"
"sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/yaml"
)
// A Transformer modifies an instance of ResMap.
type Transformer interface {
// Transform modifies data in the argument,
// e.g. adding labels to resources that can be labelled.
Transform(m ResMap) error
}
// A Generator creates an instance of ResMap.
type Generator interface {
Generate() (ResMap, error)
}
// Something that's configurable accepts an
// instance of PluginHelpers and a raw config
// object (YAML in []byte form).
type Configurable interface {
Config(h *PluginHelpers, config []byte) error
}
// NewPluginHelpers makes an instance of PluginHelpers.
func NewPluginHelpers(ldr ifc.Loader, v ifc.Validator, rf *Factory) *PluginHelpers {
return &PluginHelpers{ldr: ldr, v: v, rf: rf}
}
// PluginHelpers holds things that any or all plugins might need.
// This should be available to each plugin, in addition to
// any plugin-specific configuration.
type PluginHelpers struct {
ldr ifc.Loader
v ifc.Validator
rf *Factory
}
func (c *PluginHelpers) Loader() ifc.Loader {
return c.ldr
}
func (c *PluginHelpers) ResmapFactory() *Factory {
return c.rf
}
func (c *PluginHelpers) Validator() ifc.Validator {
return c.v
}
type GeneratorPlugin interface {
Generator
Configurable
}
type TransformerPlugin interface {
Transformer
Configurable
}
// ResMap is an interface describing operations on the
// core kustomize data structure, a list of Resources.
//
// Every Resource has two ResIds: OrgId and CurId.
//
// In a ResMap, no two resources may have the same CurId,
// but they may have the same OrgId. The latter can happen
// when mixing two or more different overlays apply different
// transformations to a common base. When looking for a
// resource to transform, try the OrgId first, and if this
// fails or finds too many, it might make sense to then try
// the CurrId. Depends on the situation.
type ResMap interface {
// Size reports the number of resources.
Size() int
// Resources provides a discardable slice
// of resource pointers, returned in the order
// as appended.
Resources() []*resource.Resource
// Append adds a Resource. Error on CurId collision.
//
// A class invariant of ResMap is that all of its
// resources must differ in their value of
// CurId(), aka current Id. The Id is the tuple
// of {namespace, group, version, kind, name}
// (see ResId).
//
// This invariant reflects the invariant of a
// kubernetes cluster, where if one tries to add
// a resource to the cluster whose Id matches
// that of a resource already in the cluster,
// only two outcomes are allowed. Either the
// incoming resource is _merged_ into the existing
// one, or the incoming resource is rejected.
// One cannot end up with two resources
// in the cluster with the same Id.
Append(*resource.Resource) error
// AppendAll appends another ResMap to self,
// failing on any OrgId collision.
AppendAll(ResMap) error
// AbsorbAll appends, replaces or merges the contents
// of another ResMap into self,
// allowing and sometimes demanding ID collisions.
// A collision would be demanded, say, when a generated
// ConfigMap has the "replace" option in its generation
// instructions, meaning it _must_ replace
// something in the known set of resources.
// If a resource id for resource X is found to already
// be in self, then the behavior field for X must
// be BehaviorMerge or BehaviorReplace. If X is not in
// self, then its behavior _cannot_ be merge or replace.
AbsorbAll(ResMap) error
// AsYaml returns the yaml form of resources.
AsYaml() ([]byte, error)
// GetByIndex returns a resource at the given index,
// nil if out of range.
GetByIndex(int) *resource.Resource
// GetIndexOfCurrentId returns the index of the resource
// with the given CurId.
// Returns error if there is more than one match.
// Returns (-1, nil) if there is no match.
GetIndexOfCurrentId(id resid.ResId) (int, error)
// GetMatchingResourcesByCurrentId returns the resources
// who's CurId is matched by the argument.
GetMatchingResourcesByCurrentId(matches IdMatcher) []*resource.Resource
// GetMatchingResourcesByOriginalId returns the resources
// who's OriginalId is matched by the argument.
GetMatchingResourcesByOriginalId(matches IdMatcher) []*resource.Resource
// GetByCurrentId is shorthand for calling
// GetMatchingResourcesByCurrentId with a matcher requiring
// an exact match, returning an error on multiple or no matches.
GetByCurrentId(resid.ResId) (*resource.Resource, error)
// GetByOriginalId is shorthand for calling
// GetMatchingResourcesByOriginalId with a matcher requiring
// an exact match, returning an error on multiple or no matches.
GetByOriginalId(resid.ResId) (*resource.Resource, error)
// GetById is a helper function which first
// attempts GetByOriginalId, then GetByCurrentId,
// returning an error if both fail to find a single
// match.
GetById(resid.ResId) (*resource.Resource, error)
// GroupedByCurrentNamespace returns a map of namespace
// to a slice of *Resource in that namespace.
// Resources for whom IsNamespaceableKind is false are
// are not included at all (see NonNamespaceable).
// Resources with an empty namespace are placed
// in the resid.DefaultNamespace entry.
GroupedByCurrentNamespace() map[string][]*resource.Resource
// GroupByOrginalNamespace performs as GroupByNamespace
// but use the original namespace instead of the current
// one to perform the grouping.
GroupedByOriginalNamespace() map[string][]*resource.Resource
// NonNamespaceable returns a slice of resources that
// cannot be placed in a namespace, e.g.
// Node, ClusterRole, Namespace itself, etc.
NonNamespaceable() []*resource.Resource
// AllIds returns all CurrentIds.
AllIds() []resid.ResId
// Replace replaces the resource with the matching CurId.
// Error if there's no match or more than one match.
// Returns the index where the replacement happened.
Replace(*resource.Resource) (int, error)
// Remove removes the resource whose CurId matches the argument.
// Error if not found.
Remove(resid.ResId) error
// Clear removes all resources and Ids.
Clear()
// SubsetThatCouldBeReferencedByResource returns a ResMap subset
// of self with resources that could be referenced by the
// resource argument.
// This is a filter; it excludes things that cannot be
// referenced by the resource, e.g. objects in other
// namespaces. Cluster wide objects are never excluded.
SubsetThatCouldBeReferencedByResource(*resource.Resource) ResMap
// DeepCopy copies the ResMap and underlying resources.
DeepCopy() ResMap
// ShallowCopy copies the ResMap but
// not the underlying resources.
ShallowCopy() ResMap
// ErrorIfNotEqualSets returns an error if the
// argument doesn't have the same resources as self.
// Ordering is _not_ taken into account,
// as this function was solely used in tests written
// before internal resource order was maintained,
// and those tests are initialized with maps which
// by definition have random ordering, and will
// fail spuriously.
// TODO: modify tests to not use resmap.FromMap,
// TODO: - and replace this with a stricter equals.
ErrorIfNotEqualSets(ResMap) error
// ErrorIfNotEqualLists returns an error if the
// argument doesn't have the resource objects
// data as self, in the same order.
// Meta information is ignored; this is similar
// to comparing the AsYaml() strings, but allows
// for more informed errors on not equals.
ErrorIfNotEqualLists(ResMap) error
// Debug prints the ResMap.
Debug(title string)
// Select returns a list of resources that
// are selected by a Selector
Select(types.Selector) ([]*resource.Resource, error)
}
// resWrangler holds the content manipulated by kustomize.
type resWrangler struct {
// Resource list maintained in load (append) order.
// This is important for transformers, which must
// be performed in a specific order, and for users
// who for whatever reasons wish the order they
// specify in kustomizations to be maintained and
// available as an option for final YAML rendering.
rList []*resource.Resource
}
func newOne() *resWrangler {
result := &resWrangler{}
result.Clear()
return result
}
// Clear implements ResMap.
func (m *resWrangler) Clear() {
m.rList = nil
}
// Size implements ResMap.
func (m *resWrangler) Size() int {
return len(m.rList)
}
func (m *resWrangler) indexOfResource(other *resource.Resource) int {
for i, r := range m.rList {
if r == other {
return i
}
}
return -1
}
// Resources implements ResMap.
func (m *resWrangler) Resources() []*resource.Resource {
tmp := make([]*resource.Resource, len(m.rList))
copy(tmp, m.rList)
return tmp
}
// Append implements ResMap.
func (m *resWrangler) Append(res *resource.Resource) error {
id := res.CurId()
if r := m.GetMatchingResourcesByCurrentId(id.Equals); len(r) > 0 {
return fmt.Errorf(
"may not add resource with an already registered id: %s", id)
}
m.rList = append(m.rList, res)
return nil
}
// Remove implements ResMap.
func (m *resWrangler) Remove(adios resid.ResId) error {
tmp := newOne()
for _, r := range m.rList {
if r.CurId() != adios {
tmp.Append(r)
}
}
if tmp.Size() != m.Size()-1 {
return fmt.Errorf("id %s not found in removal", adios)
}
m.rList = tmp.rList
return nil
}
// Replace implements ResMap.
func (m *resWrangler) Replace(res *resource.Resource) (int, error) {
id := res.CurId()
i, err := m.GetIndexOfCurrentId(id)
if err != nil {
return -1, errors.Wrap(err, "in Replace")
}
if i < 0 {
return -1, fmt.Errorf("cannot find resource with id %s to replace", id)
}
m.rList[i] = res
return i, nil
}
// AllIds implements ResMap.
func (m *resWrangler) AllIds() (ids []resid.ResId) {
ids = make([]resid.ResId, m.Size())
for i, r := range m.rList {
ids[i] = r.CurId()
}
return
}
// Debug implements ResMap.
func (m *resWrangler) Debug(title string) {
fmt.Println("--------------------------- " + title)
firstObj := true
for i, r := range m.rList {
if firstObj {
firstObj = false
} else {
fmt.Println("---")
}
fmt.Printf("# %d %s\n", i, r.OrgId())
blob, err := yaml.Marshal(r.Map())
if err != nil {
panic(err)
}
fmt.Println(string(blob))
}
}
type IdMatcher func(resid.ResId) bool
// GetByIndex implements ResMap.
func (m *resWrangler) GetByIndex(i int) *resource.Resource {
if i < 0 || i >= m.Size() {
return nil
}
return m.rList[i]
}
// GetIndexOfCurrentId implements ResMap.
func (m *resWrangler) GetIndexOfCurrentId(id resid.ResId) (int, error) {
count := 0
result := -1
for i, r := range m.rList {
if id.Equals(r.CurId()) {
count++
result = i
}
}
if count > 1 {
return -1, fmt.Errorf("id matched %d resources", count)
}
return result, nil
}
type IdFromResource func(r *resource.Resource) resid.ResId
func GetOriginalId(r *resource.Resource) resid.ResId { return r.OrgId() }
func GetCurrentId(r *resource.Resource) resid.ResId { return r.CurId() }
// GetMatchingResourcesByCurrentId implements ResMap.
func (m *resWrangler) GetMatchingResourcesByCurrentId(
matches IdMatcher) []*resource.Resource {
return m.filteredById(matches, GetCurrentId)
}
// GetMatchingResourcesByOriginalId implements ResMap.
func (m *resWrangler) GetMatchingResourcesByOriginalId(
matches IdMatcher) []*resource.Resource {
return m.filteredById(matches, GetOriginalId)
}
func (m *resWrangler) filteredById(
matches IdMatcher, idGetter IdFromResource) []*resource.Resource {
var result []*resource.Resource
for _, r := range m.rList {
if matches(idGetter(r)) {
result = append(result, r)
}
}
return result
}
// GetByCurrentId implements ResMap.
func (m *resWrangler) GetByCurrentId(
id resid.ResId) (*resource.Resource, error) {
return demandOneMatch(m.GetMatchingResourcesByCurrentId, id, "Current")
}
// GetByOriginalId implements ResMap.
func (m *resWrangler) GetByOriginalId(
id resid.ResId) (*resource.Resource, error) {
return demandOneMatch(m.GetMatchingResourcesByOriginalId, id, "Original")
}
// GetById implements ResMap.
func (m *resWrangler) GetById(
id resid.ResId) (*resource.Resource, error) {
match, err1 := m.GetByOriginalId(id)
if err1 == nil {
return match, nil
}
match, err2 := m.GetByCurrentId(id)
if err2 == nil {
return match, nil
}
return nil, fmt.Errorf(
"%s; %s; failed to find unique target for patch %s",
err1.Error(), err2.Error(), id.GvknString())
}
type resFinder func(IdMatcher) []*resource.Resource
func demandOneMatch(
f resFinder, id resid.ResId, s string) (*resource.Resource, error) {
r := f(id.Equals)
if len(r) == 1 {
return r[0], nil
}
if len(r) > 1 {
return nil, fmt.Errorf("multiple matches for %sId %s", s, id)
}
return nil, fmt.Errorf("no matches for %sId %s", s, id)
}
// GroupedByCurrentNamespace implements ResMap.GroupByCurrentNamespace
func (m *resWrangler) GroupedByCurrentNamespace() map[string][]*resource.Resource {
items := m.groupedByCurrentNamespace()
delete(items, resid.TotallyNotANamespace)
return items
}
// NonNamespaceable implements ResMap.NonNamespaceable
func (m *resWrangler) NonNamespaceable() []*resource.Resource {
return m.groupedByCurrentNamespace()[resid.TotallyNotANamespace]
}
func (m *resWrangler) groupedByCurrentNamespace() map[string][]*resource.Resource {
byNamespace := make(map[string][]*resource.Resource)
for _, res := range m.rList {
namespace := res.CurId().EffectiveNamespace()
if _, found := byNamespace[namespace]; !found {
byNamespace[namespace] = []*resource.Resource{}
}
byNamespace[namespace] = append(byNamespace[namespace], res)
}
return byNamespace
}
// GroupedByNamespace implements ResMap.GroupByOrginalNamespace
func (m *resWrangler) GroupedByOriginalNamespace() map[string][]*resource.Resource {
items := m.groupedByOriginalNamespace()
delete(items, resid.TotallyNotANamespace)
return items
}
func (m *resWrangler) groupedByOriginalNamespace() map[string][]*resource.Resource {
byNamespace := make(map[string][]*resource.Resource)
for _, res := range m.rList {
namespace := res.OrgId().EffectiveNamespace()
if _, found := byNamespace[namespace]; !found {
byNamespace[namespace] = []*resource.Resource{}
}
byNamespace[namespace] = append(byNamespace[namespace], res)
}
return byNamespace
}
// AsYaml implements ResMap.
func (m *resWrangler) AsYaml() ([]byte, error) {
firstObj := true
var b []byte
buf := bytes.NewBuffer(b)
for _, res := range m.Resources() {
out, err := yaml.Marshal(res.Map())
if err != nil {
return nil, err
}
if firstObj {
firstObj = false
} else {
if _, err = buf.WriteString("---\n"); err != nil {
return nil, err
}
}
if _, err = buf.Write(out); err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}
// ErrorIfNotEqualSets implements ResMap.
func (m *resWrangler) ErrorIfNotEqualSets(other ResMap) error {
m2, ok := other.(*resWrangler)
if !ok {
panic("bad cast")
}
if m.Size() != m2.Size() {
return fmt.Errorf(
"lists have different number of entries: %#v doesn't equal %#v",
m.rList, m2.rList)
}
seen := make(map[int]bool)
for _, r1 := range m.rList {
id := r1.CurId()
others := m2.GetMatchingResourcesByCurrentId(id.Equals)
if len(others) < 0 {
return fmt.Errorf(
"id in self missing from other; id: %s", id)
}
if len(others) > 1 {
return fmt.Errorf(
"id in self matches %d in other; id: %s", len(others), id)
}
r2 := others[0]
if !r1.KunstructEqual(r2) {
return fmt.Errorf(
"kunstruct not equal: \n -- %s,\n -- %s\n\n--\n%#v\n------\n%#v\n",
r1, r2, r1, r2)
}
seen[m2.indexOfResource(r2)] = true
}
if len(seen) != m.Size() {
return fmt.Errorf("counting problem %d != %d", len(seen), m.Size())
}
return nil
}
// ErrorIfNotEqualList implements ResMap.
func (m *resWrangler) ErrorIfNotEqualLists(other ResMap) error {
m2, ok := other.(*resWrangler)
if !ok {
panic("bad cast")
}
if m.Size() != m2.Size() {
return fmt.Errorf(
"lists have different number of entries: %#v doesn't equal %#v",
m.rList, m2.rList)
}
for i, r1 := range m.rList {
r2 := m2.rList[i]
if !r1.Equals(r2) {
return fmt.Errorf(
"Item i=%d differs:\n n1 = %s\n n2 = %s\n o1 = %s\n o2 = %s\n",
i, r1.OrgId(), r2.OrgId(), r1, r2)
}
}
return nil
}
type resCopier func(r *resource.Resource) *resource.Resource
// ShallowCopy implements ResMap.
func (m *resWrangler) ShallowCopy() ResMap {
return m.makeCopy(
func(r *resource.Resource) *resource.Resource {
return r
})
}
// DeepCopy implements ResMap.
func (m *resWrangler) DeepCopy() ResMap {
return m.makeCopy(
func(r *resource.Resource) *resource.Resource {
return r.DeepCopy()
})
}
// makeCopy copies the ResMap.
func (m *resWrangler) makeCopy(copier resCopier) ResMap {
result := &resWrangler{}
result.rList = make([]*resource.Resource, m.Size())
for i, r := range m.rList {
result.rList[i] = copier(r)
}
return result
}
// SubsetThatCouldBeReferencedByResource implements ResMap.
func (m *resWrangler) SubsetThatCouldBeReferencedByResource(
inputRes *resource.Resource) ResMap {
result := newOne()
inputId := inputRes.CurId()
isInputIdNamespaceable := inputId.IsNamespaceableKind()
rctxm := inputRes.PrefixesSuffixesEquals
for _, r := range m.Resources() {
// Need to match more accuratly both at the time of selection and transformation.
// OutmostPrefixSuffixEquals is not accurate enough since it is only using
// the outer most suffix and the last prefix. Use PrefixedSuffixesEquals instead.
resId := r.CurId()
if (!isInputIdNamespaceable || !resId.IsNamespaceableKind() || resId.IsNsEquals(inputId)) &&
r.InSameKustomizeCtx(rctxm) {
result.append(r)
}
}
return result
}
func (m *resWrangler) append(res *resource.Resource) {
m.rList = append(m.rList, res)
}
// AppendAll implements ResMap.
func (m *resWrangler) AppendAll(other ResMap) error {
if other == nil {
return nil
}
for _, res := range other.Resources() {
if err := m.Append(res); err != nil {
return err
}
}
return nil
}
// AbsorbAll implements ResMap.
func (m *resWrangler) AbsorbAll(other ResMap) error {
if other == nil {
return nil
}
for _, r := range other.Resources() {
err := m.appendReplaceOrMerge(r)
if err != nil {
return err
}
}
return nil
}
func (m *resWrangler) appendReplaceOrMerge(
res *resource.Resource) error {
id := res.CurId()
matches := m.GetMatchingResourcesByOriginalId(id.Equals)
if len(matches) == 0 {
matches = m.GetMatchingResourcesByCurrentId(id.Equals)
}
switch len(matches) {
case 0:
switch res.Behavior() {
case types.BehaviorMerge, types.BehaviorReplace:
return fmt.Errorf(
"id %#v does not exist; cannot merge or replace", id)
default:
// presumably types.BehaviorCreate
err := m.Append(res)
if err != nil {
return err
}
}
case 1:
old := matches[0]
if old == nil {
return fmt.Errorf("id lookup failure")
}
index := m.indexOfResource(old)
if index < 0 {
return fmt.Errorf("indexing problem")
}
switch res.Behavior() {
case types.BehaviorReplace:
res.Replace(old)
case types.BehaviorMerge:
res.Merge(old)
default:
return fmt.Errorf(
"id %#v exists; must merge or replace", id)
}
i, err := m.Replace(res)
if err != nil {
return err
}
if i != index {
return fmt.Errorf("unexpected index in replacement")
}
default:
return fmt.Errorf(
"found multiple objects %v that could accept merge of %v",
matches, id)
}
return nil
}
func anchorRegex(pattern string) string {
if pattern == "" {
return pattern
}
return "^" + pattern + "$"
}
// Select returns a list of resources that
// are selected by a Selector
func (m *resWrangler) Select(s types.Selector) ([]*resource.Resource, error) {
ns := regexp.MustCompile(anchorRegex(s.Namespace))
nm := regexp.MustCompile(anchorRegex(s.Name))
var result []*resource.Resource
for _, r := range m.Resources() {
curId := r.CurId()
orgId := r.OrgId()
// matches the namespace when namespace is not empty in the selector
// It first tries to match with the original namespace
// then matches with the current namespace
if r.GetNamespace() != "" {
matched := ns.MatchString(orgId.EffectiveNamespace())
if !matched {
matched = ns.MatchString(curId.EffectiveNamespace())
if !matched {
continue
}
}
}
// matches the name when name is not empty in the selector
// It first tries to match with the original name
// then matches with the current name
if r.GetName() != "" {
matched := nm.MatchString(orgId.Name)
if !matched {
matched = nm.MatchString(curId.Name)
if !matched {
continue
}
}
}
// matches the GVK
if !r.GetGvk().IsSelected(&s.Gvk) {
continue
}
// matches the label selector
matched, err := r.MatchesLabelSelector(s.LabelSelector)
if err != nil {
return nil, err
}
if !matched {
continue
}
// matches the annotation selector
matched, err = r.MatchesAnnotationSelector(s.AnnotationSelector)
if err != nil {
return nil, err
}
if !matched {
continue
}
result = append(result, r)
}
return result, nil
}

896
api/resmap/resmap_test.go Normal file
View File

@@ -0,0 +1,896 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package resmap_test
import (
"fmt"
"reflect"
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/api/resid"
. "sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/testutils/resmaptest"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
)
var rf = resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl())
var rmF = NewFactory(rf, nil)
func doAppend(t *testing.T, w ResMap, r *resource.Resource) {
err := w.Append(r)
if err != nil {
t.Fatalf("append error: %v", err)
}
}
func doRemove(t *testing.T, w ResMap, id resid.ResId) {
err := w.Remove(id)
if err != nil {
t.Fatalf("remove error: %v", err)
}
}
// Make a resource with a predictable name.
func makeCm(i int) *resource.Resource {
return rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": fmt.Sprintf("cm%03d", i),
},
})
}
// Maintain the class invariant that no two
// resources can have the same CurId().
func TestAppendRejectsDuplicateResId(t *testing.T) {
w := New()
if err := w.Append(makeCm(1)); err != nil {
t.Fatalf("append error: %v", err)
}
err := w.Append(makeCm(1))
if err == nil {
t.Fatalf("expected append error")
}
if !strings.Contains(
err.Error(),
"may not add resource with an already registered id") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestAppendRemove(t *testing.T) {
w1 := New()
doAppend(t, w1, makeCm(1))
doAppend(t, w1, makeCm(2))
doAppend(t, w1, makeCm(3))
doAppend(t, w1, makeCm(4))
doAppend(t, w1, makeCm(5))
doAppend(t, w1, makeCm(6))
doAppend(t, w1, makeCm(7))
doRemove(t, w1, makeCm(1).OrgId())
doRemove(t, w1, makeCm(3).OrgId())
doRemove(t, w1, makeCm(5).OrgId())
doRemove(t, w1, makeCm(7).OrgId())
w2 := New()
doAppend(t, w2, makeCm(2))
doAppend(t, w2, makeCm(4))
doAppend(t, w2, makeCm(6))
if !reflect.DeepEqual(w1, w1) {
w1.Debug("w1")
w2.Debug("w2")
t.Fatalf("mismatch")
}
err := w2.Append(makeCm(6))
if err == nil {
t.Fatalf("expected error")
}
}
func TestRemove(t *testing.T) {
w := New()
r := makeCm(1)
err := w.Remove(r.OrgId())
if err == nil {
t.Fatalf("expected error")
}
err = w.Append(r)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
err = w.Remove(r.OrgId())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
err = w.Remove(r.OrgId())
if err == nil {
t.Fatalf("expected error")
}
}
func TestReplace(t *testing.T) {
cm5 := makeCm(5)
cm700 := makeCm(700)
otherCm5 := makeCm(5)
w := New()
doAppend(t, w, makeCm(1))
doAppend(t, w, makeCm(2))
doAppend(t, w, makeCm(3))
doAppend(t, w, makeCm(4))
doAppend(t, w, cm5)
doAppend(t, w, makeCm(6))
doAppend(t, w, makeCm(7))
oldSize := w.Size()
_, err := w.Replace(otherCm5)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if w.Size() != oldSize {
t.Fatalf("unexpected size %d", w.Size())
}
if r, err := w.GetByCurrentId(cm5.OrgId()); err != nil || r != otherCm5 {
t.Fatalf("unexpected result r=%s, err=%v", r.CurId(), err)
}
if err := w.Append(cm5); err == nil {
t.Fatalf("expected id already there error")
}
if err := w.Remove(cm5.OrgId()); err != nil {
t.Fatalf("unexpected err: %v", err)
}
if err := w.Append(cm700); err != nil {
t.Fatalf("unexpected err: %v", err)
}
if err := w.Append(cm5); err != nil {
t.Fatalf("unexpected err: %v", err)
}
}
func TestEncodeAsYaml(t *testing.T) {
encoded := []byte(`apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
`)
input := resmaptest_test.NewRmBuilder(t, rf).Add(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
},
}).Add(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm2",
},
}).ResMap()
out, err := input.AsYaml()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(out, encoded) {
t.Fatalf("%s doesn't match expected %s", out, encoded)
}
}
func TestGetMatchingResourcesByCurrentId(t *testing.T) {
r1 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "alice",
},
})
r2 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "bob",
},
})
r3 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "bob",
"namespace": "happy",
},
})
r4 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "charlie",
"namespace": "happy",
},
})
r5 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "charlie",
"namespace": "happy",
},
})
m := resmaptest_test.NewRmBuilder(t, rf).
AddR(r1).AddR(r2).AddR(r3).AddR(r4).AddR(r5).ResMap()
result := m.GetMatchingResourcesByCurrentId(
resid.NewResId(cmap, "alice").GvknEquals)
if len(result) != 1 {
t.Fatalf("Expected single map entry but got %v", result)
}
result = m.GetMatchingResourcesByCurrentId(
resid.NewResId(cmap, "bob").GvknEquals)
if len(result) != 2 {
t.Fatalf("Expected two, got %v", result)
}
result = m.GetMatchingResourcesByCurrentId(
resid.NewResIdWithNamespace(cmap, "bob", "system").GvknEquals)
if len(result) != 2 {
t.Fatalf("Expected two but got %v", result)
}
result = m.GetMatchingResourcesByCurrentId(
resid.NewResIdWithNamespace(cmap, "bob", "happy").Equals)
if len(result) != 1 {
t.Fatalf("Expected single map entry but got %v", result)
}
result = m.GetMatchingResourcesByCurrentId(
resid.NewResId(cmap, "charlie").GvknEquals)
if len(result) != 1 {
t.Fatalf("Expected single map entry but got %v", result)
}
// nolint:goconst
tests := []struct {
name string
matcher IdMatcher
count int
}{
{
"match everything",
func(resid.ResId) bool { return true },
5,
},
{
"match nothing",
func(resid.ResId) bool { return false },
0,
},
{
"name is alice",
func(x resid.ResId) bool { return x.Name == "alice" },
1,
},
{
"name is charlie",
func(x resid.ResId) bool { return x.Name == "charlie" },
2,
},
{
"name is bob",
func(x resid.ResId) bool { return x.Name == "bob" },
2,
},
{
"happy namespace",
func(x resid.ResId) bool {
return x.Namespace == "happy"
},
3,
},
{
"happy deployment",
func(x resid.ResId) bool {
return x.Namespace == "happy" &&
x.Gvk.Kind == "Deployment"
},
1,
},
{
"happy ConfigMap",
func(x resid.ResId) bool {
return x.Namespace == "happy" &&
x.Gvk.Kind == "ConfigMap"
},
2,
},
}
for _, tst := range tests {
result := m.GetMatchingResourcesByCurrentId(tst.matcher)
if len(result) != tst.count {
t.Fatalf("test '%s'; actual: %d, expected: %d",
tst.name, len(result), tst.count)
}
}
}
func TestSubsetThatCouldBeReferencedByResource(t *testing.T) {
r1 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "alice",
},
})
r2 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "bob",
},
})
r3 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "bob",
"namespace": "happy",
},
})
r4 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "charlie",
"namespace": "happy",
},
})
r5 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "charlie",
"namespace": "happy",
},
})
r5.AddNamePrefix("little-")
r6 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "domino",
"namespace": "happy",
},
})
r6.AddNamePrefix("little-")
r7 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ClusterRoleBinding",
"metadata": map[string]interface{}{
"name": "meh",
},
})
tests := map[string]struct {
filter *resource.Resource
expected ResMap
}{
"default namespace 1": {
filter: r2,
expected: resmaptest_test.NewRmBuilder(t, rf).
AddR(r1).AddR(r2).AddR(r7).ResMap(),
},
"default namespace 2": {
filter: r1,
expected: resmaptest_test.NewRmBuilder(t, rf).
AddR(r1).AddR(r2).AddR(r7).ResMap(),
},
"happy namespace no prefix": {
filter: r3,
expected: resmaptest_test.NewRmBuilder(t, rf).
AddR(r3).AddR(r4).AddR(r5).AddR(r6).AddR(r7).ResMap(),
},
"happy namespace with prefix": {
filter: r5,
expected: resmaptest_test.NewRmBuilder(t, rf).
AddR(r3).AddR(r4).AddR(r5).AddR(r6).AddR(r7).ResMap(),
},
"cluster level": {
filter: r7,
expected: resmaptest_test.NewRmBuilder(t, rf).
AddR(r1).AddR(r2).AddR(r3).AddR(r4).AddR(r5).AddR(r6).AddR(r7).ResMap(),
},
}
m := resmaptest_test.NewRmBuilder(t, rf).
AddR(r1).AddR(r2).AddR(r3).AddR(r4).AddR(r5).AddR(r6).AddR(r7).ResMap()
for name, test := range tests {
test := test
t.Run(name, func(t *testing.T) {
got := m.SubsetThatCouldBeReferencedByResource(test.filter)
err := test.expected.ErrorIfNotEqualLists(got)
if err != nil {
test.expected.Debug("expected")
got.Debug("actual")
t.Fatalf("Expected match")
}
})
}
}
func addPfxSfx(r *resource.Resource, prefixes []string, suffixes []string) {
for _, pfx := range prefixes {
r.AddNamePrefix(pfx)
}
for _, sfx := range suffixes {
r.AddNameSuffix(sfx)
}
}
func TestSubsetThatCouldBeReferencedByResourceMultiLevel(t *testing.T) {
// Simulates ConfigMap and Deployment defined at level 1
// No prefix nor suffix added at that level
cm1 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "level1",
},
})
addPfxSfx(cm1, []string{""}, []string{""})
dep1 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "level1",
},
})
addPfxSfx(dep1, []string{""}, []string{""})
// Simulates ConfigMap and Deployment defined at level 1
// and prefix added in level 2 of kustomization
cm2p := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "level2p",
},
})
addPfxSfx(cm2p, []string{"", "level2p-"}, []string{"", ""})
dep2p := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "level2p",
},
})
addPfxSfx(dep2p, []string{"", "level2p-"}, []string{"", ""})
// Simulates ConfigMap and Deployment defined at level 1
// and suffix added in level 2 of kustomization
cm2s := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "level2s",
},
})
addPfxSfx(cm2s, []string{"", ""}, []string{"", "-level2s"})
dep2s := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "level2s",
},
})
addPfxSfx(dep2s, []string{"", ""}, []string{"", "-level2s"})
// Simulates ConfigMap and Deployment defined at level 1,
// prefix added in levels 2 and 3 of kustomization.
cm3e := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "level3e",
},
})
addPfxSfx(cm3e, []string{"", "level2p-", "level3e-"}, []string{"", "", ""})
dep3e := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "level3e",
},
})
addPfxSfx(dep3e, []string{"", "level2p-", "level3e-"}, []string{"", "", ""})
// Simulates Deployment defined at level 1, ConfigMap defined at level 2,
// prefix added in levels 2 and 3 of kustomization.
// This reproduce issue 1440.
cm3i := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "level3i",
},
})
addPfxSfx(cm3i, []string{"level2p-", "level3i-"}, []string{"", ""})
dep3i := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "level3i",
},
})
addPfxSfx(dep3i, []string{"", "level2p-", "level3i-"}, []string{"", "", ""})
tests := map[string]struct {
filter *resource.Resource
expected ResMap
}{
"level1": {
filter: dep1,
expected: resmaptest_test.NewRmBuilder(t, rf).
AddR(cm1).AddR(dep1).ResMap(),
},
"level2p": {
filter: dep2p,
expected: resmaptest_test.NewRmBuilder(t, rf).
AddR(cm2p).AddR(dep2p).ResMap(),
},
"level2s": {
filter: dep2s,
expected: resmaptest_test.NewRmBuilder(t, rf).
AddR(cm2s).AddR(dep2s).ResMap(),
},
"level3p": {
filter: dep3e,
expected: resmaptest_test.NewRmBuilder(t, rf).
AddR(cm3e).AddR(dep3e).ResMap(),
},
"level3i": {
filter: dep3i,
expected: resmaptest_test.NewRmBuilder(t, rf).
AddR(cm3i).AddR(dep3i).ResMap(),
},
}
m := resmaptest_test.NewRmBuilder(t, rf).
AddR(cm1).AddR(dep1).AddR(cm2s).AddR(dep2s).AddR(cm2p).AddR(dep2p).AddR(cm3e).AddR(dep3e).AddR(cm3i).AddR(dep3i).ResMap()
for name, test := range tests {
test := test
t.Run(name, func(t *testing.T) {
got := m.SubsetThatCouldBeReferencedByResource(test.filter)
err := test.expected.ErrorIfNotEqualLists(got)
if err != nil {
test.expected.Debug("expected")
got.Debug("actual")
t.Fatalf("Expected match")
}
})
}
}
func TestDeepCopy(t *testing.T) {
rm1 := resmaptest_test.NewRmBuilder(t, rf).Add(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
},
}).Add(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm2",
},
}).ResMap()
rm2 := rm1.DeepCopy()
if &rm1 == &rm2 {
t.Fatal("DeepCopy returned a reference to itself instead of a copy")
}
err := rm1.ErrorIfNotEqualLists(rm1)
if err != nil {
t.Fatal(err)
}
}
func TestErrorIfNotEqualSets(t *testing.T) {
r1 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
},
})
r2 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm2",
},
})
r3 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm2",
"namespace": "system",
},
})
m1 := resmaptest_test.NewRmBuilder(t, rf).AddR(r1).AddR(r2).AddR(r3).ResMap()
if err := m1.ErrorIfNotEqualSets(m1); err != nil {
t.Fatalf("object should equal itself %v", err)
}
m2 := resmaptest_test.NewRmBuilder(t, rf).AddR(r1).ResMap()
if err := m1.ErrorIfNotEqualSets(m2); err == nil {
t.Fatalf("%v should not equal %v %v", m1, m2, err)
}
m3 := resmaptest_test.NewRmBuilder(t, rf).Add(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
}}).ResMap()
if err := m2.ErrorIfNotEqualSets(m3); err != nil {
t.Fatalf("%v should equal %v %v", m2, m3, err)
}
m4 := resmaptest_test.NewRmBuilder(t, rf).AddR(r1).AddR(r2).AddR(r3).ResMap()
if err := m1.ErrorIfNotEqualSets(m4); err != nil {
t.Fatalf("expected equality between %v and %v, %v", m1, m4, err)
}
m4 = resmaptest_test.NewRmBuilder(t, rf).AddR(r3).AddR(r1).AddR(r2).ResMap()
if err := m1.ErrorIfNotEqualSets(m4); err != nil {
t.Fatalf("expected equality between %v and %v, %v", m1, m4, err)
}
m4 = m1.ShallowCopy()
if err := m1.ErrorIfNotEqualSets(m4); err != nil {
t.Fatalf("expected equality between %v and %v, %v", m1, m4, err)
}
m4 = m1.DeepCopy()
if err := m1.ErrorIfNotEqualSets(m4); err != nil {
t.Fatalf("expected equality between %v and %v, %v", m1, m4, err)
}
}
func TestErrorIfNotEqualLists(t *testing.T) {
r1 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
},
})
r2 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm2",
},
})
r3 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm2",
"namespace": "system",
},
})
m1 := resmaptest_test.NewRmBuilder(t, rf).AddR(r1).AddR(r2).AddR(r3).ResMap()
if err := m1.ErrorIfNotEqualLists(m1); err != nil {
t.Fatalf("object should equal itself %v", err)
}
m2 := resmaptest_test.NewRmBuilder(t, rf).AddR(r1).ResMap()
if err := m1.ErrorIfNotEqualLists(m2); err == nil {
t.Fatalf("%v should not equal %v %v", m1, m2, err)
}
m3 := resmaptest_test.NewRmBuilder(t, rf).Add(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
}}).ResMap()
if err := m2.ErrorIfNotEqualLists(m3); err != nil {
t.Fatalf("%v should equal %v %v", m2, m3, err)
}
m4 := resmaptest_test.NewRmBuilder(t, rf).AddR(r1).AddR(r2).AddR(r3).ResMap()
if err := m1.ErrorIfNotEqualLists(m4); err != nil {
t.Fatalf("expected equality between %v and %v, %v", m1, m4, err)
}
m4 = resmaptest_test.NewRmBuilder(t, rf).AddR(r3).AddR(r1).AddR(r2).ResMap()
if err := m1.ErrorIfNotEqualLists(m4); err == nil {
t.Fatalf("expected inequality between %v and %v, %v", m1, m4, err)
}
m4 = m1.ShallowCopy()
if err := m1.ErrorIfNotEqualLists(m4); err != nil {
t.Fatalf("expected equality between %v and %v, %v", m1, m4, err)
}
m4 = m1.DeepCopy()
if err := m1.ErrorIfNotEqualLists(m4); err != nil {
t.Fatalf("expected equality between %v and %v, %v", m1, m4, err)
}
}
func TestAppendAll(t *testing.T) {
r1 := rf.FromMap(
map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "foo-deploy1",
},
})
input1 := rmF.FromResource(r1)
r2 := rf.FromMap(
map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "StatefulSet",
"metadata": map[string]interface{}{
"name": "bar-stateful",
},
})
input2 := rmF.FromResource(r2)
expected := New()
if err := expected.Append(r1); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := expected.Append(r2); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := input1.AppendAll(input2); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := expected.ErrorIfNotEqualLists(input1); err != nil {
input1.Debug("1")
expected.Debug("ex")
t.Fatalf("%#v doesn't equal expected %#v", input1, expected)
}
if err := input1.AppendAll(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := expected.ErrorIfNotEqualLists(input1); err != nil {
t.Fatalf("%#v doesn't equal expected %#v", input1, expected)
}
}
func makeMap1() ResMap {
return rmF.FromResource(rf.FromMapAndOption(
map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cmap",
},
"data": map[string]interface{}{
"a": "x",
"b": "y",
},
}, &types.GeneratorArgs{
Behavior: "create",
}, nil))
}
func makeMap2(b types.GenerationBehavior) ResMap {
return rmF.FromResource(rf.FromMapAndOption(
map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cmap",
},
"data": map[string]interface{}{
"a": "u",
"b": "v",
"c": "w",
},
}, &types.GeneratorArgs{
Behavior: b.String(),
}, nil))
}
func TestAbsorbAll(t *testing.T) {
expected := rmF.FromResource(rf.FromMapAndOption(
map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{},
"labels": map[string]interface{}{},
"name": "cmap",
},
"data": map[string]interface{}{
"a": "u",
"b": "v",
"c": "w",
},
}, &types.GeneratorArgs{
Behavior: "create",
}, nil))
w := makeMap1()
if err := w.AbsorbAll(makeMap2(types.BehaviorMerge)); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := expected.ErrorIfNotEqualLists(w); err != nil {
t.Fatal(err)
}
w = makeMap1()
if err := w.AbsorbAll(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := w.ErrorIfNotEqualLists(makeMap1()); err != nil {
t.Fatal(err)
}
w = makeMap1()
w2 := makeMap2(types.BehaviorReplace)
if err := w.AbsorbAll(w2); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := w2.ErrorIfNotEqualLists(w); err != nil {
t.Fatal(err)
}
w = makeMap1()
w2 = makeMap2(types.BehaviorUnspecified)
err := w.AbsorbAll(w2)
if err == nil {
t.Fatalf("expected error with unspecified behavior")
}
}

178
api/resmap/selector_test.go Normal file
View File

@@ -0,0 +1,178 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.
package resmap_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/resid"
. "sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/types"
)
func setupRMForPatchTargets(t *testing.T) ResMap {
result, err := rmF.NewResMapFromBytes([]byte(`
apiVersion: group1/v1
kind: Kind1
metadata:
name: name1
namespace: ns1
labels:
app: name1
annotations:
foo: bar
---
apiVersion: group1/v1
kind: Kind1
metadata:
name: name2
namespace: default
labels:
app: name2
annotations:
foo: bar
---
apiVersion: group1/v1
kind: Kind2
metadata:
name: name3
labels:
app: name3
annotations:
bar: baz
---
apiVersion: group1/v1
kind: Kind2
metadata:
name: x-name1
namespace: x-default
`))
if err != nil {
t.Fatalf("unexpected error %v", err)
}
return result
}
func TestFindPatchTargets(t *testing.T) {
rm := setupRMForPatchTargets(t)
testcases := []struct {
target types.Selector
count int
}{
{
target: types.Selector{
Name: "name.*",
},
count: 3,
},
{
target: types.Selector{
Name: "name.*",
AnnotationSelector: "foo=bar",
},
count: 2,
},
{
target: types.Selector{
LabelSelector: "app=name1",
},
count: 1,
},
{
target: types.Selector{
Gvk: resid.Gvk{
Kind: "Kind1",
},
Name: "name.*",
},
count: 2,
},
{
target: types.Selector{
Name: "NotMatched",
},
count: 0,
},
{
target: types.Selector{
Name: "",
},
count: 4,
},
{
target: types.Selector{
Namespace: "default",
},
count: 2,
},
{
target: types.Selector{
Namespace: "",
},
count: 4,
},
{
target: types.Selector{
Namespace: "default",
Name: "name.*",
Gvk: resid.Gvk{
Kind: "Kind1",
},
},
count: 1,
},
{
target: types.Selector{
Name: "^name.*",
},
count: 3,
},
{
target: types.Selector{
Name: "name.*$",
},
count: 3,
},
{
target: types.Selector{
Name: "^name.*$",
},
count: 3,
},
{
target: types.Selector{
Namespace: "^def.*",
},
count: 2,
},
{
target: types.Selector{
Namespace: "def.*$",
},
count: 2,
},
{
target: types.Selector{
Namespace: "^def.*$",
},
count: 2,
},
{
target: types.Selector{
Namespace: "default",
},
count: 2,
},
}
for _, testcase := range testcases {
actual, err := rm.Select(testcase.target)
if err != nil {
t.Errorf("unexpected error %v", err)
}
if len(actual) != testcase.count {
t.Errorf("expected %d objects, but got %d:\n%v", testcase.count, len(actual), actual)
}
}
}

179
api/resource/factory.go Normal file
View File

@@ -0,0 +1,179 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package resource
import (
"encoding/json"
"fmt"
"log"
"strings"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/internal/kusterr"
)
// Factory makes instances of Resource.
type Factory struct {
kf ifc.KunstructuredFactory
}
// NewFactory makes an instance of Factory.
func NewFactory(kf ifc.KunstructuredFactory) *Factory {
return &Factory{kf: kf}
}
func (rf *Factory) Hasher() ifc.KunstructuredHasher {
return rf.kf.Hasher()
}
// FromMap returns a new instance of Resource.
func (rf *Factory) FromMap(m map[string]interface{}) *Resource {
return rf.makeOne(rf.kf.FromMap(m), nil)
}
// FromMapWithName returns a new instance with the given "original" name.
func (rf *Factory) FromMapWithName(n string, m map[string]interface{}) *Resource {
return rf.makeOne(rf.kf.FromMap(m), nil).setOriginalName(n)
}
// FromMapWithNamespace returns a new instance with the given "original" namespace.
func (rf *Factory) FromMapWithNamespace(n string, m map[string]interface{}) *Resource {
return rf.makeOne(rf.kf.FromMap(m), nil).setOriginalNs(n)
}
// FromMapWithNamespaceAndName returns a new instance with the given "original" namespace.
func (rf *Factory) FromMapWithNamespaceAndName(ns string, n string, m map[string]interface{}) *Resource {
return rf.makeOne(rf.kf.FromMap(m), nil).setOriginalNs(ns).setOriginalName(n)
}
// FromMapAndOption returns a new instance of Resource with given options.
func (rf *Factory) FromMapAndOption(
m map[string]interface{}, args *types.GeneratorArgs, option *types.GeneratorOptions) *Resource {
return rf.makeOne(rf.kf.FromMap(m), types.NewGenArgs(args, option))
}
// FromKunstructured returns a new instance of Resource.
func (rf *Factory) FromKunstructured(u ifc.Kunstructured) *Resource {
return rf.makeOne(u, nil)
}
// makeOne returns a new instance of Resource.
func (rf *Factory) makeOne(
u ifc.Kunstructured, o *types.GenArgs) *Resource {
if u == nil {
log.Fatal("unstruct ifc must not be null")
}
if o == nil {
o = types.NewGenArgs(nil, nil)
}
r := &Resource{
Kunstructured: u,
options: o,
}
return r.setOriginalName(r.GetName()).setOriginalNs(r.GetNamespace())
}
// SliceFromPatches returns a slice of resources given a patch path
// slice from a kustomization file.
func (rf *Factory) SliceFromPatches(
ldr ifc.Loader, paths []types.PatchStrategicMerge) ([]*Resource, error) {
var result []*Resource
for _, path := range paths {
content, err := ldr.Load(string(path))
if err != nil {
return nil, err
}
res, err := rf.SliceFromBytes(content)
if err != nil {
return nil, kusterr.Handler(err, string(path))
}
result = append(result, res...)
}
return result, nil
}
// FromBytes unmarshals bytes into one Resource.
func (rf *Factory) FromBytes(in []byte) (*Resource, error) {
result, err := rf.SliceFromBytes(in)
if err != nil {
return nil, err
}
if len(result) != 1 {
return nil, fmt.Errorf(
"expected 1 resource, found %d in %v", len(result), in)
}
return result[0], nil
}
// SliceFromBytes unmarshals bytes into a Resource slice.
func (rf *Factory) SliceFromBytes(in []byte) ([]*Resource, error) {
kunStructs, err := rf.kf.SliceFromBytes(in)
if err != nil {
return nil, err
}
var result []*Resource
for len(kunStructs) > 0 {
u := kunStructs[0]
kunStructs = kunStructs[1:]
if strings.HasSuffix(u.GetKind(), "List") {
items := u.Map()["items"]
itemsSlice, ok := items.([]interface{})
if !ok {
if items == nil {
// an empty list
continue
}
return nil, fmt.Errorf("items in List is type %T, expected array", items)
}
for _, item := range itemsSlice {
itemJSON, err := json.Marshal(item)
if err != nil {
return nil, err
}
innerU, err := rf.kf.SliceFromBytes(itemJSON)
if err != nil {
return nil, err
}
// append innerU to kunStructs so nested Lists can be handled
kunStructs = append(kunStructs, innerU...)
}
} else {
result = append(result, rf.FromKunstructured(u))
}
}
return result, nil
}
// MakeConfigMap makes an instance of Resource for ConfigMap
func (rf *Factory) MakeConfigMap(
kvLdr ifc.KvLoader,
options *types.GeneratorOptions,
args *types.ConfigMapArgs) (*Resource, error) {
u, err := rf.kf.MakeConfigMap(kvLdr, options, args)
if err != nil {
return nil, err
}
return rf.makeOne(
u,
types.NewGenArgs(
&types.GeneratorArgs{Behavior: args.Behavior},
options)), nil
}
// MakeSecret makes an instance of Resource for Secret
func (rf *Factory) MakeSecret(
kvLdr ifc.KvLoader,
options *types.GeneratorOptions,
args *types.SecretArgs) (*Resource, error) {
u, err := rf.kf.MakeSecret(kvLdr, options, args)
if err != nil {
return nil, err
}
return rf.makeOne(
u,
types.NewGenArgs(
&types.GeneratorArgs{Behavior: args.Behavior},
options)), nil
}

View File

@@ -0,0 +1,211 @@
/*
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 resource_test
import (
"reflect"
"testing"
. "sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/internal/loadertest"
)
func TestSliceFromPatches(t *testing.T) {
patchGood1 := types.PatchStrategicMerge("patch1.yaml")
patch1 := `
apiVersion: apps/v1
kind: Deployment
metadata:
name: pooh
`
patchGood2 := types.PatchStrategicMerge("patch2.yaml")
patch2 := `
apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
namespace: hundred-acre-wood
---
# some comment
---
---
`
patchBad := types.PatchStrategicMerge("patch3.yaml")
patch3 := `
WOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOT: woot
`
patchList := types.PatchStrategicMerge("patch4.yaml")
patch4 := `
apiVersion: v1
kind: List
items:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: pooh
- apiVersion: v1
kind: ConfigMap
metadata:
name: winnie
namespace: hundred-acre-wood
`
patchList2 := types.PatchStrategicMerge("patch5.yaml")
patch5 := `
apiVersion: v1
kind: DeploymentList
items:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-a
spec: &hostAliases
template:
spec:
hostAliases:
- hostnames:
- a.example.com
ip: 8.8.8.8
- apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-b
spec:
<<: *hostAliases
`
patchList3 := types.PatchStrategicMerge("patch6.yaml")
patch6 := `
apiVersion: v1
kind: List
items:
`
patchList4 := types.PatchStrategicMerge("patch7.yaml")
patch7 := `
apiVersion: v1
kind: List
`
testDeploymentSpec := map[string]interface{}{
"template": map[string]interface{}{
"spec": map[string]interface{}{
"hostAliases": []interface{}{
map[string]interface{}{
"hostnames": []interface{}{
"a.example.com",
},
"ip": "8.8.8.8",
},
},
},
},
}
testDeploymentA := factory.FromMap(
map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "deployment-a",
},
"spec": testDeploymentSpec,
})
testDeploymentB := factory.FromMap(
map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "deployment-b",
},
"spec": testDeploymentSpec,
})
l := loadertest.NewFakeLoader("/")
l.AddFile("/"+string(patchGood1), []byte(patch1))
l.AddFile("/"+string(patchGood2), []byte(patch2))
l.AddFile("/"+string(patchBad), []byte(patch3))
l.AddFile("/"+string(patchList), []byte(patch4))
l.AddFile("/"+string(patchList2), []byte(patch5))
l.AddFile("/"+string(patchList3), []byte(patch6))
l.AddFile("/"+string(patchList4), []byte(patch7))
tests := []struct {
name string
input []types.PatchStrategicMerge
expectedOut []*Resource
expectedErr bool
}{
{
name: "happy",
input: []types.PatchStrategicMerge{patchGood1, patchGood2},
expectedOut: []*Resource{testDeployment, testConfigMap},
expectedErr: false,
},
{
name: "badFileName",
input: []types.PatchStrategicMerge{patchGood1, "doesNotExist"},
expectedOut: []*Resource{},
expectedErr: true,
},
{
name: "badData",
input: []types.PatchStrategicMerge{patchGood1, patchBad},
expectedOut: []*Resource{},
expectedErr: true,
},
{
name: "listOfPatches",
input: []types.PatchStrategicMerge{patchList},
expectedOut: []*Resource{testDeployment, testConfigMap},
expectedErr: false,
},
{
name: "listWithAnchorReference",
input: []types.PatchStrategicMerge{patchList2},
expectedOut: []*Resource{testDeploymentA, testDeploymentB},
expectedErr: false,
},
{
name: "listWithNoEntries",
input: []types.PatchStrategicMerge{patchList3},
expectedOut: []*Resource{},
expectedErr: false,
},
{
name: "listWithNo'items:'",
input: []types.PatchStrategicMerge{patchList4},
expectedOut: []*Resource{},
expectedErr: false,
},
}
for _, test := range tests {
rs, err := factory.SliceFromPatches(l, test.input)
if test.expectedErr && err == nil {
t.Fatalf("%v: should return error", test.name)
}
if !test.expectedErr && err != nil {
t.Fatalf("%v: unexpected error: %s", test.name, err)
}
if len(rs) != len(test.expectedOut) {
t.Fatalf("%s: length mismatch %d != %d",
test.name, len(rs), len(test.expectedOut))
}
for i := range rs {
if !reflect.DeepEqual(test.expectedOut[i], rs[i]) {
t.Fatalf("%s: Got: %v\nexpected:%v",
test.name, test.expectedOut[i], rs[i])
}
}
}
}

327
api/resource/resource.go Normal file
View File

@@ -0,0 +1,327 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package resource implements representations of k8s API resources as "unstructured" objects.
package resource
import (
"reflect"
"strings"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/resid"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/yaml"
)
// Resource is map representation of a Kubernetes API resource object
// paired with a GenerationBehavior.
type Resource struct {
ifc.Kunstructured
originalName string
originalNs string
options *types.GenArgs
refBy []resid.ResId
refVarNames []string
namePrefixes []string
nameSuffixes []string
}
// ResCtx is an interface describing the contextual added
// kept kustomize in the context of each Resource object.
// Currently mainly the name prefix and name suffix are added.
type ResCtx interface {
AddNamePrefix(p string)
AddNameSuffix(s string)
GetOutermostNamePrefix() string
GetOutermostNameSuffix() string
GetNamePrefixes() []string
GetNameSuffixes() []string
}
// ResCtxMatcher returns true if two Resources are being
// modified in the same kustomize context.
type ResCtxMatcher func(ResCtx) bool
// DeepCopy returns a new copy of resource
func (r *Resource) DeepCopy() *Resource {
rc := &Resource{
Kunstructured: r.Kunstructured.Copy(),
}
rc.copyOtherFields(r)
return rc
}
// Replace performs replace with other resource.
func (r *Resource) Replace(other *Resource) {
r.SetLabels(mergeStringMaps(other.GetLabels(), r.GetLabels()))
r.SetAnnotations(
mergeStringMaps(other.GetAnnotations(), r.GetAnnotations()))
r.SetName(other.GetName())
r.SetNamespace(other.GetNamespace())
r.copyOtherFields(other)
}
func (r *Resource) copyOtherFields(other *Resource) {
r.originalName = other.originalName
r.originalNs = other.originalNs
r.options = other.options
r.refBy = other.copyRefBy()
r.refVarNames = copyStringSlice(other.refVarNames)
r.namePrefixes = copyStringSlice(other.namePrefixes)
r.nameSuffixes = copyStringSlice(other.nameSuffixes)
}
func (r *Resource) Equals(o *Resource) bool {
return r.ReferencesEqual(o) &&
reflect.DeepEqual(r.Kunstructured, o.Kunstructured)
}
func (r *Resource) ReferencesEqual(o *Resource) bool {
setSelf := make(map[resid.ResId]bool)
setOther := make(map[resid.ResId]bool)
for _, ref := range o.refBy {
setOther[ref] = true
}
for _, ref := range r.refBy {
if _, ok := setOther[ref]; !ok {
return false
}
setSelf[ref] = true
}
return len(setSelf) == len(setOther)
}
func (r *Resource) KunstructEqual(o *Resource) bool {
return reflect.DeepEqual(r.Kunstructured, o.Kunstructured)
}
// Merge performs merge with other resource.
func (r *Resource) Merge(other *Resource) {
r.Replace(other)
mergeConfigmap(r.Map(), other.Map(), r.Map())
}
func (r *Resource) copyRefBy() []resid.ResId {
if r.refBy == nil {
return nil
}
s := make([]resid.ResId, len(r.refBy))
copy(s, r.refBy)
return s
}
func copyStringSlice(s []string) []string {
if s == nil {
return nil
}
c := make([]string, len(s))
copy(c, s)
return c
}
// Implements ResCtx AddNamePrefix
func (r *Resource) AddNamePrefix(p string) {
r.namePrefixes = append(r.namePrefixes, p)
}
// Implements ResCtx AddNameSuffix
func (r *Resource) AddNameSuffix(s string) {
r.nameSuffixes = append(r.nameSuffixes, s)
}
// Implements ResCtx GetOutermostNamePrefix
func (r *Resource) GetOutermostNamePrefix() string {
if len(r.namePrefixes) == 0 {
return ""
}
return r.namePrefixes[len(r.namePrefixes)-1]
}
// Implements ResCtx GetOutermostNameSuffix
func (r *Resource) GetOutermostNameSuffix() string {
if len(r.nameSuffixes) == 0 {
return ""
}
return r.nameSuffixes[len(r.nameSuffixes)-1]
}
func sameEndingSubarray(a, b []string) bool {
compareLen := len(b)
if len(a) < len(b) {
compareLen = len(a)
}
if compareLen == 0 {
return true
}
alen := len(a) - 1
blen := len(b) - 1
for i := 0; i <= compareLen-1; i++ {
if a[alen-i] != b[blen-i] {
return false
}
}
return true
}
// Implements ResCtx GetNamePrefixes
func (r *Resource) GetNamePrefixes() []string {
return r.namePrefixes
}
// Implements ResCtx GetNameSuffixes
func (r *Resource) GetNameSuffixes() []string {
return r.nameSuffixes
}
// OutermostPrefixSuffixEquals returns true if both resources
// outer suffix and prefix matches.
func (r *Resource) OutermostPrefixSuffixEquals(o ResCtx) bool {
return (r.GetOutermostNamePrefix() == o.GetOutermostNamePrefix()) && (r.GetOutermostNameSuffix() == o.GetOutermostNameSuffix())
}
// PrefixesSuffixesEquals is conceptually doing the same task
// as OutermostPrefixSuffix but performs a deeper comparison
// of the suffix and prefix slices.
//
// Important note: The PrefixSuffixTransformer is stacking the
// prefix values in the reverse order of appearance in
// the transformed name. For this reason the sameEndingSubarray
// method is used (as opposed to the sameBeginningSubarray)
// to compare the prefix slice. In the same spirit, the
// GetOutermostNamePrefix is using the last element of the
// nameprefix slice and not the first.
func (r *Resource) PrefixesSuffixesEquals(o ResCtx) bool {
return sameEndingSubarray(r.GetNamePrefixes(), o.GetNamePrefixes()) && sameEndingSubarray(r.GetNameSuffixes(), o.GetNameSuffixes())
}
// This is used to compute if a referrer could potentially be impacted
// by the change of name of a referral.
func (r *Resource) InSameKustomizeCtx(rctxm ResCtxMatcher) bool {
return rctxm(r)
}
func (r *Resource) GetOriginalName() string {
return r.originalName
}
// Making this public would be bad.
func (r *Resource) setOriginalName(n string) *Resource {
r.originalName = n
return r
}
func (r *Resource) GetOriginalNs() string {
return r.originalNs
}
// Making this public would be bad.
func (r *Resource) setOriginalNs(n string) *Resource {
r.originalNs = n
return r
}
// String returns resource as JSON.
func (r *Resource) String() string {
bs, err := r.MarshalJSON()
if err != nil {
return "<" + err.Error() + ">"
}
return strings.TrimSpace(string(bs)) + r.options.String()
}
// AsYAML returns the resource in Yaml form.
// Easier to read than JSON.
func (r *Resource) AsYAML() ([]byte, error) {
json, err := r.MarshalJSON()
if err != nil {
return nil, err
}
return yaml.JSONToYAML(json)
}
// SetOptions updates the generator options for the resource.
func (r *Resource) SetOptions(o *types.GenArgs) {
r.options = o
}
// Behavior returns the behavior for the resource.
func (r *Resource) Behavior() types.GenerationBehavior {
return r.options.Behavior()
}
// NeedHashSuffix checks if the resource need a hash suffix
func (r *Resource) NeedHashSuffix() bool {
return r.options != nil && r.options.NeedsHashSuffix()
}
// GetNamespace returns the namespace the resource thinks it's in.
func (r *Resource) GetNamespace() string {
namespace, _ := r.GetString("metadata.namespace")
// if err, namespace is empty, so no need to check.
return namespace
}
// OrgId returns the original, immutable ResId for the resource.
// This doesn't have to be unique in a ResMap.
// TODO: compute this once and save it in the resource.
func (r *Resource) OrgId() resid.ResId {
return resid.NewResIdWithNamespace(
r.GetGvk(), r.GetOriginalName(), r.GetOriginalNs())
}
// CurId returns a ResId for the resource using the
// mutable parts of the resource.
// This should be unique in any ResMap.
func (r *Resource) CurId() resid.ResId {
return resid.NewResIdWithNamespace(
r.GetGvk(), r.GetName(), r.GetNamespace())
}
// GetRefBy returns the ResIds that referred to current resource
func (r *Resource) GetRefBy() []resid.ResId {
return r.refBy
}
// AppendRefBy appends a ResId into the refBy list
func (r *Resource) AppendRefBy(id resid.ResId) {
r.refBy = append(r.refBy, id)
}
// GetRefVarNames returns vars that refer to current resource
func (r *Resource) GetRefVarNames() []string {
return r.refVarNames
}
// AppendRefVarName appends a name of a var into the refVar list
func (r *Resource) AppendRefVarName(variable types.Var) {
r.refVarNames = append(r.refVarNames, variable.Name)
}
// TODO: Add BinaryData once we sync to new k8s.io/api
func mergeConfigmap(
mergedTo map[string]interface{},
maps ...map[string]interface{}) {
mergedMap := map[string]interface{}{}
for _, m := range maps {
datamap, ok := m["data"].(map[string]interface{})
if ok {
for key, value := range datamap {
mergedMap[key] = value
}
}
}
mergedTo["data"] = mergedMap
}
func mergeStringMaps(maps ...map[string]string) map[string]string {
result := map[string]string{}
for _, m := range maps {
for key, value := range m {
result[key] = value
}
}
return result
}

View File

@@ -0,0 +1,138 @@
/*
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 resource_test
import (
"reflect"
"testing"
"sigs.k8s.io/kustomize/v3/api/resid"
. "sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
)
var factory = NewFactory(
kunstruct.NewKunstructuredFactoryImpl())
var testConfigMap = factory.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "winnie",
"namespace": "hundred-acre-wood",
},
})
const genArgOptions = "{nsfx:false,beh:unspecified}"
const configMapAsString = `{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"winnie","namespace":"hundred-acre-wood"}}`
var testDeployment = factory.FromMap(
map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "pooh",
},
})
const deploymentAsString = `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"pooh"}}`
func TestAsYAML(t *testing.T) {
expected := `apiVersion: apps/v1
kind: Deployment
metadata:
name: pooh
`
yaml, err := testDeployment.AsYAML()
if err != nil {
t.Fatal(err)
}
if string(yaml) != expected {
t.Fatalf("--- expected\n%s\n--- got\n%s\n", expected, string(yaml))
}
}
func TestResourceString(t *testing.T) {
tests := []struct {
in *Resource
s string
}{
{
in: testConfigMap,
s: configMapAsString + genArgOptions,
},
{
in: testDeployment,
s: deploymentAsString + genArgOptions,
},
}
for _, test := range tests {
if test.in.String() != test.s {
t.Fatalf("Expected %s == %s", test.in.String(), test.s)
}
}
}
func TestResourceId(t *testing.T) {
tests := []struct {
in *Resource
id resid.ResId
}{
{
in: testConfigMap,
id: resid.NewResIdWithNamespace(
resid.Gvk{Version: "v1", Kind: "ConfigMap"}, "winnie", "hundred-acre-wood"),
},
{
in: testDeployment,
id: resid.NewResId(resid.Gvk{Group: "apps", Version: "v1", Kind: "Deployment"}, "pooh"),
},
}
for _, test := range tests {
if test.in.OrgId() != test.id {
t.Fatalf("Expected %v, but got %v\n", test.id, test.in.OrgId())
}
}
}
func TestDeepCopy(t *testing.T) {
r := factory.FromMap(
map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "pooh",
},
})
r.AppendRefBy(resid.NewResId(resid.Gvk{Group: "somegroup", Kind: "MyKind"}, "random"))
var1 := types.Var{
Name: "SERVICE_ONE",
ObjRef: types.Target{
Gvk: resid.Gvk{Version: "v1", Kind: "Service"},
Name: "backendOne"},
}
r.AppendRefVarName(var1)
cr := r.DeepCopy()
if !reflect.DeepEqual(r, cr) {
t.Errorf("expected %v\nbut got%v", r, cr)
}
}

View File

@@ -0,0 +1,84 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package resmaptest_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/resid"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/resource"
)
// Builds ResMaps for tests, with test-aware error handling.
type rmBuilder struct {
t *testing.T
m resmap.ResMap
rf *resource.Factory
}
func NewSeededRmBuilder(t *testing.T, rf *resource.Factory, m resmap.ResMap) *rmBuilder {
return &rmBuilder{t: t, rf: rf, m: m}
}
func NewRmBuilder(t *testing.T, rf *resource.Factory) *rmBuilder {
return NewSeededRmBuilder(t, rf, resmap.New())
}
func (rm *rmBuilder) Add(m map[string]interface{}) *rmBuilder {
return rm.AddR(rm.rf.FromMap(m))
}
func (rm *rmBuilder) AddR(r *resource.Resource) *rmBuilder {
err := rm.m.Append(r)
if err != nil {
rm.t.Fatalf("test setup failure: %v", err)
}
return rm
}
func (rm *rmBuilder) AddWithId(id resid.ResId, m map[string]interface{}) *rmBuilder {
err := rm.m.Append(rm.rf.FromMap(m))
if err != nil {
rm.t.Fatalf("test setup failure: %v", err)
}
return rm
}
func (rm *rmBuilder) AddWithName(n string, m map[string]interface{}) *rmBuilder {
err := rm.m.Append(rm.rf.FromMapWithName(n, m))
if err != nil {
rm.t.Fatalf("test setup failure: %v", err)
}
return rm
}
func (rm *rmBuilder) AddWithNs(ns string, m map[string]interface{}) *rmBuilder {
err := rm.m.Append(rm.rf.FromMapWithNamespace(ns, m))
if err != nil {
rm.t.Fatalf("test setup failure: %v", err)
}
return rm
}
func (rm *rmBuilder) AddWithNsAndName(ns string, n string, m map[string]interface{}) *rmBuilder {
err := rm.m.Append(rm.rf.FromMapWithNamespaceAndName(ns, n, m))
if err != nil {
rm.t.Fatalf("test setup failure: %v", err)
}
return rm
}
func (rm *rmBuilder) ReplaceResource(m map[string]interface{}) *rmBuilder {
r := rm.rf.FromMap(m)
_, err := rm.m.Replace(r)
if err != nil {
rm.t.Fatalf("test setup failure: %v", err)
}
return rm
}
func (rm *rmBuilder) ResMap() resmap.ResMap {
return rm.m
}

View File

@@ -0,0 +1,108 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package valtest_test defines a fakeValidator that can be used in tests
package valtest_test
import (
"errors"
"regexp"
"testing"
)
// fakeValidator can be used in tests.
type fakeValidator struct {
happy bool
called bool
t *testing.T
}
// SAD is an error string.
const SAD = "i'm not happy Bob, NOT HAPPY"
// MakeHappyMapValidator makes a fakeValidator that always passes.
func MakeHappyMapValidator(t *testing.T) *fakeValidator {
return &fakeValidator{happy: true, t: t}
}
// MakeSadMapValidator makes a fakeValidator that always fails.
func MakeSadMapValidator(t *testing.T) *fakeValidator {
return &fakeValidator{happy: false, t: t}
}
// MakeFakeValidator makes an empty Fake Validator.
func MakeFakeValidator() *fakeValidator {
return &fakeValidator{}
}
// ErrIfInvalidKey returns nil
func (v *fakeValidator) ErrIfInvalidKey(k string) error {
return nil
}
// IsEnvVarName returns nil
func (v *fakeValidator) IsEnvVarName(k string) error {
return nil
}
// MakeAnnotationValidator returns a nil function
func (v *fakeValidator) MakeAnnotationValidator() func(map[string]string) error {
return nil
}
// MakeAnnotationNameValidator returns a nil function
func (v *fakeValidator) MakeAnnotationNameValidator() func([]string) error {
return nil
}
// MakeLabelValidator returns a nil function
func (v *fakeValidator) MakeLabelValidator() func(map[string]string) error {
return nil
}
// MakeLabelNameValidator returns a nil function
func (v *fakeValidator) MakeLabelNameValidator() func([]string) error {
return nil
}
// ValidateNamespace validates namespace by regexp
func (v *fakeValidator) ValidateNamespace(s string) []string {
pattern := regexp.MustCompile(`^[a-zA-Z].*`)
if pattern.MatchString(s) {
return nil
}
return []string{"doesn't match"}
}
// Validator replaces apimachinery validation in tests.
// Can be set to fail or succeed to test error handling.
// Can confirm if run or not run by surrounding code.
func (v *fakeValidator) Validator(_ map[string]string) error {
v.called = true
if v.happy {
return nil
}
return errors.New(SAD)
}
func (v *fakeValidator) ValidatorArray(_ []string) error {
v.called = true
if v.happy {
return nil
}
return errors.New(SAD)
}
// VerifyCall returns true if Validator was used.
func (v *fakeValidator) VerifyCall() {
if !v.called {
v.t.Errorf("should have called Validator")
}
}
// VerifyNoCall returns true if Validator was not used.
func (v *fakeValidator) VerifyNoCall() {
if v.called {
v.t.Errorf("should not have called Validator")
}
}

View File

@@ -6,8 +6,8 @@ package transform
import (
"errors"
"fmt"
"sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/pkg/resmap"
)
// mapTransformer applies a string->string map to fieldSpecs.

View File

@@ -7,10 +7,10 @@ import (
"testing"
"sigs.k8s.io/kustomize/v3/api/builtinconfig"
"sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/testutils/resmaptest"
. "sigs.k8s.io/kustomize/v3/api/transform"
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
"sigs.k8s.io/kustomize/v3/pkg/resmaptest"
"sigs.k8s.io/kustomize/v3/pkg/resource"
)
var resourceFactory = resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl())

View File

@@ -6,7 +6,7 @@ package transform
import (
"fmt"
"sigs.k8s.io/kustomize/v3/pkg/resmap"
"sigs.k8s.io/kustomize/v3/api/resmap"
)
// multiTransformer contains a list of transformers.

View File

@@ -5,9 +5,9 @@ package transform_test
import (
"fmt"
"sigs.k8s.io/kustomize/v3/api/ifc"
. "sigs.k8s.io/kustomize/v3/api/transform"
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
"sigs.k8s.io/kustomize/v3/pkg/ifc"
"testing"
)

View File

@@ -3,7 +3,7 @@
package transform
import "sigs.k8s.io/kustomize/v3/pkg/resmap"
import "sigs.k8s.io/kustomize/v3/api/resmap"
// noOpTransformer contains a no-op transformer.
type noOpTransformer struct{}