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
vendor/
plugins-storage/
plugins-local/
traefik_changelog.md

View file

@ -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
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
}
var client *plugins.Client
plgs := map[string]plugins.Descriptor{}
if isPilotEnabled(staticCfg) && hasPlugins(staticCfg) {
opts := plugins.ClientOptions{
Output: outputDir,
Token: staticCfg.Pilot.Token,
}
client, err := plugins.NewClient(opts)
var err error
client, err = plugins.NewClient(opts)
if err != nil {
return nil, nil, nil, err
}
err = plugins.Setup(client, staticCfg.Experimental.Plugins, staticCfg.Experimental.DevPlugin)
err = plugins.SetupRemotePlugins(client, staticCfg.Experimental.Plugins)
if err != nil {
return nil, nil, nil, err
}
return client, staticCfg.Experimental.Plugins, staticCfg.Experimental.DevPlugin, nil
plgs = staticCfg.Experimental.Plugins
}
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
}

View file

@ -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)
}

2
go.mod
View file

@ -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

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/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=

View file

@ -958,10 +958,14 @@ func TestDo_staticConfiguration(t *testing.T) {
Version: "foobar",
},
},
DevPlugin: &plugins.DevPlugin{
GoPath: "foobar",
LocalPlugins: map[string]plugins.LocalDescriptor{
"Descriptor0": {
ModuleName: "foobar",
},
"Descriptor1": {
ModuleName: "foobar",
},
},
}
expectedConfiguration, err := os.ReadFile("./testdata/anonymized-static-config.json")

View file

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

View file

@ -5,7 +5,8 @@ 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"`
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"`
}

View file

@ -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,
}

View file

@ -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()
}

View file

@ -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,11 +9,8 @@ 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"`
}