Skip to content

Commit

Permalink
check for plugins file schema version and type
Browse files Browse the repository at this point in the history
Signed-off-by: Yingrong Zhao <yingrong.zhao@gmail.com>
  • Loading branch information
VinozzZ committed Jan 24, 2023
1 parent 2aa80a6 commit 62cd202
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 24 deletions.
95 changes: 83 additions & 12 deletions pkg/plugins/install.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package plugins

import (
"encoding/json"
"fmt"
"reflect"
"sort"

"get.porter.sh/porter/pkg/pkgmgmt"
"get.porter.sh/porter/pkg/portercontext"
"github.com/cnabio/cnab-go/schema"
)

// InstallPluginsSchemaVersion represents the version associated with the schema
// plugins configuration documents.
var InstallPluginsSchemaVersion = schema.Version("1.0.0")

type InstallOptions struct {
pkgmgmt.InstallOptions

Expand All @@ -25,8 +32,9 @@ func (o *InstallOptions) Validate(args []string, cxt *portercontext.Context) err
return fmt.Errorf("plugin URL should not be specified when --file is provided")
}

if o.Version != "" {
return fmt.Errorf("plugin version should not be specified when --file is provided")
// version should not be set to anything other than the default value
if o.Version != "" && o.Version != "latest" {
return fmt.Errorf("plugin version %s should not be specified when --file is provided", o.Version)
}

if _, err := cxt.FileSystem.Stat(o.File); err != nil {
Expand All @@ -39,20 +47,61 @@ func (o *InstallOptions) Validate(args []string, cxt *portercontext.Context) err
return o.InstallOptions.Validate(args)
}

// InstallFileOption is the go representation of plugin installation file format.
type InstallFileOption map[string]pkgmgmt.InstallOptions
// InstallPluginsSpec represents the user-defined configuration for plugins installation.
type InstallPluginsSpec struct {
SchemaType string `yaml:"schemaType"`
SchemaVersion string `yaml:"schemaVersion"`
InstallPluginsConfig `yaml:",inline"`
}

func (spec InstallPluginsSpec) Validate() error {
if InstallPluginsSchemaVersion != schema.Version(spec.SchemaVersion) {
if spec.SchemaVersion == "" {
spec.SchemaVersion = "(none)"
}
return fmt.Errorf("invalid schemaVersion provided: %s. This version of Porter is compatible with %s.", spec.SchemaVersion, InstallPluginsSchemaVersion)
}
return nil
}

// InstallPluginsConfig is the go representation of plugin installation file format.
type InstallPluginsConfig map[string]pkgmgmt.InstallOptions

func (ip InstallPluginsConfig) UnmarshalJSON(data []byte) error {
var raw map[string]interface{}
err := json.Unmarshal(data, &raw)
if err != nil {
return err
}

for k, v := range raw {
mapV, ok := v.(map[string]string)
if !ok {
continue
}
var opt pkgmgmt.InstallOptions
for k, v := range mapV {
if err := setField(opt, k, v); err != nil {
return err
}
}
ip[k] = opt
}

return nil
}

// InstallPluginsConfig is a sorted list of InstallationFileOption in alphabetical order.
type InstallPluginsConfig struct {
data InstallFileOption
// InstallPluginsConfigList is a sorted list of InstallationFileOption in alphabetical order.
type InstallPluginsConfigList struct {
data InstallPluginsConfig
keys []string
}

// NewInstallPluginConfigs returns a new instance of InstallPluginConfigs with plugins sorted in alphabetical order
// using their names.
func NewInstallPluginConfigs(opt InstallFileOption) InstallPluginsConfig {
func NewInstallPluginConfigs(opt InstallPluginsConfig) InstallPluginsConfigList {
keys := make([]string, 0, len(opt))
data := make(InstallFileOption, len(opt))
data := make(InstallPluginsConfig, len(opt))
for k, v := range opt {
keys = append(keys, k)

Expand All @@ -65,17 +114,39 @@ func NewInstallPluginConfigs(opt InstallFileOption) InstallPluginsConfig {
return keys[i] < keys[j]
})

return InstallPluginsConfig{
return InstallPluginsConfigList{
data: data,
keys: keys,
}
}

// Configs returns InstallOptions list in alphabetical order.
func (pc InstallPluginsConfig) Configs() []pkgmgmt.InstallOptions {
// Values returns InstallOptions list in alphabetical order.
func (pc InstallPluginsConfigList) Values() []pkgmgmt.InstallOptions {
value := make([]pkgmgmt.InstallOptions, 0, len(pc.keys))
for _, k := range pc.keys {
value = append(value, pc.data[k])
}
return value
}

func setField(obj interface{}, name string, value interface{}) error {
structValue := reflect.ValueOf(obj).Elem()
structFieldValue := structValue.FieldByName(name)

if !structFieldValue.IsValid() {
return fmt.Errorf("No such field: %s in obj", name)
}

if !structFieldValue.CanSet() {
return fmt.Errorf("Cannot set %s field value", name)
}

structFieldType := structFieldValue.Type()
val := reflect.ValueOf(value)
if structFieldType != val.Type() {
return fmt.Errorf("Provided value type didn't match obj field type")
}

structFieldValue.Set(val)
return nil
}
4 changes: 2 additions & 2 deletions pkg/plugins/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ func TestInstallOptions_Validate(t *testing.T) {
}

func TestInstallPluginsConfig(t *testing.T) {
input := InstallFileOption{"kubernetes": pkgmgmt.InstallOptions{URL: "test-kubernetes.com"}, "azure": pkgmgmt.InstallOptions{URL: "test-azure.com"}}
input := InstallPluginsConfig{"kubernetes": pkgmgmt.InstallOptions{URL: "test-kubernetes.com"}, "azure": pkgmgmt.InstallOptions{URL: "test-azure.com"}}
expected := []pkgmgmt.InstallOptions{{Name: "azure", PackageType: "plugin", URL: "test-azure.com"}, {Name: "kubernetes", PackageType: "plugin", URL: "test-kubernetes.com"}}

cfg := NewInstallPluginConfigs(input)
require.Equal(t, expected, cfg.Configs())
require.Equal(t, expected, cfg.Values())
}
23 changes: 14 additions & 9 deletions pkg/porter/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,23 +158,23 @@ func (p *Porter) InstallPlugin(ctx context.Context, opts plugins.InstallOptions)
ctx, log := tracing.StartSpan(ctx)
defer log.EndSpan()

installConfigs, err := p.getPluginInstallConfigs(ctx, opts)
installOpts, err := p.getPluginInstallOptions(ctx, opts)
if err != nil {
return err
}
for _, cfg := range installConfigs {
err := p.Plugins.Install(ctx, cfg)
for _, opt := range installOpts {
err := p.Plugins.Install(ctx, opt)
if err != nil {
return err
}

plugin, err := p.Plugins.GetMetadata(ctx, cfg.Name)
plugin, err := p.Plugins.GetMetadata(ctx, opt.Name)
if err != nil {
return fmt.Errorf("failed to get plugin metadata: %w", err)
}

v := plugin.GetVersionInfo()
fmt.Fprintf(p.Out, "installed %s plugin %s (%s)\n", cfg.Name, v.Version, v.Commit)
fmt.Fprintf(p.Out, "installed %s plugin %s (%s)\n", opt.Name, v.Version, v.Commit)
}

return nil
Expand All @@ -191,13 +191,13 @@ func (p *Porter) UninstallPlugin(ctx context.Context, opts pkgmgmt.UninstallOpti
return nil
}

func (p *Porter) getPluginInstallConfigs(ctx context.Context, opts plugins.InstallOptions) ([]pkgmgmt.InstallOptions, error) {
func (p *Porter) getPluginInstallOptions(ctx context.Context, opts plugins.InstallOptions) ([]pkgmgmt.InstallOptions, error) {
_, log := tracing.StartSpan(ctx)
defer log.EndSpan()

var installConfigs []pkgmgmt.InstallOptions
if opts.File != "" {
var data plugins.InstallFileOption
var data plugins.InstallPluginsSpec
if log.ShouldLog(zapcore.DebugLevel) {
// ignoring any error here, printing debug info isn't critical
contents, _ := p.FileSystem.ReadFile(opts.File)
Expand All @@ -207,9 +207,14 @@ func (p *Porter) getPluginInstallConfigs(ctx context.Context, opts plugins.Insta
if err := encoding.UnmarshalFile(p.FileSystem, opts.File, &data); err != nil {
return nil, fmt.Errorf("unable to parse %s as an installation document: %w", opts.File, err)
}
sortedCfgs := plugins.NewInstallPluginConfigs(data)

for _, config := range sortedCfgs.Configs() {
if err := data.Validate(); err != nil {
return nil, err
}

sortedCfgs := plugins.NewInstallPluginConfigs(data.InstallPluginsConfig)

for _, config := range sortedCfgs.Values() {
// if user specified a feed url or mirror using the flags, it will become
// the default value and apply to empty values parsed from the provided file
if config.FeedURL == "" {
Expand Down
2 changes: 2 additions & 0 deletions pkg/porter/testdata/plugins.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"schemaType": "Plugins",
"schemaVersion": "1.0.0",
"plugin1": {
"version": "v1.0"
},
Expand Down
2 changes: 2 additions & 0 deletions pkg/porter/testdata/plugins.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
schemaType: Plugins
schemaVersion: 1.0.0
plugin1:
version: v1.0
plugin2:
Expand Down
7 changes: 6 additions & 1 deletion tests/smoke/hello_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@ func TestHelloBundle(t *testing.T) {

test.PrepareTestBundle()
require.NoError(t, shx.Copy("testdata/buncfg.json", test.TestDir))
require.NoError(t, shx.Copy("testdata/plugins.yaml", test.TestDir))
test.Chdir(test.TestDir)

// Verify plugins installation
_, output := test.RequirePorter("plugins", "install", "-f", "plugins.yaml")
require.Contains(t, output, "installed azure plugin", "expected to see plugin successfully installed")

// Run a stateless action before we install and make sure nothing is persisted
_, output := test.RequirePorter("invoke", testdata.MyBuns, "--action=dry-run", "--reference", testdata.MyBunsRef, "-c=mybuns")
_, output = test.RequirePorter("invoke", testdata.MyBuns, "--action=dry-run", "--reference", testdata.MyBunsRef, "-c=mybuns")
t.Log(output)
test.RequireInstallationNotFound(test.CurrentNamespace(), testdata.MyBuns)

Expand Down
3 changes: 3 additions & 0 deletions tests/smoke/testdata/plugins.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
schemaType: Plugins
schemaVersion: 1.0.0
azure: {}

0 comments on commit 62cd202

Please sign in to comment.