Migrate Replacement Transformer plugin

Plugin extended to support new kustomize plugin framework which
consider each plugin as a container

Change-Id: If55b7093f711401165b7d4fd3f5b1059fde464ff
Relates-To: #340
This commit is contained in:
Dmitry Ukov 2020-09-23 18:02:59 +04:00
parent 025c2172d6
commit ca1a3a2d0b
5 changed files with 611 additions and 179 deletions

@ -0,0 +1,97 @@
/*
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
https://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 kyamlutils
import (
"fmt"
"strings"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/kio/filters"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
var _ kio.Filter = DocumentSelector{}
// DocumentSelector RNode objects
type DocumentSelector struct {
filters []kio.Filter
}
// Filters return list of defined filters for the selector
func (f DocumentSelector) Filters() []kio.Filter {
return f.filters
}
func (f DocumentSelector) byPath(path []string, val string) DocumentSelector {
// Need to have exact match of the value since grep filter considers Value
// as a regular expression
f.filters = append(f.filters, filters.GrepFilter{Path: path, Value: "^" + val + "$"})
return f
}
// ByKey adds filter by specific yaml manifest key and value
func (f DocumentSelector) ByKey(key, val string) DocumentSelector {
return f.byPath([]string{key}, val)
}
// ByAPIVersion adds filter by 'apiVersion' field value
func (f DocumentSelector) ByAPIVersion(apiver string) DocumentSelector {
if apiver != "" {
return f.ByKey(yaml.APIVersionField, apiver)
}
return f
}
// ByGVK adds filters by 'apiVersion' and 'kind; field values
func (f DocumentSelector) ByGVK(group, version, kind string) DocumentSelector {
apiver := fmt.Sprintf("%s/%s", group, version)
// Remove '/' if group or version is empty
apiver = strings.TrimPrefix(apiver, "/")
apiver = strings.TrimSuffix(apiver, "/")
newFlt := f.ByAPIVersion(apiver)
if kind != "" {
return newFlt.ByKey(yaml.KindField, kind)
}
return newFlt
}
// ByName adds filter by 'metadata.name' field value
func (f DocumentSelector) ByName(name string) DocumentSelector {
if name != "" {
return f.byPath([]string{yaml.MetadataField, yaml.NameField}, name)
}
return f
}
// ByNamespace adds filter by 'metadata.namespace' field value
func (f DocumentSelector) ByNamespace(ns string) DocumentSelector {
if ns != "" {
return f.byPath([]string{yaml.MetadataField, yaml.NamespaceField}, ns)
}
return f
}
// Filter RNode objects
func (f DocumentSelector) Filter(items []*yaml.RNode) (result []*yaml.RNode, err error) {
result = items
for i := range f.filters {
result, err = f.filters[i].Filter(result)
if err != nil {
return nil, err
}
}
return result, nil
}

@ -0,0 +1,155 @@
/*
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
https://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 kyamlutils_test
import (
"bytes"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
"opendev.org/airship/airshipctl/pkg/document/plugin/kyamlutils"
)
func documents(t *testing.T) []*yaml.RNode {
docs := `---
apiVersion: v1
kind: Pod
metadata:
name: p1
namespace: capi
---
apiVersion: v1
kind: Pod
metadata:
name: p2
namespace: capi
---
apiVersion: v1beta1
kind: Deployment
metadata:
name: p1
`
rns, err := (&kio.ByteReader{Reader: bytes.NewBufferString(docs)}).Read()
require.NoError(t, err)
return rns
}
func TestFilter(t *testing.T) {
docs := documents(t)
testCases := []struct {
name string
selector kyamlutils.DocumentSelector
expectedErr error
expectedDocs string
}{
{
name: "Get by GVK + name + namespace",
selector: kyamlutils.DocumentSelector{}.
ByGVK("", "v1", "Pod").
ByName("p1").
ByNamespace("capi"),
expectedDocs: `apiVersion: v1
kind: Pod
metadata:
name: p1
namespace: capi`,
},
{
name: "No filters",
selector: kyamlutils.DocumentSelector{},
expectedDocs: `apiVersion: v1
kind: Pod
metadata:
name: p1
namespace: capi
---
apiVersion: v1
kind: Pod
metadata:
name: p2
namespace: capi
---
apiVersion: v1beta1
kind: Deployment
metadata:
name: p1`,
},
{
name: "Get by apiVersion",
selector: kyamlutils.DocumentSelector{}.ByAPIVersion("v1beta1"),
expectedDocs: `apiVersion: v1beta1
kind: Deployment
metadata:
name: p1`,
},
{
name: "Get by empty name",
selector: kyamlutils.DocumentSelector{}.ByAPIVersion("v1beta1").ByName(""),
expectedDocs: `apiVersion: v1beta1
kind: Deployment
metadata:
name: p1`,
},
{
name: "Get by version only",
selector: kyamlutils.DocumentSelector{}.ByGVK("", "v1", ""),
expectedDocs: `apiVersion: v1
kind: Pod
metadata:
name: p1
namespace: capi
---
apiVersion: v1
kind: Pod
metadata:
name: p2
namespace: capi`,
},
{
name: "Get by kind only",
selector: kyamlutils.DocumentSelector{}.ByGVK("", "", "Deployment"),
expectedDocs: `apiVersion: v1beta1
kind: Deployment
metadata:
name: p1`,
},
{
name: "Get by empty namespace",
selector: kyamlutils.DocumentSelector{}.ByGVK("", "v1beta1", "Deployment").ByNamespace(""),
expectedDocs: `apiVersion: v1beta1
kind: Deployment
metadata:
name: p1`,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
filteredDocs, err := tc.selector.Filter(docs)
assert.Equal(t, err, tc.expectedErr)
buf := &bytes.Buffer{}
err = kio.ByteWriter{Writer: buf}.Write(filteredDocs)
require.NoError(t, err)
assert.Equal(t, tc.expectedDocs, strings.TrimSuffix(buf.String(), "\n"))
})
}
}

@ -16,9 +16,9 @@ package replacement
import (
"fmt"
"reflect"
"strings"
"sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/kustomize/api/types"
)
@ -50,11 +50,11 @@ func (e ErrBadConfiguration) Error() string {
// ErrMultipleResources returned if multiple resources were found
type ErrMultipleResources struct {
ResList []*resource.Resource
ObjRef *types.Target
}
func (e ErrMultipleResources) Error() string {
return fmt.Sprintf("found more than one resources matching from %v", e.ResList)
return fmt.Sprintf("found more than one resources matching identified by %s", printFields(e.ObjRef))
}
// ErrSourceNotFound returned if a replacement source resource does not exist in resource map
@ -63,16 +63,7 @@ type ErrSourceNotFound struct {
}
func (e ErrSourceNotFound) Error() string {
keys := [5]string{"Group:", "Version:", "Kind:", "Name:", "Namespace:"}
values := [5]string{e.ObjRef.Group, e.ObjRef.Version, e.ObjRef.Kind, e.ObjRef.Name, e.ObjRef.Namespace}
var resFilter string
for i, key := range keys {
if values[i] != "" {
resFilter += key + values[i] + " "
}
}
return fmt.Sprintf("failed to find any source resources identified by %s", strings.TrimSpace(resFilter))
return fmt.Sprintf("failed to find any source resources identified by %s", printFields(e.ObjRef))
}
// ErrTargetNotFound returned if a replacement target resource does not exist in the resource map
@ -81,18 +72,7 @@ type ErrTargetNotFound struct {
}
func (e ErrTargetNotFound) Error() string {
keys := [7]string{"Group:", "Version:", "Kind:", "Name:", "Namespace:",
"AnnotationSelector:", "LabelSelector:"}
values := [7]string{e.ObjRef.Group, e.ObjRef.Version, e.ObjRef.Kind, e.ObjRef.Name,
e.ObjRef.Namespace, e.ObjRef.AnnotationSelector, e.ObjRef.LabelSelector}
var resFilter string
for i, key := range keys {
if values[i] != "" {
resFilter += key + values[i] + " "
}
}
return fmt.Sprintf("failed to find any target resources identified by %s", strings.TrimSpace(resFilter))
return fmt.Sprintf("failed to find any target resources identified by %s", printFields(e.ObjRef))
}
// ErrPatternSubstring returned in case of issues with sub-string pattern substitution
@ -114,12 +94,24 @@ func (e ErrIndexOutOfBound) Error() string {
return fmt.Sprintf("array index out of bounds: index %d, length %d", e.Index, e.Length)
}
// ErrMapNotFound returned if map specified in fieldRef option was not found in a list
type ErrMapNotFound struct {
Key, Value, ListKey string
// ErrValueNotFound returned if value specified in fieldRef option was not found
type ErrValueNotFound struct {
ID string
}
func (e ErrMapNotFound) Error() string {
return fmt.Sprintf("unable to find map key '%s' with the value '%s' in list under '%s' key",
e.Key, e.Value, e.ListKey)
func (e ErrValueNotFound) Error() string {
return fmt.Sprintf("unable to find value identified by %s", e.ID)
}
func printFields(objRef interface{}) string {
val := reflect.ValueOf(objRef).Elem()
valType := val.Type()
var res []string
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
if field.String() != "" {
res = append(res, fmt.Sprintf("%s: %v", valType.Field(i).Name, field.Interface()))
}
}
return strings.Join(res, " ")
}

@ -19,8 +19,8 @@ import (
"sigs.k8s.io/kustomize/kyaml/yaml"
airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/document/plugin/kyamlutils"
plugtypes "opendev.org/airship/airshipctl/pkg/document/plugin/types"
"opendev.org/airship/airshipctl/pkg/errors"
)
var (
@ -115,7 +115,130 @@ func (p *plugin) Transform(m resmap.ResMap) error {
}
func (p *plugin) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
return nil, errors.ErrNotImplemented{What: "`Exec` method for replacement transformer"}
for _, r := range p.Replacements {
val, err := getValue(items, r.Source)
if err != nil {
return nil, err
}
if err := replace(items, r.Target, val); err != nil {
return nil, err
}
}
return items, nil
}
func getValue(items []*yaml.RNode, source *types.ReplSource) (*yaml.RNode, error) {
if source.Value != "" {
return yaml.NewScalarRNode(source.Value), nil
}
sources, err := kyamlutils.DocumentSelector{}.
ByAPIVersion(source.ObjRef.APIVersion).
ByGVK(source.ObjRef.Group, source.ObjRef.Version, source.ObjRef.Kind).
ByName(source.ObjRef.Name).
ByNamespace(source.ObjRef.Namespace).
Filter(items)
if err != nil {
return nil, err
}
if len(sources) > 1 {
return nil, ErrMultipleResources{ObjRef: source.ObjRef}
}
if len(sources) == 0 {
return nil, ErrSourceNotFound{ObjRef: source.ObjRef}
}
path := fmt.Sprintf("{.%s.%s}", yaml.MetadataField, yaml.NameField)
if source.FieldRef != "" {
path = source.FieldRef
}
return sources[0].Pipe(kyamlutils.JSONPathFilter{Path: path})
}
func mutateField(rnSource *yaml.RNode) func([]*yaml.RNode) error {
return func(rns []*yaml.RNode) error {
for _, rn := range rns {
rn.SetYNode(rnSource.YNode())
}
return nil
}
}
func replace(items []*yaml.RNode, target *types.ReplTarget, value *yaml.RNode) error {
targets, err := kyamlutils.DocumentSelector{}.
ByGVK(target.ObjRef.Group, target.ObjRef.Version, target.ObjRef.Kind).
ByName(target.ObjRef.Name).
ByNamespace(target.ObjRef.Namespace).
Filter(items)
if err != nil {
return err
}
if len(targets) == 0 {
return ErrTargetNotFound{ObjRef: target.ObjRef}
}
for _, tgt := range targets {
for _, fieldRef := range target.FieldRefs {
// fieldref can contain substring pattern for regexp - we need to get it
groups := substringPatternRegex.FindStringSubmatch(fieldRef)
// if there is no substring pattern
if len(groups) != 3 {
filter := kyamlutils.JSONPathFilter{Path: fieldRef, Mutator: mutateField(value), Create: true}
if _, err := tgt.Pipe(filter); err != nil {
return err
}
continue
}
if err := substituteSubstring(tgt, groups[1], groups[2], value); err != nil {
return err
}
}
}
return nil
}
func substituteSubstring(tgt *yaml.RNode, fieldRef, substringPattern string, value *yaml.RNode) error {
if err := yaml.ErrorIfInvalid(value, yaml.ScalarNode); err != nil {
return err
}
curVal, err := tgt.Pipe(kyamlutils.JSONPathFilter{Path: fieldRef})
if yaml.IsMissingOrError(curVal, err) {
return err
}
switch curVal.YNode().Kind {
case yaml.ScalarNode:
p := regexp.MustCompile(substringPattern)
if !p.MatchString(yaml.GetValue(curVal)) {
return ErrPatternSubstring{
Msg: fmt.Sprintf("pattern '%s' is defined in configuration but was not found in target value %s",
substringPattern, yaml.GetValue(curVal)),
}
}
curVal.YNode().Value = p.ReplaceAllString(yaml.GetValue(curVal), yaml.GetValue(value))
case yaml.SequenceNode:
items, err := curVal.Elements()
if err != nil {
return err
}
for _, item := range items {
if err := yaml.ErrorIfInvalid(item, yaml.ScalarNode); err != nil {
return err
}
p := regexp.MustCompile(substringPattern)
if !p.MatchString(yaml.GetValue(item)) {
return ErrPatternSubstring{
Msg: fmt.Sprintf("pattern '%s' is defined in configuration but was not found in target value %s",
substringPattern, yaml.GetValue(item)),
}
}
item.YNode().Value = p.ReplaceAllString(yaml.GetValue(item), yaml.GetValue(value))
}
default:
return ErrPatternSubstring{Msg: fmt.Sprintf("value identified by %s expected to be string", fieldRef)}
}
return nil
}
func getReplacement(m resmap.ResMap, objRef *types.Target, fieldRef string) (interface{}, error) {
@ -129,7 +252,11 @@ func getReplacement(m resmap.ResMap, objRef *types.Target, fieldRef string) (int
return nil, err
}
if len(resources) > 1 {
return nil, ErrMultipleResources{ResList: resources}
resList := make([]string, len(resources))
for i := range resources {
resList[i] = resources[i].String()
}
return nil, ErrMultipleResources{ObjRef: objRef}
}
if len(resources) == 0 {
return nil, ErrSourceNotFound{ObjRef: objRef}
@ -305,7 +432,7 @@ func updateSliceField(m []interface{}, pathToField []string, replacement interfa
if len(pathToField) == 0 {
return nil
}
path, key, value, isArray := getFirstPathSegment(pathToField[0])
_, key, value, isArray := getFirstPathSegment(pathToField[0])
if isArray {
for _, item := range m {
@ -319,7 +446,7 @@ func updateSliceField(m []interface{}, pathToField []string, replacement interfa
}
}
}
return ErrMapNotFound{Key: key, Value: value, ListKey: path}
return nil
}
index, err := strconv.Atoi(pathToField[0])

@ -5,12 +5,14 @@ package replacement_test
import (
"bytes"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/yaml"
"opendev.org/airship/airshipctl/pkg/document/plugin/replacement"
@ -70,15 +72,14 @@ spec:
assert.Error(t, err)
}
func TestReplacementTransformer(t *testing.T) {
testCases := []struct {
cfg string
in string
expectedOut string
expectedErr string
}{
{
cfg: `
var testCases = []struct {
cfg string
in string
expectedOut string
expectedErr string
}{
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -100,9 +101,9 @@ replacements:
- spec.template.spec.containers.3.image
`,
in: `
group: apps
in: `
apiVersion: v1
group: apps
kind: Deployment
metadata:
name: deploy1
@ -126,7 +127,7 @@ spec:
- image: alpine:1.8.0
name: init-alpine
`,
expectedOut: `apiVersion: v1
expectedOut: `apiVersion: v1
group: apps
kind: Deployment
metadata:
@ -151,9 +152,9 @@ spec:
- image: alpine:1.8.0
name: init-alpine
`,
},
{
cfg: `
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -168,9 +169,9 @@ replacements:
- spec.template.spec.containers[name=nginx-tagged].image%1.7.9%
`,
in: `
group: apps
in: `
apiVersion: v1
group: apps
kind: Deployment
metadata:
name: deploy1
@ -181,7 +182,7 @@ spec:
- image: nginx:1.7.9
name: nginx-tagged
`,
expectedOut: `apiVersion: v1
expectedOut: `apiVersion: v1
group: apps
kind: Deployment
metadata:
@ -193,10 +194,10 @@ spec:
- image: nginx:1.17.0
name: nginx-tagged
`,
},
},
{
cfg: `
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -212,15 +213,15 @@ replacements:
kind: Deployment
fieldrefs:
- spec.template.spec.containers`,
in: `
in: `
apiVersion: v1
kind: Pod
metadata:
name: pod
spec:
containers:
- name: myapp-container
image: busybox
- image: busybox
name: myapp-container
---
apiVersion: apps/v1
kind: Deployment
@ -232,7 +233,7 @@ kind: Deployment
metadata:
name: deploy3
`,
expectedOut: `apiVersion: v1
expectedOut: `apiVersion: v1
kind: Pod
metadata:
name: pod
@ -263,9 +264,9 @@ spec:
- image: busybox
name: myapp-container
`,
},
{
cfg: `
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -293,13 +294,13 @@ replacements:
fieldrefs:
- spec.template.spec.containers[image=debian].args.1
- spec.template.spec.containers[name=busybox].args.2`,
in: `
in: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy
labels:
foo: bar
name: deploy
spec:
template:
metadata:
@ -307,27 +308,28 @@ spec:
foo: bar
spec:
containers:
- name: command-demo-container
image: debian
command: ["printenv"]
args:
- HOSTNAME
- PORT
- name: busybox
image: busybox:latest
args:
- echo
- HOSTNAME
- PORT
- args:
- HOSTNAME
- PORT
command:
- printenv
image: debian
name: command-demo-container
- args:
- echo
- HOSTNAME
- PORT
image: busybox:latest
name: busybox
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cm
data:
HOSTNAME: example.com
PORT: 8080`,
expectedOut: `apiVersion: apps/v1
PORT: 8080
kind: ConfigMap
metadata:
name: cm`,
expectedOut: `apiVersion: apps/v1
kind: Deployment
metadata:
labels:
@ -362,9 +364,9 @@ kind: ConfigMap
metadata:
name: cm
`,
},
{
cfg: `
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -384,9 +386,9 @@ replacements:
kind: Deployment
fieldrefs:
- spec.template.spec.containers.3.image`,
in: `
group: apps
in: `
apiVersion: v1
group: apps
kind: Deployment
metadata:
name: deploy1
@ -409,7 +411,7 @@ spec:
name: nginx-sha256
- image: alpine:1.8.0
name: init-alpine`,
expectedOut: `apiVersion: v1
expectedOut: `apiVersion: v1
group: apps
kind: Deployment
metadata:
@ -434,9 +436,9 @@ spec:
- image: alpine:1.8.0
name: init-alpine
`,
},
{
cfg: `
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -452,15 +454,15 @@ replacements:
name: pod2
fieldrefs:
- spec.non.existent.field`,
in: `
in: `
apiVersion: v1
kind: Pod
metadata:
name: pod1
spec:
containers:
- name: myapp-container
image: busybox
- image: busybox
name: myapp-container
---
apiVersion: v1
kind: Pod
@ -468,9 +470,9 @@ metadata:
name: pod2
spec:
containers:
- name: myapp-container
image: busybox`,
expectedOut: `apiVersion: v1
- image: busybox
name: myapp-container`,
expectedOut: `apiVersion: v1
kind: Pod
metadata:
name: pod1
@ -491,9 +493,9 @@ spec:
existent:
field: pod1
`,
},
{
cfg: `
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -509,15 +511,15 @@ replacements:
kind: Deployment
fieldrefs:
- spec.template.spec.containers[name=myapp-container]`,
in: `
in: `
apiVersion: v1
kind: Pod
metadata:
name: pod
spec:
containers:
- name: repl
image: repl
- image: repl
name: repl
---
apiVersion: apps/v1
kind: Deployment
@ -530,7 +532,7 @@ spec:
- image: busybox
name: myapp-container
`,
expectedOut: `apiVersion: v1
expectedOut: `apiVersion: v1
kind: Pod
metadata:
name: pod
@ -550,9 +552,9 @@ spec:
- image: repl
name: repl
`,
},
{
cfg: `
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -568,15 +570,15 @@ replacements:
kind: Deployment
fieldrefs:
- spec.template.spec.containers[name=myapp-container].image%TAG%`,
in: `
in: `
apiVersion: v1
kind: Pod
metadata:
name: pod
spec:
containers:
- name: repl
image: 12345
- image: 12345
name: repl
---
apiVersion: apps/v1
kind: Deployment
@ -589,7 +591,7 @@ spec:
- image: busybox:TAG
name: myapp-container
`,
expectedOut: `apiVersion: v1
expectedOut: `apiVersion: v1
kind: Pod
metadata:
name: pod
@ -609,9 +611,9 @@ spec:
- image: busybox:12345
name: myapp-container
`,
},
{
cfg: `
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -625,7 +627,7 @@ replacements:
kind: Deployment
fieldrefs:
- spec.template.spec.containers[name=nginx-latest].image`,
in: `
in: `
apiVersion: v1
kind: Pod
metadata:
@ -643,15 +645,10 @@ spec:
containers:
- name: myapp-container
image: busybox`,
expectedErr: "found more than one resources matching from " +
"[{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"name\":\"pod1\"}," +
"\"spec\":{\"containers\":[{\"image\":\"busybox\",\"name\":\"myapp-container\"" +
"}]}}{nsfx:false,beh:unspecified} {\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":" +
"{\"name\":\"pod2\"},\"spec\":{\"containers\":[{\"image\":\"busybox\",\"name\":\"myapp-container\"}]}}" +
"{nsfx:false,beh:unspecified}]",
},
{
cfg: `
expectedErr: "found more than one resources matching identified by Gvk: ~G_~V_Pod",
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -660,17 +657,22 @@ replacements:
- source:
objref:
kind: Pod
name: pod1
name: doesNotExists
namespace: default
target:
objref:
kind: Deployment
fieldrefs:
- spec.template.spec.containers[name=nginx-latest].image`,
expectedErr: "failed to find any source resources identified by Kind:Pod Name:pod1 Namespace:default",
},
{
cfg: `
in: `apiVersion: v1
kind: Pod
metadata:
name: pod1`,
expectedErr: "failed to find any source resources identified by " +
"Gvk: ~G_~V_Pod Name: doesNotExists Namespace: default",
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -685,7 +687,7 @@ replacements:
kind: Deployment
fieldrefs:
- spec.template.spec.containers[name=nginx-latest].image`,
in: `
in: `
apiVersion: v1
kind: Pod
metadata:
@ -694,10 +696,10 @@ spec:
containers:
- name: myapp-container
image: busybox`,
expectedErr: "failed to find any target resources identified by Kind:Deployment",
},
{
cfg: `
expectedErr: "failed to find any target resources identified by Gvk: ~G_~V_Deployment",
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -713,7 +715,7 @@ replacements:
name: pod2
fieldrefs:
- labels.somelabel.key1.subkey1`,
in: `
in: `
apiVersion: v1
kind: Pod
metadata:
@ -733,10 +735,10 @@ spec:
containers:
- name: myapp-container
image: busybox`,
expectedErr: `"some string value" is not expected be a primitive type`,
},
{
cfg: `
expectedErr: `"some string value" is not expected be a primitive type`,
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -752,7 +754,7 @@ replacements:
name: pod2
fieldrefs:
- labels.somelabel[subkey1=val1]`,
in: `
in: `
apiVersion: v1
kind: Pod
metadata:
@ -772,10 +774,10 @@ spec:
containers:
- name: myapp-container
image: busybox`,
expectedErr: `"some string value" is not expected be a primitive type`,
},
{
cfg: `
expectedErr: `"some string value" is not expected be a primitive type`,
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -791,7 +793,7 @@ replacements:
name: pod2
fieldrefs:
- spec[subkey1=val1]`,
in: `
in: `
apiVersion: v1
kind: Pod
metadata:
@ -811,12 +813,12 @@ spec:
containers:
- name: myapp-container
image: busybox`,
expectedErr: "map[string]interface {}{\"containers\":[]interface " +
"{}{map[string]interface {}{\"image\":\"busybox\", \"name\":\"myapp-container\"}}} " +
"is not expected be a primitive type",
},
{
cfg: `
expectedErr: "map[string]interface {}{\"containers\":[]interface " +
"{}{map[string]interface {}{\"image\":\"busybox\", \"name\":\"myapp-container\"}}} " +
"is not expected be a primitive type",
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -832,7 +834,7 @@ replacements:
name: pod2
fieldrefs:
- spec.containers.10`,
in: `
in: `
apiVersion: v1
kind: Pod
metadata:
@ -852,10 +854,10 @@ spec:
containers:
- name: myapp-container
image: busybox`,
expectedErr: "array index out of bounds: index 10, length 1",
},
{
cfg: `
expectedErr: "array index out of bounds: index 10, length 1",
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -871,7 +873,7 @@ replacements:
name: pod2
fieldrefs:
- spec.containers.notInteger.name`,
in: `
in: `
apiVersion: v1
kind: Pod
metadata:
@ -891,10 +893,10 @@ spec:
containers:
- name: myapp-container
image: busybox`,
expectedErr: `strconv.Atoi: parsing "notInteger": invalid syntax`,
},
{
cfg: `
expectedErr: `strconv.Atoi: parsing "notInteger": invalid syntax`,
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -909,7 +911,7 @@ replacements:
kind: Deployment
fieldrefs:
- spec.template.spec.containers%TAG%`,
in: `
in: `
apiVersion: v1
kind: Pod
metadata:
@ -930,10 +932,10 @@ spec:
containers:
- image: nginx:TAG
name: nginx-latest`,
expectedErr: "pattern-based substitution can only be applied to string target fields",
},
{
cfg: `
expectedErr: "pattern-based substitution can only be applied to string target fields",
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -948,7 +950,7 @@ replacements:
kind: Deployment
fieldrefs:
- spec.template.spec.containers[name=nginx-latest].image%TAG%`,
in: `
in: `
apiVersion: v1
kind: Pod
metadata:
@ -969,10 +971,10 @@ spec:
containers:
- image: nginx:latest
name: nginx-latest`,
expectedErr: "pattern 'TAG' is defined in configuration but was not found in target value nginx:latest",
},
{
cfg: `
expectedErr: "pattern 'TAG' is defined in configuration but was not found in target value nginx:latest",
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: ReplacementTransformer
metadata:
@ -984,11 +986,27 @@ replacements:
objref:
kind: KubeadmControlPlane
fieldrefs:
- spec.kubeadmConfigSpec.files[path=konfigadm].content%{k8s-version}%
`,
in: `
- spec.kubeadmConfigSpec.files[path=konfigadm].content%{k8s-version}%`,
in: `
kind: KubeadmControlPlane
metadata:
name: cluster-controlplane
spec:
infrastructureTemplate:
apiVersion: infrastructure.cluster.x-k8s.io/v1alpha3
kind: Metal3MachineTemplate
name: $(cluster-name)
kubeadmConfigSpec:
files:
- content: |
kubernetes:
version: {k8s-version}
container_runtime:
type: docker
owner: root:root
path: konfigadm_bug_
permissions: "0640"`,
expectedOut: `kind: KubeadmControlPlane
metadata:
name: cluster-controlplane
spec:
@ -1007,10 +1025,10 @@ spec:
path: konfigadm_bug_
permissions: "0640"
`,
expectedErr: "unable to find map key 'path' with the value 'konfigadm' in list under 'files' key",
},
}
},
}
func TestReplacementTransformer(t *testing.T) {
for _, tc := range testCases {
cfg := make(map[string]interface{})
err := yaml.Unmarshal([]byte(tc.cfg), &cfg)
@ -1028,3 +1046,46 @@ spec:
assert.Equal(t, tc.expectedOut, buf.String())
}
}
func TestExec(t *testing.T) {
// TODO (dukov) Remove this once we migrate to new kustomize plugin approach
// NOTE (dukov) we need this since error format is different for new kustomize plugins
testCases[11].expectedErr = "wrong Node Kind for labels.somelabel expected: " +
"MappingNode was ScalarNode: value: {'some string value'}"
testCases[12].expectedErr = "wrong Node Kind for labels.somelabel expected: " +
"SequenceNode was ScalarNode: value: {'some string value'}"
testCases[13].expectedErr = "wrong Node Kind for spec expected: " +
"SequenceNode was MappingNode: value: {containers:\n- name: myapp-container\n image: busybox}"
testCases[15].expectedErr = "wrong Node Kind for spec.containers expected: " +
"MappingNode was SequenceNode: value: {- name: myapp-container\n image: busybox}"
testCases[16].expectedErr = "wrong Node Kind for expected: " +
"ScalarNode was MappingNode: value: {image: nginx:TAG\nname: nginx-latest}"
for i, tc := range testCases {
tc := tc
t.Run(fmt.Sprintf("Test Case %d", i), func(t *testing.T) {
cfg := make(map[string]interface{})
err := yaml.Unmarshal([]byte(tc.cfg), &cfg)
require.NoError(t, err)
plugin, err := replacement.New(cfg)
require.NoError(t, err)
buf := &bytes.Buffer{}
p := kio.Pipeline{
Inputs: []kio.Reader{&kio.ByteReader{Reader: bytes.NewBufferString(tc.in)}},
Filters: []kio.Filter{plugin},
Outputs: []kio.Writer{kio.ByteWriter{Writer: buf}},
}
err = p.Execute()
errString := ""
if err != nil {
errString = err.Error()
}
assert.Equal(t, tc.expectedErr, errString)
assert.Equal(t, tc.expectedOut, buf.String())
})
}
}