github.com/ctrox/terraform@v0.11.12-beta1/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) Entitlements(ctx context.Context, name string) (*tfe.Entitlements, error) {
   326  	return &tfe.Entitlements{
   327  		Operations:            true,
   328  		PrivateModuleRegistry: true,
   329  		Sentinel:              true,
   330  		StateStorage:          true,
   331  		Teams:                 true,
   332  		VCSIntegrations:       true,
   333  	}, nil
   334  }
   335  
   336  func (m *mockOrganizations) RunQueue(ctx context.Context, name string, options tfe.RunQueueOptions) (*tfe.RunQueue, error) {
   337  	rq := &tfe.RunQueue{}
   338  
   339  	for _, r := range m.client.Runs.runs {
   340  		rq.Items = append(rq.Items, r)
   341  	}
   342  
   343  	rq.Pagination = &tfe.Pagination{
   344  		CurrentPage:  1,
   345  		NextPage:     1,
   346  		PreviousPage: 1,
   347  		TotalPages:   1,
   348  		TotalCount:   len(rq.Items),
   349  	}
   350  
   351  	return rq, nil
   352  }
   353  
   354  type mockPlans struct {
   355  	client *mockClient
   356  	logs   map[string]string
   357  	plans  map[string]*tfe.Plan
   358  }
   359  
   360  func newMockPlans(client *mockClient) *mockPlans {
   361  	return &mockPlans{
   362  		client: client,
   363  		logs:   make(map[string]string),
   364  		plans:  make(map[string]*tfe.Plan),
   365  	}
   366  }
   367  
   368  // create is a helper function to create a mock plan that uses the configured
   369  // working directory to find the logfile.
   370  func (m *mockPlans) create(cvID, workspaceID string) (*tfe.Plan, error) {
   371  	id := generateID("plan-")
   372  	url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id)
   373  
   374  	p := &tfe.Plan{
   375  		ID:         id,
   376  		LogReadURL: url,
   377  		Status:     tfe.PlanPending,
   378  	}
   379  
   380  	w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
   381  	if !ok {
   382  		return nil, tfe.ErrResourceNotFound
   383  	}
   384  
   385  	m.logs[url] = filepath.Join(
   386  		m.client.ConfigurationVersions.uploadPaths[cvID],
   387  		w.WorkingDirectory,
   388  		"plan.log",
   389  	)
   390  	m.plans[p.ID] = p
   391  
   392  	return p, nil
   393  }
   394  
   395  func (m *mockPlans) Read(ctx context.Context, planID string) (*tfe.Plan, error) {
   396  	p, ok := m.plans[planID]
   397  	if !ok {
   398  		return nil, tfe.ErrResourceNotFound
   399  	}
   400  	// Together with the mockLogReader this allows testing queued runs.
   401  	if p.Status == tfe.PlanRunning {
   402  		p.Status = tfe.PlanFinished
   403  	}
   404  	return p, nil
   405  }
   406  
   407  func (m *mockPlans) Logs(ctx context.Context, planID string) (io.Reader, error) {
   408  	p, err := m.Read(ctx, planID)
   409  	if err != nil {
   410  		return nil, err
   411  	}
   412  
   413  	logfile, ok := m.logs[p.LogReadURL]
   414  	if !ok {
   415  		return nil, tfe.ErrResourceNotFound
   416  	}
   417  
   418  	if _, err := os.Stat(logfile); os.IsNotExist(err) {
   419  		return bytes.NewBufferString("logfile does not exist"), nil
   420  	}
   421  
   422  	logs, err := ioutil.ReadFile(logfile)
   423  	if err != nil {
   424  		return nil, err
   425  	}
   426  
   427  	done := func() (bool, error) {
   428  		p, err := m.Read(ctx, planID)
   429  		if err != nil {
   430  			return false, err
   431  		}
   432  		if p.Status != tfe.PlanFinished {
   433  			return false, nil
   434  		}
   435  		return true, nil
   436  	}
   437  
   438  	return &mockLogReader{
   439  		done: done,
   440  		logs: bytes.NewBuffer(logs),
   441  	}, nil
   442  }
   443  
   444  type mockPolicyChecks struct {
   445  	client *mockClient
   446  	checks map[string]*tfe.PolicyCheck
   447  	logs   map[string]string
   448  }
   449  
   450  func newMockPolicyChecks(client *mockClient) *mockPolicyChecks {
   451  	return &mockPolicyChecks{
   452  		client: client,
   453  		checks: make(map[string]*tfe.PolicyCheck),
   454  		logs:   make(map[string]string),
   455  	}
   456  }
   457  
   458  // create is a helper function to create a mock policy check that uses the
   459  // configured working directory to find the logfile.
   460  func (m *mockPolicyChecks) create(cvID, workspaceID string) (*tfe.PolicyCheck, error) {
   461  	id := generateID("pc-")
   462  
   463  	pc := &tfe.PolicyCheck{
   464  		ID:          id,
   465  		Actions:     &tfe.PolicyActions{},
   466  		Permissions: &tfe.PolicyPermissions{},
   467  		Scope:       tfe.PolicyScopeOrganization,
   468  		Status:      tfe.PolicyPending,
   469  	}
   470  
   471  	w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
   472  	if !ok {
   473  		return nil, tfe.ErrResourceNotFound
   474  	}
   475  
   476  	logfile := filepath.Join(
   477  		m.client.ConfigurationVersions.uploadPaths[cvID],
   478  		w.WorkingDirectory,
   479  		"policy.log",
   480  	)
   481  
   482  	if _, err := os.Stat(logfile); os.IsNotExist(err) {
   483  		return nil, nil
   484  	}
   485  
   486  	m.logs[pc.ID] = logfile
   487  	m.checks[pc.ID] = pc
   488  
   489  	return pc, nil
   490  }
   491  
   492  func (m *mockPolicyChecks) List(ctx context.Context, runID string, options tfe.PolicyCheckListOptions) (*tfe.PolicyCheckList, error) {
   493  	_, ok := m.client.Runs.runs[runID]
   494  	if !ok {
   495  		return nil, tfe.ErrResourceNotFound
   496  	}
   497  
   498  	pcl := &tfe.PolicyCheckList{}
   499  	for _, pc := range m.checks {
   500  		pcl.Items = append(pcl.Items, pc)
   501  	}
   502  
   503  	pcl.Pagination = &tfe.Pagination{
   504  		CurrentPage:  1,
   505  		NextPage:     1,
   506  		PreviousPage: 1,
   507  		TotalPages:   1,
   508  		TotalCount:   len(pcl.Items),
   509  	}
   510  
   511  	return pcl, nil
   512  }
   513  
   514  func (m *mockPolicyChecks) Read(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) {
   515  	pc, ok := m.checks[policyCheckID]
   516  	if !ok {
   517  		return nil, tfe.ErrResourceNotFound
   518  	}
   519  
   520  	logfile, ok := m.logs[pc.ID]
   521  	if !ok {
   522  		return nil, tfe.ErrResourceNotFound
   523  	}
   524  
   525  	if _, err := os.Stat(logfile); os.IsNotExist(err) {
   526  		return nil, fmt.Errorf("logfile does not exist")
   527  	}
   528  
   529  	logs, err := ioutil.ReadFile(logfile)
   530  	if err != nil {
   531  		return nil, err
   532  	}
   533  
   534  	switch {
   535  	case bytes.Contains(logs, []byte("Sentinel Result: true")):
   536  		pc.Status = tfe.PolicyPasses
   537  	case bytes.Contains(logs, []byte("Sentinel Result: false")):
   538  		switch {
   539  		case bytes.Contains(logs, []byte("hard-mandatory")):
   540  			pc.Status = tfe.PolicyHardFailed
   541  		case bytes.Contains(logs, []byte("soft-mandatory")):
   542  			pc.Actions.IsOverridable = true
   543  			pc.Permissions.CanOverride = true
   544  			pc.Status = tfe.PolicySoftFailed
   545  		}
   546  	default:
   547  		// As this is an unexpected state, we say the policy errored.
   548  		pc.Status = tfe.PolicyErrored
   549  	}
   550  
   551  	return pc, nil
   552  }
   553  
   554  func (m *mockPolicyChecks) Override(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) {
   555  	pc, ok := m.checks[policyCheckID]
   556  	if !ok {
   557  		return nil, tfe.ErrResourceNotFound
   558  	}
   559  	pc.Status = tfe.PolicyOverridden
   560  	return pc, nil
   561  }
   562  
   563  func (m *mockPolicyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reader, error) {
   564  	pc, ok := m.checks[policyCheckID]
   565  	if !ok {
   566  		return nil, tfe.ErrResourceNotFound
   567  	}
   568  
   569  	logfile, ok := m.logs[pc.ID]
   570  	if !ok {
   571  		return nil, tfe.ErrResourceNotFound
   572  	}
   573  
   574  	if _, err := os.Stat(logfile); os.IsNotExist(err) {
   575  		return bytes.NewBufferString("logfile does not exist"), nil
   576  	}
   577  
   578  	logs, err := ioutil.ReadFile(logfile)
   579  	if err != nil {
   580  		return nil, err
   581  	}
   582  
   583  	switch {
   584  	case bytes.Contains(logs, []byte("Sentinel Result: true")):
   585  		pc.Status = tfe.PolicyPasses
   586  	case bytes.Contains(logs, []byte("Sentinel Result: false")):
   587  		switch {
   588  		case bytes.Contains(logs, []byte("hard-mandatory")):
   589  			pc.Status = tfe.PolicyHardFailed
   590  		case bytes.Contains(logs, []byte("soft-mandatory")):
   591  			pc.Actions.IsOverridable = true
   592  			pc.Permissions.CanOverride = true
   593  			pc.Status = tfe.PolicySoftFailed
   594  		}
   595  	default:
   596  		// As this is an unexpected state, we say the policy errored.
   597  		pc.Status = tfe.PolicyErrored
   598  	}
   599  
   600  	return bytes.NewBuffer(logs), nil
   601  }
   602  
   603  type mockRuns struct {
   604  	client     *mockClient
   605  	runs       map[string]*tfe.Run
   606  	workspaces map[string][]*tfe.Run
   607  }
   608  
   609  func newMockRuns(client *mockClient) *mockRuns {
   610  	return &mockRuns{
   611  		client:     client,
   612  		runs:       make(map[string]*tfe.Run),
   613  		workspaces: make(map[string][]*tfe.Run),
   614  	}
   615  }
   616  
   617  func (m *mockRuns) List(ctx context.Context, workspaceID string, options tfe.RunListOptions) (*tfe.RunList, error) {
   618  	w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
   619  	if !ok {
   620  		return nil, tfe.ErrResourceNotFound
   621  	}
   622  
   623  	rl := &tfe.RunList{}
   624  	for _, r := range m.workspaces[w.ID] {
   625  		rl.Items = append(rl.Items, r)
   626  	}
   627  
   628  	rl.Pagination = &tfe.Pagination{
   629  		CurrentPage:  1,
   630  		NextPage:     1,
   631  		PreviousPage: 1,
   632  		TotalPages:   1,
   633  		TotalCount:   len(rl.Items),
   634  	}
   635  
   636  	return rl, nil
   637  }
   638  
   639  func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*tfe.Run, error) {
   640  	a, err := m.client.Applies.create(options.ConfigurationVersion.ID, options.Workspace.ID)
   641  	if err != nil {
   642  		return nil, err
   643  	}
   644  
   645  	p, err := m.client.Plans.create(options.ConfigurationVersion.ID, options.Workspace.ID)
   646  	if err != nil {
   647  		return nil, err
   648  	}
   649  
   650  	pc, err := m.client.PolicyChecks.create(options.ConfigurationVersion.ID, options.Workspace.ID)
   651  	if err != nil {
   652  		return nil, err
   653  	}
   654  
   655  	r := &tfe.Run{
   656  		ID:          generateID("run-"),
   657  		Actions:     &tfe.RunActions{IsCancelable: true},
   658  		Apply:       a,
   659  		HasChanges:  false,
   660  		Permissions: &tfe.RunPermissions{},
   661  		Plan:        p,
   662  		Status:      tfe.RunPending,
   663  	}
   664  
   665  	if pc != nil {
   666  		r.PolicyChecks = []*tfe.PolicyCheck{pc}
   667  	}
   668  
   669  	if options.IsDestroy != nil {
   670  		r.IsDestroy = *options.IsDestroy
   671  	}
   672  
   673  	w, ok := m.client.Workspaces.workspaceIDs[options.Workspace.ID]
   674  	if !ok {
   675  		return nil, tfe.ErrResourceNotFound
   676  	}
   677  	if w.CurrentRun == nil {
   678  		w.CurrentRun = r
   679  	}
   680  
   681  	m.runs[r.ID] = r
   682  	m.workspaces[options.Workspace.ID] = append(m.workspaces[options.Workspace.ID], r)
   683  
   684  	return r, nil
   685  }
   686  
   687  func (m *mockRuns) Read(ctx context.Context, runID string) (*tfe.Run, error) {
   688  	r, ok := m.runs[runID]
   689  	if !ok {
   690  		return nil, tfe.ErrResourceNotFound
   691  	}
   692  
   693  	pending := false
   694  	for _, r := range m.runs {
   695  		if r.ID != runID && r.Status == tfe.RunPending {
   696  			pending = true
   697  			break
   698  		}
   699  	}
   700  
   701  	if !pending && r.Status == tfe.RunPending {
   702  		// Only update the status if there are no other pending runs.
   703  		r.Status = tfe.RunPlanning
   704  		r.Plan.Status = tfe.PlanRunning
   705  	}
   706  
   707  	logs, _ := ioutil.ReadFile(m.client.Plans.logs[r.Plan.LogReadURL])
   708  	if r.Plan.Status == tfe.PlanFinished {
   709  		if r.IsDestroy || bytes.Contains(logs, []byte("1 to add, 0 to change, 0 to destroy")) {
   710  			r.Actions.IsCancelable = false
   711  			r.Actions.IsConfirmable = true
   712  			r.HasChanges = true
   713  			r.Permissions.CanApply = true
   714  		}
   715  
   716  		if bytes.Contains(logs, []byte("null_resource.foo: 1 error")) {
   717  			r.Actions.IsCancelable = false
   718  			r.HasChanges = false
   719  			r.Status = tfe.RunErrored
   720  		}
   721  	}
   722  
   723  	return r, nil
   724  }
   725  
   726  func (m *mockRuns) Apply(ctx context.Context, runID string, options tfe.RunApplyOptions) error {
   727  	r, ok := m.runs[runID]
   728  	if !ok {
   729  		return tfe.ErrResourceNotFound
   730  	}
   731  	if r.Status != tfe.RunPending {
   732  		// Only update the status if the run is not pending anymore.
   733  		r.Status = tfe.RunApplying
   734  		r.Apply.Status = tfe.ApplyRunning
   735  	}
   736  	return nil
   737  }
   738  
   739  func (m *mockRuns) Cancel(ctx context.Context, runID string, options tfe.RunCancelOptions) error {
   740  	panic("not implemented")
   741  }
   742  
   743  func (m *mockRuns) ForceCancel(ctx context.Context, runID string, options tfe.RunForceCancelOptions) error {
   744  	panic("not implemented")
   745  }
   746  
   747  func (m *mockRuns) Discard(ctx context.Context, runID string, options tfe.RunDiscardOptions) error {
   748  	panic("not implemented")
   749  }
   750  
   751  type mockStateVersions struct {
   752  	client        *mockClient
   753  	states        map[string][]byte
   754  	stateVersions map[string]*tfe.StateVersion
   755  	workspaces    map[string][]string
   756  }
   757  
   758  func newMockStateVersions(client *mockClient) *mockStateVersions {
   759  	return &mockStateVersions{
   760  		client:        client,
   761  		states:        make(map[string][]byte),
   762  		stateVersions: make(map[string]*tfe.StateVersion),
   763  		workspaces:    make(map[string][]string),
   764  	}
   765  }
   766  
   767  func (m *mockStateVersions) List(ctx context.Context, options tfe.StateVersionListOptions) (*tfe.StateVersionList, error) {
   768  	svl := &tfe.StateVersionList{}
   769  	for _, sv := range m.stateVersions {
   770  		svl.Items = append(svl.Items, sv)
   771  	}
   772  
   773  	svl.Pagination = &tfe.Pagination{
   774  		CurrentPage:  1,
   775  		NextPage:     1,
   776  		PreviousPage: 1,
   777  		TotalPages:   1,
   778  		TotalCount:   len(svl.Items),
   779  	}
   780  
   781  	return svl, nil
   782  }
   783  
   784  func (m *mockStateVersions) Create(ctx context.Context, workspaceID string, options tfe.StateVersionCreateOptions) (*tfe.StateVersion, error) {
   785  	id := generateID("sv-")
   786  	runID := os.Getenv("TFE_RUN_ID")
   787  	url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id)
   788  
   789  	if runID != "" && (options.Run == nil || runID != options.Run.ID) {
   790  		return nil, fmt.Errorf("option.Run.ID does not contain the ID exported by TFE_RUN_ID")
   791  	}
   792  
   793  	sv := &tfe.StateVersion{
   794  		ID:          id,
   795  		DownloadURL: url,
   796  		Serial:      *options.Serial,
   797  	}
   798  
   799  	state, err := base64.StdEncoding.DecodeString(*options.State)
   800  	if err != nil {
   801  		return nil, err
   802  	}
   803  
   804  	m.states[sv.DownloadURL] = state
   805  	m.stateVersions[sv.ID] = sv
   806  	m.workspaces[workspaceID] = append(m.workspaces[workspaceID], sv.ID)
   807  
   808  	return sv, nil
   809  }
   810  
   811  func (m *mockStateVersions) Read(ctx context.Context, svID string) (*tfe.StateVersion, error) {
   812  	sv, ok := m.stateVersions[svID]
   813  	if !ok {
   814  		return nil, tfe.ErrResourceNotFound
   815  	}
   816  	return sv, nil
   817  }
   818  
   819  func (m *mockStateVersions) Current(ctx context.Context, workspaceID string) (*tfe.StateVersion, error) {
   820  	w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
   821  	if !ok {
   822  		return nil, tfe.ErrResourceNotFound
   823  	}
   824  
   825  	svs, ok := m.workspaces[w.ID]
   826  	if !ok || len(svs) == 0 {
   827  		return nil, tfe.ErrResourceNotFound
   828  	}
   829  
   830  	sv, ok := m.stateVersions[svs[len(svs)-1]]
   831  	if !ok {
   832  		return nil, tfe.ErrResourceNotFound
   833  	}
   834  
   835  	return sv, nil
   836  }
   837  
   838  func (m *mockStateVersions) Download(ctx context.Context, url string) ([]byte, error) {
   839  	state, ok := m.states[url]
   840  	if !ok {
   841  		return nil, tfe.ErrResourceNotFound
   842  	}
   843  	return state, nil
   844  }
   845  
   846  type mockWorkspaces struct {
   847  	client         *mockClient
   848  	workspaceIDs   map[string]*tfe.Workspace
   849  	workspaceNames map[string]*tfe.Workspace
   850  }
   851  
   852  func newMockWorkspaces(client *mockClient) *mockWorkspaces {
   853  	return &mockWorkspaces{
   854  		client:         client,
   855  		workspaceIDs:   make(map[string]*tfe.Workspace),
   856  		workspaceNames: make(map[string]*tfe.Workspace),
   857  	}
   858  }
   859  
   860  func (m *mockWorkspaces) List(ctx context.Context, organization string, options tfe.WorkspaceListOptions) (*tfe.WorkspaceList, error) {
   861  	dummyWorkspaces := 10
   862  	wl := &tfe.WorkspaceList{}
   863  
   864  	// Get the prefix from the search options.
   865  	prefix := ""
   866  	if options.Search != nil {
   867  		prefix = *options.Search
   868  	}
   869  
   870  	// Get all the workspaces that match the prefix.
   871  	var ws []*tfe.Workspace
   872  	for _, w := range m.workspaceIDs {
   873  		if strings.HasPrefix(w.Name, prefix) {
   874  			ws = append(ws, w)
   875  		}
   876  	}
   877  
   878  	// Return an empty result if we have no matches.
   879  	if len(ws) == 0 {
   880  		wl.Pagination = &tfe.Pagination{
   881  			CurrentPage: 1,
   882  		}
   883  		return wl, nil
   884  	}
   885  
   886  	// Return dummy workspaces for the first page to test pagination.
   887  	if options.PageNumber <= 1 {
   888  		for i := 0; i < dummyWorkspaces; i++ {
   889  			wl.Items = append(wl.Items, &tfe.Workspace{
   890  				ID:   generateID("ws-"),
   891  				Name: fmt.Sprintf("dummy-workspace-%d", i),
   892  			})
   893  		}
   894  
   895  		wl.Pagination = &tfe.Pagination{
   896  			CurrentPage: 1,
   897  			NextPage:    2,
   898  			TotalPages:  2,
   899  			TotalCount:  len(wl.Items) + len(ws),
   900  		}
   901  
   902  		return wl, nil
   903  	}
   904  
   905  	// Return the actual workspaces that matched as the second page.
   906  	wl.Items = ws
   907  	wl.Pagination = &tfe.Pagination{
   908  		CurrentPage:  2,
   909  		PreviousPage: 1,
   910  		TotalPages:   2,
   911  		TotalCount:   len(wl.Items) + dummyWorkspaces,
   912  	}
   913  
   914  	return wl, nil
   915  }
   916  
   917  func (m *mockWorkspaces) Create(ctx context.Context, organization string, options tfe.WorkspaceCreateOptions) (*tfe.Workspace, error) {
   918  	w := &tfe.Workspace{
   919  		ID:         generateID("ws-"),
   920  		Name:       *options.Name,
   921  		Operations: !strings.HasSuffix(*options.Name, "no-operations"),
   922  		Permissions: &tfe.WorkspacePermissions{
   923  			CanQueueRun: true,
   924  			CanUpdate:   true,
   925  		},
   926  	}
   927  	if options.AutoApply != nil {
   928  		w.AutoApply = *options.AutoApply
   929  	}
   930  	if options.VCSRepo != nil {
   931  		w.VCSRepo = &tfe.VCSRepo{}
   932  	}
   933  	m.workspaceIDs[w.ID] = w
   934  	m.workspaceNames[w.Name] = w
   935  	return w, nil
   936  }
   937  
   938  func (m *mockWorkspaces) Read(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) {
   939  	w, ok := m.workspaceNames[workspace]
   940  	if !ok {
   941  		return nil, tfe.ErrResourceNotFound
   942  	}
   943  	return w, nil
   944  }
   945  
   946  func (m *mockWorkspaces) Update(ctx context.Context, organization, workspace string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) {
   947  	w, ok := m.workspaceNames[workspace]
   948  	if !ok {
   949  		return nil, tfe.ErrResourceNotFound
   950  	}
   951  
   952  	if options.Name != nil {
   953  		w.Name = *options.Name
   954  	}
   955  	if options.TerraformVersion != nil {
   956  		w.TerraformVersion = *options.TerraformVersion
   957  	}
   958  	if options.WorkingDirectory != nil {
   959  		w.WorkingDirectory = *options.WorkingDirectory
   960  	}
   961  
   962  	delete(m.workspaceNames, workspace)
   963  	m.workspaceNames[w.Name] = w
   964  
   965  	return w, nil
   966  }
   967  
   968  func (m *mockWorkspaces) Delete(ctx context.Context, organization, workspace string) error {
   969  	if w, ok := m.workspaceNames[workspace]; ok {
   970  		delete(m.workspaceIDs, w.ID)
   971  	}
   972  	delete(m.workspaceNames, workspace)
   973  	return nil
   974  }
   975  
   976  func (m *mockWorkspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) {
   977  	w, ok := m.workspaceIDs[workspaceID]
   978  	if !ok {
   979  		return nil, tfe.ErrResourceNotFound
   980  	}
   981  	if w.Locked {
   982  		return nil, tfe.ErrWorkspaceLocked
   983  	}
   984  	w.Locked = true
   985  	return w, nil
   986  }
   987  
   988  func (m *mockWorkspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
   989  	w, ok := m.workspaceIDs[workspaceID]
   990  	if !ok {
   991  		return nil, tfe.ErrResourceNotFound
   992  	}
   993  	if !w.Locked {
   994  		return nil, tfe.ErrWorkspaceNotLocked
   995  	}
   996  	w.Locked = false
   997  	return w, nil
   998  }
   999  
  1000  func (m *mockWorkspaces) ForceUnlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
  1001  	w, ok := m.workspaceIDs[workspaceID]
  1002  	if !ok {
  1003  		return nil, tfe.ErrResourceNotFound
  1004  	}
  1005  	if !w.Locked {
  1006  		return nil, tfe.ErrWorkspaceNotLocked
  1007  	}
  1008  	w.Locked = false
  1009  	return w, nil
  1010  }
  1011  
  1012  func (m *mockWorkspaces) AssignSSHKey(ctx context.Context, workspaceID string, options tfe.WorkspaceAssignSSHKeyOptions) (*tfe.Workspace, error) {
  1013  	panic("not implemented")
  1014  }
  1015  
  1016  func (m *mockWorkspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
  1017  	panic("not implemented")
  1018  }
  1019  
  1020  const alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
  1021  
  1022  func generateID(s string) string {
  1023  	b := make([]byte, 16)
  1024  	for i := range b {
  1025  		b[i] = alphanumeric[rand.Intn(len(alphanumeric))]
  1026  	}
  1027  	return s + string(b)
  1028  }