From ec912e552435bf5e618aeb203a7c1dfc27154161 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 24 Sep 2025 16:12:46 +0200 Subject: [PATCH] cli-plugins/manager: allow schema-versions <= 2.0.0 The CLI currently hard-codes the schema-version for CLI plugins to "0.1.0", which doesn't allow us to expand the schema for plugins. As there's many plugins that we shipped already, we can't break compatibility until we reach 2.0.0, but we can expand the schema with non-breaking changes. This patch makes the validation more permissive to allow new schema versions <= 2.0.0. Note that existing CLIs will still invalidate such versions, so we cannot update the version until such CLIs are no longer expected to be used, but this patch lays the ground-work to open that option. Signed-off-by: Sebastiaan van Stijn --- cli-plugins/manager/candidate_test.go | 26 ++++++++++++++++++++--- cli-plugins/manager/plugin.go | 30 +++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) 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) {