From 8d543d8483cbe481d2f07dd0c03e29d07dc1132f Mon Sep 17 00:00:00 2001 From: Donny Xia Date: Mon, 29 Jun 2020 11:12:20 -0700 Subject: [PATCH] Refactor --- api/internal/plugins/execplugin/execplugin.go | 7 +- api/internal/plugins/execplugin/fnplugin.go | 219 ------------------ api/internal/plugins/execplugin/utils.go | 108 --------- api/internal/plugins/execplugin/utils_test.go | 111 --------- api/internal/plugins/fnplugin/fnplugin.go | 190 ++++++++++++--- api/internal/plugins/loader/loader.go | 6 +- api/internal/plugins/utils/utils.go | 100 ++++++++ api/internal/plugins/utils/utils_test.go | 101 ++++++++ 8 files changed, 364 insertions(+), 478 deletions(-) delete mode 100644 api/internal/plugins/execplugin/fnplugin.go delete mode 100644 api/internal/plugins/execplugin/utils.go delete mode 100644 api/internal/plugins/execplugin/utils_test.go diff --git a/api/internal/plugins/execplugin/execplugin.go b/api/internal/plugins/execplugin/execplugin.go index d4c7d0069..22b379eb5 100644 --- a/api/internal/plugins/execplugin/execplugin.go +++ b/api/internal/plugins/execplugin/execplugin.go @@ -14,6 +14,7 @@ import ( "github.com/google/shlex" "github.com/pkg/errors" + "sigs.k8s.io/kustomize/api/internal/plugins/utils" "sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/yaml" ) @@ -108,12 +109,12 @@ func (p *ExecPlugin) Generate() (resmap.ResMap, error) { if err != nil { return nil, err } - return UpdateResourceOptions(rm) + return utils.UpdateResourceOptions(rm) } func (p *ExecPlugin) Transform(rm resmap.ResMap) error { // add ResIds as annotations to all objects so that we can add them back - inputRM, err := getResMapWithIdAnnotation(rm) + inputRM, err := utils.GetResMapWithIDAnnotation(rm) if err != nil { return err } @@ -131,7 +132,7 @@ func (p *ExecPlugin) Transform(rm resmap.ResMap) error { } // update the original ResMap based on the output - return updateResMapValues(p.path, p.h, output, rm) + return utils.UpdateResMapValues(p.path, p.h, output, rm) } // invokePlugin writes plugin config to a temp file, then diff --git a/api/internal/plugins/execplugin/fnplugin.go b/api/internal/plugins/execplugin/fnplugin.go deleted file mode 100644 index fa56db953..000000000 --- a/api/internal/plugins/execplugin/fnplugin.go +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package execplugin - -import ( - "bytes" - "fmt" - - "github.com/pkg/errors" - - "sigs.k8s.io/kustomize/api/resmap" - "sigs.k8s.io/kustomize/api/resource" - "sigs.k8s.io/kustomize/api/types" - - "sigs.k8s.io/kustomize/kyaml/kio" - "sigs.k8s.io/kustomize/kyaml/yaml" - - "sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil" - "sigs.k8s.io/kustomize/kyaml/runfn" -) - -type FnPlugin struct { - // Function runner - runFns runfn.RunFns - - // Plugin configuration data. - cfg []byte - - // Plugin name cache for error output - pluginName string - - // PluginHelpers - h *resmap.PluginHelpers -} - -func bytesToRNode(yml []byte) (*yaml.RNode, error) { - rnode, err := yaml.Parse(string(yml)) - if err != nil { - return nil, err - } - return rnode, nil -} - -func resourceToRNode(res *resource.Resource) (*yaml.RNode, error) { - yml, err := res.AsYAML() - if err != nil { - return nil, err - } - - return bytesToRNode(yml) -} - -func GetFunctionSpec(res *resource.Resource) (*runtimeutil.FunctionSpec, error) { - rnode, err := resourceToRNode(res) - if err != nil { - return nil, err - } - - fSpec := runtimeutil.GetFunctionSpec(rnode) - if fSpec == nil { - return nil, fmt.Errorf("resource %v doesn't contain function spec", res.GetGvk()) - } - - return fSpec, nil -} - -func toStorageMounts(mounts []string) []runtimeutil.StorageMount { - var sms []runtimeutil.StorageMount - for _, mount := range mounts { - sms = append(sms, runtimeutil.StringToStorageMount(mount)) - } - return sms -} - -func NewFnPlugin(o *types.FnPluginLoadingOptions) *FnPlugin { - //log.Printf("options: %v\n", o) - return &FnPlugin{ - runFns: runfn.RunFns{ - Functions: []*yaml.RNode{}, - Network: o.Network, - NetworkName: o.NetworkName, - EnableStarlark: o.EnableStar, - EnableExec: o.EnableExec, - StorageMounts: toStorageMounts(o.Mounts), - }, - } -} - -func (p *FnPlugin) Cfg() []byte { - return p.cfg -} - -func (p *FnPlugin) Config(h *resmap.PluginHelpers, config []byte) error { - p.h = h - p.cfg = config - - rnode, err := bytesToRNode(p.cfg) - if err != nil { - return err - } - - meta, err := rnode.GetMeta() - if err != nil { - return err - } - - p.pluginName = fmt.Sprintf("api: %s, kind: %s, name: %s", - meta.APIVersion, meta.Kind, meta.Name) - //log.Printf("config based pluginName: %s", p.pluginName) - - return nil -} - -func (p *FnPlugin) Generate() (resmap.ResMap, error) { - output, err := p.invokePlugin(nil) - if err != nil { - return nil, err - } - rm, err := p.h.ResmapFactory().NewResMapFromBytes(output) - if err != nil { - return nil, err - } - return UpdateResourceOptions(rm) -} - -func (p *FnPlugin) Transform(rm resmap.ResMap) error { - // add ResIds as annotations to all objects so that we can add them back - inputRM, err := getResMapWithIdAnnotation(rm) - if err != nil { - return err - } - - // encode the ResMap so it can be fed to the plugin - resources, err := inputRM.AsYaml() - if err != nil { - return err - } - - // invoke the plugin with resources as the input - output, err := p.invokePlugin(resources) - if err != nil { - return fmt.Errorf("%v %s", err, string(output)) - } - - // update the original ResMap based on the output - return updateResMapValues(p.pluginName, p.h, output, rm) -} - -// invokePlugin uses Function runner to run function as plugin -func (p *FnPlugin) invokePlugin(input []byte) ([]byte, error) { - // get config rnode - rnode, err := bytesToRNode(p.cfg) - if err != nil { - return nil, err - } - err = rnode.PipeE(yaml.SetAnnotation("config.kubernetes.io/local-config", "true")) - if err != nil { - return nil, err - } - - // we need to add config as input for generators. Some of them don't work with FunctionConfig - // and in addition kio.Pipeline won't create anything if there are no objects - // see https://github.com/kubernetes-sigs/kustomize/blob/master/kyaml/kio/kio.go#L93 - if input == nil { - yaml, err := rnode.String() - if err != nil { - return nil, err - } - input = []byte(yaml) - } - - // Transform to ResourceList - var inOut bytes.Buffer - inIn := bytes.NewReader(input) - err = kio.Pipeline{ - Inputs: []kio.Reader{&kio.ByteReader{Reader: inIn}}, - Outputs: []kio.Writer{kio.ByteWriter{ - Writer: &inOut, - WrappingKind: kio.ResourceListKind, - WrappingAPIVersion: kio.ResourceListAPIVersion}}, - }.Execute() - if err != nil { - return nil, errors.Wrap( - err, "couldn't transform to ResourceList") - } - //log.Printf("converted to:\n%s\n", inOut.String()) - - // Configure and Execute Fn - var runFnsOut bytes.Buffer - p.runFns.Input = bytes.NewReader(inOut.Bytes()) - p.runFns.Functions = append(p.runFns.Functions, rnode) - p.runFns.Output = &runFnsOut - - err = p.runFns.Execute() - if err != nil { - return nil, errors.Wrap( - err, "couldn't execute function") - } - - //log.Printf("fn returned:\n%s\n", runFnsOut.String()) - - // Convert back to a single multi-yaml doc - var outOut bytes.Buffer - outIn := bytes.NewReader(runFnsOut.Bytes()) - - err = kio.Pipeline{ - Inputs: []kio.Reader{&kio.ByteReader{Reader: outIn}}, - Outputs: []kio.Writer{kio.ByteWriter{Writer: &outOut}}, - }.Execute() - if err != nil { - return nil, errors.Wrap( - err, "couldn't transform from ResourceList") - } - - //log.Printf("converted back to:\n%s\n", outOut.String()) - - return outOut.Bytes(), nil -} diff --git a/api/internal/plugins/execplugin/utils.go b/api/internal/plugins/execplugin/utils.go deleted file mode 100644 index 4c7ac8e66..000000000 --- a/api/internal/plugins/execplugin/utils.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package execplugin - -import ( - "fmt" - "strconv" - - "sigs.k8s.io/kustomize/api/resid" - "sigs.k8s.io/kustomize/api/resmap" - "sigs.k8s.io/kustomize/api/types" - "sigs.k8s.io/yaml" -) - -const ( - idAnnotation = "kustomize.config.k8s.io/id" - HashAnnotation = "kustomize.config.k8s.io/needs-hash" - BehaviorAnnotation = "kustomize.config.k8s.io/behavior" -) - -// Returns a new copy of the given ResMap with the ResIds annotated in each Resource -func getResMapWithIdAnnotation(rm resmap.ResMap) (resmap.ResMap, error) { - inputRM := rm.DeepCopy() - for _, r := range inputRM.Resources() { - idString, err := yaml.Marshal(r.CurId()) - if err != nil { - return nil, err - } - annotations := r.GetAnnotations() - if annotations == nil { - annotations = make(map[string]string) - } - annotations[idAnnotation] = string(idString) - r.SetAnnotations(annotations) - } - return inputRM, nil -} - -// updateResMapValues updates the Resource value in the given ResMap -// with the emitted Resource values in output. -func updateResMapValues(pluginName string, h *resmap.PluginHelpers, output []byte, rm resmap.ResMap) error { - outputRM, err := h.ResmapFactory().NewResMapFromBytes(output) - if err != nil { - return err - } - for _, r := range outputRM.Resources() { - // for each emitted Resource, find the matching Resource in the original ResMap - // using its id - annotations := r.GetAnnotations() - idString, ok := annotations[idAnnotation] - if !ok { - return fmt.Errorf("the transformer %s should not remove annotation %s", - pluginName, idAnnotation) - } - id := resid.ResId{} - err := yaml.Unmarshal([]byte(idString), &id) - if err != nil { - return err - } - res, err := rm.GetByCurrentId(id) - if err != nil { - return fmt.Errorf("unable to find unique match to %s", id.String()) - } - // remove the annotation set by Kustomize to track the resource - delete(annotations, idAnnotation) - if len(annotations) == 0 { - annotations = nil - } - r.SetAnnotations(annotations) - - // update the resource value with the transformed object - res.ResetPrimaryData(r) - } - return nil -} - -// updateResourceOptions updates the generator options for each resource in the -// given ResMap based on plugin provided annotations. -func UpdateResourceOptions(rm resmap.ResMap) (resmap.ResMap, error) { - for _, r := range rm.Resources() { - // Disable name hashing by default and require plugin to explicitly - // request it for each resource. - annotations := r.GetAnnotations() - behavior := annotations[BehaviorAnnotation] - var needsHash bool - if val, ok := annotations[HashAnnotation]; ok { - b, err := strconv.ParseBool(val) - if err != nil { - return nil, fmt.Errorf( - "the annotation %q contains an invalid value (%q)", - HashAnnotation, val) - } - needsHash = b - } - delete(annotations, HashAnnotation) - delete(annotations, BehaviorAnnotation) - if len(annotations) == 0 { - annotations = nil - } - r.SetAnnotations(annotations) - r.SetOptions(types.NewGenArgs( - &types.GeneratorArgs{ - Behavior: behavior, - Options: &types.GeneratorOptions{DisableNameSuffixHash: !needsHash}})) - } - return rm, nil -} diff --git a/api/internal/plugins/execplugin/utils_test.go b/api/internal/plugins/execplugin/utils_test.go deleted file mode 100644 index 93f9cb754..000000000 --- a/api/internal/plugins/execplugin/utils_test.go +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package execplugin_test - -import ( - "fmt" - "testing" - - . "sigs.k8s.io/kustomize/api/internal/plugins/execplugin" - "sigs.k8s.io/kustomize/api/k8sdeps/kunstruct" - "sigs.k8s.io/kustomize/api/resmap" - "sigs.k8s.io/kustomize/api/resource" - "sigs.k8s.io/kustomize/api/types" -) - -func makeConfigMap(rf *resource.Factory, name, behavior string, hashValue *string) *resource.Resource { - r := rf.FromMap(map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{"name": name}, - }) - annotations := map[string]string{} - if behavior != "" { - annotations[BehaviorAnnotation] = behavior - } - if hashValue != nil { - annotations[HashAnnotation] = *hashValue - } - if len(annotations) > 0 { - r.SetAnnotations(annotations) - } - return r -} - -func makeConfigMapOptions(rf *resource.Factory, name, behavior string, disableHash bool) *resource.Resource { - return rf.FromMapAndOption(map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{"name": name}, - }, &types.GeneratorArgs{ - Behavior: behavior, - Options: &types.GeneratorOptions{DisableNameSuffixHash: disableHash}}) -} - -func strptr(s string) *string { - return &s -} - -func TestUpdateResourceOptions(t *testing.T) { - rf := resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl()) - in := resmap.New() - expected := resmap.New() - cases := []struct { - behavior string - needsHash bool - hashValue *string - }{ - {hashValue: strptr("false")}, - {hashValue: strptr("true"), needsHash: true}, - {behavior: "replace"}, - {behavior: "merge"}, - {behavior: "create"}, - {behavior: "nonsense"}, - {behavior: "merge", hashValue: strptr("false")}, - {behavior: "merge", hashValue: strptr("true"), needsHash: true}, - } - for i, c := range cases { - name := fmt.Sprintf("test%d", i) - in.Append(makeConfigMap(rf, name, c.behavior, c.hashValue)) - expected.Append(makeConfigMapOptions(rf, name, c.behavior, !c.needsHash)) - } - actual, err := UpdateResourceOptions(in) - if err != nil { - t.Fatalf("unexpected error: %v", err.Error()) - } - for i, a := range expected.Resources() { - b := actual.GetByIndex(i) - if b == nil { - t.Fatalf("resource %d missing from processed map", i) - } - if !a.Equals(b) { - t.Errorf("expected %v got %v", a, b) - } - if a.NeedHashSuffix() != b.NeedHashSuffix() { - t.Errorf("") - } - if a.Behavior() != b.Behavior() { - t.Errorf("expected %v got %v", a.Behavior(), b.Behavior()) - } - } -} - -func TestUpdateResourceOptionsWithInvalidHashAnnotationValues(t *testing.T) { - rf := resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl()) - cases := []string{ - "", - "FaLsE", - "TrUe", - "potato", - } - for i, c := range cases { - name := fmt.Sprintf("test%d", i) - in := resmap.New() - in.Append(makeConfigMap(rf, name, "", &c)) - _, err := UpdateResourceOptions(in) - if err == nil { - t.Errorf("expected error from value %q", c) - } - } -} diff --git a/api/internal/plugins/fnplugin/fnplugin.go b/api/internal/plugins/fnplugin/fnplugin.go index 32cd8c3f4..97f03d9a8 100644 --- a/api/internal/plugins/fnplugin/fnplugin.go +++ b/api/internal/plugins/fnplugin/fnplugin.go @@ -1,23 +1,38 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + package fnplugin import ( + "bytes" + "fmt" + + "github.com/pkg/errors" + "sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resource" + "sigs.k8s.io/kustomize/api/types" + + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" + + "sigs.k8s.io/kustomize/api/internal/plugins/utils" "sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil" "sigs.k8s.io/kustomize/kyaml/runfn" - "sigs.k8s.io/kustomize/kyaml/yaml" ) -// FnPlugin holds the information about thie function type FnPlugin struct { + // Function runner + runFns runfn.RunFns + // Plugin configuration data. cfg []byte + // Plugin name cache for error output + pluginName string + // PluginHelpers h *resmap.PluginHelpers - - // Function runner - RunFns runfn.RunFns } func bytesToRNode(yml []byte) (*yaml.RNode, error) { @@ -37,7 +52,7 @@ func resourceToRNode(res *resource.Resource) (*yaml.RNode, error) { return bytesToRNode(yml) } -func getFunctionSpec(res *resource.Resource) *runtimeutil.FunctionSpec { +func GetFunctionSpec(res *resource.Resource) *runtimeutil.FunctionSpec { rnode, err := resourceToRNode(res) if err != nil { return nil @@ -46,46 +61,153 @@ func getFunctionSpec(res *resource.Resource) *runtimeutil.FunctionSpec { return runtimeutil.GetFunctionSpec(rnode) } -// IsFnPlugin returns a resource is a function plugin spec or not -func IsFnPlugin(res *resource.Resource) bool { - return getFunctionSpec(res) != nil +func toStorageMounts(mounts []string) []runtimeutil.StorageMount { + var sms []runtimeutil.StorageMount + for _, mount := range mounts { + sms = append(sms, runtimeutil.StringToStorageMount(mount)) + } + return sms } -// NewFnPlugin returns a FnPlugin struct -func NewFnPlugin(res *resource.Resource) *FnPlugin { - return &FnPlugin{} +func NewFnPlugin(o *types.FnPluginLoadingOptions) *FnPlugin { + return &FnPlugin{ + runFns: runfn.RunFns{ + Functions: []*yaml.RNode{}, + Network: o.Network, + NetworkName: o.NetworkName, + EnableStarlark: o.EnableStar, + EnableExec: o.EnableExec, + StorageMounts: toStorageMounts(o.Mounts), + }, + } } -// Config accepts the plugin helper and plugin config -func (f *FnPlugin) Config(h *resmap.PluginHelpers, config []byte) error { - f.h = h - f.cfg = config - // config is the content of the config file for the functions. - // If there are multiple functions in on config file, they will - // be passed in one by one. - fn, err := bytesToRNode(config) +func (p *FnPlugin) Cfg() []byte { + return p.cfg +} + +func (p *FnPlugin) Config(h *resmap.PluginHelpers, config []byte) error { + p.h = h + p.cfg = config + + fn, err := bytesToRNode(p.cfg) if err != nil { return err } - f.RunFns.Functions = append(f.RunFns.Functions, fn) + meta, err := fn.GetMeta() + if err != nil { + return err + } + + p.pluginName = fmt.Sprintf("api: %s, kind: %s, name: %s", + meta.APIVersion, meta.Kind, meta.Name) return nil } -// Transform does the transformation when the plugin is a transformer -func (f *FnPlugin) Transform(rm resmap.ResMap) error { - // convert input to ResourceList - // add functionConfig - // invoke function - // convert back to ResMap - return nil +func (p *FnPlugin) Generate() (resmap.ResMap, error) { + output, err := p.invokePlugin(nil) + if err != nil { + return nil, err + } + rm, err := p.h.ResmapFactory().NewResMapFromBytes(output) + if err != nil { + return nil, err + } + return utils.UpdateResourceOptions(rm) } -// invoke call the actual function and send the input to it. It captures -// and returns the output -func (f *FnPlugin) invoke(input []byte) ([]byte, error) { - // setup input and output - var output []byte - return output, nil +func (p *FnPlugin) Transform(rm resmap.ResMap) error { + // add ResIds as annotations to all objects so that we can add them back + inputRM, err := utils.GetResMapWithIDAnnotation(rm) + if err != nil { + return err + } + + // encode the ResMap so it can be fed to the plugin + resources, err := inputRM.AsYaml() + if err != nil { + return err + } + + // invoke the plugin with resources as the input + output, err := p.invokePlugin(resources) + if err != nil { + return fmt.Errorf("%v %s", err, string(output)) + } + + // update the original ResMap based on the output + return utils.UpdateResMapValues(p.pluginName, p.h, output, rm) +} + +// invokePlugin uses Function runner to run function as plugin +func (p *FnPlugin) invokePlugin(input []byte) ([]byte, error) { + // get config rnode + rnode, err := bytesToRNode(p.cfg) + if err != nil { + return nil, err + } + err = rnode.PipeE(yaml.SetAnnotation("config.kubernetes.io/local-config", "true")) + if err != nil { + return nil, err + } + + // we need to add config as input for generators. Some of them don't work with FunctionConfig + // and in addition kio.Pipeline won't create anything if there are no objects + // see https://github.com/kubernetes-sigs/kustomize/blob/master/kyaml/kio/kio.go#L93 + if input == nil { + yaml, err := rnode.String() + if err != nil { + return nil, err + } + input = []byte(yaml) + } + + // Transform to ResourceList + var inOut bytes.Buffer + inIn := bytes.NewReader(input) + err = kio.Pipeline{ + Inputs: []kio.Reader{&kio.ByteReader{Reader: inIn}}, + Outputs: []kio.Writer{kio.ByteWriter{ + Writer: &inOut, + WrappingKind: kio.ResourceListKind, + WrappingAPIVersion: kio.ResourceListAPIVersion}}, + }.Execute() + if err != nil { + return nil, errors.Wrap( + err, "couldn't transform to ResourceList") + } + //log.Printf("converted to:\n%s\n", inOut.String()) + + // Configure and Execute Fn + var runFnsOut bytes.Buffer + p.runFns.Input = bytes.NewReader(inOut.Bytes()) + p.runFns.Functions = append(p.runFns.Functions, rnode) + p.runFns.Output = &runFnsOut + + err = p.runFns.Execute() + if err != nil { + return nil, errors.Wrap( + err, "couldn't execute function") + } + + //log.Printf("fn returned:\n%s\n", runFnsOut.String()) + + // Convert back to a single multi-yaml doc + var outOut bytes.Buffer + outIn := bytes.NewReader(runFnsOut.Bytes()) + + err = kio.Pipeline{ + Inputs: []kio.Reader{&kio.ByteReader{Reader: outIn}}, + Outputs: []kio.Writer{kio.ByteWriter{Writer: &outOut}}, + }.Execute() + if err != nil { + return nil, errors.Wrap( + err, "couldn't transform from ResourceList") + } + + //log.Printf("converted back to:\n%s\n", outOut.String()) + + return outOut.Bytes(), nil } diff --git a/api/internal/plugins/loader/loader.go b/api/internal/plugins/loader/loader.go index 43f4b2cbb..aaa7c3600 100644 --- a/api/internal/plugins/loader/loader.go +++ b/api/internal/plugins/loader/loader.go @@ -168,9 +168,9 @@ func (l *Loader) makeBuiltinPlugin(r resid.Gvk) (resmap.Configurable, error) { } func (l *Loader) loadPlugin(res *resource.Resource) (resmap.Configurable, error) { - _, err := execplugin.GetFunctionSpec(res) - if err == nil { - return execplugin.NewFnPlugin(&l.pc.FnpLoadingOptions), nil + spec := fnplugin.GetFunctionSpec(res) + if spec != nil { + return fnplugin.NewFnPlugin(&l.pc.FnpLoadingOptions), nil } return l.loadExecOrGoPlugin(res.OrgId()) } diff --git a/api/internal/plugins/utils/utils.go b/api/internal/plugins/utils/utils.go index af4976cea..46706627b 100644 --- a/api/internal/plugins/utils/utils.go +++ b/api/internal/plugins/utils/utils.go @@ -4,13 +4,25 @@ package utils import ( + "fmt" "os" "path/filepath" "runtime" + "strconv" "time" "sigs.k8s.io/kustomize/api/filesys" "sigs.k8s.io/kustomize/api/konfig" + "sigs.k8s.io/kustomize/api/resid" + "sigs.k8s.io/kustomize/api/resmap" + "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/yaml" +) + +const ( + idAnnotation = "kustomize.config.k8s.io/id" + HashAnnotation = "kustomize.config.k8s.io/needs-hash" + BehaviorAnnotation = "kustomize.config.k8s.io/behavior" ) func GoBin() string { @@ -113,3 +125,91 @@ func FileExists(path string) bool { } return true } + +// GetResMapWithIDAnnotation returns a new copy of the given ResMap with the ResIds annotated in each Resource +func GetResMapWithIDAnnotation(rm resmap.ResMap) (resmap.ResMap, error) { + inputRM := rm.DeepCopy() + for _, r := range inputRM.Resources() { + idString, err := yaml.Marshal(r.CurId()) + if err != nil { + return nil, err + } + annotations := r.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + annotations[idAnnotation] = string(idString) + r.SetAnnotations(annotations) + } + return inputRM, nil +} + +// UpdateResMapValues updates the Resource value in the given ResMap +// with the emitted Resource values in output. +func UpdateResMapValues(pluginName string, h *resmap.PluginHelpers, output []byte, rm resmap.ResMap) error { + outputRM, err := h.ResmapFactory().NewResMapFromBytes(output) + if err != nil { + return err + } + for _, r := range outputRM.Resources() { + // for each emitted Resource, find the matching Resource in the original ResMap + // using its id + annotations := r.GetAnnotations() + idString, ok := annotations[idAnnotation] + if !ok { + return fmt.Errorf("the transformer %s should not remove annotation %s", + pluginName, idAnnotation) + } + id := resid.ResId{} + err := yaml.Unmarshal([]byte(idString), &id) + if err != nil { + return err + } + res, err := rm.GetByCurrentId(id) + if err != nil { + return fmt.Errorf("unable to find unique match to %s", id.String()) + } + // remove the annotation set by Kustomize to track the resource + delete(annotations, idAnnotation) + if len(annotations) == 0 { + annotations = nil + } + r.SetAnnotations(annotations) + + // update the resource value with the transformed object + res.ResetPrimaryData(r) + } + return nil +} + +// UpdateResourceOptions updates the generator options for each resource in the +// given ResMap based on plugin provided annotations. +func UpdateResourceOptions(rm resmap.ResMap) (resmap.ResMap, error) { + for _, r := range rm.Resources() { + // Disable name hashing by default and require plugin to explicitly + // request it for each resource. + annotations := r.GetAnnotations() + behavior := annotations[BehaviorAnnotation] + var needsHash bool + if val, ok := annotations[HashAnnotation]; ok { + b, err := strconv.ParseBool(val) + if err != nil { + return nil, fmt.Errorf( + "the annotation %q contains an invalid value (%q)", + HashAnnotation, val) + } + needsHash = b + } + delete(annotations, HashAnnotation) + delete(annotations, BehaviorAnnotation) + if len(annotations) == 0 { + annotations = nil + } + r.SetAnnotations(annotations) + r.SetOptions(types.NewGenArgs( + &types.GeneratorArgs{ + Behavior: behavior, + Options: &types.GeneratorOptions{DisableNameSuffixHash: !needsHash}})) + } + return rm, nil +} diff --git a/api/internal/plugins/utils/utils_test.go b/api/internal/plugins/utils/utils_test.go index e4341287b..356e7680a 100644 --- a/api/internal/plugins/utils/utils_test.go +++ b/api/internal/plugins/utils/utils_test.go @@ -4,12 +4,17 @@ package utils import ( + "fmt" "path/filepath" "strings" "testing" "sigs.k8s.io/kustomize/api/filesys" + "sigs.k8s.io/kustomize/api/k8sdeps/kunstruct" "sigs.k8s.io/kustomize/api/konfig" + "sigs.k8s.io/kustomize/api/resmap" + "sigs.k8s.io/kustomize/api/resource" + "sigs.k8s.io/kustomize/api/types" ) func TestDeterminePluginSrcRoot(t *testing.T) { @@ -24,3 +29,99 @@ func TestDeterminePluginSrcRoot(t *testing.T) { t.Errorf("expected suffix '%s' in '%s'", konfig.RelPluginHome, actual) } } + +func makeConfigMap(rf *resource.Factory, name, behavior string, hashValue *string) *resource.Resource { + r := rf.FromMap(map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{"name": name}, + }) + annotations := map[string]string{} + if behavior != "" { + annotations[BehaviorAnnotation] = behavior + } + if hashValue != nil { + annotations[HashAnnotation] = *hashValue + } + if len(annotations) > 0 { + r.SetAnnotations(annotations) + } + return r +} + +func makeConfigMapOptions(rf *resource.Factory, name, behavior string, disableHash bool) *resource.Resource { + return rf.FromMapAndOption(map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{"name": name}, + }, &types.GeneratorArgs{ + Behavior: behavior, + Options: &types.GeneratorOptions{DisableNameSuffixHash: disableHash}}) +} + +func strptr(s string) *string { + return &s +} + +func TestUpdateResourceOptions(t *testing.T) { + rf := resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl()) + in := resmap.New() + expected := resmap.New() + cases := []struct { + behavior string + needsHash bool + hashValue *string + }{ + {hashValue: strptr("false")}, + {hashValue: strptr("true"), needsHash: true}, + {behavior: "replace"}, + {behavior: "merge"}, + {behavior: "create"}, + {behavior: "nonsense"}, + {behavior: "merge", hashValue: strptr("false")}, + {behavior: "merge", hashValue: strptr("true"), needsHash: true}, + } + for i, c := range cases { + name := fmt.Sprintf("test%d", i) + in.Append(makeConfigMap(rf, name, c.behavior, c.hashValue)) + expected.Append(makeConfigMapOptions(rf, name, c.behavior, !c.needsHash)) + } + actual, err := UpdateResourceOptions(in) + if err != nil { + t.Fatalf("unexpected error: %v", err.Error()) + } + for i, a := range expected.Resources() { + b := actual.GetByIndex(i) + if b == nil { + t.Fatalf("resource %d missing from processed map", i) + } + if !a.Equals(b) { + t.Errorf("expected %v got %v", a, b) + } + if a.NeedHashSuffix() != b.NeedHashSuffix() { + t.Errorf("") + } + if a.Behavior() != b.Behavior() { + t.Errorf("expected %v got %v", a.Behavior(), b.Behavior()) + } + } +} + +func TestUpdateResourceOptionsWithInvalidHashAnnotationValues(t *testing.T) { + rf := resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl()) + cases := []string{ + "", + "FaLsE", + "TrUe", + "potato", + } + for i, c := range cases { + name := fmt.Sprintf("test%d", i) + in := resmap.New() + in.Append(makeConfigMap(rf, name, "", &c)) + _, err := UpdateResourceOptions(in) + if err == nil { + t.Errorf("expected error from value %q", c) + } + } +}