From 5e3e47b48407b316e96a09d340dddd2cddcaa4d2 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Fri, 25 Jun 2021 15:50:09 +0200 Subject: [PATCH] Local private plugins. Co-authored-by: Julien Salleyron --- .gitignore | 1 + cmd/traefik/plugins.go | 77 +++++++--- cmd/traefik/traefik.go | 10 +- go.mod | 2 +- go.sum | 4 +- pkg/anonymize/anonymize_config_test.go | 10 +- .../testdata/anonymized-static-config.json | 12 +- pkg/config/static/experimental.go | 9 +- pkg/plugins/builder.go | 46 ++++-- pkg/plugins/plugins.go | 137 ++++++++++-------- pkg/plugins/types.go | 11 +- 11 files changed, 196 insertions(+), 123 deletions(-) diff --git a/.gitignore b/.gitignore index b2025089a..5d3608c1f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ cover.out vendor/ plugins-storage/ +plugins-local/ traefik_changelog.md diff --git a/cmd/traefik/plugins.go b/cmd/traefik/plugins.go index ac7730092..0eee4b545 100644 --- a/cmd/traefik/plugins.go +++ b/cmd/traefik/plugins.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "github.com/traefik/traefik/v2/pkg/config/static" "github.com/traefik/traefik/v2/pkg/plugins" ) @@ -8,35 +10,69 @@ import ( const outputDir = "./plugins-storage/" func createPluginBuilder(staticConfiguration *static.Configuration) (*plugins.Builder, error) { - client, plgs, devPlugin, err := initPlugins(staticConfiguration) + client, plgs, localPlgs, err := initPlugins(staticConfiguration) if err != nil { return nil, err } - return plugins.NewBuilder(client, plgs, devPlugin) + return plugins.NewBuilder(client, plgs, localPlgs) } -func initPlugins(staticCfg *static.Configuration) (*plugins.Client, map[string]plugins.Descriptor, *plugins.DevPlugin, error) { - if !isPilotEnabled(staticCfg) || !hasPlugins(staticCfg) { - return nil, map[string]plugins.Descriptor{}, nil, nil - } - - opts := plugins.ClientOptions{ - Output: outputDir, - Token: staticCfg.Pilot.Token, - } - - client, err := plugins.NewClient(opts) +func initPlugins(staticCfg *static.Configuration) (*plugins.Client, map[string]plugins.Descriptor, map[string]plugins.LocalDescriptor, error) { + err := checkUniquePluginNames(staticCfg.Experimental) if err != nil { return nil, nil, nil, err } - err = plugins.Setup(client, staticCfg.Experimental.Plugins, staticCfg.Experimental.DevPlugin) - if err != nil { - return nil, nil, nil, err + var client *plugins.Client + plgs := map[string]plugins.Descriptor{} + + if isPilotEnabled(staticCfg) && hasPlugins(staticCfg) { + opts := plugins.ClientOptions{ + Output: outputDir, + Token: staticCfg.Pilot.Token, + } + + var err error + client, err = plugins.NewClient(opts) + if err != nil { + return nil, nil, nil, err + } + + err = plugins.SetupRemotePlugins(client, staticCfg.Experimental.Plugins) + if err != nil { + return nil, nil, nil, err + } + + plgs = staticCfg.Experimental.Plugins } - return client, staticCfg.Experimental.Plugins, staticCfg.Experimental.DevPlugin, nil + localPlgs := map[string]plugins.LocalDescriptor{} + + if hasLocalPlugins(staticCfg) { + err := plugins.SetupLocalPlugins(staticCfg.Experimental.LocalPlugins) + if err != nil { + return nil, nil, nil, err + } + + localPlgs = staticCfg.Experimental.LocalPlugins + } + + return client, plgs, localPlgs, nil +} + +func checkUniquePluginNames(e *static.Experimental) error { + if e == nil { + return nil + } + + for s := range e.LocalPlugins { + if _, ok := e.Plugins[s]; ok { + return fmt.Errorf("the plugin's name %q must be unique", s) + } + } + + return nil } func isPilotEnabled(staticCfg *static.Configuration) bool { @@ -44,6 +80,9 @@ func isPilotEnabled(staticCfg *static.Configuration) bool { } func hasPlugins(staticCfg *static.Configuration) bool { - return staticCfg.Experimental != nil && - (len(staticCfg.Experimental.Plugins) > 0 || staticCfg.Experimental.DevPlugin != nil) + return staticCfg.Experimental != nil && len(staticCfg.Experimental.Plugins) > 0 +} + +func hasLocalPlugins(staticCfg *static.Configuration) bool { + return staticCfg.Experimental != nil && len(staticCfg.Experimental.LocalPlugins) > 0 } diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 4ebe82068..912f2b4dd 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -126,12 +126,6 @@ func runCmd(staticConfiguration *static.Configuration) error { ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - if staticConfiguration.Experimental != nil && staticConfiguration.Experimental.DevPlugin != nil { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, 30*time.Minute) - defer cancel() - } - if staticConfiguration.Ping != nil { staticConfiguration.Ping.WithContext(ctx) } @@ -240,8 +234,8 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err // Providers plugins - for s, i := range staticConfiguration.Providers.Plugin { - p, err := pluginBuilder.BuildProvider(s, i) + for name, conf := range staticConfiguration.Providers.Plugin { + p, err := pluginBuilder.BuildProvider(name, conf) if err != nil { return nil, fmt.Errorf("plugin: failed to build provider: %w", err) } diff --git a/go.mod b/go.mod index e9187c700..2b8c1619b 100644 --- a/go.mod +++ b/go.mod @@ -71,7 +71,7 @@ require ( github.com/tinylib/msgp v1.0.2 // indirect github.com/traefik/gziphandler v1.1.2-0.20210212101304-175e0fad6888 github.com/traefik/paerser v0.1.4 - github.com/traefik/yaegi v0.9.17 + github.com/traefik/yaegi v0.9.19 github.com/uber/jaeger-client-go v2.29.1+incompatible github.com/uber/jaeger-lib v2.2.0+incompatible github.com/unrolled/render v1.0.2 diff --git a/go.sum b/go.sum index 4f2bd22d4..9d2caf0a0 100644 --- a/go.sum +++ b/go.sum @@ -1020,8 +1020,8 @@ github.com/traefik/gziphandler v1.1.2-0.20210212101304-175e0fad6888 h1:GMY0C+M/w github.com/traefik/gziphandler v1.1.2-0.20210212101304-175e0fad6888/go.mod h1:sLqwoN03tkluITKL+lPEZbfsJQU2suYoKbrR/HeV9aM= github.com/traefik/paerser v0.1.4 h1:/IXjV04Gf6di51H8Jl7jyS3OylsLjIasrwXIIwj1aT8= github.com/traefik/paerser v0.1.4/go.mod h1:FIdQ4Y92ulQUGSeZgxchtBKEcLw1o551PMNg9PoIq/4= -github.com/traefik/yaegi v0.9.17 h1:sJ4Wk6S7HHHXtJnOuxC/3qjdQKRy3q9ZhNP0ZGL7Ltw= -github.com/traefik/yaegi v0.9.17/go.mod h1:FAYnRlZyuVlEkvnkHq3bvJ1lW5be6XuwgLdkYgYG6Lk= +github.com/traefik/yaegi v0.9.19 h1:ze01+pVtKmxSogy0wlAPSvm2LoDYuZj2LdH3S6GxHcQ= +github.com/traefik/yaegi v0.9.19/go.mod h1:FAYnRlZyuVlEkvnkHq3bvJ1lW5be6XuwgLdkYgYG6Lk= github.com/transip/gotransip/v6 v6.2.0 h1:0Z+qVsyeiQdWfcAUeJyF0IEKAPvhJwwpwPi2WGtBIiE= github.com/transip/gotransip/v6 v6.2.0/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= diff --git a/pkg/anonymize/anonymize_config_test.go b/pkg/anonymize/anonymize_config_test.go index d874beb7e..e8f2703aa 100644 --- a/pkg/anonymize/anonymize_config_test.go +++ b/pkg/anonymize/anonymize_config_test.go @@ -958,9 +958,13 @@ func TestDo_staticConfiguration(t *testing.T) { Version: "foobar", }, }, - DevPlugin: &plugins.DevPlugin{ - GoPath: "foobar", - ModuleName: "foobar", + LocalPlugins: map[string]plugins.LocalDescriptor{ + "Descriptor0": { + ModuleName: "foobar", + }, + "Descriptor1": { + ModuleName: "foobar", + }, }, } diff --git a/pkg/anonymize/testdata/anonymized-static-config.json b/pkg/anonymize/testdata/anonymized-static-config.json index ea511146a..76ff7cdc2 100644 --- a/pkg/anonymize/testdata/anonymized-static-config.json +++ b/pkg/anonymize/testdata/anonymized-static-config.json @@ -456,9 +456,13 @@ "version": "foobar" } }, - "devPlugin": { - "goPath": "foobar", - "moduleName": "foobar" + "localPlugins": { + "Descriptor0": { + "moduleName": "foobar" + }, + "Descriptor1": { + "moduleName": "foobar" + } } } -} \ No newline at end of file +} diff --git a/pkg/config/static/experimental.go b/pkg/config/static/experimental.go index e6be2525c..cb6cd097f 100644 --- a/pkg/config/static/experimental.go +++ b/pkg/config/static/experimental.go @@ -4,8 +4,9 @@ import "github.com/traefik/traefik/v2/pkg/plugins" // Experimental experimental Traefik features. type Experimental struct { - Plugins map[string]plugins.Descriptor `description:"Plugins configuration." json:"plugins,omitempty" toml:"plugins,omitempty" yaml:"plugins,omitempty" export:"true"` - DevPlugin *plugins.DevPlugin `description:"Dev plugin configuration." json:"devPlugin,omitempty" toml:"devPlugin,omitempty" yaml:"devPlugin,omitempty" export:"true"` - KubernetesGateway bool `description:"Allow the Kubernetes gateway api provider usage." json:"kubernetesGateway,omitempty" toml:"kubernetesGateway,omitempty" yaml:"kubernetesGateway,omitempty" export:"true"` - HTTP3 bool `description:"Enable HTTP3." json:"http3,omitempty" toml:"http3,omitempty" yaml:"http3,omitempty" export:"true"` + Plugins map[string]plugins.Descriptor `description:"Plugins configuration." json:"plugins,omitempty" toml:"plugins,omitempty" yaml:"plugins,omitempty" export:"true"` + LocalPlugins map[string]plugins.LocalDescriptor `description:"Local plugins configuration." json:"localPlugins,omitempty" toml:"localPlugins,omitempty" yaml:"localPlugins,omitempty" export:"true"` + + KubernetesGateway bool `description:"Allow the Kubernetes gateway api provider usage." json:"kubernetesGateway,omitempty" toml:"kubernetesGateway,omitempty" yaml:"kubernetesGateway,omitempty" export:"true"` + HTTP3 bool `description:"Enable HTTP3." json:"http3,omitempty" toml:"http3,omitempty" yaml:"http3,omitempty" export:"true"` } diff --git a/pkg/plugins/builder.go b/pkg/plugins/builder.go index fa2bfd90e..b7ff69555 100644 --- a/pkg/plugins/builder.go +++ b/pkg/plugins/builder.go @@ -9,8 +9,6 @@ import ( "github.com/traefik/yaegi/stdlib" ) -const devPluginName = "dev" - // Constructor creates a plugin handler. type Constructor func(context.Context, http.Handler) (http.Handler, error) @@ -35,7 +33,7 @@ type Builder struct { } // NewBuilder creates a new Builder. -func NewBuilder(client *Client, plugins map[string]Descriptor, devPlugin *DevPlugin) (*Builder, error) { +func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[string]LocalDescriptor) (*Builder, error) { pb := &Builder{ middlewareDescriptors: map[string]pluginContext{}, providerDescriptors: map[string]pluginContext{}, @@ -49,8 +47,16 @@ func NewBuilder(client *Client, plugins map[string]Descriptor, devPlugin *DevPlu } i := interp.New(interp.Options{GoPath: client.GoPath()}) - i.Use(stdlib.Symbols) - i.Use(ppSymbols()) + + err = i.Use(stdlib.Symbols) + if err != nil { + return nil, fmt.Errorf("%s: failed to load symbols: %w", desc.ModuleName, err) + } + + err = i.Use(ppSymbols()) + if err != nil { + return nil, fmt.Errorf("%s: failed to load provider symbols: %w", desc.ModuleName, err) + } _, err = i.Eval(fmt.Sprintf(`import "%s"`, manifest.Import)) if err != nil { @@ -77,33 +83,41 @@ func NewBuilder(client *Client, plugins map[string]Descriptor, devPlugin *DevPlu } } - if devPlugin != nil { - manifest, err := ReadManifest(devPlugin.GoPath, devPlugin.ModuleName) + for pName, desc := range localPlugins { + manifest, err := ReadManifest(localGoPath, desc.ModuleName) if err != nil { - return nil, fmt.Errorf("%s: failed to read manifest: %w", devPlugin.ModuleName, err) + return nil, fmt.Errorf("%s: failed to read manifest: %w", desc.ModuleName, err) } - i := interp.New(interp.Options{GoPath: devPlugin.GoPath}) - i.Use(stdlib.Symbols) - i.Use(ppSymbols()) + i := interp.New(interp.Options{GoPath: localGoPath}) + + err = i.Use(stdlib.Symbols) + if err != nil { + return nil, fmt.Errorf("%s: failed to load symbols: %w", desc.ModuleName, err) + } + + err = i.Use(ppSymbols()) + if err != nil { + return nil, fmt.Errorf("%s: failed to load provider symbols: %w", desc.ModuleName, err) + } _, err = i.Eval(fmt.Sprintf(`import "%s"`, manifest.Import)) if err != nil { - return nil, fmt.Errorf("%s: failed to import plugin code %q: %w", devPlugin.ModuleName, manifest.Import, err) + return nil, fmt.Errorf("%s: failed to import plugin code %q: %w", desc.ModuleName, manifest.Import, err) } switch manifest.Type { case "middleware": - pb.middlewareDescriptors[devPluginName] = pluginContext{ + pb.middlewareDescriptors[pName] = pluginContext{ interpreter: i, - GoPath: devPlugin.GoPath, + GoPath: localGoPath, Import: manifest.Import, BasePkg: manifest.BasePkg, } case "provider": - pb.providerDescriptors[devPluginName] = pluginContext{ + pb.providerDescriptors[pName] = pluginContext{ interpreter: i, - GoPath: devPlugin.GoPath, + GoPath: localGoPath, Import: manifest.Import, BasePkg: manifest.BasePkg, } diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index ec7775f04..fdfb9fbce 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -6,12 +6,15 @@ import ( "fmt" "strings" + "github.com/hashicorp/go-multierror" "github.com/traefik/traefik/v2/pkg/log" ) -// Setup setup plugins environment. -func Setup(client *Client, plugins map[string]Descriptor, devPlugin *DevPlugin) error { - err := checkPluginsConfiguration(plugins) +const localGoPath = "./plugins-local/" + +// SetupRemotePlugins setup remote plugins environment. +func SetupRemotePlugins(client *Client, plugins map[string]Descriptor) error { + err := checkRemotePluginsConfiguration(plugins) if err != nil { return fmt.Errorf("invalid configuration: %w", err) } @@ -53,65 +56,10 @@ func Setup(client *Client, plugins map[string]Descriptor, devPlugin *DevPlugin) } } - if devPlugin != nil { - err := checkDevPluginConfiguration(devPlugin) - if err != nil { - return fmt.Errorf("invalid configuration: %w", err) - } - } - return nil } -func checkDevPluginConfiguration(plugin *DevPlugin) error { - if plugin == nil { - return nil - } - - if plugin.GoPath == "" { - return errors.New("missing Go Path (prefer a dedicated Go Path)") - } - - if plugin.ModuleName == "" { - return errors.New("missing module name") - } - - m, err := ReadManifest(plugin.GoPath, plugin.ModuleName) - if err != nil { - return err - } - - switch m.Type { - case "middleware", "provider": - // noop - default: - return errors.New("unsupported type") - } - - if m.Import == "" { - return errors.New("missing import") - } - - if !strings.HasPrefix(m.Import, plugin.ModuleName) { - return fmt.Errorf("the import %q must be related to the module name %q", m.Import, plugin.ModuleName) - } - - if m.DisplayName == "" { - return errors.New("missing DisplayName") - } - - if m.Summary == "" { - return errors.New("missing Summary") - } - - if m.TestData == nil { - return errors.New("missing TestData") - } - - return nil -} - -func checkPluginsConfiguration(plugins map[string]Descriptor) error { +func checkRemotePluginsConfiguration(plugins map[string]Descriptor) error { if plugins == nil { return nil } @@ -147,3 +95,74 @@ func checkPluginsConfiguration(plugins map[string]Descriptor) error { return nil } + +// SetupLocalPlugins setup local plugins environment. +func SetupLocalPlugins(plugins map[string]LocalDescriptor) error { + if plugins == nil { + return nil + } + + uniq := make(map[string]struct{}) + + var errs *multierror.Error + for pAlias, descriptor := range plugins { + if descriptor.ModuleName == "" { + errs = multierror.Append(errs, fmt.Errorf("%s: plugin name is missing", pAlias)) + } + + if strings.HasPrefix(descriptor.ModuleName, "/") || strings.HasSuffix(descriptor.ModuleName, "/") { + errs = multierror.Append(errs, fmt.Errorf("%s: plugin name should not start or end with a /", pAlias)) + continue + } + + if _, ok := uniq[descriptor.ModuleName]; ok { + errs = multierror.Append(errs, fmt.Errorf("only one version of a plugin is allowed, there is a duplicate of %s", descriptor.ModuleName)) + continue + } + + uniq[descriptor.ModuleName] = struct{}{} + + err := checkLocalPluginManifest(descriptor) + errs = multierror.Append(errs, err) + } + + return errs.ErrorOrNil() +} + +func checkLocalPluginManifest(descriptor LocalDescriptor) error { + m, err := ReadManifest(localGoPath, descriptor.ModuleName) + if err != nil { + return err + } + + var errs *multierror.Error + + switch m.Type { + case "middleware", "provider": + // noop + default: + errs = multierror.Append(errs, fmt.Errorf("%s: unsupported type %q", descriptor.ModuleName, m.Type)) + } + + if m.Import == "" { + errs = multierror.Append(errs, fmt.Errorf("%s: missing import", descriptor.ModuleName)) + } + + if !strings.HasPrefix(m.Import, descriptor.ModuleName) { + errs = multierror.Append(errs, fmt.Errorf("the import %q must be related to the module name %q", m.Import, descriptor.ModuleName)) + } + + if m.DisplayName == "" { + errs = multierror.Append(errs, fmt.Errorf("%s: missing DisplayName", descriptor.ModuleName)) + } + + if m.Summary == "" { + errs = multierror.Append(errs, fmt.Errorf("%s: missing Summary", descriptor.ModuleName)) + } + + if m.TestData == nil { + errs = multierror.Append(errs, fmt.Errorf("%s: missing TestData", descriptor.ModuleName)) + } + + return errs.ErrorOrNil() +} diff --git a/pkg/plugins/types.go b/pkg/plugins/types.go index b93c972d4..78ea13a34 100644 --- a/pkg/plugins/types.go +++ b/pkg/plugins/types.go @@ -1,6 +1,6 @@ package plugins -// Descriptor The static part of a plugin configuration (prod). +// Descriptor The static part of a plugin configuration. type Descriptor struct { // ModuleName (required) ModuleName string `description:"plugin's module name." json:"moduleName,omitempty" toml:"moduleName,omitempty" yaml:"moduleName,omitempty" export:"true"` @@ -9,13 +9,10 @@ type Descriptor struct { Version string `description:"plugin's version." json:"version,omitempty" toml:"version,omitempty" yaml:"version,omitempty" export:"true"` } -// DevPlugin The static part of a plugin configuration (only for dev). -type DevPlugin struct { - // GoPath plugin's GOPATH. (required) - GoPath string `description:"plugin's GOPATH." json:"goPath,omitempty" toml:"goPath,omitempty" yaml:"goPath,omitempty" export:"true"` - +// LocalDescriptor The static part of a local plugin configuration. +type LocalDescriptor struct { // ModuleName (required) - ModuleName string `description:"plugin's module name." json:"moduleName,omitempty" toml:"moduleName,omitempty" yaml:"moduleName,omitempty" export:"true"` + ModuleName string `description:"plugin's module name." json:"moduleName,omitempty" toml:"moduleName,omitempty" yaml:"moduleName,omitempty" export:"true"` } // Manifest The plugin manifest.