From 694f746748dbf0700c16378d5fb0ca2afe47ed81 Mon Sep 17 00:00:00 2001 From: Sarah French <15078782+SarahFrench@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:31:57 +0000 Subject: [PATCH] validate: Add checking the backend block to the `validate` command (#38021) * feat: Make validate command detect when an unknown backend type is in use. * feat: Make validate command detect when the backend configuration doesn't match the schema. * fix: Stop suppressing the Required:true parts of the backend schema when validating backend blocks * test: Add test showing validation fails when a required attribute is missing from a backend's config --- .../v1.15/NEW FEATURES-20260212-104240.yaml | 5 ++ .../missing-required-attr/main.tf | 9 ++++ internal/command/validate.go | 52 +++++++++++++++++++ internal/command/validate_test.go | 52 +++++++++++-------- 4 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 .changes/v1.15/NEW FEATURES-20260212-104240.yaml create mode 100644 internal/command/testdata/invalid-backend-configuration/missing-required-attr/main.tf diff --git a/.changes/v1.15/NEW FEATURES-20260212-104240.yaml b/.changes/v1.15/NEW FEATURES-20260212-104240.yaml new file mode 100644 index 0000000000..1567580d89 --- /dev/null +++ b/.changes/v1.15/NEW FEATURES-20260212-104240.yaml @@ -0,0 +1,5 @@ +kind: NEW FEATURES +body: "validate: The validate command now checks the `backend` block. This ensures the backend type exists, that all required attributes are present, and that the backend's own validation logic passes." +time: 2026-02-12T10:42:40.333849Z +custom: + Issue: "38021" diff --git a/internal/command/testdata/invalid-backend-configuration/missing-required-attr/main.tf b/internal/command/testdata/invalid-backend-configuration/missing-required-attr/main.tf new file mode 100644 index 0000000000..ca9cb7b956 --- /dev/null +++ b/internal/command/testdata/invalid-backend-configuration/missing-required-attr/main.tf @@ -0,0 +1,9 @@ +terraform { + backend "gcs" { + # Missing required attribute "bucket" + # + # Everything else is missing as well, but this + # test fixture is intended for use testing the validate command, + # which is offline only. So lack of credentials etc is not a problem. + } +} diff --git a/internal/command/validate.go b/internal/command/validate.go index f679e2fae8..c3bd859dbc 100644 --- a/internal/command/validate.go +++ b/internal/command/validate.go @@ -8,7 +8,10 @@ import ( "path/filepath" "strings" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/terraform/internal/addrs" + backendInit "github.com/hashicorp/terraform/internal/backend/init" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/configs" @@ -87,6 +90,13 @@ func (c *ValidateCommand) validate(dir string) tfdiags.Diagnostics { diags = diags.Append(c.validateConfig(cfg)) + // Validation of backend block, if present + // Backend blocks live outside the Terraform graph so we have to do this separately. + backend := cfg.Module.Backend + if backend != nil { + diags = diags.Append(c.validateBackend(backend)) + } + // Unless excluded, we'll also do a quick validation of the Terraform test files. These live // outside the Terraform graph so we have to do this separately. if !c.ParsedArgs.NoTests { @@ -157,6 +167,48 @@ func (c *ValidateCommand) validateTestFiles(cfg *configs.Config) tfdiags.Diagnos return diags } +// We validate the backend in an offline manner, so we use PrepareConfig to validate the configuration (and ENVs present), +// but we never use the Configure method, as that will interact with third-party systems. +// +// The code in this method is very similar to the `backendInitFromConfig` method, expect it doesn't configure the backend. +func (c *ValidateCommand) validateBackend(cfg *configs.Backend) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + bf := backendInit.Backend(cfg.Type) + if bf == nil { + detail := fmt.Sprintf("There is no backend type named %q.", cfg.Type) + if msg, removed := backendInit.RemovedBackends[cfg.Type]; removed { + detail = msg + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported backend type", + Detail: detail, + Subject: &cfg.TypeRange, + }) + return diags + } + + b := bf() + backendSchema := b.ConfigSchema() + + decSpec := backendSchema.DecoderSpec() + configVal, hclDiags := hcldec.Decode(cfg.Config, decSpec, nil) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + return diags + } + + _, validateDiags := b.PrepareConfig(configVal) + diags = diags.Append(validateDiags) + if validateDiags.HasErrors() { + return diags + } + + return diags +} + func (c *ValidateCommand) Synopsis() string { return "Check whether the configuration is valid" } diff --git a/internal/command/validate_test.go b/internal/command/validate_test.go index 8f89189f5e..4ea1fe7f4b 100644 --- a/internal/command/validate_test.go +++ b/internal/command/validate_test.go @@ -221,7 +221,6 @@ func TestMissingDefinedVar(t *testing.T) { } func TestValidateWithInvalidTestFile(t *testing.T) { - // We're reusing some testing configs that were written for testing the // test command here, so we have to initalise things slightly differently // to the other tests. @@ -253,7 +252,6 @@ func TestValidateWithInvalidTestFile(t *testing.T) { } func TestValidateWithInvalidTestModule(t *testing.T) { - // We're reusing some testing configs that were written for testing the // test command here, so we have to initalise things slightly differently // to the other tests. @@ -310,7 +308,6 @@ func TestValidateWithInvalidTestModule(t *testing.T) { } func TestValidateWithInvalidOverrides(t *testing.T) { - // We're reusing some testing configs that were written for testing the // test command here, so we have to initalise things slightly differently // to the other tests. @@ -547,33 +544,44 @@ func TestValidate_backendBlocks(t *testing.T) { } }) - // TODO: Should this validation be added? - t.Run("NOT invalid when the backend type is unknown", func(t *testing.T) { + t.Run("invalid when the backend type is unknown", func(t *testing.T) { output, code := setupTest(t, "invalid-backend-configuration/unknown-backend-type") - if code != 0 { - t.Fatalf("expected a successful exit code %d\n\n%s", code, output.Stderr()) + if code != 1 { + t.Fatalf("expected an unsuccessful exit code %d\n\n%s", code, output.Stderr()) } - expectedMsg := "Success! The configuration is valid." - if !strings.Contains(output.Stdout(), expectedMsg) { - t.Fatalf("unexpected output content: wanted %q, got: %s", - expectedMsg, - output.Stdout(), + expectedErr := "Error: Unsupported backend type" + if !strings.Contains(output.Stderr(), expectedErr) { + t.Fatalf("unexpected error content: wanted %q, got: %s", + expectedErr, + output.Stderr(), ) } }) - // Backend blocks aren't validated using their schemas currently. - // TODO: Should this validation be added? - t.Run("NOT invalid when there's an unknown attribute present", func(t *testing.T) { + t.Run("invalid when there's an unknown attribute present", func(t *testing.T) { output, code := setupTest(t, "invalid-backend-configuration/unknown-attr") - if code != 0 { - t.Fatalf("expected a successful exit code %d\n\n%s", code, output.Stderr()) + if code != 1 { + t.Fatalf("expected an unsuccessful exit code %d\n\n%s", code, output.Stdout()) } - expectedMsg := "Success! The configuration is valid." - if !strings.Contains(output.Stdout(), expectedMsg) { - t.Fatalf("unexpected output content: wanted %q, got: %s", - expectedMsg, - output.Stdout(), + expectedErr := "Error: Unsupported argument" + if !strings.Contains(output.Stderr(), expectedErr) { + t.Fatalf("unexpected error content: wanted %q, got: %s", + expectedErr, + output.Stderr(), + ) + } + }) + + t.Run("invalid when a required attribute is unset", func(t *testing.T) { + output, code := setupTest(t, "invalid-backend-configuration/missing-required-attr") + if code != 1 { + t.Fatalf("expected an unsuccessful exit code %d\n\n%s", code, output.Stdout()) + } + expectedErr := "Error: Missing required argument" + if !strings.Contains(output.Stderr(), expectedErr) { + t.Fatalf("unexpected error content: wanted %q, got: %s", + expectedErr, + output.Stderr(), ) } })