diff --git a/api/internal/plugins/execplugin/execplugin.go b/api/internal/plugins/execplugin/execplugin.go index 5e63457fd..d4c7d0069 100644 --- a/api/internal/plugins/execplugin/execplugin.go +++ b/api/internal/plugins/execplugin/execplugin.go @@ -9,22 +9,16 @@ import ( "io/ioutil" "os" "os/exec" - "strconv" "strings" "github.com/google/shlex" "github.com/pkg/errors" - "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" tmpConfigFilePrefix = "kust-plugin-config-" ) @@ -114,12 +108,12 @@ func (p *ExecPlugin) Generate() (resmap.ResMap, error) { if err != nil { return nil, err } - return p.UpdateResourceOptions(rm) + return 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 := p.getResMapWithIdAnnotation(rm) + inputRM, err := getResMapWithIdAnnotation(rm) if err != nil { return err } @@ -137,7 +131,7 @@ func (p *ExecPlugin) Transform(rm resmap.ResMap) error { } // update the original ResMap based on the output - return p.updateResMapValues(output, rm) + return updateResMapValues(p.path, p.h, output, rm) } // invokePlugin writes plugin config to a temp file, then @@ -184,91 +178,3 @@ func (p *ExecPlugin) getEnv() []string { "KUSTOMIZE_PLUGIN_CONFIG_ROOT="+p.h.Loader().Root()) return env } - -// Returns a new copy of the given ResMap with the ResIds annotated in each Resource -func (p *ExecPlugin) 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 (p *ExecPlugin) updateResMapValues(output []byte, rm resmap.ResMap) error { - outputRM, err := p.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", - p.path, 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 (p *ExecPlugin) 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/execplugin_test.go b/api/internal/plugins/execplugin/execplugin_test.go index 249571dd7..d6b9021cf 100644 --- a/api/internal/plugins/execplugin/execplugin_test.go +++ b/api/internal/plugins/execplugin/execplugin_test.go @@ -4,7 +4,6 @@ package execplugin_test import ( - "fmt" "strings" "testing" @@ -17,7 +16,6 @@ import ( "sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resource" valtest_test "sigs.k8s.io/kustomize/api/testutils/valtest" - "sigs.k8s.io/kustomize/api/types" ) func TestExecPluginConfig(t *testing.T) { @@ -91,107 +89,3 @@ metadata: t.Fatalf("unexpected arg array: %#v", p.Args()) } } - -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) { - p := NewExecPlugin("") - if err := p.ErrIfNotExecutable(); err == nil { - t.Fatalf("expected unexecutable error") - } - 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 := p.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) { - p := NewExecPlugin("") - if err := p.ErrIfNotExecutable(); err == nil { - t.Fatalf("expected unexecutable error") - } - 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 := p.UpdateResourceOptions(in) - if err == nil { - t.Errorf("expected error from value %q", c) - } - } -} diff --git a/api/internal/plugins/execplugin/fnplugin.go b/api/internal/plugins/execplugin/fnplugin.go new file mode 100644 index 000000000..fa56db953 --- /dev/null +++ b/api/internal/plugins/execplugin/fnplugin.go @@ -0,0 +1,219 @@ +// 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 new file mode 100644 index 000000000..4c7ac8e66 --- /dev/null +++ b/api/internal/plugins/execplugin/utils.go @@ -0,0 +1,108 @@ +// 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 new file mode 100644 index 000000000..93f9cb754 --- /dev/null +++ b/api/internal/plugins/execplugin/utils_test.go @@ -0,0 +1,111 @@ +// 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/loader/loader.go b/api/internal/plugins/loader/loader.go index 09dc3a167..c61f264c4 100644 --- a/api/internal/plugins/loader/loader.go +++ b/api/internal/plugins/loader/loader.go @@ -116,7 +116,7 @@ func (l *Loader) loadAndConfigurePlugin( if isBuiltinPlugin(res) { switch l.pc.BpLoadingOptions { case types.BploLoadFromFileSys: - c, err = l.loadPlugin(res.OrgId()) + c, err = l.loadPlugin(res) case types.BploUseStaticallyLinked: // Instead of looking for and loading a .so file, // instantiate the plugin from a generated factory @@ -131,7 +131,7 @@ func (l *Loader) loadAndConfigurePlugin( } else { switch l.pc.PluginRestrictions { case types.PluginRestrictionsNone: - c, err = l.loadPlugin(res.OrgId()) + c, err = l.loadPlugin(res) case types.PluginRestrictionsBuiltinsOnly: err = types.NewErrOnlyBuiltinPluginsAllowed(res.OrgId().Kind) default: @@ -166,7 +166,15 @@ func (l *Loader) makeBuiltinPlugin(r resid.Gvk) (resmap.Configurable, error) { return nil, errors.Errorf("unable to load builtin %s", r) } -func (l *Loader) loadPlugin(resId resid.ResId) (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 + } + return l.loadExecOrGoPlugin(res.OrgId()) +} + +func (l *Loader) loadExecOrGoPlugin(resId resid.ResId) (resmap.Configurable, error) { // First try to load the plugin as an executable. p := execplugin.NewExecPlugin(l.absolutePluginPath(resId)) err := p.ErrIfNotExecutable() diff --git a/api/krusty/fnplugin_test.go b/api/krusty/fnplugin_test.go new file mode 100644 index 000000000..1aaa2ac90 --- /dev/null +++ b/api/krusty/fnplugin_test.go @@ -0,0 +1,406 @@ +package krusty_test + +import ( + "os/exec" + "testing" + + kusttest_test "sigs.k8s.io/kustomize/api/testutils/kusttest" +) + +func TestFnExecGenerator(t *testing.T) { + th := kusttest_test.MakeEnhancedHarness(t) + defer th.Reset() + + th.WriteK("/app", ` +resources: +- short_secret.yaml +generators: +- gener.yaml +`) + + // Create some additional resource just to make sure everything is added + th.WriteF("/app/short_secret.yaml", ` +apiVersion: v1 +kind: Secret +metadata: + labels: + airshipit.org/ephemeral-user-data: "true" + name: node1-bmc-secret +type: Opaque +stringData: + userData: | + bootcmd: + - mkdir /mnt/vda +`) + + th.WriteF("/app/gener.yaml", ` +kind: executable +metadata: + name: demo + annotations: + config.kubernetes.io/function: | + exec: + path: ./fnplugin_test/fnexectest.sh +spec: +`) + o := th.MakeOptionsPluginsEnabled() + o.PluginConfig.FnpLoadingOptions.EnableExec = true + m := th.Run("/app", o) + th.AssertActualEqualsExpected(m, ` +apiVersion: v1 +kind: Secret +metadata: + labels: + airshipit.org/ephemeral-user-data: "true" + name: node1-bmc-secret +stringData: + userData: | + bootcmd: + - mkdir /mnt/vda +type: Opaque +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + config.kubernetes.io/path: deployment_nginx.yaml + tshirt-size: small + labels: + app: nginx + name: nginx +spec: + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - image: nginx + name: nginx +`) +} + +func skipIfNoDocker(t *testing.T) { + if _, err := exec.LookPath("docker"); err != nil { + t.Skip("skipping because docker binary wasn't found in PATH") + } +} + +func TestFnContainerGenerator(t *testing.T) { + skipIfNoDocker(t) + + th := kusttest_test.MakeEnhancedHarness(t) + defer th.Reset() + + th.WriteK("/app", ` +resources: +- short_secret.yaml +generators: +- gener.yaml +`) + // Create generator config + th.WriteF("/app/gener.yaml", ` +apiVersion: examples.config.kubernetes.io/v1beta1 +kind: CockroachDB +metadata: + name: demo + annotations: + config.kubernetes.io/function: | + container: + image: gcr.io/kustomize-functions/example-cockroachdb:v0.1.0 +spec: + replicas: 3 +`) + // Create some additional resource just to make sure everything is added + th.WriteF("/app/short_secret.yaml", ` +apiVersion: v1 +kind: Secret +metadata: + labels: + airshipit.org/ephemeral-user-data: "true" + name: node1-bmc-secret +type: Opaque +stringData: + userData: | + bootcmd: + - mkdir /mnt/vda +`) + m := th.Run("/app", th.MakeOptionsPluginsEnabled()) + th.AssertActualEqualsExpected(m, ` +apiVersion: v1 +kind: Secret +metadata: + labels: + airshipit.org/ephemeral-user-data: "true" + name: node1-bmc-secret +stringData: + userData: | + bootcmd: + - mkdir /mnt/vda +type: Opaque +--- +apiVersion: policy/v1beta1 +kind: PodDisruptionBudget +metadata: + annotations: + config.kubernetes.io/path: config/demo-budget_poddisruptionbudget.yaml + labels: + app: cockroachdb + name: demo + name: demo-budget +spec: + minAvailable: 67% + selector: + matchLabels: + app: cockroachdb + name: demo +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + config.kubernetes.io/path: config/demo-public_service.yaml + labels: + app: cockroachdb + name: demo + name: demo-public +spec: + ports: + - name: grpc + port: 26257 + targetPort: 26257 + - name: http + port: 8080 + targetPort: 8080 + selector: + app: cockroachdb + name: demo +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + config.kubernetes.io/path: config/demo_service.yaml + prometheus.io/path: _status/vars + prometheus.io/port: "8080" + prometheus.io/scrape: "true" + service.alpha.kubernetes.io/tolerate-unready-endpoints: "true" + labels: + app: cockroachdb + name: demo + name: demo +spec: + clusterIP: None + ports: + - name: grpc + port: 26257 + targetPort: 26257 + - name: http + port: 8080 + targetPort: 8080 + selector: + app: cockroachdb + name: demo +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + annotations: + config.kubernetes.io/path: config/demo_statefulset.yaml + labels: + app: cockroachdb + name: demo + name: demo +spec: + replicas: 3 + selector: + matchLabels: + app: cockroachdb + name: demo + serviceName: demo + template: + metadata: + labels: + app: cockroachdb + name: demo + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - cockroachdb + topologyKey: kubernetes.io/hostname + weight: 100 + containers: + - command: + - /bin/bash + - -ecx + - | + # The use of qualified `+"`hostname -f`"+` is crucial: + # Other nodes aren't able to look up the unqualified hostname. + CRARGS=("start" "--logtostderr" "--insecure" "--host" "$(hostname -f)" "--http-host" "0.0.0.0") + # We only want to initialize a new cluster (by omitting the join flag) + # if we're sure that we're the first node (i.e. index 0) and that + # there aren't any other nodes running as part of the cluster that + # this is supposed to be a part of (which indicates that a cluster + # already exists and we should make sure not to create a new one). + # It's fine to run without --join on a restart if there aren't any + # other nodes. + if [ ! "$(hostname)" == "cockroachdb-0" ] || [ -e "/cockroach/cockroach-data/cluster_exists_marker" ] + then + # We don't join cockroachdb in order to avoid a node attempting + # to join itself, which currently doesn't work + # (https://github.com/cockroachdb/cockroach/issues/9625). + CRARGS+=("--join" "cockroachdb-public") + fi + exec /cockroach/cockroach ${CRARGS[*]} + image: cockroachdb/cockroach:v1.1.0 + imagePullPolicy: IfNotPresent + name: demo + ports: + - containerPort: 26257 + name: grpc + - containerPort: 8080 + name: http + volumeMounts: + - mountPath: /cockroach/cockroach-data + name: datadir + initContainers: + - args: + - -on-start=/on-start.sh + - -service=cockroachdb + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + image: cockroachdb/cockroach-k8s-init:0.1 + imagePullPolicy: IfNotPresent + name: bootstrap + volumeMounts: + - mountPath: /cockroach/cockroach-data + name: datadir + terminationGracePeriodSeconds: 60 + volumes: + - name: datadir + persistentVolumeClaim: + claimName: datadir + volumeClaimTemplates: + - metadata: + name: datadir + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +`) +} + +func TestFnContainerTransformer(t *testing.T) { + skipIfNoDocker(t) + + th := kusttest_test.MakeEnhancedHarness(t) + defer th.Reset() + + th.WriteK("/app", ` +resources: +- data.yaml +transformers: +- transf1.yaml +- transf2.yaml +`) + + th.WriteF("/app/data.yaml", ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + labels: + app: nginx + annotations: + tshirt-size: small # this injects the resource reservations +spec: + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx +`) + // This transformer should add resource reservations based on annotation in data.yaml + // See https://github.com/kubernetes-sigs/kustomize/tree/master/functions/examples/injection-tshirt-sizes + th.WriteF("/app/transf1.yaml", ` +apiVersion: examples.config.kubernetes.io/v1beta1 +kind: Validator +metadata: + name: valid + annotations: + config.kubernetes.io/function: |- + container: + image: gcr.io/kustomize-functions/example-tshirt:v0.2.0 +`) + // This transformer will check resources without and won't do any changes + // See https://github.com/kubernetes-sigs/kustomize/tree/master/functions/examples/validator-kubeval + th.WriteF("/app/transf2.yaml", ` +apiVersion: examples.config.kubernetes.io/v1beta1 +kind: Kubeval +metadata: + name: validate + annotations: + config.kubernetes.io/function: | + container: + image: gcr.io/kustomize-functions/example-validator-kubeval:v0.1.0 +spec: + strict: true + ignoreMissingSchemas: true + + # TODO: Update this to use network/volumes features. + # Relevant issues: + # - https://github.com/kubernetes-sigs/kustomize/issues/1901 + # - https://github.com/kubernetes-sigs/kustomize/issues/1902 + kubernetesVersion: "1.16.0" + schemaLocation: "file:///schemas" +`) + m := th.Run("/app", th.MakeOptionsPluginsEnabled()) + th.AssertActualEqualsExpected(m, ` +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + config.kubernetes.io/path: deployment_nginx.yaml + tshirt-size: small + labels: + app: nginx + name: nginx +spec: + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - image: nginx + name: nginx + resources: + requests: + cpu: 200m + memory: 50M +`) +} diff --git a/api/krusty/fnplugin_test/fnexectest.sh b/api/krusty/fnplugin_test/fnexectest.sh new file mode 100755 index 000000000..ed98a837c --- /dev/null +++ b/api/krusty/fnplugin_test/fnexectest.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +# not sure if we want to generate bash scripts, since we always want to run +# only trusted executables +cat <