...

Source file src/oss.terrastruct.com/d2/d2plugin/plugin.go

Documentation: oss.terrastruct.com/d2/d2plugin

     1  // Package d2plugin enables the d2 CLI to run functions bundled
     2  // with the d2 binary or via external plugin binaries.
     3  //
     4  // Binary plugins are stored in $PATH with the prefix d2plugin-*. i.e the binary for
     5  // dagre might be d2plugin-dagre. See ListPlugins() below.
     6  package d2plugin
     7  
     8  import (
     9  	"context"
    10  	"encoding/json"
    11  	"os/exec"
    12  
    13  	"oss.terrastruct.com/util-go/xexec"
    14  	"oss.terrastruct.com/util-go/xmain"
    15  
    16  	"oss.terrastruct.com/d2/d2graph"
    17  )
    18  
    19  // plugins contains the bundled d2 plugins.
    20  //
    21  // See plugin_* files for the plugins available for bundling.
    22  var plugins []Plugin
    23  
    24  type PluginSpecificFlag struct {
    25  	Name    string
    26  	Type    string
    27  	Default interface{}
    28  	Usage   string
    29  	// Must match the tag in the opt
    30  	Tag string
    31  }
    32  
    33  func (f *PluginSpecificFlag) AddToOpts(opts *xmain.Opts) {
    34  	switch f.Type {
    35  	case "string":
    36  		opts.String("", f.Name, "", f.Default.(string), f.Usage)
    37  	case "int64":
    38  		var val int64
    39  		switch defaultType := f.Default.(type) {
    40  		case int64:
    41  			val = defaultType
    42  		case float64:
    43  			// json unmarshals numbers to float64
    44  			val = int64(defaultType)
    45  		}
    46  		opts.Int64("", f.Name, "", val, f.Usage)
    47  	case "[]int64":
    48  		var slice []int64
    49  		switch defaultType := f.Default.(type) {
    50  		case []int64:
    51  			slice = defaultType
    52  		case []interface{}:
    53  			for _, v := range defaultType {
    54  				switch defaultType := v.(type) {
    55  				case int64:
    56  					slice = append(slice, defaultType)
    57  				case float64:
    58  					// json unmarshals numbers to float64
    59  					slice = append(slice, int64(defaultType))
    60  				}
    61  			}
    62  		}
    63  		opts.Int64Slice("", f.Name, "", slice, f.Usage)
    64  	}
    65  }
    66  
    67  type Plugin interface {
    68  	// Info returns the current info information of the plugin.
    69  	Info(context.Context) (*PluginInfo, error)
    70  
    71  	Flags(context.Context) ([]PluginSpecificFlag, error)
    72  
    73  	HydrateOpts([]byte) error
    74  
    75  	// Layout runs the plugin's autolayout algorithm on the input graph
    76  	// and returns a new graph with the computed placements.
    77  	Layout(context.Context, *d2graph.Graph) error
    78  
    79  	// PostProcess makes changes to the default render
    80  	PostProcess(context.Context, []byte) ([]byte, error)
    81  }
    82  
    83  type RoutingPlugin interface {
    84  	// RouteEdges runs the plugin's edge routing algorithm for the given edges in the input graph
    85  	RouteEdges(context.Context, *d2graph.Graph, []*d2graph.Edge) error
    86  }
    87  
    88  // PluginInfo is the current info information of a plugin.
    89  // note: The two fields Type and Path are not set by the plugin
    90  // itself but only in ListPlugins.
    91  type PluginInfo struct {
    92  	Name      string `json:"name"`
    93  	ShortHelp string `json:"shortHelp"`
    94  	LongHelp  string `json:"longHelp"`
    95  
    96  	// Set to bundled when returning from the plugin.
    97  	// execPlugin will set to binary when used.
    98  	// bundled | binary
    99  	Type string `json:"type"`
   100  	// If Type == binary then this contains the absolute path to the binary.
   101  	Path string `json:"path"`
   102  
   103  	Features []PluginFeature `json:"features"`
   104  }
   105  
   106  const binaryPrefix = "d2plugin-"
   107  
   108  func ListPlugins(ctx context.Context) ([]Plugin, error) {
   109  	// 1. Run Info on all bundled plugins in the global plugins array.
   110  	//    - set Type for each bundled plugin to "bundled".
   111  	// 2. Iterate through directories in $PATH and look for executables within these
   112  	//    directories with the prefix d2plugin-*
   113  	// 3. Run each plugin binary with the argument info. e.g. d2plugin-dagre info
   114  
   115  	var ps []Plugin
   116  	ps = append(ps, plugins...)
   117  
   118  	matches, err := xexec.SearchPath(binaryPrefix)
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  BINARY_PLUGINS_LOOP:
   123  	for _, path := range matches {
   124  		p := &execPlugin{path: path}
   125  		info, err := p.Info(ctx)
   126  		if err != nil {
   127  			return nil, err
   128  		}
   129  		for _, p2 := range ps {
   130  			info2, err := p2.Info(ctx)
   131  			if err != nil {
   132  				return nil, err
   133  			}
   134  			if info.Name == info2.Name {
   135  				continue BINARY_PLUGINS_LOOP
   136  			}
   137  		}
   138  		ps = append(ps, p)
   139  	}
   140  	return ps, nil
   141  }
   142  
   143  func ListPluginInfos(ctx context.Context, ps []Plugin) ([]*PluginInfo, error) {
   144  	var infoSlice []*PluginInfo
   145  	for _, p := range ps {
   146  		info, err := p.Info(ctx)
   147  		if err != nil {
   148  			return nil, err
   149  		}
   150  		infoSlice = append(infoSlice, info)
   151  	}
   152  
   153  	return infoSlice, nil
   154  }
   155  
   156  // FindPlugin finds the plugin with the given name.
   157  //  1. It first searches the bundled plugins in the global plugins slice.
   158  //  2. If not found, it then searches each directory in $PATH for a binary with the name
   159  //     d2plugin-<name>.
   160  //  3. If such a binary is found, it builds an execPlugin in exec.go
   161  //     to get a plugin implementation around the binary and returns it.
   162  func FindPlugin(ctx context.Context, ps []Plugin, name string) (Plugin, error) {
   163  	for _, p := range ps {
   164  		info, err := p.Info(ctx)
   165  		if err != nil {
   166  			return nil, err
   167  		}
   168  		if info.Name == name {
   169  			return p, nil
   170  		}
   171  	}
   172  	return nil, exec.ErrNotFound
   173  }
   174  
   175  func ListPluginFlags(ctx context.Context, ps []Plugin) ([]PluginSpecificFlag, error) {
   176  	var out []PluginSpecificFlag
   177  	for _, p := range ps {
   178  		flags, err := p.Flags(ctx)
   179  		if err != nil {
   180  			return nil, err
   181  		}
   182  		out = append(out, flags...)
   183  	}
   184  
   185  	return out, nil
   186  }
   187  
   188  func HydratePluginOpts(ctx context.Context, ms *xmain.State, plugin Plugin) error {
   189  	opts := make(map[string]interface{})
   190  	flags, err := plugin.Flags(ctx)
   191  	if err != nil {
   192  		return err
   193  	}
   194  	for _, f := range flags {
   195  		switch f.Type {
   196  		case "string":
   197  			val, _ := ms.Opts.Flags.GetString(f.Name)
   198  			opts[f.Tag] = val
   199  		case "int64":
   200  			val, _ := ms.Opts.Flags.GetInt64(f.Name)
   201  			opts[f.Tag] = val
   202  		case "[]int64":
   203  			val, _ := ms.Opts.Flags.GetInt64Slice(f.Name)
   204  			opts[f.Tag] = val
   205  		}
   206  	}
   207  
   208  	b, err := json.Marshal(opts)
   209  	if err != nil {
   210  		return err
   211  	}
   212  
   213  	return plugin.HydrateOpts(b)
   214  }
   215  

View as plain text