Local private plugins.

Co-authored-by: Julien Salleyron <julien.salleyron@gmail.com>
This commit is contained in:
Ludovic Fernandez 2021-06-25 15:50:09 +02:00 committed by GitHub
parent a243ac4dde
commit 5e3e47b484
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 196 additions and 123 deletions

1
.gitignore vendored
View file

@ -17,4 +17,5 @@
cover.out cover.out
vendor/ vendor/
plugins-storage/ plugins-storage/
plugins-local/
traefik_changelog.md traefik_changelog.md

View file

@ -1,6 +1,8 @@
package main package main
import ( import (
"fmt"
"github.com/traefik/traefik/v2/pkg/config/static" "github.com/traefik/traefik/v2/pkg/config/static"
"github.com/traefik/traefik/v2/pkg/plugins" "github.com/traefik/traefik/v2/pkg/plugins"
) )
@ -8,35 +10,69 @@ import (
const outputDir = "./plugins-storage/" const outputDir = "./plugins-storage/"
func createPluginBuilder(staticConfiguration *static.Configuration) (*plugins.Builder, error) { func createPluginBuilder(staticConfiguration *static.Configuration) (*plugins.Builder, error) {
client, plgs, devPlugin, err := initPlugins(staticConfiguration) client, plgs, localPlgs, err := initPlugins(staticConfiguration)
if err != nil { if err != nil {
return nil, err 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) { func initPlugins(staticCfg *static.Configuration) (*plugins.Client, map[string]plugins.Descriptor, map[string]plugins.LocalDescriptor, error) {
if !isPilotEnabled(staticCfg) || !hasPlugins(staticCfg) { err := checkUniquePluginNames(staticCfg.Experimental)
return nil, map[string]plugins.Descriptor{}, nil, nil
}
opts := plugins.ClientOptions{
Output: outputDir,
Token: staticCfg.Pilot.Token,
}
client, err := plugins.NewClient(opts)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
err = plugins.Setup(client, staticCfg.Experimental.Plugins, staticCfg.Experimental.DevPlugin) var client *plugins.Client
if err != nil { plgs := map[string]plugins.Descriptor{}
return nil, nil, nil, err
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 { func isPilotEnabled(staticCfg *static.Configuration) bool {
@ -44,6 +80,9 @@ func isPilotEnabled(staticCfg *static.Configuration) bool {
} }
func hasPlugins(staticCfg *static.Configuration) bool { func hasPlugins(staticCfg *static.Configuration) bool {
return staticCfg.Experimental != nil && return staticCfg.Experimental != nil && len(staticCfg.Experimental.Plugins) > 0
(len(staticCfg.Experimental.Plugins) > 0 || staticCfg.Experimental.DevPlugin != nil) }
func hasLocalPlugins(staticCfg *static.Configuration) bool {
return staticCfg.Experimental != nil && len(staticCfg.Experimental.LocalPlugins) > 0
} }

View file

@ -126,12 +126,6 @@ func runCmd(staticConfiguration *static.Configuration) error {
ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) 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 { if staticConfiguration.Ping != nil {
staticConfiguration.Ping.WithContext(ctx) staticConfiguration.Ping.WithContext(ctx)
} }
@ -240,8 +234,8 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
// Providers plugins // Providers plugins
for s, i := range staticConfiguration.Providers.Plugin { for name, conf := range staticConfiguration.Providers.Plugin {
p, err := pluginBuilder.BuildProvider(s, i) p, err := pluginBuilder.BuildProvider(name, conf)
if err != nil { if err != nil {
return nil, fmt.Errorf("plugin: failed to build provider: %w", err) return nil, fmt.Errorf("plugin: failed to build provider: %w", err)
} }

2
go.mod
View file

@ -71,7 +71,7 @@ require (
github.com/tinylib/msgp v1.0.2 // indirect github.com/tinylib/msgp v1.0.2 // indirect
github.com/traefik/gziphandler v1.1.2-0.20210212101304-175e0fad6888 github.com/traefik/gziphandler v1.1.2-0.20210212101304-175e0fad6888
github.com/traefik/paerser v0.1.4 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-client-go v2.29.1+incompatible
github.com/uber/jaeger-lib v2.2.0+incompatible github.com/uber/jaeger-lib v2.2.0+incompatible
github.com/unrolled/render v1.0.2 github.com/unrolled/render v1.0.2

4
go.sum
View file

@ -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/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 h1:/IXjV04Gf6di51H8Jl7jyS3OylsLjIasrwXIIwj1aT8=
github.com/traefik/paerser v0.1.4/go.mod h1:FIdQ4Y92ulQUGSeZgxchtBKEcLw1o551PMNg9PoIq/4= 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.19 h1:ze01+pVtKmxSogy0wlAPSvm2LoDYuZj2LdH3S6GxHcQ=
github.com/traefik/yaegi v0.9.17/go.mod h1:FAYnRlZyuVlEkvnkHq3bvJ1lW5be6XuwgLdkYgYG6Lk= 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 h1:0Z+qVsyeiQdWfcAUeJyF0IEKAPvhJwwpwPi2WGtBIiE=
github.com/transip/gotransip/v6 v6.2.0/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g= 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= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=

View file

@ -958,9 +958,13 @@ func TestDo_staticConfiguration(t *testing.T) {
Version: "foobar", Version: "foobar",
}, },
}, },
DevPlugin: &plugins.DevPlugin{ LocalPlugins: map[string]plugins.LocalDescriptor{
GoPath: "foobar", "Descriptor0": {
ModuleName: "foobar", ModuleName: "foobar",
},
"Descriptor1": {
ModuleName: "foobar",
},
}, },
} }

View file

@ -456,9 +456,13 @@
"version": "foobar" "version": "foobar"
} }
}, },
"devPlugin": { "localPlugins": {
"goPath": "foobar", "Descriptor0": {
"moduleName": "foobar" "moduleName": "foobar"
},
"Descriptor1": {
"moduleName": "foobar"
}
} }
} }
} }

View file

@ -4,8 +4,9 @@ import "github.com/traefik/traefik/v2/pkg/plugins"
// Experimental experimental Traefik features. // Experimental experimental Traefik features.
type Experimental struct { type Experimental struct {
Plugins map[string]plugins.Descriptor `description:"Plugins configuration." json:"plugins,omitempty" toml:"plugins,omitempty" yaml:"plugins,omitempty" export:"true"` 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"` 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"` 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"`
} }

View file

@ -9,8 +9,6 @@ import (
"github.com/traefik/yaegi/stdlib" "github.com/traefik/yaegi/stdlib"
) )
const devPluginName = "dev"
// Constructor creates a plugin handler. // Constructor creates a plugin handler.
type Constructor func(context.Context, http.Handler) (http.Handler, error) type Constructor func(context.Context, http.Handler) (http.Handler, error)
@ -35,7 +33,7 @@ type Builder struct {
} }
// NewBuilder creates a new Builder. // 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{ pb := &Builder{
middlewareDescriptors: map[string]pluginContext{}, middlewareDescriptors: map[string]pluginContext{},
providerDescriptors: 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 := 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)) _, err = i.Eval(fmt.Sprintf(`import "%s"`, manifest.Import))
if err != nil { if err != nil {
@ -77,33 +83,41 @@ func NewBuilder(client *Client, plugins map[string]Descriptor, devPlugin *DevPlu
} }
} }
if devPlugin != nil { for pName, desc := range localPlugins {
manifest, err := ReadManifest(devPlugin.GoPath, devPlugin.ModuleName) manifest, err := ReadManifest(localGoPath, desc.ModuleName)
if err != nil { 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 := interp.New(interp.Options{GoPath: localGoPath})
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)) _, err = i.Eval(fmt.Sprintf(`import "%s"`, manifest.Import))
if err != nil { 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 { switch manifest.Type {
case "middleware": case "middleware":
pb.middlewareDescriptors[devPluginName] = pluginContext{ pb.middlewareDescriptors[pName] = pluginContext{
interpreter: i, interpreter: i,
GoPath: devPlugin.GoPath, GoPath: localGoPath,
Import: manifest.Import, Import: manifest.Import,
BasePkg: manifest.BasePkg, BasePkg: manifest.BasePkg,
} }
case "provider": case "provider":
pb.providerDescriptors[devPluginName] = pluginContext{ pb.providerDescriptors[pName] = pluginContext{
interpreter: i, interpreter: i,
GoPath: devPlugin.GoPath, GoPath: localGoPath,
Import: manifest.Import, Import: manifest.Import,
BasePkg: manifest.BasePkg, BasePkg: manifest.BasePkg,
} }

View file

@ -6,12 +6,15 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/hashicorp/go-multierror"
"github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/log"
) )
// Setup setup plugins environment. const localGoPath = "./plugins-local/"
func Setup(client *Client, plugins map[string]Descriptor, devPlugin *DevPlugin) error {
err := checkPluginsConfiguration(plugins) // SetupRemotePlugins setup remote plugins environment.
func SetupRemotePlugins(client *Client, plugins map[string]Descriptor) error {
err := checkRemotePluginsConfiguration(plugins)
if err != nil { if err != nil {
return fmt.Errorf("invalid configuration: %w", err) 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 return nil
} }
func checkDevPluginConfiguration(plugin *DevPlugin) error { func checkRemotePluginsConfiguration(plugins map[string]Descriptor) 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 {
if plugins == nil { if plugins == nil {
return nil return nil
} }
@ -147,3 +95,74 @@ func checkPluginsConfiguration(plugins map[string]Descriptor) error {
return nil 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()
}

View file

@ -1,6 +1,6 @@
package plugins package plugins
// Descriptor The static part of a plugin configuration (prod). // Descriptor The static part of a plugin configuration.
type Descriptor struct { type Descriptor struct {
// ModuleName (required) // 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"`
@ -9,13 +9,10 @@ type Descriptor struct {
Version string `description:"plugin's version." json:"version,omitempty" toml:"version,omitempty" yaml:"version,omitempty" export:"true"` 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). // LocalDescriptor The static part of a local plugin configuration.
type DevPlugin struct { type LocalDescriptor struct {
// GoPath plugin's GOPATH. (required)
GoPath string `description:"plugin's GOPATH." json:"goPath,omitempty" toml:"goPath,omitempty" yaml:"goPath,omitempty" export:"true"`
// ModuleName (required) // 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. // Manifest The plugin manifest.