Skip to content

Commit 4c76e2a

Browse files
committed
feat: Add Validate method for asserting that a plan's description of a backend or state store is complete
The alternative approach would be to change the existing `Backend` field in the `Plan` struct to be a pointer. I'm open to either option, but the approach of using an `Empty` method matches existing work in the `workdir` package when inspecting the backend state file, and that seems a similar use-case to inspecting the plan file.
1 parent 03935d5 commit 4c76e2a

File tree

2 files changed

+242
-5
lines changed

2 files changed

+242
-5
lines changed

internal/plans/plan.go

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
package plans
55

66
import (
7+
"errors"
8+
"fmt"
79
"sort"
810
"time"
911

@@ -256,6 +258,22 @@ func NewBackend(typeName string, config cty.Value, configSchema *configschema.Bl
256258
}, nil
257259
}
258260

261+
func (b *Backend) Validate() error {
262+
if b == nil {
263+
return errors.New("plan contains a nil Backend")
264+
}
265+
if b.Type == "" {
266+
return fmt.Errorf("plan's description of a backend has an unset Type: %#v", b)
267+
}
268+
if b.Workspace == "" {
269+
return fmt.Errorf("plan's description of a backend has the Workspace unset: %#v", b)
270+
}
271+
if len(b.Config) == 0 {
272+
return fmt.Errorf("plan's description of a backend includes no Config: %#v", b)
273+
}
274+
return nil
275+
}
276+
259277
// StateStore represents the state store-related configuration and other data as it
260278
// existed when a plan was created.
261279
type StateStore struct {
@@ -264,8 +282,8 @@ type StateStore struct {
264282

265283
Provider *Provider
266284

267-
// Config is the configuration of the state store, whose schema is obtained
268-
// from the host provider's GetProviderSchema response.
285+
// Config is the configuration of the state store, excluding the nested provider block.
286+
// The schema is determined by the state store's type and data received via GetProviderSchema RPC.
269287
Config DynamicValue
270288

271289
// Workspace is the name of the workspace that was active when the plan
@@ -277,15 +295,50 @@ type StateStore struct {
277295
Workspace string
278296
}
279297

298+
func (s *StateStore) Validate() error {
299+
if s == nil {
300+
return errors.New("plan contains a nil StateStore")
301+
}
302+
if s.Type == "" {
303+
return fmt.Errorf("plan's description of a state store has an unset Type: %#v", s)
304+
}
305+
if len(s.Config) == 0 {
306+
return fmt.Errorf("plan's description of a state store includes no Config: %#v", s)
307+
}
308+
if err := s.Provider.Validate(); err != nil {
309+
return err
310+
}
311+
if s.Workspace == "" {
312+
return fmt.Errorf("plan's description of a state store has an unset Workspace: %#v", s)
313+
}
314+
return nil
315+
}
316+
280317
type Provider struct {
281318
Version *version.Version // The specific provider version used for the state store. Should be set using a getproviders.Version, etc.
282319
Source *tfaddr.Provider // The FQN/fully-qualified name of the provider.
283320

284-
// Config is the configuration of the state store, whose schema is obtained
285-
// from the host provider's GetProviderSchema response.
321+
// Config is the configuration of the provider block nested within state_store.
322+
// The schema is determined by data received via GetProviderSchema RPC.
286323
Config DynamicValue
287324
}
288325

326+
func (p *Provider) Validate() error {
327+
if p == nil {
328+
return errors.New("plan's description of a state store contains a nil Provider")
329+
}
330+
if p.Version == nil {
331+
return fmt.Errorf("plan's description of a state store contains a nil provider Version: %#v", p)
332+
}
333+
if p.Source == nil {
334+
return fmt.Errorf("plan's description of a state store contains a nil provider Source: %#v", p)
335+
}
336+
if len(p.Config) == 0 {
337+
return fmt.Errorf("plan's description of a state store includes no provider Config: %#v", p)
338+
}
339+
return nil
340+
}
341+
289342
func NewStateStore(typeName string, ver *version.Version, source *tfaddr.Provider, storeConfig cty.Value, storeSchema *configschema.Block, providerConfig cty.Value, providerSchema *configschema.Block, workspaceName string) (*StateStore, error) {
290343
sdv, err := NewDynamicValue(storeConfig, storeSchema.ImpliedType())
291344
if err != nil {

internal/plans/plan_test.go

Lines changed: 185 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import (
77
"testing"
88

99
"github.com/go-test/deep"
10+
"github.com/zclconf/go-cty/cty"
1011

12+
version "github.com/hashicorp/go-version"
1113
"github.com/hashicorp/terraform/internal/addrs"
12-
"github.com/zclconf/go-cty/cty"
14+
"github.com/hashicorp/terraform/internal/configs/configschema"
1315
)
1416

1517
func TestProviderAddrs(t *testing.T) {
@@ -105,6 +107,188 @@ func TestProviderAddrs(t *testing.T) {
105107
}
106108
}
107109

110+
func TestBackend_Validate(t *testing.T) {
111+
112+
typeName := "foobar"
113+
workspace := "default"
114+
config := cty.ObjectVal(map[string]cty.Value{
115+
"bool": cty.BoolVal(true),
116+
})
117+
schema := &configschema.Block{
118+
Attributes: map[string]*configschema.Attribute{
119+
"bool": {
120+
Type: cty.Bool,
121+
},
122+
},
123+
}
124+
125+
// Not-empty cases
126+
t.Run("backend is not valid if all values are set", func(t *testing.T) {
127+
b, err := NewBackend(typeName, config, schema, workspace)
128+
if err != nil {
129+
t.Fatalf("unexpected error: %s", err)
130+
}
131+
if err := b.Validate(); err != nil {
132+
t.Fatalf("expected the Backend to be valid, given all input values were provided: %s", err)
133+
}
134+
})
135+
t.Run("backend is not empty if the schema contains no attributes or blocks", func(t *testing.T) {
136+
emptyConfig := cty.ObjectVal(map[string]cty.Value{})
137+
emptySchema := &configschema.Block{
138+
Attributes: map[string]*configschema.Attribute{
139+
// No attributes
140+
},
141+
}
142+
b, err := NewBackend(typeName, emptyConfig, emptySchema, workspace)
143+
if err != nil {
144+
t.Fatalf("unexpected error: %s", err)
145+
}
146+
if err := b.Validate(); err != nil {
147+
t.Fatalf("expected the Backend to be valid, as empty schemas should be tolerated: %s", err)
148+
}
149+
})
150+
151+
// Empty cases
152+
t.Run("backend is empty if type name is missing", func(t *testing.T) {
153+
b, err := NewBackend("", config, schema, workspace)
154+
if err != nil {
155+
t.Fatalf("unexpected error: %s", err)
156+
}
157+
if err := b.Validate(); err == nil {
158+
t.Fatalf("expected the Backend to be invalid, given the type being unset: %#v", b)
159+
}
160+
})
161+
t.Run("backend is empty if workspace name is missing", func(t *testing.T) {
162+
b, err := NewBackend(typeName, config, schema, "")
163+
if err != nil {
164+
t.Fatalf("unexpected error: %s", err)
165+
}
166+
if err := b.Validate(); err == nil {
167+
t.Fatalf("expected the Backend to be invalid, given the type being unset: %#v", b)
168+
}
169+
})
170+
}
171+
172+
func TestStateStore_Validate(t *testing.T) {
173+
typeName := "test_store"
174+
providerVersion := version.Must(version.NewSemver("1.2.3"))
175+
source := addrs.MustParseProviderSourceString("hashicorp/test")
176+
workspace := "default"
177+
config := cty.ObjectVal(map[string]cty.Value{
178+
"bool": cty.BoolVal(true),
179+
})
180+
schema := &configschema.Block{
181+
Attributes: map[string]*configschema.Attribute{
182+
"bool": {
183+
Type: cty.Bool,
184+
},
185+
},
186+
}
187+
188+
// Not-empty cases
189+
t.Run("state store is not empty if all values are set", func(t *testing.T) {
190+
s, err := NewStateStore(typeName, providerVersion, &source, config, schema, config, schema, workspace)
191+
if err != nil {
192+
t.Fatalf("unexpected error: %s", err)
193+
}
194+
if err := s.Validate(); err != nil {
195+
t.Fatalf("expected the StateStore to be valid, given all input values were provided: %s", err)
196+
}
197+
})
198+
t.Run("state store is not empty if the state store config is present but contains all null values", func(t *testing.T) {
199+
nullConfig := cty.ObjectVal(map[string]cty.Value{
200+
"bool": cty.NullVal(cty.Bool),
201+
})
202+
s, err := NewStateStore(typeName, providerVersion, &source, nullConfig, schema, config, schema, workspace)
203+
if err != nil {
204+
t.Fatalf("unexpected error: %s", err)
205+
}
206+
if err := s.Validate(); err != nil {
207+
t.Fatalf("expected the StateStore to be valid, despite the state store config containing only null values: %s", err)
208+
}
209+
})
210+
t.Run("state store is not empty if the provider config is present but contains all null values", func(t *testing.T) {
211+
nullConfig := cty.ObjectVal(map[string]cty.Value{
212+
"bool": cty.NullVal(cty.Bool),
213+
})
214+
s, err := NewStateStore(typeName, providerVersion, &source, nullConfig, schema, config, schema, workspace)
215+
if err != nil {
216+
t.Fatalf("unexpected error: %s", err)
217+
}
218+
if err := s.Validate(); err != nil {
219+
t.Fatalf("expected the StateStore to be valid, despite the provider config containing only null values: %s", err)
220+
}
221+
})
222+
t.Run("state store is not incorrectly identified as empty if the state store's schema contains no attributes or blocks", func(t *testing.T) {
223+
emptyConfig := cty.ObjectVal(map[string]cty.Value{})
224+
emptySchema := &configschema.Block{
225+
Attributes: map[string]*configschema.Attribute{
226+
// No attributes
227+
},
228+
}
229+
s, err := NewStateStore(typeName, providerVersion, &source, emptyConfig, emptySchema, config, schema, workspace)
230+
if err != nil {
231+
t.Fatalf("unexpected error: %s", err)
232+
}
233+
if err := s.Validate(); err != nil {
234+
t.Fatalf("expected the StateStore to be valid, as empty schemas should be tolerated: %s", err)
235+
}
236+
})
237+
t.Run("state store is not incorrectly identified as empty if the provider's schema contains no attributes or blocks", func(t *testing.T) {
238+
emptyConfig := cty.ObjectVal(map[string]cty.Value{})
239+
emptySchema := &configschema.Block{
240+
Attributes: map[string]*configschema.Attribute{
241+
// No attributes
242+
},
243+
}
244+
s, err := NewStateStore(typeName, providerVersion, &source, config, schema, emptyConfig, emptySchema, workspace)
245+
if err != nil {
246+
t.Fatalf("unexpected error: %s", err)
247+
}
248+
if err := s.Validate(); err != nil {
249+
t.Fatalf("expected the StateStore to be valid, as empty schemas should be tolerated: %s", err)
250+
}
251+
})
252+
253+
// Empty cases
254+
t.Run("state store is empty if the type is missing", func(t *testing.T) {
255+
s, err := NewStateStore("", providerVersion, &source, config, schema, config, schema, workspace)
256+
if err != nil {
257+
t.Fatalf("unexpected error: %s", err)
258+
}
259+
if err := s.Validate(); err == nil {
260+
t.Fatalf("expected the StateStore to be invalid, given the type name is missing: %s", err)
261+
}
262+
})
263+
t.Run("state store is empty if the provider version is missing", func(t *testing.T) {
264+
s, err := NewStateStore(typeName, nil, &source, config, schema, config, schema, workspace)
265+
if err != nil {
266+
t.Fatalf("unexpected error: %s", err)
267+
}
268+
if err := s.Validate(); err == nil {
269+
t.Fatalf("expected the StateStore to be invalid, given the version is missing: %s", err)
270+
}
271+
})
272+
t.Run("state store is empty if the provider source is missing", func(t *testing.T) {
273+
s, err := NewStateStore(typeName, providerVersion, nil, config, schema, config, schema, workspace)
274+
if err != nil {
275+
t.Fatalf("unexpected error: %s", err)
276+
}
277+
if err := s.Validate(); err == nil {
278+
t.Fatalf("expected the StateStore to be invalid, given the version is missing: %s", err)
279+
}
280+
})
281+
t.Run("state store is empty if the workspace name is missing", func(t *testing.T) {
282+
s, err := NewStateStore(typeName, providerVersion, &source, cty.NilVal, schema, config, schema, "")
283+
if err != nil {
284+
t.Fatalf("unexpected error: %s", err)
285+
}
286+
if err := s.Validate(); err == nil {
287+
t.Fatalf("expected the StateStore to be invalid, given the workspace name is missing: %s", err)
288+
}
289+
})
290+
}
291+
108292
// Module outputs should not effect the result of Empty
109293
func TestModuleOutputChangesEmpty(t *testing.T) {
110294
changes := &ChangesSrc{

0 commit comments

Comments
 (0)