@@ -5,16 +5,21 @@ package command
55
66import (
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+
212323func (c * ValidateCommand ) Synopsis () string {
213324 return "Check whether the configuration is valid"
214325}
0 commit comments