Merge branch 'master' into transformer-no-create-arrays

This commit is contained in:
Tom Wieczorek
2019-05-28 11:27:04 +02:00
1136 changed files with 20658 additions and 457722 deletions

View File

@@ -1,42 +0,0 @@
/*
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 config
// Annotation is to mark a field as annotations.
// "x-kubernetes-annotation": ""
const Annotation = "x-kubernetes-annotation"
// LabelSelector is to mark a field as LabelSelector
// "x-kubernetes-label-selector": ""
const LabelSelector = "x-kubernetes-label-selector"
// Identity is to mark a field as Identity
// "x-kubernetes-identity": ""
const Identity = "x-kubernetes-identity"
// Version marks the type version of an object ref field
// "x-kubernetes-object-ref-api-version": <apiVersion name>
const Version = "x-kubernetes-object-ref-api-version"
// Kind marks the type name of an object ref field
// "x-kubernetes-object-ref-kind": <kind name>
const Kind = "x-kubernetes-object-ref-kind"
// NameKey marks the field key that refers to an object of an object ref field
// "x-kubernetes-object-ref-name-key": "name"
// default is "name"
const NameKey = "x-kubernetes-object-ref-name-key"

View File

@@ -130,6 +130,11 @@ commonLabels:
group: batch
kind: CronJob
- path: spec/jobTemplate/metadata/labels
create: true
group: batch
kind: CronJob
- path: spec/jobTemplate/spec/template/metadata/labels
create: true
group: batch

View File

@@ -31,6 +31,7 @@ func GetDefaultFieldSpecs() []byte {
[]byte(namespaceFieldSpecs),
[]byte(varReferenceFieldSpecs),
[]byte(nameReferenceFieldSpecs),
[]byte(imagesFieldSpecs),
}
return bytes.Join(configData, []byte("\n"))
}
@@ -45,5 +46,6 @@ func GetDefaultFieldSpecsAsMap() map[string]string {
result["namespace"] = namespaceFieldSpecs
result["varreference"] = varReferenceFieldSpecs
result["namereference"] = nameReferenceFieldSpecs
result["images"] = imagesFieldSpecs
return result
}

View File

@@ -0,0 +1,23 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package defaultconfig
const (
// imageFieldSpecs is left empty since `containers` and `initContainers`
// of *ANY* kind in *ANY* path are builtin supported in code
imagesFieldSpecs = ``
)

View File

@@ -108,13 +108,15 @@ nameReference:
kind: Job
- path: spec/jobTemplate/spec/template/spec/volumes/configMap/name
kind: CronJob
- path: spec/jobTemplate/spec/template/spec/volumes/projected/sources/configMap/name
kind: CronJob
- path: spec/jobTemplate/spec/template/spec/containers/env/valueFrom/configMapKeyRef/name
kind: CronJob
- path: spec/jobTemplate/spec/template/spec/initContainers/env/valueFrom/configMapKeyRef/name
kind: CronJob
- path: spec/jobTemplate/spec/template/spec/containers/envFrom/configMapRef/name
kind: CronJob
- path: spec/jobTemplate/spec/template/spec/initContainers/envFrom/configmapRef/name
- path: spec/jobTemplate/spec/template/spec/initContainers/envFrom/configMapRef/name
kind: CronJob
- kind: Secret
@@ -150,6 +152,8 @@ nameReference:
kind: Deployment
- path: spec/template/spec/imagePullSecrets/name
kind: Deployment
- path: spec/template/spec/volumes/projected/sources/secret/name
kind: Deployment
- path: spec/template/spec/volumes/secret/secretName
kind: ReplicaSet
- path: spec/template/spec/containers/env/valueFrom/secretKeyRef/name
@@ -186,6 +190,8 @@ nameReference:
kind: StatefulSet
- path: spec/template/spec/imagePullSecrets/name
kind: StatefulSet
- path: spec/template/spec/volumes/projected/sources/secret/name
kind: StatefulSet
- path: spec/template/spec/volumes/secret/secretName
kind: Job
- path: spec/template/spec/containers/env/valueFrom/secretKeyRef/name
@@ -200,6 +206,8 @@ nameReference:
kind: Job
- path: spec/jobTemplate/spec/template/spec/volumes/secret/secretName
kind: CronJob
- path: spec/jobTemplate/spec/template/spec/volumes/projected/sources/secret/name
kind: CronJob
- path: spec/jobTemplate/spec/template/spec/containers/env/valueFrom/secretKeyRef/name
kind: CronJob
- path: spec/jobTemplate/spec/template/spec/initContainers/env/valueFrom/secretKeyRef/name
@@ -241,6 +249,9 @@ nameReference:
kind: Ingress
- path: spec/backend/serviceName
kind: Ingress
- path: spec/service/name
kind: APIService
group: apiregistration.k8s.io
- kind: Role
group: rbac.authorization.k8s.io
@@ -279,7 +290,7 @@ nameReference:
- path: spec/jobTemplate/spec/template/spec/serviceAccountName
kind: CronJob
- path: spec/template/spec/serviceAccountName
kind: job
kind: Job
- path: spec/template/spec/serviceAccountName
kind: DaemonSet
@@ -306,5 +317,14 @@ nameReference:
fieldSpecs:
- path: spec/volumeName
kind: PersistentVolumeClaim
- kind: StorageClass
version: v1
group: storage.k8s.io
fieldSpecs:
- path: spec/storageClassName
kind: PersistentVolume
- path: spec/storageClassName
kind: PersistentVolumeClaim
`
)

View File

@@ -19,73 +19,183 @@ package defaultconfig
const (
varReferenceFieldSpecs = `
varReference:
- path: spec/template/spec/initContainers/command
kind: StatefulSet
- path: spec/template/spec/containers/command
kind: StatefulSet
- path: spec/template/spec/initContainers/command
kind: Deployment
- path: spec/template/spec/containers/command
kind: Deployment
- path: spec/template/spec/containers/command
kind: Job
- path: spec/jobTemplate/spec/template/spec/containers/args
kind: CronJob
- path: spec/jobTemplate/spec/template/spec/containers/command
kind: CronJob
- path: spec/template/spec/initContainers/args
kind: StatefulSet
- path: spec/template/spec/containers/args
kind: StatefulSet
- path: spec/template/spec/initContainers/args
kind: Deployment
- path: spec/template/spec/containers/args
kind: Deployment
- path: spec/template/spec/containers/args
kind: Job
- path: spec/jobTemplate/spec/template/spec/containers/args
kind: CronJob
- path: spec/template/spec/initContainers/env/value
kind: StatefulSet
- path: spec/template/spec/containers/env/value
kind: StatefulSet
- path: spec/template/spec/initContainers/env/value
kind: Deployment
- path: spec/template/spec/containers/env/value
kind: Deployment
- path: spec/template/spec/containers/env/value
kind: Job
- path: spec/jobTemplate/spec/template/spec/containers/env/value
kind: CronJob
- path: spec/containers/command
kind: Pod
- path: spec/jobTemplate/spec/template/spec/containers/volumeMounts/mountPath
kind: CronJob
- path: spec/containers/args
kind: Pod
- path: spec/jobTemplate/spec/template/spec/initContainers/args
kind: CronJob
- path: spec/containers/env/value
kind: Pod
- path: spec/jobTemplate/spec/template/spec/initContainers/command
kind: CronJob
- path: spec/jobTemplate/spec/template/spec/initContainers/env/value
kind: CronJob
- path: spec/jobTemplate/spec/template/spec/initContainers/volumeMounts/mountPath
kind: CronJob
- path: spec/template/spec/containers/args
kind: DaemonSet
- path: spec/template/spec/containers/command
kind: DaemonSet
- path: spec/template/spec/containers/env/value
kind: DaemonSet
- path: spec/template/spec/containers/volumeMounts/mountPath
kind: DaemonSet
- path: spec/template/spec/initContainers/args
kind: DaemonSet
- path: spec/template/spec/initContainers/command
kind: DaemonSet
- path: spec/template/spec/initContainers/env/value
kind: DaemonSet
- path: spec/template/spec/initContainers/volumeMounts/mountPath
kind: DaemonSet
- path: spec/template/spec/containers/args
kind: Deployment
- path: spec/template/spec/containers/command
kind: Deployment
- path: spec/template/spec/containers/env/value
kind: Deployment
- path: spec/template/spec/containers/volumeMounts/mountPath
kind: Deployment
- path: spec/template/spec/initContainers/args
kind: Deployment
- path: spec/template/spec/initContainers/command
kind: Deployment
- path: spec/template/spec/initContainers/env/value
kind: Deployment
- path: spec/template/spec/initContainers/volumeMounts/mountPath
kind: Deployment
- path: spec/rules/host
kind: Ingress
- path: spec/tls/hosts
kind: Ingress
- path: spec/tls/secretName
kind: Ingress
- path: spec/template/spec/containers/args
kind: Job
- path: spec/template/spec/containers/command
kind: Job
- path: spec/template/spec/containers/env/value
kind: Job
- path: spec/template/spec/containers/volumeMounts/mountPath
kind: Job
- path: spec/template/spec/initContainers/args
kind: Job
- path: spec/template/spec/initContainers/command
kind: Job
- path: spec/template/spec/initContainers/env/value
kind: Job
- path: spec/template/spec/initContainers/volumeMounts/mountPath
kind: Job
- path: spec/containers/args
kind: Pod
- path: spec/containers/command
kind: Pod
- path: spec/containers/env/value
kind: Pod
- path: spec/containers/volumeMounts/mountPath
kind: Pod
- path: spec/initContainers/args
kind: Pod
- path: spec/initContainers/command
kind: Pod
- path: spec/initContainers/env/value
kind: Pod
- path: spec/initContainers/volumeMounts/mountPath
kind: Pod
- path: spec/template/spec/containers/args
kind: ReplicaSet
- path: spec/template/spec/containers/command
kind: ReplicaSet
- path: spec/template/spec/containers/env/value
kind: ReplicaSet
- path: spec/template/spec/containers/volumeMounts/mountPath
kind: ReplicaSet
- path: spec/template/spec/initContainers/args
kind: ReplicaSet
- path: spec/template/spec/initContainers/command
kind: ReplicaSet
- path: spec/template/spec/initContainers/env/value
kind: ReplicaSet
- path: spec/template/spec/initContainers/volumeMounts/mountPath
kind: ReplicaSet
- path: spec/template/spec/containers/args
kind: StatefulSet
- path: spec/template/spec/containers/command
kind: StatefulSet
- path: spec/template/spec/containers/env/value
kind: StatefulSet
- path: spec/template/spec/containers/volumeMounts/mountPath
kind: StatefulSet
- path: spec/template/spec/initContainers/args
kind: StatefulSet
- path: spec/template/spec/initContainers/command
kind: StatefulSet
- path: spec/template/spec/initContainers/env/value
kind: StatefulSet
- path: spec/template/spec/initContainers/volumeMounts/mountPath
kind: StatefulSet
- path: metadata/labels
`
)

View File

@@ -19,9 +19,8 @@ package config
import (
"log"
"github.com/ghodss/yaml"
"sigs.k8s.io/kustomize/pkg/ifc"
"sigs.k8s.io/kustomize/pkg/transformers/config/defaultconfig"
"sigs.k8s.io/yaml"
)
// Factory makes instances of TransformerConfig.
@@ -29,6 +28,21 @@ type Factory struct {
ldr ifc.Loader
}
// MakeTransformerConfig returns a merger of custom config,
// if any, with default config.
func MakeTransformerConfig(
ldr ifc.Loader, paths []string) (*TransformerConfig, error) {
t1 := MakeDefaultConfig()
if len(paths) == 0 {
return t1, nil
}
t2, err := NewFactory(ldr).FromFiles(paths)
if err != nil {
return nil, err
}
return t1.Merge(t2)
}
func NewFactory(l ifc.Loader) *Factory {
return &Factory{ldr: l}
}
@@ -53,7 +67,10 @@ func (tf *Factory) FromFiles(
if err != nil {
return nil, err
}
result = result.Merge(t)
result, err = result.Merge(t)
if err != nil {
return nil, err
}
}
return result, nil
}
@@ -68,19 +85,3 @@ func makeTransformerConfigFromBytes(data []byte) (*TransformerConfig, error) {
t.sortFields()
return &t, nil
}
// EmptyConfig returns an empty TransformerConfig object
func (tf *Factory) EmptyConfig() *TransformerConfig {
return &TransformerConfig{}
}
// DefaultConfig returns a default TransformerConfig.
// This should never fail, hence the Fatal panic.
func (tf *Factory) DefaultConfig() *TransformerConfig {
c, err := makeTransformerConfigFromBytes(
defaultconfig.GetDefaultFieldSpecs())
if err != nil {
log.Fatalf("Unable to make default transformconfig: %v", err)
}
return c
}

View File

@@ -1,45 +1,33 @@
/*
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.
*/
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package config
import (
"reflect"
"sigs.k8s.io/kustomize/pkg/fs"
"sigs.k8s.io/kustomize/pkg/gvk"
"sigs.k8s.io/kustomize/pkg/ifc"
"sigs.k8s.io/kustomize/pkg/loader"
"testing"
"sigs.k8s.io/kustomize/internal/loadertest"
"sigs.k8s.io/kustomize/pkg/gvk"
)
func TestMakeDefaultTransformerConfig(t *testing.T) {
func TestMakeDefaultConfig(t *testing.T) {
// Confirm default can be made without fatal error inside call.
l, _, _ := makeFakeLoaderAndOutput()
_ = NewFactory(l).DefaultConfig()
_ = MakeDefaultConfig()
}
func makeFakeLoaderAndOutput() (ifc.Loader, *TransformerConfig, *TransformerConfig) {
transformerConfig := `
func TestFromFiles(t *testing.T) {
ldr := loadertest.NewFakeLoader("/app")
ldr.AddFile("/app/config.yaml", []byte(`
namePrefix:
- path: nameprefix/path
kind: SomeKind
`
fakeFS := fs.MakeFakeFS()
fakeFS.WriteFile("/transformerconfig/test/config.yaml", []byte(transformerConfig))
ldr := loader.NewFileLoaderAtRoot(fakeFS)
`))
tcfg, err := NewFactory(ldr).FromFiles([]string{"/app/config.yaml"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := &TransformerConfig{
NamePrefix: []FieldSpec{
{
@@ -48,16 +36,6 @@ namePrefix:
},
},
}
return ldr, expected, NewFactory(ldr).DefaultConfig()
}
func TestMakeTransformerConfigFromFiles(t *testing.T) {
ldr, expected, _ := makeFakeLoaderAndOutput()
tcfg, err := NewFactory(ldr).FromFiles([]string{"transformerconfig/test/config.yaml"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(tcfg, expected) {
t.Fatalf("expected %v\n but go6t %v\n", expected, tcfg)
}

View File

@@ -20,140 +20,182 @@ import (
"encoding/json"
"strings"
"github.com/ghodss/yaml"
"github.com/go-openapi/spec"
"github.com/pkg/errors"
"k8s.io/kube-openapi/pkg/common"
"sigs.k8s.io/kustomize/pkg/gvk"
"sigs.k8s.io/kustomize/pkg/ifc"
"sigs.k8s.io/yaml"
)
// LoadCRDs parse CRD schemas from paths into a TransformerConfig
func (tf *Factory) LoadCRDs(paths []string) (*TransformerConfig, error) {
tc := tf.EmptyConfig()
type myProperties map[string]spec.Schema
type nameToApiMap map[string]common.OpenAPIDefinition
// LoadConfigFromCRDs parse CRD schemas from paths into a TransformerConfig
func LoadConfigFromCRDs(
ldr ifc.Loader, paths []string) (*TransformerConfig, error) {
tc := MakeEmptyConfig()
for _, path := range paths {
otherTc, err := tf.loadCRD(path)
content, err := ldr.Load(path)
if err != nil {
return nil, err
}
m, err := makeNameToApiMap(content)
if err != nil {
return nil, errors.Wrapf(err, "unable to parse open API definition from '%s'", path)
}
otherTc, err := makeConfigFromApiMap(m)
if err != nil {
return nil, err
}
tc, err = tc.Merge(otherTc)
if err != nil {
return nil, err
}
tc = tc.Merge(otherTc)
}
return tc, nil
}
func (tf *Factory) loadCRD(path string) (*TransformerConfig, error) {
result := tf.EmptyConfig()
content, err := tf.loader().Load(path)
if err != nil {
return result, err
}
var types map[string]common.OpenAPIDefinition
func makeNameToApiMap(content []byte) (result nameToApiMap, err error) {
if content[0] == '{' {
err = json.Unmarshal(content, &types)
err = json.Unmarshal(content, &result)
} else {
err = yaml.Unmarshal(content, &types)
}
if err != nil {
return nil, err
err = yaml.Unmarshal(content, &result)
}
return
}
crds := getCRDs(types)
for crd, k := range crds {
tc := tf.EmptyConfig()
err = loadCrdIntoConfig(
types, crd, crd, k, []string{}, tc)
func makeConfigFromApiMap(m nameToApiMap) (*TransformerConfig, error) {
result := MakeEmptyConfig()
for name, api := range m {
if !looksLikeAk8sType(api.Schema.SchemaProps.Properties) {
continue
}
tc := MakeEmptyConfig()
err := loadCrdIntoConfig(
tc, makeGvkFromTypeName(name), m, name, []string{})
if err != nil {
return result, err
}
result, err = result.Merge(tc)
if err != nil {
return result, err
}
result = result.Merge(tc)
}
return result, nil
}
// getCRDs get all CRD types
func getCRDs(types map[string]common.OpenAPIDefinition) map[string]gvk.Gvk {
crds := map[string]gvk.Gvk{}
for typename, t := range types {
properties := t.Schema.SchemaProps.Properties
_, foundKind := properties["kind"]
_, foundAPIVersion := properties["apiVersion"]
_, foundMetadata := properties["metadata"]
if foundKind && foundAPIVersion && foundMetadata {
// TODO: Get Group and Version for CRD from the openAPI definition once
// "x-kubernetes-group-version-kind" is available in CRD
kind := strings.Split(typename, ".")[len(strings.Split(typename, "."))-1]
crds[typename] = gvk.Gvk{Kind: kind}
}
}
return crds
// TODO: Get Group and Version for CRD from the
// openAPI definition once
// "x-kubernetes-group-version-kind" is available in CRD
func makeGvkFromTypeName(n string) gvk.Gvk {
names := strings.Split(n, ".")
kind := names[len(names)-1]
return gvk.Gvk{Kind: kind}
}
func looksLikeAk8sType(properties myProperties) bool {
_, ok := properties["kind"]
if !ok {
return false
}
_, ok = properties["apiVersion"]
if !ok {
return false
}
_, ok = properties["metadata"]
if !ok {
return false
}
return true
}
const (
// "x-kubernetes-annotation": ""
xAnnotation = "x-kubernetes-annotation"
// "x-kubernetes-label-selector": ""
xLabelSelector = "x-kubernetes-label-selector"
// "x-kubernetes-identity": ""
xIdentity = "x-kubernetes-identity"
// "x-kubernetes-object-ref-api-version": <apiVersion name>
xVersion = "x-kubernetes-object-ref-api-version"
// "x-kubernetes-object-ref-kind": <kind name>
xKind = "x-kubernetes-object-ref-kind"
// "x-kubernetes-object-ref-name-key": "name"
// default is "name"
xNameKey = "x-kubernetes-object-ref-name-key"
)
// loadCrdIntoConfig loads a CRD spec into a TransformerConfig
func loadCrdIntoConfig(
types map[string]common.OpenAPIDefinition,
atype string, crd string, in gvk.Gvk,
path []string, config *TransformerConfig) error {
if _, ok := types[crd]; !ok {
theConfig *TransformerConfig, theGvk gvk.Gvk, theMap nameToApiMap,
typeName string, path []string) (err error) {
api, ok := theMap[typeName]
if !ok {
return nil
}
for propname, property := range types[atype].Schema.SchemaProps.Properties {
_, annotate := property.Extensions.GetString(Annotation)
for propName, property := range api.Schema.SchemaProps.Properties {
_, annotate := property.Extensions.GetString(xAnnotation)
if annotate {
config.AddAnnotationFieldSpec(
FieldSpec{
CreateIfNotPresent: false,
Gvk: in,
Path: strings.Join(append(path, propname), "/"),
},
)
err = theConfig.AddAnnotationFieldSpec(
makeFs(theGvk, append(path, propName)))
if err != nil {
return
}
}
_, label := property.Extensions.GetString(LabelSelector)
_, label := property.Extensions.GetString(xLabelSelector)
if label {
config.AddLabelFieldSpec(
FieldSpec{
CreateIfNotPresent: false,
Gvk: in,
Path: strings.Join(append(path, propname), "/"),
},
)
err = theConfig.AddLabelFieldSpec(
makeFs(theGvk, append(path, propName)))
if err != nil {
return
}
}
_, identity := property.Extensions.GetString(Identity)
_, identity := property.Extensions.GetString(xIdentity)
if identity {
config.AddPrefixFieldSpec(
FieldSpec{
CreateIfNotPresent: false,
Gvk: in,
Path: strings.Join(append(path, propname), "/"),
},
)
err = theConfig.AddPrefixFieldSpec(
makeFs(theGvk, append(path, propName)))
if err != nil {
return
}
}
version, ok := property.Extensions.GetString(Version)
version, ok := property.Extensions.GetString(xVersion)
if ok {
kind, ok := property.Extensions.GetString(Kind)
kind, ok := property.Extensions.GetString(xKind)
if ok {
nameKey, ok := property.Extensions.GetString(NameKey)
nameKey, ok := property.Extensions.GetString(xNameKey)
if !ok {
nameKey = "name"
}
config.AddNamereferenceFieldSpec(NameBackReferences{
Gvk: gvk.Gvk{Kind: kind, Version: version},
FieldSpecs: []FieldSpec{
{
CreateIfNotPresent: false,
Gvk: in,
Path: strings.Join(append(path, propname, nameKey), "/"),
},
},
})
err = theConfig.AddNamereferenceFieldSpec(
NameBackReferences{
Gvk: gvk.Gvk{Kind: kind, Version: version},
FieldSpecs: []FieldSpec{
makeFs(theGvk, append(path, propName, nameKey))},
})
if err != nil {
return
}
}
}
if property.Ref.GetURL() != nil {
loadCrdIntoConfig(
types, property.Ref.String(), crd, in,
append(path, propname), config)
theConfig, theGvk, theMap,
property.Ref.String(), append(path, propName))
}
}
return nil
}
func makeFs(in gvk.Gvk, path []string) FieldSpec {
return FieldSpec{
CreateIfNotPresent: false,
Gvk: in,
Path: strings.Join(path, "/"),
}
}

View File

@@ -20,9 +20,9 @@ import (
"reflect"
"testing"
"sigs.k8s.io/kustomize/internal/loadertest"
"sigs.k8s.io/kustomize/pkg/gvk"
"sigs.k8s.io/kustomize/pkg/ifc"
"sigs.k8s.io/kustomize/pkg/internal/loadertest"
)
// This defines two CRD's: Bee and MyKind.
@@ -182,8 +182,7 @@ func TestLoadCRDs(t *testing.T) {
NameReference: nbrs,
}
actualTc, err := NewFactory(makeLoader(t)).LoadCRDs(
[]string{"crd.json"})
actualTc, err := LoadConfigFromCRDs(makeLoader(t), []string{"crd.json"})
if err != nil {
t.Fatalf("unexpected error:%v", err)
}

View File

@@ -57,6 +57,11 @@ func (fs FieldSpec) String() string {
"%s:%v:%s", fs.Gvk.String(), fs.CreateIfNotPresent, fs.Path)
}
// If true, the primary key is the same, but other fields might not be.
func (fs FieldSpec) effectivelyEquals(other FieldSpec) bool {
return fs.IsSelected(&other.Gvk) && fs.Path == other.Path
}
// PathSlice converts the path string to a slice of strings,
// separated by a '/'. Forward slash can be contained in a
// fieldname. such as ingress.kubernetes.io/auth-secret in
@@ -76,7 +81,6 @@ func (fs FieldSpec) PathSlice() []string {
if !strings.Contains(fs.Path, escapedForwardSlash) {
return strings.Split(fs.Path, "/")
}
s := strings.Replace(fs.Path, escapedForwardSlash, tempSlashReplacement, -1)
paths := strings.Split(s, "/")
var result []string
@@ -93,3 +97,43 @@ func (s fsSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s fsSlice) Less(i, j int) bool {
return s[i].Gvk.IsLessThan(s[j].Gvk)
}
// mergeAll merges the argument into this, returning the result.
// Items already present are ignored.
// Items that conflict (primary key matches, but remain data differs)
// result in an error.
func (s fsSlice) mergeAll(incoming fsSlice) (result fsSlice, err error) {
result = s
for _, x := range incoming {
result, err = result.mergeOne(x)
if err != nil {
return nil, err
}
}
return result, nil
}
// mergeOne merges the argument into this, returning the result.
// If the item's primary key is already present, and there are no
// conflicts, it is ignored (we don't want duplicates).
// If there is a conflict, the merge fails.
func (s fsSlice) mergeOne(x FieldSpec) (fsSlice, error) {
i := s.index(x)
if i > -1 {
// It's already there.
if s[i].CreateIfNotPresent != x.CreateIfNotPresent {
return nil, fmt.Errorf("conflicting fieldspecs")
}
return s, nil
}
return append(s, x), nil
}
func (s fsSlice) index(fs FieldSpec) int {
for i, x := range s {
if x.effectivelyEquals(fs) {
return i
}
}
return -1
}

View File

@@ -17,8 +17,12 @@ limitations under the License.
package config
import (
"fmt"
"reflect"
"strings"
"testing"
"sigs.k8s.io/kustomize/pkg/gvk"
)
func TestPathSlice(t *testing.T) {
@@ -44,3 +48,133 @@ func TestPathSlice(t *testing.T) {
}
}
}
var mergeTests = []struct {
name string
original fsSlice
incoming fsSlice
err error
result fsSlice
}{
{
"normal",
fsSlice{
{
Path: "whatever",
Gvk: gvk.Gvk{Group: "apple"},
CreateIfNotPresent: false,
},
{
Path: "whatever",
Gvk: gvk.Gvk{Group: "pear"},
CreateIfNotPresent: false,
},
},
fsSlice{
{
Path: "home",
Gvk: gvk.Gvk{Group: "beans"},
CreateIfNotPresent: false,
},
},
nil,
fsSlice{
{
Path: "whatever",
Gvk: gvk.Gvk{Group: "apple"},
CreateIfNotPresent: false,
},
{
Path: "whatever",
Gvk: gvk.Gvk{Group: "pear"},
CreateIfNotPresent: false,
},
{
Path: "home",
Gvk: gvk.Gvk{Group: "beans"},
CreateIfNotPresent: false,
},
},
},
{
"ignore copy",
fsSlice{
{
Path: "whatever",
Gvk: gvk.Gvk{Group: "apple"},
CreateIfNotPresent: false,
},
{
Path: "whatever",
Gvk: gvk.Gvk{Group: "pear"},
CreateIfNotPresent: false,
},
},
fsSlice{
{
Path: "whatever",
Gvk: gvk.Gvk{Group: "apple"},
CreateIfNotPresent: false,
},
},
nil,
fsSlice{
{
Path: "whatever",
Gvk: gvk.Gvk{Group: "apple"},
CreateIfNotPresent: false,
},
{
Path: "whatever",
Gvk: gvk.Gvk{Group: "pear"},
CreateIfNotPresent: false,
},
},
},
{
"error on conflict",
fsSlice{
{
Path: "whatever",
Gvk: gvk.Gvk{Group: "apple"},
CreateIfNotPresent: false,
},
{
Path: "whatever",
Gvk: gvk.Gvk{Group: "pear"},
CreateIfNotPresent: false,
},
},
fsSlice{
{
Path: "whatever",
Gvk: gvk.Gvk{Group: "apple"},
CreateIfNotPresent: true,
},
},
fmt.Errorf("hey"),
fsSlice{},
},
}
func TestFsSlice_MergeAll(t *testing.T) {
for _, item := range mergeTests {
result, err := item.original.mergeAll(item.incoming)
if item.err == nil {
if err != nil {
t.Fatalf("test %s: unexpected err %v", item.name, err)
}
if !reflect.DeepEqual(item.result, result) {
t.Fatalf("test %s: expected: %v\n but got: %v\n",
item.name, item.result, result)
}
} else {
if err == nil {
t.Fatalf("test %s: expected err: %v", item.name, err)
}
if !strings.Contains(err.Error(), "conflicting fieldspecs") {
t.Fatalf("test %s: unexpected err: %v", item.name, err)
}
}
}
}

View File

@@ -52,7 +52,7 @@ import (
// }
type NameBackReferences struct {
gvk.Gvk `json:",inline,omitempty" yaml:",inline,omitempty"`
FieldSpecs []FieldSpec `json:"FieldSpecs,omitempty" yaml:"FieldSpecs,omitempty"`
FieldSpecs fsSlice `json:"FieldSpecs,omitempty" yaml:"FieldSpecs,omitempty"`
}
func (n NameBackReferences) String() string {
@@ -72,20 +72,27 @@ func (s nbrSlice) Less(i, j int) bool {
return s[i].Gvk.IsLessThan(s[j].Gvk)
}
func (s nbrSlice) mergeAll(o nbrSlice) nbrSlice {
result := s
func (s nbrSlice) mergeAll(o nbrSlice) (result nbrSlice, err error) {
result = s
for _, r := range o {
result = result.mergeOne(r)
result, err = result.mergeOne(r)
if err != nil {
return nil, err
}
}
return result
return result, nil
}
func (s nbrSlice) mergeOne(other NameBackReferences) nbrSlice {
func (s nbrSlice) mergeOne(other NameBackReferences) (nbrSlice, error) {
var result nbrSlice
var err error
found := false
for _, c := range s {
if c.Gvk.Equals(other.Gvk) {
c.FieldSpecs = append(c.FieldSpecs, other.FieldSpecs...)
c.FieldSpecs, err = c.FieldSpecs.mergeAll(other.FieldSpecs)
if err != nil {
return nil, err
}
found = true
}
result = append(result, c)
@@ -94,5 +101,5 @@ func (s nbrSlice) mergeOne(other NameBackReferences) nbrSlice {
if !found {
result = append(result, other)
}
return result
return result, nil
}

View File

@@ -89,17 +89,19 @@ func TestMergeAll(t *testing.T) {
Gvk: gvk.Gvk{
Kind: "ConfigMap",
},
// Current behavior allows repeats of FieldSpec
FieldSpecs: append(fsSlice1, fsSlice1...),
FieldSpecs: fsSlice1,
},
{
Gvk: gvk.Gvk{
Kind: "Secret",
},
FieldSpecs: append(fsSlice2, fsSlice2...),
FieldSpecs: fsSlice2,
},
}
actual := nbrsSlice1.mergeAll(nbrsSlice2)
actual, err := nbrsSlice1.mergeAll(nbrsSlice2)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("expected\n %v\n but got\n %v\n", expected, actual)
}

View File

@@ -19,7 +19,10 @@ limitations under the License.
package config
import (
"log"
"sort"
"sigs.k8s.io/kustomize/pkg/transformers/config/defaultconfig"
)
// TransformerConfig holds the data needed to perform transformations.
@@ -31,6 +34,22 @@ type TransformerConfig struct {
CommonAnnotations fsSlice `json:"commonAnnotations,omitempty" yaml:"commonAnnotations,omitempty"`
NameReference nbrSlice `json:"nameReference,omitempty" yaml:"nameReference,omitempty"`
VarReference fsSlice `json:"varReference,omitempty" yaml:"varReference,omitempty"`
Images fsSlice `json:"images,omitempty" yaml:"images,omitempty"`
}
// MakeEmptyConfig returns an empty TransformerConfig object
func MakeEmptyConfig() *TransformerConfig {
return &TransformerConfig{}
}
// MakeDefaultConfig returns a default TransformerConfig.
func MakeDefaultConfig() *TransformerConfig {
c, err := makeTransformerConfigFromBytes(
defaultconfig.GetDefaultFieldSpecs())
if err != nil {
log.Fatalf("Unable to make default transformconfig: %v", err)
}
return c
}
// sortFields provides determinism in logging, tests, etc.
@@ -41,46 +60,81 @@ func (t *TransformerConfig) sortFields() {
sort.Sort(t.CommonAnnotations)
sort.Sort(t.NameReference)
sort.Sort(t.VarReference)
sort.Sort(t.Images)
}
// AddPrefixFieldSpec adds a FieldSpec to NamePrefix
func (t *TransformerConfig) AddPrefixFieldSpec(fs FieldSpec) {
t.NamePrefix = append(t.NamePrefix, fs)
func (t *TransformerConfig) AddPrefixFieldSpec(fs FieldSpec) (err error) {
t.NamePrefix, err = t.NamePrefix.mergeOne(fs)
return err
}
// AddSuffixFieldSpec adds a FieldSpec to NameSuffix
func (t *TransformerConfig) AddSuffixFieldSpec(fs FieldSpec) {
t.NameSuffix = append([]FieldSpec{fs}, t.NameSuffix...)
func (t *TransformerConfig) AddSuffixFieldSpec(fs FieldSpec) (err error) {
t.NameSuffix, err = t.NameSuffix.mergeOne(fs)
return err
}
// AddLabelFieldSpec adds a FieldSpec to CommonLabels
func (t *TransformerConfig) AddLabelFieldSpec(fs FieldSpec) {
t.CommonLabels = append(t.CommonLabels, fs)
func (t *TransformerConfig) AddLabelFieldSpec(fs FieldSpec) (err error) {
t.CommonLabels, err = t.CommonLabels.mergeOne(fs)
return err
}
// AddAnnotationFieldSpec adds a FieldSpec to CommonAnnotations
func (t *TransformerConfig) AddAnnotationFieldSpec(fs FieldSpec) {
t.CommonAnnotations = append(t.CommonAnnotations, fs)
func (t *TransformerConfig) AddAnnotationFieldSpec(fs FieldSpec) (err error) {
t.CommonAnnotations, err = t.CommonAnnotations.mergeOne(fs)
return err
}
// AddNamereferenceFieldSpec adds a NameBackReferences to NameReference
func (t *TransformerConfig) AddNamereferenceFieldSpec(nbrs NameBackReferences) {
t.NameReference = t.NameReference.mergeOne(nbrs)
func (t *TransformerConfig) AddNamereferenceFieldSpec(
nbrs NameBackReferences) (err error) {
t.NameReference, err = t.NameReference.mergeOne(nbrs)
return err
}
// Merge merges two TransformerConfigs objects into a new TransformerConfig object
func (t *TransformerConfig) Merge(input *TransformerConfig) *TransformerConfig {
// Merge merges two TransformerConfigs objects into
// a new TransformerConfig object
func (t *TransformerConfig) Merge(input *TransformerConfig) (
merged *TransformerConfig, err error) {
if input == nil {
return t
return t, nil
}
merged = &TransformerConfig{}
merged.NamePrefix, err = t.NamePrefix.mergeAll(input.NamePrefix)
if err != nil {
return nil, err
}
merged.NameSuffix, err = t.NameSuffix.mergeAll(input.NameSuffix)
if err != nil {
return nil, err
}
merged.NameSpace, err = t.NameSpace.mergeAll(input.NameSpace)
if err != nil {
return nil, err
}
merged.CommonAnnotations, err = t.CommonAnnotations.mergeAll(
input.CommonAnnotations)
if err != nil {
return nil, err
}
merged.CommonLabels, err = t.CommonLabels.mergeAll(input.CommonLabels)
if err != nil {
return nil, err
}
merged.VarReference, err = t.VarReference.mergeAll(input.VarReference)
if err != nil {
return nil, err
}
merged.NameReference, err = t.NameReference.mergeAll(input.NameReference)
if err != nil {
return nil, err
}
merged.Images, err = t.Images.mergeAll(input.Images)
if err != nil {
return nil, err
}
merged := &TransformerConfig{}
merged.NamePrefix = append(t.NamePrefix, input.NamePrefix...)
merged.NameSuffix = append(input.NameSuffix, t.NameSuffix...)
merged.NameSpace = append(t.NameSpace, input.NameSpace...)
merged.CommonAnnotations = append(t.CommonAnnotations, input.CommonAnnotations...)
merged.CommonLabels = append(t.CommonLabels, input.CommonLabels...)
merged.VarReference = append(t.VarReference, input.VarReference...)
merged.NameReference = t.NameReference.mergeAll(input.NameReference)
merged.sortFields()
return merged
return merged, nil
}

View File

@@ -42,7 +42,10 @@ func TestAddNamereferenceFieldSpec(t *testing.T) {
},
}
cfg.AddNamereferenceFieldSpec(nbrs)
err := cfg.AddNamereferenceFieldSpec(nbrs)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if len(cfg.NameReference) != 1 {
t.Fatal("failed to add namereference FieldSpec")
}
@@ -57,19 +60,31 @@ func TestAddFieldSpecs(t *testing.T) {
CreateIfNotPresent: true,
}
cfg.AddPrefixFieldSpec(fieldSpec)
err := cfg.AddPrefixFieldSpec(fieldSpec)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if len(cfg.NamePrefix) != 1 {
t.Fatalf("failed to add nameprefix FieldSpec")
}
cfg.AddSuffixFieldSpec(fieldSpec)
err = cfg.AddSuffixFieldSpec(fieldSpec)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if len(cfg.NameSuffix) != 1 {
t.Fatalf("failed to add namesuffix FieldSpec")
}
cfg.AddLabelFieldSpec(fieldSpec)
err = cfg.AddLabelFieldSpec(fieldSpec)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if len(cfg.CommonLabels) != 1 {
t.Fatalf("failed to add nameprefix FieldSpec")
}
cfg.AddAnnotationFieldSpec(fieldSpec)
err = cfg.AddAnnotationFieldSpec(fieldSpec)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if len(cfg.CommonAnnotations) != 1 {
t.Fatalf("failed to add nameprefix FieldSpec")
}
@@ -128,7 +143,10 @@ func TestMerge(t *testing.T) {
cfgb.AddPrefixFieldSpec(fieldSpecs[1])
cfga.AddSuffixFieldSpec(fieldSpecs[1])
actual := cfga.Merge(cfgb)
actual, err := cfga.Merge(cfgb)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if len(actual.NamePrefix) != 2 {
t.Fatal("merge failed for namePrefix FieldSpec")
@@ -154,7 +172,10 @@ func TestMerge(t *testing.T) {
t.Fatalf("expected: %v\n but got: %v\n", expected, actual)
}
actual = cfga.Merge(nil)
actual, err = cfga.Merge(nil)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if !reflect.DeepEqual(actual, cfga) {
t.Fatalf("expected: %v\n but got: %v\n", cfga, actual)
}

202
pkg/transformers/image.go Normal file
View File

@@ -0,0 +1,202 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package transformers
import (
"fmt"
"regexp"
"strings"
"sigs.k8s.io/kustomize/pkg/image"
"sigs.k8s.io/kustomize/pkg/resmap"
"sigs.k8s.io/kustomize/pkg/transformers/config"
)
// imageTransformer replace image names and tags
type imageTransformer struct {
images []image.Image
fieldSpecs []config.FieldSpec
}
var _ Transformer = &imageTransformer{}
// NewImageTransformer constructs an imageTransformer.
func NewImageTransformer(slice []image.Image, fs []config.FieldSpec) (Transformer, error) {
return &imageTransformer{slice, fs}, nil
}
// Transform finds the matching images and replaces name, tag and/or digest
func (pt *imageTransformer) Transform(m resmap.ResMap) error {
if len(pt.images) == 0 {
return nil
}
for id := range m {
objMap := m[id].Map()
for _, path := range pt.fieldSpecs {
if !id.Gvk().IsSelected(&path.Gvk) {
continue
}
err := mutateField(objMap, path.PathSlice(), false, pt.mutateImage)
if err != nil {
return err
}
}
// Kept for backward compatibility
if err := pt.findAndReplaceImage(objMap); err != nil && id.Gvk().Kind != `CustomResourceDefinition` {
return err
}
}
return nil
}
func (pt *imageTransformer) mutateImage(in interface{}) (interface{}, error) {
original, ok := in.(string)
if !ok {
return nil, fmt.Errorf("image path is not of type string but %T", in)
}
for _, img := range pt.images {
if !isImageMatched(original, img.Name) {
continue
}
name, tag := split(original)
if img.NewName != "" {
name = img.NewName
}
if img.NewTag != "" {
tag = ":" + img.NewTag
}
if img.Digest != "" {
tag = "@" + img.Digest
}
return name + tag, nil
}
return original, nil
}
/*
findAndReplaceImage replaces the image name and tags inside one object
It searches the object for container session
then loops though all images inside containers session,
finds matched ones and update the image name and tag name
*/
func (pt *imageTransformer) findAndReplaceImage(obj map[string]interface{}) error {
paths := []string{"containers", "initContainers"}
updated := false
for _, path := range paths {
containers, found := obj[path]
if found {
if _, err := pt.updateContainers(containers); err != nil {
return err
}
updated = true
}
}
if !updated {
return pt.findContainers(obj)
}
return nil
}
func (pt *imageTransformer) updateContainers(in interface{}) (interface{}, error) {
containers, ok := in.([]interface{})
if !ok {
return nil, fmt.Errorf("containers path is not of type []interface{} but %T", in)
}
for i := range containers {
container := containers[i].(map[string]interface{})
containerImage, found := container["image"]
if !found {
continue
}
imageName := containerImage.(string)
for _, img := range pt.images {
if !isImageMatched(imageName, img.Name) {
continue
}
newImage, err := pt.mutateImage(imageName)
if err != nil {
return nil, err
}
container["image"] = newImage
break
}
}
return containers, nil
}
func (pt *imageTransformer) findContainers(obj map[string]interface{}) error {
for key := range obj {
switch typedV := obj[key].(type) {
case map[string]interface{}:
err := pt.findAndReplaceImage(typedV)
if err != nil {
return err
}
case []interface{}:
for i := range typedV {
item := typedV[i]
typedItem, ok := item.(map[string]interface{})
if ok {
err := pt.findAndReplaceImage(typedItem)
if err != nil {
return err
}
}
}
}
}
return nil
}
func isImageMatched(s, t string) bool {
// Tag values are limited to [a-zA-Z0-9_.-].
pattern, _ := regexp.Compile("^" + t + "(:[a-zA-Z0-9_.-]*)?$")
return pattern.MatchString(s)
}
// split separates and returns the name and tag parts
// from the image string using either colon `:` or at `@` separators.
// Note that the returned tag keeps its separator.
func split(imageName string) (name string, tag string) {
// check if image name contains a domain
// if domain is present, ignore domain and check for `:`
ic := -1
if slashIndex := strings.Index(imageName, "/"); slashIndex < 0 {
ic = strings.LastIndex(imageName, ":")
} else {
lastIc := strings.LastIndex(imageName[slashIndex:], ":")
// set ic only if `:` is present
if lastIc > 0 {
ic = slashIndex + lastIc
}
}
ia := strings.LastIndex(imageName, "@")
if ic < 0 && ia < 0 {
return imageName, ""
}
i := ic
if ic < 0 {
i = ia
}
name = imageName[:i]
tag = imageName[i:]
return
}

View File

@@ -1,126 +0,0 @@
/*
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 transformers
import (
"regexp"
"sigs.k8s.io/kustomize/pkg/resmap"
"sigs.k8s.io/kustomize/pkg/types"
)
// imageTagTransformer replace image tags
type imageTagTransformer struct {
imageTags []types.ImageTag
}
var _ Transformer = &imageTagTransformer{}
// NewImageTagTransformer constructs a imageTagTransformer.
func NewImageTagTransformer(slice []types.ImageTag) (Transformer, error) {
return &imageTagTransformer{slice}, nil
}
// Transform finds the matching images and replace the tag
func (pt *imageTagTransformer) Transform(resources resmap.ResMap) error {
if len(pt.imageTags) == 0 {
return nil
}
for _, res := range resources {
err := pt.findAndReplaceTag(res.Map())
if err != nil {
return err
}
}
return nil
}
/*
findAndReplaceTag replaces the image tags inside one object
It searches the object for container session
then loops though all images inside containers session, finds matched ones and update the tag name
*/
func (pt *imageTagTransformer) findAndReplaceTag(obj map[string]interface{}) error {
paths := []string{"containers", "initContainers"}
found := false
for _, path := range paths {
_, found = obj[path]
if found {
err := pt.updateContainers(obj, path)
if err != nil {
return err
}
}
}
if !found {
return pt.findContainers(obj)
}
return nil
}
func (pt *imageTagTransformer) updateContainers(obj map[string]interface{}, path string) error {
containers := obj[path].([]interface{})
for i := range containers {
container := containers[i].(map[string]interface{})
image, found := container["image"]
if !found {
continue
}
for _, imagetag := range pt.imageTags {
if isImageMatched(image.(string), imagetag.Name) {
container["image"] = imagetag.Name + ":" + imagetag.NewTag
if imagetag.Digest != "" {
container["image"] = imagetag.Name + "@" + imagetag.Digest
}
break
}
}
}
return nil
}
func (pt *imageTagTransformer) findContainers(obj map[string]interface{}) error {
for key := range obj {
switch typedV := obj[key].(type) {
case map[string]interface{}:
err := pt.findAndReplaceTag(typedV)
if err != nil {
return err
}
case []interface{}:
for i := range typedV {
item := typedV[i]
typedItem, ok := item.(map[string]interface{})
if ok {
err := pt.findAndReplaceTag(typedItem)
if err != nil {
return err
}
}
}
}
}
return nil
}
func isImageMatched(s, t string) bool {
// Tag values are limited to [a-zA-Z0-9_.-].
pattern, _ := regexp.Compile("^" + t + "(:[a-zA-Z0-9_.-]*)?$")
return pattern.MatchString(s)
}

View File

@@ -1,189 +0,0 @@
/*
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 transformers
import (
"reflect"
"testing"
"sigs.k8s.io/kustomize/k8sdeps/kunstruct"
"sigs.k8s.io/kustomize/pkg/gvk"
"sigs.k8s.io/kustomize/pkg/resid"
"sigs.k8s.io/kustomize/pkg/resmap"
"sigs.k8s.io/kustomize/pkg/resource"
"sigs.k8s.io/kustomize/pkg/types"
)
func TestImageTagTransformer(t *testing.T) {
var rf = resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl())
m := resmap.ResMap{
resid.NewResId(deploy, "deploy1"): rf.FromMap(
map[string]interface{}{
"group": "apps",
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "deploy1",
},
"spec": map[string]interface{}{
"template": map[string]interface{}{
"spec": map[string]interface{}{
"initContainers": []interface{}{
map[string]interface{}{
"name": "nginx2",
"image": "my-nginx:1.8.0",
},
},
"containers": []interface{}{
map[string]interface{}{
"name": "nginx",
"image": "nginx:1.7.9",
},
map[string]interface{}{
"name": "replaced-with-digest",
"image": "foobar:1",
},
},
},
},
},
}),
resid.NewResId(gvk.Gvk{Kind: "randomKind"}, "random"): rf.FromMap(
map[string]interface{}{
"spec": map[string]interface{}{
"template": map[string]interface{}{
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "nginx1",
"image": "nginx",
},
map[string]interface{}{
"name": "myimage",
"image": "myprivaterepohostname:1234/my/image:latest",
},
},
},
},
},
"spec2": map[string]interface{}{
"template": map[string]interface{}{
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "nginx3",
"image": "nginx:v1",
},
map[string]interface{}{
"name": "nginx4",
"image": "my-nginx:latest",
},
},
},
},
},
}),
}
expected := resmap.ResMap{
resid.NewResId(deploy, "deploy1"): rf.FromMap(
map[string]interface{}{
"group": "apps",
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "deploy1",
},
"spec": map[string]interface{}{
"template": map[string]interface{}{
"spec": map[string]interface{}{
"initContainers": []interface{}{
map[string]interface{}{
"name": "nginx2",
"image": "my-nginx:previous",
},
},
"containers": []interface{}{
map[string]interface{}{
"name": "nginx",
"image": "nginx:v2",
},
map[string]interface{}{
"name": "replaced-with-digest",
"image": "foobar@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3",
},
},
},
},
},
}),
resid.NewResId(gvk.Gvk{Kind: "randomKind"}, "random"): rf.FromMap(
map[string]interface{}{
"spec": map[string]interface{}{
"template": map[string]interface{}{
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "nginx1",
"image": "nginx:v2",
},
map[string]interface{}{
"name": "myimage",
"image": "myprivaterepohostname:1234/my/image:v1.0.1",
},
},
},
},
},
"spec2": map[string]interface{}{
"template": map[string]interface{}{
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "nginx3",
"image": "nginx:v2",
},
map[string]interface{}{
"name": "nginx4",
"image": "my-nginx:previous",
},
},
},
},
},
}),
}
it, err := NewImageTagTransformer([]types.ImageTag{
{Name: "nginx", NewTag: "v2"},
{Name: "my-nginx", NewTag: "previous"},
{Name: "myprivaterepohostname:1234/my/image", NewTag: "v1.0.1"},
{Name: "foobar", Digest: "sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3"},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
err = it.Transform(m)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(m, expected) {
err = expected.ErrorIfNotEqual(m)
t.Fatalf("actual doesn't match expected: %v", err)
}
}

View File

@@ -76,7 +76,9 @@ func (o *mapTransformer) Transform(m resmap.ResMap) error {
func (o *mapTransformer) addMap(in interface{}) (interface{}, error) {
m, ok := in.(map[string]interface{})
if !ok {
if in == nil {
m = map[string]interface{}{}
} else if !ok {
return nil, fmt.Errorf("%#v is expected to be %T", in, m)
}
for k, v := range o.m {

View File

@@ -44,7 +44,7 @@ var crb = gvk.Gvk{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "Clus
var sa = gvk.Gvk{Version: "v1", Kind: "ServiceAccount"}
var ingress = gvk.Gvk{Kind: "Ingress"}
var rf = resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl())
var defaultTransformerConfig = config.NewFactory(nil).DefaultConfig()
var defaultTransformerConfig = config.MakeDefaultConfig()
func TestLabelsRun(t *testing.T) {
m := resmap.ResMap{
@@ -355,6 +355,12 @@ func TestLabelsRun(t *testing.T) {
"spec": map[string]interface{}{
"schedule": "* 23 * * *",
"jobTemplate": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"label-key1": "label-value1",
"label-key2": "label-value2",
},
},
"spec": map[string]interface{}{
"template": map[string]interface{}{
"metadata": map[string]interface{}{
@@ -390,6 +396,12 @@ func TestLabelsRun(t *testing.T) {
"spec": map[string]interface{}{
"schedule": "* 23 * * *",
"jobTemplate": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"label-key1": "label-value1",
"label-key2": "label-value2",
},
},
"spec": map[string]interface{}{
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
@@ -571,3 +583,48 @@ func TestAnnotationsRun(t *testing.T) {
t.Fatalf("actual doesn't match expected: %v", err)
}
}
func TestAnnotaionsRunWithNullValue(t *testing.T) {
m := resmap.ResMap{
resid.NewResId(cmap, "cm1"): rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
"annotations": nil,
},
}),
}
expected := resmap.ResMap{
resid.NewResId(cmap, "cm1"): rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
"annotations": map[string]interface{}{
"anno-key1": "anno-value1",
"anno-key2": "anno-value2",
},
},
}),
}
at, err := NewAnnotationsMapTransformer(
map[string]string{"anno-key1": "anno-value1", "anno-key2": "anno-value2"},
defaultTransformerConfig.CommonAnnotations)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
err = at.Transform(m)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(m, expected) {
err = expected.ErrorIfNotEqual(m)
t.Fatalf("actual doesn't match expected: %v", err)
}
}

View File

@@ -17,10 +17,11 @@ limitations under the License.
package transformers
import (
"errors"
"fmt"
"log"
"sigs.k8s.io/kustomize/pkg/gvk"
"sigs.k8s.io/kustomize/pkg/resid"
"sigs.k8s.io/kustomize/pkg/resmap"
"sigs.k8s.io/kustomize/pkg/transformers/config"
)
@@ -33,18 +34,31 @@ var _ Transformer = &nameReferenceTransformer{}
// NewNameReferenceTransformer constructs a nameReferenceTransformer
// with a given slice of NameBackReferences.
func NewNameReferenceTransformer(
br []config.NameBackReferences) (Transformer, error) {
func NewNameReferenceTransformer(br []config.NameBackReferences) Transformer {
if br == nil {
return nil, errors.New("backrefs not expected to be nil")
log.Fatal("backrefs not expected to be nil")
}
return &nameReferenceTransformer{backRefs: br}, nil
return &nameReferenceTransformer{backRefs: br}
}
// Transform does the field update according to fieldSpecs.
// The old name is in the key in the map and the new name is in the object
// associated with the key. e.g. if <k, v> is one of the key-value pair in the map,
// then the old name is k.Name and the new name is v.GetName()
// Transform updates name references in resource A that refer to resource B,
// given that B's name may have changed.
//
// For example, a HorizontalPodAutoscaler (HPA) necessarily refers to a
// Deployment (the thing that the HPA scales). The Deployment name might change
// (e.g. prefix added), and the reference in the HPA has to be fixed.
//
// In the outer loop below, we encounter an HPA. In scanning backrefs, we
// find that HPA refers to a Deployment. So we find all resources in the same
// namespace as the HPA (and with the same prefix and suffix), and look through
// them to find all the Deployments with a resId that has a Name matching the
// field in HPA. For each match, we overwrite the HPA name field with the value
// found in the Deployment's name field (the name in the raw object - the
// modified name - not the unmodified name in the resId).
//
// This assumes that the name stored in a ResId (the ResMap key) isn't modified
// by name transformers. Name transformers should only modify the name in the
// body of the resource object (the value in the ResMap).
func (o *nameReferenceTransformer) Transform(m resmap.ResMap) error {
// TODO: Too much looping.
// Even more hidden loops in FilterBy,
@@ -57,7 +71,7 @@ func (o *nameReferenceTransformer) Transform(m resmap.ResMap) error {
m[id].Map(), fSpec.PathSlice(),
fSpec.CreateIfNotPresent,
o.updateNameReference(
backRef.Gvk, m.FilterBy(id)))
id, backRef.Gvk, m.FilterBy(id)))
if err != nil {
return err
}
@@ -69,14 +83,14 @@ func (o *nameReferenceTransformer) Transform(m resmap.ResMap) error {
}
func (o *nameReferenceTransformer) updateNameReference(
backRef gvk.Gvk, m resmap.ResMap) func(in interface{}) (interface{}, error) {
rid resid.ResId, backRef gvk.Gvk, m resmap.ResMap) func(in interface{}) (interface{}, error) {
return func(in interface{}) (interface{}, error) {
switch in.(type) {
case string:
s, _ := in.(string)
for id, res := range m {
if id.Gvk().IsSelected(&backRef) && id.Name() == s {
matchedIds := m.FindByGVKN(id)
matchedIds := m.GetMatchingIds(id.GvknEquals)
// If there's more than one match, there's no way
// to know which one to pick, so emit error.
if len(matchedIds) > 1 {
@@ -85,6 +99,7 @@ func (o *nameReferenceTransformer) updateNameReference(
}
// Return transformed name of the object,
// complete with prefixes, hashes, etc.
res.AppendRefBy(rid)
return res.GetName(), nil
}
}
@@ -102,7 +117,7 @@ func (o *nameReferenceTransformer) updateNameReference(
for id, res := range m {
indexes := indexOf(id.Name(), names)
if id.Gvk().IsSelected(&backRef) && len(indexes) > 0 {
matchedIds := m.FindByGVKN(id)
matchedIds := m.GetMatchingIds(id.GvknEquals)
if len(matchedIds) > 1 {
return nil, fmt.Errorf(
"Multiple matches for name %s:\n %v", id, matchedIds)
@@ -110,6 +125,7 @@ func (o *nameReferenceTransformer) updateNameReference(
for _, index := range indexes {
l[index] = res.GetName()
}
res.AppendRefBy(rid)
return l, nil
}
}

View File

@@ -148,6 +148,9 @@ func TestNameReferenceHappyRun(t *testing.T) {
"configMap": map[string]interface{}{
"name": "cm2",
},
"secret": map[string]interface{}{
"name": "secret1",
},
},
},
"secret": map[string]interface{}{
@@ -184,6 +187,9 @@ func TestNameReferenceHappyRun(t *testing.T) {
"configMap": map[string]interface{}{
"name": "cm2",
},
"secret": map[string]interface{}{
"name": "secret1",
},
},
},
},
@@ -235,6 +241,43 @@ func TestNameReferenceHappyRun(t *testing.T) {
},
},
}),
resid.NewResId(cronjob, "cronjob1"): rf.FromMap(
map[string]interface{}{
"apiVersion": "batch/v1beta1",
"kind": "CronJob",
"metadata": map[string]interface{}{
"name": "cronjob1",
},
"spec": map[string]interface{}{
"schedule": "0 14 * * *",
"jobTemplate": map[string]interface{}{
"spec": map[string]interface{}{
"template": map[string]interface{}{
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "main",
"image": "myimage",
},
},
"volumes": map[string]interface{}{
"projected": map[string]interface{}{
"sources": map[string]interface{}{
"configMap": map[string]interface{}{
"name": "cm2",
},
"secret": map[string]interface{}{
"name": "secret1",
},
},
},
},
},
},
},
},
},
}),
}
expected := resmap.ResMap{}
@@ -307,6 +350,9 @@ func TestNameReferenceHappyRun(t *testing.T) {
"configMap": map[string]interface{}{
"name": "someprefix-cm2-somehash",
},
"secret": map[string]interface{}{
"name": "someprefix-secret1-somehash",
},
},
},
"secret": map[string]interface{}{
@@ -343,6 +389,9 @@ func TestNameReferenceHappyRun(t *testing.T) {
"configMap": map[string]interface{}{
"name": "someprefix-cm2-somehash",
},
"secret": map[string]interface{}{
"name": "someprefix-secret1-somehash",
},
},
},
},
@@ -405,11 +454,45 @@ func TestNameReferenceHappyRun(t *testing.T) {
},
},
})
nrt, err := NewNameReferenceTransformer(defaultTransformerConfig.NameReference)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
err = nrt.Transform(m)
expected[resid.NewResId(cronjob, "cronjob1")] = rf.FromMap(
map[string]interface{}{
"apiVersion": "batch/v1beta1",
"kind": "CronJob",
"metadata": map[string]interface{}{
"name": "cronjob1",
},
"spec": map[string]interface{}{
"schedule": "0 14 * * *",
"jobTemplate": map[string]interface{}{
"spec": map[string]interface{}{
"template": map[string]interface{}{
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "main",
"image": "myimage",
},
},
"volumes": map[string]interface{}{
"projected": map[string]interface{}{
"sources": map[string]interface{}{
"configMap": map[string]interface{}{
"name": "someprefix-cm2-somehash",
},
"secret": map[string]interface{}{
"name": "someprefix-secret1-somehash",
},
},
},
},
},
},
},
},
},
})
nrt := NewNameReferenceTransformer(defaultTransformerConfig.NameReference)
err := nrt.Transform(m)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -471,19 +554,78 @@ func TestNameReferenceUnhappyRun(t *testing.T) {
expectedErr: "is expected to be either a string or a []interface{}"},
}
nrt, err := NewNameReferenceTransformer(defaultTransformerConfig.NameReference)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
nrt := NewNameReferenceTransformer(defaultTransformerConfig.NameReference)
for _, test := range tests {
err = nrt.Transform(test.resMap)
err := nrt.Transform(test.resMap)
if err == nil {
t.Fatalf("expected error to happen")
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Fatalf("Incorrect error.\nExpected: %s, but got %v", test.expectedErr, err)
t.Fatalf("Incorrect error.\nExpected: %s, but got %v",
test.expectedErr, err)
}
}
}
func TestNameReferencePersistentVolumeHappyRun(t *testing.T) {
rf := resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl())
m := resmap.ResMap{
resid.NewResId(pv, "volume1"): rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "PersistentVolume",
"metadata": map[string]interface{}{
"name": "someprefix-volume1",
},
}),
resid.NewResId(pvc, "claim1"): rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "PersistentVolumeClaim",
"metadata": map[string]interface{}{
"name": "someprefix-claim1",
"namespace": "some-namespace",
},
"spec": map[string]interface{}{
"volumeName": "volume1",
},
}),
}
expected := resmap.ResMap{
resid.NewResId(pv, "volume1"): rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "PersistentVolume",
"metadata": map[string]interface{}{
"name": "someprefix-volume1",
},
}),
resid.NewResId(pvc, "claim1"): rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "PersistentVolumeClaim",
"metadata": map[string]interface{}{
"name": "someprefix-claim1",
"namespace": "some-namespace",
},
"spec": map[string]interface{}{
"volumeName": "someprefix-volume1",
},
}),
}
expected[resid.NewResId(pv, "volume1")].AppendRefBy(resid.NewResId(pvc, "claim1"))
nrt := NewNameReferenceTransformer(defaultTransformerConfig.NameReference)
err := nrt.Transform(m)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(m, expected) {
err = expected.ErrorIfNotEqual(m)
t.Fatalf("actual doesn't match expected: %v", err)
}
}

View File

@@ -48,14 +48,65 @@ func NewNamespaceTransformer(ns string, cf []config.FieldSpec) Transformer {
// Transform adds the namespace.
func (o *namespaceTransformer) Transform(m resmap.ResMap) error {
mf := resmap.ResMap{}
mf := o.filterResmap(m)
for id := range mf {
objMap := mf[id].Map()
for _, path := range o.fieldSpecsToUse {
switch path.Path {
// Special casing .metadata.namespace since it is a common metadata field across all runtime.Object
// We should add namespace if it's namespaced resource; otherwise, we should not.
case "metadata/namespace":
if id.Gvk().IsSelected(&path.Gvk) && !id.Gvk().IsClusterKind() {
if len(objMap) > 0 {
err := mutateField(
objMap, path.PathSlice(), path.CreateIfNotPresent,
func(_ interface{}) (interface{}, error) {
return o.namespace, nil
})
if err != nil {
return err
}
}
}
default:
if !id.Gvk().IsSelected(&path.Gvk) {
continue
}
// make sure the object is non empty
if len(objMap) > 0 {
err := mutateField(
objMap, path.PathSlice(), path.CreateIfNotPresent,
func(_ interface{}) (interface{}, error) {
return o.namespace, nil
})
if err != nil {
return err
}
}
}
if !id.Gvk().IsClusterKind() {
newid := id.CopyWithNewNamespace(o.namespace)
m[newid] = mf[id]
} else {
m[id] = mf[id]
}
}
}
o.updateClusterRoleBinding(m)
return nil
}
func (o *namespaceTransformer) filterResmap(m resmap.ResMap) resmap.ResMap {
mf := resmap.ResMap{}
for id := range m {
found := false
for _, path := range o.fieldSpecsToSkip {
if id.Gvk().IsSelected(&path.Gvk) {
found = true
break
mf[id] = m[id]
delete(m, id)
}
}
if !found {
@@ -63,27 +114,7 @@ func (o *namespaceTransformer) Transform(m resmap.ResMap) error {
delete(m, id)
}
}
for id := range mf {
objMap := mf[id].Map()
for _, path := range o.fieldSpecsToUse {
if !id.Gvk().IsSelected(&path.Gvk) {
continue
}
err := mutateField(objMap, path.PathSlice(), path.CreateIfNotPresent, func(_ interface{}) (interface{}, error) {
return o.namespace, nil
})
if err != nil {
return err
}
newid := id.CopyWithNewNamespace(o.namespace)
m[newid] = mf[id]
}
}
o.updateClusterRoleBinding(m)
return nil
return mf
}
func (o *namespaceTransformer) updateClusterRoleBinding(m resmap.ResMap) {
@@ -99,7 +130,10 @@ func (o *namespaceTransformer) updateClusterRoleBinding(m resmap.ResMap) {
continue
}
objMap := m[id].Map()
subjects := objMap["subjects"].([]interface{})
subjects, ok := objMap["subjects"].([]interface{})
if subjects == nil || !ok {
continue
}
for i := range subjects {
subject := subjects[i].(map[string]interface{})
kind, foundk := subject["kind"]

View File

@@ -47,6 +47,9 @@ func TestNamespaceRun(t *testing.T) {
"namespace": "foo",
},
}),
resid.NewResId(cmap, "cm3"): rf.FromMap(
map[string]interface{}{},
),
resid.NewResId(ns, "ns1"): rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
@@ -134,6 +137,9 @@ func TestNamespaceRun(t *testing.T) {
"namespace": "test",
},
}),
resid.NewResIdWithPrefixNamespace(cmap, "cm3", "", "test"): rf.FromMap(
map[string]interface{}{},
),
resid.NewResIdWithPrefixNamespace(sa, "default", "", "test"): rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",

View File

@@ -19,7 +19,6 @@ package transformers
import (
"errors"
"fmt"
"log"
"sigs.k8s.io/kustomize/pkg/gvk"
"sigs.k8s.io/kustomize/pkg/resmap"
@@ -43,20 +42,20 @@ var prefixSuffixFieldSpecsToSkip = []config.FieldSpec{
},
}
// deprecateNamePrefixSuffixFieldSpec will be moved into prefixSuffixFieldSpecsToSkip in next release
var deprecateNamePrefixSuffixFieldSpec = config.FieldSpec{
Gvk: gvk.Gvk{Kind: "Namespace"},
}
// NewNamePrefixSuffixTransformer construct a namePrefixSuffixTransformer.
func NewNamePrefixSuffixTransformer(np, ns string, pc []config.FieldSpec) (Transformer, error) {
// NewNamePrefixSuffixTransformer makes a namePrefixSuffixTransformer.
func NewNamePrefixSuffixTransformer(
np, ns string, fieldSpecs []config.FieldSpec) (Transformer, error) {
if len(np) == 0 && len(ns) == 0 {
return NewNoOpTransformer(), nil
}
if pc == nil {
if fieldSpecs == nil {
return nil, errors.New("fieldSpecs is not expected to be nil")
}
return &namePrefixSuffixTransformer{fieldSpecsToUse: pc, prefix: np, suffix: ns, fieldSpecsToSkip: prefixSuffixFieldSpecsToSkip}, nil
return &namePrefixSuffixTransformer{
prefix: np,
suffix: ns,
fieldSpecsToUse: fieldSpecs,
fieldSpecsToSkip: prefixSuffixFieldSpecsToSkip}, nil
}
// Transform prepends the name prefix and appends the name suffix.
@@ -80,15 +79,16 @@ func (o *namePrefixSuffixTransformer) Transform(m resmap.ResMap) error {
}
for id := range mf {
if id.Gvk().IsSelected(&deprecateNamePrefixSuffixFieldSpec.Gvk) {
log.Println("Adding nameprefix and namesuffix to Namespace resource will be deprecated in next release.")
}
objMap := mf[id].Map()
for _, path := range o.fieldSpecsToUse {
if !id.Gvk().IsSelected(&path.Gvk) {
continue
}
err := mutateField(objMap, path.PathSlice(), path.CreateIfNotPresent, o.addPrefixSuffix)
err := mutateField(
objMap,
path.PathSlice(),
path.CreateIfNotPresent,
o.addPrefixSuffix)
if err != nil {
return err
}
@@ -99,7 +99,8 @@ func (o *namePrefixSuffixTransformer) Transform(m resmap.ResMap) error {
return nil
}
func (o *namePrefixSuffixTransformer) addPrefixSuffix(in interface{}) (interface{}, error) {
func (o *namePrefixSuffixTransformer) addPrefixSuffix(
in interface{}) (interface{}, error) {
s, ok := in.(string)
if !ok {
return nil, fmt.Errorf("%#v is expected to be %T", in, s)

View File

@@ -1,66 +1,108 @@
/*
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 transformers
import (
"fmt"
"sigs.k8s.io/kustomize/pkg/expansion"
"sigs.k8s.io/kustomize/pkg/resmap"
"sigs.k8s.io/kustomize/pkg/transformers/config"
)
type refvarTransformer struct {
fieldSpecs []config.FieldSpec
vars map[string]string
type RefVarTransformer struct {
varMap map[string]string
replacementCounts map[string]int
fieldSpecs []config.FieldSpec
mappingFunc func(string) string
}
// NewRefVarTransformer returns a Trasformer that replaces $(VAR) style variables with values.
func NewRefVarTransformer(vars map[string]string, p []config.FieldSpec) Transformer {
return &refvarTransformer{
vars: vars,
fieldSpecs: p,
// NewRefVarTransformer returns a new RefVarTransformer
// that replaces $(VAR) style variables with values.
// The fieldSpecs are the places to look for occurrences of $(VAR).
func NewRefVarTransformer(
varMap map[string]string, fs []config.FieldSpec) *RefVarTransformer {
return &RefVarTransformer{
varMap: varMap,
fieldSpecs: fs,
}
}
// Transform determines the final values of variables:
//
// 1. Determine the final value of each variable:
// a. If the variable's Value is set, expand the `$(var)` references to other
// variables in the .Value field; the sources of variables are the declared
// variables of the container and the service environment variables
// b. If a source is defined for an environment variable, resolve the source
// 2. Create the container's environment in the order variables are declared
// 3. Add remaining service environment vars
func (rv *refvarTransformer) Transform(resources resmap.ResMap) error {
for resId := range resources {
objMap := resources[resId].Map()
for _, pc := range rv.fieldSpecs {
if !resId.Gvk().IsSelected(&pc.Gvk) {
continue
// replaceVars accepts as 'in' a string, or string array, which can have
// embedded instances of $VAR style variables, e.g. a container command string.
// The function returns the string with the variables expanded to their final
// values.
func (rv *RefVarTransformer) replaceVars(in interface{}) (interface{}, error) {
switch vt := in.(type) {
case []interface{}:
var xs []string
for _, a := range in.([]interface{}) {
xs = append(xs, expansion.Expand(a.(string), rv.mappingFunc))
}
return xs, nil
case map[string]interface{}:
inMap := in.(map[string]interface{})
xs := make(map[string]interface{}, len(inMap))
for k, v := range inMap {
s, ok := v.(string)
if !ok {
return nil, fmt.Errorf("%#v is expected to be %T", v, s)
}
err := mutateField(objMap, pc.PathSlice(), false, func(in interface{}) (interface{}, error) {
var (
mappingFunc = expansion.MappingFuncFor(rv.vars)
)
switch vt := in.(type) {
case []interface{}:
var xs []string
for _, a := range in.([]interface{}) {
xs = append(xs, expansion.Expand(a.(string), mappingFunc))
}
return xs, nil
case interface{}:
s, ok := in.(string)
if !ok {
return nil, fmt.Errorf("%#v is expected to be %T", in, s)
}
runtimeVal := expansion.Expand(s, mappingFunc)
return runtimeVal, nil
default:
return "", fmt.Errorf("invalid type encountered %T", vt)
xs[k] = expansion.Expand(s, rv.mappingFunc)
}
return xs, nil
case interface{}:
s, ok := in.(string)
if !ok {
return nil, fmt.Errorf("%#v is expected to be %T", in, s)
}
return expansion.Expand(s, rv.mappingFunc), nil
case nil:
return nil, nil
default:
return "", fmt.Errorf("invalid type encountered %T", vt)
}
}
// UnusedVars returns slice of Var names that were unused
// after a Transform run.
func (rv *RefVarTransformer) UnusedVars() []string {
var unused []string
for k := range rv.varMap {
_, ok := rv.replacementCounts[k]
if !ok {
unused = append(unused, k)
}
}
return unused
}
// Transform replaces $(VAR) style variables with values.
func (rv *RefVarTransformer) Transform(m resmap.ResMap) error {
rv.replacementCounts = make(map[string]int)
rv.mappingFunc = expansion.MappingFuncFor(
rv.replacementCounts, rv.varMap)
for id, res := range m {
for _, fieldSpec := range rv.fieldSpecs {
if id.Gvk().IsSelected(&fieldSpec.Gvk) {
if err := mutateField(
res.Map(), fieldSpec.PathSlice(),
false, rv.replaceVars); err != nil {
return err
}
})
if err != nil {
return err
}
}
}

View File

@@ -0,0 +1,109 @@
/*
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 transformers
import (
"reflect"
"testing"
"sigs.k8s.io/kustomize/pkg/resid"
"sigs.k8s.io/kustomize/pkg/resmap"
"sigs.k8s.io/kustomize/pkg/transformers/config"
)
func TestVarRef(t *testing.T) {
type given struct {
varMap map[string]string
fs []config.FieldSpec
res resmap.ResMap
}
type expected struct {
res resmap.ResMap
unused []string
}
testCases := []struct {
description string
given given
expected expected
}{
{
description: "var replacement in map[string]",
given: given{
varMap: map[string]string{
"FOO": "replacementForFoo",
"BAR": "replacementForBar",
},
fs: []config.FieldSpec{
{Gvk: cmap, Path: "data"},
},
res: resmap.ResMap{
resid.NewResId(cmap, "cm1"): rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
},
"data": map[string]interface{}{
"item1": "$(FOO)",
"item2": "bla",
},
}),
},
},
expected: expected{
res: resmap.ResMap{
resid.NewResId(cmap, "cm1"): rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
},
"data": map[string]interface{}{
"item1": "replacementForFoo",
"item2": "bla",
},
}),
},
unused: []string{"BAR"},
},
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
// arrange
tr := NewRefVarTransformer(tc.given.varMap, tc.given.fs)
// act
err := tr.Transform(tc.given.res)
// assert
if err != nil {
t.Errorf("unexpected error: %v", err)
}
a, e := tc.given.res, tc.expected.res
if !reflect.DeepEqual(a, e) {
err = e.ErrorIfNotEqual(a)
t.Fatalf("actual doesn't match expected: \nACTUAL:\n%v\nEXPECTED:\n%v\nERR: %v", a, e, err)
}
})
}
}

View File

@@ -17,10 +17,17 @@ limitations under the License.
// Package transformers has implementations of resmap.ResMap transformers.
package transformers
import "sigs.k8s.io/kustomize/pkg/resmap"
import (
"sigs.k8s.io/kustomize/pkg/resmap"
)
// A Transformer modifies an instance of resmap.ResMap.
type Transformer interface {
// Transform modifies data in the argument, e.g. adding labels to resources that can be labelled.
Transform(m resmap.ResMap) error
}
// A Generator creates an instance of resmap.ResMap.
type Generator interface {
Generate() (resmap.ResMap, error)
}