Skip to content

Commit f2818db

Browse files
PSS : Add fs and inmem state storage implementations to the builtin simplev6 provider, update grpcwrap package, use PSS implementation in E2E test (#37790)
* feat: Implement `inmem` state store in provider-simple-v6 * feat: Add filesystem state store `fs` in provider-simple-v6, no locking implemented * refactor: Move PSS chunking-related constants into the `pluggable` package, so they can be reused. * feat: Implement PSS-related methods in grpcwrap package * test: Add E2E test checking an init and apply (no plan) workflow is usable with both PSS implementations * fix: Ensure state stores are configured with a suggested chunk size from Core --------- Co-authored-by: Radek Simko <[email protected]>
1 parent 078ac7c commit f2818db

File tree

16 files changed

+1250
-41
lines changed

16 files changed

+1250
-41
lines changed

internal/backend/local/backend.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"context"
88
"errors"
99
"fmt"
10-
"io/ioutil"
1110
"log"
1211
"os"
1312
"path/filepath"
@@ -207,7 +206,7 @@ func (b *Local) Workspaces() ([]string, tfdiags.Diagnostics) {
207206
// the listing always start with "default"
208207
envs := []string{backend.DefaultStateName}
209208

210-
entries, err := ioutil.ReadDir(b.stateWorkspaceDir())
209+
entries, err := os.ReadDir(b.stateWorkspaceDir())
211210
// no error if there's no envs configured
212211
if os.IsNotExist(err) {
213212
return envs, nil
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package pluggable
5+
6+
const (
7+
// DefaultStateStoreChunkSize is the default chunk size proposed
8+
// to the provider.
9+
// This can be tweaked but should provide reasonable performance
10+
// trade-offs for average network conditions and state file sizes.
11+
DefaultStateStoreChunkSize int64 = 8 << 20 // 8 MB
12+
13+
// MaxStateStoreChunkSize is the highest chunk size provider may choose
14+
// which we still consider reasonable/safe.
15+
// This reflects terraform-plugin-go's max. RPC message size of 256MB
16+
// and leaves plenty of space for other variable data like diagnostics.
17+
MaxStateStoreChunkSize int64 = 128 << 20 // 128 MB
18+
)

internal/backend/pluggable/pluggable.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ func (p *Pluggable) Configure(config cty.Value) tfdiags.Diagnostics {
105105
req := providers.ConfigureStateStoreRequest{
106106
TypeName: p.typeName,
107107
Config: config,
108+
Capabilities: providers.StateStoreClientCapabilities{
109+
ChunkSize: DefaultStateStoreChunkSize,
110+
},
108111
}
109112
resp := p.provider.ConfigureStateStore(req)
110113
return resp.Diagnostics

internal/backend/remote-state/inmem/backend_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func TestBackendLocked(t *testing.T) {
6565
backend.TestBackendStateLocks(t, b1, b2)
6666
}
6767

68-
// use the this backen to test the remote.State implementation
68+
// use this backend to test the remote.State implementation
6969
func TestRemoteState(t *testing.T) {
7070
defer Reset()
7171
b := backend.TestBackendConfig(t, New(), hcl.EmptyBody())

internal/builtin/providers/terraform/provider.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
// Provider is an implementation of providers.Interface
1717
type Provider struct{}
1818

19+
var _ providers.Interface = &Provider{}
20+
1921
// NewProvider returns a new terraform provider
2022
func NewProvider() providers.Interface {
2123
return &Provider{}

internal/command/arguments/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ func ParseInit(args []string, experimentsEnabled bool) (*Init, tfdiags.Diagnosti
146146
diags = diags.Append(tfdiags.Sourceless(
147147
tfdiags.Error,
148148
"Cannot use -enable-pluggable-state-storage-experiment flag without experiments enabled",
149-
"Terraform cannot use the-enable-pluggable-state-storage-experiment flag (or TF_ENABLE_PLUGGABLE_STATE_STORAGE environment variable) unless experiments are enabled.",
149+
"Terraform cannot use the -enable-pluggable-state-storage-experiment flag (or TF_ENABLE_PLUGGABLE_STATE_STORAGE environment variable) unless experiments are enabled.",
150150
))
151151
}
152152
if !init.CreateDefaultWorkspace {

internal/command/e2etest/primary_test.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package e2etest
55

66
import (
7+
"os"
78
"path/filepath"
89
"reflect"
910
"sort"
@@ -12,7 +13,9 @@ import (
1213

1314
"github.com/davecgh/go-spew/spew"
1415
"github.com/hashicorp/terraform/internal/e2e"
16+
"github.com/hashicorp/terraform/internal/getproviders"
1517
"github.com/hashicorp/terraform/internal/plans"
18+
"github.com/hashicorp/terraform/internal/states/statefile"
1619
"github.com/zclconf/go-cty/cty"
1720
)
1821

@@ -230,3 +233,146 @@ func TestPrimaryChdirOption(t *testing.T) {
230233
t.Errorf("incorrect destroy tally; want 0 destroyed:\n%s", stdout)
231234
}
232235
}
236+
237+
func TestPrimary_stateStore(t *testing.T) {
238+
239+
if !canRunGoBuild {
240+
// We're running in a separate-build-then-run context, so we can't
241+
// currently execute this test which depends on being able to build
242+
// new executable at runtime.
243+
//
244+
// (See the comment on canRunGoBuild's declaration for more information.)
245+
t.Skip("can't run without building a new provider executable")
246+
}
247+
248+
t.Setenv(e2e.TestExperimentFlag, "true")
249+
terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform")
250+
251+
fixturePath := filepath.Join("testdata", "full-workflow-with-state-store-fs")
252+
tf := e2e.NewBinary(t, terraformBin, fixturePath)
253+
254+
// In order to test integration with PSS we need a provider plugin implementing a state store.
255+
// Here will build the simple6 (built with protocol v6) provider, which implements PSS.
256+
simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6")
257+
simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider)
258+
259+
// Move the provider binaries into a directory that we will point terraform
260+
// to using the -plugin-dir cli flag.
261+
platform := getproviders.CurrentPlatform.String()
262+
hashiDir := "cache/registry.terraform.io/hashicorp/"
263+
if err := os.MkdirAll(tf.Path(hashiDir, "simple6/0.0.1/", platform), os.ModePerm); err != nil {
264+
t.Fatal(err)
265+
}
266+
if err := os.Rename(simple6ProviderExe, tf.Path(hashiDir, "simple6/0.0.1/", platform, "terraform-provider-simple6")); err != nil {
267+
t.Fatal(err)
268+
}
269+
270+
//// INIT
271+
stdout, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color")
272+
if err != nil {
273+
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
274+
}
275+
276+
if !strings.Contains(stdout, "Terraform created an empty state file for the default workspace") {
277+
t.Errorf("notice about creating the default workspace is missing from init output:\n%s", stdout)
278+
}
279+
280+
//// PLAN
281+
// No separate plan step; this test lets the apply make a plan.
282+
283+
//// APPLY
284+
stdout, stderr, err = tf.Run("apply", "-auto-approve", "-no-color")
285+
if err != nil {
286+
t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
287+
}
288+
289+
if !strings.Contains(stdout, "Resources: 1 added, 0 changed, 0 destroyed") {
290+
t.Errorf("incorrect apply tally; want 1 added:\n%s", stdout)
291+
}
292+
293+
// Check the statefile saved by the fs state store.
294+
path := "terraform.tfstate.d/default/terraform.tfstate"
295+
f, err := tf.OpenFile(path)
296+
if err != nil {
297+
t.Fatalf("unexpected error opening state file %s: %s\nstderr:\n%s", path, err, stderr)
298+
}
299+
defer f.Close()
300+
301+
stateFile, err := statefile.Read(f)
302+
if err != nil {
303+
t.Fatalf("unexpected error reading statefile %s: %s\nstderr:\n%s", path, err, stderr)
304+
}
305+
306+
r := stateFile.State.RootModule().Resources
307+
if len(r) != 1 {
308+
t.Fatalf("expected state to include one resource, but got %d", len(r))
309+
}
310+
if _, ok := r["terraform_data.my-data"]; !ok {
311+
t.Fatalf("expected state to include terraform_data.my-data but it's missing")
312+
}
313+
}
314+
315+
func TestPrimary_stateStore_inMem(t *testing.T) {
316+
if !canRunGoBuild {
317+
// We're running in a separate-build-then-run context, so we can't
318+
// currently execute this test which depends on being able to build
319+
// new executable at runtime.
320+
//
321+
// (See the comment on canRunGoBuild's declaration for more information.)
322+
t.Skip("can't run without building a new provider executable")
323+
}
324+
325+
t.Setenv(e2e.TestExperimentFlag, "true")
326+
terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform")
327+
328+
fixturePath := filepath.Join("testdata", "full-workflow-with-state-store-inmem")
329+
tf := e2e.NewBinary(t, terraformBin, fixturePath)
330+
331+
// In order to test integration with PSS we need a provider plugin implementing a state store.
332+
// Here will build the simple6 (built with protocol v6) provider, which implements PSS.
333+
simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6")
334+
simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider)
335+
336+
// Move the provider binaries into a directory that we will point terraform
337+
// to using the -plugin-dir cli flag.
338+
platform := getproviders.CurrentPlatform.String()
339+
hashiDir := "cache/registry.terraform.io/hashicorp/"
340+
if err := os.MkdirAll(tf.Path(hashiDir, "simple6/0.0.1/", platform), os.ModePerm); err != nil {
341+
t.Fatal(err)
342+
}
343+
if err := os.Rename(simple6ProviderExe, tf.Path(hashiDir, "simple6/0.0.1/", platform, "terraform-provider-simple6")); err != nil {
344+
t.Fatal(err)
345+
}
346+
347+
//// INIT
348+
//
349+
// Note - the inmem PSS implementation means that the default workspace state created during init
350+
// is lost as soon as the command completes.
351+
stdout, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color")
352+
if err != nil {
353+
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
354+
}
355+
356+
if !strings.Contains(stdout, "Terraform created an empty state file for the default workspace") {
357+
t.Errorf("notice about creating the default workspace is missing from init output:\n%s", stdout)
358+
}
359+
360+
//// PLAN
361+
// No separate plan step; this test lets the apply make a plan.
362+
363+
//// APPLY
364+
//
365+
// Note - the inmem PSS implementation means that writing to the default workspace during apply
366+
// is creating the default state file for the first time.
367+
stdout, stderr, err = tf.Run("apply", "-auto-approve", "-no-color")
368+
if err != nil {
369+
t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
370+
}
371+
372+
if !strings.Contains(stdout, "Resources: 1 added, 0 changed, 0 destroyed") {
373+
t.Errorf("incorrect apply tally; want 1 added:\n%s", stdout)
374+
}
375+
376+
// We cannot inspect state or perform a destroy here, as the state isn't persisted between steps
377+
// when we use the simple6_inmem state store.
378+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
terraform {
2+
required_providers {
3+
simple6 = {
4+
source = "registry.terraform.io/hashicorp/simple6"
5+
}
6+
}
7+
8+
state_store "simple6_fs" {
9+
provider "simple6" {}
10+
}
11+
}
12+
13+
variable "name" {
14+
default = "world"
15+
}
16+
17+
resource "terraform_data" "my-data" {
18+
input = "hello ${var.name}"
19+
}
20+
21+
output "greeting" {
22+
value = resource.terraform_data.my-data.output
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
terraform {
2+
required_providers {
3+
simple6 = {
4+
source = "registry.terraform.io/hashicorp/simple6"
5+
}
6+
}
7+
8+
state_store "simple6_inmem" {
9+
provider "simple6" {}
10+
}
11+
}
12+
13+
variable "name" {
14+
default = "world"
15+
}
16+
17+
resource "terraform_data" "my-data" {
18+
input = "hello ${var.name}"
19+
}
20+
21+
output "greeting" {
22+
value = resource.terraform_data.my-data.output
23+
}

internal/command/meta_backend.go

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,6 @@ import (
5050
tfversion "github.com/hashicorp/terraform/version"
5151
)
5252

53-
const (
54-
// defaultStateStoreChunkSize is the default chunk size proposed
55-
// to the provider.
56-
// This can be tweaked but should provide reasonable performance
57-
// trade-offs for average network conditions and state file sizes.
58-
defaultStateStoreChunkSize int64 = 8 << 20 // 8 MB
59-
60-
// maxStateStoreChunkSize is the highest chunk size provider may choose
61-
// which we still consider reasonable/safe.
62-
// This reflects terraform-plugin-go's max. RPC message size of 256MB
63-
// and leaves plenty of space for other variable data like diagnostics.
64-
maxStateStoreChunkSize int64 = 128 << 20 // 128 MB
65-
)
66-
6753
// BackendOpts are the options used to initialize a backendrun.OperationsBackend.
6854
type BackendOpts struct {
6955
// BackendConfig is a representation of the backend configuration block given in
@@ -2085,7 +2071,7 @@ func (m *Meta) savedStateStore(sMgr *clistate.LocalState, factory providers.Fact
20852071
TypeName: s.StateStore.Type,
20862072
Config: stateStoreConfigVal,
20872073
Capabilities: providers.StateStoreClientCapabilities{
2088-
ChunkSize: defaultStateStoreChunkSize,
2074+
ChunkSize: backendPluggable.DefaultStateStoreChunkSize,
20892075
},
20902076
})
20912077
diags = diags.Append(cfgStoreResp.Diagnostics)
@@ -2094,10 +2080,10 @@ func (m *Meta) savedStateStore(sMgr *clistate.LocalState, factory providers.Fact
20942080
}
20952081

20962082
chunkSize := cfgStoreResp.Capabilities.ChunkSize
2097-
if chunkSize == 0 || chunkSize > maxStateStoreChunkSize {
2083+
if chunkSize == 0 || chunkSize > backendPluggable.MaxStateStoreChunkSize {
20982084
diags = diags.Append(fmt.Errorf("Failed to negotiate acceptable chunk size. "+
20992085
"Expected size > 0 and <= %d bytes, provider wants %d bytes",
2100-
maxStateStoreChunkSize, chunkSize,
2086+
backendPluggable.MaxStateStoreChunkSize, chunkSize,
21012087
))
21022088
return nil, diags
21032089
}
@@ -2362,7 +2348,7 @@ func (m *Meta) stateStoreInitFromConfig(c *configs.StateStore, factory providers
23622348
TypeName: c.Type,
23632349
Config: stateStoreConfigVal,
23642350
Capabilities: providers.StateStoreClientCapabilities{
2365-
ChunkSize: defaultStateStoreChunkSize,
2351+
ChunkSize: backendPluggable.DefaultStateStoreChunkSize,
23662352
},
23672353
})
23682354
diags = diags.Append(cfgStoreResp.Diagnostics)
@@ -2371,10 +2357,10 @@ func (m *Meta) stateStoreInitFromConfig(c *configs.StateStore, factory providers
23712357
}
23722358

23732359
chunkSize := cfgStoreResp.Capabilities.ChunkSize
2374-
if chunkSize == 0 || chunkSize > maxStateStoreChunkSize {
2360+
if chunkSize == 0 || chunkSize > backendPluggable.MaxStateStoreChunkSize {
23752361
diags = diags.Append(fmt.Errorf("Failed to negotiate acceptable chunk size. "+
23762362
"Expected size > 0 and <= %d bytes, provider wants %d bytes",
2377-
maxStateStoreChunkSize, chunkSize,
2363+
backendPluggable.MaxStateStoreChunkSize, chunkSize,
23782364
))
23792365
return nil, cty.NilVal, cty.NilVal, diags
23802366
}

0 commit comments

Comments
 (0)