Skip to content

Commit 3db7c75

Browse files
authored
PSS: Fix use of reattached providers in init, enable use of reattached providers during plan-apply workflow (#38182)
* refactor: Check that the state storage provider is present when beginning to initialise a state store for use in a non-init command. Ensure reattached providers can be used. Previously we passed all required providers into backend options to be used within `stateStoreConfig`, which is invoked via (Meta).Backend. The new approach enforces that the provider is present while assembling the backend options passed to (Meta).Backend from (Meta).backend, which is non-init specific. As this code is defending against users running non-init commands before an init, this place feels appropriate and isn't able to impact the init command. * fix: Reattached PSS providers should return early when checking locks, and an empty locks file is only bad if there isn't a reattached PSS provider * test: Assert that running init with reattached PSS provider is ok, via an E2E test that uses the reattach feature. * fix: Allow builtin or reattached providers to be used for state stores when generating a plan file * test: Expand E2E test to show using a reattached provider can be used for a workflow of init, plan with -out, and apply. * chore: Replace 'io/ioutil' and format code in unmanaged e2e tests
1 parent d4d46b3 commit 3db7c75

File tree

8 files changed

+472
-147
lines changed

8 files changed

+472
-147
lines changed

internal/command/e2etest/pluggable_state_store_test.go

Lines changed: 142 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,164 @@ package e2etest
55

66
import (
77
"bytes"
8+
"context"
9+
"encoding/json"
810
"fmt"
11+
"io"
912
"os"
1013
"path"
1114
"path/filepath"
1215
"strings"
1316
"testing"
1417

1518
"github.com/google/go-cmp/cmp"
19+
"github.com/hashicorp/go-hclog"
20+
"github.com/hashicorp/go-plugin"
1621
"github.com/hashicorp/go-version"
1722
"github.com/hashicorp/terraform/internal/addrs"
1823
"github.com/hashicorp/terraform/internal/e2e"
1924
"github.com/hashicorp/terraform/internal/getproviders"
25+
"github.com/hashicorp/terraform/internal/grpcwrap"
26+
tfplugin "github.com/hashicorp/terraform/internal/plugin6"
27+
simple "github.com/hashicorp/terraform/internal/provider-simple-v6"
2028
"github.com/hashicorp/terraform/internal/states"
2129
"github.com/hashicorp/terraform/internal/states/statefile"
30+
proto "github.com/hashicorp/terraform/internal/tfplugin6"
2231
)
2332

33+
// Test that users can do the full init-plan-apply workflow with pluggable state storage
34+
// when the state storage provider is reattached/unmanaged by Terraform.
35+
// As well as ensuring that the state store can be initialised ok, this tests that
36+
// the state store's details can be stored in the plan file despite the fact it's reattached.
37+
func TestPrimary_stateStore_unmanaged_separatePlan(t *testing.T) {
38+
fixturePath := filepath.Join("testdata", "full-workflow-with-state-store-fs")
39+
40+
t.Setenv(e2e.TestExperimentFlag, "true")
41+
terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform")
42+
tf := e2e.NewBinary(t, terraformBin, fixturePath)
43+
44+
reattachCh := make(chan *plugin.ReattachConfig)
45+
closeCh := make(chan struct{})
46+
provider := &providerServer{
47+
ProviderServer: grpcwrap.Provider6(simple.Provider()),
48+
}
49+
ctx, cancel := context.WithCancel(context.Background())
50+
defer cancel()
51+
go plugin.Serve(&plugin.ServeConfig{
52+
Logger: hclog.New(&hclog.LoggerOptions{
53+
Name: "plugintest",
54+
Level: hclog.Trace,
55+
Output: io.Discard,
56+
}),
57+
Test: &plugin.ServeTestConfig{
58+
Context: ctx,
59+
ReattachConfigCh: reattachCh,
60+
CloseCh: closeCh,
61+
},
62+
GRPCServer: plugin.DefaultGRPCServer,
63+
VersionedPlugins: map[int]plugin.PluginSet{
64+
6: {
65+
"provider": &tfplugin.GRPCProviderPlugin{
66+
GRPCProvider: func() proto.ProviderServer {
67+
return provider
68+
},
69+
},
70+
},
71+
},
72+
})
73+
config := <-reattachCh
74+
if config == nil {
75+
t.Fatalf("no reattach config received")
76+
}
77+
reattachStr, err := json.Marshal(map[string]reattachConfig{
78+
"hashicorp/simple6": {
79+
Protocol: string(config.Protocol),
80+
ProtocolVersion: 6,
81+
Pid: config.Pid,
82+
Test: true,
83+
Addr: reattachConfigAddr{
84+
Network: config.Addr.Network(),
85+
String: config.Addr.String(),
86+
},
87+
},
88+
})
89+
if err != nil {
90+
t.Fatal(err)
91+
}
92+
93+
tf.AddEnv("TF_REATTACH_PROVIDERS=" + string(reattachStr))
94+
95+
// Required for the local state files to be written to the temp directory,
96+
// instead of the e2e directory in the repo.
97+
t.Chdir(tf.WorkDir())
98+
99+
//// INIT
100+
t.Setenv("TF_ENABLE_PLUGGABLE_STATE_STORAGE", "1")
101+
stdout, stderr, err := tf.Run("init")
102+
if err != nil {
103+
t.Fatalf("unexpected init error: %s\nstderr:\n%s\nstdout:\n%s", err, stderr, stdout)
104+
}
105+
if !provider.ReadStateBytesCalled() {
106+
t.Error("ReadStateBytes not called on un-managed provider")
107+
}
108+
if !provider.WriteStateBytesCalled() {
109+
t.Error("WriteStateBytes not called on un-managed provider")
110+
}
111+
provider.ResetReadStateBytesCalled()
112+
provider.ResetWriteStateBytesCalled()
113+
114+
// Make sure we didn't download the binary
115+
if strings.Contains(stdout, "Installing hashicorp/simple6 v") {
116+
t.Errorf("test provider download message is present in init output:\n%s", stdout)
117+
}
118+
if tf.FileExists(filepath.Join(".terraform", "plugins", "registry.terraform.io", "hashicorp", "simple6")) {
119+
t.Errorf("test provider binary found in .terraform dir")
120+
}
121+
122+
//// PLAN
123+
stdout, stderr, err = tf.Run("plan", "-out=tfplan")
124+
if err != nil {
125+
t.Fatalf("unexpected plan error: %s\nstderr:\n%s\nstdout:\n%s", err, stderr, stdout)
126+
}
127+
if !provider.ReadStateBytesCalled() {
128+
t.Error("ReadStateBytes not called on un-managed provider")
129+
}
130+
if provider.WriteStateBytesCalled() {
131+
t.Error("WriteStateBytes should not be called on un-managed provider during plan")
132+
}
133+
provider.ResetReadStateBytesCalled()
134+
provider.ResetWriteStateBytesCalled()
135+
136+
//// APPLY
137+
stdout, stderr, err = tf.Run("apply", "tfplan")
138+
if err != nil {
139+
t.Fatalf("unexpected apply error: %s\nstderr:\n%s\nstdout:\n%s", err, stderr, stdout)
140+
}
141+
if !provider.ReadStateBytesCalled() {
142+
t.Error("ReadStateBytes not called on un-managed provider")
143+
}
144+
if !provider.WriteStateBytesCalled() {
145+
t.Error("WriteStateBytes not called on un-managed provider")
146+
}
147+
provider.ResetReadStateBytesCalled()
148+
provider.ResetWriteStateBytesCalled()
149+
150+
// Check the apply process has made a state file as expected.
151+
stateFilePath := filepath.Join("states", "default", "terraform.tfstate")
152+
if !tf.FileExists(stateFilePath) {
153+
t.Fatalf("state file not found at expected path: %s", filepath.Join(tf.WorkDir(), stateFilePath))
154+
}
155+
156+
//// DESTROY
157+
stdout, stderr, err = tf.Run("destroy", "-auto-approve")
158+
if err != nil {
159+
t.Fatalf("unexpected destroy error: %s\nstderr:\n%s\nstdout:\n%s", err, stderr, stdout)
160+
}
161+
162+
cancel()
163+
<-closeCh
164+
}
165+
24166
// Tests using `terraform workspace` commands in combination with pluggable state storage.
25167
func TestPrimary_stateStore_workspaceCmd(t *testing.T) {
26168
if !canRunGoBuild {
@@ -131,7 +273,6 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) {
131273
// > `terraform state show`
132274
// > `terraform state list`
133275
func TestPrimary_stateStore_stateCmds(t *testing.T) {
134-
135276
if !canRunGoBuild {
136277
// We're running in a separate-build-then-run context, so we can't
137278
// currently execute this test which depends on being able to build
@@ -208,7 +349,6 @@ resource "terraform_data" "my-data" {
208349
// > `terraform output`
209350
// > `terraform output <name>`
210351
func TestPrimary_stateStore_outputCmd(t *testing.T) {
211-
212352
if !canRunGoBuild {
213353
// We're running in a separate-build-then-run context, so we can't
214354
// currently execute this test which depends on being able to build
@@ -278,7 +418,6 @@ func TestPrimary_stateStore_outputCmd(t *testing.T) {
278418
// > `terraform show <path-to-state-file>`
279419
// > `terraform show <path-to-plan-file>`
280420
func TestPrimary_stateStore_showCmd(t *testing.T) {
281-
282421
if !canRunGoBuild {
283422
// We're running in a separate-build-then-run context, so we can't
284423
// currently execute this test which depends on being able to build

internal/command/e2etest/unmanaged_test.go

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ package e2etest
66
import (
77
"context"
88
"encoding/json"
9-
"io/ioutil"
9+
"io"
1010
"path/filepath"
1111
"strings"
1212
"sync"
@@ -53,6 +53,8 @@ type providerServer struct {
5353
planResourceChangeCalled bool
5454
applyResourceChangeCalled bool
5555
listResourceCalled bool
56+
readStateBytesCalled bool
57+
writeStateBytesCalled bool
5658
}
5759

5860
func (p *providerServer) PlanResourceChange(ctx context.Context, req *proto.PlanResourceChange_Request) (*proto.PlanResourceChange_Response, error) {
@@ -71,6 +73,22 @@ func (p *providerServer) ApplyResourceChange(ctx context.Context, req *proto.App
7173
return p.ProviderServer.ApplyResourceChange(ctx, req)
7274
}
7375

76+
func (p *providerServer) WriteStateBytes(server proto.Provider_WriteStateBytesServer) error {
77+
p.Lock()
78+
defer p.Unlock()
79+
80+
p.writeStateBytesCalled = true
81+
return p.ProviderServer.WriteStateBytes(server)
82+
}
83+
84+
func (p *providerServer) ReadStateBytes(req *proto.ReadStateBytes_Request, server proto.Provider_ReadStateBytesServer) error {
85+
p.Lock()
86+
defer p.Unlock()
87+
88+
p.readStateBytesCalled = true
89+
return p.ProviderServer.ReadStateBytes(req, server)
90+
}
91+
7492
func (p *providerServer) ListResource(req *proto.ListResource_Request, res proto.Provider_ListResourceServer) error {
7593
p.Lock()
7694
defer p.Unlock()
@@ -85,6 +103,7 @@ func (p *providerServer) PlanResourceChangeCalled() bool {
85103

86104
return p.planResourceChangeCalled
87105
}
106+
88107
func (p *providerServer) ResetPlanResourceChangeCalled() {
89108
p.Lock()
90109
defer p.Unlock()
@@ -98,6 +117,7 @@ func (p *providerServer) ApplyResourceChangeCalled() bool {
98117

99118
return p.applyResourceChangeCalled
100119
}
120+
101121
func (p *providerServer) ResetApplyResourceChangeCalled() {
102122
p.Lock()
103123
defer p.Unlock()
@@ -112,6 +132,34 @@ func (p *providerServer) ListResourceCalled() bool {
112132
return p.listResourceCalled
113133
}
114134

135+
func (p *providerServer) ReadStateBytesCalled() bool {
136+
p.Lock()
137+
defer p.Unlock()
138+
139+
return p.readStateBytesCalled
140+
}
141+
142+
func (p *providerServer) ResetReadStateBytesCalled() {
143+
p.Lock()
144+
defer p.Unlock()
145+
146+
p.readStateBytesCalled = false
147+
}
148+
149+
func (p *providerServer) WriteStateBytesCalled() bool {
150+
p.Lock()
151+
defer p.Unlock()
152+
153+
return p.writeStateBytesCalled
154+
}
155+
156+
func (p *providerServer) ResetWriteStateBytesCalled() {
157+
p.Lock()
158+
defer p.Unlock()
159+
160+
p.writeStateBytesCalled = false
161+
}
162+
115163
type providerServer5 struct {
116164
sync.Mutex
117165
proto5.ProviderServer
@@ -151,6 +199,7 @@ func (p *providerServer5) PlanResourceChangeCalled() bool {
151199

152200
return p.planResourceChangeCalled
153201
}
202+
154203
func (p *providerServer5) ResetPlanResourceChangeCalled() {
155204
p.Lock()
156205
defer p.Unlock()
@@ -164,6 +213,7 @@ func (p *providerServer5) ApplyResourceChangeCalled() bool {
164213

165214
return p.applyResourceChangeCalled
166215
}
216+
167217
func (p *providerServer5) ResetApplyResourceChangeCalled() {
168218
p.Lock()
169219
defer p.Unlock()
@@ -195,7 +245,7 @@ func TestUnmanagedSeparatePlan(t *testing.T) {
195245
Logger: hclog.New(&hclog.LoggerOptions{
196246
Name: "plugintest",
197247
Level: hclog.Trace,
198-
Output: ioutil.Discard,
248+
Output: io.Discard,
199249
}),
200250
Test: &plugin.ServeTestConfig{
201251
Context: ctx,
@@ -300,7 +350,7 @@ func TestUnmanagedSeparatePlan_proto5(t *testing.T) {
300350
Logger: hclog.New(&hclog.LoggerOptions{
301351
Name: "plugintest",
302352
Level: hclog.Trace,
303-
Output: ioutil.Discard,
353+
Output: io.Discard,
304354
}),
305355
Test: &plugin.ServeTestConfig{
306356
Context: ctx,

internal/command/init_run_experiment.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,6 @@ func (c *InitCommand) initPssBackend(ctx context.Context, root *configs.Module,
429429

430430
opts = &BackendOpts{
431431
StateStoreConfig: root.StateStore,
432-
ProviderRequirements: root.ProviderRequirements,
433432
Locks: configLocks,
434433
CreateDefaultWorkspace: initArgs.CreateDefaultWorkspace,
435434
ConfigOverride: configOverride,

0 commit comments

Comments
 (0)