mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-14 02:20:53 +00:00
Make resource, resmap public.
This commit is contained in:
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
95
api/ifc/ifc.go
Normal 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"
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
312
api/loader/fileloader.go
Normal file
@@ -0,0 +1,312 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package loader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/filesys"
|
||||
"sigs.k8s.io/kustomize/v3/api/ifc"
|
||||
"sigs.k8s.io/kustomize/v3/pkg/git"
|
||||
)
|
||||
|
||||
// fileLoader is a kustomization's interface to files.
|
||||
//
|
||||
// The directory in which a kustomization file sits
|
||||
// is referred to below as the kustomization's _root_.
|
||||
//
|
||||
// An instance of fileLoader has an immutable root,
|
||||
// and offers a `New` method returning a new loader
|
||||
// with a new root.
|
||||
//
|
||||
// A kustomization file refers to two kinds of files:
|
||||
//
|
||||
// * supplemental data paths
|
||||
//
|
||||
// `Load` is used to visit these paths.
|
||||
//
|
||||
// These paths refer to resources, patches,
|
||||
// data for ConfigMaps and Secrets, etc.
|
||||
//
|
||||
// The loadRestrictor may disallow certain paths
|
||||
// or classes of paths.
|
||||
//
|
||||
// * bases (other kustomizations)
|
||||
//
|
||||
// `New` is used to load bases.
|
||||
//
|
||||
// A base can be either a remote git repo URL, or
|
||||
// a directory specified relative to the current
|
||||
// root. In the former case, the repo is locally
|
||||
// cloned, and the new loader is rooted on a path
|
||||
// in that clone.
|
||||
//
|
||||
// As loaders create new loaders, a root history
|
||||
// is established, and used to disallow:
|
||||
//
|
||||
// - A base that is a repository that, in turn,
|
||||
// specifies a base repository seen previously
|
||||
// in the loading stack (a cycle).
|
||||
//
|
||||
// - An overlay depending on a base positioned at
|
||||
// or above it. I.e. '../foo' is OK, but '.',
|
||||
// '..', '../..', etc. are disallowed. Allowing
|
||||
// such a base has no advantages and encourages
|
||||
// cycles, particularly if some future change
|
||||
// were to introduce globbing to file
|
||||
// specifications in the kustomization file.
|
||||
//
|
||||
// These restrictions assure that kustomizations
|
||||
// are self-contained and relocatable, and impose
|
||||
// some safety when relying on remote kustomizations,
|
||||
// e.g. a remotely loaded ConfigMap generator specified
|
||||
// to read from /etc/passwd will fail.
|
||||
//
|
||||
type fileLoader struct {
|
||||
// Loader that spawned this loader.
|
||||
// Used to avoid cycles.
|
||||
referrer *fileLoader
|
||||
|
||||
// An absolute, cleaned path to a directory.
|
||||
// The Load function will read non-absolute
|
||||
// paths relative to this directory.
|
||||
root filesys.ConfirmedDir
|
||||
|
||||
// Restricts behavior of Load function.
|
||||
loadRestrictor LoadRestrictorFunc
|
||||
|
||||
// If this is non-nil, the files were
|
||||
// obtained from the given repository.
|
||||
repoSpec *git.RepoSpec
|
||||
|
||||
// File system utilities.
|
||||
fSys filesys.FileSystem
|
||||
|
||||
// Used to clone repositories.
|
||||
cloner git.Cloner
|
||||
|
||||
// Used to clean up, as needed.
|
||||
cleaner func() error
|
||||
}
|
||||
|
||||
const CWD = "."
|
||||
|
||||
// NewFileLoaderAtCwd returns a loader that loads from ".".
|
||||
// A convenience for kustomize edit commands.
|
||||
func NewFileLoaderAtCwd(fSys filesys.FileSystem) *fileLoader {
|
||||
return newLoaderOrDie(
|
||||
RestrictionRootOnly, fSys, CWD)
|
||||
}
|
||||
|
||||
// NewFileLoaderAtRoot returns a loader that loads from "/".
|
||||
// A convenience for tests.
|
||||
func NewFileLoaderAtRoot(fSys filesys.FileSystem) *fileLoader {
|
||||
return newLoaderOrDie(
|
||||
RestrictionRootOnly, fSys, string(filepath.Separator))
|
||||
}
|
||||
|
||||
// Root returns the absolute path that is prepended to any
|
||||
// relative paths used in Load.
|
||||
func (fl *fileLoader) Root() string {
|
||||
return fl.root.String()
|
||||
}
|
||||
|
||||
func newLoaderOrDie(
|
||||
lr LoadRestrictorFunc,
|
||||
fSys filesys.FileSystem, path string) *fileLoader {
|
||||
root, err := demandDirectoryRoot(fSys, path)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to make loader at '%s'; %v", path, err)
|
||||
}
|
||||
return newLoaderAtConfirmedDir(
|
||||
lr, root, fSys, nil, git.ClonerUsingGitExec)
|
||||
}
|
||||
|
||||
// newLoaderAtConfirmedDir returns a new fileLoader with given root.
|
||||
func newLoaderAtConfirmedDir(
|
||||
lr LoadRestrictorFunc,
|
||||
root filesys.ConfirmedDir, fSys filesys.FileSystem,
|
||||
referrer *fileLoader, cloner git.Cloner) *fileLoader {
|
||||
return &fileLoader{
|
||||
loadRestrictor: lr,
|
||||
root: root,
|
||||
referrer: referrer,
|
||||
fSys: fSys,
|
||||
cloner: cloner,
|
||||
cleaner: func() error { return nil },
|
||||
}
|
||||
}
|
||||
|
||||
// Assure that the given path is in fact a directory.
|
||||
func demandDirectoryRoot(
|
||||
fSys filesys.FileSystem, path string) (filesys.ConfirmedDir, error) {
|
||||
if path == "" {
|
||||
return "", fmt.Errorf(
|
||||
"loader root cannot be empty")
|
||||
}
|
||||
d, f, err := fSys.CleanedAbs(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(
|
||||
"absolute path error in '%s' : %v", path, err)
|
||||
}
|
||||
if f != "" {
|
||||
return "", fmt.Errorf(
|
||||
"got file '%s', but '%s' must be a directory to be a root",
|
||||
f, path)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// New returns a new Loader, rooted relative to current loader,
|
||||
// or rooted in a temp directory holding a git repo clone.
|
||||
func (fl *fileLoader) New(path string) (ifc.Loader, error) {
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("new root cannot be empty")
|
||||
}
|
||||
repoSpec, err := git.NewRepoSpecFromUrl(path)
|
||||
if err == nil {
|
||||
// Treat this as git repo clone request.
|
||||
if err := fl.errIfRepoCycle(repoSpec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newLoaderAtGitClone(
|
||||
repoSpec, fl.fSys, fl, fl.cloner)
|
||||
}
|
||||
if filepath.IsAbs(path) {
|
||||
return nil, fmt.Errorf("new root '%s' cannot be absolute", path)
|
||||
}
|
||||
root, err := demandDirectoryRoot(fl.fSys, fl.root.Join(path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := fl.errIfGitContainmentViolation(root); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := fl.errIfArgEqualOrHigher(root); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newLoaderAtConfirmedDir(
|
||||
fl.loadRestrictor, root, fl.fSys, fl, fl.cloner), nil
|
||||
}
|
||||
|
||||
// newLoaderAtGitClone returns a new Loader pinned to a temporary
|
||||
// directory holding a cloned git repo.
|
||||
func newLoaderAtGitClone(
|
||||
repoSpec *git.RepoSpec, fSys filesys.FileSystem,
|
||||
referrer *fileLoader, cloner git.Cloner) (ifc.Loader, error) {
|
||||
cleaner := repoSpec.Cleaner(fSys)
|
||||
err := cloner(repoSpec)
|
||||
if err != nil {
|
||||
cleaner()
|
||||
return nil, err
|
||||
}
|
||||
root, f, err := fSys.CleanedAbs(repoSpec.AbsPath())
|
||||
if err != nil {
|
||||
cleaner()
|
||||
return nil, err
|
||||
}
|
||||
// We don't know that the path requested in repoSpec
|
||||
// is a directory until we actually clone it and look
|
||||
// inside. That just happened, hence the error check
|
||||
// is here.
|
||||
if f != "" {
|
||||
cleaner()
|
||||
return nil, fmt.Errorf(
|
||||
"'%s' refers to file '%s'; expecting directory",
|
||||
repoSpec.AbsPath(), f)
|
||||
}
|
||||
return &fileLoader{
|
||||
// Clones never allowed to escape root.
|
||||
loadRestrictor: RestrictionRootOnly,
|
||||
root: root,
|
||||
referrer: referrer,
|
||||
repoSpec: repoSpec,
|
||||
fSys: fSys,
|
||||
cloner: cloner,
|
||||
cleaner: cleaner,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (fl *fileLoader) errIfGitContainmentViolation(
|
||||
base filesys.ConfirmedDir) error {
|
||||
containingRepo := fl.containingRepo()
|
||||
if containingRepo == nil {
|
||||
return nil
|
||||
}
|
||||
if !base.HasPrefix(containingRepo.CloneDir()) {
|
||||
return fmt.Errorf(
|
||||
"security; bases in kustomizations found in "+
|
||||
"cloned git repos must be within the repo, "+
|
||||
"but base '%s' is outside '%s'",
|
||||
base, containingRepo.CloneDir())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Looks back through referrers for a git repo, returning nil
|
||||
// if none found.
|
||||
func (fl *fileLoader) containingRepo() *git.RepoSpec {
|
||||
if fl.repoSpec != nil {
|
||||
return fl.repoSpec
|
||||
}
|
||||
if fl.referrer == nil {
|
||||
return nil
|
||||
}
|
||||
return fl.referrer.containingRepo()
|
||||
}
|
||||
|
||||
// errIfArgEqualOrHigher tests whether the argument,
|
||||
// is equal to or above the root of any ancestor.
|
||||
func (fl *fileLoader) errIfArgEqualOrHigher(
|
||||
candidateRoot filesys.ConfirmedDir) error {
|
||||
if fl.root.HasPrefix(candidateRoot) {
|
||||
return fmt.Errorf(
|
||||
"cycle detected: candidate root '%s' contains visited root '%s'",
|
||||
candidateRoot, fl.root)
|
||||
}
|
||||
if fl.referrer == nil {
|
||||
return nil
|
||||
}
|
||||
return fl.referrer.errIfArgEqualOrHigher(candidateRoot)
|
||||
}
|
||||
|
||||
// TODO(monopole): Distinguish branches?
|
||||
// I.e. Allow a distinction between git URI with
|
||||
// path foo and tag bar and a git URI with the same
|
||||
// path but a different tag?
|
||||
func (fl *fileLoader) errIfRepoCycle(newRepoSpec *git.RepoSpec) error {
|
||||
// TODO(monopole): Use parsed data instead of Raw().
|
||||
if fl.repoSpec != nil &&
|
||||
strings.HasPrefix(fl.repoSpec.Raw(), newRepoSpec.Raw()) {
|
||||
return fmt.Errorf(
|
||||
"cycle detected: URI '%s' referenced by previous URI '%s'",
|
||||
newRepoSpec.Raw(), fl.repoSpec.Raw())
|
||||
}
|
||||
if fl.referrer == nil {
|
||||
return nil
|
||||
}
|
||||
return fl.referrer.errIfRepoCycle(newRepoSpec)
|
||||
}
|
||||
|
||||
// Load returns the content of file at the given path,
|
||||
// else an error. Relative paths are taken relative
|
||||
// to the root.
|
||||
func (fl *fileLoader) Load(path string) ([]byte, error) {
|
||||
if !filepath.IsAbs(path) {
|
||||
path = fl.root.Join(path)
|
||||
}
|
||||
path, err := fl.loadRestrictor(fl.fSys, fl.root, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fl.fSys.ReadFile(path)
|
||||
}
|
||||
|
||||
// Cleanup runs the cleaner.
|
||||
func (fl *fileLoader) Cleanup() error {
|
||||
return fl.cleaner()
|
||||
}
|
||||
594
api/loader/fileloader_test.go
Normal file
594
api/loader/fileloader_test.go
Normal file
@@ -0,0 +1,594 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package loader
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/filesys"
|
||||
"sigs.k8s.io/kustomize/v3/api/ifc"
|
||||
"sigs.k8s.io/kustomize/v3/pkg/git"
|
||||
"sigs.k8s.io/kustomize/v3/pkg/pgmconfig"
|
||||
)
|
||||
|
||||
type testData struct {
|
||||
path string
|
||||
expectedContent string
|
||||
}
|
||||
|
||||
var testCases = []testData{
|
||||
{
|
||||
path: "foo/project/fileA.yaml",
|
||||
expectedContent: "fileA content",
|
||||
},
|
||||
{
|
||||
path: "foo/project/subdir1/fileB.yaml",
|
||||
expectedContent: "fileB content",
|
||||
},
|
||||
{
|
||||
path: "foo/project/subdir2/fileC.yaml",
|
||||
expectedContent: "fileC content",
|
||||
},
|
||||
{
|
||||
path: "foo/project/fileD.yaml",
|
||||
expectedContent: "fileD content",
|
||||
},
|
||||
}
|
||||
|
||||
func MakeFakeFs(td []testData) filesys.FileSystem {
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
for _, x := range td {
|
||||
fSys.WriteFile("/"+x.path, []byte(x.expectedContent))
|
||||
}
|
||||
return fSys
|
||||
}
|
||||
|
||||
func makeLoader() *fileLoader {
|
||||
return NewFileLoaderAtRoot(MakeFakeFs(testCases))
|
||||
|
||||
}
|
||||
func TestLoaderLoad(t *testing.T) {
|
||||
l1 := makeLoader()
|
||||
if "/" != l1.Root() {
|
||||
t.Fatalf("incorrect root: '%s'\n", l1.Root())
|
||||
}
|
||||
for _, x := range testCases {
|
||||
b, err := l1.Load(x.path)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected load error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual([]byte(x.expectedContent), b) {
|
||||
t.Fatalf("in load expected %s, but got %s", x.expectedContent, b)
|
||||
}
|
||||
}
|
||||
l2, err := l1.New("foo/project")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if "/foo/project" != l2.Root() {
|
||||
t.Fatalf("incorrect root: %s\n", l2.Root())
|
||||
}
|
||||
for _, x := range testCases {
|
||||
b, err := l2.Load(strings.TrimPrefix(x.path, "foo/project/"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected load error %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual([]byte(x.expectedContent), b) {
|
||||
t.Fatalf("in load expected %s, but got %s", x.expectedContent, b)
|
||||
}
|
||||
}
|
||||
l2, err = l1.New("foo/project/") // Assure trailing slash stripped
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if "/foo/project" != l2.Root() {
|
||||
t.Fatalf("incorrect root: %s\n", l2.Root())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoaderNewSubDir(t *testing.T) {
|
||||
l1, err := makeLoader().New("foo/project")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
l2, err := l1.New("subdir1")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if "/foo/project/subdir1" != l2.Root() {
|
||||
t.Fatalf("incorrect root: %s\n", l2.Root())
|
||||
}
|
||||
x := testCases[1]
|
||||
b, err := l2.Load("fileB.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected load error %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual([]byte(x.expectedContent), b) {
|
||||
t.Fatalf("in load expected %s, but got %s", x.expectedContent, b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoaderBadRelative(t *testing.T) {
|
||||
l1, err := makeLoader().New("foo/project/subdir1")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if "/foo/project/subdir1" != l1.Root() {
|
||||
t.Fatalf("incorrect root: %s\n", l1.Root())
|
||||
}
|
||||
|
||||
// Cannot cd into a file.
|
||||
l2, err := l1.New("fileB.yaml")
|
||||
if err == nil {
|
||||
t.Fatalf("expected err, but got root %s", l2.Root())
|
||||
}
|
||||
|
||||
// It's not okay to stay at the same place.
|
||||
l2, err = l1.New(".")
|
||||
if err == nil {
|
||||
t.Fatalf("expected err, but got root %s", l2.Root())
|
||||
}
|
||||
|
||||
// It's not okay to go up and back down into same place.
|
||||
l2, err = l1.New("../subdir1")
|
||||
if err == nil {
|
||||
t.Fatalf("expected err, but got root %s", l2.Root())
|
||||
}
|
||||
|
||||
// It's not okay to go up via a relative path.
|
||||
l2, err = l1.New("..")
|
||||
if err == nil {
|
||||
t.Fatalf("expected err, but got root %s", l2.Root())
|
||||
}
|
||||
|
||||
// It's not okay to go up via an absolute path.
|
||||
l2, err = l1.New("/foo/project")
|
||||
if err == nil {
|
||||
t.Fatalf("expected err, but got root %s", l2.Root())
|
||||
}
|
||||
|
||||
// It's not okay to go to the root.
|
||||
l2, err = l1.New("/")
|
||||
if err == nil {
|
||||
t.Fatalf("expected err, but got root %s", l2.Root())
|
||||
}
|
||||
|
||||
// It's okay to go up and down to a sibling.
|
||||
l2, err = l1.New("../subdir2")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected new error %v", err)
|
||||
}
|
||||
if "/foo/project/subdir2" != l2.Root() {
|
||||
t.Fatalf("incorrect root: %s\n", l2.Root())
|
||||
}
|
||||
x := testCases[2]
|
||||
b, err := l2.Load("fileC.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected load error %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual([]byte(x.expectedContent), b) {
|
||||
t.Fatalf("in load expected %s, but got %s", x.expectedContent, b)
|
||||
}
|
||||
|
||||
// It's not OK to go over to a previously visited directory.
|
||||
// Must disallow going back and forth in a cycle.
|
||||
l1, err = l2.New("../subdir1")
|
||||
if err == nil {
|
||||
t.Fatalf("expected err, but got root %s", l1.Root())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoaderMisc(t *testing.T) {
|
||||
l := makeLoader()
|
||||
_, err := l.New("")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error for empty root location not returned")
|
||||
}
|
||||
_, err = l.New("https://google.com/project")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error")
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
contentOk = "hi there, i'm OK data"
|
||||
contentExteriorData = "i am data from outside the root"
|
||||
)
|
||||
|
||||
// Create a structure like this
|
||||
//
|
||||
// /tmp/kustomize-test-random
|
||||
// ├── base
|
||||
// │ ├── okayData
|
||||
// │ ├── symLinkToOkayData -> okayData
|
||||
// │ └── symLinkToExteriorData -> ../exteriorData
|
||||
// └── exteriorData
|
||||
//
|
||||
func commonSetupForLoaderRestrictionTest() (string, filesys.FileSystem, error) {
|
||||
dir, err := ioutil.TempDir("", "kustomize-test-")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
fSys := filesys.MakeFsOnDisk()
|
||||
fSys.Mkdir(filepath.Join(dir, "base"))
|
||||
|
||||
fSys.WriteFile(
|
||||
filepath.Join(dir, "base", "okayData"), []byte(contentOk))
|
||||
|
||||
fSys.WriteFile(
|
||||
filepath.Join(dir, "exteriorData"), []byte(contentExteriorData))
|
||||
|
||||
os.Symlink(
|
||||
filepath.Join(dir, "base", "okayData"),
|
||||
filepath.Join(dir, "base", "symLinkToOkayData"))
|
||||
os.Symlink(
|
||||
filepath.Join(dir, "exteriorData"),
|
||||
filepath.Join(dir, "base", "symLinkToExteriorData"))
|
||||
return dir, fSys, nil
|
||||
}
|
||||
|
||||
// Make sure everything works when loading files
|
||||
// in or below the loader root.
|
||||
func doSanityChecksAndDropIntoBase(
|
||||
t *testing.T, l ifc.Loader) ifc.Loader {
|
||||
data, err := l.Load(path.Join("base", "okayData"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if string(data) != contentOk {
|
||||
t.Fatalf("unexpected content: %v", data)
|
||||
}
|
||||
data, err = l.Load("exteriorData")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if string(data) != contentExteriorData {
|
||||
t.Fatalf("unexpected content: %v", data)
|
||||
}
|
||||
|
||||
// Drop in.
|
||||
l, err = l.New("base")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Reading okayData works.
|
||||
data, err = l.Load("okayData")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if string(data) != contentOk {
|
||||
t.Fatalf("unexpected content: %v", data)
|
||||
}
|
||||
|
||||
// Reading local symlink to okayData works.
|
||||
data, err = l.Load("symLinkToOkayData")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if string(data) != contentOk {
|
||||
t.Fatalf("unexpected content: %v", data)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func TestRestrictionRootOnlyInRealLoader(t *testing.T) {
|
||||
dir, fSys, err := commonSetupForLoaderRestrictionTest()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
var l ifc.Loader
|
||||
|
||||
l = newLoaderOrDie(RestrictionRootOnly, fSys, dir)
|
||||
|
||||
l = doSanityChecksAndDropIntoBase(t, l)
|
||||
|
||||
// Reading symlink to exteriorData fails.
|
||||
_, err = l.Load("symLinkToExteriorData")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "is not in or below") {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
|
||||
// Attempt to read "up" fails, though earlier we were
|
||||
// able to read this file when root was "..".
|
||||
_, err = l.Load("../exteriorData")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "is not in or below") {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestrictionNoneInRealLoader(t *testing.T) {
|
||||
dir, fSys, err := commonSetupForLoaderRestrictionTest()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
var l ifc.Loader
|
||||
|
||||
l = newLoaderOrDie(RestrictionNone, fSys, dir)
|
||||
|
||||
l = doSanityChecksAndDropIntoBase(t, l)
|
||||
|
||||
// Reading symlink to exteriorData works.
|
||||
_, err = l.Load("symLinkToExteriorData")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Attempt to read "up" works.
|
||||
_, err = l.Load("../exteriorData")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func splitOnNthSlash(v string, n int) (string, string) {
|
||||
left := ""
|
||||
for i := 0; i < n; i++ {
|
||||
k := strings.Index(v, "/")
|
||||
if k < 0 {
|
||||
break
|
||||
}
|
||||
left = left + v[:k+1]
|
||||
v = v[k+1:]
|
||||
}
|
||||
return left[:len(left)-1], v
|
||||
}
|
||||
|
||||
func TestSplit(t *testing.T) {
|
||||
p := "a/b/c/d/e/f/g"
|
||||
if left, right := splitOnNthSlash(p, 2); left != "a/b" || right != "c/d/e/f/g" {
|
||||
t.Fatalf("got left='%s', right='%s'", left, right)
|
||||
}
|
||||
if left, right := splitOnNthSlash(p, 3); left != "a/b/c" || right != "d/e/f/g" {
|
||||
t.Fatalf("got left='%s', right='%s'", left, right)
|
||||
}
|
||||
if left, right := splitOnNthSlash(p, 6); left != "a/b/c/d/e/f" || right != "g" {
|
||||
t.Fatalf("got left='%s', right='%s'", left, right)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLoaderAtGitClone(t *testing.T) {
|
||||
rootUrl := "github.com/someOrg/someRepo"
|
||||
pathInRepo := "foo/base"
|
||||
url := rootUrl + "/" + pathInRepo
|
||||
coRoot := "/tmp"
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys.MkdirAll(coRoot)
|
||||
fSys.MkdirAll(coRoot + "/" + pathInRepo)
|
||||
fSys.WriteFile(
|
||||
coRoot+"/"+pathInRepo+"/"+
|
||||
pgmconfig.DefaultKustomizationFileName(),
|
||||
[]byte(`
|
||||
whatever
|
||||
`))
|
||||
|
||||
repoSpec, err := git.NewRepoSpecFromUrl(url)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
l, err := newLoaderAtGitClone(
|
||||
repoSpec, fSys, nil,
|
||||
git.DoNothingCloner(filesys.ConfirmedDir(coRoot)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if coRoot+"/"+pathInRepo != l.Root() {
|
||||
t.Fatalf("expected root '%s', got '%s'\n",
|
||||
coRoot+"/"+pathInRepo, l.Root())
|
||||
}
|
||||
if _, err = l.New(url); err == nil {
|
||||
t.Fatalf("expected cycle error 1")
|
||||
}
|
||||
if _, err = l.New(rootUrl + "/" + "foo"); err == nil {
|
||||
t.Fatalf("expected cycle error 2")
|
||||
}
|
||||
|
||||
pathInRepo = "foo/overlay"
|
||||
fSys.MkdirAll(coRoot + "/" + pathInRepo)
|
||||
url = rootUrl + "/" + pathInRepo
|
||||
l2, err := l.New(url)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if coRoot+"/"+pathInRepo != l2.Root() {
|
||||
t.Fatalf("expected root '%s', got '%s'\n",
|
||||
coRoot+"/"+pathInRepo, l2.Root())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoaderDisallowsLocalBaseFromRemoteOverlay(t *testing.T) {
|
||||
// Define an overlay-base structure in the file system.
|
||||
topDir := "/whatever"
|
||||
cloneRoot := topDir + "/someClone"
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys.MkdirAll(topDir + "/highBase")
|
||||
fSys.MkdirAll(cloneRoot + "/foo/base")
|
||||
fSys.MkdirAll(cloneRoot + "/foo/overlay")
|
||||
|
||||
var l1 ifc.Loader
|
||||
|
||||
// Establish that a local overlay can navigate
|
||||
// to the local bases.
|
||||
l1 = newLoaderOrDie(
|
||||
RestrictionRootOnly, fSys, cloneRoot+"/foo/overlay")
|
||||
if l1.Root() != cloneRoot+"/foo/overlay" {
|
||||
t.Fatalf("unexpected root %s", l1.Root())
|
||||
}
|
||||
l2, err := l1.New("../base")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if l2.Root() != cloneRoot+"/foo/base" {
|
||||
t.Fatalf("unexpected root %s", l2.Root())
|
||||
}
|
||||
l3, err := l2.New("../../../highBase")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if l3.Root() != topDir+"/highBase" {
|
||||
t.Fatalf("unexpected root %s", l3.Root())
|
||||
}
|
||||
|
||||
// Establish that a Kustomization found in cloned
|
||||
// repo can reach (non-remote) bases inside the clone
|
||||
// but cannot reach a (non-remote) base outside the
|
||||
// clone but legitimately on the local file system.
|
||||
// This is to avoid a surprising interaction between
|
||||
// a remote K and local files. The remote K would be
|
||||
// non-functional on its own since by definition it
|
||||
// would refer to a non-remote base file that didn't
|
||||
// exist in its own repository, so presumably the
|
||||
// remote K would be deliberately designed to phish
|
||||
// for local K's.
|
||||
repoSpec, err := git.NewRepoSpecFromUrl(
|
||||
"github.com/someOrg/someRepo/foo/overlay")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
l1, err = newLoaderAtGitClone(
|
||||
repoSpec, fSys, nil,
|
||||
git.DoNothingCloner(filesys.ConfirmedDir(cloneRoot)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if l1.Root() != cloneRoot+"/foo/overlay" {
|
||||
t.Fatalf("unexpected root %s", l1.Root())
|
||||
}
|
||||
// This is okay.
|
||||
l2, err = l1.New("../base")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if l2.Root() != cloneRoot+"/foo/base" {
|
||||
t.Fatalf("unexpected root %s", l2.Root())
|
||||
}
|
||||
// This is not okay.
|
||||
l3, err = l2.New("../../../highBase")
|
||||
if err == nil {
|
||||
t.Fatalf("expected err")
|
||||
}
|
||||
if !strings.Contains(err.Error(),
|
||||
"base '/whatever/highBase' is outside '/whatever/someClone'") {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalLoaderReferencingGitBase(t *testing.T) {
|
||||
topDir := "/whatever"
|
||||
cloneRoot := topDir + "/someClone"
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys.MkdirAll(topDir)
|
||||
fSys.MkdirAll(cloneRoot + "/foo/base")
|
||||
|
||||
root, err := demandDirectoryRoot(fSys, topDir)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
l1 := newLoaderAtConfirmedDir(
|
||||
RestrictionRootOnly, root, fSys, nil,
|
||||
git.DoNothingCloner(filesys.ConfirmedDir(cloneRoot)))
|
||||
if l1.Root() != topDir {
|
||||
t.Fatalf("unexpected root %s", l1.Root())
|
||||
}
|
||||
l2, err := l1.New("github.com/someOrg/someRepo/foo/base")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
if l2.Root() != cloneRoot+"/foo/base" {
|
||||
t.Fatalf("unexpected root %s", l2.Root())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoDirectCycleDetection(t *testing.T) {
|
||||
topDir := "/cycles"
|
||||
cloneRoot := topDir + "/someClone"
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys.MkdirAll(topDir)
|
||||
fSys.MkdirAll(cloneRoot)
|
||||
|
||||
root, err := demandDirectoryRoot(fSys, topDir)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v\n", err)
|
||||
}
|
||||
l1 := newLoaderAtConfirmedDir(
|
||||
RestrictionRootOnly, root, fSys, nil,
|
||||
git.DoNothingCloner(filesys.ConfirmedDir(cloneRoot)))
|
||||
p1 := "github.com/someOrg/someRepo/foo"
|
||||
rs1, err := git.NewRepoSpecFromUrl(p1)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
l1.repoSpec = rs1
|
||||
_, err = l1.New(p1)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cycle detected") {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoIndirectCycleDetection(t *testing.T) {
|
||||
topDir := "/cycles"
|
||||
cloneRoot := topDir + "/someClone"
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys.MkdirAll(topDir)
|
||||
fSys.MkdirAll(cloneRoot)
|
||||
|
||||
root, err := demandDirectoryRoot(fSys, topDir)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
l0 := newLoaderAtConfirmedDir(
|
||||
RestrictionRootOnly, root, fSys, nil,
|
||||
git.DoNothingCloner(filesys.ConfirmedDir(cloneRoot)))
|
||||
|
||||
p1 := "github.com/someOrg/someRepo1"
|
||||
p2 := "github.com/someOrg/someRepo2"
|
||||
|
||||
l1, err := l0.New(p1)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
l2, err := l1.New(p2)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
_, err = l2.New(p1)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cycle detected") {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
}
|
||||
34
api/loader/loader.go
Normal file
34
api/loader/loader.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package loader has a data loading interface and various implementations.
|
||||
package loader
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/v3/api/filesys"
|
||||
"sigs.k8s.io/kustomize/v3/api/ifc"
|
||||
"sigs.k8s.io/kustomize/v3/pkg/git"
|
||||
)
|
||||
|
||||
// NewLoader returns a Loader pointed at the given target.
|
||||
// If the target is remote, the loader will be restricted
|
||||
// to the root and below only. If the target is local, the
|
||||
// loader will have the restrictions passed in. Regardless,
|
||||
// if a local target attempts to transitively load remote bases,
|
||||
// the remote bases will all be root-only restricted.
|
||||
func NewLoader(
|
||||
lr LoadRestrictorFunc,
|
||||
target string, fSys filesys.FileSystem) (ifc.Loader, error) {
|
||||
repoSpec, err := git.NewRepoSpecFromUrl(target)
|
||||
if err == nil {
|
||||
// The target qualifies as a remote git target.
|
||||
return newLoaderAtGitClone(
|
||||
repoSpec, fSys, nil, git.ClonerUsingGitExec)
|
||||
}
|
||||
root, err := demandDirectoryRoot(fSys, target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newLoaderAtConfirmedDir(
|
||||
lr, root, fSys, nil, git.ClonerUsingGitExec), nil
|
||||
}
|
||||
76
api/loader/loadrestrictions.go
Normal file
76
api/loader/loadrestrictions.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package loader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"sigs.k8s.io/kustomize/v3/api/filesys"
|
||||
)
|
||||
|
||||
//go:generate stringer -type=loadRestrictions
|
||||
type loadRestrictions int
|
||||
|
||||
const (
|
||||
unknown loadRestrictions = iota
|
||||
rootOnly
|
||||
none
|
||||
)
|
||||
|
||||
const (
|
||||
flagName = "load_restrictor"
|
||||
)
|
||||
|
||||
var (
|
||||
flagValue = rootOnly.String()
|
||||
flagHelp = "if set to '" + none.String() +
|
||||
"', local kustomizations may load files from outside their root. " +
|
||||
"This does, however, break the relocatability of the kustomization."
|
||||
)
|
||||
|
||||
func AddFlagLoadRestrictor(set *pflag.FlagSet) {
|
||||
set.StringVar(
|
||||
&flagValue, flagName,
|
||||
rootOnly.String(), flagHelp)
|
||||
}
|
||||
|
||||
func ValidateFlagLoadRestrictor() (LoadRestrictorFunc, error) {
|
||||
switch flagValue {
|
||||
case rootOnly.String():
|
||||
return RestrictionRootOnly, nil
|
||||
case none.String():
|
||||
return RestrictionNone, nil
|
||||
default:
|
||||
return nil, fmt.Errorf(
|
||||
"illegal flag value --%s %s; legal values: %v",
|
||||
flagName, flagValue,
|
||||
[]string{rootOnly.String(), none.String()})
|
||||
}
|
||||
}
|
||||
|
||||
type LoadRestrictorFunc func(
|
||||
filesys.FileSystem, filesys.ConfirmedDir, string) (string, error)
|
||||
|
||||
func RestrictionRootOnly(
|
||||
fSys filesys.FileSystem, root filesys.ConfirmedDir, path string) (string, error) {
|
||||
d, f, err := fSys.CleanedAbs(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if f == "" {
|
||||
return "", fmt.Errorf("'%s' must be a file", path)
|
||||
}
|
||||
if !d.HasPrefix(root) {
|
||||
return "", fmt.Errorf(
|
||||
"security; file '%s' is not in or below '%s'",
|
||||
path, root)
|
||||
}
|
||||
return d.Join(f), nil
|
||||
}
|
||||
|
||||
func RestrictionNone(
|
||||
_ filesys.FileSystem, _ filesys.ConfirmedDir, path string) (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
41
api/loader/loadrestrictions_string.go
Normal file
41
api/loader/loadrestrictions_string.go
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by "stringer -type=loadRestrictions"; DO NOT EDIT.
|
||||
|
||||
package loader
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[unknown-0]
|
||||
_ = x[rootOnly-1]
|
||||
_ = x[none-2]
|
||||
}
|
||||
|
||||
const _loadRestrictions_name = "unknownrootOnlynone"
|
||||
|
||||
var _loadRestrictions_index = [...]uint8{0, 7, 15, 19}
|
||||
|
||||
func (i loadRestrictions) String() string {
|
||||
if i < 0 || i >= loadRestrictions(len(_loadRestrictions_index)-1) {
|
||||
return "loadRestrictions(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _loadRestrictions_name[_loadRestrictions_index[i]:_loadRestrictions_index[i+1]]
|
||||
}
|
||||
74
api/loader/loadrestrictions_test.go
Normal file
74
api/loader/loadrestrictions_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package loader
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/filesys"
|
||||
)
|
||||
|
||||
func TestRestrictionNone(t *testing.T) {
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
root := filesys.ConfirmedDir("irrelevant")
|
||||
path := "whatever"
|
||||
p, err := RestrictionNone(fSys, root, path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p != path {
|
||||
t.Fatalf("expected '%s', got '%s'", path, p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestrictionRootOnly(t *testing.T) {
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
root := filesys.ConfirmedDir("/tmp/foo")
|
||||
|
||||
path := "/tmp/foo/whatever/beans"
|
||||
p, err := RestrictionRootOnly(fSys, root, path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p != path {
|
||||
t.Fatalf("expected '%s', got '%s'", path, p)
|
||||
}
|
||||
|
||||
// Legal.
|
||||
path = "/tmp/foo/whatever/../../foo/whatever"
|
||||
p, err = RestrictionRootOnly(fSys, root, path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path = "/tmp/foo/whatever"
|
||||
if p != path {
|
||||
t.Fatalf("expected '%s', got '%s'", path, p)
|
||||
}
|
||||
|
||||
// Illegal.
|
||||
path = "/tmp/illegal"
|
||||
_, err = RestrictionRootOnly(fSys, root, path)
|
||||
if err == nil {
|
||||
t.Fatal("should have an error")
|
||||
}
|
||||
if !strings.Contains(
|
||||
err.Error(),
|
||||
"file '/tmp/illegal' is not in or below '/tmp/foo'") {
|
||||
t.Fatalf("unexpected err: %s", err)
|
||||
}
|
||||
}
|
||||
136
api/resmap/factory.go
Normal file
136
api/resmap/factory.go
Normal 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
266
api/resmap/factory_test.go
Normal 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
37
api/resmap/idslice.go
Normal 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()
|
||||
}
|
||||
52
api/resmap/idslice_test.go
Normal file
52
api/resmap/idslice_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
15
api/resmap/patchfactory.go
Normal file
15
api/resmap/patchfactory.go
Normal 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
779
api/resmap/resmap.go
Normal 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
896
api/resmap/resmap_test.go
Normal 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
178
api/resmap/selector_test.go
Normal 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
179
api/resource/factory.go
Normal 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
|
||||
}
|
||||
211
api/resource/factory_test.go
Normal file
211
api/resource/factory_test.go
Normal 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
327
api/resource/resource.go
Normal 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
|
||||
}
|
||||
138
api/resource/resource_test.go
Normal file
138
api/resource/resource_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
84
api/testutils/resmaptest/rmbuilder.go
Normal file
84
api/testutils/resmaptest/rmbuilder.go
Normal 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
|
||||
}
|
||||
108
api/testutils/valtest/fakevalidator.go
Normal file
108
api/testutils/valtest/fakevalidator.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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{}
|
||||
|
||||
Reference in New Issue
Block a user