github.com/hashicorp/packer@v1.14.3/packer/plugin_discover_test.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 "os" 10 "os/exec" 11 "path" 12 "path/filepath" 13 "runtime" 14 "strings" 15 "testing" 16 17 packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 18 pluginsdk "github.com/hashicorp/packer-plugin-sdk/plugin" 19 "github.com/hashicorp/packer-plugin-sdk/tmp" 20 "github.com/hashicorp/packer-plugin-sdk/version" 21 plugingetter "github.com/hashicorp/packer/packer/plugin-getter" 22 ) 23 24 func newPluginConfig() PluginConfig { 25 var conf PluginConfig 26 conf.PluginMinPort = 10000 27 conf.PluginMaxPort = 25000 28 return conf 29 } 30 31 func TestDiscoverReturnsIfMagicCookieSet(t *testing.T) { 32 config := newPluginConfig() 33 34 t.Setenv(pluginsdk.MagicCookieKey, pluginsdk.MagicCookieValue) 35 36 err := config.Discover() 37 if err != nil { 38 t.Fatalf("Should not have errored: %s", err) 39 } 40 41 if len(config.Builders.List()) != 0 { 42 t.Fatalf("Should not have tried to find builders") 43 } 44 } 45 46 func TestMultiPlugin_describe(t *testing.T) { 47 createMockPlugins(t, mockPlugins) 48 pluginDir := os.Getenv("PACKER_PLUGIN_PATH") 49 defer os.RemoveAll(pluginDir) 50 c := PluginConfig{} 51 err := c.Discover() 52 if err != nil { 53 t.Fatalf("error discovering plugins; %s", err.Error()) 54 } 55 56 for mockPluginName, plugin := range mockPlugins { 57 for mockBuilderName := range plugin.Builders { 58 expectedBuilderName := mockPluginName + "-" + mockBuilderName 59 60 if !c.Builders.Has(expectedBuilderName) { 61 t.Errorf("expected to find builder %q", expectedBuilderName) 62 } 63 } 64 for mockProvisionerName := range plugin.Provisioners { 65 expectedProvisionerName := mockPluginName + "-" + mockProvisionerName 66 if !c.Provisioners.Has(expectedProvisionerName) { 67 t.Errorf("expected to find builder %q", expectedProvisionerName) 68 } 69 } 70 for mockPostProcessorName := range plugin.PostProcessors { 71 expectedPostProcessorName := mockPluginName + "-" + mockPostProcessorName 72 if !c.PostProcessors.Has(expectedPostProcessorName) { 73 t.Errorf("expected to find post-processor %q", expectedPostProcessorName) 74 } 75 } 76 for mockDatasourceName := range plugin.Datasources { 77 expectedDatasourceName := mockPluginName + "-" + mockDatasourceName 78 if !c.DataSources.Has(expectedDatasourceName) { 79 t.Errorf("expected to find datasource %q", expectedDatasourceName) 80 } 81 } 82 } 83 } 84 85 func TestMultiPlugin_describe_installed(t *testing.T) { 86 createMockInstalledPlugins(t, mockInstalledPlugins, createMockChecksumFile) 87 pluginDir := os.Getenv("PACKER_PLUGIN_PATH") 88 defer os.RemoveAll(pluginDir) 89 90 c := PluginConfig{} 91 err := c.Discover() 92 if err != nil { 93 t.Fatalf("error discovering plugins; %s", err.Error()) 94 } 95 96 for mockPluginName, plugin := range mockInstalledPlugins { 97 mockPluginName = strings.Split(mockPluginName, "_")[0] 98 for mockBuilderName := range plugin.Builders { 99 expectedBuilderName := mockPluginName + "-" + mockBuilderName 100 if !c.Builders.Has(expectedBuilderName) { 101 t.Fatalf("expected to find builder %q", expectedBuilderName) 102 } 103 } 104 for mockProvisionerName := range plugin.Provisioners { 105 expectedProvisionerName := mockPluginName + "-" + mockProvisionerName 106 if !c.Provisioners.Has(expectedProvisionerName) { 107 t.Fatalf("expected to find builder %q", expectedProvisionerName) 108 } 109 } 110 for mockPostProcessorName := range plugin.PostProcessors { 111 expectedPostProcessorName := mockPluginName + "-" + mockPostProcessorName 112 if !c.PostProcessors.Has(expectedPostProcessorName) { 113 t.Fatalf("expected to find post-processor %q", expectedPostProcessorName) 114 } 115 } 116 for mockDatasourceName := range plugin.Datasources { 117 expectedDatasourceName := mockPluginName + "-" + mockDatasourceName 118 if !c.DataSources.Has(expectedDatasourceName) { 119 t.Fatalf("expected to find datasource %q", expectedDatasourceName) 120 } 121 } 122 } 123 } 124 125 func TestMultiPlugin_describe_installed_for_invalid(t *testing.T) { 126 tc := []struct { 127 desc string 128 installedPluginsMock map[string]pluginsdk.Set 129 createMockFn func(*testing.T, map[string]pluginsdk.Set) 130 }{ 131 { 132 desc: "Incorrectly named plugins", 133 installedPluginsMock: invalidInstalledPluginsMock, 134 createMockFn: func(t *testing.T, mocks map[string]pluginsdk.Set) { 135 createMockInstalledPlugins(t, mocks, createMockChecksumFile) 136 }, 137 }, 138 { 139 desc: "Plugins missing checksums", 140 installedPluginsMock: mockInstalledPlugins, 141 createMockFn: func(t *testing.T, mocks map[string]pluginsdk.Set) { 142 createMockInstalledPlugins(t, mocks) 143 }, 144 }, 145 } 146 147 for _, tt := range tc { 148 t.Run(tt.desc, func(t *testing.T) { 149 tt.createMockFn(t, tt.installedPluginsMock) 150 pluginDir := os.Getenv("PACKER_PLUGIN_PATH") 151 defer os.RemoveAll(pluginDir) 152 153 c := PluginConfig{} 154 err := c.Discover() 155 if err != nil { 156 t.Fatalf("error discovering plugins; %s", err.Error()) 157 } 158 if c.Builders.Has("feather") { 159 t.Fatalf("expected to not find builder %q", "feather") 160 } 161 for mockPluginName, plugin := range tt.installedPluginsMock { 162 mockPluginName = strings.Split(mockPluginName, "_")[0] 163 for mockBuilderName := range plugin.Builders { 164 expectedBuilderName := mockPluginName + "-" + mockBuilderName 165 if c.Builders.Has(expectedBuilderName) { 166 t.Fatalf("expected to not find builder %q", expectedBuilderName) 167 } 168 } 169 for mockProvisionerName := range plugin.Provisioners { 170 expectedProvisionerName := mockPluginName + "-" + mockProvisionerName 171 if c.Provisioners.Has(expectedProvisionerName) { 172 t.Fatalf("expected to not find builder %q", expectedProvisionerName) 173 } 174 } 175 for mockPostProcessorName := range plugin.PostProcessors { 176 expectedPostProcessorName := mockPluginName + "-" + mockPostProcessorName 177 if c.PostProcessors.Has(expectedPostProcessorName) { 178 t.Fatalf("expected to not find post-processor %q", expectedPostProcessorName) 179 } 180 } 181 for mockDatasourceName := range plugin.Datasources { 182 expectedDatasourceName := mockPluginName + "-" + mockDatasourceName 183 if c.DataSources.Has(expectedDatasourceName) { 184 t.Fatalf("expected to not find datasource %q", expectedDatasourceName) 185 } 186 } 187 } 188 }) 189 } 190 } 191 192 func TestMultiPlugin_defaultName(t *testing.T) { 193 createMockPlugins(t, defaultNameMock) 194 pluginDir := os.Getenv("PACKER_PLUGIN_PATH") 195 defer os.RemoveAll(pluginDir) 196 197 c := PluginConfig{} 198 err := c.Discover() 199 if err != nil { 200 t.Fatalf("error discovering plugins; %s ; mocks are %#v", err.Error(), defaultNameMock) 201 } 202 203 expectedBuilderNames := []string{"foo-bar", "foo-baz", "foo"} 204 for _, mockBuilderName := range expectedBuilderNames { 205 if !c.Builders.Has(mockBuilderName) { 206 t.Fatalf("expected to find builder %q; builders is %#v", mockBuilderName, c.Builders) 207 } 208 } 209 } 210 211 func TestMultiPlugin_IgnoreChecksumFile(t *testing.T) { 212 createMockPlugins(t, defaultNameMock) 213 pluginDir := os.Getenv("PACKER_PLUGIN_PATH") 214 defer os.RemoveAll(pluginDir) 215 216 fooPluginName := fmt.Sprintf("packer-plugin-foo_v1.0.0_x5.0_%s_%s", runtime.GOOS, runtime.GOARCH) 217 fooPluginPath := filepath.Join(pluginDir, "github.com", "hashicorp", "foo", fooPluginName) 218 csFile, err := generateMockChecksumFile(fooPluginPath) 219 if err != nil { 220 t.Fatal(err.Error()) 221 } 222 223 // Copy plugin contents into checksum file to validate that it is not only skipped but that it never gets loaded 224 if err := os.Rename(fooPluginPath, csFile); err != nil { 225 t.Fatalf("failed to rename plugin bin file to checkfum file needed for test: %s", err) 226 } 227 228 c := PluginConfig{} 229 err = c.Discover() 230 if err != nil { 231 t.Fatalf("error discovering plugins; %s ; mocks are %#v", err.Error(), defaultNameMock) 232 } 233 expectedBuilderNames := []string{"foo-bar", "foo-baz", "foo"} 234 for _, mockBuilderName := range expectedBuilderNames { 235 if c.Builders.Has(mockBuilderName) { 236 t.Fatalf("expected to not find builder %q; builders is %#v", mockBuilderName, c.Builders) 237 } 238 } 239 } 240 241 func TestMultiPlugin_defaultName_each_plugin_type(t *testing.T) { 242 createMockPlugins(t, doubleDefaultMock) 243 pluginDir := os.Getenv("PACKER_PLUGIN_PATH") 244 defer os.RemoveAll(pluginDir) 245 246 c := PluginConfig{} 247 err := c.Discover() 248 if err != nil { 249 t.Fatal("Should not have error because pluginsdk.DEFAULT_NAME is used twice but only once per plugin type.") 250 } 251 } 252 253 // TestHelperProcess isn't a real test. It's used as a helper process 254 // for multi-component plugin tests. 255 func TestHelperPlugins(t *testing.T) { 256 if os.Getenv("PKR_WANT_TEST_PLUGINS") != "1" { 257 return 258 } 259 defer os.Exit(0) 260 261 args := os.Args 262 for len(args) > 0 { 263 if args[0] == "--" { 264 args = args[1:] 265 break 266 } 267 args = args[1:] 268 } 269 if len(args) == 0 { 270 fmt.Fprintf(os.Stderr, "No command\n") 271 os.Exit(2) 272 } 273 274 pluginName, args := args[0], args[1:] 275 276 allMocks := []map[string]pluginsdk.Set{mockPlugins, defaultNameMock, doubleDefaultMock, badDefaultNameMock} 277 for _, mock := range allMocks { 278 plugin, found := mock[pluginName] 279 if found { 280 plugin.SetVersion(version.NewPluginVersion("1.0.0", "", "")) 281 err := plugin.RunCommand(args...) 282 if err != nil { 283 fmt.Fprintf(os.Stderr, "%v\n", err) 284 os.Exit(1) 285 } 286 os.Exit(0) 287 } 288 } 289 290 fmt.Fprintf(os.Stderr, "No %q plugin found\n", pluginName) 291 os.Exit(2) 292 } 293 294 // HasExec reports whether the current system can start new processes 295 // using os.StartProcess or (more commonly) exec.Command. 296 func HasExec() bool { 297 switch runtime.GOOS { 298 case "js": 299 return false 300 case "windows": 301 // TODO(azr): Fix this once versioning is added and we know more 302 return false 303 } 304 return true 305 } 306 307 // MustHaveExec checks that the current system can start new processes 308 // using os.StartProcess or (more commonly) exec.Command. 309 // If not, MustHaveExec calls t.Skip with an explanation. 310 func MustHaveExec(t testing.TB) { 311 if !HasExec() { 312 t.Skipf("skipping test: cannot exec subprocess on %s/%s", runtime.GOOS, runtime.GOARCH) 313 } 314 } 315 316 func MustHaveCommand(t testing.TB, cmd string) string { 317 path, err := exec.LookPath(cmd) 318 if err != nil { 319 t.Skipf("skipping test: cannot find the %q command: %v", cmd, err) 320 } 321 return path 322 } 323 324 func helperCommand(t *testing.T, s ...string) []string { 325 MustHaveExec(t) 326 327 cmd := []string{os.Args[0], "-test.run=TestHelperPlugins", "--"} 328 return append(cmd, s...) 329 } 330 331 func createMockPlugins(t *testing.T, plugins map[string]pluginsdk.Set) { 332 pluginDir, err := tmp.Dir("pkr-multi-component-plugin-test-*") 333 { 334 // create an exectutable file with a `sh` sheebang 335 // this file will look like: 336 // #!/bin/sh 337 // PKR_WANT_TEST_PLUGINS=1 ...plugin/debug.test -test.run=TestHelperPlugins -- bird $@ 338 // 'bird' is the mock plugin we want to start 339 // $@ just passes all passed arguments 340 // This will allow to run the fake plugin from go tests which in turn 341 // will run go tests callback to `TestHelperPlugins`, this one will be 342 // transparently calling our mock multi-component plugins `mockPlugins`. 343 if err != nil { 344 t.Fatal(err) 345 } 346 347 t.Logf("putting temporary mock plugins in %s", pluginDir) 348 349 shPath := MustHaveCommand(t, "bash") 350 for name := range plugins { 351 pluginName := fmt.Sprintf("packer-plugin-%s_v1.0.0_x5.0_%s_%s", name, runtime.GOOS, runtime.GOARCH) 352 pluginSubDir := fmt.Sprintf("github.com/hashicorp/%s", name) 353 err := os.MkdirAll(path.Join(pluginDir, pluginSubDir), 0755) 354 if err != nil { 355 t.Fatalf("failed to create plugin hierarchy: %s", err) 356 } 357 plugin := path.Join(pluginDir, pluginSubDir, pluginName) 358 t.Logf("creating fake plugin %s", plugin) 359 fileContent := "" 360 fileContent = fmt.Sprintf("#!%s\n", shPath) 361 fileContent += strings.Join( 362 append([]string{"PKR_WANT_TEST_PLUGINS=1"}, helperCommand(t, name, "$@")...), 363 " ") 364 if err := os.WriteFile(plugin, []byte(fileContent), os.ModePerm); err != nil { 365 t.Fatalf("failed to create fake plugin binary: %v", err) 366 } 367 368 if _, err := generateMockChecksumFile(plugin); err != nil { 369 t.Fatalf("failed to create fake plugin binary checksum file: %v", err) 370 } 371 } 372 } 373 t.Setenv("PACKER_PLUGIN_PATH", pluginDir) 374 } 375 376 func createMockChecksumFile(t testing.TB, filePath string) { 377 t.Helper() 378 cs, err := generateMockChecksumFile(filePath) 379 if err != nil { 380 t.Fatalf("%s", err.Error()) 381 } 382 t.Logf("created fake plugin checksum file %s", cs) 383 } 384 385 func generateMockChecksumFile(filePath string) (string, error) { 386 cs := plugingetter.Checksummer{ 387 Type: "sha256", 388 Hash: sha256.New(), 389 } 390 391 f, err := os.Open(filePath) 392 if err != nil { 393 return "", fmt.Errorf("failed to open fake plugin binary: %v", err) 394 } 395 defer f.Close() 396 397 sum, err := cs.Sum(f) 398 if err != nil { 399 return "", fmt.Errorf("failed to checksum fake plugin binary: %v", err) 400 } 401 402 sumfile := filePath + cs.FileExt() 403 if err := os.WriteFile(sumfile, []byte(fmt.Sprintf("%x", sum)), os.ModePerm); err != nil { 404 return "", fmt.Errorf("failed to write checksum fake plugin binary: %v", err) 405 } 406 return sumfile, nil 407 } 408 409 func createMockInstalledPlugins(t *testing.T, plugins map[string]pluginsdk.Set, opts ...func(tb testing.TB, filePath string)) { 410 pluginDir, err := tmp.Dir("pkr-multi-component-plugin-test-*") 411 { 412 // create an exectutable file with a `sh` sheebang 413 // this file will look like: 414 // #!/bin/sh 415 // PKR_WANT_TEST_PLUGINS=1 ...plugin/debug.test -test.run=TestHelperPlugins -- bird $@ 416 // 'bird' is the mock plugin we want to start 417 // $@ just passes all passed arguments 418 // This will allow to run the fake plugin from go tests which in turn 419 // will run go tests callback to `TestHelperPlugins`, this one will be 420 // transparently calling our mock multi-component plugins `mockPlugins`. 421 if err != nil { 422 t.Fatal(err) 423 } 424 dir, err := os.MkdirTemp(pluginDir, "github.com") 425 if err != nil { 426 t.Fatalf("failed to create temporary test directory: %v", err) 427 } 428 dir, err = os.MkdirTemp(dir, "hashicorp") 429 if err != nil { 430 t.Fatalf("failed to create temporary test directory: %v", err) 431 } 432 dir, err = os.MkdirTemp(dir, "plugin") 433 if err != nil { 434 t.Fatalf("failed to create temporary test directory: %v", err) 435 } 436 t.Logf("putting temporary mock installed plugins in %s", dir) 437 438 shPath := MustHaveCommand(t, "bash") 439 for name := range plugins { 440 plugin := path.Join(dir, "packer-plugin-"+name) 441 t.Logf("creating fake plugin %s", plugin) 442 fileContent := "" 443 fileContent = fmt.Sprintf("#!%s\n", shPath) 444 fileContent += strings.Join( 445 append([]string{"PKR_WANT_TEST_PLUGINS=1"}, helperCommand(t, strings.Split(name, "_")[0], "$@")...), 446 " ") 447 if err := os.WriteFile(plugin, []byte(fileContent), os.ModePerm); err != nil { 448 t.Fatalf("failed to create fake plugin binary: %v", err) 449 } 450 451 for _, opt := range opts { 452 opt(t, plugin) 453 } 454 } 455 } 456 t.Setenv("PACKER_PLUGIN_PATH", pluginDir) 457 } 458 459 func getFormattedInstalledPluginSuffix() string { 460 return fmt.Sprintf("v1.0.0_x5.0_%s_%s", runtime.GOOS, runtime.GOARCH) 461 } 462 463 var ( 464 mockPlugins = map[string]pluginsdk.Set{} 465 mockInstalledPlugins = map[string]pluginsdk.Set{} 466 invalidInstalledPluginsMock = map[string]pluginsdk.Set{} 467 defaultNameMock = map[string]pluginsdk.Set{} 468 doubleDefaultMock = map[string]pluginsdk.Set{} 469 badDefaultNameMock = map[string]pluginsdk.Set{} 470 ) 471 472 func init() { 473 mockPluginsBird := pluginsdk.NewSet() 474 mockPluginsBird.Builders = map[string]packersdk.Builder{ 475 "feather": nil, 476 "guacamole": nil, 477 } 478 mockPluginsChim := pluginsdk.NewSet() 479 mockPluginsChim.PostProcessors = map[string]packersdk.PostProcessor{ 480 "smoke": nil, 481 } 482 mockPluginsData := pluginsdk.NewSet() 483 mockPluginsData.Datasources = map[string]packersdk.Datasource{ 484 "source": nil, 485 } 486 mockPlugins["bird"] = *mockPluginsBird 487 mockPlugins["chimney"] = *mockPluginsChim 488 mockPlugins["data"] = *mockPluginsData 489 490 mockInstalledPluginsBird := pluginsdk.NewSet() 491 mockInstalledPluginsBird.Builders = map[string]packersdk.Builder{ 492 "feather": nil, 493 "guacamole": nil, 494 } 495 mockInstalledPluginsChim := pluginsdk.NewSet() 496 mockInstalledPluginsChim.PostProcessors = map[string]packersdk.PostProcessor{ 497 "smoke": nil, 498 } 499 mockInstalledPluginsData := pluginsdk.NewSet() 500 mockInstalledPluginsData.Datasources = map[string]packersdk.Datasource{ 501 "source": nil, 502 } 503 mockInstalledPlugins[fmt.Sprintf("bird_%s", getFormattedInstalledPluginSuffix())] = *mockInstalledPluginsBird 504 mockInstalledPlugins[fmt.Sprintf("chimney_%s", getFormattedInstalledPluginSuffix())] = *mockInstalledPluginsChim 505 mockInstalledPlugins[fmt.Sprintf("data_%s", getFormattedInstalledPluginSuffix())] = *mockInstalledPluginsData 506 507 invalidInstalledPluginsMockBird := pluginsdk.NewSet() 508 invalidInstalledPluginsMockBird.Builders = map[string]packersdk.Builder{ 509 "feather": nil, 510 "guacamole": nil, 511 } 512 invalidInstalledPluginsMockChimney := pluginsdk.NewSet() 513 invalidInstalledPluginsMockChimney.PostProcessors = map[string]packersdk.PostProcessor{ 514 "smoke": nil, 515 } 516 invalidInstalledPluginsMockData := pluginsdk.NewSet() 517 invalidInstalledPluginsMockData.Datasources = map[string]packersdk.Datasource{ 518 "source": nil, 519 } 520 invalidInstalledPluginsMock["bird_v0.1.1_x5.0_wrong_architecture"] = *invalidInstalledPluginsMockBird 521 invalidInstalledPluginsMock["chimney_cool_ranch"] = *invalidInstalledPluginsMockChimney 522 invalidInstalledPluginsMock["data"] = *invalidInstalledPluginsMockData 523 524 defaultNameFooSet := pluginsdk.NewSet() 525 defaultNameFooSet.Builders = map[string]packersdk.Builder{ 526 "bar": nil, 527 "baz": nil, 528 pluginsdk.DEFAULT_NAME: nil, 529 } 530 defaultNameMock["foo"] = *defaultNameFooSet 531 532 doubleDefaultYoloSet := pluginsdk.NewSet() 533 doubleDefaultYoloSet.Builders = map[string]packersdk.Builder{ 534 "bar": nil, 535 "baz": nil, 536 pluginsdk.DEFAULT_NAME: nil, 537 } 538 doubleDefaultYoloSet.PostProcessors = map[string]packersdk.PostProcessor{ 539 pluginsdk.DEFAULT_NAME: nil, 540 } 541 doubleDefaultMock["yolo"] = *doubleDefaultYoloSet 542 543 badDefaultSet := pluginsdk.NewSet() 544 badDefaultSet.Builders = map[string]packersdk.Builder{ 545 "bar": nil, 546 "baz": nil, 547 pluginsdk.DEFAULT_NAME: nil, 548 } 549 badDefaultNameMock["foo"] = *badDefaultSet 550 }