Add plugin's support for provider

Co-authored-by: Julien Salleyron <julien@traefik.io>
This commit is contained in:
Ludovic Fernandez 2021-05-11 16:14:10 +02:00 committed by GitHub
parent de2437cfec
commit 63ef0f1cee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 928 additions and 116 deletions

View file

@ -123,7 +123,7 @@ shell: build-dev-image
docs: docs:
make -C ./docs docs make -C ./docs docs
## Serve the documentation site localy ## Serve the documentation site locally
docs-serve: docs-serve:
make -C ./docs docs-serve make -C ./docs docs-serve
@ -135,6 +135,10 @@ docs-pull-images:
generate-crd: generate-crd:
./script/update-generated-crd-code.sh ./script/update-generated-crd-code.sh
## Generate code from dynamic configuration https://github.com/traefik/genconf
generate-genconf:
go run ./cmd/internal/gen/
## Create packages for the release ## Create packages for the release
release-packages: generate-webui build-dev-image release-packages: generate-webui build-dev-image
rm -rf dist rm -rf dist

View file

@ -0,0 +1,343 @@
package main
import (
"bytes"
"fmt"
"go/format"
"go/importer"
"go/token"
"go/types"
"io"
"log"
"os"
"path"
"path/filepath"
"sort"
"strings"
"golang.org/x/tools/imports"
)
// File a kind of AST element that represents a file.
type File struct {
Package string
Imports []string
Elements []Element
}
// Element is a simplified version of a symbol.
type Element struct {
Name string
Value string
}
// Centrifuge a centrifuge.
// Generate Go Structures from Go structures.
type Centrifuge struct {
IncludedImports []string
ExcludedTypes []string
ExcludedFiles []string
TypeCleaner func(types.Type, string) string
PackageCleaner func(string) string
rootPkg string
fileSet *token.FileSet
pkg *types.Package
}
// NewCentrifuge creates a new Centrifuge.
func NewCentrifuge(rootPkg string) (*Centrifuge, error) {
fileSet := token.NewFileSet()
pkg, err := importer.ForCompiler(fileSet, "source", nil).Import(rootPkg)
if err != nil {
return nil, err
}
return &Centrifuge{
fileSet: fileSet,
pkg: pkg,
rootPkg: rootPkg,
TypeCleaner: func(typ types.Type, _ string) string {
return typ.String()
},
PackageCleaner: func(s string) string {
return s
},
}, nil
}
// Run runs the code extraction and the code generation.
func (c Centrifuge) Run(dest string, pkgName string) error {
files, err := c.run(c.pkg.Scope(), c.rootPkg, pkgName)
if err != nil {
return err
}
err = fileWriter{baseDir: dest}.Write(files)
if err != nil {
return err
}
for _, p := range c.pkg.Imports() {
if contains(c.IncludedImports, p.Path()) {
fls, err := c.run(p.Scope(), p.Path(), p.Name())
if err != nil {
return err
}
err = fileWriter{baseDir: filepath.Join(dest, p.Name())}.Write(fls)
if err != nil {
return err
}
}
}
return err
}
func (c Centrifuge) run(sc *types.Scope, rootPkg string, pkgName string) (map[string]*File, error) {
files := map[string]*File{}
for _, name := range sc.Names() {
if contains(c.ExcludedTypes, name) {
continue
}
o := sc.Lookup(name)
if !o.Exported() {
continue
}
filename := filepath.Base(c.fileSet.File(o.Pos()).Name())
if contains(c.ExcludedFiles, path.Join(rootPkg, filename)) {
continue
}
fl, ok := files[filename]
if !ok {
files[filename] = &File{Package: pkgName}
fl = files[filename]
}
elt := Element{
Name: name,
}
switch ob := o.(type) {
case *types.TypeName:
switch obj := ob.Type().(*types.Named).Underlying().(type) {
case *types.Struct:
elt.Value = c.writeStruct(name, obj, rootPkg, fl)
case *types.Map:
elt.Value = fmt.Sprintf("type %s map[%s]%s\n", name, obj.Key().String(), c.TypeCleaner(obj.Elem(), rootPkg))
case *types.Slice:
elt.Value = fmt.Sprintf("type %s []%v\n", name, c.TypeCleaner(obj.Elem(), rootPkg))
case *types.Basic:
elt.Value = fmt.Sprintf("type %s %v\n", name, obj.Name())
default:
log.Printf("OTHER TYPE::: %s %T\n", name, o.Type().(*types.Named).Underlying())
continue
}
default:
log.Printf("OTHER::: %s %T\n", name, o)
continue
}
if len(elt.Value) > 0 {
fl.Elements = append(fl.Elements, elt)
}
}
return files, nil
}
func (c Centrifuge) writeStruct(name string, obj *types.Struct, rootPkg string, elt *File) string {
b := strings.Builder{}
b.WriteString(fmt.Sprintf("type %s struct {\n", name))
for i := 0; i < obj.NumFields(); i++ {
field := obj.Field(i)
if !field.Exported() {
continue
}
fPkg := c.PackageCleaner(extractPackage(field.Type()))
if fPkg != "" && fPkg != rootPkg {
elt.Imports = append(elt.Imports, fPkg)
}
fType := c.TypeCleaner(field.Type(), rootPkg)
if field.Embedded() {
b.WriteString(fmt.Sprintf("\t%s\n", fType))
continue
}
b.WriteString(fmt.Sprintf("\t%s %s", field.Name(), fType))
tags := obj.Tag(i)
if tags != "" {
tg := extractJSONTag(tags)
if tg != `json:"-"` {
b.WriteString(fmt.Sprintf(" `%s`", tg))
}
}
b.WriteString("\n")
}
b.WriteString("}\n")
return b.String()
}
func extractJSONTag(value string) string {
fields := strings.Fields(value)
for _, field := range fields {
if strings.HasPrefix(field, `json:"`) {
return field
}
}
return ""
}
func extractPackage(t types.Type) string {
switch tu := t.(type) {
case *types.Named:
return tu.Obj().Pkg().Path()
case *types.Slice:
if v, ok := tu.Elem().(*types.Named); ok {
return v.Obj().Pkg().Path()
}
return ""
case *types.Map:
if v, ok := tu.Elem().(*types.Named); ok {
return v.Obj().Pkg().Path()
}
return ""
case *types.Pointer:
return extractPackage(tu.Elem())
default:
return ""
}
}
func contains(values []string, value string) bool {
for _, val := range values {
if val == value {
return true
}
}
return false
}
type fileWriter struct {
baseDir string
}
func (f fileWriter) Write(files map[string]*File) error {
err := os.MkdirAll(f.baseDir, 0755)
if err != nil {
return err
}
for name, file := range files {
err = f.writeFile(name, file)
if err != nil {
return err
}
}
return nil
}
func (f fileWriter) writeFile(name string, desc *File) error {
if len(desc.Elements) == 0 {
return nil
}
filename := filepath.Join(f.baseDir, name)
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer func() { _ = file.Close() }()
b := bytes.NewBufferString("package ")
b.WriteString(desc.Package)
b.WriteString("\n")
b.WriteString("// Code generated by centrifuge. DO NOT EDIT.\n")
b.WriteString("\n")
f.writeImports(b, desc.Imports)
b.WriteString("\n")
for _, elt := range desc.Elements {
b.WriteString(elt.Value)
b.WriteString("\n")
}
// gofmt
source, err := format.Source(b.Bytes())
if err != nil {
log.Println(b.String())
return fmt.Errorf("failed to format sources: %w", err)
}
// goimports
process, err := imports.Process(filename, source, nil)
if err != nil {
log.Println(string(source))
return fmt.Errorf("failed to format imports: %w", err)
}
_, err = file.Write(process)
if err != nil {
return err
}
return nil
}
func (f fileWriter) writeImports(b io.StringWriter, imports []string) {
if len(imports) == 0 {
return
}
uniq := map[string]struct{}{}
sort.Strings(imports)
_, _ = b.WriteString("import (\n")
for _, s := range imports {
if _, exist := uniq[s]; exist {
continue
}
uniq[s] = struct{}{}
_, _ = b.WriteString(fmt.Sprintf(` "%s"`+"\n", s))
}
_, _ = b.WriteString(")\n")
}

124
cmd/internal/gen/main.go Normal file
View file

@ -0,0 +1,124 @@
package main
import (
"fmt"
"go/build"
"go/types"
"io/ioutil"
"log"
"path"
"path/filepath"
"strings"
)
const rootPkg = "github.com/traefik/traefik/v2/pkg/config/dynamic"
const (
destModuleName = "github.com/traefik/genconf"
destPkg = "dynamic"
)
const marsh = `package %s
import "encoding/json"
type JSONPayload struct {
*Configuration
}
func (c JSONPayload) MarshalJSON() ([]byte, error) {
if c.Configuration == nil {
return nil, nil
}
return json.Marshal(c.Configuration)
}
`
// main generate Go Structures from Go structures.
// Allows to create an external module (destModuleName) used by the plugin's providers
// that contains Go structs of the dynamic configuration and nothing else.
// These Go structs do not have any non-exported fields and do not rely on any external dependencies.
func main() {
dest := filepath.Join(path.Join(build.Default.GOPATH, "src"), destModuleName, destPkg)
log.Println("Output:", dest)
err := run(dest)
if err != nil {
log.Fatal(err)
}
}
func run(dest string) error {
centrifuge, err := NewCentrifuge(rootPkg)
if err != nil {
return err
}
centrifuge.IncludedImports = []string{
"github.com/traefik/traefik/v2/pkg/tls",
"github.com/traefik/traefik/v2/pkg/types",
}
centrifuge.ExcludedTypes = []string{
// tls
"CertificateStore", "Manager",
// dynamic
"Message", "Configurations",
// types
"HTTPCodeRanges", "HostResolverConfig",
}
centrifuge.ExcludedFiles = []string{
"github.com/traefik/traefik/v2/pkg/types/logs.go",
"github.com/traefik/traefik/v2/pkg/types/metrics.go",
}
centrifuge.TypeCleaner = cleanType
centrifuge.PackageCleaner = cleanPackage
err = centrifuge.Run(dest, destPkg)
if err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(dest, "marshaler.go"), []byte(fmt.Sprintf(marsh, destPkg)), 0666)
}
func cleanType(typ types.Type, base string) string {
if typ.String() == "github.com/traefik/traefik/v2/pkg/tls.FileOrContent" {
return "string"
}
if typ.String() == "[]github.com/traefik/traefik/v2/pkg/tls.FileOrContent" {
return "[]string"
}
if typ.String() == "github.com/traefik/paerser/types.Duration" {
return "string"
}
if strings.Contains(typ.String(), base) {
return strings.ReplaceAll(typ.String(), base+".", "")
}
if strings.Contains(typ.String(), "github.com/traefik/traefik/v2/pkg/") {
return strings.ReplaceAll(typ.String(), "github.com/traefik/traefik/v2/pkg/", "")
}
return typ.String()
}
func cleanPackage(src string) string {
switch src {
case "github.com/traefik/paerser/types":
return ""
case "github.com/traefik/traefik/v2/pkg/tls":
return path.Join(destModuleName, destPkg, "tls")
case "github.com/traefik/traefik/v2/pkg/types":
return path.Join(destModuleName, destPkg, "types")
default:
return src
}
}

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"fmt"
stdlog "log" stdlog "log"
"net/http" "net/http"
"os" "os"
@ -235,6 +236,20 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
return nil, err return nil, err
} }
// Providers plugins
for s, i := range staticConfiguration.Providers.Plugin {
p, err := pluginBuilder.BuildProvider(s, i)
if err != nil {
return nil, fmt.Errorf("plugin: failed to build provider: %w", err)
}
err = providerAggregator.AddProvider(p)
if err != nil {
return nil, fmt.Errorf("plugin: failed to add provider: %w", err)
}
}
// Metrics // Metrics
metricRegistries := registerMetricClients(staticConfiguration.Metrics) metricRegistries := registerMetricClients(staticConfiguration.Metrics)

1
go.mod
View file

@ -84,6 +84,7 @@ require (
golang.org/x/mod v0.3.0 golang.org/x/mod v0.3.0
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 golang.org/x/time v0.0.0-20201208040808-7e3f01d25324
golang.org/x/tools v0.0.0-20200904185747-39188db58858
google.golang.org/grpc v1.27.1 google.golang.org/grpc v1.27.1
gopkg.in/DataDog/dd-trace-go.v1 v1.19.0 gopkg.in/DataDog/dd-trace-go.v1 v1.19.0
gopkg.in/fsnotify.v1 v1.4.7 gopkg.in/fsnotify.v1 v1.4.7

View file

@ -212,6 +212,10 @@ func getProviders(conf static.Configuration) []string {
if !field.IsNil() { if !field.IsNil() {
providers = append(providers, v.Type().Field(i).Name) providers = append(providers, v.Type().Field(i).Name)
} }
} else if field.Kind() == reflect.Map && field.Type().Elem() == reflect.TypeOf(static.PluginConf{}) {
for _, value := range field.MapKeys() {
providers = append(providers, "plugin-"+value.String())
}
} }
} }

View file

@ -217,6 +217,9 @@ func TestHandler_Overview(t *testing.T) {
KubernetesCRD: &crd.Provider{}, KubernetesCRD: &crd.Provider{},
Rest: &rest.Provider{}, Rest: &rest.Provider{},
Rancher: &rancher.Provider{}, Rancher: &rancher.Provider{},
Plugin: map[string]static.PluginConf{
"test": map[string]interface{}{},
},
}, },
}, },
confDyn: runtime.Configuration{}, confDyn: runtime.Configuration{},

View file

@ -28,7 +28,8 @@
"KubernetesIngress", "KubernetesIngress",
"KubernetesCRD", "KubernetesCRD",
"Rest", "Rest",
"Rancher" "Rancher",
"plugin-test"
], ],
"tcp": { "tcp": {
"routers": { "routers": {

View file

@ -0,0 +1,4 @@
package static
// PluginConf holds the plugin configuration.
type PluginConf map[string]interface{}

View file

@ -190,6 +190,8 @@ type Providers struct {
ZooKeeper *zk.Provider `description:"Enable ZooKeeper backend with default settings." json:"zooKeeper,omitempty" toml:"zooKeeper,omitempty" yaml:"zooKeeper,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` ZooKeeper *zk.Provider `description:"Enable ZooKeeper backend with default settings." json:"zooKeeper,omitempty" toml:"zooKeeper,omitempty" yaml:"zooKeeper,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
Redis *redis.Provider `description:"Enable Redis backend with default settings." json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` Redis *redis.Provider `description:"Enable Redis backend with default settings." json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
HTTP *http.Provider `description:"Enable HTTP backend with default settings." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` HTTP *http.Provider `description:"Enable HTTP backend with default settings." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
Plugin map[string]PluginConf `description:"" json:"plugin,omitempty" toml:"plugin,omitempty" yaml:"plugin,omitempty"`
} }
// SetEffectiveConfiguration adds missing configuration parameters derived from existing ones. // SetEffectiveConfiguration adds missing configuration parameters derived from existing ones.

View file

@ -4,11 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"path"
"reflect"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/traefik/yaegi/interp" "github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib" "github.com/traefik/yaegi/stdlib"
) )
@ -34,13 +30,15 @@ type pluginContext struct {
// Builder is a plugin builder. // Builder is a plugin builder.
type Builder struct { type Builder struct {
descriptors map[string]pluginContext middlewareDescriptors map[string]pluginContext
providerDescriptors map[string]pluginContext
} }
// 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, devPlugin *DevPlugin) (*Builder, error) {
pb := &Builder{ pb := &Builder{
descriptors: map[string]pluginContext{}, middlewareDescriptors: map[string]pluginContext{},
providerDescriptors: map[string]pluginContext{},
} }
for pName, desc := range plugins { for pName, desc := range plugins {
@ -52,18 +50,31 @@ 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(stdlib.Symbols)
i.Use(ppSymbols())
_, 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", desc.ModuleName, manifest.Import, err) return nil, fmt.Errorf("%s: failed to import plugin code %q: %w", desc.ModuleName, manifest.Import, err)
} }
pb.descriptors[pName] = pluginContext{ switch manifest.Type {
case "middleware":
pb.middlewareDescriptors[pName] = pluginContext{
interpreter: i, interpreter: i,
GoPath: client.GoPath(), GoPath: client.GoPath(),
Import: manifest.Import, Import: manifest.Import,
BasePkg: manifest.BasePkg, BasePkg: manifest.BasePkg,
} }
case "provider":
pb.providerDescriptors[pName] = pluginContext{
interpreter: i,
GoPath: client.GoPath(),
Import: manifest.Import,
BasePkg: manifest.BasePkg,
}
default:
return nil, fmt.Errorf("unknow plugin type: %s", manifest.Type)
}
} }
if devPlugin != nil { if devPlugin != nil {
@ -74,101 +85,32 @@ func NewBuilder(client *Client, plugins map[string]Descriptor, devPlugin *DevPlu
i := interp.New(interp.Options{GoPath: devPlugin.GoPath}) i := interp.New(interp.Options{GoPath: devPlugin.GoPath})
i.Use(stdlib.Symbols) i.Use(stdlib.Symbols)
i.Use(ppSymbols())
_, 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", devPlugin.ModuleName, manifest.Import, err)
} }
pb.descriptors[devPluginName] = pluginContext{ switch manifest.Type {
case "middleware":
pb.middlewareDescriptors[devPluginName] = pluginContext{
interpreter: i, interpreter: i,
GoPath: devPlugin.GoPath, GoPath: devPlugin.GoPath,
Import: manifest.Import, Import: manifest.Import,
BasePkg: manifest.BasePkg, BasePkg: manifest.BasePkg,
} }
case "provider":
pb.providerDescriptors[devPluginName] = pluginContext{
interpreter: i,
GoPath: devPlugin.GoPath,
Import: manifest.Import,
BasePkg: manifest.BasePkg,
}
default:
return nil, fmt.Errorf("unknow plugin type: %s", manifest.Type)
}
} }
return pb, nil return pb, nil
} }
// Build builds a plugin.
func (b Builder) Build(pName string, config map[string]interface{}, middlewareName string) (Constructor, 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)
}
m, err := newMiddleware(descriptor, config, middlewareName)
if err != nil {
return nil, err
}
return m.NewHandler, err
}
// 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
}

View file

@ -0,0 +1,94 @@
package plugins
import (
"context"
"fmt"
"net/http"
"path"
"reflect"
"strings"
"github.com/mitchellh/mapstructure"
)
// Build builds a middleware plugin.
func (b Builder) Build(pName string, config map[string]interface{}, middlewareName string) (Constructor, error) {
if b.middlewareDescriptors == nil {
return nil, fmt.Errorf("no plugin definition in the static configuration: %s", pName)
}
descriptor, ok := b.middlewareDescriptors[pName]
if !ok {
return nil, fmt.Errorf("unknown plugin type: %s", pName)
}
m, err := newMiddleware(descriptor, config, middlewareName)
if err != nil {
return nil, err
}
return m.NewHandler, err
}
// 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("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("failed to create configuration decoder: %w", err)
}
err = decoder.Decode(config)
if err != nil {
return nil, fmt.Errorf("failed to decode configuration: %w", err)
}
fnNew, err := descriptor.interpreter.Eval(basePkg + `.New`)
if err != nil {
return nil, fmt.Errorf("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("invalid handler type: %T", results[0].Interface())
}
return handler, nil
}

View file

@ -81,7 +81,10 @@ func checkDevPluginConfiguration(plugin *DevPlugin) error {
return err return err
} }
if m.Type != "middleware" { switch m.Type {
case "middleware", "provider":
// noop
default:
return errors.New("unsupported type") return errors.New("unsupported type")
} }

196
pkg/plugins/providers.go Normal file
View file

@ -0,0 +1,196 @@
package plugins
import (
"context"
"encoding/json"
"fmt"
"path"
"reflect"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/provider"
"github.com/traefik/traefik/v2/pkg/safe"
)
// PP the interface of a plugin's provider.
type PP interface {
Init() error
Provide(cfgChan chan<- json.Marshaler) error
Stop() error
}
type _PP struct {
WInit func() error
WProvide func(cfgChan chan<- json.Marshaler) error
WStop func() error
}
func (p _PP) Init() error {
return p.WInit()
}
func (p _PP) Provide(cfgChan chan<- json.Marshaler) error {
return p.WProvide(cfgChan)
}
func (p _PP) Stop() error {
return p.WStop()
}
func ppSymbols() map[string]map[string]reflect.Value {
return map[string]map[string]reflect.Value{
"github.com/traefik/traefik/v2/pkg/plugins": {
"PP": reflect.ValueOf((*PP)(nil)),
"_PP": reflect.ValueOf((*_PP)(nil)),
},
}
}
// BuildProvider builds a plugin's provider.
func (b Builder) BuildProvider(pName string, config map[string]interface{}) (provider.Provider, error) {
if b.providerDescriptors == nil {
return nil, fmt.Errorf("no plugin definition in the static configuration: %s", pName)
}
descriptor, ok := b.providerDescriptors[pName]
if !ok {
return nil, fmt.Errorf("unknown plugin type: %s", pName)
}
return newProvider(descriptor, config, "plugin-"+pName)
}
// Provider is a plugin's provider wrapper.
type Provider struct {
name string
pp PP
}
func newProvider(descriptor pluginContext, config map[string]interface{}, providerName string) (*Provider, 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("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("failed to create configuration decoder: %w", err)
}
err = decoder.Decode(config)
if err != nil {
return nil, fmt.Errorf("failed to decode configuration: %w", err)
}
_, err = descriptor.interpreter.Eval(`package wrapper
import (
"context"
` + basePkg + ` "` + descriptor.Import + `"
"github.com/traefik/traefik/v2/pkg/plugins"
)
func NewWrapper(ctx context.Context, config *` + basePkg + `.Config, name string) (plugins.PP, error) {
p, err := ` + basePkg + `.New(ctx, config, name)
var pv plugins.PP = p
return pv, err
}
`)
if err != nil {
return nil, fmt.Errorf("failed to eval wrapper: %w", err)
}
fnNew, err := descriptor.interpreter.Eval("wrapper.NewWrapper")
if err != nil {
return nil, fmt.Errorf("failed to eval New: %w", err)
}
ctx := context.Background()
args := []reflect.Value{reflect.ValueOf(ctx), vConfig, reflect.ValueOf(providerName)}
results := fnNew.Call(args)
if len(results) > 1 && results[1].Interface() != nil {
return nil, results[1].Interface().(error)
}
prov, ok := results[0].Interface().(PP)
if !ok {
return nil, fmt.Errorf("invalid provider type: %T", results[0].Interface())
}
return &Provider{name: providerName, pp: prov}, nil
}
// Init wraps the Init method of a plugin.
func (p *Provider) Init() error {
return p.pp.Init()
}
// Provide wraps the Provide method of a plugin.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
defer func() {
if err := recover(); err != nil {
log.WithoutContext().WithField(log.ProviderName, p.name).Errorf("panic inside the plugin %v", err)
}
}()
cfgChan := make(chan json.Marshaler)
err := p.pp.Provide(cfgChan)
if err != nil {
return fmt.Errorf("error from %s: %w", p.name, err)
}
pool.GoCtx(func(ctx context.Context) {
logger := log.FromContext(log.With(ctx, log.Str(log.ProviderName, p.name)))
for {
select {
case <-ctx.Done():
err := p.pp.Stop()
if err != nil {
logger.Errorf("failed to stop the provider: %v", err)
}
return
case cfgPg := <-cfgChan:
marshalJSON, err := cfgPg.MarshalJSON()
if err != nil {
logger.Errorf("failed to marshal configuration: %v", err)
continue
}
cfg := &dynamic.Configuration{}
err = json.Unmarshal(marshalJSON, cfg)
if err != nil {
logger.Errorf("failed to unmarshal configuration: %v", err)
continue
}
configurationChan <- dynamic.Message{
ProviderName: p.name,
Configuration: cfg,
}
}
}
})
return nil
}

View file

@ -347,12 +347,12 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (
pluginType, rawPluginConfig, err := findPluginConfig(config.Plugin) pluginType, rawPluginConfig, err := findPluginConfig(config.Plugin)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("plugin: %w", err)
} }
plug, err := b.pluginBuilder.Build(pluginType, rawPluginConfig, middlewareName) plug, err := b.pluginBuilder.Build(pluginType, rawPluginConfig, middlewareName)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("plugin: %w", err)
} }
middleware = func(next http.Handler) (http.Handler, error) { middleware = func(next http.Handler) (http.Handler, error) {

View file

@ -15,7 +15,7 @@ type PluginsBuilder interface {
func findPluginConfig(rawConfig map[string]dynamic.PluginConf) (string, map[string]interface{}, error) { func findPluginConfig(rawConfig map[string]dynamic.PluginConf) (string, map[string]interface{}, error) {
if len(rawConfig) != 1 { if len(rawConfig) != 1 {
return "", nil, errors.New("plugin: invalid configuration: no configuration or too many plugin definition") return "", nil, errors.New("invalid configuration: no configuration or too many plugin definition")
} }
var pluginType string var pluginType string
@ -27,11 +27,11 @@ func findPluginConfig(rawConfig map[string]dynamic.PluginConf) (string, map[stri
} }
if pluginType == "" { if pluginType == "" {
return "", nil, errors.New("plugin: missing plugin type") return "", nil, errors.New("missing plugin type")
} }
if len(rawPluginConfig) == 0 { if len(rawPluginConfig) == 0 {
return "", nil, fmt.Errorf("plugin: missing plugin configuration: %s", pluginType) return "", nil, fmt.Errorf("missing plugin configuration: %s", pluginType)
} }
return pluginType, rawPluginConfig, nil return pluginType, rawPluginConfig, nil

View file

@ -20,7 +20,7 @@
<div class="text-subtitle2">PROVIDER</div> <div class="text-subtitle2">PROVIDER</div>
<div class="block-right-text"> <div class="block-right-text">
<q-avatar class="provider-logo"> <q-avatar class="provider-logo">
<q-icon :name="`img:statics/providers/${middleware.provider}.svg`" /> <q-icon :name="`img:${getProviderLogoPath(middleware.provider)}`" />
</q-avatar> </q-avatar>
<div class="block-right-text-label">{{middleware.provider}}</div> <div class="block-right-text-label">{{middleware.provider}}</div>
</div> </div>
@ -1106,6 +1106,15 @@ export default {
} }
} }
return exData return exData
},
getProviderLogoPath (provider) {
const name = provider.name.toLowerCase()
if (name.includes('plugin-')) {
return 'statics/providers/plugin.svg'
}
return `statics/providers/${name}.svg`
} }
}, },
filters: { filters: {

View file

@ -31,7 +31,7 @@
</div> </div>
<div class="col-3 text-right"> <div class="col-3 text-right">
<q-avatar class="provider-logo"> <q-avatar class="provider-logo">
<q-icon :name="`img:statics/providers/${getProvider(service)}.svg`" /> <q-icon :name="`img:${getProviderLogoPath(service)}`" />
</q-avatar> </q-avatar>
</div> </div>
</div> </div>
@ -61,6 +61,16 @@ export default {
} }
return this.data.provider return this.data.provider
},
getProviderLogoPath (service) {
const provider = this.getProvider(service)
const name = provider.toLowerCase()
if (name.includes('plugin-')) {
return 'statics/providers/plugin.svg'
}
return `statics/providers/${name}.svg`
} }
} }
} }

View file

@ -14,7 +14,7 @@
<div class="text-subtitle2">PROVIDER</div> <div class="text-subtitle2">PROVIDER</div>
<div class="block-right-text"> <div class="block-right-text">
<q-avatar class="provider-logo"> <q-avatar class="provider-logo">
<q-icon :name="`img:statics/providers/${data.provider}.svg`" /> <q-icon :name="`img:${getProviderLogoPath}`" />
</q-avatar> </q-avatar>
<div class="block-right-text-label">{{data.provider}}</div> <div class="block-right-text-label">{{data.provider}}</div>
</div> </div>
@ -127,6 +127,17 @@ export default {
} }
return value return value
} }
},
computed: {
getProviderLogoPath () {
const name = this.data.provider.toLowerCase()
if (name.includes('plugin-')) {
return 'statics/providers/plugin.svg'
}
return `statics/providers/${name}.svg`
}
} }
} }
</script> </script>

View file

@ -15,7 +15,7 @@
<div class="text-subtitle2">PROVIDER</div> <div class="text-subtitle2">PROVIDER</div>
<div class="block-right-text"> <div class="block-right-text">
<q-avatar class="provider-logo"> <q-avatar class="provider-logo">
<q-icon :name="`img:statics/providers/${data.provider}.svg`" /> <q-icon :name="`img:${getProviderLogoPath}`" />
</q-avatar> </q-avatar>
<div class="block-right-text-label">{{data.provider}}</div> <div class="block-right-text-label">{{data.provider}}</div>
</div> </div>
@ -113,6 +113,15 @@ export default {
} }
return null return null
},
getProviderLogoPath () {
const name = this.data.provider.toLowerCase()
if (name.includes('plugin-')) {
return 'statics/providers/plugin.svg'
}
return `statics/providers/${name}.svg`
} }
}, },
filters: { filters: {

View file

@ -31,7 +31,7 @@
</div> </div>
<div class="col-4"> <div class="col-4">
<q-avatar> <q-avatar>
<q-icon :name="`img:statics/providers/${getProvider(service)}.svg`" /> <q-icon :name="`img:${getProviderLogoPath(service)}`" />
</q-avatar> </q-avatar>
</div> </div>
</div> </div>
@ -61,6 +61,16 @@ export default {
} }
return this.data.provider return this.data.provider
},
getProviderLogoPath (service) {
const provider = this.getProvider(service)
const name = provider.name.toLowerCase()
if (name.includes('plugin-')) {
return 'statics/providers/plugin.svg'
}
return `statics/providers/${name}.svg`
} }
} }
} }

View file

@ -1,12 +1,23 @@
<template> <template>
<q-avatar class="provider-logo"> <q-avatar class="provider-logo">
<q-icon :name="`img:statics/providers/${name}.svg`" /> <q-icon :name="`img:${getLogoPath}`" />
</q-avatar> </q-avatar>
</template> </template>
<script> <script>
export default { export default {
props: ['name'] props: ['name'],
computed: {
getLogoPath () {
const name = this.name.toLowerCase()
if (name.includes('plugin-')) {
return 'statics/providers/plugin.svg'
}
return `statics/providers/${name}.svg`
}
}
} }
</script> </script>

View file

@ -4,7 +4,7 @@
<div class="row items-center no-wrap"> <div class="row items-center no-wrap">
<div class="col text-center"> <div class="col text-center">
<q-avatar class="provider-logo"> <q-avatar class="provider-logo">
<q-icon :name="`img:statics/providers/${getNameLogo}.svg`" /> <q-icon :name="`img:${getLogoPath}`" />
</q-avatar> </q-avatar>
</div> </div>
</div> </div>
@ -25,8 +25,14 @@ export default {
getName () { getName () {
return this.name return this.name
}, },
getNameLogo () { getLogoPath () {
return this.getName.toLowerCase() const name = this.getName.toLowerCase()
if (name.includes('plugin-')) {
return 'statics/providers/plugin.svg'
}
return `statics/providers/${name}.svg`
} }
} }
} }

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>plugin</title>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="plugin" fill-rule="nonzero">
<circle id="Oval" fill="#6DC4A8" cx="16" cy="16" r="16"></circle>
<path d="M10.7517313,10.1738295 L22.2621133,19.1941905 L21.8304412,19.7449978 C20.0202381,22.0549461 17.2269562,23.0792113 14.836947,22.5465569 L14.5702202,22.8869226 C14.1903807,23.3716261 13.5380774,23.5005798 13.1206478,23.1734862 L12.1087414,22.3804984 L10.0368349,25.0243619 C9.65696684,25.5090655 9.00463498,25.6380191 8.58723393,25.3109255 L8.33426446,25.1126429 C7.9168634,24.7855208 7.88612346,24.1213154 8.26599153,23.6366119 L10.3379265,20.9927484 L9.32602009,20.199732 C8.90861904,19.8726385 8.87790764,19.2084045 9.25774716,18.723701 L9.43776279,18.4939934 C8.15541907,16.2757226 8.42328756,13.1450149 10.3200591,10.7246368 L10.7517313,10.1738295 Z M23.5543041,11.5531311 C24.1108484,11.9892558 24.1518349,12.8748916 23.6453537,13.5211725 L20.8654425,17.2106062 L18.8915999,15.6278127 L21.6215408,11.9351683 C22.1280221,11.2888874 22.9977598,11.1169778 23.5543041,11.5531311 Z M18.0521111,7.2411761 C18.6086553,7.67732938 18.6496134,8.56296514 18.1431321,9.20924605 L15.2398281,12.8891125 L13.3222617,11.3618714 L16.1193478,7.62324192 C16.6258005,6.97696101 17.4955668,6.80505137 18.0521111,7.2411761 Z" id="Shape" fill="#FFFFFF"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB