mirror of
https://github.com/containous/traefik.git
synced 2024-12-22 13:34:03 +03:00
Add plugin's support for provider
Co-authored-by: Julien Salleyron <julien@traefik.io>
This commit is contained in:
parent
de2437cfec
commit
63ef0f1cee
6
Makefile
6
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
|
||||
|
343
cmd/internal/gen/centrifuge.go
Normal file
343
cmd/internal/gen/centrifuge.go
Normal 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
124
cmd/internal/gen/main.go
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
1
go.mod
1
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
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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{},
|
||||
|
3
pkg/api/testdata/overview-providers.json
vendored
3
pkg/api/testdata/overview-providers.json
vendored
@ -28,7 +28,8 @@
|
||||
"KubernetesIngress",
|
||||
"KubernetesCRD",
|
||||
"Rest",
|
||||
"Rancher"
|
||||
"Rancher",
|
||||
"plugin-test"
|
||||
],
|
||||
"tcp": {
|
||||
"routers": {
|
||||
|
4
pkg/config/static/plugins.go
Normal file
4
pkg/config/static/plugins.go
Normal file
@ -0,0 +1,4 @@
|
||||
package static
|
||||
|
||||
// PluginConf holds the plugin configuration.
|
||||
type PluginConf map[string]interface{}
|
@ -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.
|
||||
|
@ -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,18 +50,31 @@ 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{
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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.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{
|
||||
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
|
||||
}
|
||||
|
94
pkg/plugins/middlewares.go
Normal file
94
pkg/plugins/middlewares.go
Normal 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
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
||||
|
196
pkg/plugins/providers.go
Normal file
196
pkg/plugins/providers.go
Normal 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
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -20,7 +20,7 @@
|
||||
<div class="text-subtitle2">PROVIDER</div>
|
||||
<div class="block-right-text">
|
||||
<q-avatar class="provider-logo">
|
||||
<q-icon :name="`img:statics/providers/${middleware.provider}.svg`" />
|
||||
<q-icon :name="`img:${getProviderLogoPath(middleware.provider)}`" />
|
||||
</q-avatar>
|
||||
<div class="block-right-text-label">{{middleware.provider}}</div>
|
||||
</div>
|
||||
@ -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: {
|
||||
|
@ -31,7 +31,7 @@
|
||||
</div>
|
||||
<div class="col-3 text-right">
|
||||
<q-avatar class="provider-logo">
|
||||
<q-icon :name="`img:statics/providers/${getProvider(service)}.svg`" />
|
||||
<q-icon :name="`img:${getProviderLogoPath(service)}`" />
|
||||
</q-avatar>
|
||||
</div>
|
||||
</div>
|
||||
@ -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`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
<div class="text-subtitle2">PROVIDER</div>
|
||||
<div class="block-right-text">
|
||||
<q-avatar class="provider-logo">
|
||||
<q-icon :name="`img:statics/providers/${data.provider}.svg`" />
|
||||
<q-icon :name="`img:${getProviderLogoPath}`" />
|
||||
</q-avatar>
|
||||
<div class="block-right-text-label">{{data.provider}}</div>
|
||||
</div>
|
||||
@ -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`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<div class="text-subtitle2">PROVIDER</div>
|
||||
<div class="block-right-text">
|
||||
<q-avatar class="provider-logo">
|
||||
<q-icon :name="`img:statics/providers/${data.provider}.svg`" />
|
||||
<q-icon :name="`img:${getProviderLogoPath}`" />
|
||||
</q-avatar>
|
||||
<div class="block-right-text-label">{{data.provider}}</div>
|
||||
</div>
|
||||
@ -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: {
|
||||
|
@ -31,7 +31,7 @@
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<q-avatar>
|
||||
<q-icon :name="`img:statics/providers/${getProvider(service)}.svg`" />
|
||||
<q-icon :name="`img:${getProviderLogoPath(service)}`" />
|
||||
</q-avatar>
|
||||
</div>
|
||||
</div>
|
||||
@ -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`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,23 @@
|
||||
<template>
|
||||
<q-avatar class="provider-logo">
|
||||
<q-icon :name="`img:statics/providers/${name}.svg`" />
|
||||
<q-icon :name="`img:${getLogoPath}`" />
|
||||
</q-avatar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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>
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="row items-center no-wrap">
|
||||
<div class="col text-center">
|
||||
<q-avatar class="provider-logo">
|
||||
<q-icon :name="`img:statics/providers/${getNameLogo}.svg`" />
|
||||
<q-icon :name="`img:${getLogoPath}`" />
|
||||
</q-avatar>
|
||||
</div>
|
||||
</div>
|
||||
@ -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`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
10
webui/src/statics/providers/plugin.svg
Normal file
10
webui/src/statics/providers/plugin.svg
Normal 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 |
Loading…
Reference in New Issue
Block a user