Local private plugins.
Co-authored-by: Julien Salleyron <julien.salleyron@gmail.com>
This commit is contained in:
parent
a243ac4dde
commit
5e3e47b484
11 changed files with 196 additions and 123 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -17,4 +17,5 @@
|
|||
cover.out
|
||||
vendor/
|
||||
plugins-storage/
|
||||
plugins-local/
|
||||
traefik_changelog.md
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
2
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -456,9 +456,13 @@
|
|||
"version": "foobar"
|
||||
}
|
||||
},
|
||||
"devPlugin": {
|
||||
"goPath": "foobar",
|
||||
"localPlugins": {
|
||||
"Descriptor0": {
|
||||
"moduleName": "foobar"
|
||||
},
|
||||
"Descriptor1": {
|
||||
"moduleName": "foobar"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue