kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/command/add_test.go (about) 1 package command 2 3 import ( 4 "fmt" 5 "os" 6 "path/filepath" 7 "strings" 8 "testing" 9 10 "github.com/google/go-cmp/cmp" 11 "kubeform.dev/terraform-backend-sdk/addrs" 12 "kubeform.dev/terraform-backend-sdk/configs/configschema" 13 "kubeform.dev/terraform-backend-sdk/providers" 14 "kubeform.dev/terraform-backend-sdk/states" 15 "github.com/mitchellh/cli" 16 "github.com/zclconf/go-cty/cty" 17 ) 18 19 // simple test cases with a simple resource schema 20 func TestAdd_basic(t *testing.T) { 21 td := tempDir(t) 22 testCopyDir(t, testFixturePath("add/basic"), td) 23 defer os.RemoveAll(td) 24 defer testChdir(t, td)() 25 26 p := testProvider() 27 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 28 ResourceTypes: map[string]providers.Schema{ 29 "test_instance": { 30 Block: &configschema.Block{ 31 Attributes: map[string]*configschema.Attribute{ 32 "id": {Type: cty.String, Optional: true, Computed: true}, 33 "ami": {Type: cty.String, Optional: true, Description: "the ami to use"}, 34 "value": {Type: cty.String, Required: true, Description: "a value of a thing"}, 35 }, 36 }, 37 }, 38 }, 39 } 40 41 overrides := &testingOverrides{ 42 Providers: map[addrs.Provider]providers.Factory{ 43 addrs.NewDefaultProvider("test"): providers.FactoryFixed(p), 44 addrs.NewProvider("registry.terraform.io", "happycorp", "test"): providers.FactoryFixed(p), 45 }, 46 } 47 48 t.Run("basic", func(t *testing.T) { 49 view, done := testView(t) 50 c := &AddCommand{ 51 Meta: Meta{ 52 testingOverrides: overrides, 53 View: view, 54 }, 55 } 56 args := []string{"test_instance.new"} 57 code := c.Run(args) 58 output := done(t) 59 if code != 0 { 60 fmt.Println(output.Stderr()) 61 t.Fatalf("wrong exit status. Got %d, want 0", code) 62 } 63 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 64 # starting point for your resource configuration, with some limitations. 65 # 66 # The behavior of this command may change in future based on feedback, possibly 67 # in incompatible ways. We don't recommend building automation around this 68 # command at this time. If you have feedback about this command, please open 69 # a feature request issue in the Terraform GitHub repository. 70 resource "test_instance" "new" { 71 value = null # REQUIRED string 72 } 73 ` 74 75 if !cmp.Equal(output.Stdout(), expected) { 76 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout())) 77 } 78 }) 79 80 t.Run("basic to file", func(t *testing.T) { 81 view, done := testView(t) 82 c := &AddCommand{ 83 Meta: Meta{ 84 testingOverrides: overrides, 85 View: view, 86 }, 87 } 88 outPath := "add.tf" 89 args := []string{fmt.Sprintf("-out=%s", outPath), "test_instance.new"} 90 code := c.Run(args) 91 output := done(t) 92 if code != 0 { 93 fmt.Println(output.Stderr()) 94 t.Fatalf("wrong exit status. Got %d, want 0", code) 95 } 96 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 97 # starting point for your resource configuration, with some limitations. 98 # 99 # The behavior of this command may change in future based on feedback, possibly 100 # in incompatible ways. We don't recommend building automation around this 101 # command at this time. If you have feedback about this command, please open 102 # a feature request issue in the Terraform GitHub repository. 103 resource "test_instance" "new" { 104 value = null # REQUIRED string 105 } 106 ` 107 result, err := os.ReadFile(outPath) 108 if err != nil { 109 t.Fatalf("error reading result file %s: %s", outPath, err.Error()) 110 } 111 // While the entire directory will get removed once the whole test suite 112 // is done, we remove this lest it gets in the way of another (not yet 113 // written) test. 114 os.Remove(outPath) 115 116 if !cmp.Equal(expected, string(result)) { 117 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, string(result))) 118 } 119 }) 120 121 t.Run("basic to existing file", func(t *testing.T) { 122 view, done := testView(t) 123 c := &AddCommand{ 124 Meta: Meta{ 125 testingOverrides: overrides, 126 View: view, 127 }, 128 } 129 outPath := "add.tf" 130 args := []string{fmt.Sprintf("-out=%s", outPath), "test_instance.new"} 131 c.Run(args) 132 args = []string{fmt.Sprintf("-out=%s", outPath), "test_instance.new2"} 133 code := c.Run(args) 134 output := done(t) 135 if code != 0 { 136 fmt.Println(output.Stderr()) 137 t.Fatalf("wrong exit status. Got %d, want 0", code) 138 } 139 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 140 # starting point for your resource configuration, with some limitations. 141 # 142 # The behavior of this command may change in future based on feedback, possibly 143 # in incompatible ways. We don't recommend building automation around this 144 # command at this time. If you have feedback about this command, please open 145 # a feature request issue in the Terraform GitHub repository. 146 resource "test_instance" "new" { 147 value = null # REQUIRED string 148 } 149 # NOTE: The "terraform add" command is currently experimental and offers only a 150 # starting point for your resource configuration, with some limitations. 151 # 152 # The behavior of this command may change in future based on feedback, possibly 153 # in incompatible ways. We don't recommend building automation around this 154 # command at this time. If you have feedback about this command, please open 155 # a feature request issue in the Terraform GitHub repository. 156 resource "test_instance" "new2" { 157 value = null # REQUIRED string 158 } 159 ` 160 result, err := os.ReadFile(outPath) 161 if err != nil { 162 t.Fatalf("error reading result file %s: %s", outPath, err.Error()) 163 } 164 // While the entire directory will get removed once the whole test suite 165 // is done, we remove this lest it gets in the way of another (not yet 166 // written) test. 167 os.Remove(outPath) 168 169 if !cmp.Equal(expected, string(result)) { 170 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, string(result))) 171 } 172 }) 173 174 t.Run("optionals", func(t *testing.T) { 175 view, done := testView(t) 176 c := &AddCommand{ 177 Meta: Meta{ 178 testingOverrides: overrides, 179 View: view, 180 }, 181 } 182 args := []string{"-optional", "test_instance.new"} 183 code := c.Run(args) 184 if code != 0 { 185 t.Fatalf("wrong exit status. Got %d, want 0", code) 186 } 187 output := done(t) 188 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 189 # starting point for your resource configuration, with some limitations. 190 # 191 # The behavior of this command may change in future based on feedback, possibly 192 # in incompatible ways. We don't recommend building automation around this 193 # command at this time. If you have feedback about this command, please open 194 # a feature request issue in the Terraform GitHub repository. 195 resource "test_instance" "new" { 196 ami = null # OPTIONAL string 197 id = null # OPTIONAL string 198 value = null # REQUIRED string 199 } 200 ` 201 202 if !cmp.Equal(output.Stdout(), expected) { 203 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout())) 204 } 205 }) 206 207 t.Run("alternate provider for resource", func(t *testing.T) { 208 view, done := testView(t) 209 c := &AddCommand{ 210 Meta: Meta{ 211 testingOverrides: overrides, 212 View: view, 213 }, 214 } 215 args := []string{"-provider=provider[\"registry.terraform.io/happycorp/test\"].alias", "test_instance.new"} 216 code := c.Run(args) 217 output := done(t) 218 if code != 0 { 219 t.Fatalf("wrong exit status. Got %d, want 0", code) 220 } 221 222 // The provider happycorp/test has a localname "othertest" in the provider configuration. 223 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 224 # starting point for your resource configuration, with some limitations. 225 # 226 # The behavior of this command may change in future based on feedback, possibly 227 # in incompatible ways. We don't recommend building automation around this 228 # command at this time. If you have feedback about this command, please open 229 # a feature request issue in the Terraform GitHub repository. 230 resource "test_instance" "new" { 231 provider = othertest.alias 232 233 value = null # REQUIRED string 234 } 235 ` 236 237 if !cmp.Equal(output.Stdout(), expected) { 238 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout())) 239 } 240 }) 241 242 t.Run("resource exists error", func(t *testing.T) { 243 view, done := testView(t) 244 c := &AddCommand{ 245 Meta: Meta{ 246 testingOverrides: overrides, 247 View: view, 248 }, 249 } 250 outPath := "add.tf" 251 args := []string{fmt.Sprintf("-out=%s", outPath), "test_instance.exists"} 252 code := c.Run(args) 253 if code != 1 { 254 t.Fatalf("wrong exit status. Got %d, want 0", code) 255 } 256 257 output := done(t) 258 if !strings.Contains(output.Stderr(), "The resource test_instance.exists is already in this configuration") { 259 t.Fatalf("missing expected error message: %s", output.Stderr()) 260 } 261 }) 262 263 t.Run("output existing resource to stdout", func(t *testing.T) { 264 view, done := testView(t) 265 c := &AddCommand{ 266 Meta: Meta{ 267 testingOverrides: overrides, 268 View: view, 269 }, 270 } 271 args := []string{"test_instance.exists"} 272 code := c.Run(args) 273 output := done(t) 274 if code != 0 { 275 fmt.Println(output.Stderr()) 276 t.Fatalf("wrong exit status. Got %d, want 0", code) 277 } 278 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 279 # starting point for your resource configuration, with some limitations. 280 # 281 # The behavior of this command may change in future based on feedback, possibly 282 # in incompatible ways. We don't recommend building automation around this 283 # command at this time. If you have feedback about this command, please open 284 # a feature request issue in the Terraform GitHub repository. 285 resource "test_instance" "exists" { 286 value = null # REQUIRED string 287 } 288 ` 289 290 if !cmp.Equal(output.Stdout(), expected) { 291 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout())) 292 } 293 }) 294 295 t.Run("provider not in configuration", func(t *testing.T) { 296 view, done := testView(t) 297 c := &AddCommand{ 298 Meta: Meta{ 299 testingOverrides: overrides, 300 View: view, 301 }, 302 } 303 args := []string{"toast_instance.new"} 304 code := c.Run(args) 305 if code != 1 { 306 t.Fatalf("wrong exit status. Got %d, want 0", code) 307 } 308 309 output := done(t) 310 if !strings.Contains(output.Stderr(), "No schema found for provider registry.terraform.io/hashicorp/toast.") { 311 t.Fatalf("missing expected error message: %s", output.Stderr()) 312 } 313 }) 314 315 t.Run("no schema for resource", func(t *testing.T) { 316 view, done := testView(t) 317 c := &AddCommand{ 318 Meta: Meta{ 319 testingOverrides: overrides, 320 View: view, 321 }, 322 } 323 args := []string{"test_pet.meow"} 324 code := c.Run(args) 325 if code != 1 { 326 t.Fatalf("wrong exit status. Got %d, want 0", code) 327 } 328 329 output := done(t) 330 if !strings.Contains(output.Stderr(), "No resource schema found for test_pet.") { 331 t.Fatalf("missing expected error message: %s", output.Stderr()) 332 } 333 }) 334 } 335 336 func TestAdd(t *testing.T) { 337 td := tempDir(t) 338 testCopyDir(t, testFixturePath("add/module"), td) 339 defer os.RemoveAll(td) 340 defer testChdir(t, td)() 341 342 // a simple hashicorp/test provider, and a more complex happycorp/test provider 343 p := testProvider() 344 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 345 ResourceTypes: map[string]providers.Schema{ 346 "test_instance": { 347 Block: &configschema.Block{ 348 Attributes: map[string]*configschema.Attribute{ 349 "id": {Type: cty.String, Required: true}, 350 }, 351 }, 352 }, 353 }, 354 } 355 356 happycorp := testProvider() 357 happycorp.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 358 ResourceTypes: map[string]providers.Schema{ 359 "test_instance": { 360 Block: &configschema.Block{ 361 Attributes: map[string]*configschema.Attribute{ 362 "id": {Type: cty.String, Optional: true, Computed: true}, 363 "ami": {Type: cty.String, Optional: true, Description: "the ami to use"}, 364 "value": {Type: cty.String, Required: true, Description: "a value of a thing"}, 365 "disks": { 366 NestedType: &configschema.Object{ 367 Nesting: configschema.NestingList, 368 Attributes: map[string]*configschema.Attribute{ 369 "size": {Type: cty.String, Optional: true}, 370 "mount_point": {Type: cty.String, Required: true}, 371 }, 372 }, 373 Optional: true, 374 }, 375 }, 376 BlockTypes: map[string]*configschema.NestedBlock{ 377 "network_interface": { 378 Nesting: configschema.NestingList, 379 MinItems: 1, 380 Block: configschema.Block{ 381 Attributes: map[string]*configschema.Attribute{ 382 "device_index": {Type: cty.String, Optional: true}, 383 "description": {Type: cty.String, Optional: true}, 384 }, 385 }, 386 }, 387 }, 388 }, 389 }, 390 }, 391 } 392 providerSource, psClose := newMockProviderSource(t, map[string][]string{ 393 "registry.terraform.io/happycorp/test": {"1.0.0"}, 394 "registry.terraform.io/hashicorp/test": {"1.0.0"}, 395 }) 396 defer psClose() 397 398 overrides := &testingOverrides{ 399 Providers: map[addrs.Provider]providers.Factory{ 400 addrs.NewProvider("registry.terraform.io", "happycorp", "test"): providers.FactoryFixed(happycorp), 401 addrs.NewDefaultProvider("test"): providers.FactoryFixed(p), 402 }, 403 } 404 405 // the test fixture uses a module, so we need to run init. 406 m := Meta{ 407 testingOverrides: overrides, 408 ProviderSource: providerSource, 409 Ui: new(cli.MockUi), 410 } 411 412 init := &InitCommand{ 413 Meta: m, 414 } 415 416 code := init.Run([]string{}) 417 if code != 0 { 418 t.Fatal("init failed") 419 } 420 421 t.Run("optional", func(t *testing.T) { 422 view, done := testView(t) 423 c := &AddCommand{ 424 Meta: Meta{ 425 testingOverrides: overrides, 426 View: view, 427 }, 428 } 429 args := []string{"-optional", "test_instance.new"} 430 code := c.Run(args) 431 output := done(t) 432 if code != 0 { 433 t.Fatalf("wrong exit status. Got %d, want 0", code) 434 } 435 436 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 437 # starting point for your resource configuration, with some limitations. 438 # 439 # The behavior of this command may change in future based on feedback, possibly 440 # in incompatible ways. We don't recommend building automation around this 441 # command at this time. If you have feedback about this command, please open 442 # a feature request issue in the Terraform GitHub repository. 443 resource "test_instance" "new" { 444 ami = null # OPTIONAL string 445 disks = [{ # OPTIONAL list of object 446 mount_point = null # REQUIRED string 447 size = null # OPTIONAL string 448 }] 449 id = null # OPTIONAL string 450 value = null # REQUIRED string 451 network_interface { # REQUIRED block 452 description = null # OPTIONAL string 453 device_index = null # OPTIONAL string 454 } 455 } 456 ` 457 458 if !cmp.Equal(output.Stdout(), expected) { 459 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout())) 460 } 461 462 }) 463 464 t.Run("chooses correct provider for root module", func(t *testing.T) { 465 // in the root module of this test fixture, "test" is the local name for "happycorp/test" 466 view, done := testView(t) 467 c := &AddCommand{ 468 Meta: Meta{ 469 testingOverrides: overrides, 470 View: view, 471 }, 472 } 473 args := []string{"test_instance.new"} 474 code := c.Run(args) 475 output := done(t) 476 if code != 0 { 477 t.Fatalf("wrong exit status. Got %d, want 0", code) 478 } 479 480 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 481 # starting point for your resource configuration, with some limitations. 482 # 483 # The behavior of this command may change in future based on feedback, possibly 484 # in incompatible ways. We don't recommend building automation around this 485 # command at this time. If you have feedback about this command, please open 486 # a feature request issue in the Terraform GitHub repository. 487 resource "test_instance" "new" { 488 value = null # REQUIRED string 489 network_interface { # REQUIRED block 490 } 491 } 492 ` 493 494 if !cmp.Equal(output.Stdout(), expected) { 495 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout())) 496 } 497 }) 498 499 t.Run("chooses correct provider for child module", func(t *testing.T) { 500 // in the child module of this test fixture, "test" is a default "hashicorp/test" provider 501 view, done := testView(t) 502 c := &AddCommand{ 503 Meta: Meta{ 504 testingOverrides: overrides, 505 View: view, 506 }, 507 } 508 args := []string{"module.child.test_instance.new"} 509 code := c.Run(args) 510 output := done(t) 511 if code != 0 { 512 t.Fatalf("wrong exit status. Got %d, want 0", code) 513 } 514 515 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 516 # starting point for your resource configuration, with some limitations. 517 # 518 # The behavior of this command may change in future based on feedback, possibly 519 # in incompatible ways. We don't recommend building automation around this 520 # command at this time. If you have feedback about this command, please open 521 # a feature request issue in the Terraform GitHub repository. 522 resource "test_instance" "new" { 523 id = null # REQUIRED string 524 } 525 ` 526 527 if !cmp.Equal(output.Stdout(), expected) { 528 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout())) 529 } 530 }) 531 532 t.Run("chooses correct provider for an unknown module", func(t *testing.T) { 533 // it's weird but ok to use a new/unknown module name; terraform will 534 // fall back on default providers (unless a -provider argument is 535 // supplied) 536 view, done := testView(t) 537 c := &AddCommand{ 538 Meta: Meta{ 539 testingOverrides: overrides, 540 View: view, 541 }, 542 } 543 args := []string{"module.madeup.test_instance.new"} 544 code := c.Run(args) 545 output := done(t) 546 if code != 0 { 547 t.Fatalf("wrong exit status. Got %d, want 0", code) 548 } 549 550 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 551 # starting point for your resource configuration, with some limitations. 552 # 553 # The behavior of this command may change in future based on feedback, possibly 554 # in incompatible ways. We don't recommend building automation around this 555 # command at this time. If you have feedback about this command, please open 556 # a feature request issue in the Terraform GitHub repository. 557 resource "test_instance" "new" { 558 id = null # REQUIRED string 559 } 560 ` 561 562 if !cmp.Equal(output.Stdout(), expected) { 563 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout())) 564 } 565 }) 566 } 567 568 func TestAdd_from_state(t *testing.T) { 569 td := tempDir(t) 570 testCopyDir(t, testFixturePath("add/basic"), td) 571 defer os.RemoveAll(td) 572 defer testChdir(t, td)() 573 574 // write some state 575 testState := states.BuildState(func(s *states.SyncState) { 576 s.SetResourceInstanceCurrent( 577 addrs.Resource{ 578 Mode: addrs.ManagedResourceMode, 579 Type: "test_instance", 580 Name: "new", 581 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 582 &states.ResourceInstanceObjectSrc{ 583 AttrsJSON: []byte("{\"id\":\"bar\",\"ami\":\"ami-123456\",\"disks\":[{\"mount_point\":\"diska\",\"size\":null}],\"value\":\"bloop\"}"), 584 Status: states.ObjectReady, 585 Dependencies: []addrs.ConfigResource{}, 586 }, 587 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 588 ) 589 }) 590 f, err := os.Create("terraform.tfstate") 591 if err != nil { 592 t.Fatalf("failed to create temporary state file: %s", err) 593 } 594 defer f.Close() 595 err = writeStateForTesting(testState, f) 596 if err != nil { 597 t.Fatalf("failed to write state file: %s", err) 598 } 599 600 p := testProvider() 601 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 602 ResourceTypes: map[string]providers.Schema{ 603 "test_instance": { 604 Block: &configschema.Block{ 605 Attributes: map[string]*configschema.Attribute{ 606 "id": {Type: cty.String, Optional: true, Computed: true}, 607 "ami": {Type: cty.String, Optional: true, Description: "the ami to use"}, 608 "value": {Type: cty.String, Required: true, Description: "a value of a thing"}, 609 "disks": { 610 NestedType: &configschema.Object{ 611 Nesting: configschema.NestingList, 612 Attributes: map[string]*configschema.Attribute{ 613 "size": {Type: cty.String, Optional: true}, 614 "mount_point": {Type: cty.String, Required: true}, 615 }, 616 }, 617 Optional: true, 618 }, 619 }, 620 BlockTypes: map[string]*configschema.NestedBlock{ 621 "network_interface": { 622 Nesting: configschema.NestingList, 623 MinItems: 1, 624 Block: configschema.Block{ 625 Attributes: map[string]*configschema.Attribute{ 626 "device_index": {Type: cty.String, Optional: true}, 627 "description": {Type: cty.String, Optional: true}, 628 }, 629 }, 630 }, 631 }, 632 }, 633 }, 634 }, 635 } 636 overrides := &testingOverrides{ 637 Providers: map[addrs.Provider]providers.Factory{ 638 addrs.NewDefaultProvider("test"): providers.FactoryFixed(p), 639 addrs.NewProvider("registry.terraform.io", "happycorp", "test"): providers.FactoryFixed(p), 640 }, 641 } 642 view, done := testView(t) 643 c := &AddCommand{ 644 Meta: Meta{ 645 testingOverrides: overrides, 646 View: view, 647 }, 648 } 649 650 args := []string{"-from-state", "test_instance.new"} 651 code := c.Run(args) 652 output := done(t) 653 if code != 0 { 654 fmt.Println(output.Stderr()) 655 t.Fatalf("wrong exit status. Got %d, want 0", code) 656 } 657 658 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 659 # starting point for your resource configuration, with some limitations. 660 # 661 # The behavior of this command may change in future based on feedback, possibly 662 # in incompatible ways. We don't recommend building automation around this 663 # command at this time. If you have feedback about this command, please open 664 # a feature request issue in the Terraform GitHub repository. 665 resource "test_instance" "new" { 666 ami = "ami-123456" 667 disks = [ 668 { 669 mount_point = "diska" 670 size = null 671 }, 672 ] 673 id = "bar" 674 value = "bloop" 675 } 676 ` 677 678 if !cmp.Equal(output.Stdout(), expected) { 679 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout())) 680 } 681 682 if _, err := os.Stat(filepath.Join(td, ".terraform.tfstate.lock.info")); !os.IsNotExist(err) { 683 t.Fatal("state left locked after add") 684 } 685 }