github.com/opentofu/opentofu@v1.7.1/internal/tofu/node_provider_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 "fmt" 10 "strings" 11 "testing" 12 13 "github.com/hashicorp/hcl/v2" 14 "github.com/opentofu/opentofu/internal/addrs" 15 "github.com/opentofu/opentofu/internal/configs" 16 "github.com/opentofu/opentofu/internal/configs/configschema" 17 "github.com/opentofu/opentofu/internal/lang/marks" 18 "github.com/opentofu/opentofu/internal/providers" 19 "github.com/opentofu/opentofu/internal/tfdiags" 20 "github.com/zclconf/go-cty/cty" 21 ) 22 23 func TestNodeApplyableProviderExecute(t *testing.T) { 24 config := &configs.Provider{ 25 Name: "foo", 26 Config: configs.SynthBody("", map[string]cty.Value{ 27 "user": cty.StringVal("hello"), 28 }), 29 } 30 31 schema := &configschema.Block{ 32 Attributes: map[string]*configschema.Attribute{ 33 "user": { 34 Type: cty.String, 35 Required: true, 36 }, 37 "pw": { 38 Type: cty.String, 39 Required: true, 40 }, 41 }, 42 } 43 provider := mockProviderWithConfigSchema(schema) 44 providerAddr := addrs.AbsProviderConfig{ 45 Module: addrs.RootModule, 46 Provider: addrs.NewDefaultProvider("foo"), 47 } 48 49 n := &NodeApplyableProvider{&NodeAbstractProvider{ 50 Addr: providerAddr, 51 Config: config, 52 }} 53 54 ctx := &MockEvalContext{ProviderProvider: provider} 55 ctx.installSimpleEval() 56 ctx.ProviderInputValues = map[string]cty.Value{ 57 "pw": cty.StringVal("so secret"), 58 } 59 60 if diags := n.Execute(ctx, walkApply); diags.HasErrors() { 61 t.Fatalf("err: %s", diags.Err()) 62 } 63 64 if !ctx.ConfigureProviderCalled { 65 t.Fatal("should be called") 66 } 67 68 gotObj := ctx.ConfigureProviderConfig 69 if !gotObj.Type().HasAttribute("user") { 70 t.Fatal("configuration object does not have \"user\" attribute") 71 } 72 if got, want := gotObj.GetAttr("user"), cty.StringVal("hello"); !got.RawEquals(want) { 73 t.Errorf("wrong configuration value\ngot: %#v\nwant: %#v", got, want) 74 } 75 76 if !gotObj.Type().HasAttribute("pw") { 77 t.Fatal("configuration object does not have \"pw\" attribute") 78 } 79 if got, want := gotObj.GetAttr("pw"), cty.StringVal("so secret"); !got.RawEquals(want) { 80 t.Errorf("wrong configuration value\ngot: %#v\nwant: %#v", got, want) 81 } 82 } 83 84 func TestNodeApplyableProviderExecute_unknownImport(t *testing.T) { 85 config := &configs.Provider{ 86 Name: "foo", 87 Config: configs.SynthBody("", map[string]cty.Value{ 88 "test_string": cty.UnknownVal(cty.String), 89 }), 90 } 91 provider := mockProviderWithConfigSchema(simpleTestSchema()) 92 providerAddr := addrs.AbsProviderConfig{ 93 Module: addrs.RootModule, 94 Provider: addrs.NewDefaultProvider("foo"), 95 } 96 n := &NodeApplyableProvider{&NodeAbstractProvider{ 97 Addr: providerAddr, 98 Config: config, 99 }} 100 101 ctx := &MockEvalContext{ProviderProvider: provider} 102 ctx.installSimpleEval() 103 104 diags := n.Execute(ctx, walkImport) 105 if !diags.HasErrors() { 106 t.Fatal("expected error, got success") 107 } 108 109 detail := `Invalid provider configuration: The configuration for provider["registry.opentofu.org/hashicorp/foo"] depends on values that cannot be determined until apply.` 110 if got, want := diags.Err().Error(), detail; got != want { 111 t.Errorf("wrong diagnostic detail\n got: %q\nwant: %q", got, want) 112 } 113 114 if ctx.ConfigureProviderCalled { 115 t.Fatal("should not be called") 116 } 117 } 118 119 func TestNodeApplyableProviderExecute_unknownApply(t *testing.T) { 120 config := &configs.Provider{ 121 Name: "foo", 122 Config: configs.SynthBody("", map[string]cty.Value{ 123 "test_string": cty.UnknownVal(cty.String), 124 }), 125 } 126 provider := mockProviderWithConfigSchema(simpleTestSchema()) 127 providerAddr := addrs.AbsProviderConfig{ 128 Module: addrs.RootModule, 129 Provider: addrs.NewDefaultProvider("foo"), 130 } 131 n := &NodeApplyableProvider{&NodeAbstractProvider{ 132 Addr: providerAddr, 133 Config: config, 134 }} 135 ctx := &MockEvalContext{ProviderProvider: provider} 136 ctx.installSimpleEval() 137 138 if err := n.Execute(ctx, walkApply); err != nil { 139 t.Fatalf("err: %s", err) 140 } 141 142 if !ctx.ConfigureProviderCalled { 143 t.Fatal("should be called") 144 } 145 146 gotObj := ctx.ConfigureProviderConfig 147 if !gotObj.Type().HasAttribute("test_string") { 148 t.Fatal("configuration object does not have \"test_string\" attribute") 149 } 150 if got, want := gotObj.GetAttr("test_string"), cty.UnknownVal(cty.String); !got.RawEquals(want) { 151 t.Errorf("wrong configuration value\ngot: %#v\nwant: %#v", got, want) 152 } 153 } 154 155 func TestNodeApplyableProviderExecute_sensitive(t *testing.T) { 156 config := &configs.Provider{ 157 Name: "foo", 158 Config: configs.SynthBody("", map[string]cty.Value{ 159 "test_string": cty.StringVal("hello").Mark(marks.Sensitive), 160 }), 161 } 162 provider := mockProviderWithConfigSchema(simpleTestSchema()) 163 providerAddr := addrs.AbsProviderConfig{ 164 Module: addrs.RootModule, 165 Provider: addrs.NewDefaultProvider("foo"), 166 } 167 168 n := &NodeApplyableProvider{&NodeAbstractProvider{ 169 Addr: providerAddr, 170 Config: config, 171 }} 172 173 ctx := &MockEvalContext{ProviderProvider: provider} 174 ctx.installSimpleEval() 175 if err := n.Execute(ctx, walkApply); err != nil { 176 t.Fatalf("err: %s", err) 177 } 178 179 if !ctx.ConfigureProviderCalled { 180 t.Fatal("should be called") 181 } 182 183 gotObj := ctx.ConfigureProviderConfig 184 if !gotObj.Type().HasAttribute("test_string") { 185 t.Fatal("configuration object does not have \"test_string\" attribute") 186 } 187 if got, want := gotObj.GetAttr("test_string"), cty.StringVal("hello"); !got.RawEquals(want) { 188 t.Errorf("wrong configuration value\ngot: %#v\nwant: %#v", got, want) 189 } 190 } 191 192 func TestNodeApplyableProviderExecute_sensitiveValidate(t *testing.T) { 193 config := &configs.Provider{ 194 Name: "foo", 195 Config: configs.SynthBody("", map[string]cty.Value{ 196 "test_string": cty.StringVal("hello").Mark(marks.Sensitive), 197 }), 198 } 199 provider := mockProviderWithConfigSchema(simpleTestSchema()) 200 providerAddr := addrs.AbsProviderConfig{ 201 Module: addrs.RootModule, 202 Provider: addrs.NewDefaultProvider("foo"), 203 } 204 205 n := &NodeApplyableProvider{&NodeAbstractProvider{ 206 Addr: providerAddr, 207 Config: config, 208 }} 209 210 ctx := &MockEvalContext{ProviderProvider: provider} 211 ctx.installSimpleEval() 212 if err := n.Execute(ctx, walkValidate); err != nil { 213 t.Fatalf("err: %s", err) 214 } 215 216 if !provider.ValidateProviderConfigCalled { 217 t.Fatal("should be called") 218 } 219 220 gotObj := provider.ValidateProviderConfigRequest.Config 221 if !gotObj.Type().HasAttribute("test_string") { 222 t.Fatal("configuration object does not have \"test_string\" attribute") 223 } 224 if got, want := gotObj.GetAttr("test_string"), cty.StringVal("hello"); !got.RawEquals(want) { 225 t.Errorf("wrong configuration value\ngot: %#v\nwant: %#v", got, want) 226 } 227 } 228 229 func TestNodeApplyableProviderExecute_emptyValidate(t *testing.T) { 230 config := &configs.Provider{ 231 Name: "foo", 232 Config: configs.SynthBody("", map[string]cty.Value{}), 233 } 234 provider := mockProviderWithConfigSchema(&configschema.Block{ 235 Attributes: map[string]*configschema.Attribute{ 236 "test_string": { 237 Type: cty.String, 238 Required: true, 239 }, 240 }, 241 }) 242 providerAddr := addrs.AbsProviderConfig{ 243 Module: addrs.RootModule, 244 Provider: addrs.NewDefaultProvider("foo"), 245 } 246 247 n := &NodeApplyableProvider{&NodeAbstractProvider{ 248 Addr: providerAddr, 249 Config: config, 250 }} 251 252 ctx := &MockEvalContext{ProviderProvider: provider} 253 ctx.installSimpleEval() 254 if err := n.Execute(ctx, walkValidate); err != nil { 255 t.Fatalf("err: %s", err) 256 } 257 258 if ctx.ConfigureProviderCalled { 259 t.Fatal("should not be called") 260 } 261 } 262 263 func TestNodeApplyableProvider_Validate(t *testing.T) { 264 provider := mockProviderWithConfigSchema(&configschema.Block{ 265 Attributes: map[string]*configschema.Attribute{ 266 "region": { 267 Type: cty.String, 268 Required: true, 269 }, 270 }, 271 }) 272 ctx := &MockEvalContext{ProviderProvider: provider} 273 ctx.installSimpleEval() 274 275 t.Run("valid", func(t *testing.T) { 276 config := &configs.Provider{ 277 Name: "test", 278 Config: configs.SynthBody("", map[string]cty.Value{ 279 "region": cty.StringVal("mars"), 280 }), 281 } 282 283 node := NodeApplyableProvider{ 284 NodeAbstractProvider: &NodeAbstractProvider{ 285 Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 286 Config: config, 287 }, 288 } 289 290 diags := node.ValidateProvider(ctx, provider) 291 if diags.HasErrors() { 292 t.Errorf("unexpected error with valid config: %s", diags.Err()) 293 } 294 }) 295 296 t.Run("invalid", func(t *testing.T) { 297 config := &configs.Provider{ 298 Name: "test", 299 Config: configs.SynthBody("", map[string]cty.Value{ 300 "region": cty.MapValEmpty(cty.String), 301 }), 302 } 303 304 node := NodeApplyableProvider{ 305 NodeAbstractProvider: &NodeAbstractProvider{ 306 Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 307 Config: config, 308 }, 309 } 310 311 diags := node.ValidateProvider(ctx, provider) 312 if !diags.HasErrors() { 313 t.Error("missing expected error with invalid config") 314 } 315 }) 316 317 t.Run("empty config", func(t *testing.T) { 318 node := NodeApplyableProvider{ 319 NodeAbstractProvider: &NodeAbstractProvider{ 320 Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 321 }, 322 } 323 324 diags := node.ValidateProvider(ctx, provider) 325 if diags.HasErrors() { 326 t.Errorf("unexpected error with empty config: %s", diags.Err()) 327 } 328 }) 329 } 330 331 // This test specifically tests responses from the 332 // providers.ValidateProviderConfigFn. See 333 // TestNodeApplyableProvider_ConfigProvider_config_fn_err for 334 // providers.ConfigureProviderRequest responses. 335 func TestNodeApplyableProvider_ConfigProvider(t *testing.T) { 336 provider := mockProviderWithConfigSchema(&configschema.Block{ 337 Attributes: map[string]*configschema.Attribute{ 338 "region": { 339 Type: cty.String, 340 Optional: true, 341 }, 342 }, 343 }) 344 // For this test, we're returning an error for an optional argument. This 345 // can happen for example if an argument is only conditionally required. 346 provider.ValidateProviderConfigFn = func(req providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) { 347 region := req.Config.GetAttr("region") 348 if region.IsNull() { 349 resp.Diagnostics = resp.Diagnostics.Append( 350 tfdiags.WholeContainingBody(tfdiags.Error, "value is not found", "you did not supply a required value")) 351 } 352 return 353 } 354 ctx := &MockEvalContext{ProviderProvider: provider} 355 ctx.installSimpleEval() 356 357 t.Run("valid", func(t *testing.T) { 358 config := &configs.Provider{ 359 Name: "test", 360 Config: configs.SynthBody("", map[string]cty.Value{ 361 "region": cty.StringVal("mars"), 362 }), 363 } 364 365 node := NodeApplyableProvider{ 366 NodeAbstractProvider: &NodeAbstractProvider{ 367 Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 368 Config: config, 369 }, 370 } 371 372 diags := node.ConfigureProvider(ctx, provider, false) 373 if diags.HasErrors() { 374 t.Errorf("unexpected error with valid config: %s", diags.Err()) 375 } 376 }) 377 378 t.Run("missing required config (no config at all)", func(t *testing.T) { 379 node := NodeApplyableProvider{ 380 NodeAbstractProvider: &NodeAbstractProvider{ 381 Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 382 }, 383 } 384 385 diags := node.ConfigureProvider(ctx, provider, false) 386 if !diags.HasErrors() { 387 t.Fatal("missing expected error with nil config") 388 } 389 if !strings.Contains(diags.Err().Error(), "requires explicit configuration") { 390 t.Errorf("diagnostic is missing \"requires explicit configuration\" message: %s", diags.Err()) 391 } 392 }) 393 394 t.Run("missing required config", func(t *testing.T) { 395 config := &configs.Provider{ 396 Name: "test", 397 Config: hcl.EmptyBody(), 398 } 399 node := NodeApplyableProvider{ 400 NodeAbstractProvider: &NodeAbstractProvider{ 401 Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 402 Config: config, 403 }, 404 } 405 406 diags := node.ConfigureProvider(ctx, provider, false) 407 if !diags.HasErrors() { 408 t.Fatal("missing expected error with invalid config") 409 } 410 if !strings.Contains(diags.Err().Error(), "value is not found") { 411 t.Errorf("wrong diagnostic: %s", diags.Err()) 412 } 413 }) 414 415 } 416 417 // This test is similar to TestNodeApplyableProvider_ConfigProvider, but tests responses from the providers.ConfigureProviderRequest 418 func TestNodeApplyableProvider_ConfigProvider_config_fn_err(t *testing.T) { 419 provider := mockProviderWithConfigSchema(&configschema.Block{ 420 Attributes: map[string]*configschema.Attribute{ 421 "region": { 422 Type: cty.String, 423 Optional: true, 424 }, 425 }, 426 }) 427 ctx := &MockEvalContext{ProviderProvider: provider} 428 ctx.installSimpleEval() 429 // For this test, provider.PrepareConfigFn will succeed every time but the 430 // ctx.ConfigureProviderFn will return an error if a value is not found. 431 // 432 // This is an unlikely but real situation that occurs: 433 // https://github.com/hashicorp/terraform/issues/23087 434 ctx.ConfigureProviderFn = func(addr addrs.AbsProviderConfig, cfg cty.Value) (diags tfdiags.Diagnostics) { 435 if cfg.IsNull() { 436 diags = diags.Append(fmt.Errorf("no config provided")) 437 } else { 438 region := cfg.GetAttr("region") 439 if region.IsNull() { 440 diags = diags.Append(fmt.Errorf("value is not found")) 441 } 442 } 443 return 444 } 445 446 t.Run("valid", func(t *testing.T) { 447 config := &configs.Provider{ 448 Name: "test", 449 Config: configs.SynthBody("", map[string]cty.Value{ 450 "region": cty.StringVal("mars"), 451 }), 452 } 453 454 node := NodeApplyableProvider{ 455 NodeAbstractProvider: &NodeAbstractProvider{ 456 Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 457 Config: config, 458 }, 459 } 460 461 diags := node.ConfigureProvider(ctx, provider, false) 462 if diags.HasErrors() { 463 t.Errorf("unexpected error with valid config: %s", diags.Err()) 464 } 465 }) 466 467 t.Run("missing required config (no config at all)", func(t *testing.T) { 468 node := NodeApplyableProvider{ 469 NodeAbstractProvider: &NodeAbstractProvider{ 470 Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 471 }, 472 } 473 474 diags := node.ConfigureProvider(ctx, provider, false) 475 if !diags.HasErrors() { 476 t.Fatal("missing expected error with nil config") 477 } 478 if !strings.Contains(diags.Err().Error(), "requires explicit configuration") { 479 t.Errorf("diagnostic is missing \"requires explicit configuration\" message: %s", diags.Err()) 480 } 481 }) 482 483 t.Run("missing required config", func(t *testing.T) { 484 config := &configs.Provider{ 485 Name: "test", 486 Config: hcl.EmptyBody(), 487 } 488 node := NodeApplyableProvider{ 489 NodeAbstractProvider: &NodeAbstractProvider{ 490 Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 491 Config: config, 492 }, 493 } 494 495 diags := node.ConfigureProvider(ctx, provider, false) 496 if !diags.HasErrors() { 497 t.Fatal("missing expected error with invalid config") 498 } 499 if diags.Err().Error() != "value is not found" { 500 t.Errorf("wrong diagnostic: %s", diags.Err()) 501 } 502 }) 503 } 504 505 func TestGetSchemaError(t *testing.T) { 506 provider := &MockProvider{ 507 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 508 Diagnostics: tfdiags.Diagnostics.Append(nil, tfdiags.WholeContainingBody(tfdiags.Error, "oops", "error")), 509 }, 510 } 511 512 providerAddr := mustProviderConfig(`provider["terraform.io/some/provider"]`) 513 ctx := &MockEvalContext{ProviderProvider: provider} 514 ctx.installSimpleEval() 515 node := NodeApplyableProvider{ 516 NodeAbstractProvider: &NodeAbstractProvider{ 517 Addr: providerAddr, 518 }, 519 } 520 521 diags := node.ConfigureProvider(ctx, provider, false) 522 for _, d := range diags { 523 desc := d.Description() 524 if desc.Address != providerAddr.String() { 525 t.Fatalf("missing provider address from diagnostics: %#v", desc) 526 } 527 } 528 529 }