From e57dcf53d8144ff0b2019a664340c3b6424def5a Mon Sep 17 00:00:00 2001 From: Antoine Martin Date: Tue, 24 Jan 2023 10:55:47 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20extended=20ReplacementTransfo?= =?UTF-8?q?rmer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This transformer works the same as the builtin one that it replaces but adds extended path specifications based on the _lenses_ of knot8. The typical use case for this is when you want to replace a value in the `values` field of an application: ```yaml values: | uninode: true apps: enabled: true common: targetRevision: main repoURL: https://github.com/anotherproject/anothergit ``` This commit allows you to write: ```yaml fieldPaths: ... - spec.source.helm.values.!!yaml.common.repoURL ``` Note the double exclamation point (!!) before `yaml`. This tells _change the `common.repoURL` value of the `spec.source.helm.values` property that is encoded in YAML. Apart from `yaml`, this commit add two other encodings: - `base64`, to decode/encode base64 based values. - `regex` to allow regexp based changes. Paths for this encoding have two elements. The first one is the regexp to match and the second one is the matching group index to replace. Example: ^\s+HostName\s+(\S+)\s*$.1 It means: Look for a line with `HostName` followed by a word and replace this word (matching group 1) with the value passed. [knot8]: https://github.com/mkmik/knot8 --- go.mod | 6 +- go.sum | 11 +- pkg/extras/extender.go | 421 ++++++++++++++++++ pkg/extras/extender_test.go | 278 ++++++++++++ pkg/extras/extendertype_string.go | 26 ++ pkg/extras/replacement.go | 401 +++++++++++++++++ pkg/plugins/factories.go | 2 +- .../expected/argocd.yaml} | 0 .../functions/patch-transformer.yaml | 2 +- .../original}/argocd.yaml | 0 tests/replacement/expected/argocd.yaml | 41 ++ .../functions}/01_configmap-generator.yaml | 2 +- .../02_replacement-transformer.yaml | 4 +- .../original/argocd.yaml} | 7 + tests/test_krmfnbuiltin.sh | 21 +- 15 files changed, 1205 insertions(+), 17 deletions(-) create mode 100644 pkg/extras/extender.go create mode 100644 pkg/extras/extender_test.go create mode 100644 pkg/extras/extendertype_string.go create mode 100644 pkg/extras/replacement.go rename tests/{compare/argocd.expected.yaml => patch/expected/argocd.yaml} (100%) rename tests/{ => patch}/functions/patch-transformer.yaml (95%) rename tests/{applications => patch/original}/argocd.yaml (100%) create mode 100644 tests/replacement/expected/argocd.yaml rename tests/{functions2 => replacement/functions}/01_configmap-generator.yaml (92%) rename tests/{functions2 => replacement/functions}/02_replacement-transformer.yaml (84%) rename tests/{compare/argocd.original.yaml => replacement/original/argocd.yaml} (80%) diff --git a/go.mod b/go.mod index 333d365..1b0fb6c 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,14 @@ go 1.18 require ( github.com/go-git/go-git/v5 v5.5.2 + github.com/lithammer/dedent v1.1.0 + github.com/stretchr/testify v1.8.1 + golang.org/x/text v0.6.0 golang.org/x/tools v0.5.0 sigs.k8s.io/kustomize/api v0.12.1 sigs.k8s.io/kustomize/kyaml v0.13.10 sigs.k8s.io/yaml v1.2.0 + ) require ( @@ -39,6 +43,7 @@ require ( github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/pjbgf/sha1cd v0.2.3 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect github.com/skeema/knownhosts v1.1.0 // indirect github.com/spf13/cobra v1.4.0 // indirect @@ -49,7 +54,6 @@ require ( golang.org/x/mod v0.7.0 // indirect golang.org/x/net v0.5.0 // indirect golang.org/x/sys v0.4.0 // indirect - golang.org/x/text v0.6.0 // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 3c91dce..836e3d7 100644 --- a/go.sum +++ b/go.sum @@ -110,6 +110,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -147,13 +149,18 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= diff --git a/pkg/extras/extender.go b/pkg/extras/extender.go new file mode 100644 index 0000000..abb9851 --- /dev/null +++ b/pkg/extras/extender.go @@ -0,0 +1,421 @@ +package extras + +import ( + "bytes" + "encoding/base64" + "fmt" + "regexp" + "strconv" + "strings" + + "golang.org/x/text/cases" + "golang.org/x/text/language" + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +type Extender interface { + SetPayload(payload []byte) error + GetPayload() ([]byte, error) + Get(path []string) ([]byte, error) + Set(path []string, value any) error +} + +type ExtendedSegment struct { + Encoding string + Path []string +} + +func (e *ExtendedSegment) String() string { + if len(e.Path) > 0 { + return fmt.Sprintf("!!%s", e.Encoding) + } else { + return fmt.Sprintf("!!%s.%s", e.Encoding, strings.Join(e.Path, ".")) + } +} + +type any interface{} + +//go:generate go run golang.org/x/tools/cmd/stringer -type=ExtenderType +type ExtenderType int + +const ( + Unknown ExtenderType = iota + YamlExtender + Base64Extender + RegexExtender +) + +var stringToExtenderTypeMap map[string]ExtenderType + +func init() { //nolint:gochecknoinits + stringToExtenderTypeMap = makeStringToExtenderTypeMap() +} + +func GetByteValue(value any) []byte { + switch v := value.(type) { + case *yaml.Node: + return []byte(v.Value) + case []byte: + return v + case string: + return []byte(v) + } + return []byte{} +} + +func makeStringToExtenderTypeMap() (result map[string]ExtenderType) { + result = make(map[string]ExtenderType, 3) + for k := range ExtenderFactories { + result[k.String()] = k + } + return +} + +func GetExtenderType(n string) ExtenderType { + result, ok := stringToExtenderTypeMap[n] + if ok { + return result + } + return Unknown +} + +//////////////// +// YAML Extender +//////////////// + +type yamlExtender struct { + node *yaml.RNode +} + +func (e *yamlExtender) SetPayload(payload []byte) error { + + nodes, err := (&kio.ByteReader{ + Reader: bytes.NewBuffer(payload), + OmitReaderAnnotations: false, + PreserveSeqIndent: true, + WrapBareSeqNode: true, + }).Read() + + if err != nil { + return errors.WrapPrefixf(err, "while reading yaml payload") + } + e.node = nodes[0] + return nil +} + +func (e *yamlExtender) GetPayload() ([]byte, error) { + var b bytes.Buffer + err := (&kio.ByteWriter{Writer: &b}).Write([]*yaml.RNode{e.node}) + return b.Bytes(), err +} + +func (e *yamlExtender) LookupNode() *yaml.RNode { + seqNode, err := e.node.Pipe(yaml.Lookup(yaml.BareSeqNodeWrappingKey)) + if err == nil && !seqNode.IsNilOrEmpty() { + return seqNode + } + return e.node +} + +func (e *yamlExtender) Lookup(path []string) ([]*yaml.RNode, error) { + node, err := e.LookupNode().Pipe(&yaml.PathMatcher{Path: path}) + if err != nil { + return nil, errors.WrapPrefixf(err, "while getting path %s", strings.Join(path, ".")) + } + + return node.Elements() +} + +func (e *yamlExtender) Get(path []string) ([]byte, error) { + targetFields, err := e.Lookup(path) + if err != nil { + return nil, fmt.Errorf("error fetching elements in replacement target: %w", err) + } + + if len(targetFields) == 1 && targetFields[0].YNode().Kind == yaml.ScalarNode { + return []byte(targetFields[0].YNode().Value), nil + } + + var b bytes.Buffer + err = (&kio.ByteWriter{Writer: &b}).Write(targetFields) + if err != nil { + return nil, errors.WrapPrefixf(err, "while serializing path %s", strings.Join(path, ".")) + } + return b.Bytes(), err +} + +func (e *yamlExtender) Set(path []string, value any) error { + + targetFields, err := e.Lookup(path) + if err != nil { + return fmt.Errorf("error fetching elements in replacement target: %w", err) + } + + for _, t := range targetFields { + if t.YNode().Kind == yaml.ScalarNode { + t.YNode().Value = string(GetByteValue(value)) + } else { + if v, ok := value.(*yaml.Node); ok { + t.SetYNode(v) + } else { + return fmt.Errorf("setting non yaml object in place of object of type %s at path %s", t.YNode().Tag, strings.Join(path, ".")) + } + } + } + return nil +} + +func NewYamlExtender() Extender { + return &yamlExtender{} +} + +///////// +// Base64 +///////// + +type base64Extender struct { + decoded []byte +} + +func (e *base64Extender) SetPayload(payload []byte) error { + decoded, err := base64.StdEncoding.DecodeString(string(payload)) + if err != nil { + return errors.WrapPrefixf(err, "while decoding base64") + } + e.decoded = decoded + return nil +} + +func (e *base64Extender) GetPayload() ([]byte, error) { + return e.decoded, nil +} + +func (e *base64Extender) Get(path []string) ([]byte, error) { + if len(path) > 0 { + return nil, fmt.Errorf("path is invalid for base64: %s", strings.Join(path, ".")) + } + return e.decoded, nil +} + +func (e *base64Extender) Set(path []string, value any) error { + if len(path) > 0 { + return fmt.Errorf("path is invalid for base64: %s", strings.Join(path, ".")) + } + e.decoded = []byte(base64.StdEncoding.EncodeToString(GetByteValue(value))) + return nil +} + +func NewBase64Extender() Extender { + return &base64Extender{} +} + +///////// +// Regex +//////// + +type regexExtender struct { + text []byte +} + +func (e *regexExtender) SetPayload(payload []byte) error { + e.text = payload + return nil +} + +func (e *regexExtender) GetPayload() ([]byte, error) { + return []byte(e.text), nil +} + +func (e *regexExtender) Get(path []string) ([]byte, error) { + if len(path) < 1 { + return nil, fmt.Errorf("path for regex should at least be one") + } + re, err := regexp.Compile(path[0]) + if err != nil { + return nil, fmt.Errorf("bad regex %s", path[0]) + } + return re.Find(e.text), nil +} + +func (e *regexExtender) Set(path []string, value any) error { + if len(path) != 2 { + return fmt.Errorf("path for regex should at least be one") + } + re, err := regexp.Compile("(?m)" + path[0]) + if err != nil { + return fmt.Errorf("bad regex %s", path[0]) + } + + group, err := strconv.Atoi(path[1]) + if err != nil { + return fmt.Errorf("bad capturing group") + } + + var b bytes.Buffer + start := 0 + matched := false + + for _, v := range re.FindAllSubmatchIndex(e.text, -1) { + matched = true + startIndex := group * 2 + + b.Write(e.text[start:v[startIndex]]) + b.Write(GetByteValue(value)) + start = v[startIndex+1] + } + + if matched { + if start < len(e.text) { + b.Write(e.text[start:len(e.text)]) + } + e.text = b.Bytes() + } + + return nil +} + +func NewRegexExtender() Extender { + return ®exExtender{} +} + +//////////// +// Factories +//////////// + +var ExtenderFactories = map[ExtenderType]func() Extender{ + YamlExtender: NewYamlExtender, + Base64Extender: NewBase64Extender, + RegexExtender: NewRegexExtender, +} + +func (path *ExtendedSegment) Extender(payload []byte) (Extender, error) { + bpt := GetExtenderType(cases.Title(language.English, cases.NoLower).String(path.Encoding) + "Extender") + if f, ok := ExtenderFactories[bpt]; ok { + result := f() + if err := result.SetPayload(payload); err != nil { + return nil, err + } + + return result, nil + } + return nil, errors.Errorf("unable to load extender %s", path.Encoding) +} + +/////////////// +// ExtendedPath +/////////////// + +func splitExtendedPath(path []string, extensions *[]*ExtendedSegment) (basePath []string, err error) { + + if len(path) == 0 { + return + } + + for i, p := range path { + if strings.HasPrefix(p, "!!") { + extension := ExtendedSegment{Encoding: p[2:]} + if extension.Encoding == "" { + err = fmt.Errorf("extension cannot be empty") + return + } + *extensions = append(*extensions, &extension) + var remainder []string + remainder, err = splitExtendedPath(path[i+1:], extensions) + if err != nil { + err = errors.WrapPrefixf(err, "while getting subpath of extension %s", extension.Encoding) + return + } + extension.Path = remainder + return + } else { + basePath = append(basePath, p) + } + } + return +} + +type ExtendedPath struct { + resourcePath []string + extendedSegments *[]*ExtendedSegment +} + +func NewExtendedPath(path []string) (*ExtendedPath, error) { + extensions := []*ExtendedSegment{} + prefix, err := splitExtendedPath(path, &extensions) + if err != nil { + return nil, errors.WrapPrefixf(err, "while getting extended path") + } + + return &ExtendedPath{resourcePath: prefix, extendedSegments: &extensions}, nil +} + +func (ep *ExtendedPath) HasExtensions() bool { + return len(*ep.extendedSegments) > 0 +} + +func (ep *ExtendedPath) String() string { + out := strings.Join(ep.resourcePath, ".") + if len(*ep.extendedSegments) > 0 { + segmentStrings := []string{} + for _, s := range *ep.extendedSegments { + segmentStrings = append(segmentStrings, s.String()) + } + out = fmt.Sprintf("%s.%s", out, strings.Join(segmentStrings, ".")) + } + return out +} + +func (ep *ExtendedPath) ApplyIndex(index int, input []byte, value *yaml.Node) ([]byte, error) { + if index >= len(*ep.extendedSegments) || index < 0 { + return nil, fmt.Errorf("invalid extended path index: %d", index) + } + + segment := (*ep.extendedSegments)[index] + extender, err := segment.Extender(input) + if err != nil { + return nil, errors.WrapPrefixf(err, "creating extender at index: %d", index) + } + + if index == len(*ep.extendedSegments)-1 { + err := extender.Set(segment.Path, value) + if err != nil { + return nil, errors.WrapPrefixf(err, "setting value on path %s", segment.String()) + } + } else { + nextInput, err := extender.Get(segment.Path) + if err != nil { + return nil, errors.WrapPrefixf(err, "getting value on path %s", segment.String()) + } + newValue, err := ep.ApplyIndex(index+1, nextInput, value) + if err != nil { + return nil, err + } + + err = extender.Set(segment.Path, newValue) + if err != nil { + return nil, errors.WrapPrefixf(err, "setting value on path %s", segment.String()) + } + } + return extender.GetPayload() +} + +func (ep *ExtendedPath) Apply(target *yaml.RNode, value *yaml.RNode) error { + if target.YNode().Kind != yaml.ScalarNode { + return fmt.Errorf("extended path only works on scalar nodes") + } + + outValue := value.YNode().Value + if len(*ep.extendedSegments) > 0 { + input := []byte(target.YNode().Value) + output, err := ep.ApplyIndex(0, input, value.YNode()) + if err != nil { + return errors.WrapPrefixf(err, "applying value on extended segment %s", ep.String()) + } + + outValue = string(output) + } + target.YNode().Value = outValue + return nil +} diff --git a/pkg/extras/extender_test.go b/pkg/extras/extender_test.go new file mode 100644 index 0000000..f479a8f --- /dev/null +++ b/pkg/extras/extender_test.go @@ -0,0 +1,278 @@ +package extras + +import ( + "bytes" + "testing" + + "github.com/lithammer/dedent" + "github.com/stretchr/testify/suite" + "sigs.k8s.io/kustomize/kyaml/kio" + kyaml_utils "sigs.k8s.io/kustomize/kyaml/utils" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +type ExtenderTestSuite struct { + suite.Suite +} + +func (s *ExtenderTestSuite) SetupTest() { +} + +func (s *ExtenderTestSuite) TeardownTest() { +} + +func (s *ExtenderTestSuite) TestSplitPath() { + require := s.Require() + p := "toto.tata.!!yaml.toto.tata" + path := kyaml_utils.SmarterPathSplitter(p, ".") + + extensions := []*ExtendedSegment{} + remainder, err := splitExtendedPath(path, &extensions) + + require.NoError(err) + require.Len(remainder, 2, "Remainder path should be 2") + require.Len(extensions, 1, "Should only have one extension") + require.Equal("yaml", extensions[0].Encoding, "Extension should be yaml") + require.Len(extensions[0].Path, 2, "Extension path len should be 2") +} + +func (s *ExtenderTestSuite) TestRegexExtender() { + text := dedent.Dedent(` + PubkeyAcceptedKeyTypes +ssh-rsa + Host sishserver + HostName holepunch.in + Port 2222 + BatchMode yes + IdentityFile ~/.ssh_keys/id_rsa + IdentitiesOnly yes + LogLevel ERROR + ServerAliveInterval 10 + ServerAliveCountMax 2 + RemoteCommand sni-proxy=true + RemoteForward citest.holepunch.in:443 traefik.traefik.svc:443 + `) + expected := dedent.Dedent(` + PubkeyAcceptedKeyTypes +ssh-rsa + Host sishserver + HostName kaweezle.com + Port 2222 + BatchMode yes + IdentityFile ~/.ssh_keys/id_rsa + IdentitiesOnly yes + LogLevel ERROR + ServerAliveInterval 10 + ServerAliveCountMax 2 + RemoteCommand sni-proxy=true + RemoteForward citest.holepunch.in:443 traefik.traefik.svc:443 + `) + require := s.Require() + path := &ExtendedSegment{ + Encoding: "regex", + Path: []string{`^\s+HostName\s+(\S+)\s*$`, `1`}, + } + + extender, err := path.Extender([]byte(text)) + require.NoError(err) + require.NotNil(extender) + + require.NoError(extender.Set(path.Path, []byte("kaweezle.com"))) + + out, err := extender.GetPayload() + require.NoError(err) + require.Equal(expected, string(out), "Text should be modified") +} + +func (s *ExtenderTestSuite) TestBase64Extender() { + encoded := "UHVia2V5QWNjZXB0ZWRLZXlUeXBlcyArc3NoLXJzYQpIb3N0IHNpc2hzZXJ2ZXIKICBIb3N0TmFtZSBob2xlcHVuY2guaW4KICBQb3J0IDIyMjIKICBCYXRjaE1vZGUgeWVzCiAgSWRlbnRpdHlGaWxlIH4vLnNzaF9rZXlzL2lkX3JzYQogIElkZW50aXRpZXNPbmx5IHllcwogIExvZ0xldmVsIEVSUk9SCiAgU2VydmVyQWxpdmVJbnRlcnZhbCAxMAogIFNlcnZlckFsaXZlQ291bnRNYXggMgogIFJlbW90ZUNvbW1hbmQgc25pLXByb3h5PXRydWUKICBSZW1vdGVGb3J3YXJkIGNpdGVzdC5ob2xlcHVuY2guaW46NDQzIHRyYWVmaWsudHJhZWZpay5zdmM6NDQzCg==" + decodedExpected := dedent.Dedent(` + PubkeyAcceptedKeyTypes +ssh-rsa + Host sishserver + HostName holepunch.in + Port 2222 + BatchMode yes + IdentityFile ~/.ssh_keys/id_rsa + IdentitiesOnly yes + LogLevel ERROR + ServerAliveInterval 10 + ServerAliveCountMax 2 + RemoteCommand sni-proxy=true + RemoteForward citest.holepunch.in:443 traefik.traefik.svc:443 + `)[1:] + + modifiedEncoded := "UHVia2V5QWNjZXB0ZWRLZXlUeXBlcyArc3NoLXJzYQpIb3N0IHNpc2hzZXJ2ZXIKICBIb3N0TmFtZSBrYXdlZXpsZS5jb20KICBQb3J0IDIyMjIKICBCYXRjaE1vZGUgeWVzCiAgSWRlbnRpdHlGaWxlIH4vLnNzaF9rZXlzL2lkX3JzYQogIElkZW50aXRpZXNPbmx5IHllcwogIExvZ0xldmVsIEVSUk9SCiAgU2VydmVyQWxpdmVJbnRlcnZhbCAxMAogIFNlcnZlckFsaXZlQ291bnRNYXggMgogIFJlbW90ZUNvbW1hbmQgc25pLXByb3h5PXRydWUKICBSZW1vdGVGb3J3YXJkIGNpdGVzdC5ob2xlcHVuY2guaW46NDQzIHRyYWVmaWsudHJhZWZpay5zdmM6NDQzCg==" + + require := s.Require() + + p := `!!base64.!!regex.\s+HostName\s+(\S+).1` + path := kyaml_utils.SmarterPathSplitter(p, ".") + + extensions := []*ExtendedSegment{} + prefix, err := splitExtendedPath(path, &extensions) + require.NoError(err) + require.Len(prefix, 0, "There should be no prefix") + require.Len(extensions, 2, "There should be 2 extensions") + require.Equal("base64", extensions[0].Encoding, "The first extension should be base64") + + b64Ext := extensions[0] + b64Extender, err := b64Ext.Extender([]byte(encoded)) + require.NoError(err) + require.IsType(&base64Extender{}, b64Extender, "Should be a base64 extender") + + decoded, err := b64Extender.Get(b64Ext.Path) + require.NoError(err) + require.Equal(decodedExpected, string(decoded), "bad base64 decoding") + + regexExt := extensions[1] + reExtender, err := regexExt.Extender(decoded) + require.NoError(err) + require.IsType(®exExtender{}, reExtender, "Should be a regex extender") + + require.NoError(reExtender.Set(regexExt.Path, []byte("kaweezle.com"))) + modified, err := reExtender.GetPayload() + require.NoError(err) + require.NoError(b64Extender.Set(b64Ext.Path, modified)) + final, err := b64Extender.GetPayload() + require.NoError(err) + require.Equal(modifiedEncoded, string(final), "final base64 is bad") +} + +func (s *ExtenderTestSuite) TestYamlExtender() { + require := s.Require() + source := dedent.Dedent(` + uninode: true + common: + targetRevision: main + apps: + enabled: true + `)[1:] + expected := dedent.Dedent(` + uninode: true + common: + targetRevision: deploy/citest + apps: + enabled: true + `)[1:] + + p := `!!yaml.common.targetRevision` + path := kyaml_utils.SmarterPathSplitter(p, ".") + + extensions := []*ExtendedSegment{} + prefix, err := splitExtendedPath(path, &extensions) + require.NoError(err) + require.Len(prefix, 0, "There should be no prefix") + require.Len(extensions, 1, "There should be 2 extensions") + require.Equal("yaml", extensions[0].Encoding, "The first extension should be base64") + + yamlXP := extensions[0] + yamlExt, err := yamlXP.Extender([]byte(source)) + require.NoError(err) + value, err := yamlExt.Get(yamlXP.Path) + require.NoError(err) + require.Equal("main", string(value), "error fetching value") + require.NoError(yamlExt.Set(yamlXP.Path, []byte("deploy/citest"))) + + modified, err := yamlExt.GetPayload() + require.NoError(err) + require.Equal(expected, string(modified), "final yaml") + + value, err = yamlExt.Get(yamlXP.Path) + require.NoError(err) + require.Equal("deploy/citest", string(value), "error fetching changed value") +} + +func (s *ExtenderTestSuite) TestYamlExtenderWithSequence() { + require := s.Require() + source := dedent.Dedent(` + - name: common.targetRevision + value: main + - name: common.repoURL + value: https://github.com/antoinemartin/autocloud.git + `)[1:] + expected := dedent.Dedent(` + - name: common.targetRevision + value: deploy/citest + - name: common.repoURL + value: https://github.com/antoinemartin/autocloud.git + `)[1:] + + p := `!!yaml.[name=common.targetRevision].value` + path := kyaml_utils.SmarterPathSplitter(p, ".") + + extensions := []*ExtendedSegment{} + prefix, err := splitExtendedPath(path, &extensions) + require.NoError(err) + require.Len(prefix, 0, "There should be no prefix") + require.Len(extensions, 1, "There should be 2 extensions") + require.Equal("yaml", extensions[0].Encoding, "The first extension should be base64") + + yamlXP := extensions[0] + yamlExt, err := yamlXP.Extender([]byte(source)) + require.NoError(err) + require.NoError(yamlExt.Set(yamlXP.Path, []byte("deploy/citest"))) + + modified, err := yamlExt.GetPayload() + require.NoError(err) + require.Equal(expected, string(modified), "final yaml") +} + +func (s *ExtenderTestSuite) TestYamlExtenderWithYaml() { + require := s.Require() + sources, err := (&kio.ByteReader{Reader: bytes.NewBufferString(` +common: | + uninode: true + common: + targetRevision: main + apps: + enabled: true +`)}).Read() + require.NoError(err) + require.Len(sources, 1) + source := sources[0] + + expected := dedent.Dedent(` + common: | + uninode: true + common: + targetRevision: deploy/citest + repoURL: https://github.com/antoinemartin/autocloud.git + apps: + enabled: true + `)[1:] + + replacements, err := (&kio.ByteReader{Reader: bytes.NewBufferString(` +common: + targetRevision: deploy/citest + repoURL: https://github.com/antoinemartin/autocloud.git +`)}).Read() + require.NoError(err) + require.Len(replacements, 1) + replacement := replacements[0] + + p := `common.!!yaml.common` + path := kyaml_utils.SmarterPathSplitter(p, ".") + e, err := NewExtendedPath(path) + require.NoError(err) + require.Len(e.resourcePath, 1, "no resource path") + + sourcePath := []string{"common"} + + target, err := source.Pipe(&yaml.PathGetter{Path: e.resourcePath}) + require.NoError(err) + + value, err := replacement.Pipe(&yaml.PathGetter{Path: sourcePath}) + require.NoError(err) + err = e.Apply(target, value) + require.NoError(err) + + var b bytes.Buffer + err = (&kio.ByteWriter{Writer: &b}).Write(sources) + require.NoError(err) + + sString, err := source.String() + require.NoError(err) + require.Equal(expected, b.String(), sString, "replacement failed") +} + +func TestExtender(t *testing.T) { + suite.Run(t, new(ExtenderTestSuite)) +} diff --git a/pkg/extras/extendertype_string.go b/pkg/extras/extendertype_string.go new file mode 100644 index 0000000..a613560 --- /dev/null +++ b/pkg/extras/extendertype_string.go @@ -0,0 +1,26 @@ +// Code generated by "stringer -type=ExtenderType"; DO NOT EDIT. + +package extras + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Unknown-0] + _ = x[YamlExtender-1] + _ = x[Base64Extender-2] + _ = x[RegexExtender-3] +} + +const _ExtenderType_name = "UnknownYamlExtenderBase64ExtenderRegexExtender" + +var _ExtenderType_index = [...]uint8{0, 7, 19, 33, 46} + +func (i ExtenderType) String() string { + if i < 0 || i >= ExtenderType(len(_ExtenderType_index)-1) { + return "ExtenderType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _ExtenderType_name[_ExtenderType_index[i]:_ExtenderType_index[i+1]] +} diff --git a/pkg/extras/replacement.go b/pkg/extras/replacement.go new file mode 100644 index 0000000..578a87c --- /dev/null +++ b/pkg/extras/replacement.go @@ -0,0 +1,401 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package extras + +import ( + "errors" + "fmt" + "reflect" + "strings" + + "sigs.k8s.io/kustomize/api/konfig" + "sigs.k8s.io/kustomize/api/resmap" + "sigs.k8s.io/kustomize/api/resource" + "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kyaml/resid" + kyaml_utils "sigs.k8s.io/kustomize/kyaml/utils" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +type Filter struct { + Replacements []types.Replacement `json:"replacements,omitempty" yaml:"replacements,omitempty"` +} + +// Filter replaces values of targets with values from sources +func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { + for i, r := range f.Replacements { + if r.Source == nil || r.Targets == nil { + return nil, fmt.Errorf("replacements must specify a source and at least one target") + } + value, err := getReplacement(nodes, &f.Replacements[i]) + if err != nil { + return nil, err + } + nodes, err = applyReplacement(nodes, value, r.Targets) + if err != nil { + return nil, err + } + } + return nodes, nil +} + +func getReplacement(nodes []*yaml.RNode, r *types.Replacement) (*yaml.RNode, error) { + source, err := selectSourceNode(nodes, r.Source) + if err != nil { + return nil, err + } + + if r.Source.FieldPath == "" { + r.Source.FieldPath = types.DefaultReplacementFieldPath + } + fieldPath := kyaml_utils.SmarterPathSplitter(r.Source.FieldPath, ".") + + rn, err := source.Pipe(yaml.Lookup(fieldPath...)) + if err != nil { + return nil, fmt.Errorf("error looking up replacement source: %w", err) + } + if rn.IsNilOrEmpty() { + return nil, fmt.Errorf("fieldPath `%s` is missing for replacement source %s", r.Source.FieldPath, r.Source.ResId) + } + + return getRefinedValue(r.Source.Options, rn) +} + +// selectSourceNode finds the node that matches the selector, returning +// an error if multiple or none are found +func selectSourceNode(nodes []*yaml.RNode, selector *types.SourceSelector) (*yaml.RNode, error) { + var matches []*yaml.RNode + for _, n := range nodes { + ids, err := MakeResIds(n) + if err != nil { + return nil, fmt.Errorf("error getting node IDs: %w", err) + } + for _, id := range ids { + if id.IsSelectedBy(selector.ResId) { + if len(matches) > 0 { + return nil, fmt.Errorf( + "multiple matches for selector %s", selector) + } + matches = append(matches, n) + break + } + } + } + if len(matches) == 0 { + return nil, fmt.Errorf("nothing selected by %s", selector) + } + return matches[0], nil +} + +func getRefinedValue(options *types.FieldOptions, rn *yaml.RNode) (*yaml.RNode, error) { + if options == nil || options.Delimiter == "" { + return rn, nil + } + if rn.YNode().Kind != yaml.ScalarNode { + return nil, fmt.Errorf("delimiter option can only be used with scalar nodes") + } + value := strings.Split(yaml.GetValue(rn), options.Delimiter) + if options.Index >= len(value) || options.Index < 0 { + return nil, fmt.Errorf("options.index %d is out of bounds for value %s", options.Index, yaml.GetValue(rn)) + } + n := rn.Copy() + n.YNode().Value = value[options.Index] + return n, nil +} + +func applyReplacement(nodes []*yaml.RNode, value *yaml.RNode, targetSelectors []*types.TargetSelector) ([]*yaml.RNode, error) { + for _, selector := range targetSelectors { + if selector.Select == nil { + return nil, errors.New("target must specify resources to select") + } + if len(selector.FieldPaths) == 0 { + selector.FieldPaths = []string{types.DefaultReplacementFieldPath} + } + for _, possibleTarget := range nodes { + ids, err := MakeResIds(possibleTarget) + if err != nil { + return nil, err + } + + // filter targets by label and annotation selectors + selectByAnnoAndLabel, err := selectByAnnoAndLabel(possibleTarget, selector) + if err != nil { + return nil, err + } + if !selectByAnnoAndLabel { + continue + } + + // filter targets by matching resource IDs + for i, id := range ids { + if id.IsSelectedBy(selector.Select.ResId) && !rejectId(selector.Reject, &ids[i]) { + err := copyValueToTarget(possibleTarget, value, selector) + if err != nil { + return nil, err + } + break + } + } + } + } + return nodes, nil +} + +func selectByAnnoAndLabel(n *yaml.RNode, t *types.TargetSelector) (bool, error) { + if matchesSelect, err := matchesAnnoAndLabelSelector(n, t.Select); !matchesSelect || err != nil { + return false, err + } + for _, reject := range t.Reject { + if reject.AnnotationSelector == "" && reject.LabelSelector == "" { + continue + } + if m, err := matchesAnnoAndLabelSelector(n, reject); m || err != nil { + return false, err + } + } + return true, nil +} + +func matchesAnnoAndLabelSelector(n *yaml.RNode, selector *types.Selector) (bool, error) { + r := resource.Resource{RNode: *n} + annoMatch, err := r.MatchesAnnotationSelector(selector.AnnotationSelector) + if err != nil { + return false, err + } + labelMatch, err := r.MatchesLabelSelector(selector.LabelSelector) + if err != nil { + return false, err + } + return annoMatch && labelMatch, nil +} + +func rejectId(rejects []*types.Selector, id *resid.ResId) bool { + for _, r := range rejects { + if !r.ResId.IsEmpty() && id.IsSelectedBy(r.ResId) { + return true + } + } + return false +} + +func copyValueToTarget(target *yaml.RNode, value *yaml.RNode, selector *types.TargetSelector) error { + for _, fp := range selector.FieldPaths { + fieldPath := kyaml_utils.SmarterPathSplitter(fp, ".") + extendedPath, err := NewExtendedPath(fieldPath) + if err != nil { + return err + } + create, err := shouldCreateField(selector.Options, extendedPath.resourcePath) + if err != nil { + return err + } + + var targetFields []*yaml.RNode + if create { + createdField, createErr := target.Pipe(yaml.LookupCreate(value.YNode().Kind, extendedPath.resourcePath...)) + if createErr != nil { + return fmt.Errorf("error creating replacement node: %w", createErr) + } + targetFields = append(targetFields, createdField) + } else { + // may return multiple fields, always wrapped in a sequence node + foundFieldSequence, lookupErr := target.Pipe(&yaml.PathMatcher{Path: extendedPath.resourcePath}) + if lookupErr != nil { + return fmt.Errorf("error finding field in replacement target: %w", lookupErr) + } + targetFields, err = foundFieldSequence.Elements() + if err != nil { + return fmt.Errorf("error fetching elements in replacement target: %w", err) + } + } + + for _, t := range targetFields { + if err := setFieldValue(selector.Options, t, value, extendedPath); err != nil { + return err + } + } + + } + return nil +} + +func setFieldValue(options *types.FieldOptions, targetField *yaml.RNode, value *yaml.RNode, extendedPath *ExtendedPath) error { + value = value.Copy() + if options != nil && options.Delimiter != "" { + if extendedPath.HasExtensions() { + return fmt.Errorf("delimiter option cannot be used with extensions") + } + if targetField.YNode().Kind != yaml.ScalarNode { + return fmt.Errorf("delimiter option can only be used with scalar nodes") + } + tv := strings.Split(targetField.YNode().Value, options.Delimiter) + v := yaml.GetValue(value) + // TODO: Add a way to remove an element + switch { + case options.Index < 0: // prefix + tv = append([]string{v}, tv...) + case options.Index >= len(tv): // suffix + tv = append(tv, v) + default: // replace an element + tv[options.Index] = v + } + value.YNode().Value = strings.Join(tv, options.Delimiter) + } + + if targetField.YNode().Kind == yaml.ScalarNode { + return extendedPath.Apply(targetField, value) + } else { + if extendedPath.HasExtensions() { + return fmt.Errorf("path extensions should start at a scalar node") + } + + targetField.SetYNode(value.YNode()) + } + + return nil +} + +func shouldCreateField(options *types.FieldOptions, fieldPath []string) (bool, error) { + if options == nil || !options.Create { + return false, nil + } + // create option is not supported in a wildcard matching + for _, f := range fieldPath { + if f == "*" { + return false, fmt.Errorf("cannot support create option in a multi-value target") + } + } + return true, nil +} + +// Copied + +const ( + BuildAnnotationPreviousKinds = konfig.ConfigAnnoDomain + "/previousKinds" + BuildAnnotationPreviousNames = konfig.ConfigAnnoDomain + "/previousNames" + BuildAnnotationPrefixes = konfig.ConfigAnnoDomain + "/prefixes" + BuildAnnotationSuffixes = konfig.ConfigAnnoDomain + "/suffixes" + BuildAnnotationPreviousNamespaces = konfig.ConfigAnnoDomain + "/previousNamespaces" +) + +// MakeResIds returns all of an RNode's current and previous Ids +func MakeResIds(n *yaml.RNode) ([]resid.ResId, error) { + var result []resid.ResId + apiVersion := n.Field(yaml.APIVersionField) + var group, version string + if apiVersion != nil { + group, version = resid.ParseGroupVersion(yaml.GetValue(apiVersion.Value)) + } + result = append(result, resid.NewResIdWithNamespace( + resid.Gvk{Group: group, Version: version, Kind: n.GetKind()}, n.GetName(), n.GetNamespace()), + ) + prevIds, err := PrevIds(n) + if err != nil { + return nil, err + } + result = append(result, prevIds...) + return result, nil +} + +// PrevIds returns all of an RNode's previous Ids +func PrevIds(n *yaml.RNode) ([]resid.ResId, error) { + var ids []resid.ResId + // TODO: merge previous names and namespaces into one list of + // pairs on one annotation so there is no chance of error + annotations := n.GetAnnotations() + if _, ok := annotations[BuildAnnotationPreviousNames]; !ok { + return nil, nil + } + names := strings.Split(annotations[BuildAnnotationPreviousNames], ",") + ns := strings.Split(annotations[BuildAnnotationPreviousNamespaces], ",") + kinds := strings.Split(annotations[BuildAnnotationPreviousKinds], ",") + // This should never happen + if len(names) != len(ns) || len(names) != len(kinds) { + return nil, fmt.Errorf( + "number of previous names, " + + "number of previous namespaces, " + + "number of previous kinds not equal") + } + for i := range names { + meta, err := n.GetMeta() + if err != nil { + return nil, err + } + group, version := resid.ParseGroupVersion(meta.APIVersion) + gvk := resid.Gvk{ + Group: group, + Version: version, + Kind: kinds[i], + } + ids = append(ids, resid.NewResIdWithNamespace( + gvk, names[i], ns[i])) + } + return ids, nil +} + +// plugin + +// Replace values in targets with values from a source +type ExtendedReplacementTransformerPlugin struct { + ReplacementList []types.ReplacementField `json:"replacements,omitempty" yaml:"replacements,omitempty"` + Replacements []types.Replacement `json:"omitempty" yaml:"omitempty"` +} + +func (p *ExtendedReplacementTransformerPlugin) Config( + h *resmap.PluginHelpers, c []byte) (err error) { + p.ReplacementList = []types.ReplacementField{} + if err := yaml.Unmarshal(c, p); err != nil { + return err + } + + for _, r := range p.ReplacementList { + if r.Path != "" && (r.Source != nil || len(r.Targets) != 0) { + return fmt.Errorf("cannot specify both path and inline replacement") + } + if r.Path != "" { + // load the replacement from the path + content, err := h.Loader().Load(r.Path) + if err != nil { + return err + } + // find if the path contains a a list of replacements or a single replacement + var replacement interface{} + err = yaml.Unmarshal(content, &replacement) + if err != nil { + return err + } + items := reflect.ValueOf(replacement) + switch items.Kind() { + case reflect.Slice: + repl := []types.Replacement{} + if err := yaml.Unmarshal(content, &repl); err != nil { + return err + } + p.Replacements = append(p.Replacements, repl...) + case reflect.Map: + repl := types.Replacement{} + if err := yaml.Unmarshal(content, &repl); err != nil { + return err + } + p.Replacements = append(p.Replacements, repl) + default: + return fmt.Errorf("unsupported replacement type encountered within replacement path: %v", items.Kind()) + } + } else { + // replacement information is already loaded + p.Replacements = append(p.Replacements, r.Replacement) + } + } + return nil +} + +func (p *ExtendedReplacementTransformerPlugin) Transform(m resmap.ResMap) (err error) { + return m.ApplyFilter(Filter{ + Replacements: p.Replacements, + }) +} + +func NewExtendedReplacementTransformerPlugin() resmap.TransformerPlugin { + return &ExtendedReplacementTransformerPlugin{} +} diff --git a/pkg/plugins/factories.go b/pkg/plugins/factories.go index 58041c2..87595e2 100644 --- a/pkg/plugins/factories.go +++ b/pkg/plugins/factories.go @@ -106,7 +106,7 @@ var TransformerFactories = map[BuiltinPluginType]func() resmap.TransformerPlugin PrefixSuffixTransformer: NewMultiTransformer, PrefixTransformer: builtins.NewPrefixTransformerPlugin, SuffixTransformer: builtins.NewSuffixTransformerPlugin, - ReplacementTransformer: builtins.NewReplacementTransformerPlugin, + ReplacementTransformer: extras.NewExtendedReplacementTransformerPlugin, ReplicaCountTransformer: builtins.NewReplicaCountTransformerPlugin, ValueAddTransformer: builtins.NewValueAddTransformerPlugin, // Do not wired SortOrderTransformer as a builtin plugin. diff --git a/tests/compare/argocd.expected.yaml b/tests/patch/expected/argocd.yaml similarity index 100% rename from tests/compare/argocd.expected.yaml rename to tests/patch/expected/argocd.yaml diff --git a/tests/functions/patch-transformer.yaml b/tests/patch/functions/patch-transformer.yaml similarity index 95% rename from tests/functions/patch-transformer.yaml rename to tests/patch/functions/patch-transformer.yaml index 9ad6376..9c656f0 100644 --- a/tests/functions/patch-transformer.yaml +++ b/tests/patch/functions/patch-transformer.yaml @@ -5,7 +5,7 @@ metadata: annotations: config.kubernetes.io/function: | exec: - path: ../krmfnbuiltin + path: ../../krmfnbuiltin patch: |- - op: replace path: /spec/source/repoURL diff --git a/tests/applications/argocd.yaml b/tests/patch/original/argocd.yaml similarity index 100% rename from tests/applications/argocd.yaml rename to tests/patch/original/argocd.yaml diff --git a/tests/replacement/expected/argocd.yaml b/tests/replacement/expected/argocd.yaml new file mode 100644 index 0000000..ae05158 --- /dev/null +++ b/tests/replacement/expected/argocd.yaml @@ -0,0 +1,41 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: argo-cd + namespace: argocd + annotations: + autocloud/local: "true" +spec: + destination: + namespace: argocd + server: https://kubernetes.default.svc + ignoreDifferences: + - group: argoproj.io + jsonPointers: + - /status + kind: Application + project: default + source: + path: packages/argocd + repoURL: https://github.com/antoinemartin/autocloud.git + targetRevision: deploy/citest + helm: + parameters: + - name: common.targetRevision + value: deploy/citest + - name: common.repoURL + value: https://github.com/antoinemartin/autocloud.git + values: | + uninode: true + apps: + enabled: true + common: + targetRevision: deploy/citest + repoURL: https://github.com/antoinemartin/autocloud.git + syncPolicy: + automated: + allowEmpty: true + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/tests/functions2/01_configmap-generator.yaml b/tests/replacement/functions/01_configmap-generator.yaml similarity index 92% rename from tests/functions2/01_configmap-generator.yaml rename to tests/replacement/functions/01_configmap-generator.yaml index 926742b..0ac9370 100644 --- a/tests/functions2/01_configmap-generator.yaml +++ b/tests/replacement/functions/01_configmap-generator.yaml @@ -8,7 +8,7 @@ metadata: annotations: config.kubernetes.io/function: | exec: - path: ../krmfnbuiltin + path: ../../krmfnbuiltin # When using GitConfigMapGenerator, these are automatically injected literals: - repoURL=https://github.com/antoinemartin/autocloud.git diff --git a/tests/functions2/02_replacement-transformer.yaml b/tests/replacement/functions/02_replacement-transformer.yaml similarity index 84% rename from tests/functions2/02_replacement-transformer.yaml rename to tests/replacement/functions/02_replacement-transformer.yaml index 4d69b70..1f56b17 100644 --- a/tests/functions2/02_replacement-transformer.yaml +++ b/tests/replacement/functions/02_replacement-transformer.yaml @@ -7,7 +7,7 @@ metadata: config.kubernetes.io/prune-local: "true" config.kubernetes.io/function: | exec: - path: ../krmfnbuiltin + path: ../../krmfnbuiltin replacements: - source: kind: ConfigMap @@ -19,6 +19,7 @@ replacements: fieldPaths: - spec.source.repoURL - spec.source.helm.parameters.[name=common.repoURL].value + - spec.source.helm.values.!!yaml.common.repoURL - source: kind: ConfigMap fieldPath: data.targetRevision @@ -29,3 +30,4 @@ replacements: fieldPaths: - spec.source.targetRevision - spec.source.helm.parameters.[name=common.targetRevision].value + - spec.source.helm.values.!!yaml.common.targetRevision diff --git a/tests/compare/argocd.original.yaml b/tests/replacement/original/argocd.yaml similarity index 80% rename from tests/compare/argocd.original.yaml rename to tests/replacement/original/argocd.yaml index e88b9e7..3c07fc6 100644 --- a/tests/compare/argocd.original.yaml +++ b/tests/replacement/original/argocd.yaml @@ -25,6 +25,13 @@ spec: value: main - name: common.repoURL value: https://github.com/anotherproject/anothergit + values: | + uninode: true + apps: + enabled: true + common: + targetRevision: main + repoURL: https://github.com/anotherproject/anothergit syncPolicy: automated: allowEmpty: true diff --git a/tests/test_krmfnbuiltin.sh b/tests/test_krmfnbuiltin.sh index 969731d..ff78ecb 100755 --- a/tests/test_krmfnbuiltin.sh +++ b/tests/test_krmfnbuiltin.sh @@ -1,21 +1,22 @@ #!/bin/bash # DEPENDENCEIS -# sops # kustomize -# age # yq #set -uexo pipefail set -e pipefail -trap "cp compare/argocd.original.yaml applications/argocd.yaml" EXIT +trap "rm -rf {patch,replacement}/applications" EXIT -echo "Running kustomize with patch transformer..." -kustomize fn run --enable-exec --fn-path functions applications -diff <(yq eval -P compare/argocd.expected.yaml) <(yq eval -P applications/argocd.yaml) -cp compare/argocd.original.yaml applications/argocd.yaml -echo "Running kustomize with replacement transformer..." -kustomize fn run --enable-exec --fn-path functions2 applications -diff <(yq eval -P compare/argocd.expected.yaml) <(yq eval -P applications/argocd.yaml) + +for d in patch replacement; do + echo "Running Test in $d..." + cd $d + rm -rf appllications + cp -r original applications + kustomize fn run --enable-exec --fn-path functions applications + diff <(yq eval -P expected/argocd.yaml) <(yq eval -P applications/argocd.yaml) + cd .. +done echo "Done ok 🎉"