github.com/opentofu/opentofu@v1.7.1/internal/tofu/evaluate_test.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package tofu 7 8 import ( 9 "sync" 10 "testing" 11 12 "github.com/davecgh/go-spew/spew" 13 "github.com/zclconf/go-cty/cty" 14 15 "github.com/opentofu/opentofu/internal/addrs" 16 "github.com/opentofu/opentofu/internal/configs" 17 "github.com/opentofu/opentofu/internal/configs/configschema" 18 "github.com/opentofu/opentofu/internal/lang/marks" 19 "github.com/opentofu/opentofu/internal/plans" 20 "github.com/opentofu/opentofu/internal/providers" 21 "github.com/opentofu/opentofu/internal/states" 22 "github.com/opentofu/opentofu/internal/tfdiags" 23 ) 24 25 func TestEvaluatorGetTerraformAttr(t *testing.T) { 26 evaluator := &Evaluator{ 27 Meta: &ContextMeta{ 28 Env: "foo", 29 }, 30 } 31 data := &evaluationStateData{ 32 Evaluator: evaluator, 33 } 34 scope := evaluator.Scope(data, nil, nil, nil) 35 36 t.Run("workspace", func(t *testing.T) { 37 want := cty.StringVal("foo") 38 got, diags := scope.Data.GetTerraformAttr(addrs.TerraformAttr{ 39 Name: "workspace", 40 }, tfdiags.SourceRange{}) 41 if len(diags) != 0 { 42 t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) 43 } 44 if !got.RawEquals(want) { 45 t.Errorf("wrong result %q; want %q", got, want) 46 } 47 }) 48 } 49 50 func TestEvaluatorGetPathAttr(t *testing.T) { 51 evaluator := &Evaluator{ 52 Meta: &ContextMeta{ 53 Env: "foo", 54 }, 55 Config: &configs.Config{ 56 Module: &configs.Module{ 57 SourceDir: "bar/baz", 58 }, 59 }, 60 } 61 data := &evaluationStateData{ 62 Evaluator: evaluator, 63 } 64 scope := evaluator.Scope(data, nil, nil, nil) 65 66 t.Run("module", func(t *testing.T) { 67 want := cty.StringVal("bar/baz") 68 got, diags := scope.Data.GetPathAttr(addrs.PathAttr{ 69 Name: "module", 70 }, tfdiags.SourceRange{}) 71 if len(diags) != 0 { 72 t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) 73 } 74 if !got.RawEquals(want) { 75 t.Errorf("wrong result %#v; want %#v", got, want) 76 } 77 }) 78 79 t.Run("root", func(t *testing.T) { 80 want := cty.StringVal("bar/baz") 81 got, diags := scope.Data.GetPathAttr(addrs.PathAttr{ 82 Name: "root", 83 }, tfdiags.SourceRange{}) 84 if len(diags) != 0 { 85 t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) 86 } 87 if !got.RawEquals(want) { 88 t.Errorf("wrong result %#v; want %#v", got, want) 89 } 90 }) 91 } 92 93 func TestEvaluatorGetOutputValue(t *testing.T) { 94 evaluator := &Evaluator{ 95 Meta: &ContextMeta{ 96 Env: "foo", 97 }, 98 Config: &configs.Config{ 99 Module: &configs.Module{ 100 Outputs: map[string]*configs.Output{ 101 "some_output": { 102 Name: "some_output", 103 Sensitive: true, 104 }, 105 "some_other_output": { 106 Name: "some_other_output", 107 }, 108 }, 109 }, 110 }, 111 State: states.BuildState(func(state *states.SyncState) { 112 state.SetOutputValue(addrs.AbsOutputValue{ 113 Module: addrs.RootModuleInstance, 114 OutputValue: addrs.OutputValue{ 115 Name: "some_output", 116 }, 117 }, cty.StringVal("first"), true) 118 state.SetOutputValue(addrs.AbsOutputValue{ 119 Module: addrs.RootModuleInstance, 120 OutputValue: addrs.OutputValue{ 121 Name: "some_other_output", 122 }, 123 }, cty.StringVal("second"), false) 124 }).SyncWrapper(), 125 } 126 127 data := &evaluationStateData{ 128 Evaluator: evaluator, 129 } 130 scope := evaluator.Scope(data, nil, nil, nil) 131 132 want := cty.StringVal("first").Mark(marks.Sensitive) 133 got, diags := scope.Data.GetOutput(addrs.OutputValue{ 134 Name: "some_output", 135 }, tfdiags.SourceRange{}) 136 137 if len(diags) != 0 { 138 t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) 139 } 140 if !got.RawEquals(want) { 141 t.Errorf("wrong result %#v; want %#v", got, want) 142 } 143 144 want = cty.StringVal("second") 145 got, diags = scope.Data.GetOutput(addrs.OutputValue{ 146 Name: "some_other_output", 147 }, tfdiags.SourceRange{}) 148 149 if len(diags) != 0 { 150 t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) 151 } 152 if !got.RawEquals(want) { 153 t.Errorf("wrong result %#v; want %#v", got, want) 154 } 155 } 156 157 // This particularly tests that a sensitive attribute in config 158 // results in a value that has a "sensitive" cty Mark 159 func TestEvaluatorGetInputVariable(t *testing.T) { 160 evaluator := &Evaluator{ 161 Meta: &ContextMeta{ 162 Env: "foo", 163 }, 164 Config: &configs.Config{ 165 Module: &configs.Module{ 166 Variables: map[string]*configs.Variable{ 167 "some_var": { 168 Name: "some_var", 169 Sensitive: true, 170 Default: cty.StringVal("foo"), 171 Type: cty.String, 172 ConstraintType: cty.String, 173 }, 174 // Avoid double marking a value 175 "some_other_var": { 176 Name: "some_other_var", 177 Sensitive: true, 178 Default: cty.StringVal("bar"), 179 Type: cty.String, 180 ConstraintType: cty.String, 181 }, 182 }, 183 }, 184 }, 185 VariableValues: map[string]map[string]cty.Value{ 186 "": { 187 "some_var": cty.StringVal("bar"), 188 "some_other_var": cty.StringVal("boop").Mark(marks.Sensitive), 189 }, 190 }, 191 VariableValuesLock: &sync.Mutex{}, 192 } 193 194 data := &evaluationStateData{ 195 Evaluator: evaluator, 196 } 197 scope := evaluator.Scope(data, nil, nil, nil) 198 199 want := cty.StringVal("bar").Mark(marks.Sensitive) 200 got, diags := scope.Data.GetInputVariable(addrs.InputVariable{ 201 Name: "some_var", 202 }, tfdiags.SourceRange{}) 203 204 if len(diags) != 0 { 205 t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) 206 } 207 if !got.RawEquals(want) { 208 t.Errorf("wrong result %#v; want %#v", got, want) 209 } 210 211 want = cty.StringVal("boop").Mark(marks.Sensitive) 212 got, diags = scope.Data.GetInputVariable(addrs.InputVariable{ 213 Name: "some_other_var", 214 }, tfdiags.SourceRange{}) 215 216 if len(diags) != 0 { 217 t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) 218 } 219 if !got.RawEquals(want) { 220 t.Errorf("wrong result %#v; want %#v", got, want) 221 } 222 } 223 224 func TestEvaluatorGetResource(t *testing.T) { 225 stateSync := states.BuildState(func(ss *states.SyncState) { 226 ss.SetResourceInstanceCurrent( 227 addrs.Resource{ 228 Mode: addrs.ManagedResourceMode, 229 Type: "test_resource", 230 Name: "foo", 231 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 232 &states.ResourceInstanceObjectSrc{ 233 Status: states.ObjectReady, 234 AttrsJSON: []byte(`{"id":"foo", "nesting_list": [{"sensitive_value":"abc"}], "nesting_map": {"foo":{"foo":"x"}}, "nesting_set": [{"baz":"abc"}], "nesting_single": {"boop":"abc"}, "nesting_nesting": {"nesting_list":[{"sensitive_value":"abc"}]}, "value":"hello"}`), 235 }, 236 addrs.AbsProviderConfig{ 237 Provider: addrs.NewDefaultProvider("test"), 238 Module: addrs.RootModule, 239 }, 240 ) 241 }).SyncWrapper() 242 243 rc := &configs.Resource{ 244 Mode: addrs.ManagedResourceMode, 245 Type: "test_resource", 246 Name: "foo", 247 Config: configs.SynthBody("", map[string]cty.Value{ 248 "id": cty.StringVal("foo"), 249 }), 250 Provider: addrs.Provider{ 251 Hostname: addrs.DefaultProviderRegistryHost, 252 Namespace: "hashicorp", 253 Type: "test", 254 }, 255 } 256 257 evaluator := &Evaluator{ 258 Meta: &ContextMeta{ 259 Env: "foo", 260 }, 261 Changes: plans.NewChanges().SyncWrapper(), 262 Config: &configs.Config{ 263 Module: &configs.Module{ 264 ManagedResources: map[string]*configs.Resource{ 265 "test_resource.foo": rc, 266 }, 267 }, 268 }, 269 State: stateSync, 270 Plugins: schemaOnlyProvidersForTesting(map[addrs.Provider]providers.ProviderSchema{ 271 addrs.NewDefaultProvider("test"): { 272 ResourceTypes: map[string]providers.Schema{ 273 "test_resource": { 274 Block: &configschema.Block{ 275 Attributes: map[string]*configschema.Attribute{ 276 "id": { 277 Type: cty.String, 278 Computed: true, 279 }, 280 "value": { 281 Type: cty.String, 282 Computed: true, 283 Sensitive: true, 284 }, 285 }, 286 BlockTypes: map[string]*configschema.NestedBlock{ 287 "nesting_list": { 288 Block: configschema.Block{ 289 Attributes: map[string]*configschema.Attribute{ 290 "value": {Type: cty.String, Optional: true}, 291 "sensitive_value": {Type: cty.String, Optional: true, Sensitive: true}, 292 }, 293 }, 294 Nesting: configschema.NestingList, 295 }, 296 "nesting_map": { 297 Block: configschema.Block{ 298 Attributes: map[string]*configschema.Attribute{ 299 "foo": {Type: cty.String, Optional: true, Sensitive: true}, 300 }, 301 }, 302 Nesting: configschema.NestingMap, 303 }, 304 "nesting_set": { 305 Block: configschema.Block{ 306 Attributes: map[string]*configschema.Attribute{ 307 "baz": {Type: cty.String, Optional: true, Sensitive: true}, 308 }, 309 }, 310 Nesting: configschema.NestingSet, 311 }, 312 "nesting_single": { 313 Block: configschema.Block{ 314 Attributes: map[string]*configschema.Attribute{ 315 "boop": {Type: cty.String, Optional: true, Sensitive: true}, 316 }, 317 }, 318 Nesting: configschema.NestingSingle, 319 }, 320 "nesting_nesting": { 321 Block: configschema.Block{ 322 BlockTypes: map[string]*configschema.NestedBlock{ 323 "nesting_list": { 324 Block: configschema.Block{ 325 Attributes: map[string]*configschema.Attribute{ 326 "value": {Type: cty.String, Optional: true}, 327 "sensitive_value": {Type: cty.String, Optional: true, Sensitive: true}, 328 }, 329 }, 330 Nesting: configschema.NestingList, 331 }, 332 }, 333 }, 334 Nesting: configschema.NestingSingle, 335 }, 336 }, 337 }, 338 }, 339 }, 340 }, 341 }, t), 342 } 343 344 data := &evaluationStateData{ 345 Evaluator: evaluator, 346 } 347 scope := evaluator.Scope(data, nil, nil, nil) 348 349 want := cty.ObjectVal(map[string]cty.Value{ 350 "id": cty.StringVal("foo"), 351 "nesting_list": cty.ListVal([]cty.Value{ 352 cty.ObjectVal(map[string]cty.Value{ 353 "sensitive_value": cty.StringVal("abc").Mark(marks.Sensitive), 354 "value": cty.NullVal(cty.String), 355 }), 356 }), 357 "nesting_map": cty.MapVal(map[string]cty.Value{ 358 "foo": cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("x").Mark(marks.Sensitive)}), 359 }), 360 "nesting_nesting": cty.ObjectVal(map[string]cty.Value{ 361 "nesting_list": cty.ListVal([]cty.Value{ 362 cty.ObjectVal(map[string]cty.Value{ 363 "sensitive_value": cty.StringVal("abc").Mark(marks.Sensitive), 364 "value": cty.NullVal(cty.String), 365 }), 366 }), 367 }), 368 "nesting_set": cty.SetVal([]cty.Value{ 369 cty.ObjectVal(map[string]cty.Value{ 370 "baz": cty.StringVal("abc").Mark(marks.Sensitive), 371 }), 372 }), 373 "nesting_single": cty.ObjectVal(map[string]cty.Value{ 374 "boop": cty.StringVal("abc").Mark(marks.Sensitive), 375 }), 376 "value": cty.StringVal("hello").Mark(marks.Sensitive), 377 }) 378 379 addr := addrs.Resource{ 380 Mode: addrs.ManagedResourceMode, 381 Type: "test_resource", 382 Name: "foo", 383 } 384 got, diags := scope.Data.GetResource(addr, tfdiags.SourceRange{}) 385 386 if len(diags) != 0 { 387 t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) 388 } 389 390 if !got.RawEquals(want) { 391 t.Errorf("wrong result:\ngot: %#v\nwant: %#v", got, want) 392 } 393 } 394 395 // GetResource will return a planned object's After value 396 // if there is a change for that resource instance. 397 func TestEvaluatorGetResource_changes(t *testing.T) { 398 // Set up existing state 399 stateSync := states.BuildState(func(ss *states.SyncState) { 400 ss.SetResourceInstanceCurrent( 401 addrs.Resource{ 402 Mode: addrs.ManagedResourceMode, 403 Type: "test_resource", 404 Name: "foo", 405 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 406 &states.ResourceInstanceObjectSrc{ 407 Status: states.ObjectPlanned, 408 AttrsJSON: []byte(`{"id":"foo", "to_mark_val":"tacos", "sensitive_value":"abc"}`), 409 }, 410 addrs.AbsProviderConfig{ 411 Provider: addrs.NewDefaultProvider("test"), 412 Module: addrs.RootModule, 413 }, 414 ) 415 }).SyncWrapper() 416 417 // Create a change for the existing state resource, 418 // to exercise retrieving the After value of the change 419 changesSync := plans.NewChanges().SyncWrapper() 420 change := &plans.ResourceInstanceChange{ 421 Addr: mustResourceInstanceAddr("test_resource.foo"), 422 ProviderAddr: addrs.AbsProviderConfig{ 423 Module: addrs.RootModule, 424 Provider: addrs.NewDefaultProvider("test"), 425 }, 426 Change: plans.Change{ 427 Action: plans.Update, 428 // Provide an After value that contains a marked value 429 After: cty.ObjectVal(map[string]cty.Value{ 430 "id": cty.StringVal("foo"), 431 "to_mark_val": cty.StringVal("pizza").Mark(marks.Sensitive), 432 "sensitive_value": cty.StringVal("abc"), 433 "sensitive_collection": cty.MapVal(map[string]cty.Value{ 434 "boop": cty.StringVal("beep"), 435 }), 436 }), 437 }, 438 } 439 440 // Set up our schemas 441 schemas := &Schemas{ 442 Providers: map[addrs.Provider]providers.ProviderSchema{ 443 addrs.NewDefaultProvider("test"): { 444 ResourceTypes: map[string]providers.Schema{ 445 "test_resource": { 446 Block: &configschema.Block{ 447 Attributes: map[string]*configschema.Attribute{ 448 "id": { 449 Type: cty.String, 450 Computed: true, 451 }, 452 "to_mark_val": { 453 Type: cty.String, 454 Computed: true, 455 }, 456 "sensitive_value": { 457 Type: cty.String, 458 Computed: true, 459 Sensitive: true, 460 }, 461 "sensitive_collection": { 462 Type: cty.Map(cty.String), 463 Computed: true, 464 Sensitive: true, 465 }, 466 }, 467 }, 468 }, 469 }, 470 }, 471 }, 472 } 473 474 // The resource we'll inspect 475 addr := addrs.Resource{ 476 Mode: addrs.ManagedResourceMode, 477 Type: "test_resource", 478 Name: "foo", 479 } 480 schema, _ := schemas.ResourceTypeConfig(addrs.NewDefaultProvider("test"), addr.Mode, addr.Type) 481 // This encoding separates out the After's marks into its AfterValMarks 482 csrc, _ := change.Encode(schema.ImpliedType()) 483 changesSync.AppendResourceInstanceChange(csrc) 484 485 evaluator := &Evaluator{ 486 Meta: &ContextMeta{ 487 Env: "foo", 488 }, 489 Changes: changesSync, 490 Config: &configs.Config{ 491 Module: &configs.Module{ 492 ManagedResources: map[string]*configs.Resource{ 493 "test_resource.foo": { 494 Mode: addrs.ManagedResourceMode, 495 Type: "test_resource", 496 Name: "foo", 497 Provider: addrs.Provider{ 498 Hostname: addrs.DefaultProviderRegistryHost, 499 Namespace: "hashicorp", 500 Type: "test", 501 }, 502 }, 503 }, 504 }, 505 }, 506 State: stateSync, 507 Plugins: schemaOnlyProvidersForTesting(schemas.Providers, t), 508 } 509 510 data := &evaluationStateData{ 511 Evaluator: evaluator, 512 } 513 scope := evaluator.Scope(data, nil, nil, nil) 514 515 want := cty.ObjectVal(map[string]cty.Value{ 516 "id": cty.StringVal("foo"), 517 "to_mark_val": cty.StringVal("pizza").Mark(marks.Sensitive), 518 "sensitive_value": cty.StringVal("abc").Mark(marks.Sensitive), 519 "sensitive_collection": cty.MapVal(map[string]cty.Value{ 520 "boop": cty.StringVal("beep"), 521 }).Mark(marks.Sensitive), 522 }) 523 524 got, diags := scope.Data.GetResource(addr, tfdiags.SourceRange{}) 525 526 if len(diags) != 0 { 527 t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) 528 } 529 530 if !got.RawEquals(want) { 531 t.Errorf("wrong result:\ngot: %#v\nwant: %#v", got, want) 532 } 533 } 534 535 func TestEvaluatorGetModule(t *testing.T) { 536 // Create a new evaluator with an existing state 537 stateSync := states.BuildState(func(ss *states.SyncState) { 538 ss.SetOutputValue( 539 addrs.OutputValue{Name: "out"}.Absolute(addrs.ModuleInstance{addrs.ModuleInstanceStep{Name: "mod"}}), 540 cty.StringVal("bar"), 541 true, 542 ) 543 }).SyncWrapper() 544 evaluator := evaluatorForModule(stateSync, plans.NewChanges().SyncWrapper()) 545 data := &evaluationStateData{ 546 Evaluator: evaluator, 547 } 548 scope := evaluator.Scope(data, nil, nil, nil) 549 want := cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("bar").Mark(marks.Sensitive)}) 550 got, diags := scope.Data.GetModule(addrs.ModuleCall{ 551 Name: "mod", 552 }, tfdiags.SourceRange{}) 553 554 if len(diags) != 0 { 555 t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) 556 } 557 if !got.RawEquals(want) { 558 t.Errorf("wrong result %#v; want %#v", got, want) 559 } 560 561 // Changes should override the state value 562 changesSync := plans.NewChanges().SyncWrapper() 563 change := &plans.OutputChange{ 564 Addr: addrs.OutputValue{Name: "out"}.Absolute(addrs.ModuleInstance{addrs.ModuleInstanceStep{Name: "mod"}}), 565 Sensitive: true, 566 Change: plans.Change{ 567 After: cty.StringVal("baz"), 568 }, 569 } 570 cs, _ := change.Encode() 571 changesSync.AppendOutputChange(cs) 572 evaluator = evaluatorForModule(stateSync, changesSync) 573 data = &evaluationStateData{ 574 Evaluator: evaluator, 575 } 576 scope = evaluator.Scope(data, nil, nil, nil) 577 want = cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("baz").Mark(marks.Sensitive)}) 578 got, diags = scope.Data.GetModule(addrs.ModuleCall{ 579 Name: "mod", 580 }, tfdiags.SourceRange{}) 581 582 if len(diags) != 0 { 583 t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) 584 } 585 if !got.RawEquals(want) { 586 t.Errorf("wrong result %#v; want %#v", got, want) 587 } 588 589 // Test changes with empty state 590 evaluator = evaluatorForModule(states.NewState().SyncWrapper(), changesSync) 591 data = &evaluationStateData{ 592 Evaluator: evaluator, 593 } 594 scope = evaluator.Scope(data, nil, nil, nil) 595 want = cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("baz").Mark(marks.Sensitive)}) 596 got, diags = scope.Data.GetModule(addrs.ModuleCall{ 597 Name: "mod", 598 }, tfdiags.SourceRange{}) 599 600 if len(diags) != 0 { 601 t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) 602 } 603 if !got.RawEquals(want) { 604 t.Errorf("wrong result %#v; want %#v", got, want) 605 } 606 } 607 608 func evaluatorForModule(stateSync *states.SyncState, changesSync *plans.ChangesSync) *Evaluator { 609 return &Evaluator{ 610 Meta: &ContextMeta{ 611 Env: "foo", 612 }, 613 Config: &configs.Config{ 614 Module: &configs.Module{ 615 ModuleCalls: map[string]*configs.ModuleCall{ 616 "mod": { 617 Name: "mod", 618 }, 619 }, 620 }, 621 Children: map[string]*configs.Config{ 622 "mod": { 623 Path: addrs.Module{"module.mod"}, 624 Module: &configs.Module{ 625 Outputs: map[string]*configs.Output{ 626 "out": { 627 Name: "out", 628 Sensitive: true, 629 }, 630 }, 631 }, 632 }, 633 }, 634 }, 635 State: stateSync, 636 Changes: changesSync, 637 } 638 }