github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/helper/pluginutils/loader/init.go (about) 1 package loader 2 3 import ( 4 "fmt" 5 "os" 6 "os/exec" 7 "path/filepath" 8 "sort" 9 10 multierror "github.com/hashicorp/go-multierror" 11 plugin "github.com/hashicorp/go-plugin" 12 version "github.com/hashicorp/go-version" 13 "github.com/hashicorp/nomad/helper/pluginutils/hclspecutils" 14 "github.com/hashicorp/nomad/helper/pluginutils/hclutils" 15 "github.com/hashicorp/nomad/nomad/structs/config" 16 "github.com/hashicorp/nomad/plugins/base" 17 "github.com/zclconf/go-cty/cty/msgpack" 18 ) 19 20 // validateConfig returns whether or not the configuration is valid 21 func validateConfig(config *PluginLoaderConfig) error { 22 var mErr multierror.Error 23 if config == nil { 24 return fmt.Errorf("nil config passed") 25 } else if config.Logger == nil { 26 multierror.Append(&mErr, fmt.Errorf("nil logger passed")) 27 } 28 29 // Validate that all plugins have a binary name 30 for _, c := range config.Configs { 31 if c.Name == "" { 32 multierror.Append(&mErr, fmt.Errorf("plugin config passed without binary name")) 33 } 34 } 35 36 // Validate internal plugins 37 for k, config := range config.InternalPlugins { 38 // Validate config 39 if config == nil { 40 multierror.Append(&mErr, fmt.Errorf("nil config passed for internal plugin %s", k)) 41 continue 42 } else if config.Factory == nil { 43 multierror.Append(&mErr, fmt.Errorf("nil factory passed for internal plugin %s", k)) 44 continue 45 } 46 } 47 48 return mErr.ErrorOrNil() 49 } 50 51 // init initializes the plugin loader by compiling both internal and external 52 // plugins and selecting the highest versioned version of any given plugin. 53 func (l *PluginLoader) init(config *PluginLoaderConfig) error { 54 // Create a mapping of name to config 55 configMap := configMap(config.Configs) 56 57 // Initialize the internal plugins 58 internal, err := l.initInternal(config.InternalPlugins, configMap) 59 if err != nil { 60 return fmt.Errorf("failed to fingerprint internal plugins: %v", err) 61 } 62 63 // Scan for eligibile binaries 64 plugins, err := l.scan() 65 if err != nil { 66 return fmt.Errorf("failed to scan plugin directory %q: %v", l.pluginDir, err) 67 } 68 69 // Fingerprint the passed plugins 70 external, err := l.fingerprintPlugins(plugins, configMap) 71 if err != nil { 72 return fmt.Errorf("failed to fingerprint plugins: %v", err) 73 } 74 75 // Merge external and internal plugins 76 l.plugins = l.mergePlugins(internal, external) 77 78 // Validate that the configs are valid for the plugins 79 if err := l.validatePluginConfigs(); err != nil { 80 return fmt.Errorf("parsing plugin configurations failed: %v", err) 81 } 82 83 return nil 84 } 85 86 // initInternal initializes internal plugins. 87 func (l *PluginLoader) initInternal(plugins map[PluginID]*InternalPluginConfig, configs map[string]*config.PluginConfig) (map[PluginID]*pluginInfo, error) { 88 var mErr multierror.Error 89 fingerprinted := make(map[PluginID]*pluginInfo, len(plugins)) 90 for k, config := range plugins { 91 // Create an instance 92 raw := config.Factory(l.logger) 93 base, ok := raw.(base.BasePlugin) 94 if !ok { 95 multierror.Append(&mErr, fmt.Errorf("internal plugin %s doesn't meet base plugin interface", k)) 96 continue 97 } 98 99 info := &pluginInfo{ 100 factory: config.Factory, 101 config: config.Config, 102 } 103 104 // Try to retrieve a user specified config 105 if userConfig, ok := configs[k.Name]; ok && userConfig.Config != nil { 106 info.config = userConfig.Config 107 } 108 109 // Fingerprint base info 110 i, err := base.PluginInfo() 111 if err != nil { 112 multierror.Append(&mErr, fmt.Errorf("PluginInfo info failed for internal plugin %s: %v", k, err)) 113 continue 114 } 115 info.baseInfo = i 116 117 // Parse and set the plugin version 118 v, err := version.NewVersion(i.PluginVersion) 119 if err != nil { 120 multierror.Append(&mErr, fmt.Errorf("failed to parse version %q for internal plugin %s: %v", i.PluginVersion, k, err)) 121 continue 122 } 123 info.version = v 124 125 // Detect the plugin API version to use 126 av, err := l.selectApiVersion(i) 127 if err != nil { 128 multierror.Append(&mErr, fmt.Errorf("failed to validate API versions %v for internal plugin %s: %v", i.PluginApiVersions, k, err)) 129 continue 130 } 131 if av == "" { 132 l.logger.Warn("skipping plugin because supported API versions for plugin and Nomad do not overlap", "plugin", k) 133 continue 134 } 135 info.apiVersion = av 136 137 // Get the config schema 138 schema, err := base.ConfigSchema() 139 if err != nil { 140 multierror.Append(&mErr, fmt.Errorf("failed to retrieve config schema for internal plugin %s: %v", k, err)) 141 continue 142 } 143 info.configSchema = schema 144 145 // Store the fingerprinted config 146 fingerprinted[k] = info 147 } 148 149 if err := mErr.ErrorOrNil(); err != nil { 150 return nil, err 151 } 152 153 return fingerprinted, nil 154 } 155 156 // selectApiVersion takes in PluginInfo and returns the highest compatable 157 // version or an error if the plugins response is malformed. If there is no 158 // overlap, an empty string is returned. 159 func (l *PluginLoader) selectApiVersion(i *base.PluginInfoResponse) (string, error) { 160 if i == nil { 161 return "", fmt.Errorf("nil plugin info given") 162 } 163 if len(i.PluginApiVersions) == 0 { 164 return "", fmt.Errorf("plugin provided no compatible API versions") 165 } 166 167 pluginVersions, err := convertVersions(i.PluginApiVersions) 168 if err != nil { 169 return "", fmt.Errorf("plugin provided invalid versions: %v", err) 170 } 171 172 // Lookup the supported versions. These will be sorted highest to lowest 173 supportedVersions, ok := l.supportedVersions[i.Type] 174 if !ok { 175 return "", fmt.Errorf("unsupported plugin type %q", i.Type) 176 } 177 178 for _, sv := range supportedVersions { 179 for _, pv := range pluginVersions { 180 if sv.Equal(pv) { 181 return pv.Original(), nil 182 } 183 } 184 } 185 186 return "", nil 187 } 188 189 // convertVersions takes a list of string versions and returns a sorted list of 190 // versions from highest to lowest. 191 func convertVersions(in []string) ([]*version.Version, error) { 192 converted := make([]*version.Version, len(in)) 193 for i, v := range in { 194 vv, err := version.NewVersion(v) 195 if err != nil { 196 return nil, fmt.Errorf("failed to convert version %q : %v", v, err) 197 } 198 199 converted[i] = vv 200 } 201 202 sort.Slice(converted, func(i, j int) bool { 203 return converted[i].GreaterThan(converted[j]) 204 }) 205 206 return converted, nil 207 } 208 209 // scan scans the plugin directory and retrieves potentially eligible binaries 210 func (l *PluginLoader) scan() ([]os.FileInfo, error) { 211 if l.pluginDir == "" { 212 return nil, nil 213 } 214 215 // Capture the list of binaries in the plugins folder 216 f, err := os.Open(l.pluginDir) 217 if err != nil { 218 // There are no plugins to scan 219 if os.IsNotExist(err) { 220 l.logger.Warn("skipping external plugins since plugin_dir doesn't exist") 221 return nil, nil 222 } 223 224 return nil, fmt.Errorf("failed to open plugin directory %q: %v", l.pluginDir, err) 225 } 226 files, err := f.Readdirnames(-1) 227 f.Close() 228 if err != nil { 229 return nil, fmt.Errorf("failed to read plugin directory %q: %v", l.pluginDir, err) 230 } 231 232 var plugins []os.FileInfo 233 for _, f := range files { 234 f = filepath.Join(l.pluginDir, f) 235 s, err := os.Stat(f) 236 if err != nil { 237 return nil, fmt.Errorf("failed to stat file %q: %v", f, err) 238 } 239 if s.IsDir() { 240 l.logger.Warn("skipping subdir in plugin folder", "subdir", f) 241 continue 242 } 243 244 if !executable(f, s) { 245 l.logger.Warn("skipping un-executable file in plugin folder", "file", f) 246 continue 247 } 248 plugins = append(plugins, s) 249 } 250 251 return plugins, nil 252 } 253 254 // fingerprintPlugins fingerprints all external plugin binaries 255 func (l *PluginLoader) fingerprintPlugins(plugins []os.FileInfo, configs map[string]*config.PluginConfig) (map[PluginID]*pluginInfo, error) { 256 var mErr multierror.Error 257 fingerprinted := make(map[PluginID]*pluginInfo, len(plugins)) 258 for _, p := range plugins { 259 name := cleanPluginExecutable(p.Name()) 260 c := configs[name] 261 info, err := l.fingerprintPlugin(p, c) 262 if err != nil { 263 l.logger.Error("failed to fingerprint plugin", "plugin", name, "error", err) 264 multierror.Append(&mErr, err) 265 continue 266 } 267 if info == nil { 268 // Plugin was skipped for validation reasons 269 continue 270 } 271 272 id := PluginID{ 273 Name: info.baseInfo.Name, 274 PluginType: info.baseInfo.Type, 275 } 276 277 // Detect if we already have seen a version of this plugin 278 if prev, ok := fingerprinted[id]; ok { 279 oldVersion := prev.version 280 selectedVersion := info.version 281 skip := false 282 283 // Determine if we should keep the previous version or override 284 if prev.version.GreaterThan(info.version) { 285 oldVersion = info.version 286 selectedVersion = prev.version 287 skip = true 288 } 289 l.logger.Info("multiple versions of plugin detected", 290 "plugin", info.baseInfo.Name, "older_version", oldVersion, "selected_version", selectedVersion) 291 292 if skip { 293 continue 294 } 295 } 296 297 // Add the plugin 298 fingerprinted[id] = info 299 } 300 301 if err := mErr.ErrorOrNil(); err != nil { 302 return nil, err 303 } 304 305 return fingerprinted, nil 306 } 307 308 // fingerprintPlugin fingerprints the passed external plugin 309 func (l *PluginLoader) fingerprintPlugin(pluginExe os.FileInfo, config *config.PluginConfig) (*pluginInfo, error) { 310 info := &pluginInfo{ 311 exePath: filepath.Join(l.pluginDir, pluginExe.Name()), 312 } 313 314 // Build the command 315 cmd := exec.Command(info.exePath) 316 if config != nil { 317 cmd.Args = append(cmd.Args, config.Args...) 318 info.args = config.Args 319 info.config = config.Config 320 } 321 322 // Launch the plugin 323 client := plugin.NewClient(&plugin.ClientConfig{ 324 HandshakeConfig: base.Handshake, 325 Plugins: map[string]plugin.Plugin{ 326 base.PluginTypeBase: &base.PluginBase{}, 327 }, 328 Cmd: cmd, 329 AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, 330 Logger: l.logger, 331 }) 332 defer client.Kill() 333 334 // Connect via RPC 335 rpcClient, err := client.Client() 336 if err != nil { 337 return nil, err 338 } 339 340 // Request the plugin 341 raw, err := rpcClient.Dispense(base.PluginTypeBase) 342 if err != nil { 343 return nil, err 344 } 345 346 // Cast the plugin to the base type 347 bplugin := raw.(base.BasePlugin) 348 349 // Retrieve base plugin information 350 i, err := bplugin.PluginInfo() 351 if err != nil { 352 return nil, fmt.Errorf("failed to get plugin info for plugin %q: %v", info.exePath, err) 353 } 354 info.baseInfo = i 355 356 // Parse and set the plugin version 357 v, err := version.NewVersion(i.PluginVersion) 358 if err != nil { 359 return nil, fmt.Errorf("failed to parse plugin %q (%v) version %q: %v", 360 i.Name, info.exePath, i.PluginVersion, err) 361 } 362 info.version = v 363 364 // Detect the plugin API version to use 365 av, err := l.selectApiVersion(i) 366 if err != nil { 367 return nil, fmt.Errorf("failed to validate API versions %v for plugin %s (%v): %v", i.PluginApiVersions, i.Name, info.exePath, err) 368 } 369 if av == "" { 370 l.logger.Warn("skipping plugin because supported API versions for plugin and Nomad do not overlap", "plugin", i.Name, "path", info.exePath) 371 return nil, nil 372 } 373 info.apiVersion = av 374 375 // Retrieve the schema 376 schema, err := bplugin.ConfigSchema() 377 if err != nil { 378 return nil, fmt.Errorf("failed to get plugin config schema for plugin %q: %v", info.exePath, err) 379 } 380 info.configSchema = schema 381 382 return info, nil 383 } 384 385 // mergePlugins merges internal and external plugins, preferring the highest 386 // version. 387 func (l *PluginLoader) mergePlugins(internal, external map[PluginID]*pluginInfo) map[PluginID]*pluginInfo { 388 finalized := make(map[PluginID]*pluginInfo, len(internal)) 389 390 // Load the internal plugins 391 for k, v := range internal { 392 finalized[k] = v 393 } 394 395 for k, extPlugin := range external { 396 internal, ok := finalized[k] 397 if ok { 398 // We have overlapping plugins, determine if we should keep the 399 // internal version or override 400 if extPlugin.version.LessThan(internal.version) { 401 l.logger.Info("preferring internal version of plugin", 402 "plugin", extPlugin.baseInfo.Name, "internal_version", internal.version, 403 "external_version", extPlugin.version) 404 continue 405 } 406 } 407 408 // Add external plugin 409 finalized[k] = extPlugin 410 } 411 412 return finalized 413 } 414 415 // validatePluginConfigs is used to validate each plugins' configuration. If the 416 // plugin has a config, it is parsed with the plugins config schema and 417 // SetConfig is called to ensure the config is valid. 418 func (l *PluginLoader) validatePluginConfigs() error { 419 var mErr multierror.Error 420 for id, info := range l.plugins { 421 if err := l.validatePluginConfig(id, info); err != nil { 422 wrapped := multierror.Prefix(err, fmt.Sprintf("plugin %s:", id)) 423 multierror.Append(&mErr, wrapped) 424 } 425 } 426 427 return mErr.ErrorOrNil() 428 } 429 430 // validatePluginConfig is used to validate the plugin's configuration. If the 431 // plugin has a config, it is parsed with the plugins config schema and 432 // SetConfig is called to ensure the config is valid. 433 func (l *PluginLoader) validatePluginConfig(id PluginID, info *pluginInfo) error { 434 var mErr multierror.Error 435 436 // Check if a config is allowed 437 if info.configSchema == nil { 438 if info.config != nil { 439 return fmt.Errorf("configuration not allowed but config passed") 440 } 441 442 // Nothing to do 443 return nil 444 } 445 446 // Convert the schema to hcl 447 spec, diag := hclspecutils.Convert(info.configSchema) 448 if diag.HasErrors() { 449 multierror.Append(&mErr, diag.Errs()...) 450 return multierror.Prefix(&mErr, "failed converting config schema:") 451 } 452 453 // If there is no config, initialize it to an empty map so we can still 454 // handle defaults 455 if info.config == nil { 456 info.config = map[string]interface{}{} 457 } 458 459 // Parse the config using the spec 460 val, diag, diagErrs := hclutils.ParseHclInterface(info.config, spec, nil) 461 if diag.HasErrors() { 462 multierror.Append(&mErr, diagErrs...) 463 return multierror.Prefix(&mErr, "failed to parse config: ") 464 465 } 466 467 // Marshal the value 468 cdata, err := msgpack.Marshal(val, val.Type()) 469 if err != nil { 470 return fmt.Errorf("failed to msgpack encode config: %v", err) 471 } 472 473 // Store the marshalled config 474 info.msgpackConfig = cdata 475 476 // Dispense the plugin and set its config and ensure it is error free 477 instance, err := l.Dispense(id.Name, id.PluginType, nil, l.logger) 478 if err != nil { 479 return fmt.Errorf("failed to dispense plugin: %v", err) 480 } 481 defer instance.Kill() 482 483 b, ok := instance.Plugin().(base.BasePlugin) 484 if !ok { 485 return fmt.Errorf("dispensed plugin %s doesn't meet base plugin interface", id) 486 } 487 488 c := &base.Config{ 489 PluginConfig: cdata, 490 AgentConfig: nil, 491 ApiVersion: info.apiVersion, 492 } 493 494 if err := b.SetConfig(c); err != nil { 495 return fmt.Errorf("setting config on plugin failed: %v", err) 496 } 497 return nil 498 }