From 39ee0484515f8f640d3d5bb0675262ad946cf255 Mon Sep 17 00:00:00 2001 From: Dmitry Ukov Date: Tue, 14 Apr 2020 16:21:02 +0400 Subject: [PATCH] Introduce document plugin subcommand airship document plugin is intended to be executed as an exec plugin for kustomize document model. Environment variable is used to gather plugin configuration. Plugin to execute is determined based on group-version-kind specified in plugin configuration. Each airship plugin must implement plugin interface. Relates-To: #173 Change-Id: I4f6c3b5be140c0d8fd7519f1cedd33de1cef662c --- cmd/document/document.go | 1 + cmd/document/plugin.go | 66 +++++++++++++++++++ cmd/document/plugin_test.go | 48 ++++++++++++++ .../document-with-defaults.golden | 1 + ...document-plugin-cmd-with-empty-args.golden | 7 ++ .../document-plugin-cmd-with-help.golden | 27 ++++++++ ...-plugin-cmd-with-nonexistent-config.golden | 7 ++ pkg/document/bundle.go | 8 ++- pkg/document/plugin/errors.go | 32 +++++++++ pkg/document/plugin/run.go | 50 ++++++++++++++ pkg/document/plugin/run_test.go | 61 +++++++++++++++++ pkg/document/plugin/types/plugin.go | 30 +++++++++ 12 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 cmd/document/plugin.go create mode 100644 cmd/document/plugin_test.go create mode 100644 cmd/document/testdata/TestPluginGoldenOutput/document-plugin-cmd-with-empty-args.golden create mode 100644 cmd/document/testdata/TestPluginGoldenOutput/document-plugin-cmd-with-help.golden create mode 100644 cmd/document/testdata/TestPluginGoldenOutput/document-plugin-cmd-with-nonexistent-config.golden create mode 100644 pkg/document/plugin/errors.go create mode 100644 pkg/document/plugin/run.go create mode 100644 pkg/document/plugin/run_test.go create mode 100644 pkg/document/plugin/types/plugin.go diff --git a/cmd/document/document.go b/cmd/document/document.go index 4aa9ba12a..3a17dc39d 100644 --- a/cmd/document/document.go +++ b/cmd/document/document.go @@ -29,6 +29,7 @@ func NewDocumentCommand(rootSettings *environment.AirshipCTLSettings) *cobra.Com documentRootCmd.AddCommand(NewDocumentPullCommand(rootSettings)) documentRootCmd.AddCommand(NewRenderCommand(rootSettings)) + documentRootCmd.AddCommand(NewDocumentPluginCommand(rootSettings)) return documentRootCmd } diff --git a/cmd/document/plugin.go b/cmd/document/plugin.go new file mode 100644 index 000000000..6e12d1841 --- /dev/null +++ b/cmd/document/plugin.go @@ -0,0 +1,66 @@ +/* + 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 + + https://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 document + +import ( + "io/ioutil" + + "github.com/spf13/cobra" + + "opendev.org/airship/airshipctl/pkg/document/plugin" + "opendev.org/airship/airshipctl/pkg/environment" +) + +var longDescription = `Subcommand reads configuration file CONFIG passed as +a first argument and determines a particular plugin to execute. Additional +arguments may be passed to this sub-command abd can be used by the +particular plugin. CONFIG file must be structured as kubernetes +manifest (i.e. resource) and must have 'apiVersion' and 'kind' keys. + +Example: +$ cat /tmp/generator.yaml +--- +apiVersion: airshipit.org/v1alpha1 +kind: BareMetalHostGenerator +spec: + hostList: + - mac: 00:aa:bb:cc:dd + powerAddress: redfish+http://1.2.3.4/ + +$ airshipctl document plugin /tmp/generator.yaml + +subcommand will try to identify appropriate plugin using apiVersion and +kind keys (a.k.a group, version, kind) as an identifier. If appropriate +plugin was not found command returns an error. +` + +// NewDocumentPluginCommand creates a new command which can act as kustomize +// exec plugin. +func NewDocumentPluginCommand(rootSetting *environment.AirshipCTLSettings) *cobra.Command { + pluginCmd := &cobra.Command{ + Use: "plugin CONFIG [ARGS]", + Short: "used as kustomize exec plugin", + Long: longDescription, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := ioutil.ReadFile(args[0]) + if err != nil { + return err + } + return plugin.ConfigureAndRun(rootSetting, cfg, cmd.InOrStdin(), cmd.OutOrStdout()) + }, + } + return pluginCmd +} diff --git a/cmd/document/plugin_test.go b/cmd/document/plugin_test.go new file mode 100644 index 000000000..ee7f9502e --- /dev/null +++ b/cmd/document/plugin_test.go @@ -0,0 +1,48 @@ +/* + 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 + + https://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 document + +import ( + "fmt" + "testing" + + "opendev.org/airship/airshipctl/testutil" +) + +func TestPlugin(t *testing.T) { + cmdTests := []*testutil.CmdTest{ + { + Name: "document-plugin-cmd-with-help", + CmdLine: "--help", + Cmd: NewDocumentPluginCommand(nil), + }, + { + Name: "document-plugin-cmd-with-empty-args", + CmdLine: "", + Error: fmt.Errorf("requires at least 1 arg(s), only received 0"), + Cmd: NewDocumentPluginCommand(nil), + }, + { + Name: "document-plugin-cmd-with-nonexistent-config", + CmdLine: "/some/random/path.yaml", + Error: fmt.Errorf("open /some/random/path.yaml: no such file or directory"), + Cmd: NewDocumentPluginCommand(nil), + }, + } + + for _, tt := range cmdTests { + testutil.RunTest(t, tt) + } +} diff --git a/cmd/document/testdata/TestDocumentGoldenOutput/document-with-defaults.golden b/cmd/document/testdata/TestDocumentGoldenOutput/document-with-defaults.golden index aa02bac6a..53da27d7a 100644 --- a/cmd/document/testdata/TestDocumentGoldenOutput/document-with-defaults.golden +++ b/cmd/document/testdata/TestDocumentGoldenOutput/document-with-defaults.golden @@ -5,6 +5,7 @@ Usage: Available Commands: help Help about any command + plugin used as kustomize exec plugin pull pulls documents from remote git repository render Render documents from model diff --git a/cmd/document/testdata/TestPluginGoldenOutput/document-plugin-cmd-with-empty-args.golden b/cmd/document/testdata/TestPluginGoldenOutput/document-plugin-cmd-with-empty-args.golden new file mode 100644 index 000000000..8e125e5c7 --- /dev/null +++ b/cmd/document/testdata/TestPluginGoldenOutput/document-plugin-cmd-with-empty-args.golden @@ -0,0 +1,7 @@ +Error: requires at least 1 arg(s), only received 0 +Usage: + plugin CONFIG [ARGS] [flags] + +Flags: + -h, --help help for plugin + diff --git a/cmd/document/testdata/TestPluginGoldenOutput/document-plugin-cmd-with-help.golden b/cmd/document/testdata/TestPluginGoldenOutput/document-plugin-cmd-with-help.golden new file mode 100644 index 000000000..3e032c273 --- /dev/null +++ b/cmd/document/testdata/TestPluginGoldenOutput/document-plugin-cmd-with-help.golden @@ -0,0 +1,27 @@ +Subcommand reads configuration file CONFIG passed as +a first argument and determines a particular plugin to execute. Additional +arguments may be passed to this sub-command abd can be used by the +particular plugin. CONFIG file must be structured as kubernetes +manifest (i.e. resource) and must have 'apiVersion' and 'kind' keys. + +Example: +$ cat /tmp/generator.yaml +--- +apiVersion: airshipit.org/v1alpha1 +kind: BareMetalHostGenerator +spec: + hostList: + - mac: 00:aa:bb:cc:dd + powerAddress: redfish+http://1.2.3.4/ + +$ airshipctl document plugin /tmp/generator.yaml + +subcommand will try to identify appropriate plugin using apiVersion and +kind keys (a.k.a group, version, kind) as an identifier. If appropriate +plugin was not found command returns an error. + +Usage: + plugin CONFIG [ARGS] [flags] + +Flags: + -h, --help help for plugin diff --git a/cmd/document/testdata/TestPluginGoldenOutput/document-plugin-cmd-with-nonexistent-config.golden b/cmd/document/testdata/TestPluginGoldenOutput/document-plugin-cmd-with-nonexistent-config.golden new file mode 100644 index 000000000..6c1fedf43 --- /dev/null +++ b/cmd/document/testdata/TestPluginGoldenOutput/document-plugin-cmd-with-nonexistent-config.golden @@ -0,0 +1,7 @@ +Error: open /some/random/path.yaml: no such file or directory +Usage: + plugin CONFIG [ARGS] [flags] + +Flags: + -h, --help help for plugin + diff --git a/pkg/document/bundle.go b/pkg/document/bundle.go index 143906e79..1cdd01b36 100644 --- a/pkg/document/bundle.go +++ b/pkg/document/bundle.go @@ -18,7 +18,6 @@ import ( "io" "strings" - "sigs.k8s.io/kustomize/api/konfig" "sigs.k8s.io/kustomize/api/krusty" "sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/types" @@ -84,8 +83,11 @@ func NewBundle(fSys FileSystem, kustomizePath string) (Bundle, error) { var o = krusty.Options{ DoLegacyResourceSort: true, // Default and what we want LoadRestrictions: options.LoadRestrictions, - DoPrune: false, // Default - PluginConfig: konfig.DisabledPluginConfig(), // Default + DoPrune: false, // Default + PluginConfig: &types.PluginConfig{ + AbsPluginHome: kustomizePath, + PluginRestrictions: types.PluginRestrictionsNone, + }, } kustomizer := krusty.MakeKustomizer(fSys, &o) diff --git a/pkg/document/plugin/errors.go b/pkg/document/plugin/errors.go new file mode 100644 index 000000000..c1dcda66e --- /dev/null +++ b/pkg/document/plugin/errors.go @@ -0,0 +1,32 @@ +/* + 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 + + https://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 plugin + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// ErrPluginNotFound is returned if a plugin was not found in the plugin +// registry +type ErrPluginNotFound struct { + //PluginID group, version and kind plugin identifier + PluginID schema.GroupVersionKind +} + +func (e ErrPluginNotFound) Error() string { + return fmt.Sprintf("plugin identified by %s was not found", e.PluginID.String()) +} diff --git a/pkg/document/plugin/run.go b/pkg/document/plugin/run.go new file mode 100644 index 000000000..013710d94 --- /dev/null +++ b/pkg/document/plugin/run.go @@ -0,0 +1,50 @@ +/* + 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 + + https://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 plugin + +import ( + "io" + + "sigs.k8s.io/yaml" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + + "opendev.org/airship/airshipctl/pkg/document/plugin/types" + "opendev.org/airship/airshipctl/pkg/environment" +) + +// Registry contains factory functions for the available plugins +var Registry = make(map[schema.GroupVersionKind]types.Factory) + +// ConfigureAndRun executes particular plugin based on group, version, kind +// which have been specified in configuration file. Config file should be +// supplied as a first element of args slice +func ConfigureAndRun(settings *environment.AirshipCTLSettings, pluginCfg []byte, in io.Reader, out io.Writer) error { + var cfg unstructured.Unstructured + if err := yaml.Unmarshal(pluginCfg, &cfg); err != nil { + return err + } + pluginFactory, ok := Registry[cfg.GroupVersionKind()] + if !ok { + return ErrPluginNotFound{PluginID: cfg.GroupVersionKind()} + } + + plugin, err := pluginFactory(settings, pluginCfg) + if err != nil { + return err + } + return plugin.Run(in, out) +} diff --git a/pkg/document/plugin/run_test.go b/pkg/document/plugin/run_test.go new file mode 100644 index 000000000..b48543898 --- /dev/null +++ b/pkg/document/plugin/run_test.go @@ -0,0 +1,61 @@ +/* + 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 + + https://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 plugin_test + +import ( + "io" + "testing" + + "github.com/stretchr/testify/assert" + + "opendev.org/airship/airshipctl/pkg/document/plugin" + "opendev.org/airship/airshipctl/pkg/environment" +) + +func TestConfigureAndRun(t *testing.T) { + testCases := []struct { + pluginCfg []byte + settings *environment.AirshipCTLSettings + expectedError string + in io.Reader + out io.Writer + }{ + { + pluginCfg: []byte(""), + expectedError: "plugin identified by /, Kind= was not found", + }, + { + pluginCfg: []byte(`--- +apiVersion: airshipit.org/v1alpha1 +kind: UnknownPlugin +spec: + someField: someValue`), + expectedError: "plugin identified by airshipit.org/v1alpha1, Kind=UnknownPlugin was not found", + }, + { + pluginCfg: []byte(`--- +apiVersion: airshipit.org/v1alpha1 +kind: BareMetalGenereator +spec: - + someField: someValu`), + expectedError: "error converting YAML to JSON: yaml: line 4: block sequence entries are not allowed in this context", + }, + } + + for _, tc := range testCases { + err := plugin.ConfigureAndRun(tc.settings, tc.pluginCfg, tc.in, tc.out) + assert.EqualError(t, err, tc.expectedError) + } +} diff --git a/pkg/document/plugin/types/plugin.go b/pkg/document/plugin/types/plugin.go new file mode 100644 index 000000000..1d24a4bcc --- /dev/null +++ b/pkg/document/plugin/types/plugin.go @@ -0,0 +1,30 @@ +/* + 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 + + https://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 types + +import ( + "io" + + "opendev.org/airship/airshipctl/pkg/environment" +) + +// Plugin interface for airship document plugins +type Plugin interface { + Run(io.Reader, io.Writer) error +} + +// Factory function for plugins. Functions of such type are used in the plugin +// registry to instantiate a plugin object +type Factory func(*environment.AirshipCTLSettings, []byte) (Plugin, error)