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 }