From 9b1e89375599573733d4b9a0717345f923c1fbc7 Mon Sep 17 00:00:00 2001 From: Anton Tiurin Date: Fri, 20 Jan 2017 14:20:52 +0300 Subject: [PATCH] Introduce dynamic plugins go1.8 Plugin package brings a mechanism for dynamyc loading. StorageDriver or AccessController can be compiled as plugin and can be loaded at runtime. Signed-off-by: Anton Tiurin --- Dockerfile | 4 +- circle.yml | 2 +- configuration/configuration.go | 3 + docs/configuration.md | 19 +++++ registry/auth/auth.go | 11 ++- registry/handlers/app.go | 15 ++-- registry/pluginloader/loaders_18_test.go | 80 +++++++++++++++++++ registry/pluginloader/pluginloader.go | 54 +++++++++++++ registry/pluginloader/pluginloader_pre18.go | 13 +++ registry/pluginloader/suffix.go | 5 ++ registry/pluginloader/suffix_darwin.go | 5 ++ registry/pluginloader/suffix_windows.go | 5 ++ .../pluginloader/testplugin/testplugin.go | 8 ++ 13 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 registry/pluginloader/loaders_18_test.go create mode 100644 registry/pluginloader/pluginloader.go create mode 100644 registry/pluginloader/pluginloader_pre18.go create mode 100644 registry/pluginloader/suffix.go create mode 100644 registry/pluginloader/suffix_darwin.go create mode 100644 registry/pluginloader/suffix_windows.go create mode 100644 registry/pluginloader/testplugin/testplugin.go diff --git a/Dockerfile b/Dockerfile index fe00241a..c7efaddf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,10 @@ -FROM golang:1.8rc2-alpine +FROM golang:1.8-alpine ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution ENV DOCKER_BUILDTAGS include_oss include_gcs RUN set -ex \ - && apk add --no-cache make git + && apk add --no-cache make git build-base WORKDIR $DISTRIBUTION_DIR COPY . $DISTRIBUTION_DIR diff --git a/circle.yml b/circle.yml index 52dee234..045d13db 100644 --- a/circle.yml +++ b/circle.yml @@ -8,7 +8,7 @@ machine: post: # go - - gvm install go1.8rc2 --prefer-binary --name=stable + - gvm install go1.8 --prefer-binary --name=stable environment: # Convenient shortcuts to "common" locations diff --git a/configuration/configuration.go b/configuration/configuration.go index d72fe7d1..d04d3d41 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -48,6 +48,9 @@ type Configuration struct { // deprecated. Please use Log.Level in the future. Loglevel Loglevel `yaml:"loglevel,omitempty"` + // Plugins is a path where plugins are expected to be found + Plugins []string `yaml:"plugins,omitempty"` + // Storage is the configuration for the registry's storage driver Storage Storage `yaml:"storage"` diff --git a/docs/configuration.md b/docs/configuration.md index 805328ea..e4bc9909 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -89,6 +89,10 @@ log: to: - errors@example.com loglevel: debug # deprecated: use "log" +plugins: + - /plugins/ + - /plugin/driver1.so + - /plugin/auth1.so storage: filesystem: rootdirectory: /var/lib/registry @@ -359,6 +363,21 @@ loglevel: debug Permitted values are `error`, `warn`, `info` and `debug`. The default is `info`. +## `plugins` + +```none +plugins: + - /plugins/ + - /plugin/driver1.so + - /plugin/auth1.so +``` + +> Requires golang >= 1.8 + +Directory with plugins or paths point to plugins. Plugin are loaded before trying to initialize +any driver or authcontroller. If a path is a directory, it is scanned for plugins loading all files +with a is common shared library extension on the platform (`.so`, `.dylib`, `dll`). + ## `storage` ```none diff --git a/registry/auth/auth.go b/registry/auth/auth.go index 1c9af882..f2db0e92 100644 --- a/registry/auth/auth.go +++ b/registry/auth/auth.go @@ -198,5 +198,14 @@ func GetAccessController(name string, options map[string]interface{}) (AccessCon return initFunc(options) } - return nil, fmt.Errorf("no access controller registered with name: %s", name) + return nil, InvalidAccessControllerError{name} +} + +// InvalidAccessControllerError records an attempt to construct an unregistered storage driver +type InvalidAccessControllerError struct { + Name string +} + +func (err InvalidAccessControllerError) Error() string { + return fmt.Sprintf("no access controller registered with name: %s", err.Name) } diff --git a/registry/handlers/app.go b/registry/handlers/app.go index 04e346cc..5e4bac7f 100644 --- a/registry/handlers/app.go +++ b/registry/handlers/app.go @@ -27,6 +27,7 @@ import ( "github.com/docker/distribution/registry/auth" registrymiddleware "github.com/docker/distribution/registry/middleware/registry" repositorymiddleware "github.com/docker/distribution/registry/middleware/repository" + "github.com/docker/distribution/registry/pluginloader" "github.com/docker/distribution/registry/proxy" "github.com/docker/distribution/registry/storage" memorycache "github.com/docker/distribution/registry/storage/cache/memory" @@ -117,9 +118,6 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App { var err error app.driver, err = factory.Create(config.Storage.Type(), storageParams) if err != nil { - // TODO(stevvooe): Move the creation of a service into a protected - // method, where this is created lazily. Its status can be queried via - // a health check. panic(err) } @@ -157,6 +155,12 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App { app.configureRedis(config) app.configureLogHook(config) + if len(config.Plugins) != 0 { + if err = pluginloader.LoadPlugins(app, config.Plugins); err != nil { + ctxu.GetLogger(app).Errorf("could not load plugins from %s: %v", config.Plugins, err) + } + } + options := registrymiddleware.GetRegistryOptions() if config.Compatibility.Schema1.TrustKey != "" { app.trustKey, err = libtrust.LoadKeyFile(config.Compatibility.Schema1.TrustKey) @@ -301,11 +305,10 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App { authType := config.Auth.Type() if authType != "" { - accessController, err := auth.GetAccessController(config.Auth.Type(), config.Auth.Parameters()) + app.accessController, err = auth.GetAccessController(authType, config.Auth.Parameters()) if err != nil { - panic(fmt.Sprintf("unable to configure authorization (%s): %v", authType, err)) + panic(err) } - app.accessController = accessController ctxu.GetLogger(app).Debugf("configured %q access controller", authType) } diff --git a/registry/pluginloader/loaders_18_test.go b/registry/pluginloader/loaders_18_test.go new file mode 100644 index 00000000..359d6fc0 --- /dev/null +++ b/registry/pluginloader/loaders_18_test.go @@ -0,0 +1,80 @@ +// +build go1.8,!race + +package pluginloader + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "testing" + + check "gopkg.in/check.v1" + + ctxu "github.com/docker/distribution/context" + "github.com/docker/distribution/registry/auth" + storagedriver "github.com/docker/distribution/registry/storage/driver" + "github.com/docker/distribution/registry/storage/driver/factory" + "github.com/docker/distribution/registry/storage/driver/testsuites" +) + +func TestPlugins(t *testing.T) { + tempdir, err := ioutil.TempDir("", "ditributiontestplugins") + if err != nil { + t.Fatalf("can not create tempdirectory %v\n", err) + } + defer os.RemoveAll(tempdir) + + pluginpath := filepath.Join(tempdir, "testplugin"+suffix) + gobinary, err := exec.LookPath("go") + if err != nil { + t.Fatalf("LookPath can not locate go binary: %v", err) + } + t.Logf("compile plugin into %s by %s\n", pluginpath, gobinary) + cmd := exec.Command(gobinary, "build", "-o", pluginpath, "-buildmode", "plugin", "github.com/docker/distribution/registry/pluginloader/testplugin") + if err = cmd.Run(); err != nil { + output, _ := cmd.CombinedOutput() + t.Fatalf("plugin compilation failed %v %s\n", err, output) + } + + t.Run("TestLoadPlugin", func(t *testing.T) { + _, err := factory.Create("inmemory", nil) + if _, ok := err.(factory.InvalidStorageDriverError); !ok { + t.Fatalf("inmemory driver is not expected to be built in: %T\n", err) + } + + _, err = auth.GetAccessController("silly", nil) + if _, ok := err.(auth.InvalidAccessControllerError); !ok { + t.Fatalf("silly plugin is not expected to be built in: %T\n", err) + } + + err = LoadPlugins(ctxu.Background(), []string{pluginpath}) + if err != nil { + t.Fatalf("loading failed %v\n", err) + } + + dr, err := factory.Create("inmemory", nil) + if err != nil { + t.Fatalf("dynamic driver construction failed %v\n", err) + } + if dr == nil { + t.Fatal("driver is not expected to be nil") + } + + ac, err := auth.GetAccessController("silly", map[string]interface{}{"realm": "realm", "service": "dummy"}) + if err != nil { + t.Fatalf("AccessController construction failed %v\n", err) + } + if ac == nil { + t.Fatal("AccessController is not expected to be nil") + } + }) + + t.Run("InmemoryDriverTestSuite", func(t *testing.T) { + inmemoryDriverConstructor := func() (storagedriver.StorageDriver, error) { + return factory.Create("inmemory", nil) + } + testsuites.RegisterSuite(inmemoryDriverConstructor, testsuites.NeverSkip) + check.TestingT(t) + }) +} diff --git a/registry/pluginloader/pluginloader.go b/registry/pluginloader/pluginloader.go new file mode 100644 index 00000000..141d26ec --- /dev/null +++ b/registry/pluginloader/pluginloader.go @@ -0,0 +1,54 @@ +// +build go1.8 + +package pluginloader + +import ( + "os" + "path/filepath" + "plugin" + + ctxu "github.com/docker/distribution/context" +) + +// LoadPlugins loads plugins pointed by paths. If a path points to a directory +// this directory is scanned for files with a platform specific shared library suffix (like .so, .dylib, .dll). +// NOTE: Plugins are expected to register themselves the same way as built-ins do. +// Storage drivers should use `factory.Register`, AccessControllers `auth.Register` in init(). +func LoadPlugins(ctx ctxu.Context, paths []string) error { + for _, pluginpath := range paths { + fi, err := os.Stat(pluginpath) + if err != nil { + if os.IsNotExist(err) { + ctxu.GetLogger(ctx).Errorf("plugin file %s does not exist", pluginpath) + } else { + ctxu.GetLogger(ctx).Errorf("could not Stat plugin file %s: %v", pluginpath, err) + } + continue + } + + if !fi.IsDir() { + if err = loadplugin(pluginpath); err != nil { + ctxu.GetLogger(ctx).Errorf("could not load plugin %s: %v", pluginpath, err) + } + continue + } + + // To preserve the order we do not append this plugins to the paths + matches, err := filepath.Glob(filepath.Join(pluginpath, "*"+suffix)) + if err != nil { + return err + } + + for _, pluginpath := range matches { + if err = loadplugin(pluginpath); err != nil { + ctxu.GetLogger(ctx).Errorf("could not load plugin %s: %v", pluginpath, err) + } + } + } + return nil +} + +func loadplugin(path string) error { + _, err := plugin.Open(path) + return err +} diff --git a/registry/pluginloader/pluginloader_pre18.go b/registry/pluginloader/pluginloader_pre18.go new file mode 100644 index 00000000..10d1271c --- /dev/null +++ b/registry/pluginloader/pluginloader_pre18.go @@ -0,0 +1,13 @@ +// +build !go1.8 + +package pluginloader + +import ( + "fmt" + + "github.com/docker/distribution/context" +) + +func LoadPlugins(ctx context.Context, paths []string) error { + return fmt.Errorf("only golang >= 1.8 supports dynamic plugins") +} diff --git a/registry/pluginloader/suffix.go b/registry/pluginloader/suffix.go new file mode 100644 index 00000000..931947bc --- /dev/null +++ b/registry/pluginloader/suffix.go @@ -0,0 +1,5 @@ +// +build !darwin,!windows + +package pluginloader + +const suffix = ".so" diff --git a/registry/pluginloader/suffix_darwin.go b/registry/pluginloader/suffix_darwin.go new file mode 100644 index 00000000..e523bfc1 --- /dev/null +++ b/registry/pluginloader/suffix_darwin.go @@ -0,0 +1,5 @@ +// +build darwin + +package pluginloader + +const suffix = ".dylib" diff --git a/registry/pluginloader/suffix_windows.go b/registry/pluginloader/suffix_windows.go new file mode 100644 index 00000000..7715a2a2 --- /dev/null +++ b/registry/pluginloader/suffix_windows.go @@ -0,0 +1,5 @@ +// +build windows + +package pluginloader + +const suffix = ".dll" diff --git a/registry/pluginloader/testplugin/testplugin.go b/registry/pluginloader/testplugin/testplugin.go new file mode 100644 index 00000000..5f298a2f --- /dev/null +++ b/registry/pluginloader/testplugin/testplugin.go @@ -0,0 +1,8 @@ +package main + +import ( + _ "github.com/docker/distribution/registry/auth/silly" + _ "github.com/docker/distribution/registry/storage/driver/inmemory" +) + +func main() {}