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>
171 lines
5.0 KiB
Go
171 lines
5.0 KiB
Go
package manager
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/docker/cli/cli-plugins/metadata"
|
|
"github.com/spf13/cobra"
|
|
"gotest.tools/v3/assert"
|
|
is "gotest.tools/v3/assert/cmp"
|
|
)
|
|
|
|
type fakeCandidate struct {
|
|
path string
|
|
exec bool
|
|
meta string
|
|
}
|
|
|
|
func (c *fakeCandidate) Path() string {
|
|
return c.path
|
|
}
|
|
|
|
func (c *fakeCandidate) Metadata() ([]byte, error) {
|
|
if !c.exec {
|
|
return nil, fmt.Errorf("faked a failure to exec %q", c.path)
|
|
}
|
|
return []byte(c.meta), nil
|
|
}
|
|
|
|
func TestValidateCandidate(t *testing.T) {
|
|
const (
|
|
goodPluginName = metadata.NamePrefix + "goodplugin"
|
|
builtinName = metadata.NamePrefix + "builtin"
|
|
builtinAlias = metadata.NamePrefix + "alias"
|
|
|
|
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
|
|
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
|
|
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
|
|
)
|
|
|
|
fakeroot := &cobra.Command{Use: "docker"}
|
|
fakeroot.AddCommand(&cobra.Command{
|
|
Use: strings.TrimPrefix(builtinName, metadata.NamePrefix),
|
|
Aliases: []string{
|
|
strings.TrimPrefix(builtinAlias, metadata.NamePrefix),
|
|
},
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
plugin *fakeCandidate
|
|
|
|
// 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.
|
|
{
|
|
name: "empty path",
|
|
plugin: &fakeCandidate{path: ""},
|
|
err: "plugin candidate path cannot be empty",
|
|
},
|
|
{
|
|
name: "bad prefix",
|
|
plugin: &fakeCandidate{path: badPrefixPath},
|
|
err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix),
|
|
},
|
|
{
|
|
name: "bad path",
|
|
plugin: &fakeCandidate{path: badNamePath},
|
|
invalid: "did not match",
|
|
},
|
|
{
|
|
name: "builtin command",
|
|
plugin: &fakeCandidate{path: builtinName},
|
|
invalid: `plugin "builtin" duplicates builtin command`,
|
|
},
|
|
{
|
|
name: "builtin alias",
|
|
plugin: &fakeCandidate{path: builtinAlias},
|
|
invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`,
|
|
},
|
|
{
|
|
name: "fetch failure",
|
|
plugin: &fakeCandidate{path: goodPluginPath, exec: false},
|
|
invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath),
|
|
},
|
|
{
|
|
name: "metadata not json",
|
|
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`},
|
|
invalid: "invalid character",
|
|
},
|
|
{
|
|
name: "empty schemaversion",
|
|
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`},
|
|
invalid: `plugin SchemaVersion version cannot be empty`,
|
|
},
|
|
{
|
|
name: "invalid schemaversion",
|
|
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`},
|
|
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",
|
|
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`},
|
|
invalid: "plugin metadata does not define a vendor",
|
|
},
|
|
{
|
|
name: "empty vendor",
|
|
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`},
|
|
invalid: "plugin metadata does not define a vendor",
|
|
},
|
|
|
|
// Valid cases.
|
|
{
|
|
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) {
|
|
p, err := newPlugin(tc.plugin, fakeroot.Commands())
|
|
switch {
|
|
case tc.err != "":
|
|
assert.ErrorContains(t, err, tc.err)
|
|
case tc.invalid != "":
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, is.ErrorType(p.Err, reflect.TypeOf(&pluginError{})))
|
|
assert.ErrorContains(t, p.Err, tc.invalid)
|
|
default:
|
|
assert.NilError(t, err)
|
|
assert.Equal(t, metadata.NamePrefix+p.Name, goodPluginName)
|
|
assert.Equal(t, p.SchemaVersion, tc.expVer)
|
|
assert.Equal(t, p.Vendor, "e2e-testing")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCandidatePath(t *testing.T) {
|
|
exp := "/some/path"
|
|
cand := &candidate{path: exp}
|
|
assert.Equal(t, exp, cand.Path())
|
|
}
|