A plugin from FindPlugin will now return the correct Type and Path when calling info on it. Before, they would only be set on ListPluginInfos which makes no sense.
188 lines
4.7 KiB
Go
188 lines
4.7 KiB
Go
// Package d2plugin enables the d2 CLI to run functions bundled
|
|
// with the d2 binary or via external plugin binaries.
|
|
//
|
|
// Binary plugins are stored in $PATH with the prefix d2plugin-*. i.e the binary for
|
|
// dagre might be d2plugin-dagre. See ListPlugins() below.
|
|
package d2plugin
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os/exec"
|
|
|
|
"oss.terrastruct.com/util-go/xexec"
|
|
"oss.terrastruct.com/util-go/xmain"
|
|
|
|
"oss.terrastruct.com/d2/d2graph"
|
|
)
|
|
|
|
// plugins contains the bundled d2 plugins.
|
|
//
|
|
// See plugin_* files for the plugins available for bundling.
|
|
var plugins []Plugin
|
|
|
|
type PluginSpecificFlag struct {
|
|
Name string
|
|
Type string
|
|
Default interface{}
|
|
Usage string
|
|
// Must match the tag in the opt
|
|
Tag string
|
|
}
|
|
|
|
func (f *PluginSpecificFlag) AddToOpts(opts *xmain.Opts) {
|
|
switch f.Type {
|
|
case "string":
|
|
opts.String("", f.Name, "", f.Default.(string), f.Usage)
|
|
case "int64":
|
|
var val int64
|
|
switch defaultType := f.Default.(type) {
|
|
case int64:
|
|
val = defaultType
|
|
case float64:
|
|
// json unmarshals numbers to float64
|
|
val = int64(defaultType)
|
|
}
|
|
opts.Int64("", f.Name, "", val, f.Usage)
|
|
}
|
|
}
|
|
|
|
type Plugin interface {
|
|
// Info returns the current info information of the plugin.
|
|
Info(context.Context) (*PluginInfo, error)
|
|
|
|
Flags(context.Context) ([]PluginSpecificFlag, error)
|
|
|
|
HydrateOpts([]byte) error
|
|
|
|
// Layout runs the plugin's autolayout algorithm on the input graph
|
|
// and returns a new graph with the computed placements.
|
|
Layout(context.Context, *d2graph.Graph) error
|
|
|
|
// PostProcess makes changes to the default render
|
|
PostProcess(context.Context, []byte) ([]byte, error)
|
|
}
|
|
|
|
// PluginInfo is the current info information of a plugin.
|
|
// note: The two fields Type and Path are not set by the plugin
|
|
// itself but only in ListPlugins.
|
|
type PluginInfo struct {
|
|
Name string `json:"name"`
|
|
ShortHelp string `json:"shortHelp"`
|
|
LongHelp string `json:"longHelp"`
|
|
|
|
// Set to bundled when returning from the plugin.
|
|
// execPlugin will set to binary when used.
|
|
// bundled | binary
|
|
Type string `json:"type"`
|
|
// If Type == binary then this contains the absolute path to the binary.
|
|
Path string `json:"path"`
|
|
}
|
|
|
|
const binaryPrefix = "d2plugin-"
|
|
|
|
func ListPlugins(ctx context.Context) ([]Plugin, error) {
|
|
// 1. Run Info on all bundled plugins in the global plugins array.
|
|
// - set Type for each bundled plugin to "bundled".
|
|
// 2. Iterate through directories in $PATH and look for executables within these
|
|
// directories with the prefix d2plugin-*
|
|
// 3. Run each plugin binary with the argument info. e.g. d2plugin-dagre info
|
|
|
|
var ps []Plugin
|
|
ps = append(ps, plugins...)
|
|
|
|
matches, err := xexec.SearchPath(binaryPrefix)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
BINARY_PLUGINS_LOOP:
|
|
for _, path := range matches {
|
|
p := &execPlugin{path: path}
|
|
info, err := p.Info(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, p2 := range ps {
|
|
info2, err := p2.Info(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if info.Name == info2.Name {
|
|
continue BINARY_PLUGINS_LOOP
|
|
}
|
|
}
|
|
ps = append(ps, p)
|
|
}
|
|
return ps, nil
|
|
}
|
|
|
|
func ListPluginInfos(ctx context.Context, ps []Plugin) ([]*PluginInfo, error) {
|
|
var infoSlice []*PluginInfo
|
|
for _, p := range ps {
|
|
info, err := p.Info(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
infoSlice = append(infoSlice, info)
|
|
}
|
|
|
|
return infoSlice, nil
|
|
}
|
|
|
|
// FindPlugin finds the plugin with the given name.
|
|
// 1. It first searches the bundled plugins in the global plugins slice.
|
|
// 2. If not found, it then searches each directory in $PATH for a binary with the name
|
|
// d2plugin-<name>.
|
|
// **NOTE** When D2 upgrades to go 1.19, remember to ignore exec.ErrDot
|
|
// 3. If such a binary is found, it builds an execPlugin in exec.go
|
|
// to get a plugin implementation around the binary and returns it.
|
|
func FindPlugin(ctx context.Context, ps []Plugin, name string) (Plugin, error) {
|
|
for _, p := range ps {
|
|
info, err := p.Info(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if info.Name == name {
|
|
return p, nil
|
|
}
|
|
}
|
|
return nil, exec.ErrNotFound
|
|
}
|
|
|
|
func ListPluginFlags(ctx context.Context, ps []Plugin) ([]PluginSpecificFlag, error) {
|
|
var out []PluginSpecificFlag
|
|
for _, p := range ps {
|
|
flags, err := p.Flags(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, flags...)
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func HydratePluginOpts(ctx context.Context, ms *xmain.State, plugin Plugin) error {
|
|
opts := make(map[string]interface{})
|
|
flags, err := plugin.Flags(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, f := range flags {
|
|
switch f.Type {
|
|
case "string":
|
|
val, _ := ms.Opts.Flags.GetString(f.Name)
|
|
opts[f.Tag] = val
|
|
case "int64":
|
|
val, _ := ms.Opts.Flags.GetInt64(f.Name)
|
|
opts[f.Tag] = val
|
|
}
|
|
}
|
|
|
|
b, err := json.Marshal(opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return plugin.HydrateOpts(b)
|
|
}
|