1package e2etest
2
3import (
4	"context"
5	"encoding/json"
6	"io/ioutil"
7	"path/filepath"
8	"strings"
9	"sync"
10	"testing"
11
12	"github.com/hashicorp/go-hclog"
13	"github.com/hashicorp/go-plugin"
14	"github.com/hashicorp/terraform/internal/e2e"
15	"github.com/hashicorp/terraform/internal/grpcwrap"
16	tfplugin5 "github.com/hashicorp/terraform/internal/plugin"
17	tfplugin "github.com/hashicorp/terraform/internal/plugin6"
18	simple5 "github.com/hashicorp/terraform/internal/provider-simple"
19	simple "github.com/hashicorp/terraform/internal/provider-simple-v6"
20	proto5 "github.com/hashicorp/terraform/internal/tfplugin5"
21	proto "github.com/hashicorp/terraform/internal/tfplugin6"
22)
23
24// The tests in this file are for the "unmanaged provider workflow", which
25// includes variants of the following sequence, with different details:
26// terraform init
27// terraform plan
28// terraform apply
29//
30// These tests are run against an in-process server, and checked to make sure
31// they're not trying to control the lifecycle of the binary. They are not
32// checked for correctness of the operations themselves.
33
34type reattachConfig struct {
35	Protocol        string
36	ProtocolVersion int
37	Pid             int
38	Test            bool
39	Addr            reattachConfigAddr
40}
41
42type reattachConfigAddr struct {
43	Network string
44	String  string
45}
46
47type providerServer struct {
48	sync.Mutex
49	proto.ProviderServer
50	planResourceChangeCalled  bool
51	applyResourceChangeCalled bool
52}
53
54func (p *providerServer) PlanResourceChange(ctx context.Context, req *proto.PlanResourceChange_Request) (*proto.PlanResourceChange_Response, error) {
55	p.Lock()
56	defer p.Unlock()
57
58	p.planResourceChangeCalled = true
59	return p.ProviderServer.PlanResourceChange(ctx, req)
60}
61
62func (p *providerServer) ApplyResourceChange(ctx context.Context, req *proto.ApplyResourceChange_Request) (*proto.ApplyResourceChange_Response, error) {
63	p.Lock()
64	defer p.Unlock()
65
66	p.applyResourceChangeCalled = true
67	return p.ProviderServer.ApplyResourceChange(ctx, req)
68}
69
70func (p *providerServer) PlanResourceChangeCalled() bool {
71	p.Lock()
72	defer p.Unlock()
73
74	return p.planResourceChangeCalled
75}
76func (p *providerServer) ResetPlanResourceChangeCalled() {
77	p.Lock()
78	defer p.Unlock()
79
80	p.planResourceChangeCalled = false
81}
82
83func (p *providerServer) ApplyResourceChangeCalled() bool {
84	p.Lock()
85	defer p.Unlock()
86
87	return p.applyResourceChangeCalled
88}
89func (p *providerServer) ResetApplyResourceChangeCalled() {
90	p.Lock()
91	defer p.Unlock()
92
93	p.applyResourceChangeCalled = false
94}
95
96type providerServer5 struct {
97	sync.Mutex
98	proto5.ProviderServer
99	planResourceChangeCalled  bool
100	applyResourceChangeCalled bool
101}
102
103func (p *providerServer5) PlanResourceChange(ctx context.Context, req *proto5.PlanResourceChange_Request) (*proto5.PlanResourceChange_Response, error) {
104	p.Lock()
105	defer p.Unlock()
106
107	p.planResourceChangeCalled = true
108	return p.ProviderServer.PlanResourceChange(ctx, req)
109}
110
111func (p *providerServer5) ApplyResourceChange(ctx context.Context, req *proto5.ApplyResourceChange_Request) (*proto5.ApplyResourceChange_Response, error) {
112	p.Lock()
113	defer p.Unlock()
114
115	p.applyResourceChangeCalled = true
116	return p.ProviderServer.ApplyResourceChange(ctx, req)
117}
118
119func (p *providerServer5) PlanResourceChangeCalled() bool {
120	p.Lock()
121	defer p.Unlock()
122
123	return p.planResourceChangeCalled
124}
125func (p *providerServer5) ResetPlanResourceChangeCalled() {
126	p.Lock()
127	defer p.Unlock()
128
129	p.planResourceChangeCalled = false
130}
131
132func (p *providerServer5) ApplyResourceChangeCalled() bool {
133	p.Lock()
134	defer p.Unlock()
135
136	return p.applyResourceChangeCalled
137}
138func (p *providerServer5) ResetApplyResourceChangeCalled() {
139	p.Lock()
140	defer p.Unlock()
141
142	p.applyResourceChangeCalled = false
143}
144
145func TestUnmanagedSeparatePlan(t *testing.T) {
146	t.Parallel()
147
148	fixturePath := filepath.Join("testdata", "test-provider")
149	tf := e2e.NewBinary(terraformBin, fixturePath)
150	defer tf.Close()
151
152	reattachCh := make(chan *plugin.ReattachConfig)
153	closeCh := make(chan struct{})
154	provider := &providerServer{
155		ProviderServer: grpcwrap.Provider6(simple.Provider()),
156	}
157	ctx, cancel := context.WithCancel(context.Background())
158	defer cancel()
159	go plugin.Serve(&plugin.ServeConfig{
160		Logger: hclog.New(&hclog.LoggerOptions{
161			Name:   "plugintest",
162			Level:  hclog.Trace,
163			Output: ioutil.Discard,
164		}),
165		Test: &plugin.ServeTestConfig{
166			Context:          ctx,
167			ReattachConfigCh: reattachCh,
168			CloseCh:          closeCh,
169		},
170		GRPCServer: plugin.DefaultGRPCServer,
171		VersionedPlugins: map[int]plugin.PluginSet{
172			6: {
173				"provider": &tfplugin.GRPCProviderPlugin{
174					GRPCProvider: func() proto.ProviderServer {
175						return provider
176					},
177				},
178			},
179		},
180	})
181	config := <-reattachCh
182	if config == nil {
183		t.Fatalf("no reattach config received")
184	}
185	reattachStr, err := json.Marshal(map[string]reattachConfig{
186		"hashicorp/test": {
187			Protocol:        string(config.Protocol),
188			ProtocolVersion: 6,
189			Pid:             config.Pid,
190			Test:            true,
191			Addr: reattachConfigAddr{
192				Network: config.Addr.Network(),
193				String:  config.Addr.String(),
194			},
195		},
196	})
197	if err != nil {
198		t.Fatal(err)
199	}
200
201	tf.AddEnv("TF_REATTACH_PROVIDERS=" + string(reattachStr))
202
203	//// INIT
204	stdout, stderr, err := tf.Run("init")
205	if err != nil {
206		t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
207	}
208
209	// Make sure we didn't download the binary
210	if strings.Contains(stdout, "Installing hashicorp/test v") {
211		t.Errorf("test provider download message is present in init output:\n%s", stdout)
212	}
213	if tf.FileExists(filepath.Join(".terraform", "plugins", "registry.terraform.io", "hashicorp", "test")) {
214		t.Errorf("test provider binary found in .terraform dir")
215	}
216
217	//// PLAN
218	_, stderr, err = tf.Run("plan", "-out=tfplan")
219	if err != nil {
220		t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
221	}
222
223	if !provider.PlanResourceChangeCalled() {
224		t.Error("PlanResourceChange not called on un-managed provider")
225	}
226
227	//// APPLY
228	_, stderr, err = tf.Run("apply", "tfplan")
229	if err != nil {
230		t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
231	}
232
233	if !provider.ApplyResourceChangeCalled() {
234		t.Error("ApplyResourceChange not called on un-managed provider")
235	}
236	provider.ResetApplyResourceChangeCalled()
237
238	//// DESTROY
239	_, stderr, err = tf.Run("destroy", "-auto-approve")
240	if err != nil {
241		t.Fatalf("unexpected destroy error: %s\nstderr:\n%s", err, stderr)
242	}
243
244	if !provider.ApplyResourceChangeCalled() {
245		t.Error("ApplyResourceChange (destroy) not called on in-process provider")
246	}
247	cancel()
248	<-closeCh
249}
250
251func TestUnmanagedSeparatePlan_proto5(t *testing.T) {
252	t.Parallel()
253
254	fixturePath := filepath.Join("testdata", "test-provider")
255	tf := e2e.NewBinary(terraformBin, fixturePath)
256	defer tf.Close()
257
258	reattachCh := make(chan *plugin.ReattachConfig)
259	closeCh := make(chan struct{})
260	provider := &providerServer5{
261		ProviderServer: grpcwrap.Provider(simple5.Provider()),
262	}
263	ctx, cancel := context.WithCancel(context.Background())
264	defer cancel()
265	go plugin.Serve(&plugin.ServeConfig{
266		Logger: hclog.New(&hclog.LoggerOptions{
267			Name:   "plugintest",
268			Level:  hclog.Trace,
269			Output: ioutil.Discard,
270		}),
271		Test: &plugin.ServeTestConfig{
272			Context:          ctx,
273			ReattachConfigCh: reattachCh,
274			CloseCh:          closeCh,
275		},
276		GRPCServer: plugin.DefaultGRPCServer,
277		VersionedPlugins: map[int]plugin.PluginSet{
278			5: {
279				"provider": &tfplugin5.GRPCProviderPlugin{
280					GRPCProvider: func() proto5.ProviderServer {
281						return provider
282					},
283				},
284			},
285		},
286	})
287	config := <-reattachCh
288	if config == nil {
289		t.Fatalf("no reattach config received")
290	}
291	reattachStr, err := json.Marshal(map[string]reattachConfig{
292		"hashicorp/test": {
293			Protocol:        string(config.Protocol),
294			ProtocolVersion: 5,
295			Pid:             config.Pid,
296			Test:            true,
297			Addr: reattachConfigAddr{
298				Network: config.Addr.Network(),
299				String:  config.Addr.String(),
300			},
301		},
302	})
303	if err != nil {
304		t.Fatal(err)
305	}
306
307	tf.AddEnv("TF_REATTACH_PROVIDERS=" + string(reattachStr))
308
309	//// INIT
310	stdout, stderr, err := tf.Run("init")
311	if err != nil {
312		t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
313	}
314
315	// Make sure we didn't download the binary
316	if strings.Contains(stdout, "Installing hashicorp/test v") {
317		t.Errorf("test provider download message is present in init output:\n%s", stdout)
318	}
319	if tf.FileExists(filepath.Join(".terraform", "plugins", "registry.terraform.io", "hashicorp", "test")) {
320		t.Errorf("test provider binary found in .terraform dir")
321	}
322
323	//// PLAN
324	_, stderr, err = tf.Run("plan", "-out=tfplan")
325	if err != nil {
326		t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
327	}
328
329	if !provider.PlanResourceChangeCalled() {
330		t.Error("PlanResourceChange not called on un-managed provider")
331	}
332
333	//// APPLY
334	_, stderr, err = tf.Run("apply", "tfplan")
335	if err != nil {
336		t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
337	}
338
339	if !provider.ApplyResourceChangeCalled() {
340		t.Error("ApplyResourceChange not called on un-managed provider")
341	}
342	provider.ResetApplyResourceChangeCalled()
343
344	//// DESTROY
345	_, stderr, err = tf.Run("destroy", "-auto-approve")
346	if err != nil {
347		t.Fatalf("unexpected destroy error: %s\nstderr:\n%s", err, stderr)
348	}
349
350	if !provider.ApplyResourceChangeCalled() {
351		t.Error("ApplyResourceChange (destroy) not called on in-process provider")
352	}
353	cancel()
354	<-closeCh
355}
356