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

View File

@ -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
}

View File

@ -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"))
})
}
}

View File

@ -16,9 +16,9 @@ package replacement
import ( import (
"fmt" "fmt"
"reflect"
"strings" "strings"
"sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
) )
@ -50,11 +50,11 @@ func (e ErrBadConfiguration) Error() string {
// ErrMultipleResources returned if multiple resources were found // ErrMultipleResources returned if multiple resources were found
type ErrMultipleResources struct { type ErrMultipleResources struct {
ResList []*resource.Resource ObjRef *types.Target
} }
func (e ErrMultipleResources) Error() string { 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 // 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 { func (e ErrSourceNotFound) Error() string {
keys := [5]string{"Group:", "Version:", "Kind:", "Name:", "Namespace:"} return fmt.Sprintf("failed to find any source resources identified by %s", printFields(e.ObjRef))
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))
} }
// ErrTargetNotFound returned if a replacement target resource does not exist in the resource map // 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 { func (e ErrTargetNotFound) Error() string {
keys := [7]string{"Group:", "Version:", "Kind:", "Name:", "Namespace:", return fmt.Sprintf("failed to find any target resources identified by %s", printFields(e.ObjRef))
"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))
} }
// ErrPatternSubstring returned in case of issues with sub-string pattern substitution // 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) 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 // ErrValueNotFound returned if value specified in fieldRef option was not found
type ErrMapNotFound struct { type ErrValueNotFound struct {
Key, Value, ListKey string ID string
} }
func (e ErrMapNotFound) Error() string { func (e ErrValueNotFound) Error() string {
return fmt.Sprintf("unable to find map key '%s' with the value '%s' in list under '%s' key", return fmt.Sprintf("unable to find value identified by %s", e.ID)
e.Key, e.Value, e.ListKey) }
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, " ")
} }

View File

@ -19,8 +19,8 @@ import (
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1" 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" plugtypes "opendev.org/airship/airshipctl/pkg/document/plugin/types"
"opendev.org/airship/airshipctl/pkg/errors"
) )
var ( var (
@ -115,7 +115,130 @@ func (p *plugin) Transform(m resmap.ResMap) error {
} }
func (p *plugin) Filter(items []*yaml.RNode) ([]*yaml.RNode, 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) { 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 return nil, err
} }
if len(resources) > 1 { 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 { if len(resources) == 0 {
return nil, ErrSourceNotFound{ObjRef: objRef} return nil, ErrSourceNotFound{ObjRef: objRef}
@ -305,7 +432,7 @@ func updateSliceField(m []interface{}, pathToField []string, replacement interfa
if len(pathToField) == 0 { if len(pathToField) == 0 {
return nil return nil
} }
path, key, value, isArray := getFirstPathSegment(pathToField[0]) _, key, value, isArray := getFirstPathSegment(pathToField[0])
if isArray { if isArray {
for _, item := range m { 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]) index, err := strconv.Atoi(pathToField[0])

View File

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