github.com/rvichery/terraform@v0.11.10/backend/remote/backend_mock.go (about)

     1  package remote
     2  
     3  import (
     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  	"time"
    16  
    17  	tfe "github.com/hashicorp/go-tfe"
    18  	"github.com/hashicorp/terraform/terraform"
    19  )
    20  
    21  type mockClient struct {
    22  	Applies               *mockApplies
    23  	ConfigurationVersions *mockConfigurationVersions
    24  	Organizations         *mockOrganizations
    25  	Plans                 *mockPlans
    26  	PolicyChecks          *mockPolicyChecks
    27  	Runs                  *mockRuns
    28  	StateVersions         *mockStateVersions
    29  	Workspaces            *mockWorkspaces
    30  }
    31  
    32  func newMockClient() *mockClient {
    33  	c := &mockClient{}
    34  	c.Applies = newMockApplies(c)
    35  	c.ConfigurationVersions = newMockConfigurationVersions(c)
    36  	c.Organizations = newMockOrganizations(c)
    37  	c.Plans = newMockPlans(c)
    38  	c.PolicyChecks = newMockPolicyChecks(c)
    39  	c.Runs = newMockRuns(c)
    40  	c.StateVersions = newMockStateVersions(c)
    41  	c.Workspaces = newMockWorkspaces(c)
    42  	return c
    43  }
    44  
    45  type mockApplies struct {
    46  	client  *mockClient
    47  	applies map[string]*tfe.Apply
    48  	logs    map[string]string
    49  }
    50  
    51  func newMockApplies(client *mockClient) *mockApplies {
    52  	return &mockApplies{
    53  		client:  client,
    54  		applies: make(map[string]*tfe.Apply),
    55  		logs:    make(map[string]string),
    56  	}
    57  }
    58  
    59  // create is a helper function to create a mock apply that uses the configured
    60  // working directory to find the logfile.
    61  func (m *mockApplies) create(cvID, workspaceID string) (*tfe.Apply, error) {
    62  	c, ok := m.client.ConfigurationVersions.configVersions[cvID]
    63  	if !ok {
    64  		return nil, tfe.ErrResourceNotFound
    65  	}
    66  	if c.Speculative {
    67  		// Speculative means its plan-only so we don't create a Apply.
    68  		return nil, nil
    69  	}
    70  
    71  	id := generateID("apply-")
    72  	url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id)
    73  
    74  	a := &tfe.Apply{
    75  		ID:         id,
    76  		LogReadURL: url,
    77  		Status:     tfe.ApplyPending,
    78  	}
    79  
    80  	w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
    81  	if !ok {
    82  		return nil, tfe.ErrResourceNotFound
    83  	}
    84  
    85  	if w.AutoApply {
    86  		a.Status = tfe.ApplyRunning
    87  	}
    88  
    89  	m.logs[url] = filepath.Join(
    90  		m.client.ConfigurationVersions.uploadPaths[cvID],
    91  		w.WorkingDirectory,
    92  		"apply.log",
    93  	)
    94  	m.applies[a.ID] = a
    95  
    96  	return a, nil
    97  }
    98  
    99  func (m *mockApplies) Read(ctx context.Context, applyID string) (*tfe.Apply, error) {
   100  	a, ok := m.applies[applyID]
   101  	if !ok {
   102  		return nil, tfe.ErrResourceNotFound
   103  	}
   104  	// Together with the mockLogReader this allows testing queued runs.
   105  	if a.Status == tfe.ApplyRunning {
   106  		a.Status = tfe.ApplyFinished
   107  	}
   108  	return a, nil
   109  }
   110  
   111  func (m *mockApplies) Logs(ctx context.Context, applyID string) (io.Reader, error) {
   112  	a, err := m.Read(ctx, applyID)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	logfile, ok := m.logs[a.LogReadURL]
   118  	if !ok {
   119  		return nil, tfe.ErrResourceNotFound
   120  	}
   121  
   122  	if _, err := os.Stat(logfile); os.IsNotExist(err) {
   123  		return bytes.NewBufferString("logfile does not exist"), nil
   124  	}
   125  
   126  	logs, err := ioutil.ReadFile(logfile)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  
   131  	done := func() (bool, error) {
   132  		a, err := m.Read(ctx, applyID)
   133  		if err != nil {
   134  			return false, err
   135  		}
   136  		if a.Status != tfe.ApplyFinished {
   137  			return false, nil
   138  		}
   139  		return true, nil
   140  	}
   141  
   142  	return &mockLogReader{
   143  		done: done,
   144  		logs: bytes.NewBuffer(logs),
   145  	}, nil
   146  }
   147  
   148  type mockConfigurationVersions struct {
   149  	client         *mockClient
   150  	configVersions map[string]*tfe.ConfigurationVersion
   151  	uploadPaths    map[string]string
   152  	uploadURLs     map[string]*tfe.ConfigurationVersion
   153  }
   154  
   155  func newMockConfigurationVersions(client *mockClient) *mockConfigurationVersions {
   156  	return &mockConfigurationVersions{
   157  		client:         client,
   158  		configVersions: make(map[string]*tfe.ConfigurationVersion),
   159  		uploadPaths:    make(map[string]string),
   160  		uploadURLs:     make(map[string]*tfe.ConfigurationVersion),
   161  	}
   162  }
   163  
   164  func (m *mockConfigurationVersions) List(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionListOptions) (*tfe.ConfigurationVersionList, error) {
   165  	cvl := &tfe.ConfigurationVersionList{}
   166  	for _, cv := range m.configVersions {
   167  		cvl.Items = append(cvl.Items, cv)
   168  	}
   169  
   170  	cvl.Pagination = &tfe.Pagination{
   171  		CurrentPage:  1,
   172  		NextPage:     1,
   173  		PreviousPage: 1,
   174  		TotalPages:   1,
   175  		TotalCount:   len(cvl.Items),
   176  	}
   177  
   178  	return cvl, nil
   179  }
   180  
   181  func (m *mockConfigurationVersions) Create(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionCreateOptions) (*tfe.ConfigurationVersion, error) {
   182  	id := generateID("cv-")
   183  	url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id)
   184  
   185  	cv := &tfe.ConfigurationVersion{
   186  		ID:        id,
   187  		Status:    tfe.ConfigurationPending,
   188  		UploadURL: url,
   189  	}
   190  
   191  	m.configVersions[cv.ID] = cv
   192  	m.uploadURLs[url] = cv
   193  
   194  	return cv, nil
   195  }
   196  
   197  func (m *mockConfigurationVersions) Read(ctx context.Context, cvID string) (*tfe.ConfigurationVersion, error) {
   198  	cv, ok := m.configVersions[cvID]
   199  	if !ok {
   200  		return nil, tfe.ErrResourceNotFound
   201  	}
   202  	return cv, nil
   203  }
   204  
   205  func (m *mockConfigurationVersions) Upload(ctx context.Context, url, path string) error {
   206  	cv, ok := m.uploadURLs[url]
   207  	if !ok {
   208  		return errors.New("404 not found")
   209  	}
   210  	m.uploadPaths[cv.ID] = path
   211  	cv.Status = tfe.ConfigurationUploaded
   212  	return nil
   213  }
   214  
   215  // mockInput is a mock implementation of terraform.UIInput.
   216  type mockInput struct {
   217  	answers map[string]string
   218  }
   219  
   220  func (m *mockInput) Input(opts *terraform.InputOpts) (string, error) {
   221  	v, ok := m.answers[opts.Id]
   222  	if !ok {
   223  		return "", fmt.Errorf("unexpected input request in test: %s", opts.Id)
   224  	}
   225  	delete(m.answers, opts.Id)
   226  	return v, nil
   227  }
   228  
   229  type mockOrganizations struct {
   230  	client        *mockClient
   231  	organizations map[string]*tfe.Organization
   232  }
   233  
   234  func newMockOrganizations(client *mockClient) *mockOrganizations {
   235  	return &mockOrganizations{
   236  		client:        client,
   237  		organizations: make(map[string]*tfe.Organization),
   238  	}
   239  }
   240  
   241  func (m *mockOrganizations) List(ctx context.Context, options tfe.OrganizationListOptions) (*tfe.OrganizationList, error) {
   242  	orgl := &tfe.OrganizationList{}
   243  	for _, org := range m.organizations {
   244  		orgl.Items = append(orgl.Items, org)
   245  	}
   246  
   247  	orgl.Pagination = &tfe.Pagination{
   248  		CurrentPage:  1,
   249  		NextPage:     1,
   250  		PreviousPage: 1,
   251  		TotalPages:   1,
   252  		TotalCount:   len(orgl.Items),
   253  	}
   254  
   255  	return orgl, nil
   256  }
   257  
   258  // mockLogReader is a mock logreader that enables testing queued runs.
   259  type mockLogReader struct {
   260  	done func() (bool, error)
   261  	logs *bytes.Buffer
   262  }
   263  
   264  func (m *mockLogReader) Read(l []byte) (int, error) {
   265  	for {
   266  		if written, err := m.read(l); err != io.ErrNoProgress {
   267  			return written, err
   268  		}
   269  		time.Sleep(500 * time.Millisecond)
   270  	}
   271  }
   272  
   273  func (m *mockLogReader) read(l []byte) (int, error) {
   274  	done, err := m.done()
   275  	if err != nil {
   276  		return 0, err
   277  	}
   278  	if !done {
   279  		return 0, io.ErrNoProgress
   280  	}
   281  	return m.logs.Read(l)
   282  }
   283  
   284  func (m *mockOrganizations) Create(ctx context.Context, options tfe.OrganizationCreateOptions) (*tfe.Organization, error) {
   285  	org := &tfe.Organization{Name: *options.Name}
   286  	m.organizations[org.Name] = org
   287  	return org, nil
   288  }
   289  
   290  func (m *mockOrganizations) Read(ctx context.Context, name string) (*tfe.Organization, error) {
   291  	org, ok := m.organizations[name]
   292  	if !ok {
   293  		return nil, tfe.ErrResourceNotFound
   294  	}
   295  	return org, nil
   296  }
   297  
   298  func (m *mockOrganizations) Update(ctx context.Context, name string, options tfe.OrganizationUpdateOptions) (*tfe.Organization, error) {
   299  	org, ok := m.organizations[name]
   300  	if !ok {
   301  		return nil, tfe.ErrResourceNotFound
   302  	}
   303  	org.Name = *options.Name
   304  	return org, nil
   305  
   306  }
   307  
   308  func (m *mockOrganizations) Delete(ctx context.Context, name string) error {
   309  	delete(m.organizations, name)
   310  	return nil
   311  }
   312  
   313  func (m *mockOrganizations) Capacity(ctx context.Context, name string) (*tfe.Capacity, error) {
   314  	var pending, running int
   315  	for _, r := range m.client.Runs.runs {
   316  		if r.Status == tfe.RunPending {
   317  			pending++
   318  			continue
   319  		}
   320  		running++
   321  	}
   322  	return &tfe.Capacity{Pending: pending, Running: running}, nil
   323  }
   324  
   325  func (m *mockOrganizations) RunQueue(ctx context.Context, name string, options tfe.RunQueueOptions) (*tfe.RunQueue, error) {
   326  	rq := &tfe.RunQueue{}
   327  
   328  	for _, r := range m.client.Runs.runs {
   329  		rq.Items = append(rq.Items, r)
   330  	}
   331  
   332  	rq.Pagination = &tfe.Pagination{
   333  		CurrentPage:  1,
   334  		NextPage:     1,
   335  		PreviousPage: 1,
   336  		TotalPages:   1,
   337  		TotalCount:   len(rq.Items),
   338  	}
   339  
   340  	return rq, nil
   341  }
   342  
   343  type mockPlans struct {
   344  	client *mockClient
   345  	logs   map[string]string
   346  	plans  map[string]*tfe.Plan
   347  }
   348  
   349  func newMockPlans(client *mockClient) *mockPlans {
   350  	return &mockPlans{
   351  		client: client,
   352  		logs:   make(map[string]string),
   353  		plans:  make(map[string]*tfe.Plan),
   354  	}
   355  }
   356  
   357  // create is a helper function to create a mock plan that uses the configured
   358  // working directory to find the logfile.
   359  func (m *mockPlans) create(cvID, workspaceID string) (*tfe.Plan, error) {
   360  	id := generateID("plan-")
   361  	url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id)
   362  
   363  	p := &tfe.Plan{
   364  		ID:         id,
   365  		LogReadURL: url,
   366  		Status:     tfe.PlanPending,
   367  	}
   368  
   369  	w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
   370  	if !ok {
   371  		return nil, tfe.ErrResourceNotFound
   372  	}
   373  
   374  	m.logs[url] = filepath.Join(
   375  		m.client.ConfigurationVersions.uploadPaths[cvID],
   376  		w.WorkingDirectory,
   377  		"plan.log",
   378  	)
   379  	m.plans[p.ID] = p
   380  
   381  	return p, nil
   382  }
   383  
   384  func (m *mockPlans) Read(ctx context.Context, planID string) (*tfe.Plan, error) {
   385  	p, ok := m.plans[planID]
   386  	if !ok {
   387  		return nil, tfe.ErrResourceNotFound
   388  	}
   389  	// Together with the mockLogReader this allows testing queued runs.
   390  	if p.Status == tfe.PlanRunning {
   391  		p.Status = tfe.PlanFinished
   392  	}
   393  	return p, nil
   394  }
   395  
   396  func (m *mockPlans) Logs(ctx context.Context, planID string) (io.Reader, error) {
   397  	p, err := m.Read(ctx, planID)
   398  	if err != nil {
   399  		return nil, err
   400  	}
   401  
   402  	logfile, ok := m.logs[p.LogReadURL]
   403  	if !ok {
   404  		return nil, tfe.ErrResourceNotFound
   405  	}
   406  
   407  	if _, err := os.Stat(logfile); os.IsNotExist(err) {
   408  		return bytes.NewBufferString("logfile does not exist"), nil
   409  	}
   410  
   411  	logs, err := ioutil.ReadFile(logfile)
   412  	if err != nil {
   413  		return nil, err
   414  	}
   415  
   416  	done := func() (bool, error) {
   417  		p, err := m.Read(ctx, planID)
   418  		if err != nil {
   419  			return false, err
   420  		}
   421  		if p.Status != tfe.PlanFinished {
   422  			return false, nil
   423  		}
   424  		return true, nil
   425  	}
   426  
   427  	return &mockLogReader{
   428  		done: done,
   429  		logs: bytes.NewBuffer(logs),
   430  	}, nil
   431  }
   432  
   433  type mockPolicyChecks struct {
   434  	client *mockClient
   435  	checks map[string]*tfe.PolicyCheck
   436  	logs   map[string]string
   437  }
   438  
   439  func newMockPolicyChecks(client *mockClient) *mockPolicyChecks {
   440  	return &mockPolicyChecks{
   441  		client: client,
   442  		checks: make(map[string]*tfe.PolicyCheck),
   443  		logs:   make(map[string]string),
   444  	}
   445  }
   446  
   447  // create is a helper function to create a mock policy check that uses the
   448  // configured working directory to find the logfile.
   449  func (m *mockPolicyChecks) create(cvID, workspaceID string) (*tfe.PolicyCheck, error) {
   450  	id := generateID("pc-")
   451  
   452  	pc := &tfe.PolicyCheck{
   453  		ID:          id,
   454  		Actions:     &tfe.PolicyActions{},
   455  		Permissions: &tfe.PolicyPermissions{},
   456  		Scope:       tfe.PolicyScopeOrganization,
   457  		Status:      tfe.PolicyPending,
   458  	}
   459  
   460  	w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
   461  	if !ok {
   462  		return nil, tfe.ErrResourceNotFound
   463  	}
   464  
   465  	logfile := filepath.Join(
   466  		m.client.ConfigurationVersions.uploadPaths[cvID],
   467  		w.WorkingDirectory,
   468  		"policy.log",
   469  	)
   470  
   471  	if _, err := os.Stat(logfile); os.IsNotExist(err) {
   472  		return nil, nil
   473  	}
   474  
   475  	m.logs[pc.ID] = logfile
   476  	m.checks[pc.ID] = pc
   477  
   478  	return pc, nil
   479  }
   480  
   481  func (m *mockPolicyChecks) List(ctx context.Context, runID string, options tfe.PolicyCheckListOptions) (*tfe.PolicyCheckList, error) {
   482  	_, ok := m.client.Runs.runs[runID]
   483  	if !ok {
   484  		return nil, tfe.ErrResourceNotFound
   485  	}
   486  
   487  	pcl := &tfe.PolicyCheckList{}
   488  	for _, pc := range m.checks {
   489  		pcl.Items = append(pcl.Items, pc)
   490  	}
   491  
   492  	pcl.Pagination = &tfe.Pagination{
   493  		CurrentPage:  1,
   494  		NextPage:     1,
   495  		PreviousPage: 1,
   496  		TotalPages:   1,
   497  		TotalCount:   len(pcl.Items),
   498  	}
   499  
   500  	return pcl, nil
   501  }
   502  
   503  func (m *mockPolicyChecks) Read(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) {
   504  	pc, ok := m.checks[policyCheckID]
   505  	if !ok {
   506  		return nil, tfe.ErrResourceNotFound
   507  	}
   508  
   509  	logfile, ok := m.logs[pc.ID]
   510  	if !ok {
   511  		return nil, tfe.ErrResourceNotFound
   512  	}
   513  
   514  	if _, err := os.Stat(logfile); os.IsNotExist(err) {
   515  		return nil, fmt.Errorf("logfile does not exist")
   516  	}
   517  
   518  	logs, err := ioutil.ReadFile(logfile)
   519  	if err != nil {
   520  		return nil, err
   521  	}
   522  
   523  	switch {
   524  	case bytes.Contains(logs, []byte("Sentinel Result: true")):
   525  		pc.Status = tfe.PolicyPasses
   526  	case bytes.Contains(logs, []byte("Sentinel Result: false")):
   527  		switch {
   528  		case bytes.Contains(logs, []byte("hard-mandatory")):
   529  			pc.Status = tfe.PolicyHardFailed
   530  		case bytes.Contains(logs, []byte("soft-mandatory")):
   531  			pc.Actions.IsOverridable = true
   532  			pc.Permissions.CanOverride = true
   533  			pc.Status = tfe.PolicySoftFailed
   534  		}
   535  	default:
   536  		// As this is an unexpected state, we say the policy errored.
   537  		pc.Status = tfe.PolicyErrored
   538  	}
   539  
   540  	return pc, nil
   541  }
   542  
   543  func (m *mockPolicyChecks) Override(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) {
   544  	pc, ok := m.checks[policyCheckID]
   545  	if !ok {
   546  		return nil, tfe.ErrResourceNotFound
   547  	}
   548  	pc.Status = tfe.PolicyOverridden
   549  	return pc, nil
   550  }
   551  
   552  func (m *mockPolicyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reader, error) {
   553  	pc, ok := m.checks[policyCheckID]
   554  	if !ok {
   555  		return nil, tfe.ErrResourceNotFound
   556  	}
   557  
   558  	logfile, ok := m.logs[pc.ID]
   559  	if !ok {
   560  		return nil, tfe.ErrResourceNotFound
   561  	}
   562  
   563  	if _, err := os.Stat(logfile); os.IsNotExist(err) {
   564  		return bytes.NewBufferString("logfile does not exist"), nil
   565  	}
   566  
   567  	logs, err := ioutil.ReadFile(logfile)
   568  	if err != nil {
   569  		return nil, err
   570  	}
   571  
   572  	switch {
   573  	case bytes.Contains(logs, []byte("Sentinel Result: true")):
   574  		pc.Status = tfe.PolicyPasses
   575  	case bytes.Contains(logs, []byte("Sentinel Result: false")):
   576  		switch {
   577  		case bytes.Contains(logs, []byte("hard-mandatory")):
   578  			pc.Status = tfe.PolicyHardFailed
   579  		case bytes.Contains(logs, []byte("soft-mandatory")):
   580  			pc.Actions.IsOverridable = true
   581  			pc.Permissions.CanOverride = true
   582  			pc.Status = tfe.PolicySoftFailed
   583  		}
   584  	default:
   585  		// As this is an unexpected state, we say the policy errored.
   586  		pc.Status = tfe.PolicyErrored
   587  	}
   588  
   589  	return bytes.NewBuffer(logs), nil
   590  }
   591  
   592  type mockRuns struct {
   593  	client     *mockClient
   594  	runs       map[string]*tfe.Run
   595  	workspaces map[string][]*tfe.Run
   596  }
   597  
   598  func newMockRuns(client *mockClient) *mockRuns {
   599  	return &mockRuns{
   600  		client:     client,
   601  		runs:       make(map[string]*tfe.Run),
   602  		workspaces: make(map[string][]*tfe.Run),
   603  	}
   604  }
   605  
   606  func (m *mockRuns) List(ctx context.Context, workspaceID string, options tfe.RunListOptions) (*tfe.RunList, error) {
   607  	w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
   608  	if !ok {
   609  		return nil, tfe.ErrResourceNotFound
   610  	}
   611  
   612  	rl := &tfe.RunList{}
   613  	for _, r := range m.workspaces[w.ID] {
   614  		rl.Items = append(rl.Items, r)
   615  	}
   616  
   617  	rl.Pagination = &tfe.Pagination{
   618  		CurrentPage:  1,
   619  		NextPage:     1,
   620  		PreviousPage: 1,
   621  		TotalPages:   1,
   622  		TotalCount:   len(rl.Items),
   623  	}
   624  
   625  	return rl, nil
   626  }
   627  
   628  func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*tfe.Run, error) {
   629  	a, err := m.client.Applies.create(options.ConfigurationVersion.ID, options.Workspace.ID)
   630  	if err != nil {
   631  		return nil, err
   632  	}
   633  
   634  	p, err := m.client.Plans.create(options.ConfigurationVersion.ID, options.Workspace.ID)
   635  	if err != nil {
   636  		return nil, err
   637  	}
   638  
   639  	pc, err := m.client.PolicyChecks.create(options.ConfigurationVersion.ID, options.Workspace.ID)
   640  	if err != nil {
   641  		return nil, err
   642  	}
   643  
   644  	r := &tfe.Run{
   645  		ID:          generateID("run-"),
   646  		Actions:     &tfe.RunActions{IsCancelable: true},
   647  		Apply:       a,
   648  		HasChanges:  false,
   649  		Permissions: &tfe.RunPermissions{},
   650  		Plan:        p,
   651  		Status:      tfe.RunPending,
   652  	}
   653  
   654  	if pc != nil {
   655  		r.PolicyChecks = []*tfe.PolicyCheck{pc}
   656  	}
   657  
   658  	if options.IsDestroy != nil {
   659  		r.IsDestroy = *options.IsDestroy
   660  	}
   661  
   662  	w, ok := m.client.Workspaces.workspaceIDs[options.Workspace.ID]
   663  	if !ok {
   664  		return nil, tfe.ErrResourceNotFound
   665  	}
   666  	if w.CurrentRun == nil {
   667  		w.CurrentRun = r
   668  	}
   669  
   670  	m.runs[r.ID] = r
   671  	m.workspaces[options.Workspace.ID] = append(m.workspaces[options.Workspace.ID], r)
   672  
   673  	return r, nil
   674  }
   675  
   676  func (m *mockRuns) Read(ctx context.Context, runID string) (*tfe.Run, error) {
   677  	r, ok := m.runs[runID]
   678  	if !ok {
   679  		return nil, tfe.ErrResourceNotFound
   680  	}
   681  
   682  	pending := false
   683  	for _, r := range m.runs {
   684  		if r.ID != runID && r.Status == tfe.RunPending {
   685  			pending = true
   686  			break
   687  		}
   688  	}
   689  
   690  	if !pending && r.Status == tfe.RunPending {
   691  		// Only update the status if there are no other pending runs.
   692  		r.Status = tfe.RunPlanning
   693  		r.Plan.Status = tfe.PlanRunning
   694  	}
   695  
   696  	logs, _ := ioutil.ReadFile(m.client.Plans.logs[r.Plan.LogReadURL])
   697  	if r.Plan.Status == tfe.PlanFinished {
   698  		if r.IsDestroy || bytes.Contains(logs, []byte("1 to add, 0 to change, 0 to destroy")) {
   699  			r.Actions.IsCancelable = false
   700  			r.Actions.IsConfirmable = true
   701  			r.HasChanges = true
   702  			r.Permissions.CanApply = true
   703  		}
   704  
   705  		if bytes.Contains(logs, []byte("null_resource.foo: 1 error")) {
   706  			r.Actions.IsCancelable = false
   707  			r.HasChanges = false
   708  			r.Status = tfe.RunErrored
   709  		}
   710  	}
   711  
   712  	return r, nil
   713  }
   714  
   715  func (m *mockRuns) Apply(ctx context.Context, runID string, options tfe.RunApplyOptions) error {
   716  	r, ok := m.runs[runID]
   717  	if !ok {
   718  		return tfe.ErrResourceNotFound
   719  	}
   720  	if r.Status != tfe.RunPending {
   721  		// Only update the status if the run is not pending anymore.
   722  		r.Status = tfe.RunApplying
   723  		r.Apply.Status = tfe.ApplyRunning
   724  	}
   725  	return nil
   726  }
   727  
   728  func (m *mockRuns) Cancel(ctx context.Context, runID string, options tfe.RunCancelOptions) error {
   729  	panic("not implemented")
   730  }
   731  
   732  func (m *mockRuns) ForceCancel(ctx context.Context, runID string, options tfe.RunForceCancelOptions) error {
   733  	panic("not implemented")
   734  }
   735  
   736  func (m *mockRuns) Discard(ctx context.Context, runID string, options tfe.RunDiscardOptions) error {
   737  	panic("not implemented")
   738  }
   739  
   740  type mockStateVersions struct {
   741  	client        *mockClient
   742  	states        map[string][]byte
   743  	stateVersions map[string]*tfe.StateVersion
   744  	workspaces    map[string][]string
   745  }
   746  
   747  func newMockStateVersions(client *mockClient) *mockStateVersions {
   748  	return &mockStateVersions{
   749  		client:        client,
   750  		states:        make(map[string][]byte),
   751  		stateVersions: make(map[string]*tfe.StateVersion),
   752  		workspaces:    make(map[string][]string),
   753  	}
   754  }
   755  
   756  func (m *mockStateVersions) List(ctx context.Context, options tfe.StateVersionListOptions) (*tfe.StateVersionList, error) {
   757  	svl := &tfe.StateVersionList{}
   758  	for _, sv := range m.stateVersions {
   759  		svl.Items = append(svl.Items, sv)
   760  	}
   761  
   762  	svl.Pagination = &tfe.Pagination{
   763  		CurrentPage:  1,
   764  		NextPage:     1,
   765  		PreviousPage: 1,
   766  		TotalPages:   1,
   767  		TotalCount:   len(svl.Items),
   768  	}
   769  
   770  	return svl, nil
   771  }
   772  
   773  func (m *mockStateVersions) Create(ctx context.Context, workspaceID string, options tfe.StateVersionCreateOptions) (*tfe.StateVersion, error) {
   774  	id := generateID("sv-")
   775  	runID := os.Getenv("TFE_RUN_ID")
   776  	url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id)
   777  
   778  	if runID != "" && (options.Run == nil || runID != options.Run.ID) {
   779  		return nil, fmt.Errorf("option.Run.ID does not contain the ID exported by TFE_RUN_ID")
   780  	}
   781  
   782  	sv := &tfe.StateVersion{
   783  		ID:          id,
   784  		DownloadURL: url,
   785  		Serial:      *options.Serial,
   786  	}
   787  
   788  	state, err := base64.StdEncoding.DecodeString(*options.State)
   789  	if err != nil {
   790  		return nil, err
   791  	}
   792  
   793  	m.states[sv.DownloadURL] = state
   794  	m.stateVersions[sv.ID] = sv
   795  	m.workspaces[workspaceID] = append(m.workspaces[workspaceID], sv.ID)
   796  
   797  	return sv, nil
   798  }
   799  
   800  func (m *mockStateVersions) Read(ctx context.Context, svID string) (*tfe.StateVersion, error) {
   801  	sv, ok := m.stateVersions[svID]
   802  	if !ok {
   803  		return nil, tfe.ErrResourceNotFound
   804  	}
   805  	return sv, nil
   806  }
   807  
   808  func (m *mockStateVersions) Current(ctx context.Context, workspaceID string) (*tfe.StateVersion, error) {
   809  	w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
   810  	if !ok {
   811  		return nil, tfe.ErrResourceNotFound
   812  	}
   813  
   814  	svs, ok := m.workspaces[w.ID]
   815  	if !ok || len(svs) == 0 {
   816  		return nil, tfe.ErrResourceNotFound
   817  	}
   818  
   819  	sv, ok := m.stateVersions[svs[len(svs)-1]]
   820  	if !ok {
   821  		return nil, tfe.ErrResourceNotFound
   822  	}
   823  
   824  	return sv, nil
   825  }
   826  
   827  func (m *mockStateVersions) Download(ctx context.Context, url string) ([]byte, error) {
   828  	state, ok := m.states[url]
   829  	if !ok {
   830  		return nil, tfe.ErrResourceNotFound
   831  	}
   832  	return state, nil
   833  }
   834  
   835  type mockWorkspaces struct {
   836  	client         *mockClient
   837  	workspaceIDs   map[string]*tfe.Workspace
   838  	workspaceNames map[string]*tfe.Workspace
   839  }
   840  
   841  func newMockWorkspaces(client *mockClient) *mockWorkspaces {
   842  	return &mockWorkspaces{
   843  		client:         client,
   844  		workspaceIDs:   make(map[string]*tfe.Workspace),
   845  		workspaceNames: make(map[string]*tfe.Workspace),
   846  	}
   847  }
   848  
   849  func (m *mockWorkspaces) List(ctx context.Context, organization string, options tfe.WorkspaceListOptions) (*tfe.WorkspaceList, error) {
   850  	dummyWorkspaces := 10
   851  	wl := &tfe.WorkspaceList{}
   852  
   853  	// Get the prefix from the search options.
   854  	prefix := ""
   855  	if options.Search != nil {
   856  		prefix = *options.Search
   857  	}
   858  
   859  	// Get all the workspaces that match the prefix.
   860  	var ws []*tfe.Workspace
   861  	for _, w := range m.workspaceIDs {
   862  		if strings.HasPrefix(w.Name, prefix) {
   863  			ws = append(ws, w)
   864  		}
   865  	}
   866  
   867  	// Return an empty result if we have no matches.
   868  	if len(ws) == 0 {
   869  		wl.Pagination = &tfe.Pagination{
   870  			CurrentPage: 1,
   871  		}
   872  		return wl, nil
   873  	}
   874  
   875  	// Return dummy workspaces for the first page to test pagination.
   876  	if options.PageNumber <= 1 {
   877  		for i := 0; i < dummyWorkspaces; i++ {
   878  			wl.Items = append(wl.Items, &tfe.Workspace{
   879  				ID:   generateID("ws-"),
   880  				Name: fmt.Sprintf("dummy-workspace-%d", i),
   881  			})
   882  		}
   883  
   884  		wl.Pagination = &tfe.Pagination{
   885  			CurrentPage: 1,
   886  			NextPage:    2,
   887  			TotalPages:  2,
   888  			TotalCount:  len(wl.Items) + len(ws),
   889  		}
   890  
   891  		return wl, nil
   892  	}
   893  
   894  	// Return the actual workspaces that matched as the second page.
   895  	wl.Items = ws
   896  	wl.Pagination = &tfe.Pagination{
   897  		CurrentPage:  2,
   898  		PreviousPage: 1,
   899  		TotalPages:   2,
   900  		TotalCount:   len(wl.Items) + dummyWorkspaces,
   901  	}
   902  
   903  	return wl, nil
   904  }
   905  
   906  func (m *mockWorkspaces) Create(ctx context.Context, organization string, options tfe.WorkspaceCreateOptions) (*tfe.Workspace, error) {
   907  	w := &tfe.Workspace{
   908  		ID:   generateID("ws-"),
   909  		Name: *options.Name,
   910  		Permissions: &tfe.WorkspacePermissions{
   911  			CanQueueRun: true,
   912  			CanUpdate:   true,
   913  		},
   914  	}
   915  	if options.AutoApply != nil {
   916  		w.AutoApply = *options.AutoApply
   917  	}
   918  	if options.VCSRepo != nil {
   919  		w.VCSRepo = &tfe.VCSRepo{}
   920  	}
   921  	m.workspaceIDs[w.ID] = w
   922  	m.workspaceNames[w.Name] = w
   923  	return w, nil
   924  }
   925  
   926  func (m *mockWorkspaces) Read(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) {
   927  	w, ok := m.workspaceNames[workspace]
   928  	if !ok {
   929  		return nil, tfe.ErrResourceNotFound
   930  	}
   931  	return w, nil
   932  }
   933  
   934  func (m *mockWorkspaces) Update(ctx context.Context, organization, workspace string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) {
   935  	w, ok := m.workspaceNames[workspace]
   936  	if !ok {
   937  		return nil, tfe.ErrResourceNotFound
   938  	}
   939  
   940  	if options.Name != nil {
   941  		w.Name = *options.Name
   942  	}
   943  	if options.TerraformVersion != nil {
   944  		w.TerraformVersion = *options.TerraformVersion
   945  	}
   946  	if options.WorkingDirectory != nil {
   947  		w.WorkingDirectory = *options.WorkingDirectory
   948  	}
   949  
   950  	delete(m.workspaceNames, workspace)
   951  	m.workspaceNames[w.Name] = w
   952  
   953  	return w, nil
   954  }
   955  
   956  func (m *mockWorkspaces) Delete(ctx context.Context, organization, workspace string) error {
   957  	if w, ok := m.workspaceNames[workspace]; ok {
   958  		delete(m.workspaceIDs, w.ID)
   959  	}
   960  	delete(m.workspaceNames, workspace)
   961  	return nil
   962  }
   963  
   964  func (m *mockWorkspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) {
   965  	w, ok := m.workspaceIDs[workspaceID]
   966  	if !ok {
   967  		return nil, tfe.ErrResourceNotFound
   968  	}
   969  	w.Locked = true
   970  	return w, nil
   971  }
   972  
   973  func (m *mockWorkspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
   974  	w, ok := m.workspaceIDs[workspaceID]
   975  	if !ok {
   976  		return nil, tfe.ErrResourceNotFound
   977  	}
   978  	w.Locked = false
   979  	return w, nil
   980  }
   981  
   982  func (m *mockWorkspaces) AssignSSHKey(ctx context.Context, workspaceID string, options tfe.WorkspaceAssignSSHKeyOptions) (*tfe.Workspace, error) {
   983  	panic("not implemented")
   984  }
   985  
   986  func (m *mockWorkspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
   987  	panic("not implemented")
   988  }
   989  
   990  const alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
   991  
   992  func generateID(s string) string {
   993  	b := make([]byte, 16)
   994  	for i := range b {
   995  		b[i] = alphanumeric[rand.Intn(len(alphanumeric))]
   996  	}
   997  	return s + string(b)
   998  }