diff --git a/pkg/plugins/generators.go b/pkg/plugins/generators.go new file mode 100644 index 0000000000..ab704b0ac1 --- /dev/null +++ b/pkg/plugins/generators.go @@ -0,0 +1,98 @@ +/* +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/pgmconfig" + "sigs.k8s.io/kustomize/pkg/resid" + "sigs.k8s.io/kustomize/pkg/resmap" + "sigs.k8s.io/kustomize/pkg/resource" +) + +const generatorSymbol = "Generator" + +type Generatable interface { + Generate() (resmap.ResMap, error) +} + +type generatorLoader struct { + pluginDir string + enabled bool + rf *resmap.Factory +} + +func NewGeneratorLoader(b bool, f *resmap.Factory) generatorLoader { + return generatorLoader{ + pluginDir: filepath.Join(pgmconfig.ConfigRoot(), pgmconfig.PluginsDir), + enabled: b, + rf: f, + } +} + +func (l generatorLoader) Load(rm resmap.ResMap) (resmap.ResMap, error) { + if len(rm) == 0 { + return nil, nil + } + if !l.enabled { + return nil, fmt.Errorf("plugin is not enabled") + } + var result resmap.ResMap + for id, res := range rm { + r, err := l.load(id, res) + if err != nil { + return nil, err + } + result, err = resmap.MergeWithErrorOnIdCollision(result, r) + if err != nil { + return nil, err + } + } + return result, nil +} + +func (l generatorLoader) load(id resid.ResId, res *resource.Resource) (resmap.ResMap, 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(generatorSymbol) + 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) + } + + g, ok := c.(Generatable) + if !ok { + return nil, fmt.Errorf("plugin %s not a transformer", fileName) + } + return g.Generate() +} diff --git a/pkg/target/generatorplugin_test.go b/pkg/target/generatorplugin_test.go new file mode 100644 index 0000000000..23bd47f152 --- /dev/null +++ b/pkg/target/generatorplugin_test.go @@ -0,0 +1,73 @@ +/* +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" + "path/filepath" + "testing" + + "sigs.k8s.io/kustomize/pkg/pgmconfig" + "sigs.k8s.io/kustomize/pkg/types" +) + +func writeGenerator(th *KustTestHarness, path string) { + th.writeF(path, ` +apiVersion: strings.microwoosh.com/v1 +kind: ServiceGenerator +metadata: + name: myServiceGenerator +service: my-service +port: "12345" +`) +} + +func TestGeneratorPlugin(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, "ServiceGenerator") + if err != nil { + t.Errorf("unexpected error %v", err) + } + + th := NewKustTestHarnessWithPluginConfig( + t, "/app", types.PluginConfig{GoEnabled: true}) + th.writeK("/app", ` +generators: +- serviceGenerator.yaml +`) + writeGenerator(th, "/app/serviceGenerator.yaml") + m, err := th.makeKustTarget().MakeCustomizedResMap() + if err != nil { + t.Fatalf("Err: %v", err) + } + th.assertActualEqualsExpected(m, ` +apiVersion: v1 +kind: Service +metadata: + labels: + app: dev + name: my-service +spec: + ports: + - port: 12345 + selector: + app: dev +`) +} diff --git a/pkg/target/kusttarget.go b/pkg/target/kusttarget.go index 3ed6599f7e..c01bf63eeb 100644 --- a/pkg/target/kusttarget.go +++ b/pkg/target/kusttarget.go @@ -155,7 +155,7 @@ func (kt *KustTarget) shouldAddHashSuffixesToGeneratedResources() bool { // holding customized resources and the data/rules used // to do so. The name back references and vars are // not yet fixed. -func (kt *KustTarget) AccumulateTarget() ( +func (kt *KustTarget) AccumulateTarget() ( // nolint: gocyclo ra *accumulator.ResAccumulator, err error) { // TODO(monopole): Get rid of the KustomizationErrors accumulator. // It's not consistently used, and complicates tests. @@ -173,6 +173,17 @@ func (kt *KustTarget) AccumulateTarget() ( if err != nil { errs.Append(errors.Wrap(err, "MergeResourcesWithErrorOnIdCollision")) } + resourceFromGenerators, err := kt.loadGeneratorPlugins() + if err != nil { + errs.Append(errors.Wrap(err, "failed to load resources from generators")) + } + if len(errs.Get()) > 0 { + return ra, errs + } + err = ra.MergeResourcesWithErrorOnIdCollision(resourceFromGenerators) + if err != nil { + errs.Append(errors.Wrap(err, "MergeResourcesWithErrorOnIdCollision")) + } tConfig, err := config.MakeTransformerConfig( kt.ldr, kt.kustomization.Configurations) if err != nil { @@ -341,3 +352,13 @@ func (kt *KustTarget) loadTransformerPlugins() ([]transformers.Transformer, erro } return plugins.NewTransformerLoader(kt.pluginConfig).Load(transformerPluginConfigs) } + +func (kt *KustTarget) loadGeneratorPlugins() (resmap.ResMap, error) { + generatorPluginConfigs, err := kt.rFactory.FromFiles( + kt.ldr, kt.kustomization.Generators) + if err != nil { + return nil, err + } + gl := plugins.NewGeneratorLoader(kt.goPluginEnabled, kt.rFactory) + return gl.Load(generatorPluginConfigs) +} diff --git a/plugins/ServiceGenerator.go b/plugins/ServiceGenerator.go new file mode 100644 index 0000000000..4367daa5e1 --- /dev/null +++ b/plugins/ServiceGenerator.go @@ -0,0 +1,57 @@ +package main + +import ( + "bytes" + "text/template" + + "sigs.k8s.io/kustomize/k8sdeps/kunstruct" + "sigs.k8s.io/kustomize/pkg/ifc" + "sigs.k8s.io/kustomize/pkg/resmap" + "sigs.k8s.io/kustomize/pkg/resource" +) + +type plugin struct { + ServiceName string + Port string +} + +var Generator plugin + +var manifest = ` +apiVersion: v1 +kind: Service +metadata: + labels: + app: dev + name: {{.ServiceName}} +spec: + ports: + - port: {{.Port}} + selector: + app: dev +` + +func (p *plugin) Config(k ifc.Kunstructured) error { + var err error + p.ServiceName, err = k.GetFieldValue("service") + if err != nil { + return err + } + p.Port, err = k.GetFieldValue("port") + if err != nil { + return err + } + return nil +} + +func (p *plugin) Generate() (resmap.ResMap, error) { + var buf bytes.Buffer + + temp := template.Must(template.New("manifest").Parse(manifest)) + err := temp.Execute(&buf, p) + if err != nil { + return nil, err + } + rf := resmap.NewFactory(resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl())) + return rf.NewResMapFromBytes(buf.Bytes()) +}