feat: plugins integration.
This commit is contained in:
parent
58bf1a2ca5
commit
0186c31d59
26 changed files with 1025 additions and 51 deletions
166
pkg/plugins/builder.go
Normal file
166
pkg/plugins/builder.go
Normal file
|
@ -0,0 +1,166 @@
|
|||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/containous/yaegi/interp"
|
||||
"github.com/containous/yaegi/stdlib"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
const devPluginName = "dev"
|
||||
|
||||
// pluginContext The static part of a plugin configuration.
|
||||
type pluginContext struct {
|
||||
// GoPath plugin's GOPATH
|
||||
GoPath string `json:"goPath,omitempty" toml:"goPath,omitempty" yaml:"goPath,omitempty"`
|
||||
|
||||
// Import plugin's import/package
|
||||
Import string `json:"import,omitempty" toml:"import,omitempty" yaml:"import,omitempty"`
|
||||
|
||||
// BasePkg plugin's base package name (optional)
|
||||
BasePkg string `json:"basePkg,omitempty" toml:"basePkg,omitempty" yaml:"basePkg,omitempty"`
|
||||
|
||||
interpreter *interp.Interpreter
|
||||
}
|
||||
|
||||
// Builder is a plugin builder.
|
||||
type Builder struct {
|
||||
descriptors map[string]pluginContext
|
||||
}
|
||||
|
||||
// NewBuilder creates a new Builder.
|
||||
func NewBuilder(client *Client, plugins map[string]Descriptor, devPlugin *DevPlugin) (*Builder, error) {
|
||||
pb := &Builder{
|
||||
descriptors: map[string]pluginContext{},
|
||||
}
|
||||
|
||||
for pName, desc := range plugins {
|
||||
manifest, err := client.ReadManifest(desc.ModuleName)
|
||||
if err != nil {
|
||||
_ = client.ResetAll()
|
||||
return nil, fmt.Errorf("%s: failed to read manifest: %w", desc.ModuleName, err)
|
||||
}
|
||||
|
||||
i := interp.New(interp.Options{GoPath: client.GoPath()})
|
||||
i.Use(stdlib.Symbols)
|
||||
|
||||
_, err = i.Eval(fmt.Sprintf(`import "%s"`, manifest.Import))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to import plugin code %q: %w", desc.ModuleName, manifest.Import, err)
|
||||
}
|
||||
|
||||
pb.descriptors[pName] = pluginContext{
|
||||
interpreter: i,
|
||||
GoPath: client.GoPath(),
|
||||
Import: manifest.Import,
|
||||
BasePkg: manifest.BasePkg,
|
||||
}
|
||||
}
|
||||
|
||||
if devPlugin != nil {
|
||||
manifest, err := ReadManifest(devPlugin.GoPath, devPlugin.ModuleName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to read manifest: %w", devPlugin.ModuleName, err)
|
||||
}
|
||||
|
||||
i := interp.New(interp.Options{GoPath: devPlugin.GoPath})
|
||||
i.Use(stdlib.Symbols)
|
||||
|
||||
_, err = i.Eval(fmt.Sprintf(`import "%s"`, manifest.Import))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to import plugin code %q: %w", devPlugin.ModuleName, manifest.Import, err)
|
||||
}
|
||||
|
||||
pb.descriptors[devPluginName] = pluginContext{
|
||||
interpreter: i,
|
||||
GoPath: devPlugin.GoPath,
|
||||
Import: manifest.Import,
|
||||
BasePkg: manifest.BasePkg,
|
||||
}
|
||||
}
|
||||
|
||||
return pb, nil
|
||||
}
|
||||
|
||||
// Build builds a plugin.
|
||||
func (b Builder) Build(pName string, config map[string]interface{}, middlewareName string) (*Middleware, error) {
|
||||
if b.descriptors == nil {
|
||||
return nil, fmt.Errorf("plugin: no plugin definition in the static configuration: %s", pName)
|
||||
}
|
||||
|
||||
descriptor, ok := b.descriptors[pName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("plugin: unknown plugin type: %s", pName)
|
||||
}
|
||||
|
||||
return newMiddleware(descriptor, config, middlewareName)
|
||||
}
|
||||
|
||||
// Middleware is a HTTP handler plugin wrapper.
|
||||
type Middleware struct {
|
||||
middlewareName string
|
||||
fnNew reflect.Value
|
||||
config reflect.Value
|
||||
}
|
||||
|
||||
func newMiddleware(descriptor pluginContext, config map[string]interface{}, middlewareName string) (*Middleware, error) {
|
||||
basePkg := descriptor.BasePkg
|
||||
if basePkg == "" {
|
||||
basePkg = strings.ReplaceAll(path.Base(descriptor.Import), "-", "_")
|
||||
}
|
||||
|
||||
vConfig, err := descriptor.interpreter.Eval(basePkg + `.CreateConfig()`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("plugin: failed to eval CreateConfig: %w", err)
|
||||
}
|
||||
|
||||
cfg := &mapstructure.DecoderConfig{
|
||||
DecodeHook: mapstructure.StringToSliceHookFunc(","),
|
||||
WeaklyTypedInput: true,
|
||||
Result: vConfig.Interface(),
|
||||
}
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("plugin: failed to create configuration decoder: %w", err)
|
||||
}
|
||||
|
||||
err = decoder.Decode(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("plugin: failed to decode configuration: %w", err)
|
||||
}
|
||||
|
||||
fnNew, err := descriptor.interpreter.Eval(basePkg + `.New`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("plugin: failed to eval New: %w", err)
|
||||
}
|
||||
|
||||
return &Middleware{
|
||||
middlewareName: middlewareName,
|
||||
fnNew: fnNew,
|
||||
config: vConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewHandler creates a new HTTP handler.
|
||||
func (m *Middleware) NewHandler(ctx context.Context, next http.Handler) (http.Handler, error) {
|
||||
args := []reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(next), m.config, reflect.ValueOf(m.middlewareName)}
|
||||
results := m.fnNew.Call(args)
|
||||
|
||||
if len(results) > 1 && results[1].Interface() != nil {
|
||||
return nil, results[1].Interface().(error)
|
||||
}
|
||||
|
||||
handler, ok := results[0].Interface().(http.Handler)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("plugin: invalid handler type: %T", results[0].Interface())
|
||||
}
|
||||
|
||||
return handler, nil
|
||||
}
|
415
pkg/plugins/client.go
Normal file
415
pkg/plugins/client.go
Normal file
|
@ -0,0 +1,415 @@
|
|||
package plugins
|
||||
|
||||
import (
|
||||
zipa "archive/zip"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mod/module"
|
||||
"golang.org/x/mod/zip"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
sourcesFolder = "sources"
|
||||
archivesFolder = "archives"
|
||||
stateFilename = "state.json"
|
||||
goPathSrc = "src"
|
||||
pluginManifest = ".traefik.yml"
|
||||
)
|
||||
|
||||
const pilotURL = "https://plugin.pilot.traefik.io/public/"
|
||||
|
||||
const (
|
||||
hashHeader = "X-Plugin-Hash"
|
||||
tokenHeader = "X-Token"
|
||||
)
|
||||
|
||||
// ClientOptions the options of a Traefik Pilot client.
|
||||
type ClientOptions struct {
|
||||
Output string
|
||||
Token string
|
||||
}
|
||||
|
||||
// Client a Traefik Pilot client.
|
||||
type Client struct {
|
||||
HTTPClient *http.Client
|
||||
baseURL *url.URL
|
||||
|
||||
token string
|
||||
archives string
|
||||
stateFile string
|
||||
goPath string
|
||||
sources string
|
||||
}
|
||||
|
||||
// NewClient creates a new Traefik Pilot client.
|
||||
func NewClient(opts ClientOptions) (*Client, error) {
|
||||
baseURL, err := url.Parse(pilotURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sourcesRootPath := filepath.Join(filepath.FromSlash(opts.Output), sourcesFolder)
|
||||
err = resetDirectory(sourcesRootPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
goPath, err := ioutil.TempDir(sourcesRootPath, "gop-*")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create GoPath: %w", err)
|
||||
}
|
||||
|
||||
archivesPath := filepath.Join(filepath.FromSlash(opts.Output), archivesFolder)
|
||||
err = os.MkdirAll(archivesPath, 0o755)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create archives directory %s: %w", archivesPath, err)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
HTTPClient: &http.Client{Timeout: 5 * time.Second},
|
||||
baseURL: baseURL,
|
||||
|
||||
archives: archivesPath,
|
||||
stateFile: filepath.Join(archivesPath, stateFilename),
|
||||
|
||||
goPath: goPath,
|
||||
sources: filepath.Join(goPath, goPathSrc),
|
||||
|
||||
token: opts.Token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GoPath gets the plugins GoPath.
|
||||
func (c *Client) GoPath() string {
|
||||
return c.goPath
|
||||
}
|
||||
|
||||
// ReadManifest reads a plugin manifest.
|
||||
func (c *Client) ReadManifest(moduleName string) (*Manifest, error) {
|
||||
return ReadManifest(c.goPath, moduleName)
|
||||
}
|
||||
|
||||
// ReadManifest reads a plugin manifest.
|
||||
func ReadManifest(goPath, moduleName string) (*Manifest, error) {
|
||||
p := filepath.Join(goPath, goPathSrc, filepath.FromSlash(moduleName), pluginManifest)
|
||||
|
||||
file, err := os.Open(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open the plugin manifest %s: %w", p, err)
|
||||
}
|
||||
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
m := &Manifest{}
|
||||
err = yaml.NewDecoder(file).Decode(m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode the plugin manifest %s: %w", p, err)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Download downloads a plugin archive.
|
||||
func (c *Client) Download(ctx context.Context, pName, pVersion string) (string, error) {
|
||||
filename := c.buildArchivePath(pName, pVersion)
|
||||
|
||||
var hash string
|
||||
_, err := os.Stat(filename)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("failed to read archive %s: %w", filename, err)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
hash, err = computeHash(filename)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to compute hash: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "download", pName, pVersion))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse endpoint URL: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
if hash != "" {
|
||||
req.Header.Set(hashHeader, hash)
|
||||
}
|
||||
|
||||
if c.token != "" {
|
||||
req.Header.Set(tokenHeader, c.token)
|
||||
}
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to call service: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
err = os.MkdirAll(filepath.Dir(filename), 0o755)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
var file *os.File
|
||||
file, err = os.Create(filename)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create file %q: %w", filename, err)
|
||||
}
|
||||
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
_, err = io.Copy(file, resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to write response: %w", err)
|
||||
}
|
||||
|
||||
hash, err = computeHash(filename)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to compute hash: %w", err)
|
||||
}
|
||||
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusNotModified {
|
||||
// noop
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
data, _ := ioutil.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("error: %d: %s", resp.StatusCode, string(data))
|
||||
}
|
||||
|
||||
// Check checks the plugin archive integrity.
|
||||
func (c *Client) Check(ctx context.Context, pName, pVersion, hash string) error {
|
||||
endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "validate", pName, pVersion))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse endpoint URL: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
if hash != "" {
|
||||
req.Header.Set(hashHeader, hash)
|
||||
}
|
||||
|
||||
if c.token != "" {
|
||||
req.Header.Set(tokenHeader, c.token)
|
||||
}
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to call service: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("plugin integrity check failed")
|
||||
}
|
||||
|
||||
// Unzip unzip a plugin archive.
|
||||
func (c *Client) Unzip(pName, pVersion string) error {
|
||||
err := c.unzipModule(pName, pVersion)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.unzipArchive(pName, pVersion)
|
||||
}
|
||||
|
||||
func (c *Client) unzipModule(pName, pVersion string) error {
|
||||
src := c.buildArchivePath(pName, pVersion)
|
||||
dest := filepath.Join(c.sources, filepath.FromSlash(pName))
|
||||
|
||||
return zip.Unzip(dest, module.Version{Path: pName, Version: pVersion}, src)
|
||||
}
|
||||
|
||||
func (c *Client) unzipArchive(pName, pVersion string) error {
|
||||
zipPath := c.buildArchivePath(pName, pVersion)
|
||||
|
||||
archive, err := zipa.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() { _ = archive.Close() }()
|
||||
|
||||
dest := filepath.Join(c.sources, filepath.FromSlash(pName))
|
||||
|
||||
for _, f := range archive.File {
|
||||
err = unzipFile(f, dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func unzipFile(f *zipa.File, dest string) error {
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() { _ = rc.Close() }()
|
||||
|
||||
pathParts := strings.SplitN(f.Name, string(os.PathSeparator), 2)
|
||||
p := filepath.Join(dest, pathParts[1])
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
return os.MkdirAll(p, f.Mode())
|
||||
}
|
||||
|
||||
err = os.MkdirAll(filepath.Dir(p), 0o750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
elt, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() { _ = elt.Close() }()
|
||||
|
||||
_, err = io.Copy(elt, rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanArchives cleans plugins archives.
|
||||
func (c *Client) CleanArchives(plugins map[string]Descriptor) error {
|
||||
if _, err := os.Stat(c.stateFile); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
stateFile, err := os.Open(c.stateFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open state file %s: %w", c.stateFile, err)
|
||||
}
|
||||
|
||||
previous := make(map[string]string)
|
||||
err = json.NewDecoder(stateFile).Decode(&previous)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode state file %s: %w", c.stateFile, err)
|
||||
}
|
||||
|
||||
for pName, pVersion := range previous {
|
||||
for _, desc := range plugins {
|
||||
if desc.ModuleName == pName && desc.Version != pVersion {
|
||||
archivePath := c.buildArchivePath(pName, pVersion)
|
||||
if err = os.RemoveAll(archivePath); err != nil {
|
||||
return fmt.Errorf("failed to remove archive %s: %w", archivePath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteState writes the plugins state files.
|
||||
func (c *Client) WriteState(plugins map[string]Descriptor) error {
|
||||
m := make(map[string]string)
|
||||
|
||||
for _, descriptor := range plugins {
|
||||
m[descriptor.ModuleName] = descriptor.Version
|
||||
}
|
||||
|
||||
mp, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(c.stateFile, mp, 0o600)
|
||||
}
|
||||
|
||||
// ResetAll resets all plugins related directories.
|
||||
func (c *Client) ResetAll() error {
|
||||
if c.goPath == "" {
|
||||
return errors.New("goPath is empty")
|
||||
}
|
||||
|
||||
err := resetDirectory(filepath.Join(c.goPath, ".."))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return resetDirectory(c.archives)
|
||||
}
|
||||
|
||||
func (c *Client) buildArchivePath(pName, pVersion string) string {
|
||||
return filepath.Join(c.archives, filepath.FromSlash(pName), pVersion+".zip")
|
||||
}
|
||||
|
||||
func resetDirectory(dir string) error {
|
||||
dirPath, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(currentPath, dirPath) {
|
||||
return fmt.Errorf("cannot be deleted: the directory path %s is the parent of the current path %s", dirPath, currentPath)
|
||||
}
|
||||
|
||||
err = os.RemoveAll(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.MkdirAll(dir, 0o755)
|
||||
}
|
||||
|
||||
func computeHash(filepath string) (string, error) {
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hash := sha256.New()
|
||||
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sum := hash.Sum(nil)
|
||||
|
||||
return fmt.Sprintf("%x", sum), nil
|
||||
}
|
146
pkg/plugins/plugins.go
Normal file
146
pkg/plugins/plugins.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/containous/traefik/v2/pkg/log"
|
||||
)
|
||||
|
||||
// Setup setup plugins environment.
|
||||
func Setup(client *Client, plugins map[string]Descriptor, devPlugin *DevPlugin) error {
|
||||
err := checkPluginsConfiguration(plugins)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid configuration: %w", err)
|
||||
}
|
||||
|
||||
err = client.CleanArchives(plugins)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to clean archives: %w", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
for pAlias, desc := range plugins {
|
||||
log.FromContext(ctx).Debugf("loading of plugin: %s: %s@%s", pAlias, desc.ModuleName, desc.Version)
|
||||
|
||||
hash, err := client.Download(ctx, desc.ModuleName, desc.Version)
|
||||
if err != nil {
|
||||
_ = client.ResetAll()
|
||||
return fmt.Errorf("failed to download plugin %s: %w", desc.ModuleName, err)
|
||||
}
|
||||
|
||||
err = client.Check(ctx, desc.ModuleName, desc.Version, hash)
|
||||
if err != nil {
|
||||
_ = client.ResetAll()
|
||||
return fmt.Errorf("failed to check archive integrity of the plugin %s: %w", desc.ModuleName, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = client.WriteState(plugins)
|
||||
if err != nil {
|
||||
_ = client.ResetAll()
|
||||
return fmt.Errorf("failed to write plugins state: %w", err)
|
||||
}
|
||||
|
||||
for _, desc := range plugins {
|
||||
err = client.Unzip(desc.ModuleName, desc.Version)
|
||||
if err != nil {
|
||||
_ = client.ResetAll()
|
||||
return fmt.Errorf("failed to unzip archive: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if devPlugin != nil {
|
||||
err := checkDevPluginConfiguration(devPlugin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid configuration: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkDevPluginConfiguration(plugin *DevPlugin) error {
|
||||
if plugin == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if plugin.GoPath == "" {
|
||||
return errors.New("missing Go Path (prefer a dedicated Go Path)")
|
||||
}
|
||||
|
||||
if plugin.ModuleName == "" {
|
||||
return errors.New("missing module name")
|
||||
}
|
||||
|
||||
m, err := ReadManifest(plugin.GoPath, plugin.ModuleName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if m.Type != "middleware" {
|
||||
return errors.New("unsupported type")
|
||||
}
|
||||
|
||||
if m.Import == "" {
|
||||
return errors.New("missing import")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(m.Import, plugin.ModuleName) {
|
||||
return fmt.Errorf("the import %q must be related to the module name %q", m.Import, plugin.ModuleName)
|
||||
}
|
||||
|
||||
if m.DisplayName == "" {
|
||||
return errors.New("missing DisplayName")
|
||||
}
|
||||
|
||||
if m.Summary == "" {
|
||||
return errors.New("missing Summary")
|
||||
}
|
||||
|
||||
if m.TestData == nil {
|
||||
return errors.New("missing TestData")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkPluginsConfiguration(plugins map[string]Descriptor) error {
|
||||
if plugins == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
uniq := make(map[string]struct{})
|
||||
|
||||
var errs []string
|
||||
for pAlias, descriptor := range plugins {
|
||||
if descriptor.ModuleName == "" {
|
||||
errs = append(errs, fmt.Sprintf("%s: plugin name is missing", pAlias))
|
||||
}
|
||||
|
||||
if descriptor.Version == "" {
|
||||
errs = append(errs, fmt.Sprintf("%s: plugin version is missing", pAlias))
|
||||
}
|
||||
|
||||
if strings.HasPrefix(descriptor.ModuleName, "/") || strings.HasSuffix(descriptor.ModuleName, "/") {
|
||||
errs = append(errs, fmt.Sprintf("%s: plugin name should not start or end with a /", pAlias))
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := uniq[descriptor.ModuleName]; ok {
|
||||
errs = append(errs, fmt.Sprintf("only one version of a plugin is allowed, there is a duplicate of %s", descriptor.ModuleName))
|
||||
continue
|
||||
}
|
||||
|
||||
uniq[descriptor.ModuleName] = struct{}{}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.New(strings.Join(errs, ": "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
30
pkg/plugins/types.go
Normal file
30
pkg/plugins/types.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package plugins
|
||||
|
||||
// Descriptor The static part of a plugin configuration (prod).
|
||||
type Descriptor struct {
|
||||
// ModuleName (required)
|
||||
ModuleName string `description:"plugin's module name." json:"moduleName,omitempty" toml:"moduleName,omitempty" yaml:"moduleName,omitempty"`
|
||||
|
||||
// Version (required)
|
||||
Version string `description:"plugin's version." json:"version,omitempty" toml:"version,omitempty" yaml:"version,omitempty"`
|
||||
}
|
||||
|
||||
// DevPlugin The static part of a plugin configuration (only for dev).
|
||||
type DevPlugin struct {
|
||||
// GoPath plugin's GOPATH. (required)
|
||||
GoPath string `description:"plugin's GOPATH." json:"goPath,omitempty" toml:"goPath,omitempty" yaml:"goPath,omitempty"`
|
||||
|
||||
// ModuleName (required)
|
||||
ModuleName string `description:"plugin's module name." json:"moduleName,omitempty" toml:"moduleName,omitempty" yaml:"moduleName,omitempty"`
|
||||
}
|
||||
|
||||
// Manifest The plugin manifest.
|
||||
type Manifest struct {
|
||||
DisplayName string `yaml:"displayName"`
|
||||
Type string `yaml:"type"`
|
||||
Import string `yaml:"import"`
|
||||
BasePkg string `yaml:"basePkg"`
|
||||
Compatibility string `yaml:"compatibility"`
|
||||
Summary string `yaml:"summary"`
|
||||
TestData map[string]interface{} `yaml:"testData"`
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue