github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/plugin/plugin.go (about)

     1  package plugin
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"strings"
     9  
    10  	"github.com/evergreen-ci/evergreen/model"
    11  	"github.com/evergreen-ci/evergreen/model/artifact"
    12  	"github.com/evergreen-ci/evergreen/model/task"
    13  	"github.com/gorilla/context"
    14  	"github.com/mongodb/grip"
    15  	"github.com/mongodb/grip/slogger"
    16  	"github.com/pkg/errors"
    17  )
    18  
    19  var (
    20  	// These are slices of all plugins that have made themselves
    21  	// visible to the Evergreen system. A Plugin can add itself by appending an instance
    22  	// of itself to these slices on init, i.e. by adding the following to its
    23  	// source file:
    24  	//  func init(){
    25  	//	plugin.Publish(&MyCoolPlugin{})
    26  	//  }
    27  	// This list is then used by Agent, API, and UI Server code to register
    28  	// the published plugins.
    29  	CommandPlugins []CommandPlugin
    30  	APIPlugins     []APIPlugin
    31  	UIPlugins      []UIPlugin
    32  )
    33  
    34  // Registry manages available plugins, and produces instances of
    35  // Commands from model.PluginCommandConf, a command's representation in the config.
    36  type Registry interface {
    37  	// Make the given plugin available for usage with tasks.
    38  	// Returns an error if the plugin is invalid, or conflicts with an already
    39  	// registered plugin.
    40  	Register(p CommandPlugin) error
    41  
    42  	// Parse the parameters in the given command and return a corresponding
    43  	// Command. Returns ErrUnknownPlugin if the command refers to
    44  	// a plugin that isn't registered, or some other error if the plugin
    45  	// can't parse valid parameters from the command.
    46  	GetCommands(command model.PluginCommandConf,
    47  		funcs map[string]*model.YAMLCommandSet) ([]Command, error)
    48  
    49  	// ParseCommandConf takes a plugin command and either returns a list of
    50  	// command(s) defined by the function (if the plugin command is a function),
    51  	// or a list containing the command itself otherwise.
    52  	ParseCommandConf(command model.PluginCommandConf,
    53  		funcs map[string]*model.YAMLCommandSet) ([]model.PluginCommandConf, error)
    54  }
    55  
    56  // Logger allows any plugin to log to the appropriate place with any
    57  // The agent (which provides each plugin execution with a Logger implementation)
    58  // handles sending log data to the remote server
    59  type Logger interface {
    60  	// Log a message locally. Will be persisted in the log file on the builder, but
    61  	// not appended to the log data sent to API server.
    62  	LogLocal(level slogger.Level, messageFmt string, args ...interface{})
    63  
    64  	// Log data about the plugin's execution.
    65  	LogExecution(level slogger.Level, messageFmt string, args ...interface{})
    66  
    67  	// Log data from the plugin's actual commands, e.g. shell script output or
    68  	// stdout/stderr messages from a command
    69  	LogTask(level slogger.Level, messageFmt string, args ...interface{})
    70  
    71  	// Log system-level information (resource usage, ), etc.
    72  	LogSystem(level slogger.Level, messageFmt string, args ...interface{})
    73  
    74  	// Returns the task log writer as an io.Writer, for use in io-related
    75  	// functions, e.g. io.Copy
    76  	GetTaskLogWriter(level slogger.Level) io.Writer
    77  
    78  	// Returns the system log writer as an io.Writer, for use in io-related
    79  	// functions, e.g. io.Copy
    80  	GetSystemLogWriter(level slogger.Level) io.Writer
    81  
    82  	// Trigger immediate flushing of any buffered log messages.
    83  	Flush()
    84  }
    85  
    86  // PluginCommunicator is an interface that allows a plugin's client-side processing
    87  // (running inside an agent) to communicate with the routes it has installed
    88  // on the server-side via HTTP GET and POST requests.
    89  // Does not handle retrying of requests. The caller must also ensure that
    90  // the Body of the returned http responses are closed.
    91  type PluginCommunicator interface {
    92  
    93  	// Make a POST request to the given endpoint by submitting 'data' as
    94  	// the request body, marshaled as JSON.
    95  	TaskPostJSON(endpoint string, data interface{}) (*http.Response, error)
    96  
    97  	// Make a GET request to the given endpoint with content type "application/json"
    98  	TaskGetJSON(endpoint string) (*http.Response, error)
    99  
   100  	// Make a POST request against the results api endpoint
   101  	TaskPostResults(results *task.TestResults) error
   102  
   103  	// Make a POST request against the test_log api endpoint
   104  	TaskPostTestLog(log *model.TestLog) (string, error)
   105  
   106  	// Make a POST request against the files api endpoint
   107  	PostTaskFiles(files []*artifact.File) error
   108  }
   109  
   110  // Plugin defines the interface that all evergreen plugins must implement in order
   111  // to register themselves with Evergreen. A plugin must also implement one of the
   112  // PluginCommand, APIPlugin, or UIPlugin interfaces in order to do useful work.
   113  type Plugin interface {
   114  	// Returns the name to identify this plugin when registered.
   115  	Name() string
   116  }
   117  
   118  // CommandPlugin is implemented by plugins that add new task commands
   119  // that are run by the agent.
   120  type CommandPlugin interface {
   121  	Plugin
   122  
   123  	// Returns an ErrUnknownCommand if no such command exists
   124  	NewCommand(commandName string) (Command, error)
   125  }
   126  
   127  // APIPlugin is implemented by plugins that need to add new API hooks for
   128  // new task commands.
   129  // TODO: should this also require PluginCommand be implemented?
   130  type APIPlugin interface {
   131  	Plugin
   132  
   133  	// Configure reads in a settings map from the Evergreen config file.
   134  	Configure(conf map[string]interface{}) error
   135  
   136  	// Install any server-side handlers needed by this plugin in the API server
   137  	GetAPIHandler() http.Handler
   138  }
   139  
   140  type UIPlugin interface {
   141  	Plugin
   142  
   143  	// Install any server-side handlers needed by this plugin in the UI server
   144  	GetUIHandler() http.Handler
   145  
   146  	// GetPanelConfig returns a pointer to a plugin's UI configuration.
   147  	// or an error, if an error occur while trying to generate the config
   148  	// A nil pointer represents a plugin without a UI presence, and is
   149  	// not an error.
   150  	GetPanelConfig() (*PanelConfig, error)
   151  
   152  	// Configure reads in a settings map from the Evergreen config file.
   153  	Configure(conf map[string]interface{}) error
   154  }
   155  
   156  // AppUIPlugin represents a UIPlugin that also has a page route.
   157  type AppUIPlugin interface {
   158  	UIPlugin
   159  
   160  	// GetAppPluginInfo returns all the information
   161  	// needed for the UI server to render a page from the navigation bar.
   162  	GetAppPluginInfo() *UIPage
   163  }
   164  
   165  // Publish is called in a plugin's "init" func to
   166  // announce that plugin's presence to the entire plugin package.
   167  // This architecture is designed to make it easy to add
   168  // new external plugin code to Evergreen by simply importing the
   169  // new plugin's package in plugin/config/installed_plugins.go
   170  //
   171  // Packages implementing the Plugin interface MUST call Publish in their
   172  // init code in order for Evergreen to detect and use them. A plugin must
   173  // also implement one of CommandPlugin, APIPlugin, or UIPlugin in order to
   174  // be useable.
   175  //
   176  // See the documentation of the 10gen.com/mci/plugin/config package for more
   177  func Publish(plugin Plugin) {
   178  	published := false
   179  	if asCommand, ok := plugin.(CommandPlugin); ok {
   180  		CommandPlugins = append(CommandPlugins, asCommand)
   181  		published = true
   182  	}
   183  	if asAPI, ok := plugin.(APIPlugin); ok {
   184  		APIPlugins = append(APIPlugins, asAPI)
   185  		published = true
   186  	}
   187  	if asUI, ok := plugin.(UIPlugin); ok {
   188  		UIPlugins = append(UIPlugins, asUI)
   189  		published = true
   190  	}
   191  	if !published {
   192  		panic(fmt.Sprintf("Plugin '%v' does not implement any of CommandPlugin, APIPlugin, or UIPlugin", plugin.Name()))
   193  	}
   194  }
   195  
   196  // ErrUnknownPlugin indicates a plugin was requested that is not registered in the plugin manager.
   197  type ErrUnknownPlugin struct {
   198  	PluginName string
   199  }
   200  
   201  // Error returns information about the non-registered plugin;
   202  // satisfies the error interface
   203  func (eup *ErrUnknownPlugin) Error() string {
   204  	return fmt.Sprintf("Unknown plugin: '%v'", eup.PluginName)
   205  }
   206  
   207  // ErrUnknownCommand indicates a command is referenced from a plugin that does not support it.
   208  type ErrUnknownCommand struct {
   209  	CommandName string
   210  }
   211  
   212  func (eup *ErrUnknownCommand) Error() string {
   213  	return fmt.Sprintf("Unknown command: '%v'", eup.CommandName)
   214  }
   215  
   216  // WriteJSON writes data encoded in JSON format (Content-type: "application/json")
   217  // to the ResponseWriter with the supplied HTTP status code. Writes a 500 error
   218  // if the data cannot be JSON-encoded.
   219  func WriteJSON(w http.ResponseWriter, statusCode int, data interface{}) {
   220  	out, err := json.MarshalIndent(data, "", "  ")
   221  	if err != nil {
   222  		http.Error(w, err.Error(), http.StatusInternalServerError)
   223  		return
   224  	}
   225  
   226  	w.Header().Set("Content-Type", "application/json")
   227  	w.WriteHeader(statusCode)
   228  	_, err = w.Write(out)
   229  	grip.Warning(errors.WithStack(err))
   230  }
   231  
   232  type pluginTaskContext int
   233  
   234  const pluginTaskContextKey pluginTaskContext = 0
   235  
   236  // SetTask puts the task for an API request into the context of a request.
   237  // This task can be retrieved in a handler function by using "GetTask()"
   238  func SetTask(request *http.Request, task *task.Task) {
   239  	context.Set(request, pluginTaskContextKey, task)
   240  }
   241  
   242  // GetTask returns the task object for a plugin API request at runtime,
   243  // it is a valuable helper function for API PluginRoute handlers.
   244  func GetTask(request *http.Request) *task.Task {
   245  	if rv := context.Get(request, pluginTaskContextKey); rv != nil {
   246  		return rv.(*task.Task)
   247  	}
   248  	return nil
   249  }
   250  
   251  // SimpleRegistry is a simple, local, map-based implementation
   252  // of a plugin registry.
   253  type SimpleRegistry struct {
   254  	pluginsMapping map[string]CommandPlugin
   255  }
   256  
   257  // NewSimpleRegistry returns an initialized SimpleRegistry
   258  func NewSimpleRegistry() *SimpleRegistry {
   259  	registry := &SimpleRegistry{
   260  		pluginsMapping: map[string]CommandPlugin{},
   261  	}
   262  	return registry
   263  }
   264  
   265  // Register makes a given plugin and its commands available to the agent code.
   266  // This function returns an error if a plugin of the same name is already registered.
   267  func (sr *SimpleRegistry) Register(p CommandPlugin) error {
   268  	if _, hasKey := sr.pluginsMapping[p.Name()]; hasKey {
   269  		return errors.Errorf("Plugin with name '%v' has already been registered", p.Name())
   270  	}
   271  	sr.pluginsMapping[p.Name()] = p
   272  	return nil
   273  }
   274  
   275  func (sr *SimpleRegistry) ParseCommandConf(cmd model.PluginCommandConf, funcs map[string]*model.YAMLCommandSet) ([]model.PluginCommandConf, error) {
   276  
   277  	if funcName := cmd.Function; funcName != "" {
   278  		cmds, ok := funcs[funcName]
   279  		if !ok {
   280  			return nil, errors.Errorf("function '%v' not found in project functions", funcName)
   281  		}
   282  
   283  		cmdList := cmds.List()
   284  
   285  		cmdsParsed := make([]model.PluginCommandConf, 0, len(cmdList))
   286  
   287  		for _, c := range cmdList {
   288  			if c.Function != "" {
   289  				return nil, errors.Errorf("can not reference a function within "+
   290  					"a function: '%v' referenced within '%v'", c.Function, funcName)
   291  			}
   292  
   293  			// if no command specific type, use the function's command type
   294  			if c.Type == "" {
   295  				c.Type = cmd.Type
   296  			}
   297  
   298  			// use function name if no command display name exists
   299  			if c.DisplayName == "" {
   300  				c.DisplayName = fmt.Sprintf(`'%v' in "%v"`, c.Command, funcName)
   301  			}
   302  
   303  			cmdsParsed = append(cmdsParsed, c)
   304  		}
   305  
   306  		return cmdsParsed, nil
   307  	}
   308  
   309  	return []model.PluginCommandConf{cmd}, nil
   310  }
   311  
   312  // GetCommands finds a registered plugin for the given plugin command config
   313  // Returns ErrUnknownPlugin if the cmd refers to a plugin that isn't registered,
   314  // or some other error if the plugin can't parse valid parameters from the conf.
   315  func (sr *SimpleRegistry) GetCommands(cmd model.PluginCommandConf, funcs map[string]*model.YAMLCommandSet) ([]Command, error) {
   316  
   317  	cmds, err := sr.ParseCommandConf(cmd, funcs)
   318  	if err != nil {
   319  		return nil, err
   320  	}
   321  
   322  	cmdsParsed := make([]Command, 0, len(cmds))
   323  
   324  	for _, c := range cmds {
   325  		pluginNameParts := strings.Split(c.Command, ".")
   326  		if len(pluginNameParts) != 2 {
   327  			return nil, errors.New("Value of 'command' should be formatted: 'plugin_name.command_name'")
   328  		}
   329  		plugin, hasKey := sr.pluginsMapping[pluginNameParts[0]]
   330  		if !hasKey {
   331  			return nil, &ErrUnknownPlugin{pluginNameParts[0]}
   332  		}
   333  
   334  		command, err := plugin.NewCommand(pluginNameParts[1])
   335  		if err != nil {
   336  			return nil, errors.WithStack(err)
   337  		}
   338  
   339  		if err = command.ParseParams(c.Params); err != nil {
   340  			return nil, errors.WithStack(err)
   341  		}
   342  		cmdsParsed = append(cmdsParsed, command)
   343  	}
   344  	return cmdsParsed, nil
   345  }
   346  
   347  // Command is an interface that defines a command for a plugin.
   348  // A Command takes parameters as a map, and is executed after
   349  // those parameters are parsed.
   350  type Command interface {
   351  	// ParseParams takes a map of fields to values extracted from
   352  	// the project config and passes them to the command. Any
   353  	// errors parsing the information are returned.
   354  	ParseParams(params map[string]interface{}) error
   355  
   356  	// Execute runs the command using the agent's logger, communicator,
   357  	// task config, and a channel for interrupting long-running commands.
   358  	// Execute is called after ParseParams.
   359  	Execute(logger Logger, pluginCom PluginCommunicator,
   360  		conf *model.TaskConfig, stopChan chan bool) error
   361  
   362  	// A string name for the command
   363  	Name() string
   364  
   365  	// Plugin name
   366  	Plugin() string
   367  }