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