1package remote
2
3import (
4	"bytes"
5	"context"
6	"encoding/base64"
7	"errors"
8	"fmt"
9	"io"
10	"io/ioutil"
11	"math/rand"
12	"os"
13	"path/filepath"
14	"strings"
15	"sync"
16	"time"
17
18	tfe "github.com/hashicorp/go-tfe"
19	"github.com/hashicorp/terraform/internal/terraform"
20	tfversion "github.com/hashicorp/terraform/version"
21	"github.com/mitchellh/copystructure"
22)
23
24type mockClient struct {
25	Applies               *mockApplies
26	ConfigurationVersions *mockConfigurationVersions
27	CostEstimates         *mockCostEstimates
28	Organizations         *mockOrganizations
29	Plans                 *mockPlans
30	PolicyChecks          *mockPolicyChecks
31	Runs                  *mockRuns
32	StateVersions         *mockStateVersions
33	Variables             *mockVariables
34	Workspaces            *mockWorkspaces
35}
36
37func newMockClient() *mockClient {
38	c := &mockClient{}
39	c.Applies = newMockApplies(c)
40	c.ConfigurationVersions = newMockConfigurationVersions(c)
41	c.CostEstimates = newMockCostEstimates(c)
42	c.Organizations = newMockOrganizations(c)
43	c.Plans = newMockPlans(c)
44	c.PolicyChecks = newMockPolicyChecks(c)
45	c.Runs = newMockRuns(c)
46	c.StateVersions = newMockStateVersions(c)
47	c.Variables = newMockVariables(c)
48	c.Workspaces = newMockWorkspaces(c)
49	return c
50}
51
52type mockApplies struct {
53	client  *mockClient
54	applies map[string]*tfe.Apply
55	logs    map[string]string
56}
57
58func newMockApplies(client *mockClient) *mockApplies {
59	return &mockApplies{
60		client:  client,
61		applies: make(map[string]*tfe.Apply),
62		logs:    make(map[string]string),
63	}
64}
65
66// create is a helper function to create a mock apply that uses the configured
67// working directory to find the logfile.
68func (m *mockApplies) create(cvID, workspaceID string) (*tfe.Apply, error) {
69	c, ok := m.client.ConfigurationVersions.configVersions[cvID]
70	if !ok {
71		return nil, tfe.ErrResourceNotFound
72	}
73	if c.Speculative {
74		// Speculative means its plan-only so we don't create a Apply.
75		return nil, nil
76	}
77
78	id := generateID("apply-")
79	url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id)
80
81	a := &tfe.Apply{
82		ID:         id,
83		LogReadURL: url,
84		Status:     tfe.ApplyPending,
85	}
86
87	w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
88	if !ok {
89		return nil, tfe.ErrResourceNotFound
90	}
91
92	if w.AutoApply {
93		a.Status = tfe.ApplyRunning
94	}
95
96	m.logs[url] = filepath.Join(
97		m.client.ConfigurationVersions.uploadPaths[cvID],
98		w.WorkingDirectory,
99		"apply.log",
100	)
101	m.applies[a.ID] = a
102
103	return a, nil
104}
105
106func (m *mockApplies) Read(ctx context.Context, applyID string) (*tfe.Apply, error) {
107	a, ok := m.applies[applyID]
108	if !ok {
109		return nil, tfe.ErrResourceNotFound
110	}
111	// Together with the mockLogReader this allows testing queued runs.
112	if a.Status == tfe.ApplyRunning {
113		a.Status = tfe.ApplyFinished
114	}
115	return a, nil
116}
117
118func (m *mockApplies) Logs(ctx context.Context, applyID string) (io.Reader, error) {
119	a, err := m.Read(ctx, applyID)
120	if err != nil {
121		return nil, err
122	}
123
124	logfile, ok := m.logs[a.LogReadURL]
125	if !ok {
126		return nil, tfe.ErrResourceNotFound
127	}
128
129	if _, err := os.Stat(logfile); os.IsNotExist(err) {
130		return bytes.NewBufferString("logfile does not exist"), nil
131	}
132
133	logs, err := ioutil.ReadFile(logfile)
134	if err != nil {
135		return nil, err
136	}
137
138	done := func() (bool, error) {
139		a, err := m.Read(ctx, applyID)
140		if err != nil {
141			return false, err
142		}
143		if a.Status != tfe.ApplyFinished {
144			return false, nil
145		}
146		return true, nil
147	}
148
149	return &mockLogReader{
150		done: done,
151		logs: bytes.NewBuffer(logs),
152	}, nil
153}
154
155type mockConfigurationVersions struct {
156	client         *mockClient
157	configVersions map[string]*tfe.ConfigurationVersion
158	uploadPaths    map[string]string
159	uploadURLs     map[string]*tfe.ConfigurationVersion
160}
161
162func newMockConfigurationVersions(client *mockClient) *mockConfigurationVersions {
163	return &mockConfigurationVersions{
164		client:         client,
165		configVersions: make(map[string]*tfe.ConfigurationVersion),
166		uploadPaths:    make(map[string]string),
167		uploadURLs:     make(map[string]*tfe.ConfigurationVersion),
168	}
169}
170
171func (m *mockConfigurationVersions) List(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionListOptions) (*tfe.ConfigurationVersionList, error) {
172	cvl := &tfe.ConfigurationVersionList{}
173	for _, cv := range m.configVersions {
174		cvl.Items = append(cvl.Items, cv)
175	}
176
177	cvl.Pagination = &tfe.Pagination{
178		CurrentPage:  1,
179		NextPage:     1,
180		PreviousPage: 1,
181		TotalPages:   1,
182		TotalCount:   len(cvl.Items),
183	}
184
185	return cvl, nil
186}
187
188func (m *mockConfigurationVersions) Create(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionCreateOptions) (*tfe.ConfigurationVersion, error) {
189	id := generateID("cv-")
190	url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id)
191
192	cv := &tfe.ConfigurationVersion{
193		ID:        id,
194		Status:    tfe.ConfigurationPending,
195		UploadURL: url,
196	}
197
198	m.configVersions[cv.ID] = cv
199	m.uploadURLs[url] = cv
200
201	return cv, nil
202}
203
204func (m *mockConfigurationVersions) Read(ctx context.Context, cvID string) (*tfe.ConfigurationVersion, error) {
205	cv, ok := m.configVersions[cvID]
206	if !ok {
207		return nil, tfe.ErrResourceNotFound
208	}
209	return cv, nil
210}
211
212func (m *mockConfigurationVersions) Upload(ctx context.Context, url, path string) error {
213	cv, ok := m.uploadURLs[url]
214	if !ok {
215		return errors.New("404 not found")
216	}
217	m.uploadPaths[cv.ID] = path
218	cv.Status = tfe.ConfigurationUploaded
219	return nil
220}
221
222type mockCostEstimates struct {
223	client      *mockClient
224	estimations map[string]*tfe.CostEstimate
225	logs        map[string]string
226}
227
228func newMockCostEstimates(client *mockClient) *mockCostEstimates {
229	return &mockCostEstimates{
230		client:      client,
231		estimations: make(map[string]*tfe.CostEstimate),
232		logs:        make(map[string]string),
233	}
234}
235
236// create is a helper function to create a mock cost estimation that uses the
237// configured working directory to find the logfile.
238func (m *mockCostEstimates) create(cvID, workspaceID string) (*tfe.CostEstimate, error) {
239	id := generateID("ce-")
240
241	ce := &tfe.CostEstimate{
242		ID:                    id,
243		MatchedResourcesCount: 1,
244		ResourcesCount:        1,
245		DeltaMonthlyCost:      "0.00",
246		ProposedMonthlyCost:   "0.00",
247		Status:                tfe.CostEstimateFinished,
248	}
249
250	w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
251	if !ok {
252		return nil, tfe.ErrResourceNotFound
253	}
254
255	logfile := filepath.Join(
256		m.client.ConfigurationVersions.uploadPaths[cvID],
257		w.WorkingDirectory,
258		"cost-estimate.log",
259	)
260
261	if _, err := os.Stat(logfile); os.IsNotExist(err) {
262		return nil, nil
263	}
264
265	m.logs[ce.ID] = logfile
266	m.estimations[ce.ID] = ce
267
268	return ce, nil
269}
270
271func (m *mockCostEstimates) Read(ctx context.Context, costEstimateID string) (*tfe.CostEstimate, error) {
272	ce, ok := m.estimations[costEstimateID]
273	if !ok {
274		return nil, tfe.ErrResourceNotFound
275	}
276	return ce, nil
277}
278
279func (m *mockCostEstimates) Logs(ctx context.Context, costEstimateID string) (io.Reader, error) {
280	ce, ok := m.estimations[costEstimateID]
281	if !ok {
282		return nil, tfe.ErrResourceNotFound
283	}
284
285	logfile, ok := m.logs[ce.ID]
286	if !ok {
287		return nil, tfe.ErrResourceNotFound
288	}
289
290	if _, err := os.Stat(logfile); os.IsNotExist(err) {
291		return bytes.NewBufferString("logfile does not exist"), nil
292	}
293
294	logs, err := ioutil.ReadFile(logfile)
295	if err != nil {
296		return nil, err
297	}
298
299	ce.Status = tfe.CostEstimateFinished
300
301	return bytes.NewBuffer(logs), nil
302}
303
304// mockInput is a mock implementation of terraform.UIInput.
305type mockInput struct {
306	answers map[string]string
307}
308
309func (m *mockInput) Input(ctx context.Context, opts *terraform.InputOpts) (string, error) {
310	v, ok := m.answers[opts.Id]
311	if !ok {
312		return "", fmt.Errorf("unexpected input request in test: %s", opts.Id)
313	}
314	if v == "wait-for-external-update" {
315		select {
316		case <-ctx.Done():
317		case <-time.After(time.Minute):
318		}
319	}
320	delete(m.answers, opts.Id)
321	return v, nil
322}
323
324type mockOrganizations struct {
325	client        *mockClient
326	organizations map[string]*tfe.Organization
327}
328
329func newMockOrganizations(client *mockClient) *mockOrganizations {
330	return &mockOrganizations{
331		client:        client,
332		organizations: make(map[string]*tfe.Organization),
333	}
334}
335
336func (m *mockOrganizations) List(ctx context.Context, options tfe.OrganizationListOptions) (*tfe.OrganizationList, error) {
337	orgl := &tfe.OrganizationList{}
338	for _, org := range m.organizations {
339		orgl.Items = append(orgl.Items, org)
340	}
341
342	orgl.Pagination = &tfe.Pagination{
343		CurrentPage:  1,
344		NextPage:     1,
345		PreviousPage: 1,
346		TotalPages:   1,
347		TotalCount:   len(orgl.Items),
348	}
349
350	return orgl, nil
351}
352
353// mockLogReader is a mock logreader that enables testing queued runs.
354type mockLogReader struct {
355	done func() (bool, error)
356	logs *bytes.Buffer
357}
358
359func (m *mockLogReader) Read(l []byte) (int, error) {
360	for {
361		if written, err := m.read(l); err != io.ErrNoProgress {
362			return written, err
363		}
364		time.Sleep(1 * time.Millisecond)
365	}
366}
367
368func (m *mockLogReader) read(l []byte) (int, error) {
369	done, err := m.done()
370	if err != nil {
371		return 0, err
372	}
373	if !done {
374		return 0, io.ErrNoProgress
375	}
376	return m.logs.Read(l)
377}
378
379func (m *mockOrganizations) Create(ctx context.Context, options tfe.OrganizationCreateOptions) (*tfe.Organization, error) {
380	org := &tfe.Organization{Name: *options.Name}
381	m.organizations[org.Name] = org
382	return org, nil
383}
384
385func (m *mockOrganizations) Read(ctx context.Context, name string) (*tfe.Organization, error) {
386	org, ok := m.organizations[name]
387	if !ok {
388		return nil, tfe.ErrResourceNotFound
389	}
390	return org, nil
391}
392
393func (m *mockOrganizations) Update(ctx context.Context, name string, options tfe.OrganizationUpdateOptions) (*tfe.Organization, error) {
394	org, ok := m.organizations[name]
395	if !ok {
396		return nil, tfe.ErrResourceNotFound
397	}
398	org.Name = *options.Name
399	return org, nil
400
401}
402
403func (m *mockOrganizations) Delete(ctx context.Context, name string) error {
404	delete(m.organizations, name)
405	return nil
406}
407
408func (m *mockOrganizations) Capacity(ctx context.Context, name string) (*tfe.Capacity, error) {
409	var pending, running int
410	for _, r := range m.client.Runs.runs {
411		if r.Status == tfe.RunPending {
412			pending++
413			continue
414		}
415		running++
416	}
417	return &tfe.Capacity{Pending: pending, Running: running}, nil
418}
419
420func (m *mockOrganizations) Entitlements(ctx context.Context, name string) (*tfe.Entitlements, error) {
421	return &tfe.Entitlements{
422		Operations:            true,
423		PrivateModuleRegistry: true,
424		Sentinel:              true,
425		StateStorage:          true,
426		Teams:                 true,
427		VCSIntegrations:       true,
428	}, nil
429}
430
431func (m *mockOrganizations) RunQueue(ctx context.Context, name string, options tfe.RunQueueOptions) (*tfe.RunQueue, error) {
432	rq := &tfe.RunQueue{}
433
434	for _, r := range m.client.Runs.runs {
435		rq.Items = append(rq.Items, r)
436	}
437
438	rq.Pagination = &tfe.Pagination{
439		CurrentPage:  1,
440		NextPage:     1,
441		PreviousPage: 1,
442		TotalPages:   1,
443		TotalCount:   len(rq.Items),
444	}
445
446	return rq, nil
447}
448
449type mockPlans struct {
450	client      *mockClient
451	logs        map[string]string
452	planOutputs map[string]string
453	plans       map[string]*tfe.Plan
454}
455
456func newMockPlans(client *mockClient) *mockPlans {
457	return &mockPlans{
458		client:      client,
459		logs:        make(map[string]string),
460		planOutputs: make(map[string]string),
461		plans:       make(map[string]*tfe.Plan),
462	}
463}
464
465// create is a helper function to create a mock plan that uses the configured
466// working directory to find the logfile.
467func (m *mockPlans) create(cvID, workspaceID string) (*tfe.Plan, error) {
468	id := generateID("plan-")
469	url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id)
470
471	p := &tfe.Plan{
472		ID:         id,
473		LogReadURL: url,
474		Status:     tfe.PlanPending,
475	}
476
477	w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
478	if !ok {
479		return nil, tfe.ErrResourceNotFound
480	}
481
482	m.logs[url] = filepath.Join(
483		m.client.ConfigurationVersions.uploadPaths[cvID],
484		w.WorkingDirectory,
485		"plan.log",
486	)
487	m.plans[p.ID] = p
488
489	return p, nil
490}
491
492func (m *mockPlans) Read(ctx context.Context, planID string) (*tfe.Plan, error) {
493	p, ok := m.plans[planID]
494	if !ok {
495		return nil, tfe.ErrResourceNotFound
496	}
497	// Together with the mockLogReader this allows testing queued runs.
498	if p.Status == tfe.PlanRunning {
499		p.Status = tfe.PlanFinished
500	}
501	return p, nil
502}
503
504func (m *mockPlans) Logs(ctx context.Context, planID string) (io.Reader, error) {
505	p, err := m.Read(ctx, planID)
506	if err != nil {
507		return nil, err
508	}
509
510	logfile, ok := m.logs[p.LogReadURL]
511	if !ok {
512		return nil, tfe.ErrResourceNotFound
513	}
514
515	if _, err := os.Stat(logfile); os.IsNotExist(err) {
516		return bytes.NewBufferString("logfile does not exist"), nil
517	}
518
519	logs, err := ioutil.ReadFile(logfile)
520	if err != nil {
521		return nil, err
522	}
523
524	done := func() (bool, error) {
525		p, err := m.Read(ctx, planID)
526		if err != nil {
527			return false, err
528		}
529		if p.Status != tfe.PlanFinished {
530			return false, nil
531		}
532		return true, nil
533	}
534
535	return &mockLogReader{
536		done: done,
537		logs: bytes.NewBuffer(logs),
538	}, nil
539}
540
541func (m *mockPlans) JSONOutput(ctx context.Context, planID string) ([]byte, error) {
542	planOutput, ok := m.planOutputs[planID]
543	if !ok {
544		return nil, tfe.ErrResourceNotFound
545	}
546
547	return []byte(planOutput), nil
548}
549
550type mockPolicyChecks struct {
551	client *mockClient
552	checks map[string]*tfe.PolicyCheck
553	logs   map[string]string
554}
555
556func newMockPolicyChecks(client *mockClient) *mockPolicyChecks {
557	return &mockPolicyChecks{
558		client: client,
559		checks: make(map[string]*tfe.PolicyCheck),
560		logs:   make(map[string]string),
561	}
562}
563
564// create is a helper function to create a mock policy check that uses the
565// configured working directory to find the logfile.
566func (m *mockPolicyChecks) create(cvID, workspaceID string) (*tfe.PolicyCheck, error) {
567	id := generateID("pc-")
568
569	pc := &tfe.PolicyCheck{
570		ID:          id,
571		Actions:     &tfe.PolicyActions{},
572		Permissions: &tfe.PolicyPermissions{},
573		Scope:       tfe.PolicyScopeOrganization,
574		Status:      tfe.PolicyPending,
575	}
576
577	w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
578	if !ok {
579		return nil, tfe.ErrResourceNotFound
580	}
581
582	logfile := filepath.Join(
583		m.client.ConfigurationVersions.uploadPaths[cvID],
584		w.WorkingDirectory,
585		"policy.log",
586	)
587
588	if _, err := os.Stat(logfile); os.IsNotExist(err) {
589		return nil, nil
590	}
591
592	m.logs[pc.ID] = logfile
593	m.checks[pc.ID] = pc
594
595	return pc, nil
596}
597
598func (m *mockPolicyChecks) List(ctx context.Context, runID string, options tfe.PolicyCheckListOptions) (*tfe.PolicyCheckList, error) {
599	_, ok := m.client.Runs.runs[runID]
600	if !ok {
601		return nil, tfe.ErrResourceNotFound
602	}
603
604	pcl := &tfe.PolicyCheckList{}
605	for _, pc := range m.checks {
606		pcl.Items = append(pcl.Items, pc)
607	}
608
609	pcl.Pagination = &tfe.Pagination{
610		CurrentPage:  1,
611		NextPage:     1,
612		PreviousPage: 1,
613		TotalPages:   1,
614		TotalCount:   len(pcl.Items),
615	}
616
617	return pcl, nil
618}
619
620func (m *mockPolicyChecks) Read(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) {
621	pc, ok := m.checks[policyCheckID]
622	if !ok {
623		return nil, tfe.ErrResourceNotFound
624	}
625
626	logfile, ok := m.logs[pc.ID]
627	if !ok {
628		return nil, tfe.ErrResourceNotFound
629	}
630
631	if _, err := os.Stat(logfile); os.IsNotExist(err) {
632		return nil, fmt.Errorf("logfile does not exist")
633	}
634
635	logs, err := ioutil.ReadFile(logfile)
636	if err != nil {
637		return nil, err
638	}
639
640	switch {
641	case bytes.Contains(logs, []byte("Sentinel Result: true")):
642		pc.Status = tfe.PolicyPasses
643	case bytes.Contains(logs, []byte("Sentinel Result: false")):
644		switch {
645		case bytes.Contains(logs, []byte("hard-mandatory")):
646			pc.Status = tfe.PolicyHardFailed
647		case bytes.Contains(logs, []byte("soft-mandatory")):
648			pc.Actions.IsOverridable = true
649			pc.Permissions.CanOverride = true
650			pc.Status = tfe.PolicySoftFailed
651		}
652	default:
653		// As this is an unexpected state, we say the policy errored.
654		pc.Status = tfe.PolicyErrored
655	}
656
657	return pc, nil
658}
659
660func (m *mockPolicyChecks) Override(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) {
661	pc, ok := m.checks[policyCheckID]
662	if !ok {
663		return nil, tfe.ErrResourceNotFound
664	}
665	pc.Status = tfe.PolicyOverridden
666	return pc, nil
667}
668
669func (m *mockPolicyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reader, error) {
670	pc, ok := m.checks[policyCheckID]
671	if !ok {
672		return nil, tfe.ErrResourceNotFound
673	}
674
675	logfile, ok := m.logs[pc.ID]
676	if !ok {
677		return nil, tfe.ErrResourceNotFound
678	}
679
680	if _, err := os.Stat(logfile); os.IsNotExist(err) {
681		return bytes.NewBufferString("logfile does not exist"), nil
682	}
683
684	logs, err := ioutil.ReadFile(logfile)
685	if err != nil {
686		return nil, err
687	}
688
689	switch {
690	case bytes.Contains(logs, []byte("Sentinel Result: true")):
691		pc.Status = tfe.PolicyPasses
692	case bytes.Contains(logs, []byte("Sentinel Result: false")):
693		switch {
694		case bytes.Contains(logs, []byte("hard-mandatory")):
695			pc.Status = tfe.PolicyHardFailed
696		case bytes.Contains(logs, []byte("soft-mandatory")):
697			pc.Actions.IsOverridable = true
698			pc.Permissions.CanOverride = true
699			pc.Status = tfe.PolicySoftFailed
700		}
701	default:
702		// As this is an unexpected state, we say the policy errored.
703		pc.Status = tfe.PolicyErrored
704	}
705
706	return bytes.NewBuffer(logs), nil
707}
708
709type mockRuns struct {
710	sync.Mutex
711
712	client     *mockClient
713	runs       map[string]*tfe.Run
714	workspaces map[string][]*tfe.Run
715
716	// If modifyNewRun is non-nil, the create method will call it just before
717	// saving a new run in the runs map, so that a calling test can mimic
718	// side-effects that a real server might apply in certain situations.
719	modifyNewRun func(client *mockClient, options tfe.RunCreateOptions, run *tfe.Run)
720}
721
722func newMockRuns(client *mockClient) *mockRuns {
723	return &mockRuns{
724		client:     client,
725		runs:       make(map[string]*tfe.Run),
726		workspaces: make(map[string][]*tfe.Run),
727	}
728}
729
730func (m *mockRuns) List(ctx context.Context, workspaceID string, options tfe.RunListOptions) (*tfe.RunList, error) {
731	m.Lock()
732	defer m.Unlock()
733
734	w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
735	if !ok {
736		return nil, tfe.ErrResourceNotFound
737	}
738
739	rl := &tfe.RunList{}
740	for _, run := range m.workspaces[w.ID] {
741		rc, err := copystructure.Copy(run)
742		if err != nil {
743			panic(err)
744		}
745		rl.Items = append(rl.Items, rc.(*tfe.Run))
746	}
747
748	rl.Pagination = &tfe.Pagination{
749		CurrentPage:  1,
750		NextPage:     1,
751		PreviousPage: 1,
752		TotalPages:   1,
753		TotalCount:   len(rl.Items),
754	}
755
756	return rl, nil
757}
758
759func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*tfe.Run, error) {
760	m.Lock()
761	defer m.Unlock()
762
763	a, err := m.client.Applies.create(options.ConfigurationVersion.ID, options.Workspace.ID)
764	if err != nil {
765		return nil, err
766	}
767
768	ce, err := m.client.CostEstimates.create(options.ConfigurationVersion.ID, options.Workspace.ID)
769	if err != nil {
770		return nil, err
771	}
772
773	p, err := m.client.Plans.create(options.ConfigurationVersion.ID, options.Workspace.ID)
774	if err != nil {
775		return nil, err
776	}
777
778	pc, err := m.client.PolicyChecks.create(options.ConfigurationVersion.ID, options.Workspace.ID)
779	if err != nil {
780		return nil, err
781	}
782
783	r := &tfe.Run{
784		ID:           generateID("run-"),
785		Actions:      &tfe.RunActions{IsCancelable: true},
786		Apply:        a,
787		CostEstimate: ce,
788		HasChanges:   false,
789		Permissions:  &tfe.RunPermissions{},
790		Plan:         p,
791		ReplaceAddrs: options.ReplaceAddrs,
792		Status:       tfe.RunPending,
793		TargetAddrs:  options.TargetAddrs,
794	}
795
796	if options.Message != nil {
797		r.Message = *options.Message
798	}
799
800	if pc != nil {
801		r.PolicyChecks = []*tfe.PolicyCheck{pc}
802	}
803
804	if options.IsDestroy != nil {
805		r.IsDestroy = *options.IsDestroy
806	}
807
808	if options.Refresh != nil {
809		r.Refresh = *options.Refresh
810	}
811
812	if options.RefreshOnly != nil {
813		r.RefreshOnly = *options.RefreshOnly
814	}
815
816	w, ok := m.client.Workspaces.workspaceIDs[options.Workspace.ID]
817	if !ok {
818		return nil, tfe.ErrResourceNotFound
819	}
820	if w.CurrentRun == nil {
821		w.CurrentRun = r
822	}
823
824	if m.modifyNewRun != nil {
825		// caller-provided callback may modify the run in-place to mimic
826		// side-effects that a real server might take in some situations.
827		m.modifyNewRun(m.client, options, r)
828	}
829
830	m.runs[r.ID] = r
831	m.workspaces[options.Workspace.ID] = append(m.workspaces[options.Workspace.ID], r)
832
833	return r, nil
834}
835
836func (m *mockRuns) Read(ctx context.Context, runID string) (*tfe.Run, error) {
837	return m.ReadWithOptions(ctx, runID, nil)
838}
839
840func (m *mockRuns) ReadWithOptions(ctx context.Context, runID string, _ *tfe.RunReadOptions) (*tfe.Run, error) {
841	m.Lock()
842	defer m.Unlock()
843
844	r, ok := m.runs[runID]
845	if !ok {
846		return nil, tfe.ErrResourceNotFound
847	}
848
849	pending := false
850	for _, r := range m.runs {
851		if r.ID != runID && r.Status == tfe.RunPending {
852			pending = true
853			break
854		}
855	}
856
857	if !pending && r.Status == tfe.RunPending {
858		// Only update the status if there are no other pending runs.
859		r.Status = tfe.RunPlanning
860		r.Plan.Status = tfe.PlanRunning
861	}
862
863	logs, _ := ioutil.ReadFile(m.client.Plans.logs[r.Plan.LogReadURL])
864	if r.Status == tfe.RunPlanning && r.Plan.Status == tfe.PlanFinished {
865		if r.IsDestroy || bytes.Contains(logs, []byte("1 to add, 0 to change, 0 to destroy")) {
866			r.Actions.IsCancelable = false
867			r.Actions.IsConfirmable = true
868			r.HasChanges = true
869			r.Permissions.CanApply = true
870		}
871
872		if bytes.Contains(logs, []byte("null_resource.foo: 1 error")) {
873			r.Actions.IsCancelable = false
874			r.HasChanges = false
875			r.Status = tfe.RunErrored
876		}
877	}
878
879	// we must return a copy for the client
880	rc, err := copystructure.Copy(r)
881	if err != nil {
882		panic(err)
883	}
884
885	return rc.(*tfe.Run), nil
886}
887
888func (m *mockRuns) Apply(ctx context.Context, runID string, options tfe.RunApplyOptions) error {
889	m.Lock()
890	defer m.Unlock()
891
892	r, ok := m.runs[runID]
893	if !ok {
894		return tfe.ErrResourceNotFound
895	}
896	if r.Status != tfe.RunPending {
897		// Only update the status if the run is not pending anymore.
898		r.Status = tfe.RunApplying
899		r.Actions.IsConfirmable = false
900		r.Apply.Status = tfe.ApplyRunning
901	}
902	return nil
903}
904
905func (m *mockRuns) Cancel(ctx context.Context, runID string, options tfe.RunCancelOptions) error {
906	panic("not implemented")
907}
908
909func (m *mockRuns) ForceCancel(ctx context.Context, runID string, options tfe.RunForceCancelOptions) error {
910	panic("not implemented")
911}
912
913func (m *mockRuns) Discard(ctx context.Context, runID string, options tfe.RunDiscardOptions) error {
914	m.Lock()
915	defer m.Unlock()
916
917	r, ok := m.runs[runID]
918	if !ok {
919		return tfe.ErrResourceNotFound
920	}
921	r.Status = tfe.RunDiscarded
922	r.Actions.IsConfirmable = false
923	return nil
924}
925
926type mockStateVersions struct {
927	client        *mockClient
928	states        map[string][]byte
929	stateVersions map[string]*tfe.StateVersion
930	workspaces    map[string][]string
931}
932
933func newMockStateVersions(client *mockClient) *mockStateVersions {
934	return &mockStateVersions{
935		client:        client,
936		states:        make(map[string][]byte),
937		stateVersions: make(map[string]*tfe.StateVersion),
938		workspaces:    make(map[string][]string),
939	}
940}
941
942func (m *mockStateVersions) List(ctx context.Context, options tfe.StateVersionListOptions) (*tfe.StateVersionList, error) {
943	svl := &tfe.StateVersionList{}
944	for _, sv := range m.stateVersions {
945		svl.Items = append(svl.Items, sv)
946	}
947
948	svl.Pagination = &tfe.Pagination{
949		CurrentPage:  1,
950		NextPage:     1,
951		PreviousPage: 1,
952		TotalPages:   1,
953		TotalCount:   len(svl.Items),
954	}
955
956	return svl, nil
957}
958
959func (m *mockStateVersions) Create(ctx context.Context, workspaceID string, options tfe.StateVersionCreateOptions) (*tfe.StateVersion, error) {
960	id := generateID("sv-")
961	runID := os.Getenv("TFE_RUN_ID")
962	url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id)
963
964	if runID != "" && (options.Run == nil || runID != options.Run.ID) {
965		return nil, fmt.Errorf("option.Run.ID does not contain the ID exported by TFE_RUN_ID")
966	}
967
968	sv := &tfe.StateVersion{
969		ID:          id,
970		DownloadURL: url,
971		Serial:      *options.Serial,
972	}
973
974	state, err := base64.StdEncoding.DecodeString(*options.State)
975	if err != nil {
976		return nil, err
977	}
978
979	m.states[sv.DownloadURL] = state
980	m.stateVersions[sv.ID] = sv
981	m.workspaces[workspaceID] = append(m.workspaces[workspaceID], sv.ID)
982
983	return sv, nil
984}
985
986func (m *mockStateVersions) Read(ctx context.Context, svID string) (*tfe.StateVersion, error) {
987	return m.ReadWithOptions(ctx, svID, nil)
988}
989
990func (m *mockStateVersions) ReadWithOptions(ctx context.Context, svID string, options *tfe.StateVersionReadOptions) (*tfe.StateVersion, error) {
991	sv, ok := m.stateVersions[svID]
992	if !ok {
993		return nil, tfe.ErrResourceNotFound
994	}
995	return sv, nil
996}
997
998func (m *mockStateVersions) Current(ctx context.Context, workspaceID string) (*tfe.StateVersion, error) {
999	return m.CurrentWithOptions(ctx, workspaceID, nil)
1000}
1001
1002func (m *mockStateVersions) CurrentWithOptions(ctx context.Context, workspaceID string, options *tfe.StateVersionCurrentOptions) (*tfe.StateVersion, error) {
1003	w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
1004	if !ok {
1005		return nil, tfe.ErrResourceNotFound
1006	}
1007
1008	svs, ok := m.workspaces[w.ID]
1009	if !ok || len(svs) == 0 {
1010		return nil, tfe.ErrResourceNotFound
1011	}
1012
1013	sv, ok := m.stateVersions[svs[len(svs)-1]]
1014	if !ok {
1015		return nil, tfe.ErrResourceNotFound
1016	}
1017
1018	return sv, nil
1019}
1020
1021func (m *mockStateVersions) Download(ctx context.Context, url string) ([]byte, error) {
1022	state, ok := m.states[url]
1023	if !ok {
1024		return nil, tfe.ErrResourceNotFound
1025	}
1026	return state, nil
1027}
1028
1029type mockVariables struct {
1030	client     *mockClient
1031	workspaces map[string]*tfe.VariableList
1032}
1033
1034var _ tfe.Variables = (*mockVariables)(nil)
1035
1036func newMockVariables(client *mockClient) *mockVariables {
1037	return &mockVariables{
1038		client:     client,
1039		workspaces: make(map[string]*tfe.VariableList),
1040	}
1041}
1042
1043func (m *mockVariables) List(ctx context.Context, workspaceID string, options tfe.VariableListOptions) (*tfe.VariableList, error) {
1044	vl := m.workspaces[workspaceID]
1045	return vl, nil
1046}
1047
1048func (m *mockVariables) Create(ctx context.Context, workspaceID string, options tfe.VariableCreateOptions) (*tfe.Variable, error) {
1049	v := &tfe.Variable{
1050		ID:       generateID("var-"),
1051		Key:      *options.Key,
1052		Category: *options.Category,
1053	}
1054	if options.Value != nil {
1055		v.Value = *options.Value
1056	}
1057	if options.HCL != nil {
1058		v.HCL = *options.HCL
1059	}
1060	if options.Sensitive != nil {
1061		v.Sensitive = *options.Sensitive
1062	}
1063
1064	workspace := workspaceID
1065
1066	if m.workspaces[workspace] == nil {
1067		m.workspaces[workspace] = &tfe.VariableList{}
1068	}
1069
1070	vl := m.workspaces[workspace]
1071	vl.Items = append(vl.Items, v)
1072
1073	return v, nil
1074}
1075
1076func (m *mockVariables) Read(ctx context.Context, workspaceID string, variableID string) (*tfe.Variable, error) {
1077	panic("not implemented")
1078}
1079
1080func (m *mockVariables) Update(ctx context.Context, workspaceID string, variableID string, options tfe.VariableUpdateOptions) (*tfe.Variable, error) {
1081	panic("not implemented")
1082}
1083
1084func (m *mockVariables) Delete(ctx context.Context, workspaceID string, variableID string) error {
1085	panic("not implemented")
1086}
1087
1088type mockWorkspaces struct {
1089	client         *mockClient
1090	workspaceIDs   map[string]*tfe.Workspace
1091	workspaceNames map[string]*tfe.Workspace
1092}
1093
1094func newMockWorkspaces(client *mockClient) *mockWorkspaces {
1095	return &mockWorkspaces{
1096		client:         client,
1097		workspaceIDs:   make(map[string]*tfe.Workspace),
1098		workspaceNames: make(map[string]*tfe.Workspace),
1099	}
1100}
1101
1102func (m *mockWorkspaces) List(ctx context.Context, organization string, options tfe.WorkspaceListOptions) (*tfe.WorkspaceList, error) {
1103	dummyWorkspaces := 10
1104	wl := &tfe.WorkspaceList{}
1105
1106	// Get the prefix from the search options.
1107	prefix := ""
1108	if options.Search != nil {
1109		prefix = *options.Search
1110	}
1111
1112	// Get all the workspaces that match the prefix.
1113	var ws []*tfe.Workspace
1114	for _, w := range m.workspaceIDs {
1115		if strings.HasPrefix(w.Name, prefix) {
1116			ws = append(ws, w)
1117		}
1118	}
1119
1120	// Return an empty result if we have no matches.
1121	if len(ws) == 0 {
1122		wl.Pagination = &tfe.Pagination{
1123			CurrentPage: 1,
1124		}
1125		return wl, nil
1126	}
1127
1128	// Return dummy workspaces for the first page to test pagination.
1129	if options.PageNumber <= 1 {
1130		for i := 0; i < dummyWorkspaces; i++ {
1131			wl.Items = append(wl.Items, &tfe.Workspace{
1132				ID:   generateID("ws-"),
1133				Name: fmt.Sprintf("dummy-workspace-%d", i),
1134			})
1135		}
1136
1137		wl.Pagination = &tfe.Pagination{
1138			CurrentPage: 1,
1139			NextPage:    2,
1140			TotalPages:  2,
1141			TotalCount:  len(wl.Items) + len(ws),
1142		}
1143
1144		return wl, nil
1145	}
1146
1147	// Return the actual workspaces that matched as the second page.
1148	wl.Items = ws
1149	wl.Pagination = &tfe.Pagination{
1150		CurrentPage:  2,
1151		PreviousPage: 1,
1152		TotalPages:   2,
1153		TotalCount:   len(wl.Items) + dummyWorkspaces,
1154	}
1155
1156	return wl, nil
1157}
1158
1159func (m *mockWorkspaces) Create(ctx context.Context, organization string, options tfe.WorkspaceCreateOptions) (*tfe.Workspace, error) {
1160	if strings.HasSuffix(*options.Name, "no-operations") {
1161		options.Operations = tfe.Bool(false)
1162	} else if options.Operations == nil {
1163		options.Operations = tfe.Bool(true)
1164	}
1165	w := &tfe.Workspace{
1166		ID:         generateID("ws-"),
1167		Name:       *options.Name,
1168		Operations: *options.Operations,
1169		Permissions: &tfe.WorkspacePermissions{
1170			CanQueueApply: true,
1171			CanQueueRun:   true,
1172		},
1173	}
1174	if options.AutoApply != nil {
1175		w.AutoApply = *options.AutoApply
1176	}
1177	if options.VCSRepo != nil {
1178		w.VCSRepo = &tfe.VCSRepo{}
1179	}
1180	if options.TerraformVersion != nil {
1181		w.TerraformVersion = *options.TerraformVersion
1182	} else {
1183		w.TerraformVersion = tfversion.String()
1184	}
1185	m.workspaceIDs[w.ID] = w
1186	m.workspaceNames[w.Name] = w
1187	return w, nil
1188}
1189
1190func (m *mockWorkspaces) Read(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) {
1191	// custom error for TestRemote_plan500 in backend_plan_test.go
1192	if workspace == "network-error" {
1193		return nil, errors.New("I'm a little teacup")
1194	}
1195
1196	w, ok := m.workspaceNames[workspace]
1197	if !ok {
1198		return nil, tfe.ErrResourceNotFound
1199	}
1200	return w, nil
1201}
1202
1203func (m *mockWorkspaces) ReadByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
1204	w, ok := m.workspaceIDs[workspaceID]
1205	if !ok {
1206		return nil, tfe.ErrResourceNotFound
1207	}
1208	return w, nil
1209}
1210
1211func (m *mockWorkspaces) Update(ctx context.Context, organization, workspace string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) {
1212	w, ok := m.workspaceNames[workspace]
1213	if !ok {
1214		return nil, tfe.ErrResourceNotFound
1215	}
1216
1217	if options.Operations != nil {
1218		w.Operations = *options.Operations
1219	}
1220	if options.Name != nil {
1221		w.Name = *options.Name
1222	}
1223	if options.TerraformVersion != nil {
1224		w.TerraformVersion = *options.TerraformVersion
1225	}
1226	if options.WorkingDirectory != nil {
1227		w.WorkingDirectory = *options.WorkingDirectory
1228	}
1229
1230	delete(m.workspaceNames, workspace)
1231	m.workspaceNames[w.Name] = w
1232
1233	return w, nil
1234}
1235
1236func (m *mockWorkspaces) UpdateByID(ctx context.Context, workspaceID string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) {
1237	w, ok := m.workspaceIDs[workspaceID]
1238	if !ok {
1239		return nil, tfe.ErrResourceNotFound
1240	}
1241
1242	if options.Name != nil {
1243		w.Name = *options.Name
1244	}
1245	if options.TerraformVersion != nil {
1246		w.TerraformVersion = *options.TerraformVersion
1247	}
1248	if options.WorkingDirectory != nil {
1249		w.WorkingDirectory = *options.WorkingDirectory
1250	}
1251
1252	delete(m.workspaceNames, w.Name)
1253	m.workspaceNames[w.Name] = w
1254
1255	return w, nil
1256}
1257
1258func (m *mockWorkspaces) Delete(ctx context.Context, organization, workspace string) error {
1259	if w, ok := m.workspaceNames[workspace]; ok {
1260		delete(m.workspaceIDs, w.ID)
1261	}
1262	delete(m.workspaceNames, workspace)
1263	return nil
1264}
1265
1266func (m *mockWorkspaces) DeleteByID(ctx context.Context, workspaceID string) error {
1267	if w, ok := m.workspaceIDs[workspaceID]; ok {
1268		delete(m.workspaceIDs, w.Name)
1269	}
1270	delete(m.workspaceIDs, workspaceID)
1271	return nil
1272}
1273
1274func (m *mockWorkspaces) RemoveVCSConnection(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) {
1275	w, ok := m.workspaceNames[workspace]
1276	if !ok {
1277		return nil, tfe.ErrResourceNotFound
1278	}
1279	w.VCSRepo = nil
1280	return w, nil
1281}
1282
1283func (m *mockWorkspaces) RemoveVCSConnectionByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
1284	w, ok := m.workspaceIDs[workspaceID]
1285	if !ok {
1286		return nil, tfe.ErrResourceNotFound
1287	}
1288	w.VCSRepo = nil
1289	return w, nil
1290}
1291
1292func (m *mockWorkspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) {
1293	w, ok := m.workspaceIDs[workspaceID]
1294	if !ok {
1295		return nil, tfe.ErrResourceNotFound
1296	}
1297	if w.Locked {
1298		return nil, tfe.ErrWorkspaceLocked
1299	}
1300	w.Locked = true
1301	return w, nil
1302}
1303
1304func (m *mockWorkspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
1305	w, ok := m.workspaceIDs[workspaceID]
1306	if !ok {
1307		return nil, tfe.ErrResourceNotFound
1308	}
1309	if !w.Locked {
1310		return nil, tfe.ErrWorkspaceNotLocked
1311	}
1312	w.Locked = false
1313	return w, nil
1314}
1315
1316func (m *mockWorkspaces) ForceUnlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
1317	w, ok := m.workspaceIDs[workspaceID]
1318	if !ok {
1319		return nil, tfe.ErrResourceNotFound
1320	}
1321	if !w.Locked {
1322		return nil, tfe.ErrWorkspaceNotLocked
1323	}
1324	w.Locked = false
1325	return w, nil
1326}
1327
1328func (m *mockWorkspaces) AssignSSHKey(ctx context.Context, workspaceID string, options tfe.WorkspaceAssignSSHKeyOptions) (*tfe.Workspace, error) {
1329	panic("not implemented")
1330}
1331
1332func (m *mockWorkspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
1333	panic("not implemented")
1334}
1335
1336func (m *mockWorkspaces) RemoteStateConsumers(ctx context.Context, workspaceID string) (*tfe.WorkspaceList, error) {
1337	panic("not implemented")
1338}
1339
1340func (m *mockWorkspaces) AddRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceAddRemoteStateConsumersOptions) error {
1341	panic("not implemented")
1342}
1343
1344func (m *mockWorkspaces) RemoveRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceRemoveRemoteStateConsumersOptions) error {
1345	panic("not implemented")
1346}
1347
1348func (m *mockWorkspaces) UpdateRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceUpdateRemoteStateConsumersOptions) error {
1349	panic("not implemented")
1350}
1351
1352func (m *mockWorkspaces) Readme(ctx context.Context, workspaceID string) (io.Reader, error) {
1353	panic("not implemented")
1354}
1355
1356const alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
1357
1358func generateID(s string) string {
1359	b := make([]byte, 16)
1360	for i := range b {
1361		b[i] = alphanumeric[rand.Intn(len(alphanumeric))]
1362	}
1363	return s + string(b)
1364}
1365