github.com/hashicorp/packer@v1.14.3/packer/plugin.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package packer 5 6 import ( 7 "crypto/sha256" 8 "fmt" 9 "log" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "regexp" 14 "runtime" 15 "strings" 16 "sync" 17 18 packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 19 pluginsdk "github.com/hashicorp/packer-plugin-sdk/plugin" 20 plugingetter "github.com/hashicorp/packer/packer/plugin-getter" 21 ) 22 23 // PluginConfig helps load and use packer plugins 24 type PluginConfig struct { 25 PluginDirectory string 26 PluginMinPort int 27 PluginMaxPort int 28 Builders BuilderSet 29 Provisioners ProvisionerSet 30 PostProcessors PostProcessorSet 31 DataSources DatasourceSet 32 ReleasesOnly bool 33 // UseProtobuf is set if all the plugin candidates support protobuf, and 34 // the user has not forced usage of gob for serialisation. 35 UseProtobuf bool 36 } 37 38 // PACKERSPACE is used to represent the spaces that separate args for a command 39 // without being confused with spaces in the path to the command itself. 40 const PACKERSPACE = "-PACKERSPACE-" 41 42 var extractPluginBasename = regexp.MustCompile("^packer-plugin-([^_]+)") 43 44 // Discover discovers the latest installed version of each installed plugin. 45 // 46 // Search the directory of the executable, then the plugins directory, and 47 // finally the CWD, in that order. Any conflicts will overwrite previously 48 // found plugins, in that order. 49 // Hence, the priority order is the reverse of the search order - i.e., the 50 // CWD has the highest priority. 51 func (c *PluginConfig) Discover() error { 52 if c.Builders == nil { 53 c.Builders = MapOfBuilder{} 54 } 55 if c.Provisioners == nil { 56 c.Provisioners = MapOfProvisioner{} 57 } 58 if c.PostProcessors == nil { 59 c.PostProcessors = MapOfPostProcessor{} 60 } 61 if c.DataSources == nil { 62 c.DataSources = MapOfDatasource{} 63 } 64 65 // If we are already inside a plugin process we should not need to 66 // discover anything. 67 if os.Getenv(pluginsdk.MagicCookieKey) == pluginsdk.MagicCookieValue { 68 return nil 69 } 70 71 if c.PluginDirectory == "" { 72 c.PluginDirectory, _ = PluginFolder() 73 } 74 75 ext := "" 76 if runtime.GOOS == "windows" { 77 ext = ".exe" 78 } 79 80 installations, err := plugingetter.Requirement{}.ListInstallations(plugingetter.ListInstallationsOptions{ 81 PluginDirectory: c.PluginDirectory, 82 BinaryInstallationOptions: plugingetter.BinaryInstallationOptions{ 83 OS: runtime.GOOS, 84 ARCH: runtime.GOARCH, 85 Ext: ext, 86 APIVersionMajor: pluginsdk.APIVersionMajor, 87 APIVersionMinor: pluginsdk.APIVersionMinor, 88 Checksummers: []plugingetter.Checksummer{ 89 {Type: "sha256", Hash: sha256.New()}, 90 }, 91 ReleasesOnly: c.ReleasesOnly, 92 }, 93 }) 94 if err != nil { 95 return err 96 } 97 98 // Map of plugin basename to executable 99 // 100 // We'll use that later to register the components for each plugin 101 pluginMap := map[string]string{} 102 for _, install := range installations { 103 pluginBasename := filepath.Base(install.BinaryPath) 104 matches := extractPluginBasename.FindStringSubmatch(pluginBasename) 105 if len(matches) != 2 { 106 log.Printf("[INFO] - plugin %q could not have its name matched, ignoring", pluginBasename) 107 continue 108 } 109 110 pluginName := matches[1] 111 pluginMap[pluginName] = install.BinaryPath 112 } 113 114 for name, path := range pluginMap { 115 err := c.DiscoverMultiPlugin(name, path) 116 if err != nil { 117 return err 118 } 119 } 120 121 return nil 122 } 123 124 const ForceGobEnvvar = "PACKER_FORCE_GOB" 125 126 var PackerUseProto = true 127 128 // DiscoverMultiPlugin takes the description from a multi-component plugin 129 // binary and makes the plugins available to use in Packer. Each plugin found in the 130 // binary will be addressable using `${pluginName}-${builderName}` for example. 131 // pluginName could be manually set. It usually is a cloud name like amazon. 132 // pluginName can be extrapolated from the filename of the binary; so 133 // if the "packer-plugin-amazon" binary had an "ebs" builder one could use 134 // the "amazon-ebs" builder. 135 func (c *PluginConfig) DiscoverMultiPlugin(pluginName, pluginPath string) error { 136 desc, err := plugingetter.GetPluginDescription(pluginPath) 137 if err != nil { 138 return fmt.Errorf("failed to get plugin description from executable %q: %s", pluginPath, err) 139 } 140 141 canProto := desc.ProtocolVersion == "v2" 142 if os.Getenv(ForceGobEnvvar) != "" && os.Getenv(ForceGobEnvvar) != "0" { 143 canProto = false 144 } 145 146 // Keeps track of whether or not the plugin had components registered 147 // 148 // If no components are registered, we don't need to clamp usage of 149 // protobuf regardless if the plugin supports it or not, as we won't 150 // use it at all. 151 registered := false 152 153 pluginPrefix := pluginName + "-" 154 pluginDetails := PluginDetails{ 155 Name: pluginName, 156 Description: desc, 157 PluginPath: pluginPath, 158 } 159 160 for _, builderName := range desc.Builders { 161 builderName := builderName // copy to avoid pointer overwrite issue 162 key := pluginPrefix + builderName 163 if builderName == pluginsdk.DEFAULT_NAME { 164 key = pluginName 165 } 166 if c.Builders.Has(key) { 167 continue 168 } 169 registered = true 170 171 c.Builders.Set(key, func() (packersdk.Builder, error) { 172 args := []string{"start", "builder"} 173 174 if PackerUseProto { 175 args = append(args, "--protobuf") 176 } 177 args = append(args, builderName) 178 179 return c.Client(pluginPath, args...).Builder() 180 }) 181 GlobalPluginsDetailsStore.SetBuilder(key, pluginDetails) 182 } 183 184 if len(desc.Builders) > 0 { 185 log.Printf("[INFO] found external %v builders from %s plugin", desc.Builders, pluginName) 186 } 187 188 for _, postProcessorName := range desc.PostProcessors { 189 postProcessorName := postProcessorName // copy to avoid pointer overwrite issue 190 key := pluginPrefix + postProcessorName 191 if postProcessorName == pluginsdk.DEFAULT_NAME { 192 key = pluginName 193 } 194 if c.PostProcessors.Has(key) { 195 continue 196 } 197 registered = true 198 199 c.PostProcessors.Set(key, func() (packersdk.PostProcessor, error) { 200 args := []string{"start", "post-processor"} 201 202 if PackerUseProto { 203 args = append(args, "--protobuf") 204 } 205 args = append(args, postProcessorName) 206 207 return c.Client(pluginPath, args...).PostProcessor() 208 }) 209 GlobalPluginsDetailsStore.SetPostProcessor(key, pluginDetails) 210 } 211 212 if len(desc.PostProcessors) > 0 { 213 log.Printf("[INFO] found external %v post-processors from %s plugin", desc.PostProcessors, pluginName) 214 } 215 216 for _, provisionerName := range desc.Provisioners { 217 provisionerName := provisionerName // copy to avoid pointer overwrite issue 218 key := pluginPrefix + provisionerName 219 if provisionerName == pluginsdk.DEFAULT_NAME { 220 key = pluginName 221 } 222 if c.Provisioners.Has(key) { 223 continue 224 } 225 registered = true 226 227 c.Provisioners.Set(key, func() (packersdk.Provisioner, error) { 228 args := []string{"start", "provisioner"} 229 230 if PackerUseProto { 231 args = append(args, "--protobuf") 232 } 233 args = append(args, provisionerName) 234 235 return c.Client(pluginPath, args...).Provisioner() 236 }) 237 GlobalPluginsDetailsStore.SetProvisioner(key, pluginDetails) 238 239 } 240 if len(desc.Provisioners) > 0 { 241 log.Printf("found external %v provisioner from %s plugin", desc.Provisioners, pluginName) 242 } 243 244 for _, datasourceName := range desc.Datasources { 245 datasourceName := datasourceName // copy to avoid pointer overwrite issue 246 key := pluginPrefix + datasourceName 247 if datasourceName == pluginsdk.DEFAULT_NAME { 248 key = pluginName 249 } 250 if c.DataSources.Has(key) { 251 continue 252 } 253 registered = true 254 255 c.DataSources.Set(key, func() (packersdk.Datasource, error) { 256 args := []string{"start", "datasource"} 257 258 if PackerUseProto { 259 args = append(args, "--protobuf") 260 } 261 args = append(args, datasourceName) 262 263 return c.Client(pluginPath, args...).Datasource() 264 }) 265 GlobalPluginsDetailsStore.SetDataSource(key, pluginDetails) 266 } 267 if len(desc.Datasources) > 0 { 268 log.Printf("found external %v datasource from %s plugin", desc.Datasources, pluginName) 269 } 270 271 // Only print the log once, for the plugin that triggers that 272 // limitation in functionality. Otherwise this could be a bit 273 // verbose to print it for each non-compatible plugin. 274 if registered && !canProto && PackerUseProto { 275 log.Printf("plugin %q does not support Protobuf, forcing use of Gob", pluginPath) 276 PackerUseProto = false 277 } 278 279 return nil 280 } 281 282 func (c *PluginConfig) Client(path string, args ...string) *PluginClient { 283 originalPath := path 284 285 // Check for special case using `packer plugin PLUGIN` 286 if strings.Contains(path, PACKERSPACE) { 287 parts := strings.Split(path, PACKERSPACE) 288 path = parts[0] 289 args = parts[1:] 290 } 291 292 // First attempt to find the executable by consulting the PATH. 293 path, err := exec.LookPath(path) 294 if err != nil { 295 // If that doesn't work, look for it in the same directory 296 // as the `packer` executable (us). 297 log.Printf("[INFO] exec.LookPath: %s : %v. Checking same directory as executable.", path, err) 298 exePath, err := os.Executable() 299 if err != nil { 300 log.Printf("Couldn't get current exe path: %s", err) 301 } else { 302 log.Printf("Current exe path: %s", exePath) 303 path = filepath.Join(filepath.Dir(exePath), filepath.Base(originalPath)) 304 } 305 } 306 307 // If everything failed, just use the original path and let the error 308 // bubble through. 309 if path == "" { 310 path = originalPath 311 } 312 313 if strings.Contains(originalPath, PACKERSPACE) { 314 log.Printf("[INFO] Starting internal plugin %s", args[len(args)-1]) 315 } else { 316 log.Printf("[INFO] Starting external plugin %s %s", path, strings.Join(args, " ")) 317 } 318 var config PluginClientConfig 319 config.Cmd = exec.Command(path, args...) 320 config.Managed = true 321 config.MinPort = c.PluginMinPort 322 config.MaxPort = c.PluginMaxPort 323 return NewClient(&config) 324 } 325 326 type PluginComponentType string 327 328 const ( 329 PluginComponentBuilder PluginComponentType = "builder" 330 PluginComponentPostProcessor PluginComponentType = "post-processor" 331 PluginComponentProvisioner PluginComponentType = "provisioner" 332 PluginComponentDataSource PluginComponentType = "data-source" 333 ) 334 335 type PluginDetails struct { 336 Name string 337 Description pluginsdk.SetDescription 338 PluginPath string 339 } 340 341 type pluginsDetailsStorage struct { 342 rwMutex sync.RWMutex 343 data map[string]PluginDetails 344 } 345 346 var GlobalPluginsDetailsStore = &pluginsDetailsStorage{ 347 data: make(map[string]PluginDetails), 348 } 349 350 func (pds *pluginsDetailsStorage) set(key string, plugin PluginDetails) { 351 pds.rwMutex.Lock() 352 defer pds.rwMutex.Unlock() 353 pds.data[key] = plugin 354 } 355 356 func (pds *pluginsDetailsStorage) get(key string) (PluginDetails, bool) { 357 pds.rwMutex.RLock() 358 defer pds.rwMutex.RUnlock() 359 plugin, exists := pds.data[key] 360 return plugin, exists 361 } 362 363 func (pds *pluginsDetailsStorage) SetBuilder(name string, plugin PluginDetails) { 364 key := fmt.Sprintf("%q-%q", PluginComponentBuilder, name) 365 pds.set(key, plugin) 366 } 367 368 func (pds *pluginsDetailsStorage) GetBuilder(name string) (PluginDetails, bool) { 369 key := fmt.Sprintf("%q-%q", PluginComponentBuilder, name) 370 return pds.get(key) 371 } 372 373 func (pds *pluginsDetailsStorage) SetPostProcessor(name string, plugin PluginDetails) { 374 key := fmt.Sprintf("%q-%q", PluginComponentPostProcessor, name) 375 pds.set(key, plugin) 376 } 377 378 func (pds *pluginsDetailsStorage) GetPostProcessor(name string) (PluginDetails, bool) { 379 key := fmt.Sprintf("%q-%q", PluginComponentPostProcessor, name) 380 return pds.get(key) 381 } 382 383 func (pds *pluginsDetailsStorage) SetProvisioner(name string, plugin PluginDetails) { 384 key := fmt.Sprintf("%q-%q", PluginComponentProvisioner, name) 385 pds.set(key, plugin) 386 } 387 388 func (pds *pluginsDetailsStorage) GetProvisioner(name string) (PluginDetails, bool) { 389 key := fmt.Sprintf("%q-%q", PluginComponentProvisioner, name) 390 return pds.get(key) 391 } 392 393 func (pds *pluginsDetailsStorage) SetDataSource(name string, plugin PluginDetails) { 394 key := fmt.Sprintf("%q-%q", PluginComponentDataSource, name) 395 pds.set(key, plugin) 396 } 397 398 func (pds *pluginsDetailsStorage) GetDataSource(name string) (PluginDetails, bool) { 399 key := fmt.Sprintf("%q-%q", PluginComponentDataSource, name) 400 return pds.get(key) 401 }