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 <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2025-09-24 16:12:46 +02:00
parent 8fbb70ae56
commit ec912e5524
2 changed files with 51 additions and 5 deletions

View File

@ -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 <major>.<minor>.<patch>`,
},
{
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")
}
})

View File

@ -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 <major>.<minor>.<patch>", 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) {