github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/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 "github.com/hashicorp/terraform/internal/addrs" 12 "github.com/hashicorp/terraform/internal/configs/configschema" 13 "github.com/hashicorp/terraform/internal/providers" 14 "github.com/hashicorp/terraform/internal/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("optionals", func(t *testing.T) { 122 view, done := testView(t) 123 c := &AddCommand{ 124 Meta: Meta{ 125 testingOverrides: overrides, 126 View: view, 127 }, 128 } 129 args := []string{"-optional", "test_instance.new"} 130 code := c.Run(args) 131 if code != 0 { 132 t.Fatalf("wrong exit status. Got %d, want 0", code) 133 } 134 output := done(t) 135 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 136 # starting point for your resource configuration, with some limitations. 137 # 138 # The behavior of this command may change in future based on feedback, possibly 139 # in incompatible ways. We don't recommend building automation around this 140 # command at this time. If you have feedback about this command, please open 141 # a feature request issue in the Terraform GitHub repository. 142 resource "test_instance" "new" { 143 ami = null # OPTIONAL string 144 id = null # OPTIONAL string 145 value = null # REQUIRED string 146 } 147 ` 148 149 if !cmp.Equal(output.Stdout(), expected) { 150 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout())) 151 } 152 }) 153 154 t.Run("alternate provider for resource", func(t *testing.T) { 155 view, done := testView(t) 156 c := &AddCommand{ 157 Meta: Meta{ 158 testingOverrides: overrides, 159 View: view, 160 }, 161 } 162 args := []string{"-provider=provider[\"registry.terraform.io/happycorp/test\"].alias", "test_instance.new"} 163 code := c.Run(args) 164 output := done(t) 165 if code != 0 { 166 t.Fatalf("wrong exit status. Got %d, want 0", code) 167 } 168 169 // The provider happycorp/test has a localname "othertest" in the provider configuration. 170 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 171 # starting point for your resource configuration, with some limitations. 172 # 173 # The behavior of this command may change in future based on feedback, possibly 174 # in incompatible ways. We don't recommend building automation around this 175 # command at this time. If you have feedback about this command, please open 176 # a feature request issue in the Terraform GitHub repository. 177 resource "test_instance" "new" { 178 provider = othertest.alias 179 180 value = null # REQUIRED string 181 } 182 ` 183 184 if !cmp.Equal(output.Stdout(), expected) { 185 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout())) 186 } 187 }) 188 189 t.Run("resource exists error", func(t *testing.T) { 190 view, done := testView(t) 191 c := &AddCommand{ 192 Meta: Meta{ 193 testingOverrides: overrides, 194 View: view, 195 }, 196 } 197 args := []string{"test_instance.exists"} 198 code := c.Run(args) 199 if code != 1 { 200 t.Fatalf("wrong exit status. Got %d, want 0", code) 201 } 202 203 output := done(t) 204 if !strings.Contains(output.Stderr(), "The resource test_instance.exists is already in this configuration") { 205 t.Fatalf("missing expected error message: %s", output.Stderr()) 206 } 207 }) 208 209 t.Run("provider not in configuration", func(t *testing.T) { 210 view, done := testView(t) 211 c := &AddCommand{ 212 Meta: Meta{ 213 testingOverrides: overrides, 214 View: view, 215 }, 216 } 217 args := []string{"toast_instance.new"} 218 code := c.Run(args) 219 if code != 1 { 220 t.Fatalf("wrong exit status. Got %d, want 0", code) 221 } 222 223 output := done(t) 224 if !strings.Contains(output.Stderr(), "No schema found for provider registry.terraform.io/hashicorp/toast.") { 225 t.Fatalf("missing expected error message: %s", output.Stderr()) 226 } 227 }) 228 229 t.Run("no schema for resource", func(t *testing.T) { 230 view, done := testView(t) 231 c := &AddCommand{ 232 Meta: Meta{ 233 testingOverrides: overrides, 234 View: view, 235 }, 236 } 237 args := []string{"test_pet.meow"} 238 code := c.Run(args) 239 if code != 1 { 240 t.Fatalf("wrong exit status. Got %d, want 0", code) 241 } 242 243 output := done(t) 244 if !strings.Contains(output.Stderr(), "No resource schema found for test_pet.") { 245 t.Fatalf("missing expected error message: %s", output.Stderr()) 246 } 247 }) 248 } 249 250 func TestAdd(t *testing.T) { 251 td := tempDir(t) 252 testCopyDir(t, testFixturePath("add/module"), td) 253 defer os.RemoveAll(td) 254 defer testChdir(t, td)() 255 256 // a simple hashicorp/test provider, and a more complex happycorp/test provider 257 p := testProvider() 258 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 259 ResourceTypes: map[string]providers.Schema{ 260 "test_instance": { 261 Block: &configschema.Block{ 262 Attributes: map[string]*configschema.Attribute{ 263 "id": {Type: cty.String, Required: true}, 264 }, 265 }, 266 }, 267 }, 268 } 269 270 happycorp := testProvider() 271 happycorp.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 272 ResourceTypes: map[string]providers.Schema{ 273 "test_instance": { 274 Block: &configschema.Block{ 275 Attributes: map[string]*configschema.Attribute{ 276 "id": {Type: cty.String, Optional: true, Computed: true}, 277 "ami": {Type: cty.String, Optional: true, Description: "the ami to use"}, 278 "value": {Type: cty.String, Required: true, Description: "a value of a thing"}, 279 "disks": { 280 NestedType: &configschema.Object{ 281 Nesting: configschema.NestingList, 282 Attributes: map[string]*configschema.Attribute{ 283 "size": {Type: cty.String, Optional: true}, 284 "mount_point": {Type: cty.String, Required: true}, 285 }, 286 }, 287 Optional: true, 288 }, 289 }, 290 BlockTypes: map[string]*configschema.NestedBlock{ 291 "network_interface": { 292 Nesting: configschema.NestingList, 293 MinItems: 1, 294 Block: configschema.Block{ 295 Attributes: map[string]*configschema.Attribute{ 296 "device_index": {Type: cty.String, Optional: true}, 297 "description": {Type: cty.String, Optional: true}, 298 }, 299 }, 300 }, 301 }, 302 }, 303 }, 304 }, 305 } 306 providerSource, psClose := newMockProviderSource(t, map[string][]string{ 307 "registry.terraform.io/happycorp/test": {"1.0.0"}, 308 "registry.terraform.io/hashicorp/test": {"1.0.0"}, 309 }) 310 defer psClose() 311 312 overrides := &testingOverrides{ 313 Providers: map[addrs.Provider]providers.Factory{ 314 addrs.NewProvider("registry.terraform.io", "happycorp", "test"): providers.FactoryFixed(happycorp), 315 addrs.NewDefaultProvider("test"): providers.FactoryFixed(p), 316 }, 317 } 318 319 // the test fixture uses a module, so we need to run init. 320 m := Meta{ 321 testingOverrides: overrides, 322 ProviderSource: providerSource, 323 Ui: new(cli.MockUi), 324 } 325 326 init := &InitCommand{ 327 Meta: m, 328 } 329 330 code := init.Run([]string{}) 331 if code != 0 { 332 t.Fatal("init failed") 333 } 334 335 t.Run("optional", func(t *testing.T) { 336 view, done := testView(t) 337 c := &AddCommand{ 338 Meta: Meta{ 339 testingOverrides: overrides, 340 View: view, 341 }, 342 } 343 args := []string{"-optional", "test_instance.new"} 344 code := c.Run(args) 345 output := done(t) 346 if code != 0 { 347 t.Fatalf("wrong exit status. Got %d, want 0", code) 348 } 349 350 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 351 # starting point for your resource configuration, with some limitations. 352 # 353 # The behavior of this command may change in future based on feedback, possibly 354 # in incompatible ways. We don't recommend building automation around this 355 # command at this time. If you have feedback about this command, please open 356 # a feature request issue in the Terraform GitHub repository. 357 resource "test_instance" "new" { 358 ami = null # OPTIONAL string 359 disks = [{ # OPTIONAL list of object 360 mount_point = null # REQUIRED string 361 size = null # OPTIONAL string 362 }] 363 id = null # OPTIONAL string 364 value = null # REQUIRED string 365 network_interface { # REQUIRED block 366 description = null # OPTIONAL string 367 device_index = null # OPTIONAL string 368 } 369 } 370 ` 371 372 if !cmp.Equal(output.Stdout(), expected) { 373 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout())) 374 } 375 376 }) 377 378 t.Run("chooses correct provider for root module", func(t *testing.T) { 379 // in the root module of this test fixture, "test" is the local name for "happycorp/test" 380 view, done := testView(t) 381 c := &AddCommand{ 382 Meta: Meta{ 383 testingOverrides: overrides, 384 View: view, 385 }, 386 } 387 args := []string{"test_instance.new"} 388 code := c.Run(args) 389 output := done(t) 390 if code != 0 { 391 t.Fatalf("wrong exit status. Got %d, want 0", code) 392 } 393 394 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 395 # starting point for your resource configuration, with some limitations. 396 # 397 # The behavior of this command may change in future based on feedback, possibly 398 # in incompatible ways. We don't recommend building automation around this 399 # command at this time. If you have feedback about this command, please open 400 # a feature request issue in the Terraform GitHub repository. 401 resource "test_instance" "new" { 402 value = null # REQUIRED string 403 network_interface { # REQUIRED block 404 } 405 } 406 ` 407 408 if !cmp.Equal(output.Stdout(), expected) { 409 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout())) 410 } 411 }) 412 413 t.Run("chooses correct provider for child module", func(t *testing.T) { 414 // in the child module of this test fixture, "test" is a default "hashicorp/test" provider 415 view, done := testView(t) 416 c := &AddCommand{ 417 Meta: Meta{ 418 testingOverrides: overrides, 419 View: view, 420 }, 421 } 422 args := []string{"module.child.test_instance.new"} 423 code := c.Run(args) 424 output := done(t) 425 if code != 0 { 426 t.Fatalf("wrong exit status. Got %d, want 0", code) 427 } 428 429 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 430 # starting point for your resource configuration, with some limitations. 431 # 432 # The behavior of this command may change in future based on feedback, possibly 433 # in incompatible ways. We don't recommend building automation around this 434 # command at this time. If you have feedback about this command, please open 435 # a feature request issue in the Terraform GitHub repository. 436 resource "test_instance" "new" { 437 id = null # REQUIRED string 438 } 439 ` 440 441 if !cmp.Equal(output.Stdout(), expected) { 442 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout())) 443 } 444 }) 445 446 t.Run("chooses correct provider for an unknown module", func(t *testing.T) { 447 // it's weird but ok to use a new/unknown module name; terraform will 448 // fall back on default providers (unless a -provider argument is 449 // supplied) 450 view, done := testView(t) 451 c := &AddCommand{ 452 Meta: Meta{ 453 testingOverrides: overrides, 454 View: view, 455 }, 456 } 457 args := []string{"module.madeup.test_instance.new"} 458 code := c.Run(args) 459 output := done(t) 460 if code != 0 { 461 t.Fatalf("wrong exit status. Got %d, want 0", code) 462 } 463 464 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 465 # starting point for your resource configuration, with some limitations. 466 # 467 # The behavior of this command may change in future based on feedback, possibly 468 # in incompatible ways. We don't recommend building automation around this 469 # command at this time. If you have feedback about this command, please open 470 # a feature request issue in the Terraform GitHub repository. 471 resource "test_instance" "new" { 472 id = null # REQUIRED string 473 } 474 ` 475 476 if !cmp.Equal(output.Stdout(), expected) { 477 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout())) 478 } 479 }) 480 } 481 482 func TestAdd_from_state(t *testing.T) { 483 td := tempDir(t) 484 testCopyDir(t, testFixturePath("add/basic"), td) 485 defer os.RemoveAll(td) 486 defer testChdir(t, td)() 487 488 // write some state 489 testState := states.BuildState(func(s *states.SyncState) { 490 s.SetResourceInstanceCurrent( 491 addrs.Resource{ 492 Mode: addrs.ManagedResourceMode, 493 Type: "test_instance", 494 Name: "new", 495 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 496 &states.ResourceInstanceObjectSrc{ 497 AttrsJSON: []byte("{\"id\":\"bar\",\"ami\":\"ami-123456\",\"disks\":[{\"mount_point\":\"diska\",\"size\":null}],\"value\":\"bloop\"}"), 498 Status: states.ObjectReady, 499 Dependencies: []addrs.ConfigResource{}, 500 }, 501 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 502 ) 503 }) 504 f, err := os.Create("terraform.tfstate") 505 if err != nil { 506 t.Fatalf("failed to create temporary state file: %s", err) 507 } 508 defer f.Close() 509 err = writeStateForTesting(testState, f) 510 if err != nil { 511 t.Fatalf("failed to write state file: %s", err) 512 } 513 514 p := testProvider() 515 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 516 ResourceTypes: map[string]providers.Schema{ 517 "test_instance": { 518 Block: &configschema.Block{ 519 Attributes: map[string]*configschema.Attribute{ 520 "id": {Type: cty.String, Optional: true, Computed: true}, 521 "ami": {Type: cty.String, Optional: true, Description: "the ami to use"}, 522 "value": {Type: cty.String, Required: true, Description: "a value of a thing"}, 523 "disks": { 524 NestedType: &configschema.Object{ 525 Nesting: configschema.NestingList, 526 Attributes: map[string]*configschema.Attribute{ 527 "size": {Type: cty.String, Optional: true}, 528 "mount_point": {Type: cty.String, Required: true}, 529 }, 530 }, 531 Optional: true, 532 }, 533 }, 534 BlockTypes: map[string]*configschema.NestedBlock{ 535 "network_interface": { 536 Nesting: configschema.NestingList, 537 MinItems: 1, 538 Block: configschema.Block{ 539 Attributes: map[string]*configschema.Attribute{ 540 "device_index": {Type: cty.String, Optional: true}, 541 "description": {Type: cty.String, Optional: true}, 542 }, 543 }, 544 }, 545 }, 546 }, 547 }, 548 }, 549 } 550 overrides := &testingOverrides{ 551 Providers: map[addrs.Provider]providers.Factory{ 552 addrs.NewDefaultProvider("test"): providers.FactoryFixed(p), 553 addrs.NewProvider("registry.terraform.io", "happycorp", "test"): providers.FactoryFixed(p), 554 }, 555 } 556 view, done := testView(t) 557 c := &AddCommand{ 558 Meta: Meta{ 559 testingOverrides: overrides, 560 View: view, 561 }, 562 } 563 564 args := []string{"-from-state", "test_instance.new"} 565 code := c.Run(args) 566 output := done(t) 567 if code != 0 { 568 fmt.Println(output.Stderr()) 569 t.Fatalf("wrong exit status. Got %d, want 0", code) 570 } 571 572 expected := `# NOTE: The "terraform add" command is currently experimental and offers only a 573 # starting point for your resource configuration, with some limitations. 574 # 575 # The behavior of this command may change in future based on feedback, possibly 576 # in incompatible ways. We don't recommend building automation around this 577 # command at this time. If you have feedback about this command, please open 578 # a feature request issue in the Terraform GitHub repository. 579 resource "test_instance" "new" { 580 ami = "ami-123456" 581 disks = [ 582 { 583 mount_point = "diska" 584 size = null 585 }, 586 ] 587 id = "bar" 588 value = "bloop" 589 } 590 ` 591 592 if !cmp.Equal(output.Stdout(), expected) { 593 t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout())) 594 } 595 596 if _, err := os.Stat(filepath.Join(td, ".terraform.tfstate.lock.info")); !os.IsNotExist(err) { 597 t.Fatal("state left locked after add") 598 } 599 }