2022-11-03 13:54:49 +00:00
|
|
|
// 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"
|
2022-12-31 01:30:25 +00:00
|
|
|
"encoding/json"
|
2022-11-03 13:54:49 +00:00
|
|
|
"os/exec"
|
2025-04-07 06:45:36 +00:00
|
|
|
"strings"
|
2022-11-03 13:54:49 +00:00
|
|
|
|
2022-12-01 19:05:00 +00:00
|
|
|
"oss.terrastruct.com/util-go/xexec"
|
2022-12-30 21:19:48 +00:00
|
|
|
"oss.terrastruct.com/util-go/xmain"
|
2022-12-01 19:19:39 +00:00
|
|
|
|
|
|
|
|
"oss.terrastruct.com/d2/d2graph"
|
2022-11-03 13:54:49 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// plugins contains the bundled d2 plugins.
|
|
|
|
|
//
|
|
|
|
|
// See plugin_* files for the plugins available for bundling.
|
|
|
|
|
var plugins []Plugin
|
|
|
|
|
|
2022-12-30 08:09:28 +00:00
|
|
|
type PluginSpecificFlag struct {
|
|
|
|
|
Name string
|
|
|
|
|
Type string
|
|
|
|
|
Default interface{}
|
|
|
|
|
Usage string
|
|
|
|
|
// Must match the tag in the opt
|
|
|
|
|
Tag string
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-30 21:19:48 +00:00
|
|
|
func (f *PluginSpecificFlag) AddToOpts(opts *xmain.Opts) {
|
|
|
|
|
switch f.Type {
|
|
|
|
|
case "string":
|
|
|
|
|
opts.String("", f.Name, "", f.Default.(string), f.Usage)
|
|
|
|
|
case "int64":
|
2022-12-31 00:16:29 +00:00
|
|
|
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)
|
2023-03-18 00:37:01 +00:00
|
|
|
case "[]int64":
|
|
|
|
|
var slice []int64
|
2023-03-18 00:48:21 +00:00
|
|
|
switch defaultType := f.Default.(type) {
|
|
|
|
|
case []int64:
|
|
|
|
|
slice = defaultType
|
|
|
|
|
case []interface{}:
|
|
|
|
|
for _, v := range defaultType {
|
|
|
|
|
switch defaultType := v.(type) {
|
|
|
|
|
case int64:
|
|
|
|
|
slice = append(slice, defaultType)
|
|
|
|
|
case float64:
|
|
|
|
|
// json unmarshals numbers to float64
|
|
|
|
|
slice = append(slice, int64(defaultType))
|
|
|
|
|
}
|
2023-03-18 00:37:01 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
opts.Int64Slice("", f.Name, "", slice, f.Usage)
|
2022-12-30 21:19:48 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
type Plugin interface {
|
|
|
|
|
// Info returns the current info information of the plugin.
|
|
|
|
|
Info(context.Context) (*PluginInfo, error)
|
|
|
|
|
|
2022-12-30 19:33:32 +00:00
|
|
|
Flags(context.Context) ([]PluginSpecificFlag, error)
|
2022-12-30 08:09:28 +00:00
|
|
|
|
|
|
|
|
HydrateOpts([]byte) error
|
2022-12-30 05:09:53 +00:00
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-21 20:31:36 +00:00
|
|
|
type RoutingPlugin interface {
|
|
|
|
|
// RouteEdges runs the plugin's edge routing algorithm for the given edges in the input graph
|
|
|
|
|
RouteEdges(context.Context, *d2graph.Graph, []*d2graph.Edge) error
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-16 00:31:27 +00:00
|
|
|
type routeEdgesInput struct {
|
2024-04-16 05:10:50 +00:00
|
|
|
G []byte `json:"g"`
|
|
|
|
|
GEdges []byte `json:"gEdges"`
|
2024-04-16 00:31:27 +00:00
|
|
|
}
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
// 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"`
|
|
|
|
|
|
2023-02-16 23:48:01 +00:00
|
|
|
// Set to bundled when returning from the plugin.
|
|
|
|
|
// execPlugin will set to binary when used.
|
2022-11-03 13:54:49 +00:00
|
|
|
// bundled | binary
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
// If Type == binary then this contains the absolute path to the binary.
|
|
|
|
|
Path string `json:"path"`
|
2023-02-19 05:51:55 +00:00
|
|
|
|
2023-02-20 17:05:00 +00:00
|
|
|
Features []PluginFeature `json:"features"`
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const binaryPrefix = "d2plugin-"
|
|
|
|
|
|
2023-01-03 22:48:23 +00:00
|
|
|
func ListPlugins(ctx context.Context) ([]Plugin, error) {
|
2022-11-03 13:54:49 +00:00
|
|
|
// 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
|
|
|
|
|
|
2023-01-03 22:48:23 +00:00
|
|
|
var ps []Plugin
|
|
|
|
|
ps = append(ps, plugins...)
|
2022-11-03 13:54:49 +00:00
|
|
|
|
|
|
|
|
matches, err := xexec.SearchPath(binaryPrefix)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2023-01-03 22:48:23 +00:00
|
|
|
BINARY_PLUGINS_LOOP:
|
2022-11-03 13:54:49 +00:00
|
|
|
for _, path := range matches {
|
|
|
|
|
p := &execPlugin{path: path}
|
|
|
|
|
info, err := p.Info(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2023-01-03 22:48:23 +00:00
|
|
|
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
|
|
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
infoSlice = append(infoSlice, info)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return infoSlice, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FindPlugin finds the plugin with the given name.
|
2023-03-14 18:18:46 +00:00
|
|
|
// 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>.
|
|
|
|
|
// 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.
|
2023-01-03 22:48:23 +00:00
|
|
|
func FindPlugin(ctx context.Context, ps []Plugin, name string) (Plugin, error) {
|
|
|
|
|
for _, p := range ps {
|
2022-11-03 13:54:49 +00:00
|
|
|
info, err := p.Info(ctx)
|
|
|
|
|
if err != nil {
|
2023-01-03 22:48:23 +00:00
|
|
|
return nil, err
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
2025-04-07 06:45:36 +00:00
|
|
|
if strings.EqualFold(info.Name, name) {
|
2023-01-03 22:48:23 +00:00
|
|
|
return p, nil
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
}
|
2023-01-03 22:48:23 +00:00
|
|
|
return nil, exec.ErrNotFound
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
2022-12-30 08:09:28 +00:00
|
|
|
|
2023-01-03 22:48:23 +00:00
|
|
|
func ListPluginFlags(ctx context.Context, ps []Plugin) ([]PluginSpecificFlag, error) {
|
2022-12-30 08:09:28 +00:00
|
|
|
var out []PluginSpecificFlag
|
2023-01-03 22:48:23 +00:00
|
|
|
for _, p := range ps {
|
2022-12-31 00:07:50 +00:00
|
|
|
flags, err := p.Flags(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
out = append(out, flags...)
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-30 19:33:32 +00:00
|
|
|
return out, nil
|
2022-12-30 08:09:28 +00:00
|
|
|
}
|
2022-12-31 01:30:25 +00:00
|
|
|
|
|
|
|
|
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
|
2023-03-18 00:37:01 +00:00
|
|
|
case "[]int64":
|
|
|
|
|
val, _ := ms.Opts.Flags.GetInt64Slice(f.Name)
|
|
|
|
|
opts[f.Tag] = val
|
2022-12-31 01:30:25 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
b, err := json.Marshal(opts)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return plugin.HydrateOpts(b)
|
|
|
|
|
}
|