github.com/opentofu/opentofu@v1.7.1/internal/tofu/node_resource_validate_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 "errors" 10 "strings" 11 "testing" 12 13 "github.com/hashicorp/hcl/v2" 14 "github.com/hashicorp/hcl/v2/hcltest" 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/providers" 20 "github.com/opentofu/opentofu/internal/provisioners" 21 "github.com/opentofu/opentofu/internal/tfdiags" 22 "github.com/zclconf/go-cty/cty" 23 ) 24 25 func TestNodeValidatableResource_ValidateProvisioner_valid(t *testing.T) { 26 ctx := &MockEvalContext{} 27 ctx.installSimpleEval() 28 mp := &MockProvisioner{} 29 ps := &configschema.Block{} 30 ctx.ProvisionerSchemaSchema = ps 31 ctx.ProvisionerProvisioner = mp 32 33 pc := &configs.Provisioner{ 34 Type: "baz", 35 Config: hcl.EmptyBody(), 36 Connection: &configs.Connection{ 37 Config: configs.SynthBody("", map[string]cty.Value{ 38 "host": cty.StringVal("localhost"), 39 "type": cty.StringVal("ssh"), 40 "port": cty.NumberIntVal(10022), 41 }), 42 }, 43 } 44 45 rc := &configs.Resource{ 46 Mode: addrs.ManagedResourceMode, 47 Type: "test_foo", 48 Name: "bar", 49 Config: configs.SynthBody("", map[string]cty.Value{}), 50 } 51 52 node := NodeValidatableResource{ 53 NodeAbstractResource: &NodeAbstractResource{ 54 Addr: mustConfigResourceAddr("test_foo.bar"), 55 Config: rc, 56 }, 57 } 58 59 diags := node.validateProvisioner(ctx, pc) 60 if diags.HasErrors() { 61 t.Fatalf("node.Eval failed: %s", diags.Err()) 62 } 63 if !mp.ValidateProvisionerConfigCalled { 64 t.Fatalf("p.ValidateProvisionerConfig not called") 65 } 66 } 67 68 func TestNodeValidatableResource_ValidateProvisioner__warning(t *testing.T) { 69 ctx := &MockEvalContext{} 70 ctx.installSimpleEval() 71 mp := &MockProvisioner{} 72 ps := &configschema.Block{} 73 ctx.ProvisionerSchemaSchema = ps 74 ctx.ProvisionerProvisioner = mp 75 76 pc := &configs.Provisioner{ 77 Type: "baz", 78 Config: hcl.EmptyBody(), 79 } 80 81 rc := &configs.Resource{ 82 Mode: addrs.ManagedResourceMode, 83 Type: "test_foo", 84 Name: "bar", 85 Config: configs.SynthBody("", map[string]cty.Value{}), 86 Managed: &configs.ManagedResource{}, 87 } 88 89 node := NodeValidatableResource{ 90 NodeAbstractResource: &NodeAbstractResource{ 91 Addr: mustConfigResourceAddr("test_foo.bar"), 92 Config: rc, 93 }, 94 } 95 96 { 97 var diags tfdiags.Diagnostics 98 diags = diags.Append(tfdiags.SimpleWarning("foo is deprecated")) 99 mp.ValidateProvisionerConfigResponse = provisioners.ValidateProvisionerConfigResponse{ 100 Diagnostics: diags, 101 } 102 } 103 104 diags := node.validateProvisioner(ctx, pc) 105 if len(diags) != 1 { 106 t.Fatalf("wrong number of diagnostics in %s; want one warning", diags.ErrWithWarnings()) 107 } 108 109 if got, want := diags[0].Description().Summary, mp.ValidateProvisionerConfigResponse.Diagnostics[0].Description().Summary; got != want { 110 t.Fatalf("wrong warning %q; want %q", got, want) 111 } 112 } 113 114 func TestNodeValidatableResource_ValidateProvisioner__connectionInvalid(t *testing.T) { 115 ctx := &MockEvalContext{} 116 ctx.installSimpleEval() 117 mp := &MockProvisioner{} 118 ps := &configschema.Block{} 119 ctx.ProvisionerSchemaSchema = ps 120 ctx.ProvisionerProvisioner = mp 121 122 pc := &configs.Provisioner{ 123 Type: "baz", 124 Config: hcl.EmptyBody(), 125 Connection: &configs.Connection{ 126 Config: configs.SynthBody("", map[string]cty.Value{ 127 "type": cty.StringVal("ssh"), 128 "bananananananana": cty.StringVal("foo"), 129 "bazaz": cty.StringVal("bar"), 130 }), 131 }, 132 } 133 134 rc := &configs.Resource{ 135 Mode: addrs.ManagedResourceMode, 136 Type: "test_foo", 137 Name: "bar", 138 Config: configs.SynthBody("", map[string]cty.Value{}), 139 Managed: &configs.ManagedResource{}, 140 } 141 142 node := NodeValidatableResource{ 143 NodeAbstractResource: &NodeAbstractResource{ 144 Addr: mustConfigResourceAddr("test_foo.bar"), 145 Config: rc, 146 }, 147 } 148 149 diags := node.validateProvisioner(ctx, pc) 150 if !diags.HasErrors() { 151 t.Fatalf("node.Eval succeeded; want error") 152 } 153 if len(diags) != 3 { 154 t.Fatalf("wrong number of diagnostics; want two errors\n\n%s", diags.Err()) 155 } 156 157 errStr := diags.Err().Error() 158 if !(strings.Contains(errStr, "bananananananana") && strings.Contains(errStr, "bazaz")) { 159 t.Fatalf("wrong errors %q; want something about each of our invalid connInfo keys", errStr) 160 } 161 } 162 163 func TestNodeValidatableResource_ValidateResource_managedResource(t *testing.T) { 164 mp := simpleMockProvider() 165 mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { 166 if got, want := req.TypeName, "test_object"; got != want { 167 t.Fatalf("wrong resource type\ngot: %#v\nwant: %#v", got, want) 168 } 169 if got, want := req.Config.GetAttr("test_string"), cty.StringVal("bar"); !got.RawEquals(want) { 170 t.Fatalf("wrong value for test_string\ngot: %#v\nwant: %#v", got, want) 171 } 172 if got, want := req.Config.GetAttr("test_number"), cty.NumberIntVal(2); !got.RawEquals(want) { 173 t.Fatalf("wrong value for test_number\ngot: %#v\nwant: %#v", got, want) 174 } 175 return providers.ValidateResourceConfigResponse{} 176 } 177 178 p := providers.Interface(mp) 179 rc := &configs.Resource{ 180 Mode: addrs.ManagedResourceMode, 181 Type: "test_object", 182 Name: "foo", 183 Config: configs.SynthBody("", map[string]cty.Value{ 184 "test_string": cty.StringVal("bar"), 185 "test_number": cty.NumberIntVal(2).Mark(marks.Sensitive), 186 }), 187 } 188 node := NodeValidatableResource{ 189 NodeAbstractResource: &NodeAbstractResource{ 190 Addr: mustConfigResourceAddr("test_foo.bar"), 191 Config: rc, 192 ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 193 }, 194 } 195 196 ctx := &MockEvalContext{} 197 ctx.installSimpleEval() 198 ctx.ProviderSchemaSchema = mp.GetProviderSchema() 199 ctx.ProviderProvider = p 200 201 err := node.validateResource(ctx) 202 if err != nil { 203 t.Fatalf("err: %s", err) 204 } 205 206 if !mp.ValidateResourceConfigCalled { 207 t.Fatal("Expected ValidateResourceConfig to be called, but it was not!") 208 } 209 } 210 211 func TestNodeValidatableResource_ValidateResource_managedResourceCount(t *testing.T) { 212 // Setup 213 mp := simpleMockProvider() 214 mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { 215 if got, want := req.TypeName, "test_object"; got != want { 216 t.Fatalf("wrong resource type\ngot: %#v\nwant: %#v", got, want) 217 } 218 if got, want := req.Config.GetAttr("test_string"), cty.StringVal("bar"); !got.RawEquals(want) { 219 t.Fatalf("wrong value for test_string\ngot: %#v\nwant: %#v", got, want) 220 } 221 return providers.ValidateResourceConfigResponse{} 222 } 223 224 p := providers.Interface(mp) 225 226 ctx := &MockEvalContext{} 227 ctx.installSimpleEval() 228 ctx.ProviderSchemaSchema = mp.GetProviderSchema() 229 ctx.ProviderProvider = p 230 231 tests := []struct { 232 name string 233 count hcl.Expression 234 }{ 235 { 236 "simple count", 237 hcltest.MockExprLiteral(cty.NumberIntVal(2)), 238 }, 239 { 240 "marked count value", 241 hcltest.MockExprLiteral(cty.NumberIntVal(3).Mark("marked")), 242 }, 243 } 244 245 for _, test := range tests { 246 t.Run(test.name, func(t *testing.T) { 247 rc := &configs.Resource{ 248 Mode: addrs.ManagedResourceMode, 249 Type: "test_object", 250 Name: "foo", 251 Count: test.count, 252 Config: configs.SynthBody("", map[string]cty.Value{ 253 "test_string": cty.StringVal("bar"), 254 }), 255 } 256 node := NodeValidatableResource{ 257 NodeAbstractResource: &NodeAbstractResource{ 258 Addr: mustConfigResourceAddr("test_foo.bar"), 259 Config: rc, 260 ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 261 }, 262 } 263 264 diags := node.validateResource(ctx) 265 if diags.HasErrors() { 266 t.Fatalf("err: %s", diags.Err()) 267 } 268 269 if !mp.ValidateResourceConfigCalled { 270 t.Fatal("Expected ValidateResourceConfig to be called, but it was not!") 271 } 272 }) 273 } 274 } 275 276 func TestNodeValidatableResource_ValidateResource_dataSource(t *testing.T) { 277 mp := simpleMockProvider() 278 mp.ValidateDataResourceConfigFn = func(req providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse { 279 if got, want := req.TypeName, "test_object"; got != want { 280 t.Fatalf("wrong resource type\ngot: %#v\nwant: %#v", got, want) 281 } 282 if got, want := req.Config.GetAttr("test_string"), cty.StringVal("bar"); !got.RawEquals(want) { 283 t.Fatalf("wrong value for test_string\ngot: %#v\nwant: %#v", got, want) 284 } 285 if got, want := req.Config.GetAttr("test_number"), cty.NumberIntVal(2); !got.RawEquals(want) { 286 t.Fatalf("wrong value for test_number\ngot: %#v\nwant: %#v", got, want) 287 } 288 return providers.ValidateDataResourceConfigResponse{} 289 } 290 291 p := providers.Interface(mp) 292 rc := &configs.Resource{ 293 Mode: addrs.DataResourceMode, 294 Type: "test_object", 295 Name: "foo", 296 Config: configs.SynthBody("", map[string]cty.Value{ 297 "test_string": cty.StringVal("bar"), 298 "test_number": cty.NumberIntVal(2).Mark(marks.Sensitive), 299 }), 300 } 301 302 node := NodeValidatableResource{ 303 NodeAbstractResource: &NodeAbstractResource{ 304 Addr: mustConfigResourceAddr("test_foo.bar"), 305 Config: rc, 306 ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 307 }, 308 } 309 310 ctx := &MockEvalContext{} 311 ctx.installSimpleEval() 312 ctx.ProviderSchemaSchema = mp.GetProviderSchema() 313 ctx.ProviderProvider = p 314 315 diags := node.validateResource(ctx) 316 if diags.HasErrors() { 317 t.Fatalf("err: %s", diags.Err()) 318 } 319 320 if !mp.ValidateDataResourceConfigCalled { 321 t.Fatal("Expected ValidateDataSourceConfig to be called, but it was not!") 322 } 323 } 324 325 func TestNodeValidatableResource_ValidateResource_valid(t *testing.T) { 326 mp := simpleMockProvider() 327 mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { 328 return providers.ValidateResourceConfigResponse{} 329 } 330 331 p := providers.Interface(mp) 332 rc := &configs.Resource{ 333 Mode: addrs.ManagedResourceMode, 334 Type: "test_object", 335 Name: "foo", 336 Config: configs.SynthBody("", map[string]cty.Value{}), 337 } 338 node := NodeValidatableResource{ 339 NodeAbstractResource: &NodeAbstractResource{ 340 Addr: mustConfigResourceAddr("test_object.foo"), 341 Config: rc, 342 ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 343 }, 344 } 345 346 ctx := &MockEvalContext{} 347 ctx.installSimpleEval() 348 ctx.ProviderSchemaSchema = mp.GetProviderSchema() 349 ctx.ProviderProvider = p 350 351 diags := node.validateResource(ctx) 352 if diags.HasErrors() { 353 t.Fatalf("err: %s", diags.Err()) 354 } 355 } 356 357 func TestNodeValidatableResource_ValidateResource_warningsAndErrorsPassedThrough(t *testing.T) { 358 mp := simpleMockProvider() 359 mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { 360 var diags tfdiags.Diagnostics 361 diags = diags.Append(tfdiags.SimpleWarning("warn")) 362 diags = diags.Append(errors.New("err")) 363 return providers.ValidateResourceConfigResponse{ 364 Diagnostics: diags, 365 } 366 } 367 368 p := providers.Interface(mp) 369 rc := &configs.Resource{ 370 Mode: addrs.ManagedResourceMode, 371 Type: "test_object", 372 Name: "foo", 373 Config: configs.SynthBody("", map[string]cty.Value{}), 374 } 375 node := NodeValidatableResource{ 376 NodeAbstractResource: &NodeAbstractResource{ 377 Addr: mustConfigResourceAddr("test_foo.bar"), 378 Config: rc, 379 ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 380 }, 381 } 382 383 ctx := &MockEvalContext{} 384 ctx.installSimpleEval() 385 ctx.ProviderSchemaSchema = mp.GetProviderSchema() 386 ctx.ProviderProvider = p 387 388 diags := node.validateResource(ctx) 389 if !diags.HasErrors() { 390 t.Fatal("unexpected success; want error") 391 } 392 393 bySeverity := map[tfdiags.Severity]tfdiags.Diagnostics{} 394 for _, diag := range diags { 395 bySeverity[diag.Severity()] = append(bySeverity[diag.Severity()], diag) 396 } 397 if len(bySeverity[tfdiags.Warning]) != 1 || bySeverity[tfdiags.Warning][0].Description().Summary != "warn" { 398 t.Errorf("Expected 1 warning 'warn', got: %s", bySeverity[tfdiags.Warning].ErrWithWarnings()) 399 } 400 if len(bySeverity[tfdiags.Error]) != 1 || bySeverity[tfdiags.Error][0].Description().Summary != "err" { 401 t.Errorf("Expected 1 error 'err', got: %s", bySeverity[tfdiags.Error].Err()) 402 } 403 } 404 405 func TestNodeValidatableResource_ValidateResource_invalidDependsOn(t *testing.T) { 406 mp := simpleMockProvider() 407 mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { 408 return providers.ValidateResourceConfigResponse{} 409 } 410 411 // We'll check a _valid_ config first, to make sure we're not failing 412 // for some other reason, and then make it invalid. 413 p := providers.Interface(mp) 414 rc := &configs.Resource{ 415 Mode: addrs.ManagedResourceMode, 416 Type: "test_object", 417 Name: "foo", 418 Config: configs.SynthBody("", map[string]cty.Value{}), 419 DependsOn: []hcl.Traversal{ 420 // Depending on path.module is pointless, since it is immediately 421 // available, but we allow all of the referencable addrs here 422 // for consistency: referencing them is harmless, and avoids the 423 // need for us to document a different subset of addresses that 424 // are valid in depends_on. 425 // For the sake of this test, it's a valid address we can use that 426 // doesn't require something else to exist in the configuration. 427 { 428 hcl.TraverseRoot{ 429 Name: "path", 430 }, 431 hcl.TraverseAttr{ 432 Name: "module", 433 }, 434 }, 435 }, 436 } 437 node := NodeValidatableResource{ 438 NodeAbstractResource: &NodeAbstractResource{ 439 Addr: mustConfigResourceAddr("test_foo.bar"), 440 Config: rc, 441 ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 442 }, 443 } 444 445 ctx := &MockEvalContext{} 446 ctx.installSimpleEval() 447 448 ctx.ProviderSchemaSchema = mp.GetProviderSchema() 449 ctx.ProviderProvider = p 450 451 diags := node.validateResource(ctx) 452 if diags.HasErrors() { 453 t.Fatalf("error for supposedly-valid config: %s", diags.ErrWithWarnings()) 454 } 455 456 // Now we'll make it invalid by adding additional traversal steps at 457 // the end of what we're referencing. This is intended to catch the 458 // situation where the user tries to depend on e.g. a specific resource 459 // attribute, rather than the whole resource, like aws_instance.foo.id. 460 rc.DependsOn = append(rc.DependsOn, hcl.Traversal{ 461 hcl.TraverseRoot{ 462 Name: "path", 463 }, 464 hcl.TraverseAttr{ 465 Name: "module", 466 }, 467 hcl.TraverseAttr{ 468 Name: "extra", 469 }, 470 }) 471 472 diags = node.validateResource(ctx) 473 if !diags.HasErrors() { 474 t.Fatal("no error for invalid depends_on") 475 } 476 if got, want := diags.Err().Error(), "Invalid depends_on reference"; !strings.Contains(got, want) { 477 t.Fatalf("wrong error\ngot: %s\nwant: Message containing %q", got, want) 478 } 479 480 // Test for handling an unknown root without attribute, like a 481 // typo that omits the dot inbetween "path.module". 482 rc.DependsOn = append(rc.DependsOn, hcl.Traversal{ 483 hcl.TraverseRoot{ 484 Name: "pathmodule", 485 }, 486 }) 487 488 diags = node.validateResource(ctx) 489 if !diags.HasErrors() { 490 t.Fatal("no error for invalid depends_on") 491 } 492 if got, want := diags.Err().Error(), "Invalid depends_on reference"; !strings.Contains(got, want) { 493 t.Fatalf("wrong error\ngot: %s\nwant: Message containing %q", got, want) 494 } 495 } 496 497 func TestNodeValidatableResource_ValidateResource_invalidIgnoreChangesNonexistent(t *testing.T) { 498 mp := simpleMockProvider() 499 mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { 500 return providers.ValidateResourceConfigResponse{} 501 } 502 503 // We'll check a _valid_ config first, to make sure we're not failing 504 // for some other reason, and then make it invalid. 505 p := providers.Interface(mp) 506 rc := &configs.Resource{ 507 Mode: addrs.ManagedResourceMode, 508 Type: "test_object", 509 Name: "foo", 510 Config: configs.SynthBody("", map[string]cty.Value{}), 511 Managed: &configs.ManagedResource{ 512 IgnoreChanges: []hcl.Traversal{ 513 { 514 hcl.TraverseAttr{ 515 Name: "test_string", 516 }, 517 }, 518 }, 519 }, 520 } 521 node := NodeValidatableResource{ 522 NodeAbstractResource: &NodeAbstractResource{ 523 Addr: mustConfigResourceAddr("test_foo.bar"), 524 Config: rc, 525 ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 526 }, 527 } 528 529 ctx := &MockEvalContext{} 530 ctx.installSimpleEval() 531 532 ctx.ProviderSchemaSchema = mp.GetProviderSchema() 533 ctx.ProviderProvider = p 534 535 diags := node.validateResource(ctx) 536 if diags.HasErrors() { 537 t.Fatalf("error for supposedly-valid config: %s", diags.ErrWithWarnings()) 538 } 539 540 // Now we'll make it invalid by attempting to ignore a nonexistent 541 // attribute. 542 rc.Managed.IgnoreChanges = append(rc.Managed.IgnoreChanges, hcl.Traversal{ 543 hcl.TraverseAttr{ 544 Name: "nonexistent", 545 }, 546 }) 547 548 diags = node.validateResource(ctx) 549 if !diags.HasErrors() { 550 t.Fatal("no error for invalid ignore_changes") 551 } 552 if got, want := diags.Err().Error(), "Unsupported attribute: This object has no argument, nested block, or exported attribute named \"nonexistent\""; !strings.Contains(got, want) { 553 t.Fatalf("wrong error\ngot: %s\nwant: Message containing %q", got, want) 554 } 555 } 556 557 func TestNodeValidatableResource_ValidateResource_invalidIgnoreChangesComputed(t *testing.T) { 558 // construct a schema with a computed attribute 559 ms := &configschema.Block{ 560 Attributes: map[string]*configschema.Attribute{ 561 "test_string": { 562 Type: cty.String, 563 Optional: true, 564 }, 565 "computed_string": { 566 Type: cty.String, 567 Computed: true, 568 Optional: false, 569 }, 570 }, 571 } 572 573 mp := &MockProvider{ 574 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 575 Provider: providers.Schema{Block: ms}, 576 ResourceTypes: map[string]providers.Schema{ 577 "test_object": providers.Schema{Block: ms}, 578 }, 579 }, 580 } 581 582 mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { 583 return providers.ValidateResourceConfigResponse{} 584 } 585 586 // We'll check a _valid_ config first, to make sure we're not failing 587 // for some other reason, and then make it invalid. 588 p := providers.Interface(mp) 589 rc := &configs.Resource{ 590 Mode: addrs.ManagedResourceMode, 591 Type: "test_object", 592 Name: "foo", 593 Config: configs.SynthBody("", map[string]cty.Value{}), 594 Managed: &configs.ManagedResource{ 595 IgnoreChanges: []hcl.Traversal{ 596 { 597 hcl.TraverseAttr{ 598 Name: "test_string", 599 }, 600 }, 601 }, 602 }, 603 } 604 node := NodeValidatableResource{ 605 NodeAbstractResource: &NodeAbstractResource{ 606 Addr: mustConfigResourceAddr("test_foo.bar"), 607 Config: rc, 608 ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 609 }, 610 } 611 612 ctx := &MockEvalContext{} 613 ctx.installSimpleEval() 614 615 ctx.ProviderSchemaSchema = mp.GetProviderSchema() 616 ctx.ProviderProvider = p 617 618 diags := node.validateResource(ctx) 619 if diags.HasErrors() { 620 t.Fatalf("error for supposedly-valid config: %s", diags.ErrWithWarnings()) 621 } 622 623 // Now we'll make it invalid by attempting to ignore a computed 624 // attribute. 625 rc.Managed.IgnoreChanges = append(rc.Managed.IgnoreChanges, hcl.Traversal{ 626 hcl.TraverseAttr{ 627 Name: "computed_string", 628 }, 629 }) 630 631 diags = node.validateResource(ctx) 632 if diags.HasErrors() { 633 t.Fatalf("got unexpected error: %s", diags.ErrWithWarnings()) 634 } 635 if got, want := diags.ErrWithWarnings().Error(), `Redundant ignore_changes element: Adding an attribute name to ignore_changes tells OpenTofu to ignore future changes to the argument in configuration after the object has been created, retaining the value originally configured. 636 637 The attribute computed_string is decided by the provider alone and therefore there can be no configured value to compare with. Including this attribute in ignore_changes has no effect. Remove the attribute from ignore_changes to quiet this warning.`; !strings.Contains(got, want) { 638 t.Fatalf("wrong error\ngot: %s\nwant: Message containing %q", got, want) 639 } 640 }