github.com/hernad/nomad@v1.6.112/helper/pluginutils/loader/loader.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/exec" 10 11 log "github.com/hashicorp/go-hclog" 12 plugin "github.com/hashicorp/go-plugin" 13 version "github.com/hashicorp/go-version" 14 "github.com/hernad/nomad/nomad/structs/config" 15 "github.com/hernad/nomad/plugins" 16 "github.com/hernad/nomad/plugins/base" 17 "github.com/hernad/nomad/plugins/device" 18 "github.com/hernad/nomad/plugins/drivers" 19 "github.com/hernad/nomad/plugins/shared/hclspec" 20 ) 21 22 // PluginCatalog is used to retrieve plugins, either external or internal 23 type PluginCatalog interface { 24 // Dispense returns the plugin given its name and type. This will also 25 // configure the plugin 26 Dispense(name, pluginType string, config *base.AgentConfig, logger log.Logger) (PluginInstance, error) 27 28 // Reattach is used to reattach to a previously launched external plugin. 29 Reattach(name, pluginType string, config *plugin.ReattachConfig) (PluginInstance, error) 30 31 // Catalog returns the catalog of all plugins keyed by plugin type 32 Catalog() map[string][]*base.PluginInfoResponse 33 } 34 35 // InternalPluginConfig is used to configure launching an internal plugin. 36 type InternalPluginConfig struct { 37 Config map[string]interface{} 38 Factory plugins.PluginCtxFactory 39 } 40 41 // PluginID is a tuple identifying a plugin 42 type PluginID struct { 43 // Name is the name of the plugin 44 Name string 45 46 // PluginType is the plugin's type 47 PluginType string 48 } 49 50 // String returns a friendly representation of the plugin. 51 func (id PluginID) String() string { 52 return fmt.Sprintf("%q (%v)", id.Name, id.PluginType) 53 } 54 55 func PluginInfoID(resp *base.PluginInfoResponse) PluginID { 56 return PluginID{ 57 Name: resp.Name, 58 PluginType: resp.Type, 59 } 60 } 61 62 // PluginLoaderConfig configures a plugin loader. 63 type PluginLoaderConfig struct { 64 // Logger is the logger used by the plugin loader 65 Logger log.Logger 66 67 // PluginDir is the directory scanned for loading plugins 68 PluginDir string 69 70 // Configs is an optional set of configs for plugins 71 Configs []*config.PluginConfig 72 73 // InternalPlugins allows registering internal plugins. 74 InternalPlugins map[PluginID]*InternalPluginConfig 75 76 // SupportedVersions is a mapping of plugin type to the supported versions 77 SupportedVersions map[string][]string 78 } 79 80 // PluginLoader is used to retrieve plugins either externally or from internal 81 // factories. 82 type PluginLoader struct { 83 // logger is the plugin loaders logger 84 logger log.Logger 85 86 // supportedVersions is a mapping of plugin type to the supported versions 87 supportedVersions map[string][]*version.Version 88 89 // pluginDir is the directory containing plugin binaries 90 pluginDir string 91 92 // plugins maps a plugin to information required to launch it 93 plugins map[PluginID]*pluginInfo 94 } 95 96 // pluginInfo captures the necessary information to launch and configure a 97 // plugin. 98 type pluginInfo struct { 99 factory plugins.PluginCtxFactory 100 101 exePath string 102 args []string 103 104 baseInfo *base.PluginInfoResponse 105 version *version.Version 106 apiVersion string 107 108 configSchema *hclspec.Spec 109 config map[string]interface{} 110 msgpackConfig []byte 111 } 112 113 // NewPluginLoader returns an instance of a plugin loader or an error if the 114 // plugins could not be loaded 115 func NewPluginLoader(config *PluginLoaderConfig) (*PluginLoader, error) { 116 if err := validateConfig(config); err != nil { 117 return nil, fmt.Errorf("invalid plugin loader configuration passed: %v", err) 118 } 119 120 // Convert the versions 121 supportedVersions := make(map[string][]*version.Version, len(config.SupportedVersions)) 122 for pType, versions := range config.SupportedVersions { 123 converted, err := convertVersions(versions) 124 if err != nil { 125 return nil, err 126 } 127 supportedVersions[pType] = converted 128 } 129 130 logger := config.Logger.Named("plugin_loader").With("plugin_dir", config.PluginDir) 131 l := &PluginLoader{ 132 logger: logger, 133 supportedVersions: supportedVersions, 134 pluginDir: config.PluginDir, 135 plugins: make(map[PluginID]*pluginInfo), 136 } 137 138 if err := l.init(config); err != nil { 139 return nil, fmt.Errorf("failed to initialize plugin loader: %v", err) 140 } 141 142 return l, nil 143 } 144 145 // Dispense returns a plugin instance, loading it either internally or by 146 // launching an external plugin. 147 func (l *PluginLoader) Dispense(name, pluginType string, config *base.AgentConfig, logger log.Logger) (PluginInstance, error) { 148 id := PluginID{ 149 Name: name, 150 PluginType: pluginType, 151 } 152 pinfo, ok := l.plugins[id] 153 if !ok { 154 return nil, fmt.Errorf("unknown plugin with name %q and type %q", name, pluginType) 155 } 156 157 // If the plugin is internal, launch via the factory 158 var instance PluginInstance 159 if pinfo.factory != nil { 160 ctx, cancel := context.WithCancel(context.Background()) 161 instance = &internalPluginInstance{ 162 instance: pinfo.factory(ctx, logger), 163 apiVersion: pinfo.apiVersion, 164 killFn: cancel, 165 } 166 } else { 167 var err error 168 instance, err = l.dispensePlugin(pinfo.baseInfo.Type, pinfo.apiVersion, pinfo.exePath, pinfo.args, nil, logger) 169 if err != nil { 170 return nil, fmt.Errorf("failed to launch plugin: %v", err) 171 } 172 } 173 174 // Cast to the base type and set the config 175 b, ok := instance.Plugin().(base.BasePlugin) 176 if !ok { 177 return nil, fmt.Errorf("plugin %s doesn't implement base plugin interface", id) 178 } 179 180 c := &base.Config{ 181 PluginConfig: pinfo.msgpackConfig, 182 AgentConfig: config, 183 ApiVersion: pinfo.apiVersion, 184 } 185 186 if err := b.SetConfig(c); err != nil { 187 return nil, fmt.Errorf("setting config for plugin %s failed: %v", id, err) 188 } 189 190 return instance, nil 191 } 192 193 // Reattach reattaches to a previously launched external plugin. 194 func (l *PluginLoader) Reattach(name, pluginType string, config *plugin.ReattachConfig) (PluginInstance, error) { 195 return l.dispensePlugin(pluginType, "", "", nil, config, l.logger) 196 } 197 198 // dispensePlugin is used to launch or reattach to an external plugin. 199 func (l *PluginLoader) dispensePlugin( 200 pluginType, apiVersion, cmd string, args []string, reattach *plugin.ReattachConfig, 201 logger log.Logger) (PluginInstance, error) { 202 203 var pluginCmd *exec.Cmd 204 if cmd != "" && reattach != nil { 205 return nil, fmt.Errorf("both launch command and reattach config specified") 206 } else if cmd == "" && reattach == nil { 207 return nil, fmt.Errorf("one of launch command or reattach config must be specified") 208 } else if cmd != "" { 209 pluginCmd = exec.Command(cmd, args...) 210 } 211 212 client := plugin.NewClient(&plugin.ClientConfig{ 213 HandshakeConfig: base.Handshake, 214 Plugins: getPluginMap(pluginType, logger), 215 Cmd: pluginCmd, 216 AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, 217 Logger: logger, 218 Reattach: reattach, 219 }) 220 221 // Connect via RPC 222 rpcClient, err := client.Client() 223 if err != nil { 224 client.Kill() 225 return nil, err 226 } 227 228 // Request the plugin 229 raw, err := rpcClient.Dispense(pluginType) 230 if err != nil { 231 client.Kill() 232 return nil, err 233 } 234 235 instance := &externalPluginInstance{ 236 client: client, 237 instance: raw, 238 } 239 240 if apiVersion != "" { 241 instance.apiVersion = apiVersion 242 } else { 243 // We do not know the API version since we are reattaching, so discover 244 // it 245 bplugin := raw.(base.BasePlugin) 246 247 // Retrieve base plugin information 248 i, err := bplugin.PluginInfo() 249 if err != nil { 250 return nil, fmt.Errorf("failed to get plugin info for plugin: %v", err) 251 } 252 253 apiVersion, err := l.selectApiVersion(i) 254 if err != nil { 255 return nil, fmt.Errorf("failed to validate API versions %v for plugin %s: %v", i.PluginApiVersions, i.Name, err) 256 } 257 if apiVersion == "" { 258 return nil, fmt.Errorf("failed to reattach to plugin because supported API versions for the plugin and Nomad do not overlap") 259 } 260 261 instance.apiVersion = apiVersion 262 } 263 264 return instance, nil 265 } 266 267 // getPluginMap returns a plugin map based on the type of plugin being launched. 268 func getPluginMap(pluginType string, logger log.Logger) map[string]plugin.Plugin { 269 pmap := map[string]plugin.Plugin{ 270 base.PluginTypeBase: &base.PluginBase{}, 271 } 272 273 switch pluginType { 274 case base.PluginTypeDevice: 275 pmap[base.PluginTypeDevice] = &device.PluginDevice{} 276 case base.PluginTypeDriver: 277 pmap[base.PluginTypeDriver] = drivers.NewDriverPlugin(nil, logger) 278 } 279 280 return pmap 281 } 282 283 // Catalog returns the catalog of all plugins 284 func (l *PluginLoader) Catalog() map[string][]*base.PluginInfoResponse { 285 c := make(map[string][]*base.PluginInfoResponse, 3) 286 for id, info := range l.plugins { 287 c[id.PluginType] = append(c[id.PluginType], info.baseInfo) 288 } 289 return c 290 }