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  }