From c46515c5da92ad9efdbd9d2a6dfce2f8ecc83184 Mon Sep 17 00:00:00 2001 From: Liam Cervante Date: Wed, 21 Feb 2024 10:47:35 +0100 Subject: [PATCH] stacks: add support for external providers to validate context (#34701) --- internal/backend/local/backend_local.go | 2 +- internal/backend/local/test.go | 2 +- internal/command/validate.go | 2 +- internal/terraform/context_apply_test.go | 8 +- internal/terraform/context_functions_test.go | 4 +- internal/terraform/context_plan_test.go | 10 +- internal/terraform/context_refresh_test.go | 2 +- internal/terraform/context_test.go | 6 +- internal/terraform/context_validate.go | 48 +++- internal/terraform/context_validate_test.go | 251 +++++++++++++------ 10 files changed, 235 insertions(+), 100 deletions(-) diff --git a/internal/backend/local/backend_local.go b/internal/backend/local/backend_local.go index dfc3bf771d..329b12e505 100644 --- a/internal/backend/local/backend_local.go +++ b/internal/backend/local/backend_local.go @@ -131,7 +131,7 @@ func (b *Local) localRun(op *backend.Operation) (*backend.LocalRun, *configload. // If validation is enabled, validate if b.OpValidation { log.Printf("[TRACE] backend/local: running validation operation") - validateDiags := ret.Core.Validate(ret.Config) + validateDiags := ret.Core.Validate(ret.Config, nil) diags = diags.Append(validateDiags) } } diff --git a/internal/backend/local/test.go b/internal/backend/local/test.go index 235ea6f610..63e19a397b 100644 --- a/internal/backend/local/test.go +++ b/internal/backend/local/test.go @@ -583,7 +583,7 @@ func (runner *TestFileRunner) validate(config *configs.Config, run *moduletest.R defer done() log.Printf("[DEBUG] TestFileRunner: starting validate for %s/%s", file.Name, run.Name) - validateDiags = tfCtx.Validate(config) + validateDiags = tfCtx.Validate(config, nil) log.Printf("[DEBUG] TestFileRunner: completed validate for %s/%s", file.Name, run.Name) }() waitDiags, cancelled := runner.wait(tfCtx, runningCtx, run, file, nil, moduletest.Running, start) diff --git a/internal/command/validate.go b/internal/command/validate.go index 87172e2b96..a574f00c0f 100644 --- a/internal/command/validate.go +++ b/internal/command/validate.go @@ -94,7 +94,7 @@ func (c *ValidateCommand) validate(dir, testDir string, noTests bool) tfdiags.Di return diags } - return diags.Append(tfCtx.Validate(cfg)) + return diags.Append(tfCtx.Validate(cfg, nil)) } diags = diags.Append(validate(cfg)) diff --git a/internal/terraform/context_apply_test.go b/internal/terraform/context_apply_test.go index 5a54d1746f..6b5af095a3 100644 --- a/internal/terraform/context_apply_test.go +++ b/internal/terraform/context_apply_test.go @@ -7178,7 +7178,7 @@ func TestContext2Apply_targetedDestroy(t *testing.T) { }, }) - if diags := ctx.Validate(m); diags.HasErrors() { + if diags := ctx.Validate(m, nil); diags.HasErrors() { t.Fatalf("validate errors: %s", diags.Err()) } @@ -7736,7 +7736,7 @@ func TestContext2Apply_vars(t *testing.T) { ctx := testContext2(t, opts) m := fixture.Config - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if len(diags) != 0 { t.Fatalf("bad: %s", diags.ErrWithWarnings()) } @@ -7799,7 +7799,7 @@ func TestContext2Apply_varsEnv(t *testing.T) { ctx := testContext2(t, opts) m := fixture.Config - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if len(diags) != 0 { t.Fatalf("bad: %s", diags.ErrWithWarnings()) } @@ -9367,7 +9367,7 @@ func TestContext2Apply_invalidIndexRef(t *testing.T) { addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected validation failure: %s", diags.Err()) } diff --git a/internal/terraform/context_functions_test.go b/internal/terraform/context_functions_test.go index 46c9b2076f..9c3a26ec8d 100644 --- a/internal/terraform/context_functions_test.go +++ b/internal/terraform/context_functions_test.go @@ -141,7 +141,7 @@ output "second" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("expected error") } @@ -311,7 +311,7 @@ func TestContext2Validate_providerFunctionDiagnostics(t *testing.T) { }, }) - diags := ctx.Validate(test.cfg) + diags := ctx.Validate(test.cfg, nil) if !diags.HasErrors() { t.Fatal("expected diagnsotics, got none") } diff --git a/internal/terraform/context_plan_test.go b/internal/terraform/context_plan_test.go index 6b8ef99417..e5b1217662 100644 --- a/internal/terraform/context_plan_test.go +++ b/internal/terraform/context_plan_test.go @@ -1864,7 +1864,7 @@ func TestContext2Plan_computedInFunction(t *testing.T) { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) assertNoErrors(t, diags) _, diags = ctx.Plan(m, states.NewState(), DefaultPlanOpts) @@ -5264,7 +5264,7 @@ func TestContext2Plan_resourceNestedCount(t *testing.T) { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatalf("validate errors: %s", diags.Err()) } @@ -5348,7 +5348,7 @@ func TestContext2Plan_selfRef(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected validation failure: %s", diags.Err()) } @@ -5384,7 +5384,7 @@ func TestContext2Plan_selfRefMulti(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected validation failure: %s", diags.Err()) } @@ -5420,7 +5420,7 @@ func TestContext2Plan_selfRefMultiAll(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected validation failure: %s", diags.Err()) } diff --git a/internal/terraform/context_refresh_test.go b/internal/terraform/context_refresh_test.go index 3c29b00b87..ecfba2a86e 100644 --- a/internal/terraform/context_refresh_test.go +++ b/internal/terraform/context_refresh_test.go @@ -1229,7 +1229,7 @@ func TestContext2Validate(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if len(diags) != 0 { t.Fatalf("unexpected error: %#v", diags.ErrWithWarnings()) } diff --git a/internal/terraform/context_test.go b/internal/terraform/context_test.go index a61426a611..32f383f59d 100644 --- a/internal/terraform/context_test.go +++ b/internal/terraform/context_test.go @@ -106,7 +106,7 @@ func TestNewContextRequiredVersion(t *testing.T) { t.Fatalf("unexpected NewContext errors: %s", diags.Err()) } - diags = c.Validate(mod) + diags = c.Validate(mod, nil) if diags.HasErrors() != tc.Err { t.Fatalf("err: %s", diags.Err()) } @@ -165,7 +165,7 @@ terraform {} t.Fatalf("unexpected NewContext errors: %s", diags.Err()) } - diags = c.Validate(mod) + diags = c.Validate(mod, nil) if diags.HasErrors() != tc.Err { t.Fatalf("err: %s", diags.Err()) } @@ -210,7 +210,7 @@ resource "implicit_thing" "b" { // require doing some pretty weird things that aren't common enough to // be worth the complexity to check for them. - validateDiags := ctx.Validate(cfg) + validateDiags := ctx.Validate(cfg, nil) _, planDiags := ctx.Plan(cfg, nil, DefaultPlanOpts) tests := map[string]tfdiags.Diagnostics{ diff --git a/internal/terraform/context_validate.go b/internal/terraform/context_validate.go index fef40f2ce4..4e067d7751 100644 --- a/internal/terraform/context_validate.go +++ b/internal/terraform/context_validate.go @@ -6,14 +6,35 @@ package terraform import ( "log" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) +// ValidateOpts are the various options the affect the details of how Terraform +// will validate a configuration. +type ValidateOpts struct { + // ExternalProviders are clients for pre-configured providers that are + // treated as being passed into the root module from the caller. This + // is equivalent to writing a "providers" argument inside a "module" + // block in the Terraform language, but for the root module the caller + // is written in Go rather than the Terraform language. + // + // Note, that while Terraform Core will not call ValidateProviderConfig or + // ConfigureProvider on any providers in this map, as with the other context + // functions, the Validate function never calls ConfigureProvider anyway. + // + // Normally, the validate function would call the ValidateProviderConfig + // function on the provider, but the config may rely on variables that are + // not available to this function. Therefore, it is the responsibility of + // the caller to ensure that the provider configurations are valid. + ExternalProviders map[addrs.RootProviderConfig]providers.Interface +} + // Validate performs semantic validation of a configuration, and returns // any warnings or errors. // @@ -25,11 +46,18 @@ import ( // such as root module input variables. However, the Plan function includes // all of the same checks as Validate, in addition to the other work it does // to consider the previous run state and the planning options. -func (c *Context) Validate(config *configs.Config) tfdiags.Diagnostics { +// +// The opts can be nil, and the ExternalProviders field of the opts can be nil. +func (c *Context) Validate(config *configs.Config, opts *ValidateOpts) tfdiags.Diagnostics { defer c.acquireRun("validate")() var diags tfdiags.Diagnostics + if opts == nil { + // Just make sure we don't get any nil pointer exceptions later. + opts = &ValidateOpts{} + } + moreDiags := c.checkConfigDependencies(config) diags = diags.Append(moreDiags) // If required dependencies are not available then we'll bail early since @@ -60,11 +88,12 @@ func (c *Context) Validate(config *configs.Config) tfdiags.Diagnostics { } graph, moreDiags := (&PlanGraphBuilder{ - Config: config, - Plugins: c.plugins, - State: states.NewState(), - RootVariableValues: varValues, - Operation: walkValidate, + Config: config, + Plugins: c.plugins, + State: states.NewState(), + RootVariableValues: varValues, + Operation: walkValidate, + ExternalProviderConfigs: opts.ExternalProviders, }).Build(addrs.RootModuleInstance) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { @@ -72,8 +101,9 @@ func (c *Context) Validate(config *configs.Config) tfdiags.Diagnostics { } walker, walkDiags := c.walk(graph, walkValidate, &graphWalkOpts{ - Config: config, - ProviderFuncResults: providers.NewFunctionResultsTable(nil), + Config: config, + ProviderFuncResults: providers.NewFunctionResultsTable(nil), + ExternalProviderConfigs: opts.ExternalProviders, }) diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walkDiags) diff --git a/internal/terraform/context_validate_test.go b/internal/terraform/context_validate_test.go index 14270493af..7b5f0c8775 100644 --- a/internal/terraform/context_validate_test.go +++ b/internal/terraform/context_validate_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" @@ -37,7 +38,7 @@ func TestContext2Validate_badCount(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -60,7 +61,7 @@ func TestContext2Validate_badResource_reference(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -86,7 +87,7 @@ func TestContext2Validate_badVar(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -163,7 +164,7 @@ func TestContext2Validate_computedVar(t *testing.T) { return } - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -203,7 +204,7 @@ func TestContext2Validate_computedInFunction(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -241,7 +242,7 @@ func TestContext2Validate_countComputed(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -265,7 +266,7 @@ func TestContext2Validate_countNegative(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -291,7 +292,7 @@ func TestContext2Validate_countVariable(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -345,7 +346,7 @@ func TestContext2Validate_moduleBadOutput(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -371,7 +372,7 @@ func TestContext2Validate_moduleGood(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -400,7 +401,7 @@ func TestContext2Validate_moduleBadResource(t *testing.T) { Diagnostics: tfdiags.Diagnostics{}.Append(fmt.Errorf("bad")), } - diags := c.Validate(m) + diags := c.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -427,7 +428,7 @@ func TestContext2Validate_moduleDepsShouldNotCycle(t *testing.T) { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -468,7 +469,7 @@ func TestContext2Validate_moduleProviderVar(t *testing.T) { return } - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -509,7 +510,7 @@ func TestContext2Validate_moduleProviderInheritUnused(t *testing.T) { return } - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -548,7 +549,7 @@ func TestContext2Validate_orphans(t *testing.T) { } } - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -584,7 +585,7 @@ func TestContext2Validate_providerConfig_bad(t *testing.T) { Diagnostics: tfdiags.Diagnostics{}.Append(fmt.Errorf("bad")), } - diags := c.Validate(m) + diags := c.Validate(m, nil) if len(diags) != 1 { t.Fatalf("wrong number of diagnostics %d; want %d", len(diags), 1) } @@ -623,7 +624,7 @@ func TestContext2Validate_providerConfig_skippedEmpty(t *testing.T) { Diagnostics: tfdiags.Diagnostics{}.Append(fmt.Errorf("should not be called")), } - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -655,7 +656,7 @@ func TestContext2Validate_providerConfig_good(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -690,7 +691,7 @@ func TestContext2Validate_requiredProviderConfig(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -726,7 +727,7 @@ func TestContext2Validate_provisionerConfig_bad(t *testing.T) { Diagnostics: tfdiags.Diagnostics{}.Append(fmt.Errorf("bad")), } - diags := c.Validate(m) + diags := c.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -758,7 +759,7 @@ func TestContext2Validate_badResourceConnection(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) t.Log(diags.Err()) if !diags.HasErrors() { t.Fatalf("succeeded; want error") @@ -791,7 +792,7 @@ func TestContext2Validate_badProvisionerConnection(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) t.Log(diags.Err()) if !diags.HasErrors() { t.Fatalf("succeeded; want error") @@ -840,7 +841,7 @@ func TestContext2Validate_provisionerConfig_good(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -906,7 +907,7 @@ func TestContext2Validate_resourceConfig_bad(t *testing.T) { Diagnostics: tfdiags.Diagnostics{}.Append(fmt.Errorf("bad")), } - diags := c.Validate(m) + diags := c.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -932,7 +933,7 @@ func TestContext2Validate_resourceConfig_good(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -970,7 +971,7 @@ func TestContext2Validate_tainted(t *testing.T) { } } - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -1007,7 +1008,7 @@ func TestContext2Validate_targetedDestroy(t *testing.T) { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -1039,7 +1040,7 @@ func TestContext2Validate_varRefUnknown(t *testing.T) { return providers.ValidateResourceConfigResponse{} } - c.Validate(m) + c.Validate(m, nil) // Input variables are always unknown during the validate walk, because // we're checking for validity of all possible input values. Validity @@ -1075,7 +1076,7 @@ func TestContext2Validate_interpolateVar(t *testing.T) { UIInput: input, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -1107,7 +1108,7 @@ func TestContext2Validate_interpolateComputedModuleVarDef(t *testing.T) { UIInput: input, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -1127,7 +1128,7 @@ func TestContext2Validate_interpolateMap(t *testing.T) { UIInput: input, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -1177,7 +1178,7 @@ resource "aws_instance" "foo" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1208,7 +1209,7 @@ output "out" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1244,7 +1245,7 @@ resource "aws_instance" "foo" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1279,7 +1280,7 @@ output "root" { ctx := testContext2(t, &ContextOpts{}) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1302,7 +1303,7 @@ output "out" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1332,7 +1333,7 @@ output "out" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1362,7 +1363,7 @@ output "out" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1391,7 +1392,7 @@ resource "test_instance" "bar" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1423,7 +1424,7 @@ resource "test_instance" "bar" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1447,7 +1448,7 @@ func TestContext2Validate_variableCustomValidationsFail(t *testing.T) { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1481,7 +1482,7 @@ variable "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error\ngot: %s", diags.Err().Error()) } @@ -1542,7 +1543,7 @@ resource "aws_instance" "foo" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -1569,7 +1570,7 @@ resource "aws_instance" "foo" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1599,7 +1600,7 @@ resource "aws_instance" "foo" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1680,7 +1681,7 @@ output "out" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -1707,7 +1708,7 @@ output "out" { `, }) - diags := testContext2(t, &ContextOpts{}).Validate(m) + diags := testContext2(t, &ContextOpts{}).Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1745,7 +1746,7 @@ output "out" { `, }) - diags := testContext2(t, &ContextOpts{}).Validate(m) + diags := testContext2(t, &ContextOpts{}).Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1793,7 +1794,7 @@ resource "test_instance" "a" { addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1843,7 +1844,7 @@ func TestContext2Validate_sensitiveProvisionerConfig(t *testing.T) { return pr.ValidateProvisionerConfigResponse } - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -1937,7 +1938,7 @@ resource "test_instance" "c" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -2004,7 +2005,7 @@ resource "test_object" "t" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -2061,7 +2062,7 @@ output "out" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -2094,7 +2095,7 @@ func TestContext2Validate_nonNullableVariableDefaultValidation(t *testing.T) { ctx := testContext2(t, &ContextOpts{}) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -2137,7 +2138,7 @@ resource "aws_instance" "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -2180,7 +2181,7 @@ resource "aws_instance" "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -2226,7 +2227,7 @@ resource "aws_instance" "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -2267,7 +2268,7 @@ resource "aws_instance" "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -2316,7 +2317,7 @@ resource "aws_instance" "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -2357,7 +2358,7 @@ resource "aws_instance" "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -2403,7 +2404,7 @@ resource "aws_instance" "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -2446,7 +2447,7 @@ resource "aws_instance" "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -2480,7 +2481,7 @@ locals { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) warn := diags.ErrWithWarnings().Error() if !strings.Contains(warn, `The attribute "foo" is deprecated`) { t.Fatalf("expected deprecated warning, got: %q\n", warn) @@ -2511,7 +2512,7 @@ resource "aws_instance" "follow" { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -2593,7 +2594,7 @@ output "result" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -2625,7 +2626,7 @@ output "result" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if p.CallFunctionCalled { t.Error("CallFunction was called, but should not have been") } @@ -2660,7 +2661,7 @@ output "result" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if p.CallFunctionCalled { t.Error("CallFunction was called, but should not have been") } @@ -2694,7 +2695,7 @@ output "result" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if p.CallFunctionCalled { t.Error("CallFunction was called, but should not have been") } @@ -2728,7 +2729,7 @@ output "result" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if p.CallFunctionCalled { t.Error("CallFunction was called, but should not have been") } @@ -2762,7 +2763,7 @@ output "result" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if p.CallFunctionCalled { t.Error("CallFunction was called, but should not have been") } @@ -2796,7 +2797,7 @@ output "result" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if p.CallFunctionCalled { t.Error("CallFunction was called, but should not have been") } @@ -2833,7 +2834,7 @@ output "result" { // For this case, validation should succeed without calling the // function yet, because the function doesn't declare that it handles // unknown values and so we must defer validation until a later phase. - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if p.CallFunctionCalled { t.Error("CallFunction was called, but should not have been") } @@ -2862,7 +2863,7 @@ output "result" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if p.CallFunctionCalled { t.Error("CallFunction was called, but should not have been") } @@ -2876,3 +2877,107 @@ output "result" { } }) } + +func TestContextValidate_externalProviders(t *testing.T) { + + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + bar = { + source = "hashicorp/bar" + } + } +} + +provider "bar" {} + +resource "bar_instance" "test" { + foo = "foo" # should be an int +} +`, + }) + + mustNotConfigure := func(providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + return providers.ConfigureProviderResponse{ + Diagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Pre-configured provider was reconfigured by the modules runtime", + "An externally-configured provider should not have its ConfigureProvider function called during planning.", + ), + }, + } + } + + providerAddr := addrs.NewDefaultProvider("bar") + providerConfigAddr := addrs.RootProviderConfig{ + Provider: providerAddr, + } + + provider := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + // We have a required attribute that is not set, we're + // expecting this to not matter as we shouldn't validate + // the provider configuration as we're using an external + // provider. + "required": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + ResourceTypes: map[string]providers.Schema{ + "bar_instance": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + // We should still validate this attribute as being + // incorrect, even though we have an external + // provider. + "foo": { + Type: cty.Number, + Required: true, + }, + }, + }, + }, + }, + }, + ConfigureProviderFn: mustNotConfigure, + } + + ctx, diags := NewContext(&ContextOpts{ + PreloadedProviderSchemas: map[addrs.Provider]providers.ProviderSchema{ + providerAddr: *provider.GetProviderSchemaResponse, + }, + }) + assertNoDiagnostics(t, diags) + + // Many of the MockProvider methods check for this, so we'll set it to be + // true externally. + provider.ConfigureProviderCalled = true + + diags = ctx.Validate(m, &ValidateOpts{ + ExternalProviders: map[addrs.RootProviderConfig]providers.Interface{ + providerConfigAddr: provider, + }, + }) + + // We should have exactly one diagnostic, stating there was an error in the + // resource. But nothing complaining about the provider itself. + + if len(diags) != 1 { + t.Fatalf("expected exactly one diagnostic, got %d", len(diags)) + } + + if diff := cmp.Diff(diags[0].Description().Summary, "Incorrect attribute value type"); len(diff) > 0 { + t.Errorf("unexpected diagnostic summary: %s", diff) + } + if diff := cmp.Diff(diags[0].Description().Detail, "Inappropriate value for attribute \"foo\": a number is required."); len(diff) > 0 { + t.Errorf("unexpected diagnostic detail: %s", diff) + } +}