Skip to content

Commit 743e84b

Browse files
committed
feat: Allow validate command to validate state_store blocks.
1 parent bdfce32 commit 743e84b

10 files changed

Lines changed: 522 additions & 7 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
terraform {
2+
required_providers {
3+
test = {
4+
source = "hashicorp/test"
5+
}
6+
}
7+
state_store "test_store" {
8+
provider "test" {
9+
# Test mock provider will create a required attribute for the provider
10+
# and there are no attributes here in the config...
11+
}
12+
value = "foobar"
13+
}
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
terraform {
2+
required_providers {
3+
test = {
4+
source = "hashicorp/test"
5+
}
6+
}
7+
state_store "test_store" {
8+
provider "test" {
9+
# missing required attribute "value"
10+
}
11+
}
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
terraform {
2+
required_providers {
3+
test = {
4+
source = "hashicorp/test"
5+
}
6+
}
7+
state_store "test_store" {
8+
provider "test" {
9+
region = "region1"
10+
region = "region2" # Should trigger an error
11+
}
12+
}
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
terraform {
2+
required_providers {
3+
test = {
4+
source = "hashicorp/test"
5+
}
6+
}
7+
state_store "test_store" {
8+
provider "test" {}
9+
value = "value1"
10+
value = "value2" # Should trigger an error
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
terraform {
2+
required_providers {
3+
test = {
4+
source = "hashicorp/test"
5+
}
6+
}
7+
state_store "test_store" {
8+
provider "test" {
9+
unknown = "this isn't in the test provider's schema" # Should trigger an error
10+
}
11+
}
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
terraform {
2+
required_providers {
3+
test = {
4+
source = "hashicorp/test"
5+
}
6+
}
7+
state_store "test_store" {
8+
provider "test" {}
9+
unknown = "this isn't in test_store's schema" # Should trigger an error
10+
}
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
terraform {
2+
required_providers {
3+
test = {
4+
source = "hashicorp/test"
5+
}
6+
}
7+
state_store "test_nonexistent" { # nonexistent is not a valid state store type in the mocked provider
8+
provider "test" {}
9+
}
10+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
terraform {
2+
required_providers {
3+
test = {
4+
source = "hashicorp/test"
5+
}
6+
}
7+
state_store "test_store" {
8+
provider "test" {
9+
region = "saturn"
10+
}
11+
value = "foobar"
12+
}
13+
}
14+
15+
# This config is valid, but the test will force the provider
16+
# or state store's config validation methods to return an error.

internal/command/validate.go

Lines changed: 118 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,21 @@ package command
55

66
import (
77
"fmt"
8+
"maps"
89
"path/filepath"
10+
"slices"
911
"strings"
1012

1113
"github.com/hashicorp/hcl/v2"
1214
"github.com/hashicorp/hcl/v2/hcldec"
1315
"github.com/hashicorp/terraform/internal/addrs"
1416
backendInit "github.com/hashicorp/terraform/internal/backend/init"
17+
backendPluggable "github.com/hashicorp/terraform/internal/backend/pluggable"
1518
"github.com/hashicorp/terraform/internal/command/arguments"
1619
"github.com/hashicorp/terraform/internal/command/views"
1720
"github.com/hashicorp/terraform/internal/configs"
21+
"github.com/hashicorp/terraform/internal/didyoumean"
22+
"github.com/hashicorp/terraform/internal/providers"
1823
"github.com/hashicorp/terraform/internal/terraform"
1924
"github.com/hashicorp/terraform/internal/tfdiags"
2025
)
@@ -92,9 +97,11 @@ func (c *ValidateCommand) validate(dir string) tfdiags.Diagnostics {
9297

9398
// Validation of backend block, if present
9499
// Backend blocks live outside the Terraform graph so we have to do this separately.
95-
backend := cfg.Module.Backend
96-
if backend != nil {
97-
diags = diags.Append(c.validateBackend(backend))
100+
switch {
101+
case cfg.Module.Backend != nil:
102+
diags = diags.Append(c.validateBackend(cfg.Module.Backend))
103+
case cfg.Module.StateStore != nil:
104+
diags = diags.Append(c.validateStateStore(cfg.Module.StateStore))
98105
}
99106

100107
// Unless excluded, we'll also do a quick validation of the Terraform test files. These live
@@ -134,7 +141,6 @@ func (c *ValidateCommand) validateTestFiles(cfg *configs.Config) tfdiags.Diagnos
134141
diags = diags.Append(file.Validate(cfg))
135142

136143
for _, run := range file.Runs {
137-
138144
if run.Module != nil {
139145
// Then we can also validate the referenced modules, but we are
140146
// only going to do this is if they are local modules.
@@ -144,7 +150,6 @@ func (c *ValidateCommand) validateTestFiles(cfg *configs.Config) tfdiags.Diagnos
144150
// the registry, the expectation is that the author of the
145151
// module should have ran `terraform validate` themselves.
146152
if _, ok := run.Module.Source.(addrs.ModuleSourceLocal); ok {
147-
148153
if validated := validatedModules[run.Module.Source.String()]; !validated {
149154

150155
// Since we can reference the same module twice, let's
@@ -153,14 +158,12 @@ func (c *ValidateCommand) validateTestFiles(cfg *configs.Config) tfdiags.Diagnos
153158
validatedModules[run.Module.Source.String()] = true
154159
diags = diags.Append(c.validateConfig(run.ConfigUnderTest))
155160
}
156-
157161
}
158162

159163
diags = diags.Append(run.Validate(run.ConfigUnderTest))
160164
} else {
161165
diags = diags.Append(run.Validate(cfg))
162166
}
163-
164167
}
165168
}
166169

@@ -209,6 +212,114 @@ func (c *ValidateCommand) validateBackend(cfg *configs.Backend) tfdiags.Diagnost
209212
return diags
210213
}
211214

215+
// We validate the state store in an offline manner, so we use:
216+
// - State store's PrepareConfig method to validate the state_store block.
217+
// - Provider's ValidateProviderConfig to validate the nested provider block.
218+
// We don't use the Configure method, as that will interact with third-party systems.
219+
//
220+
// The code in this method is very similar to the `stateStoreInitFromConfig` method,
221+
// expect it doesn't configure the provider or the state store.
222+
func (c *ValidateCommand) validateStateStore(cfg *configs.StateStore) tfdiags.Diagnostics {
223+
var diags tfdiags.Diagnostics
224+
225+
locks, depsDiags := c.Meta.lockedDependencies()
226+
if depsDiags.HasErrors() {
227+
// Add some context to the error so it's obvious that it's related to the state store.
228+
newDiag := &hcl.Diagnostic{
229+
Severity: hcl.DiagError,
230+
Summary: "Unable to validate state store configuration",
231+
Detail: fmt.Sprintf("An unexpected error was encountered when loading the dependency locks file. Make sure the working directory has been initialized and try again. Error: %s", diags.Err()),
232+
Subject: &cfg.DeclRange,
233+
}
234+
return diags.Append(newDiag)
235+
}
236+
diags = diags.Append(depsDiags) // Preserve any warnings
237+
238+
factory, pDiags := c.Meta.StateStoreProviderFactoryFromConfig(cfg, locks)
239+
diags = diags.Append(pDiags)
240+
if pDiags.HasErrors() {
241+
return diags
242+
}
243+
244+
provider, err := factory()
245+
if err != nil {
246+
diags = diags.Append(fmt.Errorf("Unable to validate state store configuration. Terraform was unable to obtain a provider instance during state store initialization: %w", err))
247+
return diags
248+
}
249+
defer provider.Close()
250+
251+
resp := provider.GetProviderSchema()
252+
253+
if len(resp.StateStores) == 0 {
254+
diags = diags.Append(&hcl.Diagnostic{
255+
Severity: hcl.DiagError,
256+
Summary: "Provider does not support pluggable state storage",
257+
Detail: fmt.Sprintf("There are no state stores implemented by provider %s (%q)",
258+
cfg.Provider.Name,
259+
cfg.ProviderAddr),
260+
Subject: &cfg.DeclRange,
261+
})
262+
return diags
263+
}
264+
265+
schema, exists := resp.StateStores[cfg.Type]
266+
if !exists {
267+
suggestions := slices.Sorted(maps.Keys(resp.StateStores))
268+
suggestion := didyoumean.NameSuggestion(cfg.Type, suggestions)
269+
if suggestion != "" {
270+
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
271+
}
272+
diags = diags.Append(&hcl.Diagnostic{
273+
Severity: hcl.DiagError,
274+
Summary: "State store not implemented by the provider",
275+
Detail: fmt.Sprintf("State store %q is not implemented by provider %s (%q)%s",
276+
cfg.Type, cfg.Provider.Name,
277+
cfg.ProviderAddr, suggestion),
278+
Subject: &cfg.DeclRange,
279+
})
280+
return diags
281+
}
282+
283+
// Handle the nested provider block.
284+
pDecSpec := resp.Provider.Body.DecoderSpec()
285+
pConfig := cfg.Provider.Config
286+
providerConfigVal, pDecDiags := hcldec.Decode(pConfig, pDecSpec, nil)
287+
diags = diags.Append(pDecDiags)
288+
if pDecDiags.HasErrors() {
289+
return diags
290+
}
291+
292+
// Handle the schema for the state store itself, excluding the provider block.
293+
ssdecSpec := schema.Body.DecoderSpec()
294+
stateStoreConfigVal, ssDecDiags := hcldec.Decode(cfg.Config, ssdecSpec, nil)
295+
diags = diags.Append(ssDecDiags)
296+
if ssDecDiags.HasErrors() {
297+
return diags
298+
}
299+
300+
// Validate the provider config
301+
//
302+
// NOTE: We don't configure the provider because the validate command is offline-only.
303+
validateResp := provider.ValidateProviderConfig(providers.ValidateProviderConfigRequest{
304+
Config: providerConfigVal,
305+
})
306+
diags = diags.Append(validateResp.Diagnostics)
307+
if validateResp.Diagnostics.HasErrors() {
308+
return diags
309+
}
310+
311+
// Validate the state store config
312+
//
313+
// NOTE: We don't configure the state store because the validate command is offline-only.
314+
p, err := backendPluggable.NewPluggable(provider, cfg.Type)
315+
if err != nil {
316+
diags = diags.Append(err)
317+
}
318+
_, validateDiags := p.PrepareConfig(stateStoreConfigVal)
319+
diags = diags.Append(validateDiags)
320+
return diags
321+
}
322+
212323
func (c *ValidateCommand) Synopsis() string {
213324
return "Check whether the configuration is valid"
214325
}

0 commit comments

Comments
 (0)