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
This commit is contained in:
Dmitry Ukov 2020-04-14 16:21:02 +04:00
parent a0cb765d73
commit 39ee048451
12 changed files with 335 additions and 3 deletions

View File

@ -29,6 +29,7 @@ func NewDocumentCommand(rootSettings *environment.AirshipCTLSettings) *cobra.Com
documentRootCmd.AddCommand(NewDocumentPullCommand(rootSettings)) documentRootCmd.AddCommand(NewDocumentPullCommand(rootSettings))
documentRootCmd.AddCommand(NewRenderCommand(rootSettings)) documentRootCmd.AddCommand(NewRenderCommand(rootSettings))
documentRootCmd.AddCommand(NewDocumentPluginCommand(rootSettings))
return documentRootCmd return documentRootCmd
} }

66
cmd/document/plugin.go Normal file
View File

@ -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
}

View File

@ -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)
}
}

View File

@ -5,6 +5,7 @@ Usage:
Available Commands: Available Commands:
help Help about any command help Help about any command
plugin used as kustomize exec plugin
pull pulls documents from remote git repository pull pulls documents from remote git repository
render Render documents from model render Render documents from model

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -18,7 +18,6 @@ import (
"io" "io"
"strings" "strings"
"sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/kustomize/api/krusty" "sigs.k8s.io/kustomize/api/krusty"
"sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
@ -84,8 +83,11 @@ func NewBundle(fSys FileSystem, kustomizePath string) (Bundle, error) {
var o = krusty.Options{ var o = krusty.Options{
DoLegacyResourceSort: true, // Default and what we want DoLegacyResourceSort: true, // Default and what we want
LoadRestrictions: options.LoadRestrictions, LoadRestrictions: options.LoadRestrictions,
DoPrune: false, // Default DoPrune: false, // Default
PluginConfig: konfig.DisabledPluginConfig(), // Default PluginConfig: &types.PluginConfig{
AbsPluginHome: kustomizePath,
PluginRestrictions: types.PluginRestrictionsNone,
},
} }
kustomizer := krusty.MakeKustomizer(fSys, &o) kustomizer := krusty.MakeKustomizer(fSys, &o)

View File

@ -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())
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)