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  }