github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/cloud/backend_context_test.go (about)

     1  package cloud
     2  
     3  import (
     4  	"context"
     5  	"reflect"
     6  	"testing"
     7  
     8  	tfe "github.com/hashicorp/go-tfe"
     9  	"github.com/hashicorp/terraform/internal/backend"
    10  	"github.com/hashicorp/terraform/internal/command/arguments"
    11  	"github.com/hashicorp/terraform/internal/command/clistate"
    12  	"github.com/hashicorp/terraform/internal/command/views"
    13  	"github.com/hashicorp/terraform/internal/configs"
    14  	"github.com/hashicorp/terraform/internal/initwd"
    15  	"github.com/hashicorp/terraform/internal/states/statemgr"
    16  	"github.com/hashicorp/terraform/internal/terminal"
    17  	"github.com/hashicorp/terraform/internal/terraform"
    18  	"github.com/hashicorp/terraform/internal/tfdiags"
    19  	"github.com/zclconf/go-cty/cty"
    20  )
    21  
    22  func TestRemoteStoredVariableValue(t *testing.T) {
    23  	tests := map[string]struct {
    24  		Def       *tfe.Variable
    25  		Want      cty.Value
    26  		WantError string
    27  	}{
    28  		"string literal": {
    29  			&tfe.Variable{
    30  				Key:       "test",
    31  				Value:     "foo",
    32  				HCL:       false,
    33  				Sensitive: false,
    34  			},
    35  			cty.StringVal("foo"),
    36  			``,
    37  		},
    38  		"string HCL": {
    39  			&tfe.Variable{
    40  				Key:       "test",
    41  				Value:     `"foo"`,
    42  				HCL:       true,
    43  				Sensitive: false,
    44  			},
    45  			cty.StringVal("foo"),
    46  			``,
    47  		},
    48  		"list HCL": {
    49  			&tfe.Variable{
    50  				Key:       "test",
    51  				Value:     `[]`,
    52  				HCL:       true,
    53  				Sensitive: false,
    54  			},
    55  			cty.EmptyTupleVal,
    56  			``,
    57  		},
    58  		"null HCL": {
    59  			&tfe.Variable{
    60  				Key:       "test",
    61  				Value:     `null`,
    62  				HCL:       true,
    63  				Sensitive: false,
    64  			},
    65  			cty.NullVal(cty.DynamicPseudoType),
    66  			``,
    67  		},
    68  		"literal sensitive": {
    69  			&tfe.Variable{
    70  				Key:       "test",
    71  				HCL:       false,
    72  				Sensitive: true,
    73  			},
    74  			cty.UnknownVal(cty.String),
    75  			``,
    76  		},
    77  		"HCL sensitive": {
    78  			&tfe.Variable{
    79  				Key:       "test",
    80  				HCL:       true,
    81  				Sensitive: true,
    82  			},
    83  			cty.DynamicVal,
    84  			``,
    85  		},
    86  		"HCL computation": {
    87  			// This (stored expressions containing computation) is not a case
    88  			// we intentionally supported, but it became possible for remote
    89  			// operations in Terraform 0.12 (due to Terraform Cloud/Enterprise
    90  			// just writing the HCL verbatim into generated `.tfvars` files).
    91  			// We support it here for consistency, and we continue to support
    92  			// it in both places for backward-compatibility. In practice,
    93  			// there's little reason to do computation in a stored variable
    94  			// value because references are not supported.
    95  			&tfe.Variable{
    96  				Key:       "test",
    97  				Value:     `[for v in ["a"] : v]`,
    98  				HCL:       true,
    99  				Sensitive: false,
   100  			},
   101  			cty.TupleVal([]cty.Value{cty.StringVal("a")}),
   102  			``,
   103  		},
   104  		"HCL syntax error": {
   105  			&tfe.Variable{
   106  				Key:       "test",
   107  				Value:     `[`,
   108  				HCL:       true,
   109  				Sensitive: false,
   110  			},
   111  			cty.DynamicVal,
   112  			`Invalid expression for var.test: The value of variable "test" is marked in the remote workspace as being specified in HCL syntax, but the given value is not valid HCL. Stored variable values must be valid literal expressions and may not contain references to other variables or calls to functions.`,
   113  		},
   114  		"HCL with references": {
   115  			&tfe.Variable{
   116  				Key:       "test",
   117  				Value:     `foo.bar`,
   118  				HCL:       true,
   119  				Sensitive: false,
   120  			},
   121  			cty.DynamicVal,
   122  			`Invalid expression for var.test: The value of variable "test" is marked in the remote workspace as being specified in HCL syntax, but the given value is not valid HCL. Stored variable values must be valid literal expressions and may not contain references to other variables or calls to functions.`,
   123  		},
   124  	}
   125  
   126  	for name, test := range tests {
   127  		t.Run(name, func(t *testing.T) {
   128  			v := &remoteStoredVariableValue{
   129  				definition: test.Def,
   130  			}
   131  			// This ParseVariableValue implementation ignores the parsing mode,
   132  			// so we'll just always parse literal here. (The parsing mode is
   133  			// selected by the remote server, not by our local configuration.)
   134  			gotIV, diags := v.ParseVariableValue(configs.VariableParseLiteral)
   135  			if test.WantError != "" {
   136  				if !diags.HasErrors() {
   137  					t.Fatalf("missing expected error\ngot:  <no error>\nwant: %s", test.WantError)
   138  				}
   139  				errStr := diags.Err().Error()
   140  				if errStr != test.WantError {
   141  					t.Fatalf("wrong error\ngot:  %s\nwant: %s", errStr, test.WantError)
   142  				}
   143  			} else {
   144  				if diags.HasErrors() {
   145  					t.Fatalf("unexpected error\ngot:  %s\nwant: <no error>", diags.Err().Error())
   146  				}
   147  				got := gotIV.Value
   148  				if !test.Want.RawEquals(got) {
   149  					t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
   150  				}
   151  			}
   152  		})
   153  	}
   154  }
   155  
   156  func TestRemoteContextWithVars(t *testing.T) {
   157  	catTerraform := tfe.CategoryTerraform
   158  	catEnv := tfe.CategoryEnv
   159  
   160  	tests := map[string]struct {
   161  		Opts      *tfe.VariableCreateOptions
   162  		WantError string
   163  	}{
   164  		"Terraform variable": {
   165  			&tfe.VariableCreateOptions{
   166  				Category: &catTerraform,
   167  			},
   168  			`Value for undeclared variable: A variable named "key" was assigned a value, but the root module does not declare a variable of that name. To use this value, add a "variable" block to the configuration.`,
   169  		},
   170  		"environment variable": {
   171  			&tfe.VariableCreateOptions{
   172  				Category: &catEnv,
   173  			},
   174  			``,
   175  		},
   176  	}
   177  
   178  	for name, test := range tests {
   179  		t.Run(name, func(t *testing.T) {
   180  			configDir := "./testdata/empty"
   181  
   182  			b, bCleanup := testBackendWithName(t)
   183  			defer bCleanup()
   184  
   185  			_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
   186  			defer configCleanup()
   187  
   188  			workspaceID, err := b.getRemoteWorkspaceID(context.Background(), testBackendSingleWorkspaceName)
   189  			if err != nil {
   190  				t.Fatal(err)
   191  			}
   192  
   193  			streams, _ := terminal.StreamsForTesting(t)
   194  			view := views.NewStateLocker(arguments.ViewHuman, views.NewView(streams))
   195  
   196  			op := &backend.Operation{
   197  				ConfigDir:    configDir,
   198  				ConfigLoader: configLoader,
   199  				StateLocker:  clistate.NewLocker(0, view),
   200  				Workspace:    testBackendSingleWorkspaceName,
   201  			}
   202  
   203  			v := test.Opts
   204  			if v.Key == nil {
   205  				key := "key"
   206  				v.Key = &key
   207  			}
   208  			b.client.Variables.Create(context.TODO(), workspaceID, *v)
   209  
   210  			_, _, diags := b.LocalRun(op)
   211  
   212  			if test.WantError != "" {
   213  				if !diags.HasErrors() {
   214  					t.Fatalf("missing expected error\ngot:  <no error>\nwant: %s", test.WantError)
   215  				}
   216  				errStr := diags.Err().Error()
   217  				if errStr != test.WantError {
   218  					t.Fatalf("wrong error\ngot:  %s\nwant: %s", errStr, test.WantError)
   219  				}
   220  				// When Context() returns an error, it should unlock the state,
   221  				// so re-locking it is expected to succeed.
   222  				stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName)
   223  				if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
   224  					t.Fatalf("unexpected error locking state: %s", err.Error())
   225  				}
   226  			} else {
   227  				if diags.HasErrors() {
   228  					t.Fatalf("unexpected error\ngot:  %s\nwant: <no error>", diags.Err().Error())
   229  				}
   230  				// When Context() succeeds, this should fail w/ "workspace already locked"
   231  				stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName)
   232  				if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err == nil {
   233  					t.Fatal("unexpected success locking state after Context")
   234  				}
   235  			}
   236  		})
   237  	}
   238  }
   239  
   240  func TestRemoteVariablesDoNotOverride(t *testing.T) {
   241  	catTerraform := tfe.CategoryTerraform
   242  
   243  	varName1 := "key1"
   244  	varName2 := "key2"
   245  	varName3 := "key3"
   246  
   247  	varValue1 := "value1"
   248  	varValue2 := "value2"
   249  	varValue3 := "value3"
   250  
   251  	tests := map[string]struct {
   252  		localVariables    map[string]backend.UnparsedVariableValue
   253  		remoteVariables   []*tfe.VariableCreateOptions
   254  		expectedVariables terraform.InputValues
   255  	}{
   256  		"no local variables": {
   257  			map[string]backend.UnparsedVariableValue{},
   258  			[]*tfe.VariableCreateOptions{
   259  				{
   260  					Key:      &varName1,
   261  					Value:    &varValue1,
   262  					Category: &catTerraform,
   263  				},
   264  				{
   265  					Key:      &varName2,
   266  					Value:    &varValue2,
   267  					Category: &catTerraform,
   268  				},
   269  				{
   270  					Key:      &varName3,
   271  					Value:    &varValue3,
   272  					Category: &catTerraform,
   273  				},
   274  			},
   275  			terraform.InputValues{
   276  				varName1: &terraform.InputValue{
   277  					Value:      cty.StringVal(varValue1),
   278  					SourceType: terraform.ValueFromInput,
   279  					SourceRange: tfdiags.SourceRange{
   280  						Filename: "",
   281  						Start:    tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
   282  						End:      tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
   283  					},
   284  				},
   285  				varName2: &terraform.InputValue{
   286  					Value:      cty.StringVal(varValue2),
   287  					SourceType: terraform.ValueFromInput,
   288  					SourceRange: tfdiags.SourceRange{
   289  						Filename: "",
   290  						Start:    tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
   291  						End:      tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
   292  					},
   293  				},
   294  				varName3: &terraform.InputValue{
   295  					Value:      cty.StringVal(varValue3),
   296  					SourceType: terraform.ValueFromInput,
   297  					SourceRange: tfdiags.SourceRange{
   298  						Filename: "",
   299  						Start:    tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
   300  						End:      tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
   301  					},
   302  				},
   303  			},
   304  		},
   305  		"single conflicting local variable": {
   306  			map[string]backend.UnparsedVariableValue{
   307  				varName3: testUnparsedVariableValue{source: terraform.ValueFromNamedFile, value: cty.StringVal(varValue3)},
   308  			},
   309  			[]*tfe.VariableCreateOptions{
   310  				{
   311  					Key:      &varName1,
   312  					Value:    &varValue1,
   313  					Category: &catTerraform,
   314  				}, {
   315  					Key:      &varName2,
   316  					Value:    &varValue2,
   317  					Category: &catTerraform,
   318  				}, {
   319  					Key:      &varName3,
   320  					Value:    &varValue3,
   321  					Category: &catTerraform,
   322  				},
   323  			},
   324  			terraform.InputValues{
   325  				varName1: &terraform.InputValue{
   326  					Value:      cty.StringVal(varValue1),
   327  					SourceType: terraform.ValueFromInput,
   328  					SourceRange: tfdiags.SourceRange{
   329  						Filename: "",
   330  						Start:    tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
   331  						End:      tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
   332  					},
   333  				},
   334  				varName2: &terraform.InputValue{
   335  					Value:      cty.StringVal(varValue2),
   336  					SourceType: terraform.ValueFromInput,
   337  					SourceRange: tfdiags.SourceRange{
   338  						Filename: "",
   339  						Start:    tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
   340  						End:      tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
   341  					},
   342  				},
   343  				varName3: &terraform.InputValue{
   344  					Value:      cty.StringVal(varValue3),
   345  					SourceType: terraform.ValueFromNamedFile,
   346  					SourceRange: tfdiags.SourceRange{
   347  						Filename: "fake.tfvars",
   348  						Start:    tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
   349  						End:      tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
   350  					},
   351  				},
   352  			},
   353  		},
   354  		"no conflicting local variable": {
   355  			map[string]backend.UnparsedVariableValue{
   356  				varName3: testUnparsedVariableValue{source: terraform.ValueFromNamedFile, value: cty.StringVal(varValue3)},
   357  			},
   358  			[]*tfe.VariableCreateOptions{
   359  				{
   360  					Key:      &varName1,
   361  					Value:    &varValue1,
   362  					Category: &catTerraform,
   363  				}, {
   364  					Key:      &varName2,
   365  					Value:    &varValue2,
   366  					Category: &catTerraform,
   367  				},
   368  			},
   369  			terraform.InputValues{
   370  				varName1: &terraform.InputValue{
   371  					Value:      cty.StringVal(varValue1),
   372  					SourceType: terraform.ValueFromInput,
   373  					SourceRange: tfdiags.SourceRange{
   374  						Filename: "",
   375  						Start:    tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
   376  						End:      tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
   377  					},
   378  				},
   379  				varName2: &terraform.InputValue{
   380  					Value:      cty.StringVal(varValue2),
   381  					SourceType: terraform.ValueFromInput,
   382  					SourceRange: tfdiags.SourceRange{
   383  						Filename: "",
   384  						Start:    tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
   385  						End:      tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
   386  					},
   387  				},
   388  				varName3: &terraform.InputValue{
   389  					Value:      cty.StringVal(varValue3),
   390  					SourceType: terraform.ValueFromNamedFile,
   391  					SourceRange: tfdiags.SourceRange{
   392  						Filename: "fake.tfvars",
   393  						Start:    tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
   394  						End:      tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
   395  					},
   396  				},
   397  			},
   398  		},
   399  	}
   400  
   401  	for name, test := range tests {
   402  		t.Run(name, func(t *testing.T) {
   403  			configDir := "./testdata/variables"
   404  
   405  			b, bCleanup := testBackendWithName(t)
   406  			defer bCleanup()
   407  
   408  			_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
   409  			defer configCleanup()
   410  
   411  			workspaceID, err := b.getRemoteWorkspaceID(context.Background(), testBackendSingleWorkspaceName)
   412  			if err != nil {
   413  				t.Fatal(err)
   414  			}
   415  
   416  			streams, _ := terminal.StreamsForTesting(t)
   417  			view := views.NewStateLocker(arguments.ViewHuman, views.NewView(streams))
   418  
   419  			op := &backend.Operation{
   420  				ConfigDir:    configDir,
   421  				ConfigLoader: configLoader,
   422  				StateLocker:  clistate.NewLocker(0, view),
   423  				Workspace:    testBackendSingleWorkspaceName,
   424  				Variables:    test.localVariables,
   425  			}
   426  
   427  			for _, v := range test.remoteVariables {
   428  				b.client.Variables.Create(context.TODO(), workspaceID, *v)
   429  			}
   430  
   431  			lr, _, diags := b.LocalRun(op)
   432  
   433  			if diags.HasErrors() {
   434  				t.Fatalf("unexpected error\ngot:  %s\nwant: <no error>", diags.Err().Error())
   435  			}
   436  			// When Context() succeeds, this should fail w/ "workspace already locked"
   437  			stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName)
   438  			if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err == nil {
   439  				t.Fatal("unexpected success locking state after Context")
   440  			}
   441  
   442  			actual := lr.PlanOpts.SetVariables
   443  			expected := test.expectedVariables
   444  
   445  			for expectedKey := range expected {
   446  				actualValue := actual[expectedKey]
   447  				expectedValue := expected[expectedKey]
   448  
   449  				if !reflect.DeepEqual(*actualValue, *expectedValue) {
   450  					t.Fatalf("unexpected variable '%s'\ngot:  %v\nwant: %v", expectedKey, actualValue, expectedValue)
   451  				}
   452  			}
   453  		})
   454  	}
   455  }