mirror of
https://github.com/hashicorp/terraform.git
synced 2026-05-28 04:03:27 -04:00
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
This commit is contained in:
parent
c1f6360120
commit
694f746748
4 changed files with 96 additions and 22 deletions
5
.changes/v1.15/NEW FEATURES-20260212-104240.yaml
Normal file
5
.changes/v1.15/NEW FEATURES-20260212-104240.yaml
Normal file
|
|
@ -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"
|
||||
9
internal/command/testdata/invalid-backend-configuration/missing-required-attr/main.tf
vendored
Normal file
9
internal/command/testdata/invalid-backend-configuration/missing-required-attr/main.tf
vendored
Normal file
|
|
@ -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…
Reference in a new issue