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