mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-05-17 18:25:26 +00:00
Move ks8deps to api for now.
This commit is contained in:
@@ -8,11 +8,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/builtinconfig"
|
||||
"sigs.k8s.io/kustomize/v3/api/k8sdeps/kunstruct"
|
||||
"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/k8sdeps/kunstruct"
|
||||
)
|
||||
|
||||
func TestNameReferenceHappyRun(t *testing.T) {
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/k8sdeps/kunstruct"
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestRefVarTransformer(t *testing.T) {
|
||||
|
||||
@@ -12,12 +12,12 @@ import (
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/builtinconfig"
|
||||
. "sigs.k8s.io/kustomize/v3/api/internal/accumulator"
|
||||
"sigs.k8s.io/kustomize/v3/api/k8sdeps/kunstruct"
|
||||
"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"
|
||||
)
|
||||
|
||||
func makeResAccumulator(t *testing.T) (*ResAccumulator, *resource.Factory) {
|
||||
|
||||
72
api/k8sdeps/configmapandsecret/configmapfactory.go
Normal file
72
api/k8sdeps/configmapandsecret/configmapfactory.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package configmapandsecret generates configmaps and secrets per generator rules.
|
||||
package configmapandsecret
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/kustomize/v3/api/types"
|
||||
)
|
||||
|
||||
func makeFreshConfigMap(
|
||||
args *types.ConfigMapArgs) *v1.ConfigMap {
|
||||
cm := &v1.ConfigMap{}
|
||||
cm.APIVersion = "v1"
|
||||
cm.Kind = "ConfigMap"
|
||||
cm.Name = args.Name
|
||||
cm.Namespace = args.Namespace
|
||||
cm.Data = map[string]string{}
|
||||
return cm
|
||||
}
|
||||
|
||||
// MakeConfigMap returns a new ConfigMap, or nil and an error.
|
||||
func (f *Factory) MakeConfigMap(
|
||||
args *types.ConfigMapArgs) (*v1.ConfigMap, error) {
|
||||
all, err := f.kvLdr.Load(args.KvPairSources)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "loading KV pairs")
|
||||
}
|
||||
cm := makeFreshConfigMap(args)
|
||||
for _, p := range all {
|
||||
err = f.addKvToConfigMap(cm, p)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "trouble mapping")
|
||||
}
|
||||
}
|
||||
if f.options != nil {
|
||||
cm.SetLabels(f.options.Labels)
|
||||
cm.SetAnnotations(f.options.Annotations)
|
||||
}
|
||||
return cm, nil
|
||||
}
|
||||
|
||||
// addKvToConfigMap adds the given key and data to the given config map.
|
||||
// Error if key invalid, or already exists.
|
||||
func (f *Factory) addKvToConfigMap(configMap *v1.ConfigMap, p types.Pair) error {
|
||||
if err := f.kvLdr.Validator().ErrIfInvalidKey(p.Key); err != nil {
|
||||
return err
|
||||
}
|
||||
// If the configmap data contains byte sequences that are all in the UTF-8
|
||||
// range, we will write it to .Data
|
||||
if utf8.Valid([]byte(p.Value)) {
|
||||
if _, entryExists := configMap.Data[p.Key]; entryExists {
|
||||
return fmt.Errorf(keyExistsErrorMsg, p.Key, configMap.Data)
|
||||
}
|
||||
configMap.Data[p.Key] = p.Value
|
||||
return nil
|
||||
}
|
||||
// otherwise, it's BinaryData
|
||||
if configMap.BinaryData == nil {
|
||||
configMap.BinaryData = map[string][]byte{}
|
||||
}
|
||||
if _, entryExists := configMap.BinaryData[p.Key]; entryExists {
|
||||
return fmt.Errorf(keyExistsErrorMsg, p.Key, configMap.BinaryData)
|
||||
}
|
||||
configMap.BinaryData[p.Key] = []byte(p.Value)
|
||||
return nil
|
||||
}
|
||||
158
api/k8sdeps/configmapandsecret/configmapfactory_test.go
Normal file
158
api/k8sdeps/configmapandsecret/configmapfactory_test.go
Normal file
@@ -0,0 +1,158 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configmapandsecret
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/kustomize/v3/api/filesys"
|
||||
"sigs.k8s.io/kustomize/v3/api/kv"
|
||||
"sigs.k8s.io/kustomize/v3/api/loader"
|
||||
"sigs.k8s.io/kustomize/v3/api/testutils/valtest"
|
||||
"sigs.k8s.io/kustomize/v3/api/types"
|
||||
)
|
||||
|
||||
func makeEnvConfigMap(name string) *corev1.ConfigMap {
|
||||
return &corev1.ConfigMap{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ConfigMap",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"DB_USERNAME": "admin",
|
||||
"DB_PASSWORD": "somepw",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func makeFileConfigMap(name string) *corev1.ConfigMap {
|
||||
return &corev1.ConfigMap{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ConfigMap",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"app-init.ini": `FOO=bar
|
||||
BAR=baz
|
||||
`,
|
||||
},
|
||||
BinaryData: map[string][]byte{
|
||||
"app.bin": {0xff, 0xfd},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func makeLiteralConfigMap(name string) *corev1.ConfigMap {
|
||||
cm := &corev1.ConfigMap{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ConfigMap",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"a": "x",
|
||||
"b": "y",
|
||||
"c": "Hello World",
|
||||
"d": "true",
|
||||
},
|
||||
}
|
||||
cm.SetLabels(map[string]string{"foo": "bar"})
|
||||
return cm
|
||||
}
|
||||
|
||||
func TestConstructConfigMap(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
input types.ConfigMapArgs
|
||||
options *types.GeneratorOptions
|
||||
expected *corev1.ConfigMap
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{
|
||||
description: "construct config map from env",
|
||||
input: types.ConfigMapArgs{
|
||||
GeneratorArgs: types.GeneratorArgs{
|
||||
Name: "envConfigMap",
|
||||
KvPairSources: types.KvPairSources{
|
||||
EnvSources: []string{
|
||||
filepath.Join("configmap", "app.env"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
options: nil,
|
||||
expected: makeEnvConfigMap("envConfigMap"),
|
||||
},
|
||||
{
|
||||
description: "construct config map from file",
|
||||
input: types.ConfigMapArgs{
|
||||
GeneratorArgs: types.GeneratorArgs{
|
||||
Name: "fileConfigMap",
|
||||
KvPairSources: types.KvPairSources{
|
||||
FileSources: []string{
|
||||
filepath.Join("configmap", "app-init.ini"),
|
||||
filepath.Join("configmap", "app.bin"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
options: nil,
|
||||
expected: makeFileConfigMap("fileConfigMap"),
|
||||
},
|
||||
{
|
||||
description: "construct config map from literal",
|
||||
input: types.ConfigMapArgs{
|
||||
GeneratorArgs: types.GeneratorArgs{
|
||||
Name: "literalConfigMap",
|
||||
KvPairSources: types.KvPairSources{
|
||||
LiteralSources: []string{"a=x", "b=y", "c=\"Hello World\"", "d='true'"},
|
||||
},
|
||||
},
|
||||
},
|
||||
options: &types.GeneratorOptions{
|
||||
Labels: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
expected: makeLiteralConfigMap("literalConfigMap"),
|
||||
},
|
||||
}
|
||||
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys.WriteFile(
|
||||
filesys.RootedPath("configmap", "app.env"),
|
||||
[]byte("DB_USERNAME=admin\nDB_PASSWORD=somepw\n"))
|
||||
fSys.WriteFile(
|
||||
filesys.RootedPath("configmap", "app-init.ini"),
|
||||
[]byte("FOO=bar\nBAR=baz\n"))
|
||||
fSys.WriteFile(
|
||||
filesys.RootedPath("configmap", "app.bin"),
|
||||
[]byte{0xff, 0xfd})
|
||||
kvLdr := kv.NewLoader(
|
||||
loader.NewFileLoaderAtRoot(fSys),
|
||||
valtest_test.MakeFakeValidator())
|
||||
for _, tc := range testCases {
|
||||
f := NewFactory(kvLdr, tc.options)
|
||||
cm, err := f.MakeConfigMap(&tc.input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(*cm, *tc.expected) {
|
||||
t.Fatalf("in testcase: %q updated:\n%#v\ndoesn't match expected:\n%#v\n", tc.description, *cm, tc.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
23
api/k8sdeps/configmapandsecret/factory.go
Normal file
23
api/k8sdeps/configmapandsecret/factory.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configmapandsecret
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/v3/api/ifc"
|
||||
"sigs.k8s.io/kustomize/v3/api/types"
|
||||
)
|
||||
|
||||
// Factory makes ConfigMaps and Secrets.
|
||||
type Factory struct {
|
||||
kvLdr ifc.KvLoader
|
||||
options *types.GeneratorOptions
|
||||
}
|
||||
|
||||
// NewFactory returns a new factory that makes ConfigMaps and Secrets.
|
||||
func NewFactory(
|
||||
kvLdr ifc.KvLoader, o *types.GeneratorOptions) *Factory {
|
||||
return &Factory{kvLdr: kvLdr, options: o}
|
||||
}
|
||||
|
||||
const keyExistsErrorMsg = "cannot add key %s, another key by that name already exists: %v"
|
||||
58
api/k8sdeps/configmapandsecret/secretfactory.go
Normal file
58
api/k8sdeps/configmapandsecret/secretfactory.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configmapandsecret
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/kustomize/v3/api/types"
|
||||
)
|
||||
|
||||
func makeFreshSecret(
|
||||
args *types.SecretArgs) *corev1.Secret {
|
||||
s := &corev1.Secret{}
|
||||
s.APIVersion = "v1"
|
||||
s.Kind = "Secret"
|
||||
s.Name = args.Name
|
||||
s.Namespace = args.Namespace
|
||||
s.Type = corev1.SecretType(args.Type)
|
||||
if s.Type == "" {
|
||||
s.Type = corev1.SecretTypeOpaque
|
||||
}
|
||||
s.Data = map[string][]byte{}
|
||||
return s
|
||||
}
|
||||
|
||||
// MakeSecret returns a new secret.
|
||||
func (f *Factory) MakeSecret(
|
||||
args *types.SecretArgs) (*corev1.Secret, error) {
|
||||
all, err := f.kvLdr.Load(args.KvPairSources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := makeFreshSecret(args)
|
||||
for _, p := range all {
|
||||
err = f.addKvToSecret(s, p.Key, p.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if f.options != nil {
|
||||
s.SetLabels(f.options.Labels)
|
||||
s.SetAnnotations(f.options.Annotations)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (f *Factory) addKvToSecret(secret *corev1.Secret, keyName, data string) error {
|
||||
if err := f.kvLdr.Validator().ErrIfInvalidKey(keyName); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, entryExists := secret.Data[keyName]; entryExists {
|
||||
return fmt.Errorf(keyExistsErrorMsg, keyName, secret.Data)
|
||||
}
|
||||
secret.Data[keyName] = []byte(data)
|
||||
return nil
|
||||
}
|
||||
143
api/k8sdeps/configmapandsecret/secretfactory_test.go
Normal file
143
api/k8sdeps/configmapandsecret/secretfactory_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configmapandsecret
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/kustomize/v3/api/filesys"
|
||||
"sigs.k8s.io/kustomize/v3/api/kv"
|
||||
"sigs.k8s.io/kustomize/v3/api/loader"
|
||||
"sigs.k8s.io/kustomize/v3/api/testutils/valtest"
|
||||
"sigs.k8s.io/kustomize/v3/api/types"
|
||||
)
|
||||
|
||||
func makeEnvSecret(name string) *corev1.Secret {
|
||||
return &corev1.Secret{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Secret",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"DB_PASSWORD": []byte("somepw"),
|
||||
"DB_USERNAME": []byte("admin"),
|
||||
},
|
||||
Type: "Opaque",
|
||||
}
|
||||
}
|
||||
|
||||
func makeFileSecret(name string) *corev1.Secret {
|
||||
return &corev1.Secret{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Secret",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"app-init.ini": []byte(`FOO=bar
|
||||
BAR=baz
|
||||
`),
|
||||
},
|
||||
Type: "Opaque",
|
||||
}
|
||||
}
|
||||
|
||||
func makeLiteralSecret(name string) *corev1.Secret {
|
||||
s := &corev1.Secret{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Secret",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"a": []byte("x"),
|
||||
"b": []byte("y"),
|
||||
},
|
||||
Type: "Opaque",
|
||||
}
|
||||
s.SetLabels(map[string]string{"foo": "bar"})
|
||||
return s
|
||||
}
|
||||
|
||||
func TestConstructSecret(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
input types.SecretArgs
|
||||
options *types.GeneratorOptions
|
||||
expected *corev1.Secret
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{
|
||||
description: "construct secret from env",
|
||||
input: types.SecretArgs{
|
||||
GeneratorArgs: types.GeneratorArgs{
|
||||
Name: "envSecret",
|
||||
KvPairSources: types.KvPairSources{
|
||||
EnvSources: []string{"secret/app.env"},
|
||||
},
|
||||
},
|
||||
},
|
||||
options: nil,
|
||||
expected: makeEnvSecret("envSecret"),
|
||||
},
|
||||
{
|
||||
description: "construct secret from file",
|
||||
input: types.SecretArgs{
|
||||
GeneratorArgs: types.GeneratorArgs{
|
||||
Name: "fileSecret",
|
||||
KvPairSources: types.KvPairSources{
|
||||
FileSources: []string{"secret/app-init.ini"},
|
||||
},
|
||||
},
|
||||
},
|
||||
options: nil,
|
||||
expected: makeFileSecret("fileSecret"),
|
||||
},
|
||||
{
|
||||
description: "construct secret from literal",
|
||||
input: types.SecretArgs{
|
||||
GeneratorArgs: types.GeneratorArgs{
|
||||
Name: "literalSecret",
|
||||
KvPairSources: types.KvPairSources{
|
||||
LiteralSources: []string{"a=x", "b=y"},
|
||||
},
|
||||
},
|
||||
},
|
||||
options: &types.GeneratorOptions{
|
||||
Labels: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
expected: makeLiteralSecret("literalSecret"),
|
||||
},
|
||||
}
|
||||
|
||||
fSys := filesys.MakeFsInMemory()
|
||||
fSys.WriteFile("/secret/app.env", []byte("DB_USERNAME=admin\nDB_PASSWORD=somepw\n"))
|
||||
fSys.WriteFile("/secret/app-init.ini", []byte("FOO=bar\nBAR=baz\n"))
|
||||
kvLdr := kv.NewLoader(
|
||||
loader.NewFileLoaderAtRoot(fSys),
|
||||
valtest_test.MakeFakeValidator())
|
||||
for _, tc := range testCases {
|
||||
f := NewFactory(kvLdr, tc.options)
|
||||
cm, err := f.MakeSecret(&tc.input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(*cm, *tc.expected) {
|
||||
t.Fatalf("in testcase: %q updated:\n%#v\ndoesn't match expected:\n%#v\n", tc.description, *cm, tc.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
76
api/k8sdeps/doc.go
Normal file
76
api/k8sdeps/doc.go
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// It's possible that kustomize's features will be vendored into
|
||||
// the kubernetes/kubernetes repo and made available to kubectl
|
||||
// commands, while at the same time the kustomize program will
|
||||
// continue to exist as an independent CLI. Vendoring snapshots
|
||||
// would be taken just before a kubectl release.
|
||||
//
|
||||
// This creates a problem in that freestanding-kustomize depends on
|
||||
// (for example):
|
||||
//
|
||||
// https://github.com/kubernetes/apimachinery/
|
||||
// tree/master/pkg/util/yaml
|
||||
//
|
||||
// It vendors that package into
|
||||
// sigs.k8s.io/kustomize/vendor/k8s.io/apimachinery/
|
||||
//
|
||||
// Whereas kubectl-kustomize would have to depend on the "staging"
|
||||
// version of this code, located at
|
||||
//
|
||||
// https://github.com/kubernetes/kubernetes/
|
||||
// blob/master/staging/src/k8s.io/apimachinery/pkg/util/yaml
|
||||
//
|
||||
// which is "vendored" via symlinks:
|
||||
// k8s.io/kubernetes/vendor/k8s.io/apimachinery
|
||||
// is a symlink to
|
||||
// ../../staging/src/k8s.io/apimachinery
|
||||
//
|
||||
// The staging version is the canonical, under-development
|
||||
// version of the code that kubectl depends on, whereas the packages
|
||||
// at kubernetes/apimachinery are periodic snapshots of staging made
|
||||
// for outside tools to depend on.
|
||||
//
|
||||
// apimachinery isn't the only package that poses this problem, just
|
||||
// using it as a specific example.
|
||||
//
|
||||
// The kubectl binary cannot vendor in kustomize code that in
|
||||
// turn vendors in the non-staging packages.
|
||||
//
|
||||
// One way to fix some of this would be to copy code - a hard fork.
|
||||
// This has all the problems associated with a hard forking.
|
||||
//
|
||||
// Another way would be to break the kustomize repo into three:
|
||||
//
|
||||
// (1) kustomize - repo with the main() function,
|
||||
// vendoring (2) and (3).
|
||||
//
|
||||
// (2) kustomize-libs - packages used by (1) with no
|
||||
// apimachinery dependence.
|
||||
//
|
||||
// (3) kustomize-k8sdeps - A thin code layer that depends
|
||||
// on (vendors) apimachinery to provide thin implementations
|
||||
// to interfaces used in (2).
|
||||
//
|
||||
// The kubectl repo would then vendor from (2) only, and have
|
||||
// a local implementation of (3). With that in mind, it's clear
|
||||
// that (3) doesn't have to be a repo; the kustomize version of
|
||||
// the thin layer can live directly in (1).
|
||||
//
|
||||
// This package is the code in (3), meant for kustomize.
|
||||
|
||||
package k8sdeps
|
||||
138
api/k8sdeps/kunstruct/factory.go
Normal file
138
api/k8sdeps/kunstruct/factory.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kunstruct
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"sigs.k8s.io/kustomize/v3/api/ifc"
|
||||
"sigs.k8s.io/kustomize/v3/api/k8sdeps/configmapandsecret"
|
||||
"sigs.k8s.io/kustomize/v3/api/types"
|
||||
)
|
||||
|
||||
// KunstructuredFactoryImpl hides construction using apimachinery types.
|
||||
type KunstructuredFactoryImpl struct {
|
||||
hasher *kustHash
|
||||
}
|
||||
|
||||
var _ ifc.KunstructuredFactory = &KunstructuredFactoryImpl{}
|
||||
|
||||
// NewKunstructuredFactoryImpl returns a factory.
|
||||
func NewKunstructuredFactoryImpl() ifc.KunstructuredFactory {
|
||||
return &KunstructuredFactoryImpl{hasher: NewKustHash()}
|
||||
}
|
||||
|
||||
// Hasher returns a kunstructured hasher
|
||||
// input: kunstructured; output: string hash.
|
||||
func (kf *KunstructuredFactoryImpl) Hasher() ifc.KunstructuredHasher {
|
||||
return kf.hasher
|
||||
}
|
||||
|
||||
// SliceFromBytes returns a slice of Kunstructured.
|
||||
func (kf *KunstructuredFactoryImpl) SliceFromBytes(
|
||||
in []byte) ([]ifc.Kunstructured, error) {
|
||||
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(in), 1024)
|
||||
var result []ifc.Kunstructured
|
||||
var err error
|
||||
for err == nil || isEmptyYamlError(err) {
|
||||
var out unstructured.Unstructured
|
||||
err = decoder.Decode(&out)
|
||||
if err == nil {
|
||||
if len(out.Object) == 0 {
|
||||
continue
|
||||
}
|
||||
err = kf.validate(out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, &UnstructAdapter{Unstructured: out})
|
||||
}
|
||||
}
|
||||
if err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func isEmptyYamlError(err error) bool {
|
||||
return strings.Contains(err.Error(), "is missing in 'null'")
|
||||
}
|
||||
|
||||
// FromMap returns an instance of Kunstructured.
|
||||
func (kf *KunstructuredFactoryImpl) FromMap(
|
||||
m map[string]interface{}) ifc.Kunstructured {
|
||||
return &UnstructAdapter{Unstructured: unstructured.Unstructured{Object: m}}
|
||||
}
|
||||
|
||||
// MakeConfigMap returns an instance of Kunstructured for ConfigMap
|
||||
func (kf *KunstructuredFactoryImpl) MakeConfigMap(
|
||||
kvLdr ifc.KvLoader,
|
||||
options *types.GeneratorOptions,
|
||||
args *types.ConfigMapArgs) (ifc.Kunstructured, error) {
|
||||
o, err := configmapandsecret.NewFactory(
|
||||
kvLdr, options).MakeConfigMap(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewKunstructuredFromObject(o)
|
||||
}
|
||||
|
||||
// MakeSecret returns an instance of Kunstructured for Secret
|
||||
func (kf *KunstructuredFactoryImpl) MakeSecret(
|
||||
kvLdr ifc.KvLoader,
|
||||
options *types.GeneratorOptions,
|
||||
args *types.SecretArgs) (ifc.Kunstructured, error) {
|
||||
o, err := configmapandsecret.NewFactory(
|
||||
kvLdr, options).MakeSecret(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewKunstructuredFromObject(o)
|
||||
}
|
||||
|
||||
// validate validates that u has kind and name
|
||||
// except for kind `List`, which doesn't require a name
|
||||
func (kf *KunstructuredFactoryImpl) validate(u unstructured.Unstructured) error {
|
||||
kind := u.GetKind()
|
||||
if kind == "" {
|
||||
return fmt.Errorf("missing kind in object %v", u)
|
||||
} else if strings.HasSuffix(kind, "List") {
|
||||
return nil
|
||||
}
|
||||
if u.GetName() == "" {
|
||||
return fmt.Errorf("missing metadata.name in object %v", u)
|
||||
}
|
||||
|
||||
if result, path := checkListItemNil(u.Object); result {
|
||||
return fmt.Errorf("empty item at %v in object %v", path, u)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkListItemNil(in interface{}) (bool, string) {
|
||||
switch v := in.(type) {
|
||||
case map[string]interface{}:
|
||||
for key, s := range v {
|
||||
if result, path := checkListItemNil(s); result {
|
||||
return result, key + "/" + path
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for index, s := range v {
|
||||
if s == nil {
|
||||
return true, ""
|
||||
}
|
||||
if result, path := checkListItemNil(s); result {
|
||||
return result, "[" + strconv.Itoa(index) + "]/" + path
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
218
api/k8sdeps/kunstruct/factory_test.go
Normal file
218
api/k8sdeps/kunstruct/factory_test.go
Normal file
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package kunstruct
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/ifc"
|
||||
)
|
||||
|
||||
func TestSliceFromBytes(t *testing.T) {
|
||||
factory := NewKunstructuredFactoryImpl()
|
||||
testConfigMap := factory.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "winnie",
|
||||
},
|
||||
})
|
||||
testList := factory.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "List",
|
||||
"items": []interface{}{
|
||||
testConfigMap.Map(),
|
||||
testConfigMap.Map(),
|
||||
},
|
||||
})
|
||||
testConfigMapList := factory.FromMap(
|
||||
map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMapList",
|
||||
"items": []interface{}{
|
||||
testConfigMap.Map(),
|
||||
testConfigMap.Map(),
|
||||
},
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
expectedOut []ifc.Kunstructured
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
name: "garbage",
|
||||
input: []byte("garbageIn: garbageOut"),
|
||||
expectedOut: []ifc.Kunstructured{},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "noBytes",
|
||||
input: []byte{},
|
||||
expectedOut: []ifc.Kunstructured{},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "goodJson",
|
||||
input: []byte(`
|
||||
{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"winnie"}}
|
||||
`),
|
||||
expectedOut: []ifc.Kunstructured{testConfigMap},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "goodYaml1",
|
||||
input: []byte(`
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: winnie
|
||||
`),
|
||||
expectedOut: []ifc.Kunstructured{testConfigMap},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "goodYaml2",
|
||||
input: []byte(`
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: winnie
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: winnie
|
||||
`),
|
||||
expectedOut: []ifc.Kunstructured{testConfigMap, testConfigMap},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "garbageInOneOfTwoObjects",
|
||||
input: []byte(`
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: winnie
|
||||
---
|
||||
WOOOOOOOOOOOOOOOOOOOOOOOOT: woot
|
||||
`),
|
||||
expectedOut: []ifc.Kunstructured{},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "emptyObjects",
|
||||
input: []byte(`
|
||||
---
|
||||
#a comment
|
||||
|
||||
---
|
||||
|
||||
`),
|
||||
expectedOut: []ifc.Kunstructured{},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "Missing .metadata.name in object",
|
||||
input: []byte(`
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
annotations:
|
||||
foo: bar
|
||||
`),
|
||||
expectedOut: nil,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "nil value in list",
|
||||
input: []byte(`
|
||||
apiVersion: builtin
|
||||
kind: ConfigMapGenerator
|
||||
metadata:
|
||||
name: kube100-site
|
||||
labels:
|
||||
app: web
|
||||
testList:
|
||||
- testA
|
||||
-
|
||||
`),
|
||||
expectedOut: nil,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "List",
|
||||
input: []byte(`
|
||||
apiVersion: v1
|
||||
kind: List
|
||||
items:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: winnie
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: winnie
|
||||
`),
|
||||
expectedOut: []ifc.Kunstructured{testList},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "ConfigMapList",
|
||||
input: []byte(`
|
||||
apiVersion: v1
|
||||
kind: ConfigMapList
|
||||
items:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: winnie
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: winnie
|
||||
`),
|
||||
expectedOut: []ifc.Kunstructured{testConfigMapList},
|
||||
expectedErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
rs, err := factory.SliceFromBytes(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])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
123
api/k8sdeps/kunstruct/hasher.go
Normal file
123
api/k8sdeps/kunstruct/hasher.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kunstruct
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"sigs.k8s.io/kustomize/v3/api/hasher"
|
||||
"sigs.k8s.io/kustomize/v3/api/ifc"
|
||||
)
|
||||
|
||||
// kustHash computes a hash of an unstructured object.
|
||||
type kustHash struct{}
|
||||
|
||||
// NewKustHash returns a kustHash object
|
||||
func NewKustHash() *kustHash {
|
||||
return &kustHash{}
|
||||
}
|
||||
|
||||
// Hash returns a hash of either a ConfigMap or a Secret
|
||||
func (h *kustHash) Hash(m ifc.Kunstructured) (string, error) {
|
||||
u := unstructured.Unstructured{
|
||||
Object: m.Map(),
|
||||
}
|
||||
kind := u.GetKind()
|
||||
switch kind {
|
||||
case "ConfigMap":
|
||||
cm, err := unstructuredToConfigmap(u)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return configMapHash(cm)
|
||||
case "Secret":
|
||||
sec, err := unstructuredToSecret(u)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return secretHash(sec)
|
||||
default:
|
||||
return "", fmt.Errorf(
|
||||
"type %s is not supported for hashing in %v",
|
||||
kind, m.Map())
|
||||
}
|
||||
}
|
||||
|
||||
// configMapHash returns a hash of the ConfigMap.
|
||||
// The Data, Kind, and Name are taken into account.
|
||||
func configMapHash(cm *v1.ConfigMap) (string, error) {
|
||||
encoded, err := encodeConfigMap(cm)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
h, err := hasher.Encode(hasher.Hash(encoded))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// SecretHash returns a hash of the Secret.
|
||||
// The Data, Kind, Name, and Type are taken into account.
|
||||
func secretHash(sec *v1.Secret) (string, error) {
|
||||
encoded, err := encodeSecret(sec)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
h, err := hasher.Encode(hasher.Hash(encoded))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// encodeConfigMap encodes a ConfigMap.
|
||||
// Data, Kind, and Name are taken into account.
|
||||
func encodeConfigMap(cm *v1.ConfigMap) (string, error) {
|
||||
// json.Marshal sorts the keys in a stable order in the encoding
|
||||
m := map[string]interface{}{"kind": "ConfigMap", "name": cm.Name, "data": cm.Data}
|
||||
if len(cm.BinaryData) > 0 {
|
||||
m["binaryData"] = cm.BinaryData
|
||||
}
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// encodeSecret encodes a Secret.
|
||||
// Data, Kind, Name, and Type are taken into account.
|
||||
func encodeSecret(sec *v1.Secret) (string, error) {
|
||||
// json.Marshal sorts the keys in a stable order in the encoding
|
||||
data, err := json.Marshal(map[string]interface{}{"kind": "Secret", "type": sec.Type, "name": sec.Name, "data": sec.Data})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func unstructuredToConfigmap(u unstructured.Unstructured) (*v1.ConfigMap, error) {
|
||||
marshaled, err := json.Marshal(u.Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out v1.ConfigMap
|
||||
err = json.Unmarshal(marshaled, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func unstructuredToSecret(u unstructured.Unstructured) (*v1.Secret, error) {
|
||||
marshaled, err := json.Marshal(u.Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out v1.Secret
|
||||
err = json.Unmarshal(marshaled, &out)
|
||||
return &out, err
|
||||
}
|
||||
180
api/k8sdeps/kunstruct/hasher_test.go
Normal file
180
api/k8sdeps/kunstruct/hasher_test.go
Normal file
@@ -0,0 +1,180 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kunstruct
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func TestConfigMapHash(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
cm *v1.ConfigMap
|
||||
hash string
|
||||
err string
|
||||
}{
|
||||
// empty map
|
||||
{"empty data", &v1.ConfigMap{Data: map[string]string{}, BinaryData: map[string][]byte{}}, "42745tchd9", ""},
|
||||
// one key
|
||||
{"one key", &v1.ConfigMap{Data: map[string]string{"one": ""}}, "9g67k2htb6", ""},
|
||||
// three keys (tests sorting order)
|
||||
{"three keys", &v1.ConfigMap{Data: map[string]string{"two": "2", "one": "", "three": "3"}}, "f5h7t85m9b", ""},
|
||||
// empty binary data map
|
||||
{"empty binary data", &v1.ConfigMap{BinaryData: map[string][]byte{}}, "dk855m5d49", ""},
|
||||
// one key with binary data
|
||||
{"one key with binary data", &v1.ConfigMap{BinaryData: map[string][]byte{"one": []byte("")}}, "mk79584b8c", ""},
|
||||
// three keys with binary data (tests sorting order)
|
||||
{"three keys with binary data", &v1.ConfigMap{BinaryData: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}}, "t458mc6db2", ""},
|
||||
// two keys, one with string and another with binary data
|
||||
{"two keys with one each", &v1.ConfigMap{Data: map[string]string{"one": ""}, BinaryData: map[string][]byte{"two": []byte("")}}, "698h7c7t9m", ""},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
h, err := configMapHash(c.cm)
|
||||
if SkipRest(t, c.desc, err, c.err) {
|
||||
continue
|
||||
}
|
||||
if c.hash != h {
|
||||
t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretHash(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
secret *v1.Secret
|
||||
hash string
|
||||
err string
|
||||
}{
|
||||
// empty map
|
||||
{"empty data", &v1.Secret{Type: "my-type", Data: map[string][]byte{}}, "t75bgf6ctb", ""},
|
||||
// one key
|
||||
{"one key", &v1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}}, "74bd68bm66", ""},
|
||||
// three keys (tests sorting order)
|
||||
{"three keys", &v1.Secret{Type: "my-type", Data: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}}, "dgcb6h9tmk", ""},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
h, err := secretHash(c.secret)
|
||||
if SkipRest(t, c.desc, err, c.err) {
|
||||
continue
|
||||
}
|
||||
if c.hash != h {
|
||||
t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeConfigMap(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
cm *v1.ConfigMap
|
||||
expect string
|
||||
err string
|
||||
}{
|
||||
// empty map
|
||||
{"empty data", &v1.ConfigMap{Data: map[string]string{}}, `{"data":{},"kind":"ConfigMap","name":""}`, ""},
|
||||
// one key
|
||||
{"one key", &v1.ConfigMap{Data: map[string]string{"one": ""}}, `{"data":{"one":""},"kind":"ConfigMap","name":""}`, ""},
|
||||
// three keys (tests sorting order)
|
||||
{"three keys", &v1.ConfigMap{Data: map[string]string{"two": "2", "one": "", "three": "3"}},
|
||||
`{"data":{"one":"","three":"3","two":"2"},"kind":"ConfigMap","name":""}`, ""},
|
||||
// empty binary map
|
||||
{"empty data", &v1.ConfigMap{BinaryData: map[string][]byte{}}, `{"data":null,"kind":"ConfigMap","name":""}`, ""},
|
||||
// one key with binary data
|
||||
{"one key", &v1.ConfigMap{BinaryData: map[string][]byte{"one": []byte("")}},
|
||||
`{"binaryData":{"one":""},"data":null,"kind":"ConfigMap","name":""}`, ""},
|
||||
// three keys with binary data (tests sorting order)
|
||||
{"three keys", &v1.ConfigMap{BinaryData: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")}},
|
||||
`{"binaryData":{"one":"","three":"Mw==","two":"Mg=="},"data":null,"kind":"ConfigMap","name":""}`, ""},
|
||||
// two keys, one string and one binary values
|
||||
{"two keys with one each", &v1.ConfigMap{Data: map[string]string{"one": ""}, BinaryData: map[string][]byte{"two": []byte("")}},
|
||||
`{"binaryData":{"two":""},"data":{"one":""},"kind":"ConfigMap","name":""}`, ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
s, err := encodeConfigMap(c.cm)
|
||||
if SkipRest(t, c.desc, err, c.err) {
|
||||
continue
|
||||
}
|
||||
if s != c.expect {
|
||||
t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.cm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeSecret(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
secret *v1.Secret
|
||||
expect string
|
||||
err string
|
||||
}{
|
||||
// empty map
|
||||
{"empty data", &v1.Secret{Type: "my-type", Data: map[string][]byte{}}, `{"data":{},"kind":"Secret","name":"","type":"my-type"}`, ""},
|
||||
// one key
|
||||
{"one key", &v1.Secret{Type: "my-type", Data: map[string][]byte{"one": []byte("")}}, `{"data":{"one":""},"kind":"Secret","name":"","type":"my-type"}`, ""},
|
||||
// three keys (tests sorting order) - note json.Marshal base64 encodes the values because they come in as []byte
|
||||
{"three keys", &v1.Secret{
|
||||
Type: "my-type",
|
||||
Data: map[string][]byte{"two": []byte("2"), "one": []byte(""), "three": []byte("3")},
|
||||
},
|
||||
`{"data":{"one":"","three":"Mw==","two":"Mg=="},"kind":"Secret","name":"","type":"my-type"}`, ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
s, err := encodeSecret(c.secret)
|
||||
if SkipRest(t, c.desc, err, c.err) {
|
||||
continue
|
||||
}
|
||||
if s != c.expect {
|
||||
t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.secret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// warn devs who change types that they might have to update a hash function
|
||||
// not perfect, as it only checks the number of top-level fields
|
||||
func TestTypeStability(t *testing.T) {
|
||||
errfmt := `case %q, expected %d fields but got %d
|
||||
Depending on the field(s) you added, you may need to modify the hash function for this type.
|
||||
To guide you: the hash function targets fields that comprise the contents of objects,
|
||||
not their metadata (e.g. the Data of a ConfigMap, but nothing in ObjectMeta).
|
||||
`
|
||||
cases := []struct {
|
||||
typeName string
|
||||
obj interface{}
|
||||
expect int
|
||||
}{
|
||||
{"ConfigMap", v1.ConfigMap{}, 4},
|
||||
{"Secret", v1.Secret{}, 5},
|
||||
}
|
||||
for _, c := range cases {
|
||||
val := reflect.ValueOf(c.obj)
|
||||
if num := val.NumField(); c.expect != num {
|
||||
t.Errorf(errfmt, c.typeName, c.expect, num)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SkipRest returns true if there was a non-nil error or if we expected an error that didn't happen,
|
||||
// and logs the appropriate error on the test object.
|
||||
// The return value indicates whether we should skip the rest of the test case due to the error result.
|
||||
func SkipRest(t *testing.T, desc string, err error, contains string) bool {
|
||||
if err != nil {
|
||||
if len(contains) == 0 {
|
||||
t.Errorf("case %q, expect nil error but got %q", desc, err.Error())
|
||||
} else if !strings.Contains(err.Error(), contains) {
|
||||
t.Errorf("case %q, expect error to contain %q but got %q", desc, contains, err.Error())
|
||||
}
|
||||
return true
|
||||
} else if len(contains) > 0 {
|
||||
t.Errorf("case %q, expect error to contain %q but got nil error", desc, contains)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
95
api/k8sdeps/kunstruct/helper.go
Normal file
95
api/k8sdeps/kunstruct/helper.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package kunstruct provides unstructured from api machinery and factory for creating unstructured
|
||||
package kunstruct
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A PathSection contains a list of nested fields, which may end with an
|
||||
// indexable value. For instance, foo.bar resolves to a PathSection with 2
|
||||
// fields and no index, while foo[0].bar resolves to two path sections, the
|
||||
// first containing the field foo and the index 0, and the second containing
|
||||
// the field bar, with no index. The latter PathSection references the bar
|
||||
// field of the first item in the foo list
|
||||
type PathSection struct {
|
||||
fields []string
|
||||
idx int
|
||||
}
|
||||
|
||||
func newPathSection() PathSection {
|
||||
return PathSection{idx: -1}
|
||||
}
|
||||
|
||||
func appendNonEmpty(section *PathSection, field string) {
|
||||
if len(field) != 0 {
|
||||
section.fields = append(section.fields, field)
|
||||
}
|
||||
}
|
||||
|
||||
func parseFields(path string) (result []PathSection, err error) {
|
||||
section := newPathSection()
|
||||
if !strings.Contains(path, "[") {
|
||||
section.fields = strings.Split(path, ".")
|
||||
result = append(result, section)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
start := 0
|
||||
insideParentheses := false
|
||||
for i, c := range path {
|
||||
switch c {
|
||||
case '.':
|
||||
if !insideParentheses {
|
||||
appendNonEmpty(§ion, path[start:i])
|
||||
start = i + 1
|
||||
}
|
||||
case '[':
|
||||
if !insideParentheses {
|
||||
appendNonEmpty(§ion, path[start:i])
|
||||
start = i + 1
|
||||
insideParentheses = true
|
||||
} else {
|
||||
return nil, fmt.Errorf("nested parentheses are not allowed: %s", path)
|
||||
}
|
||||
case ']':
|
||||
if insideParentheses {
|
||||
// Assign this index to the current
|
||||
// PathSection, save it to the result, then begin
|
||||
// a new PathSection
|
||||
tmpIdx, err := strconv.Atoi(path[start:i])
|
||||
if err == nil {
|
||||
// We have detected an integer so an array.
|
||||
section.idx = tmpIdx
|
||||
} else {
|
||||
// We have detected the downwardapi syntax
|
||||
appendNonEmpty(§ion, path[start:i])
|
||||
}
|
||||
result = append(result, section)
|
||||
section = newPathSection()
|
||||
|
||||
start = i + 1
|
||||
insideParentheses = false
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid field path %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
if start < len(path)-1 {
|
||||
appendNonEmpty(§ion, path[start:])
|
||||
result = append(result, section)
|
||||
}
|
||||
|
||||
for _, section := range result {
|
||||
for i, f := range section.fields {
|
||||
if strings.HasPrefix(f, "\"") || strings.HasPrefix(f, "'") {
|
||||
section.fields[i] = strings.Trim(f, "\"'")
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
152
api/k8sdeps/kunstruct/helper_test.go
Normal file
152
api/k8sdeps/kunstruct/helper_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kunstruct
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type PathSectionSlice []PathSection
|
||||
|
||||
func buildPath(idx int, fields ...string) PathSectionSlice {
|
||||
return PathSectionSlice{PathSection{fields: fields, idx: idx}}
|
||||
}
|
||||
|
||||
func (a PathSectionSlice) addSection(idx int, fields ...string) PathSectionSlice {
|
||||
return append(a, PathSection{fields: fields, idx: idx})
|
||||
}
|
||||
|
||||
func TestParseField(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pathToField string
|
||||
expectedValue []PathSection
|
||||
errorExpected bool
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "oneField",
|
||||
pathToField: "Kind",
|
||||
expectedValue: buildPath(-1, "Kind"),
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "twoFields",
|
||||
pathToField: "metadata.name",
|
||||
expectedValue: buildPath(-1, "metadata", "name"),
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "threeFields",
|
||||
pathToField: "spec.ports.port",
|
||||
expectedValue: buildPath(-1, "spec", "ports", "port"),
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
pathToField: "",
|
||||
expectedValue: buildPath(-1, ""),
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "validStringIndex",
|
||||
pathToField: "that[1]",
|
||||
expectedValue: buildPath(1, "that"),
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "sliceInSlice",
|
||||
pathToField: "that[1][0]",
|
||||
expectedValue: buildPath(1, "that").addSection(0),
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "validStructSubField",
|
||||
pathToField: "those[1].field2",
|
||||
expectedValue: buildPath(1, "those").addSection(-1, "field2"),
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldIndex",
|
||||
pathToField: "these[1].field2[0]",
|
||||
expectedValue: buildPath(1, "these").addSection(0, "field2"),
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldIndexSubfield",
|
||||
pathToField: "complextree[1].field2[1].stringsubfield",
|
||||
expectedValue: buildPath(1, "complextree").addSection(1, "field2").addSection(-1, "stringsubfield"),
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "validStructDownwardAPI",
|
||||
pathToField: `metadata.labels["app.kubernetes.io/component"]`,
|
||||
expectedValue: buildPath(-1, "metadata", "labels", "app.kubernetes.io/component"),
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "invalidIndexInIndex",
|
||||
pathToField: "complextree[1[0]]",
|
||||
errorExpected: true,
|
||||
errorMsg: "nested parentheses are not allowed: complextree[1[0]]",
|
||||
},
|
||||
{
|
||||
name: "invalidClosingBrackets",
|
||||
pathToField: "complextree[1]]",
|
||||
errorExpected: true,
|
||||
errorMsg: "invalid field path complextree[1]]",
|
||||
},
|
||||
{
|
||||
name: "validFieldsWithQuotes",
|
||||
pathToField: "'complextree'[1].field2[1].'stringsubfield'",
|
||||
expectedValue: buildPath(1, "complextree").addSection(1, "field2").addSection(-1, "stringsubfield"),
|
||||
errorExpected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
s, err := parseFields(test.pathToField)
|
||||
if test.errorExpected {
|
||||
compareExpectedParserError(t, test.name, test.pathToField, err, test.errorMsg)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
unExpectedParserError(t, test.name, test.pathToField, err)
|
||||
}
|
||||
compareParserValues(t, test.name, test.pathToField, test.expectedValue, s)
|
||||
}
|
||||
}
|
||||
|
||||
// unExpectedError function handles unexpected error
|
||||
func unExpectedParserError(t *testing.T, name string, pathToField string, err error) {
|
||||
t.Fatalf("%q; path %q - unexpected error %v", name, pathToField, err)
|
||||
}
|
||||
|
||||
// compareExpectedError compares the expectedError and the actualError return by parseFields
|
||||
func compareExpectedParserError(t *testing.T, name string, pathToField string, err error, errorMsg string) {
|
||||
if err == nil {
|
||||
t.Fatalf("%q; path %q - should return error, but no error returned",
|
||||
name, pathToField)
|
||||
}
|
||||
|
||||
if errorMsg != err.Error() {
|
||||
t.Fatalf("%q; path %q - expected error: \"%s\", got error: \"%v\"",
|
||||
name, pathToField, errorMsg, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// compareValues compares the expectedValue and actualValue returned by parseFields
|
||||
func compareParserValues(t *testing.T, name string, pathToField string, expectedValue PathSectionSlice, actualValue []PathSection) {
|
||||
t.Helper()
|
||||
if len(expectedValue) != len(actualValue) {
|
||||
t.Fatalf("%q; Path: %s Got: %v Expected: %v", name, pathToField, actualValue, expectedValue)
|
||||
}
|
||||
|
||||
for idx, expected := range expectedValue {
|
||||
if !reflect.DeepEqual(expected, actualValue[idx]) {
|
||||
t.Fatalf("%q; Path: %s idx: %v Fields Got: %v Expected: %v", name, pathToField, idx, actualValue[idx], expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
350
api/k8sdeps/kunstruct/unstructadapter.go
Normal file
350
api/k8sdeps/kunstruct/unstructadapter.go
Normal file
@@ -0,0 +1,350 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package kunstruct provides unstructured from api machinery and factory for creating unstructured
|
||||
package kunstruct
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"sigs.k8s.io/kustomize/v3/api/ifc"
|
||||
"sigs.k8s.io/kustomize/v3/api/resid"
|
||||
)
|
||||
|
||||
var _ ifc.Kunstructured = &UnstructAdapter{}
|
||||
|
||||
// UnstructAdapter wraps unstructured.Unstructured from
|
||||
// https://github.com/kubernetes/apimachinery/blob/master/
|
||||
// pkg/apis/meta/v1/unstructured/unstructured.go
|
||||
// to isolate dependence on apimachinery.
|
||||
type UnstructAdapter struct {
|
||||
unstructured.Unstructured
|
||||
}
|
||||
|
||||
// NewKunstructuredFromObject returns a new instance of Kunstructured.
|
||||
func NewKunstructuredFromObject(obj runtime.Object) (ifc.Kunstructured, error) {
|
||||
// Convert obj to a byte stream, then convert that to JSON (Unstructured).
|
||||
marshaled, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return &UnstructAdapter{}, err
|
||||
}
|
||||
var u unstructured.Unstructured
|
||||
err = u.UnmarshalJSON(marshaled)
|
||||
// creationTimestamp always 'null', remove it
|
||||
u.SetCreationTimestamp(metav1.Time{})
|
||||
return &UnstructAdapter{Unstructured: u}, err
|
||||
}
|
||||
|
||||
// GetGvk returns the Gvk name of the object.
|
||||
func (fs *UnstructAdapter) GetGvk() resid.Gvk {
|
||||
x := fs.GroupVersionKind()
|
||||
return resid.Gvk{
|
||||
Group: x.Group,
|
||||
Version: x.Version,
|
||||
Kind: x.Kind,
|
||||
}
|
||||
}
|
||||
|
||||
// SetGvk set the Gvk of the object to the input Gvk
|
||||
func (fs *UnstructAdapter) SetGvk(g resid.Gvk) {
|
||||
fs.SetGroupVersionKind(toSchemaGvk(g))
|
||||
}
|
||||
|
||||
// Copy provides a copy behind an interface.
|
||||
func (fs *UnstructAdapter) Copy() ifc.Kunstructured {
|
||||
return &UnstructAdapter{*fs.DeepCopy()}
|
||||
}
|
||||
|
||||
// Map returns the unstructured content map.
|
||||
func (fs *UnstructAdapter) Map() map[string]interface{} {
|
||||
return fs.Object
|
||||
}
|
||||
|
||||
// SetMap overrides the unstructured content map.
|
||||
func (fs *UnstructAdapter) SetMap(m map[string]interface{}) {
|
||||
fs.Object = m
|
||||
}
|
||||
|
||||
func (fs *UnstructAdapter) selectSubtree(path string) (map[string]interface{}, []string, bool, error) {
|
||||
sections, err := parseFields(path)
|
||||
if len(sections) == 0 || (err != nil) {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
|
||||
content := fs.UnstructuredContent()
|
||||
lastSectionIdx := len(sections)
|
||||
|
||||
// There are multiple sections to walk
|
||||
for sectionIdx := 0; sectionIdx < lastSectionIdx; sectionIdx++ {
|
||||
idx := sections[sectionIdx].idx
|
||||
fields := sections[sectionIdx].fields
|
||||
|
||||
if idx == -1 {
|
||||
// This section has no index
|
||||
return content, fields, true, nil
|
||||
}
|
||||
|
||||
// This section is terminated by an indexed field.
|
||||
// Let's extract the slice first
|
||||
indexedField, found, err := unstructured.NestedFieldNoCopy(content, fields...)
|
||||
if !found || err != nil {
|
||||
return content, fields, found, err
|
||||
}
|
||||
s, ok := indexedField.([]interface{})
|
||||
if !ok {
|
||||
return content, fields, false, fmt.Errorf("%v is of the type %T, expected []interface{}", indexedField, indexedField)
|
||||
}
|
||||
if idx >= len(s) {
|
||||
return content, fields, false, fmt.Errorf("index %d is out of bounds", idx)
|
||||
}
|
||||
|
||||
if sectionIdx == lastSectionIdx-1 {
|
||||
// This is the last section. Let's build a fake map
|
||||
// to let the rest of the field extraction to work.
|
||||
idxstring := fmt.Sprintf("[%v]", idx)
|
||||
newContent := map[string]interface{}{idxstring: s[idx]}
|
||||
newFields := []string{idxstring}
|
||||
return newContent, newFields, true, nil
|
||||
}
|
||||
|
||||
newContent, ok := s[idx].(map[string]interface{})
|
||||
if !ok {
|
||||
// Only map are supported here
|
||||
return content, fields, false,
|
||||
fmt.Errorf("%#v is expected to be of type map[string]interface{}", s[idx])
|
||||
}
|
||||
content = newContent
|
||||
}
|
||||
|
||||
// It seems to be an invalid path
|
||||
return nil, []string{}, false, nil
|
||||
}
|
||||
|
||||
// GetFieldValue returns the value at the given fieldpath.
|
||||
func (fs *UnstructAdapter) GetFieldValue(path string) (interface{}, error) {
|
||||
content, fields, found, err := fs.selectSubtree(path)
|
||||
if !found || err != nil {
|
||||
return nil, noFieldError{Field: path}
|
||||
}
|
||||
|
||||
s, found, err := unstructured.NestedFieldNoCopy(
|
||||
content, fields...)
|
||||
if found || err != nil {
|
||||
return s, err
|
||||
}
|
||||
return nil, noFieldError{Field: path}
|
||||
}
|
||||
|
||||
// GetString returns value at the given fieldpath.
|
||||
func (fs *UnstructAdapter) GetString(path string) (string, error) {
|
||||
content, fields, found, err := fs.selectSubtree(path)
|
||||
if !found || err != nil {
|
||||
return "", noFieldError{Field: path}
|
||||
}
|
||||
|
||||
s, found, err := unstructured.NestedString(
|
||||
content, fields...)
|
||||
if found || err != nil {
|
||||
return s, err
|
||||
}
|
||||
return "", noFieldError{Field: path}
|
||||
}
|
||||
|
||||
// GetStringSlice returns value at the given fieldpath.
|
||||
func (fs *UnstructAdapter) GetStringSlice(path string) ([]string, error) {
|
||||
content, fields, found, err := fs.selectSubtree(path)
|
||||
if !found || err != nil {
|
||||
return []string{}, noFieldError{Field: path}
|
||||
}
|
||||
|
||||
s, found, err := unstructured.NestedStringSlice(
|
||||
content, fields...)
|
||||
if found || err != nil {
|
||||
return s, err
|
||||
}
|
||||
return []string{}, noFieldError{Field: path}
|
||||
}
|
||||
|
||||
// GetBool returns value at the given fieldpath.
|
||||
func (fs *UnstructAdapter) GetBool(path string) (bool, error) {
|
||||
content, fields, found, err := fs.selectSubtree(path)
|
||||
if !found || err != nil {
|
||||
return false, noFieldError{Field: path}
|
||||
}
|
||||
|
||||
s, found, err := unstructured.NestedBool(
|
||||
content, fields...)
|
||||
if found || err != nil {
|
||||
return s, err
|
||||
}
|
||||
return false, noFieldError{Field: path}
|
||||
}
|
||||
|
||||
// GetFloat64 returns value at the given fieldpath.
|
||||
func (fs *UnstructAdapter) GetFloat64(path string) (float64, error) {
|
||||
content, fields, found, err := fs.selectSubtree(path)
|
||||
if !found || err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
s, found, err := unstructured.NestedFloat64(
|
||||
content, fields...)
|
||||
if found || err != nil {
|
||||
return s, err
|
||||
}
|
||||
return 0, noFieldError{Field: path}
|
||||
}
|
||||
|
||||
// GetInt64 returns value at the given fieldpath.
|
||||
func (fs *UnstructAdapter) GetInt64(path string) (int64, error) {
|
||||
content, fields, found, err := fs.selectSubtree(path)
|
||||
if !found || err != nil {
|
||||
return 0, noFieldError{Field: path}
|
||||
}
|
||||
|
||||
s, found, err := unstructured.NestedInt64(
|
||||
content, fields...)
|
||||
if found || err != nil {
|
||||
return s, err
|
||||
}
|
||||
return 0, noFieldError{Field: path}
|
||||
}
|
||||
|
||||
// GetSlice returns value at the given fieldpath.
|
||||
func (fs *UnstructAdapter) GetSlice(path string) ([]interface{}, error) {
|
||||
content, fields, found, err := fs.selectSubtree(path)
|
||||
if !found || err != nil {
|
||||
return nil, noFieldError{Field: path}
|
||||
}
|
||||
|
||||
s, found, err := unstructured.NestedSlice(
|
||||
content, fields...)
|
||||
if found || err != nil {
|
||||
return s, err
|
||||
}
|
||||
return nil, noFieldError{Field: path}
|
||||
}
|
||||
|
||||
// GetStringMap returns value at the given fieldpath.
|
||||
func (fs *UnstructAdapter) GetStringMap(path string) (map[string]string, error) {
|
||||
content, fields, found, err := fs.selectSubtree(path)
|
||||
if !found || err != nil {
|
||||
return nil, noFieldError{Field: path}
|
||||
}
|
||||
|
||||
s, found, err := unstructured.NestedStringMap(
|
||||
content, fields...)
|
||||
if found || err != nil {
|
||||
return s, err
|
||||
}
|
||||
return nil, noFieldError{Field: path}
|
||||
}
|
||||
|
||||
// GetMap returns value at the given fieldpath.
|
||||
func (fs *UnstructAdapter) GetMap(path string) (map[string]interface{}, error) {
|
||||
content, fields, found, err := fs.selectSubtree(path)
|
||||
if !found || err != nil {
|
||||
return nil, noFieldError{Field: path}
|
||||
}
|
||||
|
||||
s, found, err := unstructured.NestedMap(
|
||||
content, fields...)
|
||||
if found || err != nil {
|
||||
return s, err
|
||||
}
|
||||
return nil, noFieldError{Field: path}
|
||||
}
|
||||
|
||||
func (fs *UnstructAdapter) MatchesLabelSelector(selector string) (bool, error) {
|
||||
s, err := labels.Parse(selector)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return s.Matches(labels.Set(fs.GetLabels())), nil
|
||||
}
|
||||
|
||||
func (fs *UnstructAdapter) MatchesAnnotationSelector(selector string) (bool, error) {
|
||||
s, err := labels.Parse(selector)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return s.Matches(labels.Set(fs.GetAnnotations())), nil
|
||||
}
|
||||
|
||||
func (fs *UnstructAdapter) Patch(patch ifc.Kunstructured) error {
|
||||
versionedObj, err := scheme.Scheme.New(
|
||||
toSchemaGvk(patch.GetGvk()))
|
||||
merged := map[string]interface{}{}
|
||||
saveName := fs.GetName()
|
||||
switch {
|
||||
case runtime.IsNotRegisteredError(err):
|
||||
baseBytes, err := json.Marshal(fs.Map())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
patchBytes, err := json.Marshal(patch.Map())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mergedBytes, err := jsonpatch.MergePatch(baseBytes, patchBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(mergedBytes, &merged)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case err != nil:
|
||||
return err
|
||||
default:
|
||||
// Use Strategic-Merge-Patch to handle types w/ schema
|
||||
// TODO: Change this to use the new Merge package.
|
||||
// Store the name of the target object, because this name may have been munged.
|
||||
// Apply this name to the patched object.
|
||||
lookupPatchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
merged, err = strategicpatch.StrategicMergeMapPatchUsingLookupPatchMeta(
|
||||
fs.Map(),
|
||||
patch.Map(),
|
||||
lookupPatchMeta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fs.SetMap(merged)
|
||||
if len(fs.Map()) != 0 {
|
||||
// if the patch deletes the object
|
||||
// don't reset the name
|
||||
fs.SetName(saveName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// toSchemaGvk converts to a schema.GroupVersionKind.
|
||||
func toSchemaGvk(x resid.Gvk) schema.GroupVersionKind {
|
||||
return schema.GroupVersionKind{
|
||||
Group: x.Group,
|
||||
Version: x.Version,
|
||||
Kind: x.Kind,
|
||||
}
|
||||
}
|
||||
|
||||
// noFieldError is returned when a field is expected, but missing.
|
||||
type noFieldError struct {
|
||||
Field string
|
||||
}
|
||||
|
||||
func (e noFieldError) Error() string {
|
||||
return fmt.Sprintf("no field named '%s'", e.Field)
|
||||
}
|
||||
831
api/k8sdeps/kunstruct/unstructadapter_test.go
Normal file
831
api/k8sdeps/kunstruct/unstructadapter_test.go
Normal file
@@ -0,0 +1,831 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kunstruct
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var kunstructured = NewKunstructuredFactoryImpl().FromMap(map[string]interface{}{
|
||||
"Kind": "Service",
|
||||
"metadata": map[string]interface{}{
|
||||
"labels": map[string]interface{}{
|
||||
"app": "application-name",
|
||||
},
|
||||
"name": "service-name",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"ports": map[string]interface{}{
|
||||
"port": int64(80),
|
||||
},
|
||||
},
|
||||
"this": map[string]interface{}{
|
||||
"is": map[string]interface{}{
|
||||
"aNumber": int64(1000),
|
||||
"aFloat": float64(1.001),
|
||||
"aNilValue": nil,
|
||||
"aBool": true,
|
||||
"anEmptyMap": map[string]interface{}{},
|
||||
"anEmptySlice": []interface{}{},
|
||||
"unrecognizable": testing.InternalExample{
|
||||
Name: "fooBar",
|
||||
},
|
||||
},
|
||||
},
|
||||
"that": []interface{}{
|
||||
"idx0",
|
||||
"idx1",
|
||||
"idx2",
|
||||
"idx3",
|
||||
},
|
||||
"those": []interface{}{
|
||||
map[string]interface{}{
|
||||
"field1": "idx0foo",
|
||||
"field2": "idx0bar",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"field1": "idx1foo",
|
||||
"field2": "idx1bar",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"field1": "idx2foo",
|
||||
"field2": "idx2bar",
|
||||
},
|
||||
},
|
||||
"these": []interface{}{
|
||||
map[string]interface{}{
|
||||
"field1": []interface{}{"idx010", "idx011"},
|
||||
"field2": []interface{}{"idx020", "idx021"},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"field1": []interface{}{"idx110", "idx111"},
|
||||
"field2": []interface{}{"idx120", "idx121"},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"field1": []interface{}{"idx210", "idx211"},
|
||||
"field2": []interface{}{"idx220", "idx221"},
|
||||
},
|
||||
},
|
||||
"complextree": []interface{}{
|
||||
map[string]interface{}{
|
||||
"field1": []interface{}{
|
||||
map[string]interface{}{
|
||||
"stringsubfield": "idx1010",
|
||||
"intsubfield": int64(1010),
|
||||
"floatsubfield": float64(1.010),
|
||||
"boolfield": true,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"stringsubfield": "idx1011",
|
||||
"intsubfield": int64(1011),
|
||||
"floatsubfield": float64(1.011),
|
||||
"boolfield": false,
|
||||
},
|
||||
},
|
||||
"field2": []interface{}{
|
||||
map[string]interface{}{
|
||||
"stringsubfield": "idx1020",
|
||||
"intsubfield": int64(1020),
|
||||
"floatsubfield": float64(1.020),
|
||||
"boolfield": true,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"stringsubfield": "idx1021",
|
||||
"intsubfield": int64(1021),
|
||||
"floatsubfield": float64(1.021),
|
||||
"boolfield": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"field1": []interface{}{
|
||||
map[string]interface{}{
|
||||
"stringsubfield": "idx1110",
|
||||
"intsubfield": int64(1110),
|
||||
"floatsubfield": float64(1.110),
|
||||
"boolfield": true,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"stringsubfield": "idx1111",
|
||||
"intsubfield": int64(1111),
|
||||
"floatsubfield": float64(1.111),
|
||||
"boolfield": false,
|
||||
},
|
||||
},
|
||||
"field2": []interface{}{
|
||||
map[string]interface{}{
|
||||
"stringsubfield": "idx1120",
|
||||
"intsubfield": int64(1120),
|
||||
"floatsubfield": float64(1.1120),
|
||||
"boolfield": true,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"stringsubfield": "idx1121",
|
||||
"intsubfield": int64(1121),
|
||||
"floatsubfield": float64(1.1121),
|
||||
"boolfield": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
func TestGetFieldValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pathToField string
|
||||
expectedValue interface{}
|
||||
errorExpected bool
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "oneField",
|
||||
pathToField: "Kind",
|
||||
expectedValue: "Service",
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "twoFields",
|
||||
pathToField: "metadata.name",
|
||||
expectedValue: "service-name",
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "threeFields",
|
||||
pathToField: "spec.ports.port",
|
||||
expectedValue: int64(80),
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
pathToField: "",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named ''",
|
||||
},
|
||||
{
|
||||
name: "emptyDotEmpty",
|
||||
pathToField: ".",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named '.'",
|
||||
},
|
||||
{
|
||||
name: "twoFieldsOneMissing",
|
||||
pathToField: "metadata.banana",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'metadata.banana'",
|
||||
},
|
||||
{
|
||||
name: "deeperMissingField",
|
||||
pathToField: "this.is.aDeep.field.that.does.not.exist",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'this.is.aDeep.field.that.does.not.exist'",
|
||||
},
|
||||
{
|
||||
name: "emptyMap",
|
||||
pathToField: "this.is.anEmptyMap",
|
||||
errorExpected: false,
|
||||
expectedValue: map[string]interface{}{},
|
||||
},
|
||||
{
|
||||
name: "emptySlice",
|
||||
pathToField: "this.is.anEmptySlice",
|
||||
errorExpected: false,
|
||||
expectedValue: []interface{}{},
|
||||
},
|
||||
{
|
||||
name: "numberAsValue",
|
||||
pathToField: "this.is.aNumber",
|
||||
errorExpected: false,
|
||||
expectedValue: int64(1000),
|
||||
},
|
||||
{
|
||||
name: "floatAsValue",
|
||||
pathToField: "this.is.aFloat",
|
||||
errorExpected: false,
|
||||
expectedValue: float64(1.001),
|
||||
},
|
||||
{
|
||||
name: "boolAsValue",
|
||||
pathToField: "this.is.aBool",
|
||||
errorExpected: false,
|
||||
expectedValue: true,
|
||||
},
|
||||
{
|
||||
name: "nilAsValue",
|
||||
pathToField: "this.is.aNilValue",
|
||||
errorExpected: false,
|
||||
expectedValue: nil,
|
||||
},
|
||||
{
|
||||
name: "unrecognizable",
|
||||
pathToField: "this.is.unrecognizable.Name",
|
||||
errorExpected: true,
|
||||
errorMsg: ".this.is.unrecognizable.Name accessor error: {fooBar <nil> false} is of the type testing.InternalExample, expected map[string]interface{}",
|
||||
},
|
||||
{
|
||||
name: "validStringIndex",
|
||||
pathToField: "that[2]",
|
||||
expectedValue: "idx2",
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "outOfBoundIndex",
|
||||
pathToField: "that[99]",
|
||||
errorMsg: "no field named 'that[99]'",
|
||||
errorExpected: true,
|
||||
},
|
||||
{
|
||||
name: "accessorError",
|
||||
pathToField: "that[downwardapi]",
|
||||
errorMsg: ".that.downwardapi accessor error: [idx0 idx1 idx2 idx3] is of the type []interface {}, expected map[string]interface{}",
|
||||
errorExpected: true,
|
||||
},
|
||||
{
|
||||
name: "unknownSlice",
|
||||
pathToField: "unknown[0]",
|
||||
errorMsg: "no field named 'unknown[0]'",
|
||||
errorExpected: true,
|
||||
},
|
||||
{
|
||||
name: "sliceInSlice",
|
||||
pathToField: "that[2][0]",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'that[2][0]'",
|
||||
},
|
||||
{
|
||||
name: "validStructIndex",
|
||||
pathToField: "those[1]",
|
||||
errorExpected: false,
|
||||
expectedValue: map[string]interface{}{"field1": "idx1foo", "field2": "idx1bar"},
|
||||
},
|
||||
{
|
||||
name: "validStructSubField",
|
||||
pathToField: "those[1].field2",
|
||||
errorExpected: false,
|
||||
expectedValue: "idx1bar",
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldIndex",
|
||||
pathToField: "these[1].field2[1]",
|
||||
errorExpected: false,
|
||||
expectedValue: "idx121",
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldOutOfBoundIndex",
|
||||
pathToField: "these[1].field2[99]",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'these[1].field2[99]'",
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldIndexSubfield",
|
||||
pathToField: "complextree[1].field2[1].stringsubfield",
|
||||
errorExpected: false,
|
||||
expectedValue: "idx1121",
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldIndexInvalidName",
|
||||
pathToField: "complextree[1].field2[1].invalidsubfield",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'complextree[1].field2[1].invalidsubfield'",
|
||||
},
|
||||
{
|
||||
name: "validDownwardAPILabels",
|
||||
pathToField: `metadata.labels["app"]`,
|
||||
errorExpected: false,
|
||||
expectedValue: "application-name",
|
||||
},
|
||||
{
|
||||
name: "validDownwardAPISpecs",
|
||||
pathToField: `spec.ports['port']`,
|
||||
errorExpected: false,
|
||||
expectedValue: int64(80),
|
||||
},
|
||||
{
|
||||
name: "validDownwardAPIThis",
|
||||
pathToField: `this.is[aFloat]`,
|
||||
errorExpected: false,
|
||||
expectedValue: float64(1.001),
|
||||
},
|
||||
{
|
||||
name: "downwardAPIInvalidLabel",
|
||||
pathToField: `metadata.labels["theisnotanint"]`,
|
||||
errorExpected: true,
|
||||
errorMsg: `no field named 'metadata.labels["theisnotanint"]'`,
|
||||
},
|
||||
{
|
||||
name: "downwardAPIInvalidLabel2",
|
||||
pathToField: `invalidfield.labels["app"]`,
|
||||
errorExpected: true,
|
||||
errorMsg: `no field named 'invalidfield.labels["app"]'`,
|
||||
},
|
||||
{
|
||||
name: "invalidIndexInIndex",
|
||||
pathToField: "complextree[1[0]]",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'complextree[1[0]]'",
|
||||
},
|
||||
{
|
||||
name: "invalidClosingBrackets",
|
||||
pathToField: "complextree[1]]",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'complextree[1]]'",
|
||||
},
|
||||
{
|
||||
name: "validFieldsWithQuotes",
|
||||
pathToField: "'complextree'[1].field2[1].'stringsubfield'",
|
||||
errorExpected: false,
|
||||
expectedValue: "idx1121",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
s, err := kunstructured.GetFieldValue(test.pathToField)
|
||||
if test.errorExpected {
|
||||
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
unExpectedError(t, test.name, test.pathToField, err)
|
||||
}
|
||||
compareValues(t, test.name, test.pathToField, test.expectedValue, s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pathToField string
|
||||
expectedValue string
|
||||
errorExpected bool
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "oneField",
|
||||
pathToField: "Kind",
|
||||
expectedValue: "Service",
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "twoFields",
|
||||
pathToField: "metadata.name",
|
||||
expectedValue: "service-name",
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "emptyMap",
|
||||
pathToField: "this.is.anEmptyMap",
|
||||
errorExpected: true,
|
||||
errorMsg: ".this.is.anEmptyMap accessor error: map[] is of the type map[string]interface {}, expected string",
|
||||
},
|
||||
{
|
||||
name: "twoFieldsOneMissing",
|
||||
pathToField: "metadata.banana",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'metadata.banana'",
|
||||
},
|
||||
{
|
||||
name: "emptySlice",
|
||||
pathToField: "this.is.anEmptySlice",
|
||||
errorExpected: true,
|
||||
errorMsg: ".this.is.anEmptySlice accessor error: [] is of the type []interface {}, expected string",
|
||||
},
|
||||
{
|
||||
name: "numberAsValue",
|
||||
pathToField: "this.is.aNumber",
|
||||
errorExpected: true,
|
||||
errorMsg: ".this.is.aNumber accessor error: 1000 is of the type int64, expected string",
|
||||
},
|
||||
{
|
||||
name: "nilAsValue",
|
||||
pathToField: "this.is.aNilValue",
|
||||
errorExpected: true,
|
||||
errorMsg: ".this.is.aNilValue accessor error: <nil> is of the type <nil>, expected string",
|
||||
},
|
||||
{
|
||||
name: "validStringIndex",
|
||||
pathToField: "that[2]",
|
||||
expectedValue: "idx2",
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "validStructIndex",
|
||||
pathToField: "those[1]",
|
||||
errorExpected: true,
|
||||
errorMsg: ".[1] accessor error: map[field1:idx1foo field2:idx1bar] is of the type map[string]interface {}, expected string",
|
||||
},
|
||||
{
|
||||
name: "validStructSubField",
|
||||
pathToField: "those[1].field2",
|
||||
errorExpected: false,
|
||||
expectedValue: "idx1bar",
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldIndex",
|
||||
pathToField: "these[1].field2[1]",
|
||||
errorExpected: false,
|
||||
expectedValue: "idx121",
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldIndexSubfield",
|
||||
pathToField: "complextree[1].field2[1].stringsubfield",
|
||||
errorExpected: false,
|
||||
expectedValue: "idx1121",
|
||||
},
|
||||
{
|
||||
name: "invalidIndexInMap",
|
||||
pathToField: "this.is[1]",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'this.is[1]'",
|
||||
},
|
||||
{
|
||||
name: "anotherInvalidIndexInMap",
|
||||
pathToField: "this.is[1].aString",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'this.is[1].aString'",
|
||||
},
|
||||
{
|
||||
name: "validDownwardAPIField",
|
||||
pathToField: `metadata.labels["app"]`,
|
||||
errorExpected: false,
|
||||
expectedValue: "application-name",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
s, err := kunstructured.GetString(test.pathToField)
|
||||
if test.errorExpected {
|
||||
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
unExpectedError(t, test.name, test.pathToField, err)
|
||||
}
|
||||
compareValues(t, test.name, test.pathToField, test.expectedValue, s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetInt64(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pathToField string
|
||||
expectedValue int64
|
||||
errorExpected bool
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "numberAsValue",
|
||||
pathToField: "this.is.aNumber",
|
||||
errorExpected: false,
|
||||
expectedValue: int64(1000),
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldIndexSubfield",
|
||||
pathToField: "complextree[1].field2[1].intsubfield",
|
||||
errorExpected: false,
|
||||
expectedValue: int64(1121),
|
||||
},
|
||||
{
|
||||
name: "twoFieldsOneMissing",
|
||||
pathToField: "metadata.banana",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'metadata.banana'",
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldOutOfBoundIndex",
|
||||
pathToField: "these[1].field2[99]",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'these[1].field2[99]'",
|
||||
},
|
||||
{
|
||||
name: "validDownwardAPISpecs",
|
||||
pathToField: `spec.ports['port']`,
|
||||
errorExpected: false,
|
||||
expectedValue: int64(80),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
s, err := kunstructured.GetInt64(test.pathToField)
|
||||
if test.errorExpected {
|
||||
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
unExpectedError(t, test.name, test.pathToField, err)
|
||||
}
|
||||
compareValues(t, test.name, test.pathToField, test.expectedValue, s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFloat64(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pathToField string
|
||||
expectedValue float64
|
||||
errorExpected bool
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "floatAsValue",
|
||||
pathToField: "this.is.aFloat",
|
||||
errorExpected: false,
|
||||
expectedValue: float64(1.001),
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldIndexSubfield",
|
||||
pathToField: "complextree[1].field2[1].floatsubfield",
|
||||
errorExpected: false,
|
||||
expectedValue: float64(1.1121),
|
||||
},
|
||||
{
|
||||
name: "validDownwardAPIThis",
|
||||
pathToField: `this.is[aFloat]`,
|
||||
errorExpected: false,
|
||||
expectedValue: float64(1.001),
|
||||
},
|
||||
{
|
||||
name: "twoFieldsOneMissing",
|
||||
pathToField: "metadata.banana",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'metadata.banana'",
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldOutOfBoundIndex",
|
||||
pathToField: "these[1].field2[99]",
|
||||
errorExpected: true,
|
||||
errorMsg: "index 99 is out of bounds",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
s, err := kunstructured.GetFloat64(test.pathToField)
|
||||
if test.errorExpected {
|
||||
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
unExpectedError(t, test.name, test.pathToField, err)
|
||||
}
|
||||
compareValues(t, test.name, test.pathToField, test.expectedValue, s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBool(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pathToField string
|
||||
expectedValue bool
|
||||
errorExpected bool
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "boolAsValue",
|
||||
pathToField: "this.is.aBool",
|
||||
errorExpected: false,
|
||||
expectedValue: true,
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldIndexSubfield",
|
||||
pathToField: "complextree[1].field2[1].boolfield",
|
||||
errorExpected: false,
|
||||
expectedValue: false,
|
||||
},
|
||||
{
|
||||
name: "twoFieldsOneMissing",
|
||||
pathToField: "metadata.banana",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'metadata.banana'",
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldOutOfBoundIndex",
|
||||
pathToField: "these[1].field2[99]",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'these[1].field2[99]'",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
s, err := kunstructured.GetBool(test.pathToField)
|
||||
if test.errorExpected {
|
||||
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
unExpectedError(t, test.name, test.pathToField, err)
|
||||
}
|
||||
compareValues(t, test.name, test.pathToField, test.expectedValue, s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStringMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pathToField string
|
||||
errorExpected bool
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "validStringMap",
|
||||
pathToField: "those[2]",
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "twoFieldsOneMissing",
|
||||
pathToField: "metadata.banana",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'metadata.banana'",
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldOutOfBoundIndex",
|
||||
pathToField: "these[1].field2[99]",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'these[1].field2[99]'",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
_, err := kunstructured.GetStringMap(test.pathToField)
|
||||
if test.errorExpected {
|
||||
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
unExpectedError(t, test.name, test.pathToField, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pathToField string
|
||||
errorExpected bool
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "validMap",
|
||||
pathToField: "those[2]",
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldIndexSubfield",
|
||||
pathToField: "complextree[1].field2[1]",
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "twoFieldsOneMissing",
|
||||
pathToField: "metadata.banana",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'metadata.banana'",
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldOutOfBoundIndex",
|
||||
pathToField: "these[1].field2[99]",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'these[1].field2[99]'",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
_, err := kunstructured.GetMap(test.pathToField)
|
||||
if test.errorExpected {
|
||||
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
unExpectedError(t, test.name, test.pathToField, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStringSlice(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pathToField string
|
||||
errorExpected bool
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "validStringSlice",
|
||||
pathToField: "that",
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "twoFieldsOneMissing",
|
||||
pathToField: "metadata.banana",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'metadata.banana'",
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldOutOfBoundIndex",
|
||||
pathToField: "these[1].field2[99]",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'these[1].field2[99]'",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
_, err := kunstructured.GetStringSlice(test.pathToField)
|
||||
if test.errorExpected {
|
||||
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
unExpectedError(t, test.name, test.pathToField, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSlice(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pathToField string
|
||||
errorExpected bool
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "validSlice1",
|
||||
pathToField: "that",
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "validSlice2",
|
||||
pathToField: "those",
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "validSlice3",
|
||||
pathToField: "these",
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "validSlice4",
|
||||
pathToField: "complextree",
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
name: "twoFieldsOneMissing",
|
||||
pathToField: "metadata.banana",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'metadata.banana'",
|
||||
},
|
||||
{
|
||||
name: "validStructSubFieldOutOfBoundIndex",
|
||||
pathToField: "these[1].field2[99]",
|
||||
errorExpected: true,
|
||||
errorMsg: "no field named 'these[1].field2[99]'",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
_, err := kunstructured.GetSlice(test.pathToField)
|
||||
if test.errorExpected {
|
||||
compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
unExpectedError(t, test.name, test.pathToField, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unExpectedError function handles unexpected error
|
||||
func unExpectedError(t *testing.T, name string, pathToField string, err error) {
|
||||
t.Fatalf("%q; path %q - unexpected error %v", name, pathToField, err)
|
||||
}
|
||||
|
||||
// compareExpectedError compares the expectedError and the actualError return by GetFieldValue
|
||||
func compareExpectedError(t *testing.T, name string, pathToField string, err error, errorMsg string) {
|
||||
if err == nil {
|
||||
t.Fatalf("%q; path %q - should return error, but no error returned",
|
||||
name, pathToField)
|
||||
}
|
||||
|
||||
if errorMsg != err.Error() {
|
||||
t.Fatalf("%q; path %q - expected error: \"%s\", got error: \"%v\"",
|
||||
name, pathToField, errorMsg, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// compareValues compares the expectedValue and actualValue returned by GetFieldValue
|
||||
func compareValues(t *testing.T, name string, pathToField string, expectedValue interface{}, actualValue interface{}) {
|
||||
t.Helper()
|
||||
switch typedV := expectedValue.(type) {
|
||||
case nil, string, bool, float64, int, int64:
|
||||
if expectedValue != actualValue {
|
||||
t.Fatalf("%q; Got: %v Expected: %v", name, actualValue, expectedValue)
|
||||
}
|
||||
case map[string]interface{}:
|
||||
if !reflect.DeepEqual(expectedValue, actualValue) {
|
||||
t.Fatalf("%q; Got: %v Expected: %v", name, actualValue, expectedValue)
|
||||
}
|
||||
case []interface{}:
|
||||
if !reflect.DeepEqual(expectedValue, actualValue) {
|
||||
t.Fatalf("%q; Got: %v Expected: %v", name, actualValue, expectedValue)
|
||||
}
|
||||
default:
|
||||
t.Logf("%T value at `%s`", typedV, pathToField)
|
||||
}
|
||||
}
|
||||
25
api/k8sdeps/transformer/factory.go
Normal file
25
api/k8sdeps/transformer/factory.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package transformer provides transformer factory
|
||||
package transformer
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/v3/api/k8sdeps/transformer/patch"
|
||||
"sigs.k8s.io/kustomize/v3/api/resmap"
|
||||
"sigs.k8s.io/kustomize/v3/api/resource"
|
||||
)
|
||||
|
||||
// FactoryImpl makes patch transformer and name hash transformer
|
||||
type FactoryImpl struct{}
|
||||
|
||||
// NewFactoryImpl makes a new factoryImpl instance
|
||||
func NewFactoryImpl() *FactoryImpl {
|
||||
return &FactoryImpl{}
|
||||
}
|
||||
|
||||
func (p *FactoryImpl) MergePatches(patches []*resource.Resource,
|
||||
rf *resource.Factory) (
|
||||
resmap.ResMap, error) {
|
||||
return patch.MergePatches(patches, rf)
|
||||
}
|
||||
221
api/k8sdeps/transformer/patch/conflictdetector.go
Normal file
221
api/k8sdeps/transformer/patch/conflictdetector.go
Normal file
@@ -0,0 +1,221 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package patch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/mergepatch"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"sigs.k8s.io/kustomize/v3/api/resid"
|
||||
"sigs.k8s.io/kustomize/v3/api/resmap"
|
||||
"sigs.k8s.io/kustomize/v3/api/resource"
|
||||
)
|
||||
|
||||
type conflictDetector interface {
|
||||
hasConflict(patch1, patch2 *resource.Resource) (bool, error)
|
||||
findConflict(conflictingPatchIdx int, patches []*resource.Resource) (*resource.Resource, error)
|
||||
mergePatches(patch1, patch2 *resource.Resource) (*resource.Resource, error)
|
||||
}
|
||||
|
||||
type jsonMergePatch struct {
|
||||
rf *resource.Factory
|
||||
}
|
||||
|
||||
var _ conflictDetector = &jsonMergePatch{}
|
||||
|
||||
func newJMPConflictDetector(rf *resource.Factory) conflictDetector {
|
||||
return &jsonMergePatch{rf: rf}
|
||||
}
|
||||
|
||||
func (jmp *jsonMergePatch) hasConflict(
|
||||
patch1, patch2 *resource.Resource) (bool, error) {
|
||||
return mergepatch.HasConflicts(patch1.Map(), patch2.Map())
|
||||
}
|
||||
|
||||
func (jmp *jsonMergePatch) findConflict(
|
||||
conflictingPatchIdx int, patches []*resource.Resource) (*resource.Resource, error) {
|
||||
for i, patch := range patches {
|
||||
if i == conflictingPatchIdx {
|
||||
continue
|
||||
}
|
||||
if !patches[conflictingPatchIdx].OrgId().Equals(patch.OrgId()) {
|
||||
continue
|
||||
}
|
||||
conflict, err := mergepatch.HasConflicts(
|
||||
patch.Map(),
|
||||
patches[conflictingPatchIdx].Map())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if conflict {
|
||||
return patch, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (jmp *jsonMergePatch) mergePatches(
|
||||
patch1, patch2 *resource.Resource) (*resource.Resource, error) {
|
||||
baseBytes, err := json.Marshal(patch1.Map())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patchBytes, err := json.Marshal(patch2.Map())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mergedBytes, err := jsonpatch.MergeMergePatches(baseBytes, patchBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mergedMap := make(map[string]interface{})
|
||||
err = json.Unmarshal(mergedBytes, &mergedMap)
|
||||
return jmp.rf.FromMap(mergedMap), err
|
||||
}
|
||||
|
||||
type strategicMergePatch struct {
|
||||
lookupPatchMeta strategicpatch.LookupPatchMeta
|
||||
rf *resource.Factory
|
||||
}
|
||||
|
||||
var _ conflictDetector = &strategicMergePatch{}
|
||||
|
||||
func newSMPConflictDetector(
|
||||
versionedObj runtime.Object,
|
||||
rf *resource.Factory) (conflictDetector, error) {
|
||||
lookupPatchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObj)
|
||||
return &strategicMergePatch{lookupPatchMeta: lookupPatchMeta, rf: rf}, err
|
||||
}
|
||||
|
||||
func (smp *strategicMergePatch) hasConflict(p1, p2 *resource.Resource) (bool, error) {
|
||||
return strategicpatch.MergingMapsHaveConflicts(
|
||||
p1.Map(), p2.Map(), smp.lookupPatchMeta)
|
||||
}
|
||||
|
||||
func (smp *strategicMergePatch) findConflict(
|
||||
conflictingPatchIdx int, patches []*resource.Resource) (*resource.Resource, error) {
|
||||
for i, patch := range patches {
|
||||
if i == conflictingPatchIdx {
|
||||
continue
|
||||
}
|
||||
if !patches[conflictingPatchIdx].OrgId().Equals(patch.OrgId()) {
|
||||
continue
|
||||
}
|
||||
conflict, err := strategicpatch.MergingMapsHaveConflicts(
|
||||
patch.Map(),
|
||||
patches[conflictingPatchIdx].Map(),
|
||||
smp.lookupPatchMeta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if conflict {
|
||||
return patch, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (smp *strategicMergePatch) mergePatches(patch1, patch2 *resource.Resource) (*resource.Resource, error) {
|
||||
if hasDeleteDirectiveMarker(patch2.Map()) {
|
||||
if hasDeleteDirectiveMarker(patch1.Map()) {
|
||||
return nil, fmt.Errorf("cannot merge patches both containing '$patch: delete' directives")
|
||||
}
|
||||
patch1, patch2 = patch2, patch1
|
||||
}
|
||||
mergeJSONMap, err := strategicpatch.MergeStrategicMergeMapPatchUsingLookupPatchMeta(
|
||||
smp.lookupPatchMeta, patch1.Map(), patch2.Map())
|
||||
return smp.rf.FromMap(mergeJSONMap), err
|
||||
}
|
||||
|
||||
// MergePatches merge and index patches by OrgId.
|
||||
// It errors out if there is conflict between patches.
|
||||
func MergePatches(patches []*resource.Resource,
|
||||
rf *resource.Factory) (resmap.ResMap, error) {
|
||||
rc := resmap.New()
|
||||
for ix, patch := range patches {
|
||||
id := patch.OrgId()
|
||||
existing := rc.GetMatchingResourcesByOriginalId(id.Equals)
|
||||
if len(existing) == 0 {
|
||||
rc.Append(patch)
|
||||
continue
|
||||
}
|
||||
if len(existing) > 1 {
|
||||
return nil, fmt.Errorf("self conflict in patches")
|
||||
}
|
||||
|
||||
versionedObj, err := scheme.Scheme.New(toSchemaGvk(id.Gvk))
|
||||
if err != nil && !runtime.IsNotRegisteredError(err) {
|
||||
return nil, err
|
||||
}
|
||||
var cd conflictDetector
|
||||
if err != nil {
|
||||
cd = newJMPConflictDetector(rf)
|
||||
} else {
|
||||
cd, err = newSMPConflictDetector(versionedObj, rf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
conflict, err := cd.hasConflict(existing[0], patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if conflict {
|
||||
conflictingPatch, err := cd.findConflict(ix, patches)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf(
|
||||
"conflict between %#v and %#v",
|
||||
conflictingPatch.Map(), patch.Map())
|
||||
}
|
||||
merged, err := cd.mergePatches(existing[0], patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rc.Replace(merged)
|
||||
}
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// toSchemaGvk converts to a schema.GroupVersionKind.
|
||||
func toSchemaGvk(x resid.Gvk) schema.GroupVersionKind {
|
||||
return schema.GroupVersionKind{
|
||||
Group: x.Group,
|
||||
Version: x.Version,
|
||||
Kind: x.Kind,
|
||||
}
|
||||
}
|
||||
|
||||
func hasDeleteDirectiveMarker(patch map[string]interface{}) bool {
|
||||
if v, ok := patch["$patch"]; ok && v == "delete" {
|
||||
return true
|
||||
}
|
||||
for _, v := range patch {
|
||||
switch typedV := v.(type) {
|
||||
case map[string]interface{}:
|
||||
if hasDeleteDirectiveMarker(typedV) {
|
||||
return true
|
||||
}
|
||||
case []interface{}:
|
||||
for _, sv := range typedV {
|
||||
typedE, ok := sv.(map[string]interface{})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if hasDeleteDirectiveMarker(typedE) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
102
api/k8sdeps/validator/validators.go
Normal file
102
api/k8sdeps/validator/validators.go
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package validator provides functions to validate labels, annotations,
|
||||
// namespaces and configmap/secret keys using apimachinery functions.
|
||||
package validator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
apivalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
v1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// KustValidator validates Labels and annotations by apimachinery
|
||||
type KustValidator struct{}
|
||||
|
||||
// NewKustValidator returns a KustValidator object
|
||||
func NewKustValidator() *KustValidator {
|
||||
return &KustValidator{}
|
||||
}
|
||||
|
||||
func (v *KustValidator) ErrIfInvalidKey(k string) error {
|
||||
if errs := validation.IsConfigMapKey(k); len(errs) != 0 {
|
||||
return fmt.Errorf(
|
||||
"%q is not a valid key name: %s",
|
||||
k, strings.Join(errs, ";"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *KustValidator) IsEnvVarName(k string) error {
|
||||
if errs := validation.IsEnvVarName(k); len(errs) != 0 {
|
||||
return fmt.Errorf(
|
||||
"%q is not a valid key name: %s",
|
||||
k, strings.Join(errs, ";"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MakeAnnotationValidator returns a MapValidatorFunc using apimachinery.
|
||||
func (v *KustValidator) MakeAnnotationValidator() func(map[string]string) error {
|
||||
return func(x map[string]string) error {
|
||||
errs := apivalidation.ValidateAnnotations(x, field.NewPath("field"))
|
||||
if len(errs) > 0 {
|
||||
return errors.New(errs.ToAggregate().Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MakeAnnotationNameValidator returns a MapValidatorFunc using apimachinery.
|
||||
func (v *KustValidator) MakeAnnotationNameValidator() func([]string) error {
|
||||
return func(x []string) error {
|
||||
errs := field.ErrorList{}
|
||||
fldPath := field.NewPath("field")
|
||||
for _, k := range x {
|
||||
for _, msg := range validation.IsQualifiedName(strings.ToLower(k)) {
|
||||
errs = append(errs, field.Invalid(fldPath, k, msg))
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.New(errs.ToAggregate().Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MakeLabelValidator returns a MapValidatorFunc using apimachinery.
|
||||
func (v *KustValidator) MakeLabelValidator() func(map[string]string) error {
|
||||
return func(x map[string]string) error {
|
||||
errs := v1validation.ValidateLabels(x, field.NewPath("field"))
|
||||
if len(errs) > 0 {
|
||||
return errors.New(errs.ToAggregate().Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MakeLabelNameValidator returns a ArrayValidatorFunc using apimachinery.
|
||||
func (v *KustValidator) MakeLabelNameValidator() func([]string) error {
|
||||
return func(x []string) error {
|
||||
errs := field.ErrorList{}
|
||||
fldPath := field.NewPath("field")
|
||||
for _, k := range x {
|
||||
errs = append(errs, v1validation.ValidateLabelName(k, fldPath)...)
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.New(errs.ToAggregate().Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateNamespace validates a string is a valid namespace using apimachinery.
|
||||
func (v *KustValidator) ValidateNamespace(s string) []string {
|
||||
return validation.IsDNS1123Label(s)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/internal/loadertest"
|
||||
"sigs.k8s.io/kustomize/v3/api/k8sdeps/kunstruct"
|
||||
"sigs.k8s.io/kustomize/v3/api/plugins/config"
|
||||
. "sigs.k8s.io/kustomize/v3/api/plugins/execplugin"
|
||||
"sigs.k8s.io/kustomize/v3/api/plugins/loader"
|
||||
@@ -16,7 +17,6 @@ import (
|
||||
"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/k8sdeps/kunstruct"
|
||||
)
|
||||
|
||||
func TestExecPluginConfig(t *testing.T) {
|
||||
|
||||
@@ -10,17 +10,15 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/plugins/builtins"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"sigs.k8s.io/kustomize/v3/api/ifc"
|
||||
"sigs.k8s.io/kustomize/v3/api/plugins/builtins"
|
||||
"sigs.k8s.io/kustomize/v3/api/plugins/config"
|
||||
"sigs.k8s.io/kustomize/v3/api/plugins/execplugin"
|
||||
"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/types"
|
||||
"sigs.k8s.io/kustomize/v3/pkg/plugins"
|
||||
)
|
||||
|
||||
type Loader struct {
|
||||
@@ -176,11 +174,11 @@ func (l *Loader) loadGoPlugin(id resid.ResId) (resmap.Configurable, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "plugin %s fails to load", absPath)
|
||||
}
|
||||
symbol, err := p.Lookup(plugins.PluginSymbol)
|
||||
symbol, err := p.Lookup(config.PluginSymbol)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(
|
||||
err, "plugin %s doesn't have symbol %s",
|
||||
regId, plugins.PluginSymbol)
|
||||
regId, config.PluginSymbol)
|
||||
}
|
||||
c, ok := symbol.(resmap.Configurable)
|
||||
if !ok {
|
||||
|
||||
@@ -7,13 +7,13 @@ import (
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/internal/loadertest"
|
||||
"sigs.k8s.io/kustomize/v3/api/k8sdeps/kunstruct"
|
||||
"sigs.k8s.io/kustomize/v3/api/plugins/config"
|
||||
. "sigs.k8s.io/kustomize/v3/api/plugins/loader"
|
||||
"sigs.k8s.io/kustomize/v3/api/resmap"
|
||||
"sigs.k8s.io/kustomize/v3/api/resource"
|
||||
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
|
||||
"sigs.k8s.io/kustomize/v3/api/testutils/valtest"
|
||||
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/k8sdeps/kunstruct"
|
||||
"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(
|
||||
|
||||
@@ -20,10 +20,10 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/k8sdeps/kunstruct"
|
||||
"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(
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/filesys"
|
||||
"sigs.k8s.io/kustomize/v3/api/k8sdeps/kunstruct"
|
||||
"sigs.k8s.io/kustomize/v3/api/k8sdeps/transformer"
|
||||
fLdr "sigs.k8s.io/kustomize/v3/api/loader"
|
||||
"sigs.k8s.io/kustomize/v3/api/plugins/config"
|
||||
pLdr "sigs.k8s.io/kustomize/v3/api/plugins/loader"
|
||||
@@ -18,8 +20,6 @@ import (
|
||||
"sigs.k8s.io/kustomize/v3/api/target"
|
||||
"sigs.k8s.io/kustomize/v3/api/testutils/kusttest"
|
||||
"sigs.k8s.io/kustomize/v3/api/testutils/valtest"
|
||||
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
|
||||
"sigs.k8s.io/kustomize/v3/k8sdeps/transformer"
|
||||
)
|
||||
|
||||
func TestPluginDir(t *testing.T) {
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/builtinconfig/consts"
|
||||
"sigs.k8s.io/kustomize/v3/api/internal/loadertest"
|
||||
"sigs.k8s.io/kustomize/v3/api/k8sdeps/kunstruct"
|
||||
"sigs.k8s.io/kustomize/v3/api/k8sdeps/transformer"
|
||||
fLdr "sigs.k8s.io/kustomize/v3/api/loader"
|
||||
"sigs.k8s.io/kustomize/v3/api/pgmconfig"
|
||||
"sigs.k8s.io/kustomize/v3/api/plugins/config"
|
||||
@@ -20,8 +22,6 @@ import (
|
||||
"sigs.k8s.io/kustomize/v3/api/target"
|
||||
"sigs.k8s.io/kustomize/v3/api/testutils/valtest"
|
||||
"sigs.k8s.io/kustomize/v3/api/types"
|
||||
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
|
||||
"sigs.k8s.io/kustomize/v3/k8sdeps/transformer"
|
||||
)
|
||||
|
||||
// KustTestHarness is an environment for running a kustomize build,
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/builtinconfig"
|
||||
"sigs.k8s.io/kustomize/v3/api/k8sdeps/kunstruct"
|
||||
"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"
|
||||
)
|
||||
|
||||
var resourceFactory = resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl())
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kustomize/v3/api/ifc"
|
||||
"sigs.k8s.io/kustomize/v3/api/k8sdeps/kunstruct"
|
||||
. "sigs.k8s.io/kustomize/v3/api/transform"
|
||||
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
|
||||
)
|
||||
|
||||
type noopMutator struct {
|
||||
|
||||
Reference in New Issue
Block a user