github.com/hashicorp/packer@v1.14.3/packer_test/common/plugin.go (about)

     1  package common
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"runtime"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/hashicorp/go-version"
    14  	"github.com/hashicorp/packer-plugin-sdk/plugin"
    15  	"github.com/hashicorp/packer/packer_test/common/check"
    16  )
    17  
    18  // LDFlags compiles the ldflags for the plugin to compile based on the information provided.
    19  func LDFlags(version *version.Version) string {
    20  	pluginPackage := "github.com/hashicorp/packer-plugin-tester"
    21  
    22  	ldflagsArg := fmt.Sprintf("-X %s/version.Version=%s", pluginPackage, version.Core())
    23  	if version.Prerelease() != "" {
    24  		ldflagsArg = fmt.Sprintf("%s -X %s/version.VersionPrerelease=%s", ldflagsArg, pluginPackage, version.Prerelease())
    25  	}
    26  	if version.Metadata() != "" {
    27  		ldflagsArg = fmt.Sprintf("%s -X %s/version.VersionMetadata=%s", ldflagsArg, pluginPackage, version.Metadata())
    28  	}
    29  
    30  	return ldflagsArg
    31  }
    32  
    33  // BinaryName is the raw name of the plugin binary to produce
    34  //
    35  // It's expected to be in the "mini-plugin_<version>[-<prerelease>][+<metadata>]" format
    36  func BinaryName(version *version.Version) string {
    37  	retStr := fmt.Sprintf("mini-plugin_%s", version.Core())
    38  	if version.Prerelease() != "" {
    39  		retStr = fmt.Sprintf("%s-%s", retStr, version.Prerelease())
    40  	}
    41  	if version.Metadata() != "" {
    42  		retStr = fmt.Sprintf("%s+%s", retStr, version.Metadata())
    43  	}
    44  
    45  	return retStr
    46  }
    47  
    48  // ExpectedInstalledName is the expected full name of the plugin once installed.
    49  func ExpectedInstalledName(versionStr string) string {
    50  	version.Must(version.NewVersion(versionStr))
    51  
    52  	versionStr = strings.ReplaceAll(versionStr, "v", "")
    53  
    54  	ext := ""
    55  	if runtime.GOOS == "windows" {
    56  		ext = ".exe"
    57  	}
    58  
    59  	return fmt.Sprintf("packer-plugin-tester_v%s_x%s.%s_%s_%s%s",
    60  		versionStr,
    61  		plugin.APIVersionMajor,
    62  		plugin.APIVersionMinor,
    63  		runtime.GOOS, runtime.GOARCH, ext)
    64  }
    65  
    66  // BuildCustomisation is a function that allows you to change things on a plugin's
    67  // local files, with a way to rollback those changes after the fact.
    68  //
    69  // The function is meant to take a path parameter to the directory for the plugin,
    70  // and returns a function that unravels those changes once the build process is done.
    71  type BuildCustomisation func(string) (error, func())
    72  
    73  const SDKModule = "github.com/hashicorp/packer-plugin-sdk"
    74  
    75  // UseDependency invokes go get and go mod tidy to update a package required
    76  // by the plugin, and use it to build the plugin with that change.
    77  func UseDependency(remoteModule, ref string) BuildCustomisation {
    78  	return func(path string) (error, func()) {
    79  		modPath := filepath.Join(path, "go.mod")
    80  
    81  		stat, err := os.Stat(modPath)
    82  		if err != nil {
    83  			return fmt.Errorf("cannot stat mod file %q: %s", modPath, err), nil
    84  		}
    85  
    86  		// Save old go.mod file from dir
    87  		oldGoMod, err := os.ReadFile(modPath)
    88  		if err != nil {
    89  			return fmt.Errorf("failed to read current mod file %q: %s", modPath, err), nil
    90  		}
    91  
    92  		modSpec := fmt.Sprintf("%s@%s", remoteModule, ref)
    93  		cmd := exec.Command("go", "get", modSpec)
    94  		cmd.Dir = path
    95  		err = cmd.Run()
    96  		if err != nil {
    97  			return fmt.Errorf("failed to run go get %s: %s", modSpec, err), nil
    98  		}
    99  
   100  		cmd = exec.Command("go", "mod", "tidy")
   101  		cmd.Dir = path
   102  		err = cmd.Run()
   103  		if err != nil {
   104  			return fmt.Errorf("failed to run go mod tidy: %s", err), nil
   105  		}
   106  
   107  		return nil, func() {
   108  			err = os.WriteFile(modPath, oldGoMod, stat.Mode())
   109  			if err != nil {
   110  				fmt.Fprintf(os.Stderr, "failed to reset modfile %q: %s; manual cleanup may be needed", modPath, err)
   111  			}
   112  			cmd := exec.Command("go", "mod", "tidy")
   113  			cmd.Dir = path
   114  			_ = cmd.Run()
   115  		}
   116  	}
   117  }
   118  
   119  // GetPluginPath gets the path for a pre-compiled plugin in the current test suite.
   120  //
   121  // The version only is needed, as the path to a compiled version of the tester
   122  // plugin will be returned, so it can be installed after the fact.
   123  //
   124  // If the version requested does not exist, the function will panic.
   125  func (ts *PackerTestSuite) GetPluginPath(t *testing.T, version string) string {
   126  	path, ok := ts.compiledPlugins.Load(version)
   127  	if !ok {
   128  		t.Fatalf("tester plugin in version %q was not built, either build it during suite init, or with BuildTestPlugin", version)
   129  	}
   130  
   131  	return path.(string)
   132  }
   133  
   134  type CompilationResult struct {
   135  	Error   error
   136  	Version string
   137  }
   138  
   139  // Ready processes a series of CompilationResults, as returned by CompilePlugin
   140  //
   141  // If any of the jobs requested failed, the test will fail also.
   142  func Ready(t *testing.T, results []chan CompilationResult) {
   143  	for _, res := range results {
   144  		jobErr := <-res
   145  		empty := CompilationResult{}
   146  		if jobErr != empty {
   147  			t.Errorf("failed to compile plugin at version %s: %s", jobErr.Version, jobErr.Error)
   148  		}
   149  	}
   150  
   151  	if t.Failed() {
   152  		t.Fatalf("some plugins failed to be compiled, see logs for more info")
   153  	}
   154  }
   155  
   156  type compilationJob struct {
   157  	versionString  string
   158  	suite          *PackerTestSuite
   159  	done           bool
   160  	resultCh       chan CompilationResult
   161  	customisations []BuildCustomisation
   162  }
   163  
   164  // CompilationJobs keeps a queue of compilation jobs for plugins
   165  //
   166  // This approach allows us to avoid conflicts between compilation jobs.
   167  // Typically building the plugin with different ldflags is safe to perform
   168  // in parallel on the same file set, however customisations tend to be more
   169  // conflictual, as two concurrent compilation jobs may end-up compiling the
   170  // wrong plugin, which may cause some tests to misbehave, or even compilation
   171  // jobs to fail.
   172  //
   173  // The solution to this approach is to have a global queue for every plugin
   174  // compilation to be performed safely.
   175  var CompilationJobs = make(chan compilationJob, 10)
   176  
   177  // CompilePlugin builds a tester plugin with the specified version.
   178  //
   179  // The plugin's code is contained in a subdirectory of this file, and lets us
   180  // change the attributes of the plugin binary itself, like the SDK version,
   181  // the plugin's version, etc.
   182  //
   183  // The plugin is functional, and can be used to run builds with.
   184  // There won't be anything substantial created though, its goal is only
   185  // to validate the core functionality of Packer.
   186  //
   187  // The path to the plugin is returned, it won't be removed automatically
   188  // though, deletion is the caller's responsibility.
   189  //
   190  // Note: each tester plugin may only be compiled once for a specific version in
   191  // a test suite. The version may include core (mandatory), pre-release and
   192  // metadata. Unlike Packer core, metadata does matter for the version being built.
   193  //
   194  // Note: the compilation will process asynchronously, and should be waited upon
   195  // before tests that use this plugin may proceed. Refer to the `Ready` function
   196  // for doing that.
   197  func (ts *PackerTestSuite) CompilePlugin(versionString string, customisations ...BuildCustomisation) chan CompilationResult {
   198  	resultCh := make(chan CompilationResult)
   199  
   200  	CompilationJobs <- compilationJob{
   201  		versionString:  versionString,
   202  		suite:          ts,
   203  		customisations: customisations,
   204  		done:           false,
   205  		resultCh:       resultCh,
   206  	}
   207  
   208  	return resultCh
   209  }
   210  
   211  func init() {
   212  	// Run a processor coroutine for the duration of the test.
   213  	//
   214  	// It's simpler to have this occurring on the side at all times, without
   215  	// trying to manage its lifecycle based on the current amount of queued
   216  	// tasks, since this is linked to the test lifecycle, and as it's a single
   217  	// coroutine, we can leave it run until the process exits.
   218  	go func() {
   219  		for job := range CompilationJobs {
   220  			log.Printf("compiling plugin on version %s", job.versionString)
   221  			err := compilePlugin(job.suite, job.versionString, job.customisations...)
   222  			if err != nil {
   223  				job.resultCh <- CompilationResult{
   224  					Error:   err,
   225  					Version: job.versionString,
   226  				}
   227  			}
   228  			close(job.resultCh)
   229  		}
   230  	}()
   231  }
   232  
   233  // compilePlugin performs the actual compilation procedure for the plugin, and
   234  // registers it to the test suite instance passed as a parameter.
   235  func compilePlugin(ts *PackerTestSuite, versionString string, customisations ...BuildCustomisation) error {
   236  	// Fail to build plugin if already built.
   237  	//
   238  	// Especially with customisations being a thing, relying on cache to get and
   239  	// build a plugin at once means that the function is not idempotent anymore,
   240  	// and therefore we cannot rely on it being called twice and producing the
   241  	// same result, so we forbid it.
   242  	if _, ok := ts.compiledPlugins.Load(versionString); ok {
   243  		return fmt.Errorf("plugin version %q was already built, use GetTestPlugin instead", versionString)
   244  	}
   245  
   246  	v := version.Must(version.NewSemver(versionString))
   247  
   248  	testDir, err := currentDir()
   249  	if err != nil {
   250  		return fmt.Errorf("failed to compile plugin binary: %s", err)
   251  	}
   252  
   253  	testerPluginDir := filepath.Join(testDir, "plugin_tester")
   254  	for _, custom := range customisations {
   255  		err, cleanup := custom(testerPluginDir)
   256  		if err != nil {
   257  			return fmt.Errorf("failed to prepare plugin workdir: %s", err)
   258  		}
   259  		defer cleanup()
   260  	}
   261  
   262  	outBin := filepath.Join(ts.pluginsDirectory, BinaryName(v))
   263  
   264  	compileCommand := exec.Command("go", "build", "-C", testerPluginDir, "-o", outBin, "-ldflags", LDFlags(v), ".")
   265  	logs, err := compileCommand.CombinedOutput()
   266  	if err != nil {
   267  		return fmt.Errorf("failed to compile plugin binary: %s\ncompiler logs: %s", err, logs)
   268  	}
   269  
   270  	ts.compiledPlugins.Store(v.String(), outBin)
   271  	return nil
   272  }
   273  
   274  type PluginDirSpec struct {
   275  	dirPath string
   276  	suite   *PackerTestSuite
   277  }
   278  
   279  // MakePluginDir installs a list of plugins into a temporary directory and returns its path
   280  //
   281  // This can be set in the environment for a test through a function like t.SetEnv(), so
   282  // packer will be able to use that directory for running its functions.
   283  //
   284  // Deletion of the directory is the caller's responsibility.
   285  //
   286  // Note: all of the plugin versions specified to be installed in this plugin directory
   287  // must have been compiled beforehand.
   288  func (ts *PackerTestSuite) MakePluginDir() *PluginDirSpec {
   289  	var err error
   290  
   291  	pluginTempDir, err := os.MkdirTemp("", "packer-plugin-dir-temp-")
   292  	if err != nil {
   293  		return nil
   294  	}
   295  
   296  	return &PluginDirSpec{
   297  		dirPath: pluginTempDir,
   298  		suite:   ts,
   299  	}
   300  }
   301  
   302  // InstallPluginVersions installs several versions of the tester plugin under
   303  // github.com/hashicorp/tester.
   304  //
   305  // Each version of the plugin needs to have been pre-compiled.
   306  //
   307  // If a plugin is missing, the temporary directory will be removed.
   308  func (ps *PluginDirSpec) InstallPluginVersions(pluginVersions ...string) *PluginDirSpec {
   309  	t := ps.suite.T()
   310  
   311  	var err error
   312  
   313  	defer func() {
   314  		if err != nil || t.Failed() {
   315  			rmErr := os.RemoveAll(ps.Dir())
   316  			if rmErr != nil {
   317  				t.Logf("failed to remove temporary plugin directory %q: %s. This may need manual intervention.", ps.Dir(), err)
   318  			}
   319  			t.Fatalf("failed to install plugins to temporary plugin directory %q: %s", ps.Dir(), err)
   320  		}
   321  	}()
   322  
   323  	for _, pluginVersion := range pluginVersions {
   324  		path := ps.suite.GetPluginPath(t, pluginVersion)
   325  		cmd := ps.suite.PackerCommand().SetArgs("plugins", "install", "--path", path, "github.com/hashicorp/tester").AddEnv("PACKER_PLUGIN_PATH", ps.Dir())
   326  		cmd.Assert(check.MustSucceed())
   327  		out, stderr, cmdErr := cmd.run()
   328  		if cmdErr != nil {
   329  			err = fmt.Errorf("failed to install tester plugin version %q: %s\nCommand stdout: %s\nCommand stderr: %s", pluginVersion, err, out, stderr)
   330  		}
   331  	}
   332  
   333  	return ps
   334  }
   335  
   336  // Dir returns the temporary plugin dir for use in other functions
   337  func (ps PluginDirSpec) Dir() string {
   338  	return ps.dirPath
   339  }
   340  
   341  func (ps *PluginDirSpec) Cleanup() {
   342  	pluginDir := ps.Dir()
   343  	if pluginDir == "" {
   344  		return
   345  	}
   346  
   347  	err := os.RemoveAll(pluginDir)
   348  	if err != nil {
   349  		ps.suite.T().Logf("failed to remove temporary plugin directory %q: %s. This may need manual intervention.", pluginDir, err)
   350  	}
   351  }
   352  
   353  // ManualPluginInstall emulates how Packer installs plugins with `packer plugins install`
   354  //
   355  // This is used for some tests if we want to install a plugin that cannot be installed
   356  // through the normal commands (typically because Packer rejects it).
   357  func ManualPluginInstall(t *testing.T, dest, srcPlugin, versionStr string) {
   358  	err := os.MkdirAll(dest, 0755)
   359  	if err != nil {
   360  		t.Fatalf("failed to create destination directories %q: %s", dest, err)
   361  	}
   362  
   363  	pluginName := ExpectedInstalledName(versionStr)
   364  	destPath := filepath.Join(dest, pluginName)
   365  
   366  	CopyFile(t, destPath, srcPlugin)
   367  
   368  	shaPath := fmt.Sprintf("%s_SHA256SUM", destPath)
   369  	WriteFile(t, shaPath, SHA256Sum(t, destPath))
   370  }