Skip to content

Commit 9343f85

Browse files
authored
check for plugins file schema version and type (#2532)
* check for plugins file schema version and type Signed-off-by: Yingrong Zhao <yingrong.zhao@gmail.com>
1 parent c24f457 commit 9343f85

File tree

12 files changed

+252
-76
lines changed

12 files changed

+252
-76
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
2+
---
3+
title: "Quickly set up a Porter environment with required plugins"
4+
description: "How to install multiple plugins with Porter"
5+
date: "2023-01-24"
6+
authorname: "Yingrong Zhao"
7+
author: "@vinozzz"
8+
authorlink: "https://github.com/vinozzz"
9+
authorimage: "https://github.com/vinozzz.png"
10+
tags: ["best-practice", "plugins"]
11+
summary: |
12+
Setting up your Porter environment with your required plugins using the new `--file` flag with `porter plugins install` command.
13+
---
14+
15+
### Breaking change
16+
The recent porter v1.0.5 release introduced a new flag `--file` on `porter plugins install` command. Its intention is to allow users to install multiple plugins through a plugins definition file with a single porter command. However, it did not work as expected due to bad file format.
17+
18+
The fix that contains the correct schema has been published with a new v1.0.6 release. If you have an existing plugins file, please update it to work with v1.0.6+.
19+
20+
### Install multiple plugins with a single command
21+
Now, you can install multiple plugins using a plugin definition yaml file like below:
22+
```yaml
23+
schemaType: Plugins
24+
schemaVersion: 1.0.0
25+
plugins:
26+
azure:
27+
version: v1.0.1
28+
kubernetes:
29+
version: v1.0.1
30+
```
31+
32+
After creating the file, you can run the command:
33+
```bash
34+
porter plugins install -f <path-to-the-file>
35+
```
36+
37+
The output from the command should look like this:
38+
```
39+
installed azure plugin v1.0.1 (e361abc)
40+
installed kubernetes plugin v1.0.1 (f01c944)
41+
```
42+
43+
Make sure to update your current plugins schema file to the [latest format](/reference/file-formats/#plugins)
44+
Please [let us know][contact] how the change went (good or bad), and we are happy to help if you have questions, or you would like help with your migration.
45+
46+
[announced]: https://github.com/docker/roadmap/issues/209
47+
[Install Porter]: /install/
48+
[contact]: /community/

docs/content/reference/file-formats.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -162,21 +162,22 @@ You can use this [json schema][plugins-schema] to validate a plugins config file
162162
```yaml
163163
schemaType: Plugins
164164
schemaVersion: 1.0.0
165-
azure:
166-
version: v1.0.0
167-
feedURL: https://cdn.porter.sh/plugins/atom.xml
168-
url: https://example.com
169-
mirror: https://example.com
165+
plugins:
166+
azure:
167+
version: v1.0.0
168+
feedURL: https://cdn.porter.sh/plugins/atom.xml
169+
url: https://example.com
170+
mirror: https://example.com
170171
```
171172
172173
| Field | Required | Description |
173174
|----------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------|
174175
| schemaType | false | The type of document. This isn't used by Porter but is included when Porter outputs the file, so that editors can determine the resource type. |
175176
| schemaVersion | true | The version of the Plugins schema used in this file. |
176-
| <pluginName>.version | false | The version of the plugin. |
177-
| <pluginName>.feedURL | false | The url of an atom feed where the plugin can be downloaded.
178-
| <pluginName>.url | false | The url from where the plugin can be downloaded. |
179-
| <pluginName>.mirror | false | The mirror of official Porter assets. |
177+
| plugins.<pluginName>.version | false | The version of the plugin. |
178+
| plugins.<pluginName>.feedURL | false | The url of an atom feed where the plugin can be downloaded.
179+
| plugins.<pluginName>.url | false | The url from where the plugin can be downloaded. |
180+
| plugins.<pluginName>.mirror | false | The mirror of official Porter assets. |
180181
181182
[cs-schema]: /schema/v1/credential-set.schema.json
182183
[ps-schema]: /schema/v1/parameter-set.schema.json

pkg/plugins/install.go

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@ package plugins
33
import (
44
"fmt"
55
"sort"
6+
"strings"
67

78
"get.porter.sh/porter/pkg/pkgmgmt"
89
"get.porter.sh/porter/pkg/portercontext"
10+
"github.com/cnabio/cnab-go/schema"
911
)
1012

13+
// InstallPluginsSchemaVersion represents the version associated with the schema
14+
// plugins configuration documents.
15+
var InstallPluginsSchemaVersion = schema.Version("1.0.0")
16+
1117
type InstallOptions struct {
1218
pkgmgmt.InstallOptions
1319

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

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

3239
if _, err := cxt.FileSystem.Stat(o.File); err != nil {
@@ -39,20 +46,41 @@ func (o *InstallOptions) Validate(args []string, cxt *portercontext.Context) err
3946
return o.InstallOptions.Validate(args)
4047
}
4148

42-
// InstallFileOption is the go representation of plugin installation file format.
43-
type InstallFileOption map[string]pkgmgmt.InstallOptions
49+
// InstallPluginsSpec represents the user-defined configuration for plugins installation.
50+
type InstallPluginsSpec struct {
51+
SchemaType string `yaml:"schemaType"`
52+
SchemaVersion string `yaml:"schemaVersion"`
53+
Plugins InstallPluginsConfig `yaml:"plugins"`
54+
}
55+
56+
func (spec InstallPluginsSpec) Validate() error {
57+
if spec.SchemaType != "" && strings.ToLower(spec.SchemaType) != "plugins" {
58+
return fmt.Errorf("invalid schemaType %s, expected Plugins", spec.SchemaType)
59+
}
60+
61+
if InstallPluginsSchemaVersion != schema.Version(spec.SchemaVersion) {
62+
if spec.SchemaVersion == "" {
63+
spec.SchemaVersion = "(none)"
64+
}
65+
return fmt.Errorf("invalid schemaVersion provided: %s. This version of Porter is compatible with %s.", spec.SchemaVersion, InstallPluginsSchemaVersion)
66+
}
67+
return nil
68+
}
69+
70+
// InstallPluginsConfig is the go representation of plugin installation file format.
71+
type InstallPluginsConfig map[string]pkgmgmt.InstallOptions
4472

45-
// InstallPluginsConfig is a sorted list of InstallationFileOption in alphabetical order.
46-
type InstallPluginsConfig struct {
47-
data InstallFileOption
73+
// InstallPluginsConfigList is a sorted list of InstallationFileOption in alphabetical order.
74+
type InstallPluginsConfigList struct {
75+
data InstallPluginsConfig
4876
keys []string
4977
}
5078

5179
// NewInstallPluginConfigs returns a new instance of InstallPluginConfigs with plugins sorted in alphabetical order
5280
// using their names.
53-
func NewInstallPluginConfigs(opt InstallFileOption) InstallPluginsConfig {
81+
func NewInstallPluginConfigs(opt InstallPluginsConfig) InstallPluginsConfigList {
5482
keys := make([]string, 0, len(opt))
55-
data := make(InstallFileOption, len(opt))
83+
data := make(InstallPluginsConfig, len(opt))
5684
for k, v := range opt {
5785
keys = append(keys, k)
5886

@@ -65,14 +93,14 @@ func NewInstallPluginConfigs(opt InstallFileOption) InstallPluginsConfig {
6593
return keys[i] < keys[j]
6694
})
6795

68-
return InstallPluginsConfig{
96+
return InstallPluginsConfigList{
6997
data: data,
7098
keys: keys,
7199
}
72100
}
73101

74-
// Configs returns InstallOptions list in alphabetical order.
75-
func (pc InstallPluginsConfig) Configs() []pkgmgmt.InstallOptions {
102+
// Values returns InstallOptions list in alphabetical order.
103+
func (pc InstallPluginsConfigList) Values() []pkgmgmt.InstallOptions {
76104
value := make([]pkgmgmt.InstallOptions, 0, len(pc.keys))
77105
for _, k := range pc.keys {
78106
value = append(value, pc.data[k])

pkg/plugins/install_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ func TestInstallOptions_Validate(t *testing.T) {
1919
}
2020

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

2525
cfg := NewInstallPluginConfigs(input)
26-
require.Equal(t, expected, cfg.Configs())
26+
require.Equal(t, expected, cfg.Values())
2727
}

pkg/porter/plugins.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -158,23 +158,23 @@ func (p *Porter) InstallPlugin(ctx context.Context, opts plugins.InstallOptions)
158158
ctx, log := tracing.StartSpan(ctx)
159159
defer log.EndSpan()
160160

161-
installConfigs, err := p.getPluginInstallConfigs(ctx, opts)
161+
installOpts, err := p.getPluginInstallOptions(ctx, opts)
162162
if err != nil {
163163
return err
164164
}
165-
for _, cfg := range installConfigs {
166-
err := p.Plugins.Install(ctx, cfg)
165+
for _, opt := range installOpts {
166+
err := p.Plugins.Install(ctx, opt)
167167
if err != nil {
168168
return err
169169
}
170170

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

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

180180
return nil
@@ -191,13 +191,13 @@ func (p *Porter) UninstallPlugin(ctx context.Context, opts pkgmgmt.UninstallOpti
191191
return nil
192192
}
193193

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

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

212-
for _, config := range sortedCfgs.Configs() {
211+
if err := data.Validate(); err != nil {
212+
return nil, err
213+
}
214+
215+
sortedCfgs := plugins.NewInstallPluginConfigs(data.Plugins)
216+
217+
for _, config := range sortedCfgs.Values() {
213218
// if user specified a feed url or mirror using the flags, it will become
214219
// the default value and apply to empty values parsed from the provided file
215220
if config.FeedURL == "" {

pkg/porter/plugins_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@ package porter
33
import (
44
"context"
55
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
69
"testing"
710

811
"get.porter.sh/porter/pkg/pkgmgmt"
912
"get.porter.sh/porter/pkg/pkgmgmt/client"
1013
"get.porter.sh/porter/pkg/plugins"
1114
"get.porter.sh/porter/pkg/printer"
1215
"get.porter.sh/porter/pkg/test"
16+
"get.porter.sh/porter/pkg/yaml"
17+
"get.porter.sh/porter/tests"
1318
"github.com/stretchr/testify/assert"
1419
"github.com/stretchr/testify/require"
20+
"github.com/xeipuuv/gojsonschema"
1521
)
1622

1723
func TestPorter_PrintPlugins(t *testing.T) {
@@ -310,6 +316,63 @@ func TestPorter_InstallPlugin(t *testing.T) {
310316
}
311317
}
312318

319+
func TestPorter_InstallPluginsSchema(t *testing.T) {
320+
p := NewTestPorter(t)
321+
schema, err := os.ReadFile(filepath.Join(p.RepoRoot, "pkg/schema/plugins.schema.json"))
322+
require.NoError(t, err, "failed to read plugins.schema.json file")
323+
testcases := []struct {
324+
name string
325+
path string
326+
wantErr string
327+
}{
328+
{
329+
name: "valid",
330+
path: "testdata/plugins.json",
331+
wantErr: "",
332+
},
333+
{
334+
name: "invalid",
335+
path: "testdata/invalid-plugins.json",
336+
wantErr: "(root): Additional property invalid-field is not allowed\nplugins.plugin1: Additional property random-field is not allowed\n",
337+
},
338+
}
339+
340+
for _, tc := range testcases {
341+
t.Run(tc.name, func(t *testing.T) {
342+
// Load manifest as a go dump
343+
testManifest, err := os.ReadFile(tc.path)
344+
require.NoError(t, err, "failed to read %s", tc.path)
345+
346+
m := make(map[string]interface{})
347+
err = yaml.Unmarshal(testManifest, &m)
348+
require.NoError(t, err, "failed to unmarshal %s", tc.path)
349+
350+
// Load the manifest schema returned from `porter schema`
351+
manifestLoader := gojsonschema.NewGoLoader(m)
352+
schemaLoader := gojsonschema.NewBytesLoader(schema)
353+
354+
// Validate the manifest against the schema
355+
fails, err := gojsonschema.Validate(schemaLoader, manifestLoader)
356+
require.NoError(t, err)
357+
358+
if tc.wantErr == "" {
359+
assert.Empty(t, fails.Errors(), "expected %s to validate against the plugins schema", tc.path)
360+
// Print any validation errors returned
361+
for _, err := range fails.Errors() {
362+
t.Logf("%s", err)
363+
}
364+
} else {
365+
var allFails strings.Builder
366+
for _, err := range fails.Errors() {
367+
allFails.WriteString(err.String())
368+
allFails.WriteString("\n")
369+
}
370+
tests.RequireOutputContains(t, tc.wantErr, allFails.String())
371+
}
372+
})
373+
}
374+
}
375+
313376
func TestPorter_UninstallPlugin(t *testing.T) {
314377
ctx := context.Background()
315378
p := NewTestPorter(t)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"schemaType": "Plugins",
3+
"schemaVersion": "1.0.0",
4+
"invalid-field": "123",
5+
"plugins": {
6+
"plugin1": {
7+
"random-field": 1
8+
},
9+
"plugin2": {
10+
"version": "v1.0"
11+
}
12+
}
13+
}

pkg/porter/testdata/plugins.json

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
{
2-
"plugin1": {
3-
"version": "v1.0"
4-
},
5-
"plugin2": {
6-
"version": "v1.0"
2+
"schemaType": "Plugins",
3+
"schemaVersion": "1.0.0",
4+
"plugins": {
5+
"plugin1": {
6+
"version": "v1.0"
7+
},
8+
"plugin2": {
9+
"version": "v1.0"
10+
}
711
}
812
}

pkg/porter/testdata/plugins.yaml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
plugin1:
2-
version: v1.0
3-
plugin2:
4-
version: v1.0
1+
schemaType: Plugins
2+
schemaVersion: 1.0.0
3+
plugins:
4+
plugin1:
5+
version: v1.0
6+
plugin2:
7+
version: v1.0

0 commit comments

Comments
 (0)