diff --git a/cli-plugins/manager/candidate_test.go b/cli-plugins/manager/candidate_test.go index ee7ff788b..a0dbb0dd7 100644 --- a/cli-plugins/manager/candidate_test.go +++ b/cli-plugins/manager/candidate_test.go @@ -55,6 +55,7 @@ func TestValidateCandidate(t *testing.T) { // Either err or invalid may be non-empty, but not both (both can be empty for a good plugin). err string invalid string + expVer string }{ // Invalid cases. { @@ -95,12 +96,17 @@ func TestValidateCandidate(t *testing.T) { { name: "empty schemaversion", plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`}, - invalid: `plugin SchemaVersion "" is not valid`, + invalid: `plugin SchemaVersion version cannot be empty`, }, { name: "invalid schemaversion", plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`}, - invalid: `plugin SchemaVersion "xyzzy" is not valid`, + invalid: `plugin SchemaVersion "xyzzy" has wrong format: must be ..`, + }, + { + name: "invalid schemaversion major", + plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "2.0.0"}`}, + invalid: `plugin SchemaVersion "2.0.0" is not supported: must be lower than 2.0.0`, }, { name: "no vendor", @@ -117,11 +123,25 @@ func TestValidateCandidate(t *testing.T) { { name: "valid", plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}, + expVer: "0.1.0", }, { // Including the deprecated "experimental" field should not break processing. name: "with legacy experimental", plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`}, + expVer: "0.1.0", + }, + { + // note that this may not be supported by older CLIs + name: "new minor schema version", + plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.2.0", "Vendor": "e2e-testing"}`}, + expVer: "0.2.0", + }, + { + // note that this may not be supported by older CLIs + name: "new major schema version", + plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "1.0.0", "Vendor": "e2e-testing"}`}, + expVer: "1.0.0", }, } { t.Run(tc.name, func(t *testing.T) { @@ -136,7 +156,7 @@ func TestValidateCandidate(t *testing.T) { default: assert.NilError(t, err) assert.Equal(t, metadata.NamePrefix+p.Name, goodPluginName) - assert.Equal(t, p.SchemaVersion, "0.1.0") + assert.Equal(t, p.SchemaVersion, tc.expVer) assert.Equal(t, p.Vendor, "e2e-testing") } }) diff --git a/cli-plugins/manager/plugin.go b/cli-plugins/manager/plugin.go index 40474ed37..824d82d12 100644 --- a/cli-plugins/manager/plugin.go +++ b/cli-plugins/manager/plugin.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" "github.com/docker/cli/cli-plugins/metadata" @@ -115,8 +116,8 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) { p.Err = wrapAsPluginError(err, "invalid metadata") return p, nil } - if p.Metadata.SchemaVersion != "0.1.0" { - p.Err = newPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion) + if err := validateSchemaVersion(p.Metadata.SchemaVersion); err != nil { + p.Err = &pluginError{cause: err} return p, nil } if p.Metadata.Vendor == "" { @@ -126,6 +127,31 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) { return p, nil } +// validateSchemaVersion validates if the plugin's schemaVersion is supported. +// +// The current schema-version is "0.1.0", but we don't want to break compatibility +// until v2.0.0 of the schema version. Check for the major version to be < 2.0.0. +// +// Note that CLI versions before 28.4.1 may not support these versions as they were +// hard-coded to only accept "0.1.0". +func validateSchemaVersion(version string) error { + if version == "0.1.0" { + return nil + } + if version == "" { + return errors.New("plugin SchemaVersion version cannot be empty") + } + major, _, ok := strings.Cut(version, ".") + majorVersion, err := strconv.Atoi(major) + if !ok || err != nil { + return fmt.Errorf("plugin SchemaVersion %q has wrong format: must be ..", version) + } + if majorVersion > 1 { + return fmt.Errorf("plugin SchemaVersion %q is not supported: must be lower than 2.0.0", version) + } + return nil +} + // RunHook executes the plugin's hooks command // and returns its unprocessed output. func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte, error) {