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
pull/38157/head
Sarah French 2 months ago committed by GitHub
parent c1f6360120
commit 694f746748
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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"

@ -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.
}
}

@ -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"
}

@ -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(),
)
}
})

Loading…
Cancel
Save