diff --git a/internal/stacks/stackruntime/internal/stackeval/component_config.go b/internal/stacks/stackruntime/internal/stackeval/component_config.go index c0c2ad2cc3..9771d0cb33 100644 --- a/internal/stacks/stackruntime/internal/stackeval/component_config.go +++ b/internal/stacks/stackruntime/internal/stackeval/component_config.go @@ -323,7 +323,23 @@ func (c *ComponentConfig) CheckProviders(ctx context.Context, phase EvalPhase) ( continue } - // TODO: Also validate the provider types are the same. + // TODO: It's not currently possible to assign a provider configuration + // with a different local name even if the types match. Find out if + // this is deliberate. Note, the component_instance CheckProviders + // function also enforces this. + // + // In theory you should be able to do this: + // provider_one = provider.provider_two.default + // + // Assuming the underlying types of the providers are the same, even if + // the local names are not. This is not possible at the moment, the + // local names must match up. + // + // We'll have to partially parse the reference here to get the local + // configuration block (uninstanced), and then resolve the underlying + // type. And then make sure it matches the type of the provider we're + // assigning it to in the module. Also, we should fix the equivalent + // function in component_instance at the same time. ret.Add(inCalleeAddr) } @@ -403,9 +419,14 @@ func (c *ComponentConfig) checkValid(ctx context.Context, phase EvalPhase) tfdia } decl := c.Declaration(ctx) - // TODO: Also check if the providers are valid. // TODO: Also check if the input variables are valid. + _, providerDiags := c.CheckProviders(ctx, phase) + diags = diags.Append(providerDiags) + if providerDiags.HasErrors() { + return diags, nil + } + providerSchemas, moreDiags := c.neededProviderSchemas(ctx, phase) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/invalid-provider/invalid-provider.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/invalid-provider/invalid-provider.tfstack.hcl new file mode 100644 index 0000000000..83926ee5ce --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/invalid-provider/invalid-provider.tfstack.hcl @@ -0,0 +1,26 @@ +required_providers { + testing = { + // The source is wrong, so validate should complain. + source = "hashicorp/wrong" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = string +} + +component "self" { + source = "../" + + providers = { + // Everything looks okay here, but the provider types are actually wrong. + testing = provider.testing.default + } + + inputs = { + input = var.input + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/missing-provider/missing-provider.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/missing-provider/missing-provider.tfstack.hcl new file mode 100644 index 0000000000..f05795622e --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/missing-provider/missing-provider.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 = "../" + + # We do actually require a provider here, Validate() should warn us. + providers = {} + + inputs = { + input = var.input + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/provider-name-clash/provider-name-clash.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/provider-name-clash/provider-name-clash.tfstack.hcl new file mode 100644 index 0000000000..42df655435 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/provider-name-clash/provider-name-clash.tfstack.hcl @@ -0,0 +1,26 @@ +required_providers { + other = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "other" "default" {} + +variable "input" { + type = string +} + +component "self" { + source = "../" + + providers = { + // Even though the names are wrong, the underlying types are the same + // so this should be okay. + testing = provider.other.default + } + + inputs = { + input = var.input + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/undeclared-provider/undeclared-provider.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/undeclared-provider/undeclared-provider.tfstack.hcl new file mode 100644 index 0000000000..fa29629b95 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/undeclared-provider/undeclared-provider.tfstack.hcl @@ -0,0 +1,16 @@ +variable "input" { + type = string +} + +component "self" { + source = "../" + + providers = { + # We haven't provided a definition for this anywhere. + testing = provider.testing.default + } + + inputs = { + input = var.input + } +} diff --git a/internal/stacks/stackruntime/validate_test.go b/internal/stacks/stackruntime/validate_test.go index bbd66c48e1..0897f36ee8 100644 --- a/internal/stacks/stackruntime/validate_test.go +++ b/internal/stacks/stackruntime/validate_test.go @@ -5,6 +5,7 @@ package stackruntime import ( "context" + "path/filepath" "testing" "time" @@ -25,14 +26,24 @@ import ( // potentially be included in here unless it depends on provider plugins // to complete validation, since this test cannot supply provider plugins. func TestValidate_valid(t *testing.T) { - validConfigDirs := []string{ - "empty", - "variable-output-roundtrip", - "variable-output-roundtrip-nested", + validConfigDirs := map[string]struct { + skip bool + }{ + "empty": {}, + "variable-output-roundtrip": {}, + "variable-output-roundtrip-nested": {}, + filepath.Join("with-single-input", "provider-name-clash"): { + skip: true, + }, } - for _, name := range validConfigDirs { + for name, tc := range validConfigDirs { t.Run(name, func(t *testing.T) { + if tc.skip { + // We've added this test before the implementation was ready. + t.SkipNow() + } + ctx := context.Background() cfg := loadMainBundleConfigForTest(t, name) @@ -55,6 +66,7 @@ func TestValidate_valid(t *testing.T) { func TestValidate_invalid(t *testing.T) { tcs := map[string]struct { diags func() tfdiags.Diagnostics + skip bool }{ "validate-undeclared-variable": { diags: func() tfdiags.Diagnostics { @@ -88,10 +100,53 @@ func TestValidate_invalid(t *testing.T) { return diags }, }, + filepath.Join("with-single-input", "undeclared-provider"): { + diags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Component requires undeclared provider", + Detail: "The root module for component.self requires a configuration for provider \"hashicorp/testing\", which isn't declared as a dependency of this stack configuration.\n\nDeclare this provider in the stack's required_providers block, and then assign a configuration for that provider in this component's \"providers\" argument.", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/undeclared-provider/undeclared-provider.tfstack.hcl"), + Start: hcl.Pos{Line: 5, Column: 1, Byte: 38}, + End: hcl.Pos{Line: 5, Column: 17, Byte: 54}, + }, + }) + return diags + }, + }, + filepath.Join("with-single-input", "missing-provider"): { + diags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing required provider configuration", + Detail: "The root module for component.self requires a provider configuration named \"testing\" for provider \"hashicorp/testing\", which is not assigned in the component's \"providers\" argument.", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/missing-provider/missing-provider.tfstack.hcl"), + Start: hcl.Pos{Line: 14, Column: 1, Byte: 169}, + End: hcl.Pos{Line: 14, Column: 17, Byte: 185}, + }, + }) + return diags + }, + }, + filepath.Join("with-single-input", "invalid-provider"): { + // TODO: Enable this test case, when we have a good error message + // for provider type mismatches. Currently, we return the same + // error as for missing provider, which is not ideal. + skip: true, + }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { + if tc.skip { + // We've added this test before the implementation was ready. + t.SkipNow() + } + ctx := context.Background() cfg := loadMainBundleConfigForTest(t, name)