From b2cc7dbadf4117fc9eb70cea97c2712625f3e32d Mon Sep 17 00:00:00 2001 From: Liam Cervante Date: Mon, 26 Feb 2024 11:36:19 +0100 Subject: [PATCH] stacks: validate the types of input variables during validation (#34722) --- .../internal/stackeval/component_config.go | 28 +++++- .../internal/stackeval/component_instance.go | 68 +-------------- .../internal/stackeval/expressions.go | 73 +++++++++++++++- .../stackruntime/internal/stackeval/stack.go | 9 +- internal/stacks/stackruntime/plan_test.go | 67 ++++++++++++++- .../input-from-component-list.tfstack.hcl | 36 ++++++++ .../input-from-component.tfstack.hcl | 28 ++++++ .../input-from-missing-component.tfstack.hcl | 21 +++++ .../input-from-provider.tfstack.hcl | 21 +++++ .../missing-variable.tfstack.hcl | 23 +++++ .../undeclared-variable.tfstack.hcl | 21 +++++ .../test/with-single-output/single-output.tf | 14 +++ internal/stacks/stackruntime/validate_test.go | 85 +++++++++++++++++++ 13 files changed, 425 insertions(+), 69 deletions(-) create mode 100644 internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-component-list/input-from-component-list.tfstack.hcl create mode 100644 internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-component/input-from-component.tfstack.hcl create mode 100644 internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-missing-component/input-from-missing-component.tfstack.hcl create mode 100644 internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-provider/input-from-provider.tfstack.hcl create mode 100644 internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/missing-variable/missing-variable.tfstack.hcl create mode 100644 internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/undeclared-variable/undeclared-variable.tfstack.hcl create mode 100644 internal/stacks/stackruntime/testdata/mainbundle/test/with-single-output/single-output.tf diff --git a/internal/stacks/stackruntime/internal/stackeval/component_config.go b/internal/stacks/stackruntime/internal/stackeval/component_config.go index 623981ec17..9ec192496b 100644 --- a/internal/stacks/stackruntime/internal/stackeval/component_config.go +++ b/internal/stacks/stackruntime/internal/stackeval/component_config.go @@ -257,6 +257,21 @@ func (c *ComponentConfig) InputsType(ctx context.Context) (cty.Type, *typeexpr.D return retTy, defs } +func (c *ComponentConfig) CheckInputVariableValues(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { + wantTy, defs := c.InputsType(ctx) + if wantTy == cty.NilType { + // Suggests that the module tree is invalid. We validate the full module + // tree elsewhere, which will hopefully detect the problems here. + return nil + } + + decl := c.Declaration(ctx) + + // We don't care about the returned value, only that it has no errors. + _, diags := EvalComponentInputVariables(ctx, wantTy, defs, decl, phase, c) + return diags +} + // RequiredProviderInstances returns a description of all of the provider // instance slots ("provider configurations" in main Terraform language // terminology) that are either explicitly declared or implied by the @@ -434,11 +449,18 @@ func (c *ComponentConfig) checkValid(ctx context.Context, phase EvalPhase) tfdia } decl := c.Declaration(ctx) - // TODO: Also check if the input variables are valid. + variableDiags := c.CheckInputVariableValues(ctx, phase) + diags = diags.Append(variableDiags) + // We don't actually exit if we found errors with the input variables, + // we can still validate the actual module tree without them. _, providerDiags := c.CheckProviders(ctx, phase) diags = diags.Append(providerDiags) if providerDiags.HasErrors() { + // If there's invalid provider configuration, we can't actually go + // on and validate the module tree. We need the providers and if + // they're invalid we'll just get crazy and confusing errors + // later if we try and carry on. return diags, nil } @@ -448,7 +470,9 @@ func (c *ComponentConfig) checkValid(ctx context.Context, phase EvalPhase) tfdia return diags, nil } - // TODO: Manually validate the provider configs. + // TODO: Manually validate the provider configs. Probably shouldn't + // actually do this here though. We can validate all the provider + // configs in the stack configuration in one go at a higher level. tfCtx, err := terraform.NewContext(&terraform.ContextOpts{ PreloadedProviderSchemas: providerSchemas, diff --git a/internal/stacks/stackruntime/internal/stackeval/component_instance.go b/internal/stacks/stackruntime/internal/stackeval/component_instance.go index 1a5d84fead..33140a246e 100644 --- a/internal/stacks/stackruntime/internal/stackeval/component_instance.go +++ b/internal/stacks/stackruntime/internal/stackeval/component_instance.go @@ -9,7 +9,6 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/convert" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/collections" @@ -74,7 +73,6 @@ func (c *ComponentInstance) InputVariableValues(ctx context.Context, phase EvalP } func (c *ComponentInstance) CheckInputVariableValues(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics wantTy, defs := c.call.Config(ctx).InputsType(ctx) decl := c.call.Declaration(ctx) @@ -83,70 +81,12 @@ func (c *ComponentInstance) CheckInputVariableValues(ctx context.Context, phase // just report that we don't know the input variable values and trust // that the module's problems will be reported by some other return // path. - return cty.DynamicVal, diags - } - - v := cty.EmptyObjectVal - expr := decl.Inputs - rng := decl.DeclRange - var hclCtx *hcl.EvalContext - if expr != nil { - result, moreDiags := EvalExprAndEvalContext(ctx, expr, phase, c) - diags = diags.Append(moreDiags) - if moreDiags.HasErrors() { - return cty.DynamicVal, diags - } - expr = result.Expression - hclCtx = result.EvalContext - v = result.Value - rng = tfdiags.SourceRangeFromHCL(result.Expression.Range()) - } - - if defs != nil { - v = defs.Apply(v) - } - v, err := convert.Convert(v, wantTy) - if err != nil { - // A conversion failure here could either be caused by an author-provided - // expression that's invalid or by the author omitting the argument - // altogether when there's at least one required attribute, so we'll - // return slightly different messages in each case. - if expr != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid inputs for component", - Detail: fmt.Sprintf("Invalid input variable definition object: %s.", tfdiags.FormatError(err)), - Subject: rng.ToHCL().Ptr(), - Expression: expr, - EvalContext: hclCtx, - }) - } else { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Missing required inputs for component", - Detail: fmt.Sprintf("Must provide \"inputs\" argument to define the component's input variables: %s.", tfdiags.FormatError(err)), - Subject: rng.ToHCL().Ptr(), - }) - } - return cty.DynamicVal, diags - } - - for _, path := range stackconfigtypes.ProviderInstancePathsInValue(v) { - err := path.NewErrorf("cannot send provider configuration reference to Terraform module input variable") - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid inputs for component", - Detail: fmt.Sprintf( - "Invalid input variable definition object: %s.\n\nUse the separate \"providers\" argument to specify the provider configurations to use for this component's root module.", - tfdiags.FormatError(err), - ), - Subject: rng.ToHCL().Ptr(), - Expression: expr, - EvalContext: hclCtx, - }) + return cty.DynamicVal, nil } - return v, diags + // We actually checked the errors statically already, so we only care about + // the value here. + return EvalComponentInputVariables(ctx, wantTy, defs, decl, phase, c) } // inputValuesForModulesRuntime adapts the result of diff --git a/internal/stacks/stackruntime/internal/stackeval/expressions.go b/internal/stacks/stackruntime/internal/stackeval/expressions.go index 09328266be..0dee7801cc 100644 --- a/internal/stacks/stackruntime/internal/stackeval/expressions.go +++ b/internal/stacks/stackruntime/internal/stackeval/expressions.go @@ -10,10 +10,15 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcldec" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackconfig/stackconfigtypes" + "github.com/hashicorp/terraform/internal/stacks/stackconfig/typeexpr" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) type EvalPhase rune @@ -203,6 +208,72 @@ func evalContextForTraversals(ctx context.Context, traversals []hcl.Traversal, p return hclCtx, diags } +func EvalComponentInputVariables(ctx context.Context, wantTy cty.Type, defs *typeexpr.Defaults, decl *stackconfig.Component, phase EvalPhase, scope ExpressionScope) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + v := cty.EmptyObjectVal + expr := decl.Inputs + rng := decl.DeclRange + var hclCtx *hcl.EvalContext + if expr != nil { + result, moreDiags := EvalExprAndEvalContext(ctx, expr, phase, scope) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return cty.DynamicVal, diags + } + expr = result.Expression + hclCtx = result.EvalContext + v = result.Value + rng = tfdiags.SourceRangeFromHCL(result.Expression.Range()) + } + + if defs != nil { + v = defs.Apply(v) + } + v, err := convert.Convert(v, wantTy) + if err != nil { + // A conversion failure here could either be caused by an author-provided + // expression that's invalid or by the author omitting the argument + // altogether when there's at least one required attribute, so we'll + // return slightly different messages in each case. + if expr != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid inputs for component", + Detail: fmt.Sprintf("Invalid input variable definition object: %s.", tfdiags.FormatError(err)), + Subject: rng.ToHCL().Ptr(), + Expression: expr, + EvalContext: hclCtx, + }) + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing required inputs for component", + Detail: fmt.Sprintf("Must provide \"inputs\" argument to define the component's input variables: %s.", tfdiags.FormatError(err)), + Subject: rng.ToHCL().Ptr(), + }) + } + return cty.DynamicVal, diags + } + + for _, path := range stackconfigtypes.ProviderInstancePathsInValue(v) { + err := path.NewErrorf("cannot send provider configuration reference to Terraform module input variable") + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid inputs for component", + Detail: fmt.Sprintf( + "Invalid input variable definition object: %s.\n\nUse the separate \"providers\" argument to specify the provider configurations to use for this component's root module.", + tfdiags.FormatError(err), + ), + Subject: rng.ToHCL().Ptr(), + Expression: expr, + EvalContext: hclCtx, + }) + } + + return v, diags +} + // EvalExprAndEvalContext evaluates the given HCL expression in the given // expression scope and returns the resulting value, along with the HCL // evaluation context that was used to produce it. diff --git a/internal/stacks/stackruntime/internal/stackeval/stack.go b/internal/stacks/stackruntime/internal/stackeval/stack.go index 0bcb47b5b4..08160d4c46 100644 --- a/internal/stacks/stackruntime/internal/stackeval/stack.go +++ b/internal/stacks/stackruntime/internal/stackeval/stack.go @@ -169,7 +169,7 @@ func (s *Stack) StackConfig(ctx context.Context) *StackConfig { func (s *Stack) ConfigDeclarations(ctx context.Context) *stackconfig.Declarations { // The declarations really belong to the static StackConfig, since // all instances of a particular stack configuration share the same - // source code. + // source code.ResolveExpressionReference return s.StackConfig(ctx).ConfigDeclarations(ctx) } @@ -410,6 +410,13 @@ func (s *Stack) resolveExpressionReference(ctx context.Context, ref stackaddrs.R // TODO: Most of the below would benefit from "Did you mean..." suggestions // when something is missing but there's a similarly-named object nearby. + // See also a very similar function in stack_config.go. Both are returning + // similar referenceable objects but the context is different. For example, + // in this function we return an instanced Component, while in the other + // function we return a static ComponentConfig. + // + // Some of the returned types are the same across both functions, but most + // are different in terms of static vs dynamic types. switch addr := ref.Target.(type) { case stackaddrs.InputVariable: ret := s.InputVariable(ctx, addr) diff --git a/internal/stacks/stackruntime/plan_test.go b/internal/stacks/stackruntime/plan_test.go index aeab381de6..e0b5b584ea 100644 --- a/internal/stacks/stackruntime/plan_test.go +++ b/internal/stacks/stackruntime/plan_test.go @@ -62,7 +62,21 @@ func TestPlan_valid(t *testing.T) { changesCh := make(chan stackplan.PlannedChange, 8) diagsCh := make(chan tfdiags.Diagnostic, 2) req := PlanRequest{ - Config: cfg, + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(), nil + }, + }, + InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue { + inputs := map[stackaddrs.InputVariable]ExternalInputValue{} + for k, v := range tc.planInputVars { + inputs[stackaddrs.InputVariable{Name: k}] = ExternalInputValue{ + Value: v, + } + } + return inputs + }(), ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ @@ -128,6 +142,15 @@ func TestPlan_invalid(t *testing.T) { return stacks_testing_provider.NewProvider(), nil }, }, + InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue { + inputs := map[stackaddrs.InputVariable]ExternalInputValue{} + for k, v := range tc.planInputVars { + inputs[stackaddrs.InputVariable{Name: k}] = ExternalInputValue{ + Value: v, + } + } + return inputs + }(), ForcePlanTimestamp: &fakePlanTimestamp, } resp := PlanResponse{ @@ -1139,6 +1162,48 @@ func TestPlanWithSensitivePropagationNested(t *testing.T) { } } +func TestPlanWithForEach(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "input-from-component-list")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(), nil + }, + }, + + ForcePlanTimestamp: &fakePlanTimestamp, + + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + stackaddrs.InputVariable{Name: "components"}: { + Value: cty.ListVal([]cty.Value{cty.StringVal("one"), cty.StringVal("two"), cty.StringVal("three")}), + DefRange: tfdiags.SourceRange{}, + }, + }, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + _, diags := collectPlanOutput(changesCh, diagsCh) + + reportDiagnosticsForTest(t, diags) + if len(diags) != 0 { + t.FailNow() // We reported the diags above/ + } +} + // collectPlanOutput consumes the two output channels emitting results from // a call to [Plan], and collects all of the data written to them before // returning once changesCh has been closed by the sender to indicate that diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-component-list/input-from-component-list.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-component-list/input-from-component-list.tfstack.hcl new file mode 100644 index 0000000000..3996006c2f --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-component-list/input-from-component-list.tfstack.hcl @@ -0,0 +1,36 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +variable "components" { + type = set(string) +} + +provider "testing" "default" {} + +component "output" { + source = "../../with-single-output" + + providers = { + testing = provider.testing.default + } + + for_each = var.components +} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = component.output[each.value].id + } + + for_each = var.components +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-component/input-from-component.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-component/input-from-component.tfstack.hcl new file mode 100644 index 0000000000..ecb50730df --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-component/input-from-component.tfstack.hcl @@ -0,0 +1,28 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "output" { + source = "../../with-single-output" + + providers = { + testing = provider.testing.default + } +} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = component.output.id + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-missing-component/input-from-missing-component.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-missing-component/input-from-missing-component.tfstack.hcl new file mode 100644 index 0000000000..750ceb3fe3 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-missing-component/input-from-missing-component.tfstack.hcl @@ -0,0 +1,21 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + // This component doesn't exist. We should see an error. + input = component.output.id + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-provider/input-from-provider.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-provider/input-from-provider.tfstack.hcl new file mode 100644 index 0000000000..f8c4990368 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-provider/input-from-provider.tfstack.hcl @@ -0,0 +1,21 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + # Shouldn't be able to reference providers from here. + input = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/missing-variable/missing-variable.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/missing-variable/missing-variable.tfstack.hcl new file mode 100644 index 0000000000..84c35a1f83 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/missing-variable/missing-variable.tfstack.hcl @@ -0,0 +1,23 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = string +} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + # We do have a required variable, so this should complain. + inputs = {} +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/undeclared-variable/undeclared-variable.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/undeclared-variable/undeclared-variable.tfstack.hcl new file mode 100644 index 0000000000..37531f212b --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/undeclared-variable/undeclared-variable.tfstack.hcl @@ -0,0 +1,21 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + # var.input is not defined + input = var.input + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-output/single-output.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-output/single-output.tf new file mode 100644 index 0000000000..554f80a069 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-output/single-output.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +resource "testing_resource" "data" {} + +output "id" { + value = testing_resource.data.id +} diff --git a/internal/stacks/stackruntime/validate_test.go b/internal/stacks/stackruntime/validate_test.go index e730cd15f8..8c53405baa 100644 --- a/internal/stacks/stackruntime/validate_test.go +++ b/internal/stacks/stackruntime/validate_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/providers" @@ -21,6 +22,11 @@ import ( type validateTestInput struct { skip bool diags func() tfdiags.Diagnostics + + // planInputVars is used only in the plan tests to provide a set of input + // variables to use for the plan request. Validate operates statically so + // does not need any input variables. + planInputVars map[string]cty.Value } var ( @@ -29,6 +35,16 @@ var ( "empty": {}, "variable-output-roundtrip": {}, "variable-output-roundtrip-nested": {}, + filepath.Join("with-single-input", "input-from-component"): {}, + filepath.Join("with-single-input", "input-from-component-list"): { + planInputVars: map[string]cty.Value{ + "components": cty.SetVal([]cty.Value{ + cty.StringVal("one"), + cty.StringVal("two"), + cty.StringVal("three"), + }), + }, + }, filepath.Join("with-single-input", "provider-name-clash"): { skip: true, }, @@ -106,6 +122,70 @@ var ( // error as for missing provider, which is not ideal. skip: true, }, + filepath.Join("with-single-input", "undeclared-variable"): { + diags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared input variable", + Detail: `There is no variable "input" block declared in this stack.`, + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/undeclared-variable/undeclared-variable.tfstack.hcl"), + Start: hcl.Pos{Line: 19, Column: 13, Byte: 284}, + End: hcl.Pos{Line: 19, Column: 22, Byte: 293}, + }, + }) + return diags + }, + }, + filepath.Join("with-single-input", "missing-variable"): { + diags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid inputs for component", + Detail: "Invalid input variable definition object: attribute \"input\" is required.", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/missing-variable/missing-variable.tfstack.hcl"), + Start: hcl.Pos{Line: 22, Column: 12, Byte: 338}, + End: hcl.Pos{Line: 22, Column: 14, Byte: 340}, + }, + }) + return diags + }, + }, + filepath.Join("with-single-input", "input-from-missing-component"): { + diags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared component", + Detail: "There is no component \"output\" block declared in this stack.", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/input-from-missing-component/input-from-missing-component.tfstack.hcl"), + Start: hcl.Pos{Line: 19, Column: 13, Byte: 314}, + End: hcl.Pos{Line: 19, Column: 29, Byte: 330}, + }, + }) + return diags + }, + }, + filepath.Join("with-single-input", "input-from-provider"): { + diags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid inputs for component", + Detail: "Invalid input variable definition object: attribute \"input\": string required.", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/input-from-provider/input-from-provider.tfstack.hcl"), + Start: hcl.Pos{Line: 17, Column: 12, Byte: 239}, + End: hcl.Pos{Line: 20, Column: 4, Byte: 339}, + }, + }) + return diags + }, + }, } ) @@ -129,6 +209,11 @@ func TestValidate_valid(t *testing.T) { diags := Validate(ctx, &ValidateRequest{ Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(), nil + }, + }, }) // The following will fail the test if there are any error