commit ca1a3a2d0b8bbb667f3f754cc0e3e5276d5e0a40 Author: Dmitry Ukov Date: Wed Sep 23 18:02:59 2020 +0400 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 diff --git a/pkg/document/plugin/kyamlutils/document_selector.go b/pkg/document/plugin/kyamlutils/document_selector.go new file mode 100644 index 0000000..a99450c --- /dev/null +++ b/pkg/document/plugin/kyamlutils/document_selector.go @@ -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 +} diff --git a/pkg/document/plugin/kyamlutils/document_selector_test.go b/pkg/document/plugin/kyamlutils/document_selector_test.go new file mode 100644 index 0000000..a5ff540 --- /dev/null +++ b/pkg/document/plugin/kyamlutils/document_selector_test.go @@ -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")) + }) + } +} diff --git a/pkg/document/plugin/replacement/errors.go b/pkg/document/plugin/replacement/errors.go index 5fd9942..7dfd8b4 100644 --- a/pkg/document/plugin/replacement/errors.go +++ b/pkg/document/plugin/replacement/errors.go @@ -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 ErrValueNotFound) Error() string { + return fmt.Sprintf("unable to find value identified by %s", e.ID) } -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 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, " ") } diff --git a/pkg/document/plugin/replacement/transformer.go b/pkg/document/plugin/replacement/transformer.go index 4d89a39..cbaa0ef 100644 --- a/pkg/document/plugin/replacement/transformer.go +++ b/pkg/document/plugin/replacement/transformer.go @@ -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]) diff --git a/pkg/document/plugin/replacement/transformer_test.go b/pkg/document/plugin/replacement/transformer_test.go index 92184a1..d799680 100644 --- a/pkg/document/plugin/replacement/transformer_test.go +++ b/pkg/document/plugin/replacement/transformer_test.go @@ -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,10 +986,8 @@ 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 @@ -1005,12 +1005,30 @@ spec: type: docker owner: root:root path: konfigadm_bug_ + permissions: "0640"`, + expectedOut: `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" `, - 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()) + }) + } +}