From 0186c31d59cc6ea7fd272a5ef81efb4668183854 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Mon, 20 Apr 2020 18:36:34 +0200 Subject: [PATCH] feat: plugins integration. --- .gitignore | 1 + .golangci.toml | 4 +- cmd/traefik/plugins.go | 42 ++ cmd/traefik/traefik.go | 20 +- .../reference/static-configuration/cli-ref.md | 18 + .../reference/static-configuration/env-ref.md | 18 + go.mod | 4 + go.sum | 6 + pkg/config/dynamic/middlewares.go | 2 + pkg/config/dynamic/plugins.go | 27 ++ pkg/config/dynamic/zz_generated.deepcopy.go | 7 + pkg/config/static/experimental.go | 16 + pkg/config/static/static_config.go | 10 - pkg/plugins/builder.go | 166 +++++++ pkg/plugins/client.go | 415 ++++++++++++++++++ pkg/plugins/plugins.go | 146 ++++++ pkg/plugins/types.go | 30 ++ pkg/provider/kubernetes/crd/kubernetes.go | 1 + .../crd/traefik/v1alpha1/middleware.go | 45 +- .../traefik/v1alpha1/zz_generated.deepcopy.go | 7 + pkg/server/middleware/middlewares.go | 27 +- pkg/server/middleware/middlewares_test.go | 8 +- pkg/server/middleware/plugins.go | 32 ++ pkg/server/router/router_test.go | 10 +- pkg/server/routerfactory.go | 8 +- pkg/server/routerfactory_test.go | 6 +- 26 files changed, 1025 insertions(+), 51 deletions(-) create mode 100644 cmd/traefik/plugins.go create mode 100644 pkg/config/dynamic/plugins.go create mode 100644 pkg/config/static/experimental.go create mode 100644 pkg/plugins/builder.go create mode 100644 pkg/plugins/client.go create mode 100644 pkg/plugins/plugins.go create mode 100644 pkg/plugins/types.go create mode 100644 pkg/server/middleware/plugins.go diff --git a/.gitignore b/.gitignore index 5a4cf35fb..425f92309 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ *.exe cover.out vendor/ +plugins-storage/ diff --git a/.golangci.toml b/.golangci.toml index e0f5ffbe0..14307c954 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -100,7 +100,7 @@ text = "string `traefik` has (\\d) occurrences, make it a constant" [[issues.exclude-rules]] path = "pkg/server/middleware/middlewares.go" - text = "Function 'buildConstructor' is too long \\(\\d+ > 230\\)" + text = "Function 'buildConstructor' has too many statements" [[issues.exclude-rules]] # FIXME must be fixed path = "cmd/context.go" text = "S1000: should use a simple channel send/receive instead of `select` with a single case" @@ -112,4 +112,4 @@ text = "printf-like formatting function 'SetErrorWithEvent' should be named 'SetErrorWithEventf'" [[issues.exclude-rules]] path = "pkg/log/deprecated.go" - linters = ["godot"] + linters = ["godot"] \ No newline at end of file diff --git a/cmd/traefik/plugins.go b/cmd/traefik/plugins.go new file mode 100644 index 000000000..0f0e80079 --- /dev/null +++ b/cmd/traefik/plugins.go @@ -0,0 +1,42 @@ +package main + +import ( + "github.com/containous/traefik/v2/pkg/config/static" + "github.com/containous/traefik/v2/pkg/plugins" +) + +const outputDir = "./plugins-storage/" + +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.Experimental.Pilot.Token, + } + + client, err := plugins.NewClient(opts) + 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 + } + + return client, staticCfg.Experimental.Plugins, staticCfg.Experimental.DevPlugin, nil +} + +func isPilotEnabled(staticCfg *static.Configuration) bool { + return staticCfg.Experimental != nil && + staticCfg.Experimental.Pilot != nil && + staticCfg.Experimental.Pilot.Token != "" +} + +func hasPlugins(staticCfg *static.Configuration) bool { + return staticCfg.Experimental != nil && + len(staticCfg.Experimental.Plugins) > 0 || staticCfg.Experimental.DevPlugin != nil +} diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index dcb4a394d..cb34c7fc5 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -24,6 +24,7 @@ import ( "github.com/containous/traefik/v2/pkg/metrics" "github.com/containous/traefik/v2/pkg/middlewares/accesslog" "github.com/containous/traefik/v2/pkg/pilot" + "github.com/containous/traefik/v2/pkg/plugins" "github.com/containous/traefik/v2/pkg/provider/acme" "github.com/containous/traefik/v2/pkg/provider/aggregator" "github.com/containous/traefik/v2/pkg/provider/traefik" @@ -119,6 +120,12 @@ func runCmd(staticConfiguration *static.Configuration) error { ctx := cmd.ContextWithSignal(context.Background()) + 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) } @@ -192,7 +199,18 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err accessLog := setupAccessLog(staticConfiguration.AccessLog) chainBuilder := middleware.NewChainBuilder(*staticConfiguration, metricsRegistry, accessLog) managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, metricsRegistry) - routerFactory := server.NewRouterFactory(*staticConfiguration, managerFactory, tlsManager, chainBuilder) + + client, plgs, devPlugin, err := initPlugins(staticConfiguration) + if err != nil { + return nil, err + } + + pluginBuilder, err := plugins.NewBuilder(client, plgs, devPlugin) + if err != nil { + return nil, err + } + + routerFactory := server.NewRouterFactory(*staticConfiguration, managerFactory, tlsManager, chainBuilder, pluginBuilder) var defaultEntryPoints []string for name, cfg := range staticConfiguration.EntryPoints { diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 5bfdc27d9..43b0bd461 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -159,6 +159,24 @@ ReadTimeout is the maximum duration for reading the entire request, including th `--entrypoints..transport.respondingtimeouts.writetimeout`: WriteTimeout is the maximum duration before timing out writes of the response. If zero, no timeout is set. (Default: ```0```) +`--experimental.devplugin.gopath`: +plugin's GOPATH. + +`--experimental.devplugin.modulename`: +plugin's module name. + +`--experimental.pilot.token`: +Traefik Pilot token. + +`--experimental.plugins.`: +Plugins configuration. (Default: ```false```) + +`--experimental.plugins..modulename`: +plugin's module name. + +`--experimental.plugins..version`: +plugin's version. + `--global.checknewversion`: Periodically check if a new version has been released. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index fe494c67a..7747b6482 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -159,6 +159,24 @@ ReadTimeout is the maximum duration for reading the entire request, including th `TRAEFIK_ENTRYPOINTS__TRANSPORT_RESPONDINGTIMEOUTS_WRITETIMEOUT`: WriteTimeout is the maximum duration before timing out writes of the response. If zero, no timeout is set. (Default: ```0```) +`TRAEFIK_EXPERIMENTAL_DEVPLUGIN_GOPATH`: +plugin's GOPATH. + +`TRAEFIK_EXPERIMENTAL_DEVPLUGIN_MODULENAME`: +plugin's module name. + +`TRAEFIK_EXPERIMENTAL_PILOT_TOKEN`: +Traefik Pilot token. + +`TRAEFIK_EXPERIMENTAL_PLUGINS_`: +Plugins configuration. (Default: ```false```) + +`TRAEFIK_EXPERIMENTAL_PLUGINS__MODULENAME`: +plugin's module name. + +`TRAEFIK_EXPERIMENTAL_PLUGINS__VERSION`: +plugin's version. + `TRAEFIK_GLOBAL_CHECKNEWVERSION`: Periodically check if a new version has been released. (Default: ```false```) diff --git a/go.mod b/go.mod index 762072196..95a23d7d9 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/cenkalti/backoff/v4 v4.0.0 github.com/containerd/containerd v1.3.2 // indirect github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd + github.com/containous/yaegi v0.8.13 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/davecgh/go-spew v1.1.1 github.com/docker/cli v0.0.0-20200221155518-740919cc7fc0 @@ -57,6 +58,7 @@ require ( github.com/miekg/dns v1.1.27 github.com/mitchellh/copystructure v1.0.0 github.com/mitchellh/hashstructure v1.0.0 + github.com/mitchellh/mapstructure v1.3.2 github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect @@ -83,6 +85,7 @@ require ( github.com/vulcand/predicate v1.1.0 go.elastic.co/apm v1.7.0 go.elastic.co/apm/module/apmot v1.7.0 + golang.org/x/mod v0.2.0 golang.org/x/net v0.0.0-20200301022130-244492dfa37a golang.org/x/time v0.0.0-20191024005414-555d28b269f0 google.golang.org/grpc v1.27.1 @@ -90,6 +93,7 @@ require ( gopkg.in/fsnotify.v1 v1.4.7 gopkg.in/jcmturner/goidentity.v3 v3.0.0 // indirect gopkg.in/yaml.v2 v2.2.8 + gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 k8s.io/api v0.18.2 k8s.io/apimachinery v0.18.2 k8s.io/client-go v0.18.2 diff --git a/go.sum b/go.sum index 3c773ca3d..2db321779 100644 --- a/go.sum +++ b/go.sum @@ -160,6 +160,8 @@ github.com/containous/multibuf v0.0.0-20190809014333-8b6c9a7e6bba h1:PhR03pep+5e github.com/containous/multibuf v0.0.0-20190809014333-8b6c9a7e6bba/go.mod h1:zkWcASFUJEst6QwCrxLdkuw1gvaKqmflEipm+iecV5M= github.com/containous/mux v0.0.0-20181024131434-c33f32e26898 h1:1srn9voikJGofblBhWy3WuZWqo14Ou7NaswNG/I2yWc= github.com/containous/mux v0.0.0-20181024131434-c33f32e26898/go.mod h1:z8WW7n06n8/1xF9Jl9WmuDeZuHAhfL+bwarNjsciwwg= +github.com/containous/yaegi v0.8.13 h1:ADhAZok9av2wxge4/LxNqZHG/+rwHZcwpSLKmy9cz48= +github.com/containous/yaegi v0.8.13/go.mod h1:Yj82MHpXQ9/h3ukzc2numJQ/Wr4+M3C9YLMzNjFtd3o= github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY= github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible h1:8F3hqu9fGYLBifCmRCJsicFqDx/D68Rt3q1JMazcgBQ= @@ -520,6 +522,8 @@ github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQz github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.1 h1:cCBH2gTD2K0OtLlv/Y5H01VQCqmlDxz30kS5Y5bqfLA= github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg= +github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1019,6 +1023,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index acdb8f3a9..18114cc8e 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -38,6 +38,8 @@ type Middleware struct { PassTLSClientCert *PassTLSClientCert `json:"passTLSClientCert,omitempty" toml:"passTLSClientCert,omitempty" yaml:"passTLSClientCert,omitempty"` Retry *Retry `json:"retry,omitempty" toml:"retry,omitempty" yaml:"retry,omitempty"` ContentType *ContentType `json:"contentType,omitempty" toml:"contentType,omitempty" yaml:"contentType,omitempty"` + + Plugin map[string]PluginConf `json:"plugin,omitempty" toml:"plugin,omitempty" yaml:"plugin,omitempty"` } // +k8s:deepcopy-gen=true diff --git a/pkg/config/dynamic/plugins.go b/pkg/config/dynamic/plugins.go new file mode 100644 index 000000000..a44236695 --- /dev/null +++ b/pkg/config/dynamic/plugins.go @@ -0,0 +1,27 @@ +package dynamic + +import "k8s.io/apimachinery/pkg/runtime" + +// +k8s:deepcopy-gen=false + +// PluginConf holds the plugin configuration. +type PluginConf map[string]interface{} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PluginConf) DeepCopyInto(out *PluginConf) { + if in == nil { + *out = nil + } else { + *out = runtime.DeepCopyJSON(*in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginConf. +func (in *PluginConf) DeepCopy() *PluginConf { + if in == nil { + return nil + } + out := new(PluginConf) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 33cbef1b1..1c5a7cb81 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -730,6 +730,13 @@ func (in *Middleware) DeepCopyInto(out *Middleware) { *out = new(ContentType) **out = **in } + if in.Plugin != nil { + in, out := &in.Plugin, &out.Plugin + *out = make(map[string]PluginConf, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } return } diff --git a/pkg/config/static/experimental.go b/pkg/config/static/experimental.go new file mode 100644 index 000000000..7b8794d52 --- /dev/null +++ b/pkg/config/static/experimental.go @@ -0,0 +1,16 @@ +package static + +import "github.com/containous/traefik/v2/pkg/plugins" + +// Experimental experimental Traefik features. +type Experimental struct { + Pilot *Pilot `description:"Traefik Pilot configuration." json:"pilot,omitempty" toml:"pilot,omitempty" yaml:"pilot,omitempty"` + + Plugins map[string]plugins.Descriptor `description:"Plugins configuration." json:"plugins,omitempty" toml:"plugins,omitempty" yaml:"plugins,omitempty"` + DevPlugin *plugins.DevPlugin `description:"Dev plugin configuration." json:"devPlugin,omitempty" toml:"devPlugin,omitempty" yaml:"devPlugin,omitempty"` +} + +// Pilot Configuration related to Traefik Pilot. +type Pilot struct { + Token string `description:"Traefik Pilot token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty"` +} diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index 9e035b602..cc2de50d2 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -74,16 +74,6 @@ type Configuration struct { Experimental *Experimental `description:"experimental features." json:"experimental,omitempty" toml:"experimental,omitempty" yaml:"experimental,omitempty"` } -// Experimental the experimental feature configuration. -type Experimental struct { - Pilot *PilotConfiguration `description:"Pilot configuration." json:"pilot,omitempty" toml:"pilot,omitempty" yaml:"pilot,omitempty" export:"true"` -} - -// PilotConfiguration holds pilot configuration. -type PilotConfiguration struct { - Token string `description:"Pilot token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" export:"true"` -} - // CertificateResolver contains the configuration for the different types of certificates resolver. type CertificateResolver struct { ACME *acmeprovider.Configuration `description:"Enable ACME (Let's Encrypt): automatic SSL." json:"acme,omitempty" toml:"acme,omitempty" yaml:"acme,omitempty" export:"true"` diff --git a/pkg/plugins/builder.go b/pkg/plugins/builder.go new file mode 100644 index 000000000..448250568 --- /dev/null +++ b/pkg/plugins/builder.go @@ -0,0 +1,166 @@ +package plugins + +import ( + "context" + "fmt" + "net/http" + "path" + "reflect" + "strings" + + "github.com/containous/yaegi/interp" + "github.com/containous/yaegi/stdlib" + "github.com/mitchellh/mapstructure" +) + +const devPluginName = "dev" + +// pluginContext The static part of a plugin configuration. +type pluginContext struct { + // GoPath plugin's GOPATH + GoPath string `json:"goPath,omitempty" toml:"goPath,omitempty" yaml:"goPath,omitempty"` + + // Import plugin's import/package + Import string `json:"import,omitempty" toml:"import,omitempty" yaml:"import,omitempty"` + + // BasePkg plugin's base package name (optional) + BasePkg string `json:"basePkg,omitempty" toml:"basePkg,omitempty" yaml:"basePkg,omitempty"` + + interpreter *interp.Interpreter +} + +// Builder is a plugin builder. +type Builder struct { + descriptors map[string]pluginContext +} + +// NewBuilder creates a new Builder. +func NewBuilder(client *Client, plugins map[string]Descriptor, devPlugin *DevPlugin) (*Builder, error) { + pb := &Builder{ + descriptors: map[string]pluginContext{}, + } + + for pName, desc := range plugins { + manifest, err := client.ReadManifest(desc.ModuleName) + if err != nil { + _ = client.ResetAll() + return nil, fmt.Errorf("%s: failed to read manifest: %w", desc.ModuleName, err) + } + + i := interp.New(interp.Options{GoPath: client.GoPath()}) + i.Use(stdlib.Symbols) + + _, err = i.Eval(fmt.Sprintf(`import "%s"`, manifest.Import)) + if err != nil { + return nil, fmt.Errorf("%s: failed to import plugin code %q: %w", desc.ModuleName, manifest.Import, err) + } + + pb.descriptors[pName] = pluginContext{ + interpreter: i, + GoPath: client.GoPath(), + Import: manifest.Import, + BasePkg: manifest.BasePkg, + } + } + + if devPlugin != nil { + manifest, err := ReadManifest(devPlugin.GoPath, devPlugin.ModuleName) + if err != nil { + return nil, fmt.Errorf("%s: failed to read manifest: %w", devPlugin.ModuleName, err) + } + + i := interp.New(interp.Options{GoPath: devPlugin.GoPath}) + i.Use(stdlib.Symbols) + + _, 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) + } + + pb.descriptors[devPluginName] = pluginContext{ + interpreter: i, + GoPath: devPlugin.GoPath, + Import: manifest.Import, + BasePkg: manifest.BasePkg, + } + } + + return pb, nil +} + +// Build builds a plugin. +func (b Builder) Build(pName string, config map[string]interface{}, middlewareName string) (*Middleware, error) { + if b.descriptors == nil { + return nil, fmt.Errorf("plugin: no plugin definition in the static configuration: %s", pName) + } + + descriptor, ok := b.descriptors[pName] + if !ok { + return nil, fmt.Errorf("plugin: unknown plugin type: %s", pName) + } + + return newMiddleware(descriptor, config, middlewareName) +} + +// Middleware is a HTTP handler plugin wrapper. +type Middleware struct { + middlewareName string + fnNew reflect.Value + config reflect.Value +} + +func newMiddleware(descriptor pluginContext, config map[string]interface{}, middlewareName string) (*Middleware, error) { + basePkg := descriptor.BasePkg + if basePkg == "" { + basePkg = strings.ReplaceAll(path.Base(descriptor.Import), "-", "_") + } + + vConfig, err := descriptor.interpreter.Eval(basePkg + `.CreateConfig()`) + if err != nil { + return nil, fmt.Errorf("plugin: failed to eval CreateConfig: %w", err) + } + + cfg := &mapstructure.DecoderConfig{ + DecodeHook: mapstructure.StringToSliceHookFunc(","), + WeaklyTypedInput: true, + Result: vConfig.Interface(), + } + + decoder, err := mapstructure.NewDecoder(cfg) + if err != nil { + return nil, fmt.Errorf("plugin: failed to create configuration decoder: %w", err) + } + + err = decoder.Decode(config) + if err != nil { + return nil, fmt.Errorf("plugin: failed to decode configuration: %w", err) + } + + fnNew, err := descriptor.interpreter.Eval(basePkg + `.New`) + if err != nil { + return nil, fmt.Errorf("plugin: failed to eval New: %w", err) + } + + return &Middleware{ + middlewareName: middlewareName, + fnNew: fnNew, + config: vConfig, + }, nil +} + +// NewHandler creates a new HTTP handler. +func (m *Middleware) NewHandler(ctx context.Context, next http.Handler) (http.Handler, error) { + args := []reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(next), m.config, reflect.ValueOf(m.middlewareName)} + results := m.fnNew.Call(args) + + if len(results) > 1 && results[1].Interface() != nil { + return nil, results[1].Interface().(error) + } + + handler, ok := results[0].Interface().(http.Handler) + if !ok { + return nil, fmt.Errorf("plugin: invalid handler type: %T", results[0].Interface()) + } + + return handler, nil +} diff --git a/pkg/plugins/client.go b/pkg/plugins/client.go new file mode 100644 index 000000000..da9885492 --- /dev/null +++ b/pkg/plugins/client.go @@ -0,0 +1,415 @@ +package plugins + +import ( + zipa "archive/zip" + "context" + "crypto/sha256" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strings" + "time" + + "golang.org/x/mod/module" + "golang.org/x/mod/zip" + "gopkg.in/yaml.v3" +) + +const ( + sourcesFolder = "sources" + archivesFolder = "archives" + stateFilename = "state.json" + goPathSrc = "src" + pluginManifest = ".traefik.yml" +) + +const pilotURL = "https://plugin.pilot.traefik.io/public/" + +const ( + hashHeader = "X-Plugin-Hash" + tokenHeader = "X-Token" +) + +// ClientOptions the options of a Traefik Pilot client. +type ClientOptions struct { + Output string + Token string +} + +// Client a Traefik Pilot client. +type Client struct { + HTTPClient *http.Client + baseURL *url.URL + + token string + archives string + stateFile string + goPath string + sources string +} + +// NewClient creates a new Traefik Pilot client. +func NewClient(opts ClientOptions) (*Client, error) { + baseURL, err := url.Parse(pilotURL) + if err != nil { + return nil, err + } + + sourcesRootPath := filepath.Join(filepath.FromSlash(opts.Output), sourcesFolder) + err = resetDirectory(sourcesRootPath) + if err != nil { + return nil, err + } + + goPath, err := ioutil.TempDir(sourcesRootPath, "gop-*") + if err != nil { + return nil, fmt.Errorf("failed to create GoPath: %w", err) + } + + archivesPath := filepath.Join(filepath.FromSlash(opts.Output), archivesFolder) + err = os.MkdirAll(archivesPath, 0o755) + if err != nil { + return nil, fmt.Errorf("failed to create archives directory %s: %w", archivesPath, err) + } + + return &Client{ + HTTPClient: &http.Client{Timeout: 5 * time.Second}, + baseURL: baseURL, + + archives: archivesPath, + stateFile: filepath.Join(archivesPath, stateFilename), + + goPath: goPath, + sources: filepath.Join(goPath, goPathSrc), + + token: opts.Token, + }, nil +} + +// GoPath gets the plugins GoPath. +func (c *Client) GoPath() string { + return c.goPath +} + +// ReadManifest reads a plugin manifest. +func (c *Client) ReadManifest(moduleName string) (*Manifest, error) { + return ReadManifest(c.goPath, moduleName) +} + +// ReadManifest reads a plugin manifest. +func ReadManifest(goPath, moduleName string) (*Manifest, error) { + p := filepath.Join(goPath, goPathSrc, filepath.FromSlash(moduleName), pluginManifest) + + file, err := os.Open(p) + if err != nil { + return nil, fmt.Errorf("failed to open the plugin manifest %s: %w", p, err) + } + + defer func() { _ = file.Close() }() + + m := &Manifest{} + err = yaml.NewDecoder(file).Decode(m) + if err != nil { + return nil, fmt.Errorf("failed to decode the plugin manifest %s: %w", p, err) + } + + return m, nil +} + +// Download downloads a plugin archive. +func (c *Client) Download(ctx context.Context, pName, pVersion string) (string, error) { + filename := c.buildArchivePath(pName, pVersion) + + var hash string + _, err := os.Stat(filename) + if err != nil && !os.IsNotExist(err) { + return "", fmt.Errorf("failed to read archive %s: %w", filename, err) + } + + if err == nil { + hash, err = computeHash(filename) + if err != nil { + return "", fmt.Errorf("failed to compute hash: %w", err) + } + } + + endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "download", pName, pVersion)) + if err != nil { + return "", fmt.Errorf("failed to parse endpoint URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + if hash != "" { + req.Header.Set(hashHeader, hash) + } + + if c.token != "" { + req.Header.Set(tokenHeader, c.token) + } + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return "", fmt.Errorf("failed to call service: %w", err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode == http.StatusOK { + err = os.MkdirAll(filepath.Dir(filename), 0o755) + if err != nil { + return "", fmt.Errorf("failed to create directory: %w", err) + } + + var file *os.File + file, err = os.Create(filename) + if err != nil { + return "", fmt.Errorf("failed to create file %q: %w", filename, err) + } + + defer func() { _ = file.Close() }() + + _, err = io.Copy(file, resp.Body) + if err != nil { + return "", fmt.Errorf("failed to write response: %w", err) + } + + hash, err = computeHash(filename) + if err != nil { + return "", fmt.Errorf("failed to compute hash: %w", err) + } + + return hash, nil + } + + if resp.StatusCode == http.StatusNotModified { + // noop + return hash, nil + } + + data, _ := ioutil.ReadAll(resp.Body) + return "", fmt.Errorf("error: %d: %s", resp.StatusCode, string(data)) +} + +// Check checks the plugin archive integrity. +func (c *Client) Check(ctx context.Context, pName, pVersion, hash string) error { + endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "validate", pName, pVersion)) + if err != nil { + return fmt.Errorf("failed to parse endpoint URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + if hash != "" { + req.Header.Set(hashHeader, hash) + } + + if c.token != "" { + req.Header.Set(tokenHeader, c.token) + } + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return fmt.Errorf("failed to call service: %w", err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode == http.StatusOK { + return nil + } + + return fmt.Errorf("plugin integrity check failed") +} + +// Unzip unzip a plugin archive. +func (c *Client) Unzip(pName, pVersion string) error { + err := c.unzipModule(pName, pVersion) + if err == nil { + return nil + } + + return c.unzipArchive(pName, pVersion) +} + +func (c *Client) unzipModule(pName, pVersion string) error { + src := c.buildArchivePath(pName, pVersion) + dest := filepath.Join(c.sources, filepath.FromSlash(pName)) + + return zip.Unzip(dest, module.Version{Path: pName, Version: pVersion}, src) +} + +func (c *Client) unzipArchive(pName, pVersion string) error { + zipPath := c.buildArchivePath(pName, pVersion) + + archive, err := zipa.OpenReader(zipPath) + if err != nil { + return err + } + + defer func() { _ = archive.Close() }() + + dest := filepath.Join(c.sources, filepath.FromSlash(pName)) + + for _, f := range archive.File { + err = unzipFile(f, dest) + if err != nil { + return err + } + } + + return nil +} + +func unzipFile(f *zipa.File, dest string) error { + rc, err := f.Open() + if err != nil { + return err + } + + defer func() { _ = rc.Close() }() + + pathParts := strings.SplitN(f.Name, string(os.PathSeparator), 2) + p := filepath.Join(dest, pathParts[1]) + + if f.FileInfo().IsDir() { + return os.MkdirAll(p, f.Mode()) + } + + err = os.MkdirAll(filepath.Dir(p), 0o750) + if err != nil { + return err + } + + elt, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + + defer func() { _ = elt.Close() }() + + _, err = io.Copy(elt, rc) + if err != nil { + return err + } + + return nil +} + +// CleanArchives cleans plugins archives. +func (c *Client) CleanArchives(plugins map[string]Descriptor) error { + if _, err := os.Stat(c.stateFile); os.IsNotExist(err) { + return nil + } + + stateFile, err := os.Open(c.stateFile) + if err != nil { + return fmt.Errorf("failed to open state file %s: %w", c.stateFile, err) + } + + previous := make(map[string]string) + err = json.NewDecoder(stateFile).Decode(&previous) + if err != nil { + return fmt.Errorf("failed to decode state file %s: %w", c.stateFile, err) + } + + for pName, pVersion := range previous { + for _, desc := range plugins { + if desc.ModuleName == pName && desc.Version != pVersion { + archivePath := c.buildArchivePath(pName, pVersion) + if err = os.RemoveAll(archivePath); err != nil { + return fmt.Errorf("failed to remove archive %s: %w", archivePath, err) + } + } + } + } + + return nil +} + +// WriteState writes the plugins state files. +func (c *Client) WriteState(plugins map[string]Descriptor) error { + m := make(map[string]string) + + for _, descriptor := range plugins { + m[descriptor.ModuleName] = descriptor.Version + } + + mp, err := json.MarshalIndent(m, "", " ") + if err != nil { + return err + } + + return ioutil.WriteFile(c.stateFile, mp, 0o600) +} + +// ResetAll resets all plugins related directories. +func (c *Client) ResetAll() error { + if c.goPath == "" { + return errors.New("goPath is empty") + } + + err := resetDirectory(filepath.Join(c.goPath, "..")) + if err != nil { + return err + } + + return resetDirectory(c.archives) +} + +func (c *Client) buildArchivePath(pName, pVersion string) string { + return filepath.Join(c.archives, filepath.FromSlash(pName), pVersion+".zip") +} + +func resetDirectory(dir string) error { + dirPath, err := filepath.Abs(dir) + if err != nil { + return err + } + + currentPath, err := os.Getwd() + if err != nil { + return err + } + + if strings.HasPrefix(currentPath, dirPath) { + return fmt.Errorf("cannot be deleted: the directory path %s is the parent of the current path %s", dirPath, currentPath) + } + + err = os.RemoveAll(dir) + if err != nil { + return err + } + + return os.MkdirAll(dir, 0o755) +} + +func computeHash(filepath string) (string, error) { + file, err := os.Open(filepath) + if err != nil { + return "", err + } + + hash := sha256.New() + + if _, err := io.Copy(hash, file); err != nil { + return "", err + } + + sum := hash.Sum(nil) + + return fmt.Sprintf("%x", sum), nil +} diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go new file mode 100644 index 000000000..448747113 --- /dev/null +++ b/pkg/plugins/plugins.go @@ -0,0 +1,146 @@ +package plugins + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/containous/traefik/v2/pkg/log" +) + +// Setup setup plugins environment. +func Setup(client *Client, plugins map[string]Descriptor, devPlugin *DevPlugin) error { + err := checkPluginsConfiguration(plugins) + if err != nil { + return fmt.Errorf("invalid configuration: %w", err) + } + + err = client.CleanArchives(plugins) + if err != nil { + return fmt.Errorf("failed to clean archives: %w", err) + } + + ctx := context.Background() + + for pAlias, desc := range plugins { + log.FromContext(ctx).Debugf("loading of plugin: %s: %s@%s", pAlias, desc.ModuleName, desc.Version) + + hash, err := client.Download(ctx, desc.ModuleName, desc.Version) + if err != nil { + _ = client.ResetAll() + return fmt.Errorf("failed to download plugin %s: %w", desc.ModuleName, err) + } + + err = client.Check(ctx, desc.ModuleName, desc.Version, hash) + if err != nil { + _ = client.ResetAll() + return fmt.Errorf("failed to check archive integrity of the plugin %s: %w", desc.ModuleName, err) + } + } + + err = client.WriteState(plugins) + if err != nil { + _ = client.ResetAll() + return fmt.Errorf("failed to write plugins state: %w", err) + } + + for _, desc := range plugins { + err = client.Unzip(desc.ModuleName, desc.Version) + if err != nil { + _ = client.ResetAll() + return fmt.Errorf("failed to unzip archive: %w", err) + } + } + + 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 + } + + if m.Type != "middleware" { + 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 { + return nil + } + + uniq := make(map[string]struct{}) + + var errs []string + for pAlias, descriptor := range plugins { + if descriptor.ModuleName == "" { + errs = append(errs, fmt.Sprintf("%s: plugin name is missing", pAlias)) + } + + if descriptor.Version == "" { + errs = append(errs, fmt.Sprintf("%s: plugin version is missing", pAlias)) + } + + if strings.HasPrefix(descriptor.ModuleName, "/") || strings.HasSuffix(descriptor.ModuleName, "/") { + errs = append(errs, fmt.Sprintf("%s: plugin name should not start or end with a /", pAlias)) + continue + } + + if _, ok := uniq[descriptor.ModuleName]; ok { + errs = append(errs, fmt.Sprintf("only one version of a plugin is allowed, there is a duplicate of %s", descriptor.ModuleName)) + continue + } + + uniq[descriptor.ModuleName] = struct{}{} + } + + if len(errs) > 0 { + return errors.New(strings.Join(errs, ": ")) + } + + return nil +} diff --git a/pkg/plugins/types.go b/pkg/plugins/types.go new file mode 100644 index 000000000..1737765a9 --- /dev/null +++ b/pkg/plugins/types.go @@ -0,0 +1,30 @@ +package plugins + +// Descriptor The static part of a plugin configuration (prod). +type Descriptor struct { + // ModuleName (required) + ModuleName string `description:"plugin's module name." json:"moduleName,omitempty" toml:"moduleName,omitempty" yaml:"moduleName,omitempty"` + + // Version (required) + Version string `description:"plugin's version." json:"version,omitempty" toml:"version,omitempty" yaml:"version,omitempty"` +} + +// 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"` + + // ModuleName (required) + ModuleName string `description:"plugin's module name." json:"moduleName,omitempty" toml:"moduleName,omitempty" yaml:"moduleName,omitempty"` +} + +// Manifest The plugin manifest. +type Manifest struct { + DisplayName string `yaml:"displayName"` + Type string `yaml:"type"` + Import string `yaml:"import"` + BasePkg string `yaml:"basePkg"` + Compatibility string `yaml:"compatibility"` + Summary string `yaml:"summary"` + TestData map[string]interface{} `yaml:"testData"` +} diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 7a8349dd6..51880b864 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -232,6 +232,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) PassTLSClientCert: middleware.Spec.PassTLSClientCert, Retry: middleware.Spec.Retry, ContentType: middleware.Spec.ContentType, + Plugin: middleware.Spec.Plugin, } } diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/middleware.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/middleware.go index 437546bdb..a76568703 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/middleware.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/middleware.go @@ -20,28 +20,29 @@ type Middleware struct { // MiddlewareSpec holds the Middleware configuration. type MiddlewareSpec struct { - AddPrefix *dynamic.AddPrefix `json:"addPrefix,omitempty"` - StripPrefix *dynamic.StripPrefix `json:"stripPrefix,omitempty"` - StripPrefixRegex *dynamic.StripPrefixRegex `json:"stripPrefixRegex,omitempty"` - ReplacePath *dynamic.ReplacePath `json:"replacePath,omitempty"` - ReplacePathRegex *dynamic.ReplacePathRegex `json:"replacePathRegex,omitempty"` - Chain *Chain `json:"chain,omitempty"` - IPWhiteList *dynamic.IPWhiteList `json:"ipWhiteList,omitempty"` - Headers *dynamic.Headers `json:"headers,omitempty"` - Errors *ErrorPage `json:"errors,omitempty"` - RateLimit *dynamic.RateLimit `json:"rateLimit,omitempty"` - RedirectRegex *dynamic.RedirectRegex `json:"redirectRegex,omitempty"` - RedirectScheme *dynamic.RedirectScheme `json:"redirectScheme,omitempty"` - BasicAuth *BasicAuth `json:"basicAuth,omitempty"` - DigestAuth *DigestAuth `json:"digestAuth,omitempty"` - ForwardAuth *ForwardAuth `json:"forwardAuth,omitempty"` - InFlightReq *dynamic.InFlightReq `json:"inFlightReq,omitempty"` - Buffering *dynamic.Buffering `json:"buffering,omitempty"` - CircuitBreaker *dynamic.CircuitBreaker `json:"circuitBreaker,omitempty"` - Compress *dynamic.Compress `json:"compress,omitempty"` - PassTLSClientCert *dynamic.PassTLSClientCert `json:"passTLSClientCert,omitempty"` - Retry *dynamic.Retry `json:"retry,omitempty"` - ContentType *dynamic.ContentType `json:"contentType,omitempty"` + AddPrefix *dynamic.AddPrefix `json:"addPrefix,omitempty"` + StripPrefix *dynamic.StripPrefix `json:"stripPrefix,omitempty"` + StripPrefixRegex *dynamic.StripPrefixRegex `json:"stripPrefixRegex,omitempty"` + ReplacePath *dynamic.ReplacePath `json:"replacePath,omitempty"` + ReplacePathRegex *dynamic.ReplacePathRegex `json:"replacePathRegex,omitempty"` + Chain *Chain `json:"chain,omitempty"` + IPWhiteList *dynamic.IPWhiteList `json:"ipWhiteList,omitempty"` + Headers *dynamic.Headers `json:"headers,omitempty"` + Errors *ErrorPage `json:"errors,omitempty"` + RateLimit *dynamic.RateLimit `json:"rateLimit,omitempty"` + RedirectRegex *dynamic.RedirectRegex `json:"redirectRegex,omitempty"` + RedirectScheme *dynamic.RedirectScheme `json:"redirectScheme,omitempty"` + BasicAuth *BasicAuth `json:"basicAuth,omitempty"` + DigestAuth *DigestAuth `json:"digestAuth,omitempty"` + ForwardAuth *ForwardAuth `json:"forwardAuth,omitempty"` + InFlightReq *dynamic.InFlightReq `json:"inFlightReq,omitempty"` + Buffering *dynamic.Buffering `json:"buffering,omitempty"` + CircuitBreaker *dynamic.CircuitBreaker `json:"circuitBreaker,omitempty"` + Compress *dynamic.Compress `json:"compress,omitempty"` + PassTLSClientCert *dynamic.PassTLSClientCert `json:"passTLSClientCert,omitempty"` + Retry *dynamic.Retry `json:"retry,omitempty"` + ContentType *dynamic.ContentType `json:"contentType,omitempty"` + Plugin map[string]dynamic.PluginConf `json:"plugin,omitempty"` } // +k8s:deepcopy-gen=true diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go index d7ac29e4f..6b580ac39 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go @@ -687,6 +687,13 @@ func (in *MiddlewareSpec) DeepCopyInto(out *MiddlewareSpec) { *out = new(dynamic.ContentType) **out = **in } + if in.Plugin != nil { + in, out := &in.Plugin, &out.Plugin + *out = make(map[string]dynamic.PluginConf, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } return } diff --git a/pkg/server/middleware/middlewares.go b/pkg/server/middleware/middlewares.go index c1398d831..69c8b00e2 100644 --- a/pkg/server/middleware/middlewares.go +++ b/pkg/server/middleware/middlewares.go @@ -28,6 +28,7 @@ import ( "github.com/containous/traefik/v2/pkg/middlewares/stripprefix" "github.com/containous/traefik/v2/pkg/middlewares/stripprefixregex" "github.com/containous/traefik/v2/pkg/middlewares/tracing" + "github.com/containous/traefik/v2/pkg/plugins" "github.com/containous/traefik/v2/pkg/server/provider" ) @@ -40,6 +41,7 @@ const ( // Builder the middleware builder. type Builder struct { configs map[string]*runtime.MiddlewareInfo + pluginBuilder *plugins.Builder serviceBuilder serviceBuilder } @@ -48,8 +50,8 @@ type serviceBuilder interface { } // NewBuilder creates a new Builder. -func NewBuilder(configs map[string]*runtime.MiddlewareInfo, serviceBuilder serviceBuilder) *Builder { - return &Builder{configs: configs, serviceBuilder: serviceBuilder} +func NewBuilder(configs map[string]*runtime.MiddlewareInfo, serviceBuilder serviceBuilder, pluginBuilder *plugins.Builder) *Builder { + return &Builder{configs: configs, serviceBuilder: serviceBuilder, pluginBuilder: pluginBuilder} } // BuildChain creates a middleware chain. @@ -338,6 +340,27 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) ( } } + // Plugin + if config.Plugin != nil { + if middleware != nil { + return nil, badConf + } + + pluginType, rawPluginConfig, err := findPluginConfig(config.Plugin) + if err != nil { + return nil, err + } + + m, err := b.pluginBuilder.Build(pluginType, rawPluginConfig, middlewareName) + if err != nil { + return nil, err + } + + middleware = func(next http.Handler) (http.Handler, error) { + return m.NewHandler(ctx, next) + } + } + if middleware == nil { return nil, fmt.Errorf("invalid middleware %q configuration: invalid middleware type or middleware does not exist", middlewareName) } diff --git a/pkg/server/middleware/middlewares_test.go b/pkg/server/middleware/middlewares_test.go index e0749095e..5acee8396 100644 --- a/pkg/server/middleware/middlewares_test.go +++ b/pkg/server/middleware/middlewares_test.go @@ -18,7 +18,7 @@ func TestBuilder_BuildChainNilConfig(t *testing.T) { testConfig := map[string]*runtime.MiddlewareInfo{ "empty": {}, } - middlewaresBuilder := NewBuilder(testConfig, nil) + middlewaresBuilder := NewBuilder(testConfig, nil, nil) chain := middlewaresBuilder.BuildChain(context.Background(), []string{"empty"}) _, err := chain.Then(nil) @@ -29,7 +29,7 @@ func TestBuilder_BuildChainNonExistentChain(t *testing.T) { testConfig := map[string]*runtime.MiddlewareInfo{ "foobar": {}, } - middlewaresBuilder := NewBuilder(testConfig, nil) + middlewaresBuilder := NewBuilder(testConfig, nil, nil) chain := middlewaresBuilder.BuildChain(context.Background(), []string{"empty"}) _, err := chain.Then(nil) @@ -270,7 +270,7 @@ func TestBuilder_BuildChainWithContext(t *testing.T) { Middlewares: test.configuration, }, }) - builder := NewBuilder(rtConf.Middlewares, nil) + builder := NewBuilder(rtConf.Middlewares, nil, nil) result := builder.BuildChain(ctx, test.buildChain) @@ -329,7 +329,7 @@ func TestBuilder_buildConstructor(t *testing.T) { Middlewares: testConfig, }, }) - middlewaresBuilder := NewBuilder(rtConf.Middlewares, nil) + middlewaresBuilder := NewBuilder(rtConf.Middlewares, nil, nil) testCases := []struct { desc string diff --git a/pkg/server/middleware/plugins.go b/pkg/server/middleware/plugins.go new file mode 100644 index 000000000..80b2b4069 --- /dev/null +++ b/pkg/server/middleware/plugins.go @@ -0,0 +1,32 @@ +package middleware + +import ( + "errors" + "fmt" + + "github.com/containous/traefik/v2/pkg/config/dynamic" +) + +func findPluginConfig(rawConfig map[string]dynamic.PluginConf) (string, map[string]interface{}, error) { + if len(rawConfig) != 1 { + return "", nil, errors.New("plugin: invalid configuration: no configuration or too many plugin definition") + } + + var pluginType string + var rawPluginConfig map[string]interface{} + + for pType, pConfig := range rawConfig { + pluginType = pType + rawPluginConfig = pConfig + } + + if pluginType == "" { + return "", nil, errors.New("plugin: missing plugin type") + } + + if len(rawPluginConfig) == 0 { + return "", nil, fmt.Errorf("plugin: missing plugin configuration: %s", pluginType) + } + + return pluginType, rawPluginConfig, nil +} diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index 113335086..a5af6e66c 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -289,7 +289,7 @@ func TestRouterManager_Get(t *testing.T) { }) serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil, nil) - middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager) + middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil) responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares) chainBuilder := middleware.NewChainBuilder(static.Configuration{}, nil, nil) @@ -394,7 +394,7 @@ func TestAccessLog(t *testing.T) { }) serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil, nil) - middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager) + middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil) responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares) chainBuilder := middleware.NewChainBuilder(static.Configuration{}, nil, nil) @@ -682,7 +682,7 @@ func TestRuntimeConfiguration(t *testing.T) { }) serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil, nil) - middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager) + middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil) responseModifierFactory := responsemodifiers.NewBuilder(map[string]*runtime.MiddlewareInfo{}) chainBuilder := middleware.NewChainBuilder(static.Configuration{}, nil, nil) @@ -764,7 +764,7 @@ func TestProviderOnMiddlewares(t *testing.T) { }) serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil, nil) - middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager) + middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil) responseModifierFactory := responsemodifiers.NewBuilder(map[string]*runtime.MiddlewareInfo{}) chainBuilder := middleware.NewChainBuilder(staticCfg, nil, nil) @@ -825,7 +825,7 @@ func BenchmarkRouterServe(b *testing.B) { }) serviceManager := service.NewManager(rtConf.Services, &staticTransport{res}, nil, nil) - middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager) + middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil) responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares) chainBuilder := middleware.NewChainBuilder(static.Configuration{}, nil, nil) diff --git a/pkg/server/routerfactory.go b/pkg/server/routerfactory.go index 02ef786ca..eb76b1190 100644 --- a/pkg/server/routerfactory.go +++ b/pkg/server/routerfactory.go @@ -6,6 +6,7 @@ import ( "github.com/containous/traefik/v2/pkg/config/runtime" "github.com/containous/traefik/v2/pkg/config/static" "github.com/containous/traefik/v2/pkg/log" + "github.com/containous/traefik/v2/pkg/plugins" "github.com/containous/traefik/v2/pkg/responsemodifiers" "github.com/containous/traefik/v2/pkg/server/middleware" "github.com/containous/traefik/v2/pkg/server/router" @@ -26,12 +27,14 @@ type RouterFactory struct { managerFactory *service.ManagerFactory + pluginBuilder *plugins.Builder + chainBuilder *middleware.ChainBuilder tlsManager *tls.Manager } // NewRouterFactory creates a new RouterFactory. -func NewRouterFactory(staticConfiguration static.Configuration, managerFactory *service.ManagerFactory, tlsManager *tls.Manager, chainBuilder *middleware.ChainBuilder) *RouterFactory { +func NewRouterFactory(staticConfiguration static.Configuration, managerFactory *service.ManagerFactory, tlsManager *tls.Manager, chainBuilder *middleware.ChainBuilder, pluginBuilder *plugins.Builder) *RouterFactory { var entryPointsTCP, entryPointsUDP []string for name, cfg := range staticConfiguration.EntryPoints { protocol, err := cfg.GetProtocol() @@ -53,6 +56,7 @@ func NewRouterFactory(staticConfiguration static.Configuration, managerFactory * managerFactory: managerFactory, tlsManager: tlsManager, chainBuilder: chainBuilder, + pluginBuilder: pluginBuilder, } } @@ -63,7 +67,7 @@ func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string // HTTP serviceManager := f.managerFactory.Build(rtConf) - middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager) + middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, f.pluginBuilder) responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares) routerManager := router.NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory, f.chainBuilder) diff --git a/pkg/server/routerfactory_test.go b/pkg/server/routerfactory_test.go index 9b96f66c5..c5038e548 100644 --- a/pkg/server/routerfactory_test.go +++ b/pkg/server/routerfactory_test.go @@ -51,7 +51,7 @@ func TestReuseService(t *testing.T) { managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry()) tlsManager := tls.NewManager() - factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil)) + factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil), nil) entryPointsHandlers, _ := factory.CreateRouters(runtime.NewConfig(dynamic.Configuration{HTTP: dynamicConfigs})) @@ -185,7 +185,7 @@ func TestServerResponseEmptyBackend(t *testing.T) { managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry()) tlsManager := tls.NewManager() - factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil)) + factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil), nil) entryPointsHandlers, _ := factory.CreateRouters(runtime.NewConfig(dynamic.Configuration{HTTP: test.config(testServer.URL)})) @@ -224,7 +224,7 @@ func TestInternalServices(t *testing.T) { managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry()) tlsManager := tls.NewManager() - factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil)) + factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil), nil) entryPointsHandlers, _ := factory.CreateRouters(runtime.NewConfig(dynamic.Configuration{HTTP: dynamicConfigs}))