github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/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 }