diff --git a/Makefile b/Makefile index fb0d20c8f..8baea58c4 100644 --- a/Makefile +++ b/Makefile @@ -123,7 +123,7 @@ shell: build-dev-image docs: make -C ./docs docs -## Serve the documentation site localy +## Serve the documentation site locally docs-serve: make -C ./docs docs-serve @@ -135,6 +135,10 @@ docs-pull-images: generate-crd: ./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 release-packages: generate-webui build-dev-image rm -rf dist diff --git a/cmd/internal/gen/centrifuge.go b/cmd/internal/gen/centrifuge.go new file mode 100644 index 000000000..87c09f272 --- /dev/null +++ b/cmd/internal/gen/centrifuge.go @@ -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") +} diff --git a/cmd/internal/gen/main.go b/cmd/internal/gen/main.go new file mode 100644 index 000000000..fb0b95773 --- /dev/null +++ b/cmd/internal/gen/main.go @@ -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 + } +} diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index ad2d99e7a..274716cc1 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -4,6 +4,7 @@ import ( "context" "crypto/x509" "encoding/json" + "fmt" stdlog "log" "net/http" "os" @@ -235,6 +236,20 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, 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 metricRegistries := registerMetricClients(staticConfiguration.Metrics) diff --git a/go.mod b/go.mod index 88837e10a..8aa3bd200 100644 --- a/go.mod +++ b/go.mod @@ -84,6 +84,7 @@ require ( golang.org/x/mod v0.3.0 golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d 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 gopkg.in/DataDog/dd-trace-go.v1 v1.19.0 gopkg.in/fsnotify.v1 v1.4.7 diff --git a/pkg/api/handler_overview.go b/pkg/api/handler_overview.go index 8308c1af9..a9ff75139 100644 --- a/pkg/api/handler_overview.go +++ b/pkg/api/handler_overview.go @@ -212,6 +212,10 @@ func getProviders(conf static.Configuration) []string { if !field.IsNil() { 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()) + } } } diff --git a/pkg/api/handler_overview_test.go b/pkg/api/handler_overview_test.go index 00a4bc36c..8614e0098 100644 --- a/pkg/api/handler_overview_test.go +++ b/pkg/api/handler_overview_test.go @@ -217,6 +217,9 @@ func TestHandler_Overview(t *testing.T) { KubernetesCRD: &crd.Provider{}, Rest: &rest.Provider{}, Rancher: &rancher.Provider{}, + Plugin: map[string]static.PluginConf{ + "test": map[string]interface{}{}, + }, }, }, confDyn: runtime.Configuration{}, diff --git a/pkg/api/testdata/overview-providers.json b/pkg/api/testdata/overview-providers.json index f49804ea5..08fb3ffa3 100644 --- a/pkg/api/testdata/overview-providers.json +++ b/pkg/api/testdata/overview-providers.json @@ -28,7 +28,8 @@ "KubernetesIngress", "KubernetesCRD", "Rest", - "Rancher" + "Rancher", + "plugin-test" ], "tcp": { "routers": { diff --git a/pkg/config/static/plugins.go b/pkg/config/static/plugins.go new file mode 100644 index 000000000..5b2577e4e --- /dev/null +++ b/pkg/config/static/plugins.go @@ -0,0 +1,4 @@ +package static + +// PluginConf holds the plugin configuration. +type PluginConf map[string]interface{} diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index c9c41888b..a1aaaa60f 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -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"` 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"` + + Plugin map[string]PluginConf `description:"" json:"plugin,omitempty" toml:"plugin,omitempty" yaml:"plugin,omitempty"` } // SetEffectiveConfiguration adds missing configuration parameters derived from existing ones. diff --git a/pkg/plugins/builder.go b/pkg/plugins/builder.go index 3416c7ee4..fa2bfd90e 100644 --- a/pkg/plugins/builder.go +++ b/pkg/plugins/builder.go @@ -4,11 +4,7 @@ import ( "context" "fmt" "net/http" - "path" - "reflect" - "strings" - "github.com/mitchellh/mapstructure" "github.com/traefik/yaegi/interp" "github.com/traefik/yaegi/stdlib" ) @@ -34,13 +30,15 @@ type pluginContext struct { // Builder is a plugin builder. type Builder struct { - descriptors map[string]pluginContext + middlewareDescriptors map[string]pluginContext + providerDescriptors 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{}, + middlewareDescriptors: map[string]pluginContext{}, + providerDescriptors: map[string]pluginContext{}, } for pName, desc := range plugins { @@ -52,17 +50,30 @@ 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.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, + switch manifest.Type { + case "middleware": + pb.middlewareDescriptors[pName] = pluginContext{ + interpreter: i, + GoPath: client.GoPath(), + Import: manifest.Import, + 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) } } @@ -74,101 +85,32 @@ func NewBuilder(client *Client, plugins map[string]Descriptor, devPlugin *DevPlu i := interp.New(interp.Options{GoPath: devPlugin.GoPath}) i.Use(stdlib.Symbols) + i.Use(ppSymbols()) _, 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, + switch manifest.Type { + case "middleware": + pb.middlewareDescriptors[devPluginName] = pluginContext{ + interpreter: i, + GoPath: devPlugin.GoPath, + Import: manifest.Import, + 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 } - -// 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 -} diff --git a/pkg/plugins/middlewares.go b/pkg/plugins/middlewares.go new file mode 100644 index 000000000..e9011de73 --- /dev/null +++ b/pkg/plugins/middlewares.go @@ -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 +} diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 26302f671..ec7775f04 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -81,7 +81,10 @@ func checkDevPluginConfiguration(plugin *DevPlugin) error { return err } - if m.Type != "middleware" { + switch m.Type { + case "middleware", "provider": + // noop + default: return errors.New("unsupported type") } diff --git a/pkg/plugins/providers.go b/pkg/plugins/providers.go new file mode 100644 index 000000000..0cb95935f --- /dev/null +++ b/pkg/plugins/providers.go @@ -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 +} diff --git a/pkg/server/middleware/middlewares.go b/pkg/server/middleware/middlewares.go index 66b7aca45..547e04248 100644 --- a/pkg/server/middleware/middlewares.go +++ b/pkg/server/middleware/middlewares.go @@ -347,12 +347,12 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) ( pluginType, rawPluginConfig, err := findPluginConfig(config.Plugin) if err != nil { - return nil, err + return nil, fmt.Errorf("plugin: %w", err) } plug, err := b.pluginBuilder.Build(pluginType, rawPluginConfig, middlewareName) if err != nil { - return nil, err + return nil, fmt.Errorf("plugin: %w", err) } middleware = func(next http.Handler) (http.Handler, error) { diff --git a/pkg/server/middleware/plugins.go b/pkg/server/middleware/plugins.go index 6b73d91dd..a40179151 100644 --- a/pkg/server/middleware/plugins.go +++ b/pkg/server/middleware/plugins.go @@ -15,7 +15,7 @@ type PluginsBuilder interface { 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") + return "", nil, errors.New("invalid configuration: no configuration or too many plugin definition") } var pluginType string @@ -27,11 +27,11 @@ func findPluginConfig(rawConfig map[string]dynamic.PluginConf) (string, map[stri } if pluginType == "" { - return "", nil, errors.New("plugin: missing plugin type") + return "", nil, errors.New("missing plugin type") } 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 diff --git a/webui/src/components/_commons/PanelMiddlewares.vue b/webui/src/components/_commons/PanelMiddlewares.vue index 13837b73c..35b526335 100644 --- a/webui/src/components/_commons/PanelMiddlewares.vue +++ b/webui/src/components/_commons/PanelMiddlewares.vue @@ -20,7 +20,7 @@
PROVIDER
{{middleware.provider}}
@@ -1106,6 +1106,15 @@ export default { } } return exData + }, + getProviderLogoPath (provider) { + const name = provider.name.toLowerCase() + + if (name.includes('plugin-')) { + return 'statics/providers/plugin.svg' + } + + return `statics/providers/${name}.svg` } }, filters: { diff --git a/webui/src/components/_commons/PanelMirroringServices.vue b/webui/src/components/_commons/PanelMirroringServices.vue index 09a613894..d0d748fb7 100644 --- a/webui/src/components/_commons/PanelMirroringServices.vue +++ b/webui/src/components/_commons/PanelMirroringServices.vue @@ -31,7 +31,7 @@
@@ -61,6 +61,16 @@ export default { } 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` } } } diff --git a/webui/src/components/_commons/PanelRouterDetails.vue b/webui/src/components/_commons/PanelRouterDetails.vue index 26ddfc6f3..41eaca57e 100644 --- a/webui/src/components/_commons/PanelRouterDetails.vue +++ b/webui/src/components/_commons/PanelRouterDetails.vue @@ -14,7 +14,7 @@
PROVIDER
{{data.provider}}
@@ -127,6 +127,17 @@ export default { } return value } + }, + computed: { + getProviderLogoPath () { + const name = this.data.provider.toLowerCase() + + if (name.includes('plugin-')) { + return 'statics/providers/plugin.svg' + } + + return `statics/providers/${name}.svg` + } } } diff --git a/webui/src/components/_commons/PanelServiceDetails.vue b/webui/src/components/_commons/PanelServiceDetails.vue index c6ac60cef..a6052935b 100644 --- a/webui/src/components/_commons/PanelServiceDetails.vue +++ b/webui/src/components/_commons/PanelServiceDetails.vue @@ -15,7 +15,7 @@
PROVIDER
{{data.provider}}
@@ -113,6 +113,15 @@ export default { } return null + }, + getProviderLogoPath () { + const name = this.data.provider.toLowerCase() + + if (name.includes('plugin-')) { + return 'statics/providers/plugin.svg' + } + + return `statics/providers/${name}.svg` } }, filters: { diff --git a/webui/src/components/_commons/PanelWeightedServices.vue b/webui/src/components/_commons/PanelWeightedServices.vue index fb0ec582c..f5c9d3880 100644 --- a/webui/src/components/_commons/PanelWeightedServices.vue +++ b/webui/src/components/_commons/PanelWeightedServices.vue @@ -31,7 +31,7 @@
- +
@@ -61,6 +61,16 @@ export default { } 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` } } } diff --git a/webui/src/components/_commons/ProviderIcon.vue b/webui/src/components/_commons/ProviderIcon.vue index ecf749f74..035a548a4 100644 --- a/webui/src/components/_commons/ProviderIcon.vue +++ b/webui/src/components/_commons/ProviderIcon.vue @@ -1,12 +1,23 @@ diff --git a/webui/src/components/dashboard/PanelProvider.vue b/webui/src/components/dashboard/PanelProvider.vue index 2c79867a5..dba854c35 100644 --- a/webui/src/components/dashboard/PanelProvider.vue +++ b/webui/src/components/dashboard/PanelProvider.vue @@ -4,7 +4,7 @@
@@ -25,8 +25,14 @@ export default { getName () { return this.name }, - getNameLogo () { - return this.getName.toLowerCase() + getLogoPath () { + const name = this.getName.toLowerCase() + + if (name.includes('plugin-')) { + return 'statics/providers/plugin.svg' + } + + return `statics/providers/${name}.svg` } } } diff --git a/webui/src/statics/providers/plugin.svg b/webui/src/statics/providers/plugin.svg new file mode 100644 index 000000000..5a6a63769 --- /dev/null +++ b/webui/src/statics/providers/plugin.svg @@ -0,0 +1,10 @@ + + + plugin + + + + + + + \ No newline at end of file