Merge pull request #46017 from thaJeztah/pkg_plugin_cleanup
pkg/plugins: some cleaning up (step 1)
This commit is contained in:
commit
536e3692c6
13 changed files with 138 additions and 160 deletions
|
@ -26,7 +26,7 @@ const (
|
|||
dummyHost = "plugin.moby.localhost"
|
||||
)
|
||||
|
||||
func newTransport(addr string, tlsConfig *tlsconfig.Options) (transport.Transport, error) {
|
||||
func newTransport(addr string, tlsConfig *tlsconfig.Options) (*transport.HTTPTransport, error) {
|
||||
tr := &http.Transport{}
|
||||
|
||||
if tlsConfig != nil {
|
||||
|
@ -77,7 +77,7 @@ func NewClientWithTimeout(addr string, tlsConfig *tlsconfig.Options, timeout tim
|
|||
}
|
||||
|
||||
// newClientWithTransport creates a new plugin client with a given transport.
|
||||
func newClientWithTransport(tr transport.Transport, timeout time.Duration) *Client {
|
||||
func newClientWithTransport(tr *transport.HTTPTransport, timeout time.Duration) *Client {
|
||||
return &Client{
|
||||
http: &http.Client{
|
||||
Transport: tr,
|
||||
|
@ -87,10 +87,16 @@ func newClientWithTransport(tr transport.Transport, timeout time.Duration) *Clie
|
|||
}
|
||||
}
|
||||
|
||||
// requestFactory defines an interface that transports can implement to
|
||||
// create new requests. It's used in testing.
|
||||
type requestFactory interface {
|
||||
NewRequest(path string, data io.Reader) (*http.Request, error)
|
||||
}
|
||||
|
||||
// Client represents a plugin client.
|
||||
type Client struct {
|
||||
http *http.Client // http client to use
|
||||
requestFactory transport.RequestFactory
|
||||
requestFactory requestFactory
|
||||
}
|
||||
|
||||
// RequestOpts is the set of options that can be passed into a request
|
||||
|
|
|
@ -13,34 +13,35 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotFound plugin not found
|
||||
ErrNotFound = errors.New("plugin not found")
|
||||
socketsPath = "/run/docker/plugins"
|
||||
)
|
||||
// ErrNotFound plugin not found
|
||||
var ErrNotFound = errors.New("plugin not found")
|
||||
|
||||
const defaultSocketsPath = "/run/docker/plugins"
|
||||
|
||||
// LocalRegistry defines a registry that is local (using unix socket).
|
||||
type LocalRegistry struct {
|
||||
SpecsPaths func() []string
|
||||
socketsPath string
|
||||
specsPaths []string
|
||||
}
|
||||
|
||||
func NewLocalRegistry() LocalRegistry {
|
||||
return LocalRegistry{
|
||||
SpecsPaths,
|
||||
socketsPath: defaultSocketsPath,
|
||||
specsPaths: specsPaths(),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan scans all the plugin paths and returns all the names it found
|
||||
func (l *LocalRegistry) Scan() ([]string, error) {
|
||||
var names []string
|
||||
dirEntries, err := os.ReadDir(socketsPath)
|
||||
dirEntries, err := os.ReadDir(l.socketsPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, errors.Wrap(err, "error reading dir entries")
|
||||
}
|
||||
|
||||
for _, entry := range dirEntries {
|
||||
if entry.IsDir() {
|
||||
fi, err := os.Stat(filepath.Join(socketsPath, entry.Name(), entry.Name()+".sock"))
|
||||
fi, err := os.Stat(filepath.Join(l.socketsPath, entry.Name(), entry.Name()+".sock"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
@ -53,31 +54,30 @@ func (l *LocalRegistry) Scan() ([]string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
for _, p := range l.SpecsPaths() {
|
||||
dirEntries, err := os.ReadDir(p)
|
||||
for _, p := range l.specsPaths {
|
||||
dirEntries, err = os.ReadDir(p)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, errors.Wrap(err, "error reading dir entries")
|
||||
}
|
||||
|
||||
for _, fi := range dirEntries {
|
||||
if fi.IsDir() {
|
||||
infos, err := os.ReadDir(filepath.Join(p, fi.Name()))
|
||||
for _, entry := range dirEntries {
|
||||
if entry.IsDir() {
|
||||
infos, err := os.ReadDir(filepath.Join(p, entry.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, info := range infos {
|
||||
if strings.TrimSuffix(info.Name(), filepath.Ext(info.Name())) == fi.Name() {
|
||||
fi = info
|
||||
if strings.TrimSuffix(info.Name(), filepath.Ext(info.Name())) == entry.Name() {
|
||||
entry = info
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ext := filepath.Ext(fi.Name())
|
||||
switch ext {
|
||||
switch ext := filepath.Ext(entry.Name()); ext {
|
||||
case ".spec", ".json":
|
||||
plugin := strings.TrimSuffix(fi.Name(), ext)
|
||||
plugin := strings.TrimSuffix(entry.Name(), ext)
|
||||
names = append(names, plugin)
|
||||
default:
|
||||
}
|
||||
|
@ -88,21 +88,20 @@ func (l *LocalRegistry) Scan() ([]string, error) {
|
|||
|
||||
// Plugin returns the plugin registered with the given name (or returns an error).
|
||||
func (l *LocalRegistry) Plugin(name string) (*Plugin, error) {
|
||||
socketpaths := pluginPaths(socketsPath, name, ".sock")
|
||||
|
||||
for _, p := range socketpaths {
|
||||
socketPaths := pluginPaths(l.socketsPath, name, ".sock")
|
||||
for _, p := range socketPaths {
|
||||
if fi, err := os.Stat(p); err == nil && fi.Mode()&os.ModeSocket != 0 {
|
||||
return NewLocalPlugin(name, "unix://"+p), nil
|
||||
}
|
||||
}
|
||||
|
||||
var txtspecpaths []string
|
||||
for _, p := range l.SpecsPaths() {
|
||||
txtspecpaths = append(txtspecpaths, pluginPaths(p, name, ".spec")...)
|
||||
txtspecpaths = append(txtspecpaths, pluginPaths(p, name, ".json")...)
|
||||
var txtSpecPaths []string
|
||||
for _, p := range l.specsPaths {
|
||||
txtSpecPaths = append(txtSpecPaths, pluginPaths(p, name, ".spec")...)
|
||||
txtSpecPaths = append(txtSpecPaths, pluginPaths(p, name, ".json")...)
|
||||
}
|
||||
|
||||
for _, p := range txtspecpaths {
|
||||
for _, p := range txtSpecPaths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
if strings.HasSuffix(p, ".json") {
|
||||
return readPluginJSONInfo(name, p)
|
||||
|
@ -113,6 +112,25 @@ func (l *LocalRegistry) Plugin(name string) (*Plugin, error) {
|
|||
return nil, errors.Wrapf(ErrNotFound, "could not find plugin %s in v1 plugin registry", name)
|
||||
}
|
||||
|
||||
// SpecsPaths returns paths in which to look for plugins, in order of priority.
|
||||
//
|
||||
// On Windows:
|
||||
//
|
||||
// - "%programdata%\docker\plugins"
|
||||
//
|
||||
// On Unix in non-rootless mode:
|
||||
//
|
||||
// - "/etc/docker/plugins"
|
||||
// - "/usr/lib/docker/plugins"
|
||||
//
|
||||
// On Unix in rootless-mode:
|
||||
//
|
||||
// - "$XDG_CONFIG_HOME/docker/plugins" (or "/etc/docker/plugins" if $XDG_CONFIG_HOME is not set)
|
||||
// - "$HOME/.local/lib/docker/plugins" (pr "/usr/lib/docker/plugins" if $HOME is set)
|
||||
func SpecsPaths() []string {
|
||||
return specsPaths()
|
||||
}
|
||||
|
||||
func readPluginInfo(name, path string) (*Plugin, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
|
|
|
@ -6,27 +6,12 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func Setup(t *testing.T) (string, func(), LocalRegistry) {
|
||||
tmpdir, err := os.MkdirTemp("", "docker-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backup := socketsPath
|
||||
socketsPath = tmpdir
|
||||
|
||||
return tmpdir, func() {
|
||||
socketsPath = backup
|
||||
os.RemoveAll(tmpdir)
|
||||
}, LocalRegistry{
|
||||
func() []string {
|
||||
return []string{tmpdir}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSpecPlugin(t *testing.T) {
|
||||
tmpdir, unregister, r := Setup(t)
|
||||
defer unregister()
|
||||
tmpdir := t.TempDir()
|
||||
r := LocalRegistry{
|
||||
socketsPath: tmpdir,
|
||||
specsPaths: []string{tmpdir},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
path string
|
||||
|
@ -74,8 +59,11 @@ func TestFileSpecPlugin(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFileJSONSpecPlugin(t *testing.T) {
|
||||
tmpdir, unregister, r := Setup(t)
|
||||
defer unregister()
|
||||
tmpdir := t.TempDir()
|
||||
r := LocalRegistry{
|
||||
socketsPath: tmpdir,
|
||||
specsPaths: []string{tmpdir},
|
||||
}
|
||||
|
||||
p := filepath.Join(tmpdir, "example.json")
|
||||
spec := `{
|
||||
|
@ -119,8 +107,11 @@ func TestFileJSONSpecPlugin(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFileJSONSpecPluginWithoutTLSConfig(t *testing.T) {
|
||||
tmpdir, unregister, r := Setup(t)
|
||||
defer unregister()
|
||||
tmpdir := t.TempDir()
|
||||
r := LocalRegistry{
|
||||
socketsPath: tmpdir,
|
||||
specsPaths: []string{tmpdir},
|
||||
}
|
||||
|
||||
p := filepath.Join(tmpdir, "example.json")
|
||||
spec := `{
|
||||
|
|
|
@ -9,32 +9,23 @@ import (
|
|||
)
|
||||
|
||||
func rootlessConfigPluginsPath() string {
|
||||
configHome, err := homedir.GetConfigHome()
|
||||
if err == nil {
|
||||
if configHome, err := homedir.GetConfigHome(); err != nil {
|
||||
return filepath.Join(configHome, "docker/plugins")
|
||||
}
|
||||
|
||||
return "/etc/docker/plugins"
|
||||
}
|
||||
|
||||
func rootlessLibPluginsPath() string {
|
||||
libHome, err := homedir.GetLibHome()
|
||||
if err == nil {
|
||||
if libHome, err := homedir.GetLibHome(); err == nil {
|
||||
return filepath.Join(libHome, "docker/plugins")
|
||||
}
|
||||
|
||||
return "/usr/lib/docker/plugins"
|
||||
}
|
||||
|
||||
// SpecsPaths returns
|
||||
// { "%programdata%\docker\plugins" } on Windows,
|
||||
// { "/etc/docker/plugins", "/usr/lib/docker/plugins" } on Unix in non-rootless mode,
|
||||
// { "$XDG_CONFIG_HOME/docker/plugins", "$HOME/.local/lib/docker/plugins" } on Unix in rootless mode
|
||||
// with fallback to the corresponding path in non-rootless mode if $XDG_CONFIG_HOME or $HOME is not set.
|
||||
func SpecsPaths() []string {
|
||||
// specsPaths is the non-Windows implementation of [SpecsPaths].
|
||||
func specsPaths() []string {
|
||||
if rootless.RunningWithRootlessKit() {
|
||||
return []string{rootlessConfigPluginsPath(), rootlessLibPluginsPath()}
|
||||
}
|
||||
|
||||
return []string{"/etc/docker/plugins", "/usr/lib/docker/plugins"}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,11 @@ import (
|
|||
|
||||
func TestLocalSocket(t *testing.T) {
|
||||
// TODO Windows: Enable a similar version for Windows named pipes
|
||||
tmpdir, unregister, r := Setup(t)
|
||||
defer unregister()
|
||||
tmpdir := t.TempDir()
|
||||
r := LocalRegistry{
|
||||
socketsPath: tmpdir,
|
||||
specsPaths: []string{tmpdir},
|
||||
}
|
||||
|
||||
cases := []string{
|
||||
filepath.Join(tmpdir, "echo.sock"),
|
||||
|
@ -62,8 +65,11 @@ func TestLocalSocket(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestScan(t *testing.T) {
|
||||
tmpdir, unregister, r := Setup(t)
|
||||
defer unregister()
|
||||
tmpdir := t.TempDir()
|
||||
r := LocalRegistry{
|
||||
socketsPath: tmpdir,
|
||||
specsPaths: []string{tmpdir},
|
||||
}
|
||||
|
||||
pluginNames, err := r.Scan()
|
||||
if err != nil {
|
||||
|
@ -103,8 +109,11 @@ func TestScan(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestScanNotPlugins(t *testing.T) {
|
||||
tmpdir, unregister, localRegistry := Setup(t)
|
||||
defer unregister()
|
||||
tmpdir := t.TempDir()
|
||||
localRegistry := LocalRegistry{
|
||||
socketsPath: tmpdir,
|
||||
specsPaths: []string{tmpdir},
|
||||
}
|
||||
|
||||
// not that `Setup()` above sets the sockets path and spec path dirs, which
|
||||
// `Scan()` uses to find plugins to the returned `tmpdir`
|
||||
|
|
|
@ -5,11 +5,7 @@ import (
|
|||
"path/filepath"
|
||||
)
|
||||
|
||||
// SpecsPaths returns
|
||||
// { "%programdata%\docker\plugins" } on Windows,
|
||||
// { "/etc/docker/plugins", "/usr/lib/docker/plugins" } on Unix in non-rootless mode,
|
||||
// { "$XDG_CONFIG_HOME/docker/plugins", "$HOME/.local/lib/docker/plugins" } on Unix in rootless mode
|
||||
// with fallback to the corresponding path in non-rootless mode if $XDG_CONFIG_HOME or $HOME is not set.
|
||||
func SpecsPaths() []string {
|
||||
// specsPaths is the Windows implementation of [SpecsPaths].
|
||||
func specsPaths() []string {
|
||||
return []string{filepath.Join(os.Getenv("programdata"), "docker", "plugins")}
|
||||
}
|
||||
|
|
|
@ -64,27 +64,33 @@ func TestGet(t *testing.T) {
|
|||
p.Manifest = &Manifest{Implements: []string{fruitImplements}}
|
||||
storage.plugins[fruitPlugin] = p
|
||||
|
||||
plugin, err := Get(fruitPlugin, fruitImplements)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.Name() != plugin.Name() {
|
||||
t.Fatalf("No matching plugin with name %s found", plugin.Name())
|
||||
}
|
||||
if plugin.Client() != nil {
|
||||
t.Fatal("expected nil Client but found one")
|
||||
}
|
||||
if !plugin.IsV1() {
|
||||
t.Fatal("Expected true for V1 plugin")
|
||||
}
|
||||
t.Run("success", func(t *testing.T) {
|
||||
plugin, err := Get(fruitPlugin, fruitImplements)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.Name() != plugin.Name() {
|
||||
t.Errorf("no matching plugin with name %s found", plugin.Name())
|
||||
}
|
||||
if plugin.Client() != nil {
|
||||
t.Error("expected nil Client but found one")
|
||||
}
|
||||
if !plugin.IsV1() {
|
||||
t.Error("Expected true for V1 plugin")
|
||||
}
|
||||
})
|
||||
|
||||
// check negative case where plugin fruit doesn't implement banana
|
||||
_, err = Get("fruit", "banana")
|
||||
assert.Assert(t, errors.Is(err, ErrNotImplements))
|
||||
t.Run("not implemented", func(t *testing.T) {
|
||||
_, err := Get("fruit", "banana")
|
||||
assert.Assert(t, errors.Is(err, ErrNotImplements))
|
||||
})
|
||||
|
||||
// check negative case where plugin vegetable doesn't exist
|
||||
_, err = Get("vegetable", "potato")
|
||||
assert.Assert(t, errors.Is(err, ErrNotFound))
|
||||
t.Run("not exists", func(t *testing.T) {
|
||||
_, err := Get("vegetable", "potato")
|
||||
assert.Assert(t, errors.Is(err, ErrNotFound))
|
||||
})
|
||||
}
|
||||
|
||||
func TestPluginWithNoManifest(t *testing.T) {
|
||||
|
@ -127,8 +133,11 @@ func TestPluginWithNoManifest(t *testing.T) {
|
|||
|
||||
func TestGetAll(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpdir, unregister, r := Setup(t)
|
||||
defer unregister()
|
||||
tmpdir := t.TempDir()
|
||||
r := LocalRegistry{
|
||||
socketsPath: tmpdir,
|
||||
specsPaths: []string{tmpdir},
|
||||
}
|
||||
|
||||
p := filepath.Join(tmpdir, "example.json")
|
||||
spec := `{
|
||||
|
|
|
@ -101,6 +101,12 @@ func (p *Plugin) IsV1() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// ScopedPath returns the path scoped to the plugin's rootfs.
|
||||
// For v1 plugins, this always returns the path unchanged as v1 plugins run directly on the host.
|
||||
func (p *Plugin) ScopedPath(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
// NewLocalPlugin creates a new local plugin.
|
||||
func NewLocalPlugin(name, addr string) *Plugin {
|
||||
return &Plugin{
|
||||
|
@ -195,10 +201,6 @@ func (p *Plugin) implements(kind string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func load(name string) (*Plugin, error) {
|
||||
return loadWithRetry(name, true)
|
||||
}
|
||||
|
||||
func loadWithRetry(name string, retry bool) (*Plugin, error) {
|
||||
registry := NewLocalRegistry()
|
||||
start := time.Now()
|
||||
|
@ -248,7 +250,7 @@ func get(name string) (*Plugin, error) {
|
|||
if ok {
|
||||
return pl, pl.activate()
|
||||
}
|
||||
return load(name)
|
||||
return loadWithRetry(name, true)
|
||||
}
|
||||
|
||||
// Get returns the plugin given the specified name and requested implementation.
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
//go:build !windows
|
||||
|
||||
package plugins // import "github.com/docker/docker/pkg/plugins"
|
||||
|
||||
// ScopedPath returns the path scoped to the plugin's rootfs.
|
||||
// For v1 plugins, this always returns the path unchanged as v1 plugins run directly on the host.
|
||||
func (p *Plugin) ScopedPath(s string) string {
|
||||
return s
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package plugins // import "github.com/docker/docker/pkg/plugins"
|
||||
|
||||
// ScopedPath returns the path scoped to the plugin's rootfs.
|
||||
// For v1 plugins, this always returns the path unchanged as v1 plugins run directly on the host.
|
||||
func (p *Plugin) ScopedPath(s string) string {
|
||||
return s
|
||||
}
|
|
@ -3,20 +3,21 @@ package transport // import "github.com/docker/docker/pkg/plugins/transport"
|
|||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// httpTransport holds an http.RoundTripper
|
||||
// HTTPTransport holds an [http.RoundTripper]
|
||||
// and information about the scheme and address the transport
|
||||
// sends request to.
|
||||
type httpTransport struct {
|
||||
type HTTPTransport struct {
|
||||
http.RoundTripper
|
||||
scheme string
|
||||
addr string
|
||||
}
|
||||
|
||||
// NewHTTPTransport creates a new httpTransport.
|
||||
func NewHTTPTransport(r http.RoundTripper, scheme, addr string) Transport {
|
||||
return httpTransport{
|
||||
// NewHTTPTransport creates a new HTTPTransport.
|
||||
func NewHTTPTransport(r http.RoundTripper, scheme, addr string) *HTTPTransport {
|
||||
return &HTTPTransport{
|
||||
RoundTripper: r,
|
||||
scheme: scheme,
|
||||
addr: addr,
|
||||
|
@ -25,11 +26,15 @@ func NewHTTPTransport(r http.RoundTripper, scheme, addr string) Transport {
|
|||
|
||||
// NewRequest creates a new http.Request and sets the URL
|
||||
// scheme and address with the transport's fields.
|
||||
func (t httpTransport) NewRequest(path string, data io.Reader) (*http.Request, error) {
|
||||
req, err := newHTTPRequest(path, data)
|
||||
func (t HTTPTransport) NewRequest(path string, data io.Reader) (*http.Request, error) {
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, path, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("Accept", VersionMimetype)
|
||||
req.URL.Scheme = t.scheme
|
||||
req.URL.Host = t.addr
|
||||
return req, nil
|
||||
|
|
|
@ -11,8 +11,7 @@ import (
|
|||
|
||||
func TestHTTPTransport(t *testing.T) {
|
||||
var r io.Reader
|
||||
roundTripper := &http.Transport{}
|
||||
newTransport := NewHTTPTransport(roundTripper, "http", "0.0.0.0")
|
||||
newTransport := NewHTTPTransport(&http.Transport{}, "http", "0.0.0.0")
|
||||
request, err := newTransport.NewRequest("", r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
@ -1,36 +1,4 @@
|
|||
package transport // import "github.com/docker/docker/pkg/plugins/transport"
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// VersionMimetype is the Content-Type the engine sends to plugins.
|
||||
const VersionMimetype = "application/vnd.docker.plugins.v1.2+json"
|
||||
|
||||
// RequestFactory defines an interface that
|
||||
// transports can implement to create new requests.
|
||||
type RequestFactory interface {
|
||||
NewRequest(path string, data io.Reader) (*http.Request, error)
|
||||
}
|
||||
|
||||
// Transport defines an interface that plugin transports
|
||||
// must implement.
|
||||
type Transport interface {
|
||||
http.RoundTripper
|
||||
RequestFactory
|
||||
}
|
||||
|
||||
// newHTTPRequest creates a new request with a path and a body.
|
||||
func newHTTPRequest(path string, data io.Reader) (*http.Request, error) {
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, path, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("Accept", VersionMimetype)
|
||||
return req, nil
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue