move cli-plugins metadata types/consts to a separate package

This prevents cli-plugins having to import the plugin-manager.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2025-03-04 23:16:33 +01:00
parent ceef542046
commit 4321293972
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
13 changed files with 94 additions and 57 deletions

View File

@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -97,7 +97,7 @@ func main() {
cmd.AddCommand(goodbye, apiversion, exitStatus2) cmd.AddCommand(goodbye, apiversion, exitStatus2)
return cmd return cmd
}, },
manager.Metadata{ metadata.Metadata{
SchemaVersion: "0.1.0", SchemaVersion: "0.1.0",
Vendor: "Docker Inc.", Vendor: "Docker Inc.",
Version: "testing", Version: "testing",

View File

@ -1,6 +1,10 @@
package manager package manager
import "os/exec" import (
"os/exec"
"github.com/docker/cli/cli-plugins/metadata"
)
// Candidate represents a possible plugin candidate, for mocking purposes // Candidate represents a possible plugin candidate, for mocking purposes
type Candidate interface { type Candidate interface {
@ -17,5 +21,5 @@ func (c *candidate) Path() string {
} }
func (c *candidate) Metadata() ([]byte, error) { func (c *candidate) Metadata() ([]byte, error) {
return exec.Command(c.path, MetadataSubcommandName).Output() // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments" return exec.Command(c.path, metadata.MetadataSubcommandName).Output() // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
} }

View File

@ -6,6 +6,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp" "gotest.tools/v3/assert/cmp"
@ -30,10 +31,10 @@ func (c *fakeCandidate) Metadata() ([]byte, error) {
func TestValidateCandidate(t *testing.T) { func TestValidateCandidate(t *testing.T) {
const ( const (
goodPluginName = NamePrefix + "goodplugin" goodPluginName = metadata.NamePrefix + "goodplugin"
builtinName = NamePrefix + "builtin" builtinName = metadata.NamePrefix + "builtin"
builtinAlias = NamePrefix + "alias" builtinAlias = metadata.NamePrefix + "alias"
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble" badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456" badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
@ -43,9 +44,9 @@ func TestValidateCandidate(t *testing.T) {
fakeroot := &cobra.Command{Use: "docker"} fakeroot := &cobra.Command{Use: "docker"}
fakeroot.AddCommand(&cobra.Command{ fakeroot.AddCommand(&cobra.Command{
Use: strings.TrimPrefix(builtinName, NamePrefix), Use: strings.TrimPrefix(builtinName, metadata.NamePrefix),
Aliases: []string{ Aliases: []string{
strings.TrimPrefix(builtinAlias, NamePrefix), strings.TrimPrefix(builtinAlias, metadata.NamePrefix),
}, },
}) })
@ -59,7 +60,7 @@ func TestValidateCandidate(t *testing.T) {
}{ }{
/* Each failing one of the tests */ /* Each failing one of the tests */
{name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"}, {name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"},
{name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", NamePrefix)}, {name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix)},
{name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"}, {name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"},
{name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`}, {name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`},
{name: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`}, {name: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`},
@ -84,7 +85,7 @@ func TestValidateCandidate(t *testing.T) {
assert.ErrorContains(t, p.Err, tc.invalid) assert.ErrorContains(t, p.Err, tc.invalid)
default: default:
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, NamePrefix+p.Name, goodPluginName) assert.Equal(t, metadata.NamePrefix+p.Name, goodPluginName)
assert.Equal(t, p.SchemaVersion, "0.1.0") assert.Equal(t, p.SchemaVersion, "0.1.0")
assert.Equal(t, p.Vendor, "e2e-testing") assert.Equal(t, p.Vendor, "e2e-testing")
} }

View File

@ -9,6 +9,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/fvbommel/sortorder" "github.com/fvbommel/sortorder"
@ -21,7 +22,7 @@ const (
// used to originally invoke the docker CLI when executing a // used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow // plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI. // the plugin to re-execute the original CLI.
ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND" ReexecEnvvar = metadata.ReexecEnvvar
// ResourceAttributesEnvvar is the name of the envvar that includes additional // ResourceAttributesEnvvar is the name of the envvar that includes additional
// resource attributes for OTEL. // resource attributes for OTEL.
@ -92,10 +93,10 @@ func addPluginCandidatesFromDir(res map[string][]string, d string) {
continue continue
} }
name := dentry.Name() name := dentry.Name()
if !strings.HasPrefix(name, NamePrefix) { if !strings.HasPrefix(name, metadata.NamePrefix) {
continue continue
} }
name = strings.TrimPrefix(name, NamePrefix) name = strings.TrimPrefix(name, metadata.NamePrefix)
var err error var err error
if name, err = trimExeSuffix(name); err != nil { if name, err = trimExeSuffix(name); err != nil {
continue continue
@ -200,7 +201,7 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
// fallback to their "invalid" command path. // fallback to their "invalid" command path.
return nil, errPluginNotFound(name) return nil, errPluginNotFound(name)
} }
exename := addExeSuffix(NamePrefix + name) exename := addExeSuffix(metadata.NamePrefix + name)
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile()) pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
if err != nil { if err != nil {
return nil, err return nil, err
@ -237,7 +238,7 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Env = append(cmd.Environ(), ReexecEnvvar+"="+os.Args[0]) cmd.Env = append(cmd.Environ(), metadata.ReexecEnvvar+"="+os.Args[0])
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin) cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
return cmd, nil return cmd, nil

View File

@ -1,30 +1,23 @@
package manager package manager
import (
"github.com/docker/cli/cli-plugins/metadata"
)
const ( const (
// NamePrefix is the prefix required on all plugin binary names // NamePrefix is the prefix required on all plugin binary names
NamePrefix = "docker-" NamePrefix = metadata.NamePrefix
// MetadataSubcommandName is the name of the plugin subcommand // MetadataSubcommandName is the name of the plugin subcommand
// which must be supported by every plugin and returns the // which must be supported by every plugin and returns the
// plugin metadata. // plugin metadata.
MetadataSubcommandName = "docker-cli-plugin-metadata" MetadataSubcommandName = metadata.MetadataSubcommandName
// HookSubcommandName is the name of the plugin subcommand // HookSubcommandName is the name of the plugin subcommand
// which must be implemented by plugins declaring support // which must be implemented by plugins declaring support
// for hooks in their metadata. // for hooks in their metadata.
HookSubcommandName = "docker-cli-plugin-hooks" HookSubcommandName = metadata.HookSubcommandName
) )
// Metadata provided by the plugin. // Metadata provided by the plugin.
type Metadata struct { type Metadata = metadata.Metadata
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
SchemaVersion string `json:",omitempty"`
// Vendor is the name of the plugin vendor. Mandatory
Vendor string `json:",omitempty"`
// Version is the optional version of this plugin.
Version string `json:",omitempty"`
// ShortDescription should be suitable for a single line help message.
ShortDescription string `json:",omitempty"`
// URL is a pointer to the plugin's homepage.
URL string `json:",omitempty"`
}

View File

@ -9,6 +9,7 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -17,7 +18,7 @@ var pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$")
// Plugin represents a potential plugin with all it's metadata. // Plugin represents a potential plugin with all it's metadata.
type Plugin struct { type Plugin struct {
Metadata metadata.Metadata
Name string `json:",omitempty"` Name string `json:",omitempty"`
Path string `json:",omitempty"` Path string `json:",omitempty"`
@ -50,12 +51,12 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
if fullname, err = trimExeSuffix(fullname); err != nil { if fullname, err = trimExeSuffix(fullname); err != nil {
return Plugin{}, errors.Wrapf(err, "plugin candidate %q", path) return Plugin{}, errors.Wrapf(err, "plugin candidate %q", path)
} }
if !strings.HasPrefix(fullname, NamePrefix) { if !strings.HasPrefix(fullname, metadata.NamePrefix) {
return Plugin{}, errors.Errorf("plugin candidate %q: does not have %q prefix", path, NamePrefix) return Plugin{}, errors.Errorf("plugin candidate %q: does not have %q prefix", path, metadata.NamePrefix)
} }
p := Plugin{ p := Plugin{
Name: strings.TrimPrefix(fullname, NamePrefix), Name: strings.TrimPrefix(fullname, metadata.NamePrefix),
Path: path, Path: path,
} }
@ -112,9 +113,9 @@ func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte,
return nil, wrapAsPluginError(err, "failed to marshall hook data") return nil, wrapAsPluginError(err, "failed to marshall hook data")
} }
pCmd := exec.CommandContext(ctx, p.Path, p.Name, HookSubcommandName, string(hDataBytes)) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments" pCmd := exec.CommandContext(ctx, p.Path, p.Name, metadata.HookSubcommandName, string(hDataBytes)) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
pCmd.Env = os.Environ() pCmd.Env = os.Environ()
pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0]) pCmd.Env = append(pCmd.Env, metadata.ReexecEnvvar+"="+os.Args[0])
hookCmdOutput, err := pCmd.Output() hookCmdOutput, err := pCmd.Output()
if err != nil { if err != nil {
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand") return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")

View File

@ -0,0 +1,36 @@
package metadata
const (
// NamePrefix is the prefix required on all plugin binary names
NamePrefix = "docker-"
// MetadataSubcommandName is the name of the plugin subcommand
// which must be supported by every plugin and returns the
// plugin metadata.
MetadataSubcommandName = "docker-cli-plugin-metadata"
// HookSubcommandName is the name of the plugin subcommand
// which must be implemented by plugins declaring support
// for hooks in their metadata.
HookSubcommandName = "docker-cli-plugin-hooks"
// ReexecEnvvar is the name of an ennvar which is set to the command
// used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI.
ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
)
// Metadata provided by the plugin.
type Metadata struct {
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
SchemaVersion string `json:",omitempty"`
// Vendor is the name of the plugin vendor. Mandatory
Vendor string `json:",omitempty"`
// Version is the optional version of this plugin.
Version string `json:",omitempty"`
// ShortDescription should be suitable for a single line help message.
ShortDescription string `json:",omitempty"`
// URL is a pointer to the plugin's homepage.
URL string `json:",omitempty"`
}

View File

@ -9,7 +9,7 @@ import (
"sync" "sync"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli-plugins/socket" "github.com/docker/cli/cli-plugins/socket"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/connhelper" "github.com/docker/cli/cli/connhelper"
@ -30,7 +30,7 @@ import (
var PersistentPreRunE func(*cobra.Command, []string) error var PersistentPreRunE func(*cobra.Command, []string) error
// RunPlugin executes the specified plugin command // RunPlugin executes the specified plugin command
func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) error { func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) error {
tcmd := newPluginCommand(dockerCli, plugin, meta) tcmd := newPluginCommand(dockerCli, plugin, meta)
var persistentPreRunOnce sync.Once var persistentPreRunOnce sync.Once
@ -81,7 +81,7 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager
} }
// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function. // Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) { func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) {
otel.SetErrorHandler(debug.OTELErrorHandler) otel.SetErrorHandler(debug.OTELErrorHandler)
dockerCli, err := command.NewDockerCli() dockerCli, err := command.NewDockerCli()
@ -111,7 +111,7 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
func withPluginClientConn(name string) command.CLIOption { func withPluginClientConn(name string) command.CLIOption {
return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) { return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) {
cmd := "docker" cmd := "docker"
if x := os.Getenv(manager.ReexecEnvvar); x != "" { if x := os.Getenv(metadata.ReexecEnvvar); x != "" {
cmd = x cmd = x
} }
var flags []string var flags []string
@ -140,9 +140,9 @@ func withPluginClientConn(name string) command.CLIOption {
}) })
} }
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cli.TopLevelCommand { func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) *cli.TopLevelCommand {
name := plugin.Name() name := plugin.Name()
fullname := manager.NamePrefix + name fullname := metadata.NamePrefix + name
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name), Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name),
@ -177,12 +177,12 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags()) return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags())
} }
func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.Command { func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra.Command {
if meta.ShortDescription == "" { if meta.ShortDescription == "" {
meta.ShortDescription = plugin.Short meta.ShortDescription = plugin.Short
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: manager.MetadataSubcommandName, Use: metadata.MetadataSubcommandName,
Hidden: true, Hidden: true,
// Suppress the global/parent PersistentPreRunE, which // Suppress the global/parent PersistentPreRunE, which
// needlessly initializes the client and tries to // needlessly initializes the client and tries to
@ -200,8 +200,8 @@ func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.
// RunningStandalone tells a CLI plugin it is run standalone by direct execution // RunningStandalone tells a CLI plugin it is run standalone by direct execution
func RunningStandalone() bool { func RunningStandalone() bool {
if os.Getenv(manager.ReexecEnvvar) != "" { if os.Getenv(metadata.ReexecEnvvar) != "" {
return false return false
} }
return len(os.Args) < 2 || os.Args[1] != manager.MetadataSubcommandName return len(os.Args) < 2 || os.Args[1] != metadata.MetadataSubcommandName
} }

View File

@ -7,6 +7,7 @@ import (
"time" "time"
pluginmanager "github.com/docker/cli/cli-plugins/manager" pluginmanager "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
registrytypes "github.com/docker/docker/api/types/registry" registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
@ -201,7 +202,7 @@ var samplePluginsInfo = []pluginmanager.Plugin{
{ {
Name: "goodplugin", Name: "goodplugin",
Path: "/path/to/docker-goodplugin", Path: "/path/to/docker-goodplugin",
Metadata: pluginmanager.Metadata{ Metadata: metadata.Metadata{
SchemaVersion: "0.1.0", SchemaVersion: "0.1.0",
ShortDescription: "unit test is good", ShortDescription: "unit test is good",
Vendor: "ACME Corp", Vendor: "ACME Corp",
@ -211,7 +212,7 @@ var samplePluginsInfo = []pluginmanager.Plugin{
{ {
Name: "unversionedplugin", Name: "unversionedplugin",
Path: "/path/to/docker-unversionedplugin", Path: "/path/to/docker-unversionedplugin",
Metadata: pluginmanager.Metadata{ Metadata: metadata.Metadata{
SchemaVersion: "0.1.0", SchemaVersion: "0.1.0",
ShortDescription: "this plugin has no version", ShortDescription: "this plugin has no version",
Vendor: "ACME Corp", Vendor: "ACME Corp",

View File

@ -5,7 +5,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/metadata"
"gotest.tools/v3/icmd" "gotest.tools/v3/icmd"
) )
@ -21,7 +21,7 @@ func TestCLIPluginDialStdio(t *testing.T) {
// the connhelper stuff. // the connhelper stuff.
helloworld := filepath.Join(os.Getenv("DOCKER_CLI_E2E_PLUGINS_EXTRA_DIRS"), "docker-helloworld") helloworld := filepath.Join(os.Getenv("DOCKER_CLI_E2E_PLUGINS_EXTRA_DIRS"), "docker-helloworld")
cmd := icmd.Command(helloworld, "--config=blah", "--log-level", "debug", "helloworld", "--who=foo") cmd := icmd.Command(helloworld, "--config=blah", "--log-level", "debug", "helloworld", "--who=foo")
res := icmd.RunCmd(cmd, icmd.WithEnv(manager.ReexecEnvvar+"=/bin/true")) res := icmd.RunCmd(cmd, icmd.WithEnv(metadata.ReexecEnvvar+"=/bin/true"))
res.Assert(t, icmd.Expected{ res.Assert(t, icmd.Expected{
ExitCode: 0, ExitCode: 0,
Err: `msg="commandconn: starting /bin/true with [--config=blah --log-level debug system dial-stdio]"`, Err: `msg="commandconn: starting /bin/true with [--config=blah --log-level debug system dial-stdio]"`,

View File

@ -7,11 +7,11 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/metadata"
) )
func main() { func main() {
if len(os.Args) == 2 && os.Args[1] == manager.MetadataSubcommandName { if len(os.Args) == 2 && os.Args[1] == metadata.MetadataSubcommandName {
fmt.Println(`{invalid-json}`) fmt.Println(`{invalid-json}`)
os.Exit(0) os.Exit(0)
} }

View File

@ -3,7 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -26,7 +26,7 @@ func main() {
}, },
} }
}, },
manager.Metadata{ metadata.Metadata{
SchemaVersion: "0.1.0", SchemaVersion: "0.1.0",
Vendor: "Docker Inc.", Vendor: "Docker Inc.",
Version: "testing", Version: "testing",

View File

@ -7,14 +7,14 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func main() { func main() {
plugin.Run(RootCmd, manager.Metadata{ plugin.Run(RootCmd, metadata.Metadata{
SchemaVersion: "0.1.0", SchemaVersion: "0.1.0",
Vendor: "Docker Inc.", Vendor: "Docker Inc.",
Version: "test", Version: "test",