Skip to content

Commit f26f779

Browse files
committed
Add filesystem state store (no locking) and E2E test
1 parent 0c5275f commit f26f779

File tree

4 files changed

+453
-2
lines changed

4 files changed

+453
-2
lines changed

internal/command/e2etest/primary_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/hashicorp/terraform/internal/e2e"
1616
"github.com/hashicorp/terraform/internal/getproviders"
1717
"github.com/hashicorp/terraform/internal/plans"
18+
"github.com/hashicorp/terraform/internal/states/statefile"
1819
"github.com/zclconf/go-cty/cty"
1920
)
2021

@@ -249,6 +250,85 @@ func TestPrimary_stateStore(t *testing.T) {
249250
}
250251
t.Parallel()
251252

253+
tf := e2e.NewBinary(t, terraformBin, "testdata/full-workflow-with-state-store-fs")
254+
255+
// In order to test integration with PSS we need a provider plugin implementing a state store.
256+
// Here will build the simple6 (built with protocol v6) provider, which implements PSS.
257+
simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6")
258+
simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider)
259+
260+
// Move the provider binaries into a directory that we will point terraform
261+
// to using the -plugin-dir cli flag.
262+
platform := getproviders.CurrentPlatform.String()
263+
hashiDir := "cache/registry.terraform.io/hashicorp/"
264+
if err := os.MkdirAll(tf.Path(hashiDir, "simple6/0.0.1/", platform), os.ModePerm); err != nil {
265+
t.Fatal(err)
266+
}
267+
if err := os.Rename(simple6ProviderExe, tf.Path(hashiDir, "simple6/0.0.1/", platform, "terraform-provider-simple6")); err != nil {
268+
t.Fatal(err)
269+
}
270+
271+
//// INIT
272+
stdout, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color")
273+
if err != nil {
274+
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
275+
}
276+
277+
if !strings.Contains(stdout, "Terraform created an empty state file for the default workspace") {
278+
t.Errorf("notice about creating the default workspace is missing from init output:\n%s", stdout)
279+
}
280+
281+
//// PLAN
282+
// No separate plan step; this test lets the apply make a plan.
283+
284+
//// APPLY
285+
stdout, stderr, err = tf.Run("apply", "-auto-approve", "-no-color")
286+
if err != nil {
287+
t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
288+
}
289+
290+
if !strings.Contains(stdout, "Resources: 1 added, 0 changed, 0 destroyed") {
291+
t.Errorf("incorrect apply tally; want 1 added:\n%s", stdout)
292+
}
293+
294+
// Check the statefile saved by the fs state store.
295+
path := "terraform.tfstate.d/default/terraform.tfstate"
296+
f, err := tf.OpenFile(path)
297+
if err != nil {
298+
t.Fatalf("unexpected error opening state file %s: %s\nstderr:\n%s", path, err, stderr)
299+
}
300+
defer f.Close()
301+
302+
stateFile, err := statefile.Read(f)
303+
if err != nil {
304+
t.Fatalf("unexpected error reading statefile %s: %s\nstderr:\n%s", path, err, stderr)
305+
}
306+
307+
r := stateFile.State.RootModule().Resources
308+
if len(r) != 1 {
309+
t.Fatalf("expected state to include one resource, but got %d", len(r))
310+
}
311+
if _, ok := r["terraform_data.my-data"]; !ok {
312+
t.Fatalf("expected state to include terraform_data.my-data but it's missing")
313+
}
314+
}
315+
316+
// Requires TF_TEST_EXPERIMENTS to be set in the environment
317+
func TestPrimary_stateStore_inMem(t *testing.T) {
318+
if v := os.Getenv("TF_TEST_EXPERIMENTS"); v == "" {
319+
t.Skip("can't run without enabling experiments in the executable terraform binary, enable with TF_TEST_EXPERIMENTS=1")
320+
}
321+
322+
if !canRunGoBuild {
323+
// We're running in a separate-build-then-run context, so we can't
324+
// currently execute this test which depends on being able to build
325+
// new executable at runtime.
326+
//
327+
// (See the comment on canRunGoBuild's declaration for more information.)
328+
t.Skip("can't run without building a new provider executable")
329+
}
330+
t.Parallel()
331+
252332
tf := e2e.NewBinary(t, terraformBin, "testdata/full-workflow-with-state-store-inmem")
253333

254334
// In order to test integration with PSS we need a provider plugin implementing a state store.
@@ -268,6 +348,9 @@ func TestPrimary_stateStore(t *testing.T) {
268348
}
269349

270350
//// INIT
351+
//
352+
// Note - the inmem PSS implementation means that the default workspace state created during init
353+
// is lost as soon as the command completes.
271354
stdout, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color")
272355
if err != nil {
273356
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
@@ -281,6 +364,9 @@ func TestPrimary_stateStore(t *testing.T) {
281364
// No separate plan step; this test lets the apply make a plan.
282365

283366
//// APPLY
367+
//
368+
// Note - the inmem PSS implementation means that writing to the default workspace during apply
369+
// is creating the default state file for the first time.
284370
stdout, stderr, err = tf.Run("apply", "-auto-approve", "-no-color")
285371
if err != nil {
286372
t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)

internal/provider-simple-v6/provider.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type simple struct {
2424
schema providers.GetProviderSchemaResponse
2525

2626
inMem *InMemStoreSingle
27+
fs *FsStore
2728
}
2829

2930
// Provider returns an instance of providers.Interface
@@ -109,7 +110,8 @@ func provider() simple {
109110
},
110111
Actions: map[string]providers.ActionSchema{},
111112
StateStores: map[string]providers.Schema{
112-
inMemStoreName: stateStoreInMemGetSchema(),
113+
inMemStoreName: stateStoreInMemGetSchema(), // simple6_inmem
114+
fsStoreName: stateStoreFsGetSchema(), // simple6_fs
113115
},
114116
ServerCapabilities: providers.ServerCapabilities{
115117
PlanDestroy: true,
@@ -134,7 +136,9 @@ func provider() simple {
134136
},
135137
},
136138

137-
inMem: &InMemStoreSingle{}, // default workspace doesn't exist by default here; needs explicit creation via init command
139+
// default workspaces doesn't exist by default here; needs explicit creation via init command
140+
inMem: &InMemStoreSingle{},
141+
fs: &FsStore{},
138142
}
139143

140144
return provider
@@ -353,6 +357,9 @@ func (s simple) ValidateStateStoreConfig(req providers.ValidateStateStoreConfigR
353357
if req.TypeName == inMemStoreName {
354358
return s.inMem.ValidateStateStoreConfig(req)
355359
}
360+
if req.TypeName == fsStoreName {
361+
return s.fs.ValidateStateStoreConfig(req)
362+
}
356363

357364
var resp providers.ValidateStateStoreConfigResponse
358365
resp.Diagnostics.Append(fmt.Errorf("unsupported state store type %q", req.TypeName))
@@ -363,6 +370,9 @@ func (s simple) ConfigureStateStore(req providers.ConfigureStateStoreRequest) pr
363370
if req.TypeName == inMemStoreName {
364371
return s.inMem.ConfigureStateStore(req)
365372
}
373+
if req.TypeName == fsStoreName {
374+
return s.fs.ConfigureStateStore(req)
375+
}
366376

367377
var resp providers.ConfigureStateStoreResponse
368378
resp.Diagnostics.Append(fmt.Errorf("unsupported state store type %q", req.TypeName))
@@ -373,6 +383,9 @@ func (s simple) ReadStateBytes(req providers.ReadStateBytesRequest) providers.Re
373383
if req.TypeName == inMemStoreName {
374384
return s.inMem.ReadStateBytes(req)
375385
}
386+
if req.TypeName == fsStoreName {
387+
return s.fs.ReadStateBytes(req)
388+
}
376389

377390
var resp providers.ReadStateBytesResponse
378391
resp.Diagnostics.Append(fmt.Errorf("unsupported state store type %q", req.TypeName))
@@ -383,6 +396,9 @@ func (s simple) WriteStateBytes(req providers.WriteStateBytesRequest) providers.
383396
if req.TypeName == inMemStoreName {
384397
return s.inMem.WriteStateBytes(req)
385398
}
399+
if req.TypeName == fsStoreName {
400+
return s.fs.WriteStateBytes(req)
401+
}
386402

387403
var resp providers.WriteStateBytesResponse
388404
resp.Diagnostics.Append(fmt.Errorf("unsupported state store type %q", req.TypeName))
@@ -393,6 +409,9 @@ func (s simple) LockState(req providers.LockStateRequest) providers.LockStateRes
393409
if req.TypeName == inMemStoreName {
394410
return s.inMem.LockState(req)
395411
}
412+
if req.TypeName == fsStoreName {
413+
return s.fs.LockState(req)
414+
}
396415

397416
var resp providers.LockStateResponse
398417
resp.Diagnostics.Append(fmt.Errorf("unsupported state store type %q", req.TypeName))
@@ -403,6 +422,9 @@ func (s simple) UnlockState(req providers.UnlockStateRequest) providers.UnlockSt
403422
if req.TypeName == inMemStoreName {
404423
return s.inMem.UnlockState(req)
405424
}
425+
if req.TypeName == fsStoreName {
426+
return s.fs.UnlockState(req)
427+
}
406428

407429
var resp providers.UnlockStateResponse
408430
resp.Diagnostics.Append(fmt.Errorf("unsupported state store type %q", req.TypeName))
@@ -413,6 +435,9 @@ func (s simple) GetStates(req providers.GetStatesRequest) providers.GetStatesRes
413435
if req.TypeName == inMemStoreName {
414436
return s.inMem.GetStates(req)
415437
}
438+
if req.TypeName == fsStoreName {
439+
return s.fs.GetStates(req)
440+
}
416441

417442
var resp providers.GetStatesResponse
418443
resp.Diagnostics.Append(fmt.Errorf("unsupported state store type %q", req.TypeName))
@@ -423,6 +448,9 @@ func (s simple) DeleteState(req providers.DeleteStateRequest) providers.DeleteSt
423448
if req.TypeName == inMemStoreName {
424449
return s.inMem.DeleteState(req)
425450
}
451+
if req.TypeName == fsStoreName {
452+
return s.fs.DeleteState(req)
453+
}
426454

427455
var resp providers.DeleteStateResponse
428456
resp.Diagnostics.Append(fmt.Errorf("unsupported state store type %q", req.TypeName))

0 commit comments

Comments
 (0)