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 }