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