diff --git a/pkg/commands/build/build.go b/pkg/commands/build/build.go index 1176f25b16..cf4945d323 100644 --- a/pkg/commands/build/build.go +++ b/pkg/commands/build/build.go @@ -62,7 +62,8 @@ url examples: func NewCmdBuild( out io.Writer, fs fs.FileSystem, rf *resmap.Factory, - ptf transformer.Factory) *cobra.Command { + ptf transformer.Factory, + b bool) *cobra.Command { var o Options cmd := &cobra.Command{ @@ -75,7 +76,7 @@ func NewCmdBuild( if err != nil { return err } - return o.RunBuild(out, fs, rf, ptf) + return o.RunBuild(out, fs, rf, ptf, b) }, } cmd.Flags().StringVarP( @@ -102,13 +103,14 @@ func (o *Options) Validate(args []string) error { // RunBuild runs build command. func (o *Options) RunBuild( out io.Writer, fSys fs.FileSystem, - rf *resmap.Factory, ptf transformer.Factory) error { + rf *resmap.Factory, ptf transformer.Factory, + b bool) error { ldr, err := loader.NewLoader(o.kustomizationPath, fSys) if err != nil { return err } defer ldr.Cleanup() - kt, err := target.NewKustTarget(ldr, rf, ptf) + kt, err := target.NewKustTarget(ldr, rf, ptf, b) if err != nil { return err } diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index e2109249e7..d327bd2300 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -68,7 +68,7 @@ See https://sigs.k8s.io/kustomize build.NewCmdBuild( stdOut, fSys, resmap.NewFactory(resource.NewFactory(uf)), - transformer.NewFactoryImpl()), + transformer.NewFactoryImpl(), genMetaArgs.PluginConfig.GoEnabled), edit.NewCmdEdit(fSys, validator.NewKustValidator(), uf), misc.NewCmdConfig(fSys), misc.NewCmdVersion(stdOut), diff --git a/pkg/pgmconfig/constants.go b/pkg/pgmconfig/constants.go index 008b69da92..8418a75cea 100644 --- a/pkg/pgmconfig/constants.go +++ b/pkg/pgmconfig/constants.go @@ -28,5 +28,6 @@ var KustomizationFileNames = []string{ } const ( - PgmName = "kustomize" + PgmName = "kustomize" + PluginsDir = "plugins" ) diff --git a/pkg/plugins/transformers.go b/pkg/plugins/transformers.go new file mode 100644 index 0000000000..aae6895be2 --- /dev/null +++ b/pkg/plugins/transformers.go @@ -0,0 +1,95 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package plugins + +import ( + "fmt" + "path/filepath" + "plugin" + + "github.com/pkg/errors" + "sigs.k8s.io/kustomize/pkg/ifc" + "sigs.k8s.io/kustomize/pkg/pgmconfig" + "sigs.k8s.io/kustomize/pkg/resid" + "sigs.k8s.io/kustomize/pkg/resmap" + "sigs.k8s.io/kustomize/pkg/resource" + "sigs.k8s.io/kustomize/pkg/transformers" +) + +const transformerSymbol = "Transformer" + +type Configurable interface { + Config(k ifc.Kunstructured) error +} + +type transformerLoader struct { + pluginDir string + enabled bool +} + +func NewTransformerLoader(b bool) transformerLoader { + return transformerLoader{ + pluginDir: filepath.Join(pgmconfig.ConfigRoot(), pgmconfig.PluginsDir), + enabled: b, + } +} + +func (l transformerLoader) Load(rm resmap.ResMap) ([]transformers.Transformer, error) { + if len(rm) == 0 { + return nil, nil + } + if !l.enabled { + return nil, fmt.Errorf("plugin is not enabled") + } + var result []transformers.Transformer + for id, res := range rm { + t, err := l.load(id, res) + if err != nil { + return nil, err + } + result = append(result, t) + } + return result, nil +} + +func (l transformerLoader) load(id resid.ResId, res *resource.Resource) (transformers.Transformer, error) { + fileName := filepath.Join(l.pluginDir, id.Gvk().Kind+".so") + goPlugin, err := plugin.Open(fileName) + if err != nil { + return nil, fmt.Errorf("plugin %s file not opened", fileName) + } + + symbol, err := goPlugin.Lookup(transformerSymbol) + if err != nil { + return nil, fmt.Errorf("plugin %s fails lookup", fileName) + } + + c, ok := symbol.(Configurable) + if !ok { + return nil, fmt.Errorf("plugin %s not configurable", fileName) + } + err = c.Config(res) + if err != nil { + return nil, errors.Wrapf(err, "plugin %s fails configuration", fileName) + } + + t, ok := c.(transformers.Transformer) + if !ok { + return nil, fmt.Errorf("plugin %s not a transformer", fileName) + } + return t, nil +} diff --git a/pkg/target/kusttarget.go b/pkg/target/kusttarget.go index 25e5aff9e8..3dfb02adba 100644 --- a/pkg/target/kusttarget.go +++ b/pkg/target/kusttarget.go @@ -31,6 +31,7 @@ import ( interror "sigs.k8s.io/kustomize/pkg/internal/error" patchtransformer "sigs.k8s.io/kustomize/pkg/patch/transformer" "sigs.k8s.io/kustomize/pkg/pgmconfig" + "sigs.k8s.io/kustomize/pkg/plugins" "sigs.k8s.io/kustomize/pkg/resmap" "sigs.k8s.io/kustomize/pkg/resource" "sigs.k8s.io/kustomize/pkg/transformers" @@ -40,17 +41,19 @@ import ( // KustTarget encapsulates the entirety of a kustomization build. type KustTarget struct { - kustomization *types.Kustomization - ldr ifc.Loader - rFactory *resmap.Factory - tFactory transformer.Factory + kustomization *types.Kustomization + ldr ifc.Loader + rFactory *resmap.Factory + tFactory transformer.Factory + goPluginEnabled bool } // NewKustTarget returns a new instance of KustTarget primed with a Loader. func NewKustTarget( ldr ifc.Loader, rFactory *resmap.Factory, - tFactory transformer.Factory) (*KustTarget, error) { + tFactory transformer.Factory, + b bool) (*KustTarget, error) { content, err := loadKustFile(ldr) if err != nil { return nil, err @@ -68,10 +71,11 @@ func NewKustTarget( strings.Join(errs, "\n"), ldr.Root()) } return &KustTarget{ - kustomization: &k, - ldr: ldr, - rFactory: rFactory, - tFactory: tFactory, + kustomization: &k, + ldr: ldr, + rFactory: rFactory, + tFactory: tFactory, + goPluginEnabled: b, }, nil } @@ -252,7 +256,7 @@ func (kt *KustTarget) accumulateBases() ( continue } subKt, err := NewKustTarget( - ldr, kt.rFactory, kt.tFactory) + ldr, kt.rFactory, kt.tFactory, kt.goPluginEnabled) if err != nil { errs.Append(errors.Wrap(err, "couldn't make target for "+path)) ldr.Cleanup() @@ -318,5 +322,21 @@ func (kt *KustTarget) newTransformer( return nil, err } r = append(r, t) + + tp, err := kt.loadTransformerPlugins() + if err != nil { + return nil, err + } + r = append(r, tp...) return transformers.NewMultiTransformer(r), nil } + +func (kt *KustTarget) loadTransformerPlugins() ([]transformers.Transformer, error) { + transformerPluginConfigs, err := kt.rFactory.FromFiles( + kt.ldr, kt.kustomization.Transformers) + if err != nil { + return nil, err + } + tl := plugins.NewTransformerLoader(kt.goPluginEnabled) + return tl.Load(transformerPluginConfigs) +} diff --git a/pkg/target/kusttarget_test.go b/pkg/target/kusttarget_test.go index e57fe0f374..cde95b5443 100644 --- a/pkg/target/kusttarget_test.go +++ b/pkg/target/kusttarget_test.go @@ -204,7 +204,7 @@ func TestResources(t *testing.T) { } func TestKustomizationNotFound(t *testing.T) { - _, err := NewKustTarget(loadertest.NewFakeLoader("/foo"), nil, nil) + _, err := NewKustTarget(loadertest.NewFakeLoader("/foo"), nil, nil, false) if err == nil { t.Fatalf("expected an error") } diff --git a/pkg/target/kusttestharness_test.go b/pkg/target/kusttestharness_test.go index ea3b712bd8..70f9978621 100644 --- a/pkg/target/kusttestharness_test.go +++ b/pkg/target/kusttestharness_test.go @@ -40,6 +40,7 @@ type KustTestHarness struct { t *testing.T rf *resmap.Factory ldr loadertest.FakeLoader + b bool } func NewKustTestHarness(t *testing.T, path string) *KustTestHarness { @@ -55,12 +56,13 @@ func NewKustTestHarnessWithPluginConfig( rf: resmap.NewFactory(resource.NewFactory( kunstruct.NewKunstructuredFactoryWithGeneratorArgs( &types.GeneratorMetaArgs{PluginConfig: config}))), - ldr: loadertest.NewFakeLoader(path)} + ldr: loadertest.NewFakeLoader(path), + b: config.GoEnabled} } func (th *KustTestHarness) makeKustTarget() *KustTarget { kt, err := NewKustTarget( - th.ldr, th.rf, transformer.NewFactoryImpl()) + th.ldr, th.rf, transformer.NewFactoryImpl(), th.b) if err != nil { th.t.Fatalf("Unexpected construction error %v", err) } diff --git a/pkg/target/transformerplugin_test.go b/pkg/target/transformerplugin_test.go new file mode 100644 index 0000000000..1345d9cdfe --- /dev/null +++ b/pkg/target/transformerplugin_test.go @@ -0,0 +1,163 @@ +/* +Copyright 2019 The Kubernetes Authors. + Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package target_test + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + "fmt" + "sigs.k8s.io/kustomize/pkg/pgmconfig" + "sigs.k8s.io/kustomize/pkg/types" +) + +func writeDeployment(th *KustTestHarness, path string) { + th.writeF(path, ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myDeployment +spec: + template: + metadata: + labels: + backend: awesome + spec: + containers: + - name: whatever + image: whatever +`) +} + +func writeStringPrefixer(th *KustTestHarness, path string) { + th.writeF(path, ` +apiVersion: strings.microwoosh.com/v1 +kind: StringPrefixer +metadata: + name: myStringPrefixer +prefix: apple- +`) +} + +func writeDatePrefixer(th *KustTestHarness, path string) { + th.writeF(path, ` +apiVersion: team.dater.com/v1 +kind: DatePrefixer +metadata: + name: myDatePrefixer +`) +} + +func buildGoPlugins(dir, filename string) error { + commands := []string{ + "build", + "-buildmode", + "plugin", + "-tags=plugin", + "-o", + filename + ".so", + filename + ".go", + } + goBin := filepath.Join(os.Getenv("GOROOT"), "bin", "go") + if _, err := os.Stat(goBin); err != nil { + return fmt.Errorf("go binary not found %s", goBin) + } + cmd := exec.Command(goBin, commands...) + cmd.Env = os.Environ() + cmd.Dir = filepath.Join(dir, "kustomize", "plugins") + + return cmd.Run() +} + +func TestOrderedTransformers(t *testing.T) { + dir, err := filepath.Abs("../../..") + if err != nil { + t.Errorf("unexpected error %v", err) + } + os.Setenv(pgmconfig.XDG_CONFIG_HOME, dir) + defer os.Unsetenv(pgmconfig.XDG_CONFIG_HOME) + + err = buildGoPlugins(dir, "StringPrefixer") + if err != nil { + t.Errorf("unexpected error %v", err) + } + + err = buildGoPlugins(dir, "DatePrefixer") + if err != nil { + t.Errorf("unexpected error %v", err) + } + + th := NewKustTestHarnessWithPluginConfig( + t, "/app", types.PluginConfig{GoEnabled: true}) + th.writeK("/app", ` +resources: +- deployment.yaml +transformers: +- stringPrefixer.yaml +`) + writeDeployment(th, "/app/deployment.yaml") + writeStringPrefixer(th, "/app/stringPrefixer.yaml") + writeDatePrefixer(th, "/app/datePrefixer.yaml") + m, err := th.makeKustTarget().MakeCustomizedResMap() + if err != nil { + t.Fatalf("Err: %v", err) + } + th.assertActualEqualsExpected(m, ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: apple-myDeployment +spec: + template: + metadata: + labels: + backend: awesome + spec: + containers: + - image: whatever + name: whatever +`) +} + +func xTestTransformedTransformers(t *testing.T) { + th := NewKustTestHarnessWithPluginConfig( + t, "/app/overlay", types.PluginConfig{GoEnabled: true}) + + th.writeK("/app/base", ` +resources: +- stringPrefixer.yaml +transformers: +- datePrefixer.yaml +`) + writeStringPrefixer(th, "/app/base/stringPrefixer.yaml") + writeDatePrefixer(th, "/app/base/datePrefixer.yaml") + + th.writeK("/app/overlay", ` +resources: +- deployment.yaml +transformers: +- ../base +`) + writeDeployment(th, "/app/overlay/deployment.yaml") + + m, err := th.makeKustTarget().MakeCustomizedResMap() + if err != nil { + t.Fatalf("Err: %v", err) + } + th.assertActualEqualsExpected(m, ` +HEY +`) +} diff --git a/pkg/transformers/transformer.go b/pkg/transformers/transformer.go index dc6f8807c3..fc0803fce5 100644 --- a/pkg/transformers/transformer.go +++ b/pkg/transformers/transformer.go @@ -17,7 +17,9 @@ limitations under the License. // Package transformers has implementations of resmap.ResMap transformers. package transformers -import "sigs.k8s.io/kustomize/pkg/resmap" +import ( + "sigs.k8s.io/kustomize/pkg/resmap" +) // A Transformer modifies an instance of resmap.ResMap. type Transformer interface { diff --git a/plugins/DatePrefixer.go b/plugins/DatePrefixer.go new file mode 100644 index 0000000000..8f7daf93f9 --- /dev/null +++ b/plugins/DatePrefixer.go @@ -0,0 +1,30 @@ +// +build plugin + +package main + +import ( + "time" + + "sigs.k8s.io/kustomize/pkg/ifc" + "sigs.k8s.io/kustomize/pkg/resmap" + "sigs.k8s.io/kustomize/pkg/transformers" + "sigs.k8s.io/kustomize/pkg/transformers/config" +) + +type plugin struct{} + +var Transformer plugin + +func (p *plugin) Config(k ifc.Kunstructured) error { + return nil +} + +func (p *plugin) Transform(m resmap.ResMap) error { + tr, err := transformers.NewNamePrefixSuffixTransformer( + time.Now().Format("2006-01-02")+"-", "", + config.MakeDefaultConfig().NamePrefix) + if err != nil { + return err + } + return tr.Transform(m) +} diff --git a/plugins/StringPrefixer.go b/plugins/StringPrefixer.go new file mode 100644 index 0000000000..0c8de29f9b --- /dev/null +++ b/plugins/StringPrefixer.go @@ -0,0 +1,44 @@ +// +build plugin + +// Assuming GOPATH is something like +// ~/gopath +// and this source file is located at +// $GOPATH/src/sigs.k8s.io/kustomize/plugins/StringPrefixer.go, +// build it like this: +// dir=$GOPATH/src/sigs.k8s.io/kustomize/plugins +// go build -buildmode plugin -tags=plugin \ +// -o $dir/StringPrefixer.so $dir/StringPrefixer.go + +package main + +import ( + "sigs.k8s.io/kustomize/pkg/ifc" + "sigs.k8s.io/kustomize/pkg/resmap" + "sigs.k8s.io/kustomize/pkg/transformers" + "sigs.k8s.io/kustomize/pkg/transformers/config" +) + +type plugin struct{ + prefix string +} + +var Transformer plugin + +func (p *plugin) Config(k ifc.Kunstructured) error { + var err error + p.prefix, err = k.GetFieldValue("prefix") + if err != nil { + return err + } + return nil +} + +func (p *plugin) Transform(m resmap.ResMap) error { + tr, err := transformers.NewNamePrefixSuffixTransformer( + p.prefix, "", + config.MakeDefaultConfig().NamePrefix) + if err != nil { + return err + } + return tr.Transform(m) +} diff --git a/plugins/kvMaker.go b/plugins/kvMaker.go new file mode 100644 index 0000000000..2250f3e46d --- /dev/null +++ b/plugins/kvMaker.go @@ -0,0 +1,24 @@ +// +build plugin + +package main +var database = map[string]string{ + "TREE": "oak", + "ROCKET": "Saturn V", + "FRUIT": "apple", + "VEGETABLE": "carrot", + "SIMPSON": "homer", +} + +type plugin struct{} +var KVSource plugin +func (p plugin) Get( + root string, args []string) (map[string]string, error) { + r := make(map[string]string) + for _, k := range args { + v, ok := database[k] + if ok { + r[k] = v + } + } + return r, nil +}