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