From 471dd479e8296878378750991c865e2503aa490c Mon Sep 17 00:00:00 2001 From: Chris Arcand Date: Tue, 7 Sep 2021 15:35:32 -0500 Subject: [PATCH] Update go-tfe to 26689e These changes include additions to fulfill the interface for the client mock, plus moving all that logic (which needn't be duplicated across both the remote and cloud packages) over to the cloud package under a dedicated mock client file. --- go.mod | 6 +- go.sum | 15 +- internal/backend/remote/backend_apply_test.go | 25 +- internal/backend/remote/backend_mock.go | 1364 ----------------- internal/backend/remote/backend_plan_test.go | 29 +- internal/backend/remote/backend_state_test.go | 3 +- internal/backend/remote/testing.go | 24 +- internal/cloud/backend_apply_test.go | 24 +- internal/cloud/backend_plan_test.go | 28 +- internal/cloud/backend_state_test.go | 2 +- internal/cloud/testing.go | 23 +- .../{backend_mock.go => tfe_client_mock.go} | 359 ++--- 12 files changed, 300 insertions(+), 1602 deletions(-) delete mode 100644 internal/backend/remote/backend_mock.go rename internal/cloud/{backend_mock.go => tfe_client_mock.go} (75%) diff --git a/go.mod b/go.mod index 646309e2a..9074b31d8 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-plugin v1.4.3 github.com/hashicorp/go-retryablehttp v0.5.2 - github.com/hashicorp/go-tfe v0.15.0 + github.com/hashicorp/go-tfe v0.18.1-0.20210902165242-26689edbfddf github.com/hashicorp/go-uuid v1.0.1 github.com/hashicorp/go-version v1.2.1 github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f @@ -143,9 +143,9 @@ require ( github.com/hashicorp/go-msgpack v0.5.4 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect - github.com/hashicorp/go-slug v0.4.1 // indirect + github.com/hashicorp/go-slug v0.7.0 // indirect github.com/hashicorp/golang-lru v0.5.1 // indirect - github.com/hashicorp/jsonapi v0.0.0-20210518035559-1e50d74c8db3 // indirect + github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d // indirect github.com/hashicorp/serf v0.9.5 // indirect github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect github.com/huandu/xstrings v1.3.2 // indirect diff --git a/go.sum b/go.sum index bd8b487fb..4a2064597 100644 --- a/go.sum +++ b/go.sum @@ -370,13 +370,13 @@ github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5O github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= -github.com/hashicorp/go-slug v0.4.1 h1:/jAo8dNuLgSImoLXaX7Od7QB4TfYCVPam+OpAt5bZqc= -github.com/hashicorp/go-slug v0.4.1/go.mod h1:I5tq5Lv0E2xcNXNkmx7BSfzi1PsJ2cNjs3cC3LwyhK8= +github.com/hashicorp/go-slug v0.7.0 h1:8HIi6oreWPtnhpYd8lIGQBgp4rXzDWQTOhfILZm+nok= +github.com/hashicorp/go-slug v0.7.0/go.mod h1:Ib+IWBYfEfJGI1ZyXMGNbu2BU+aa3Dzu41RKLH301v4= github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-tfe v0.15.0 h1:vdnz1NjOhvmap+cj8iPsL8SbS4iFFVuNYFkGpF5SdoA= -github.com/hashicorp/go-tfe v0.15.0/go.mod h1:c8glB5p6XzocEWLNkuy5RxcjqN5X2PpY6NF3f2W6nIo= +github.com/hashicorp/go-tfe v0.18.1-0.20210902165242-26689edbfddf h1:Tn5cI9kacNyO40ztxmwfAaHrOGd7dELLSAueV2Xfv38= +github.com/hashicorp/go-tfe v0.18.1-0.20210902165242-26689edbfddf/go.mod h1:7lChm1Mjsh0ofrUNkP8MHljUFrnKNZNTw36S6qSbJZU= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -393,8 +393,8 @@ github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90= github.com/hashicorp/hcl/v2 v2.10.1 h1:h4Xx4fsrRE26ohAk/1iGF/JBqRQbyUqu5Lvj60U54ys= github.com/hashicorp/hcl/v2 v2.10.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= -github.com/hashicorp/jsonapi v0.0.0-20210518035559-1e50d74c8db3 h1:mzwkutymYIXR5oQT9YnfbLuuw7LZmksiHKRPUTN5ijo= -github.com/hashicorp/jsonapi v0.0.0-20210518035559-1e50d74c8db3/go.mod h1:Yog5+CPEM3c99L1CL2CFCYoSzgWm5vTU58idbRUaLik= +github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d h1:9ARUJJ1VVynB176G1HCwleORqCaXm/Vx0uUi0dL26I0= +github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d/go.mod h1:Yog5+CPEM3c99L1CL2CFCYoSzgWm5vTU58idbRUaLik= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/memberlist v0.2.2 h1:5+RffWKwqJ71YPu9mWsF7ZOscZmwfasdA8kbdC7AO2g= @@ -610,8 +610,9 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.194/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.232 h1:kwsWbh4rEw42ZDe9/812ebhbwNZxlQyZ2sTmxBOKhN4= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.232/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= diff --git a/internal/backend/remote/backend_apply_test.go b/internal/backend/remote/backend_apply_test.go index e15fe0bb3..9b4286010 100644 --- a/internal/backend/remote/backend_apply_test.go +++ b/internal/backend/remote/backend_apply_test.go @@ -14,6 +14,7 @@ import ( version "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/cloud" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" @@ -308,11 +309,11 @@ func TestRemote_applyWithoutRefresh(t *testing.T) { // We should find a run inside the mock client that has refresh set // to false. - runsAPI := b.client.Runs.(*mockRuns) - if got, want := len(runsAPI.runs), 1; got != want { + runsAPI := b.client.Runs.(*cloud.MockRuns) + if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } - for _, run := range runsAPI.runs { + for _, run := range runsAPI.Runs { if diff := cmp.Diff(false, run.Refresh); diff != "" { t.Errorf("wrong Refresh setting in the created run\n%s", diff) } @@ -377,11 +378,11 @@ func TestRemote_applyWithRefreshOnly(t *testing.T) { // We should find a run inside the mock client that has refresh-only set // to true. - runsAPI := b.client.Runs.(*mockRuns) - if got, want := len(runsAPI.runs), 1; got != want { + runsAPI := b.client.Runs.(*cloud.MockRuns) + if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } - for _, run := range runsAPI.runs { + for _, run := range runsAPI.Runs { if diff := cmp.Diff(true, run.RefreshOnly); diff != "" { t.Errorf("wrong RefreshOnly setting in the created run\n%s", diff) } @@ -448,11 +449,11 @@ func TestRemote_applyWithTarget(t *testing.T) { // We should find a run inside the mock client that has the same // target address we requested above. - runsAPI := b.client.Runs.(*mockRuns) - if got, want := len(runsAPI.runs), 1; got != want { + runsAPI := b.client.Runs.(*cloud.MockRuns) + if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } - for _, run := range runsAPI.runs { + for _, run := range runsAPI.Runs { if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" { t.Errorf("wrong TargetAddrs in the created run\n%s", diff) } @@ -523,11 +524,11 @@ func TestRemote_applyWithReplace(t *testing.T) { // We should find a run inside the mock client that has the same // refresh address we requested above. - runsAPI := b.client.Runs.(*mockRuns) - if got, want := len(runsAPI.runs), 1; got != want { + runsAPI := b.client.Runs.(*cloud.MockRuns) + if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } - for _, run := range runsAPI.runs { + for _, run := range runsAPI.Runs { if diff := cmp.Diff([]string{"null_resource.foo"}, run.ReplaceAddrs); diff != "" { t.Errorf("wrong ReplaceAddrs in the created run\n%s", diff) } diff --git a/internal/backend/remote/backend_mock.go b/internal/backend/remote/backend_mock.go deleted file mode 100644 index abf150f7c..000000000 --- a/internal/backend/remote/backend_mock.go +++ /dev/null @@ -1,1364 +0,0 @@ -package remote - -import ( - "bytes" - "context" - "encoding/base64" - "errors" - "fmt" - "io" - "io/ioutil" - "math/rand" - "os" - "path/filepath" - "strings" - "sync" - "time" - - tfe "github.com/hashicorp/go-tfe" - "github.com/hashicorp/terraform/internal/terraform" - tfversion "github.com/hashicorp/terraform/version" - "github.com/mitchellh/copystructure" -) - -type mockClient struct { - Applies *mockApplies - ConfigurationVersions *mockConfigurationVersions - CostEstimates *mockCostEstimates - Organizations *mockOrganizations - Plans *mockPlans - PolicyChecks *mockPolicyChecks - Runs *mockRuns - StateVersions *mockStateVersions - Variables *mockVariables - Workspaces *mockWorkspaces -} - -func newMockClient() *mockClient { - c := &mockClient{} - c.Applies = newMockApplies(c) - c.ConfigurationVersions = newMockConfigurationVersions(c) - c.CostEstimates = newMockCostEstimates(c) - c.Organizations = newMockOrganizations(c) - c.Plans = newMockPlans(c) - c.PolicyChecks = newMockPolicyChecks(c) - c.Runs = newMockRuns(c) - c.StateVersions = newMockStateVersions(c) - c.Variables = newMockVariables(c) - c.Workspaces = newMockWorkspaces(c) - return c -} - -type mockApplies struct { - client *mockClient - applies map[string]*tfe.Apply - logs map[string]string -} - -func newMockApplies(client *mockClient) *mockApplies { - return &mockApplies{ - client: client, - applies: make(map[string]*tfe.Apply), - logs: make(map[string]string), - } -} - -// create is a helper function to create a mock apply that uses the configured -// working directory to find the logfile. -func (m *mockApplies) create(cvID, workspaceID string) (*tfe.Apply, error) { - c, ok := m.client.ConfigurationVersions.configVersions[cvID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - if c.Speculative { - // Speculative means its plan-only so we don't create a Apply. - return nil, nil - } - - id := generateID("apply-") - url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) - - a := &tfe.Apply{ - ID: id, - LogReadURL: url, - Status: tfe.ApplyPending, - } - - w, ok := m.client.Workspaces.workspaceIDs[workspaceID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - if w.AutoApply { - a.Status = tfe.ApplyRunning - } - - m.logs[url] = filepath.Join( - m.client.ConfigurationVersions.uploadPaths[cvID], - w.WorkingDirectory, - "apply.log", - ) - m.applies[a.ID] = a - - return a, nil -} - -func (m *mockApplies) Read(ctx context.Context, applyID string) (*tfe.Apply, error) { - a, ok := m.applies[applyID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - // Together with the mockLogReader this allows testing queued runs. - if a.Status == tfe.ApplyRunning { - a.Status = tfe.ApplyFinished - } - return a, nil -} - -func (m *mockApplies) Logs(ctx context.Context, applyID string) (io.Reader, error) { - a, err := m.Read(ctx, applyID) - if err != nil { - return nil, err - } - - logfile, ok := m.logs[a.LogReadURL] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - if _, err := os.Stat(logfile); os.IsNotExist(err) { - return bytes.NewBufferString("logfile does not exist"), nil - } - - logs, err := ioutil.ReadFile(logfile) - if err != nil { - return nil, err - } - - done := func() (bool, error) { - a, err := m.Read(ctx, applyID) - if err != nil { - return false, err - } - if a.Status != tfe.ApplyFinished { - return false, nil - } - return true, nil - } - - return &mockLogReader{ - done: done, - logs: bytes.NewBuffer(logs), - }, nil -} - -type mockConfigurationVersions struct { - client *mockClient - configVersions map[string]*tfe.ConfigurationVersion - uploadPaths map[string]string - uploadURLs map[string]*tfe.ConfigurationVersion -} - -func newMockConfigurationVersions(client *mockClient) *mockConfigurationVersions { - return &mockConfigurationVersions{ - client: client, - configVersions: make(map[string]*tfe.ConfigurationVersion), - uploadPaths: make(map[string]string), - uploadURLs: make(map[string]*tfe.ConfigurationVersion), - } -} - -func (m *mockConfigurationVersions) List(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionListOptions) (*tfe.ConfigurationVersionList, error) { - cvl := &tfe.ConfigurationVersionList{} - for _, cv := range m.configVersions { - cvl.Items = append(cvl.Items, cv) - } - - cvl.Pagination = &tfe.Pagination{ - CurrentPage: 1, - NextPage: 1, - PreviousPage: 1, - TotalPages: 1, - TotalCount: len(cvl.Items), - } - - return cvl, nil -} - -func (m *mockConfigurationVersions) Create(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionCreateOptions) (*tfe.ConfigurationVersion, error) { - id := generateID("cv-") - url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) - - cv := &tfe.ConfigurationVersion{ - ID: id, - Status: tfe.ConfigurationPending, - UploadURL: url, - } - - m.configVersions[cv.ID] = cv - m.uploadURLs[url] = cv - - return cv, nil -} - -func (m *mockConfigurationVersions) Read(ctx context.Context, cvID string) (*tfe.ConfigurationVersion, error) { - cv, ok := m.configVersions[cvID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - return cv, nil -} - -func (m *mockConfigurationVersions) Upload(ctx context.Context, url, path string) error { - cv, ok := m.uploadURLs[url] - if !ok { - return errors.New("404 not found") - } - m.uploadPaths[cv.ID] = path - cv.Status = tfe.ConfigurationUploaded - return nil -} - -type mockCostEstimates struct { - client *mockClient - estimations map[string]*tfe.CostEstimate - logs map[string]string -} - -func newMockCostEstimates(client *mockClient) *mockCostEstimates { - return &mockCostEstimates{ - client: client, - estimations: make(map[string]*tfe.CostEstimate), - logs: make(map[string]string), - } -} - -// create is a helper function to create a mock cost estimation that uses the -// configured working directory to find the logfile. -func (m *mockCostEstimates) create(cvID, workspaceID string) (*tfe.CostEstimate, error) { - id := generateID("ce-") - - ce := &tfe.CostEstimate{ - ID: id, - MatchedResourcesCount: 1, - ResourcesCount: 1, - DeltaMonthlyCost: "0.00", - ProposedMonthlyCost: "0.00", - Status: tfe.CostEstimateFinished, - } - - w, ok := m.client.Workspaces.workspaceIDs[workspaceID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - logfile := filepath.Join( - m.client.ConfigurationVersions.uploadPaths[cvID], - w.WorkingDirectory, - "cost-estimate.log", - ) - - if _, err := os.Stat(logfile); os.IsNotExist(err) { - return nil, nil - } - - m.logs[ce.ID] = logfile - m.estimations[ce.ID] = ce - - return ce, nil -} - -func (m *mockCostEstimates) Read(ctx context.Context, costEstimateID string) (*tfe.CostEstimate, error) { - ce, ok := m.estimations[costEstimateID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - return ce, nil -} - -func (m *mockCostEstimates) Logs(ctx context.Context, costEstimateID string) (io.Reader, error) { - ce, ok := m.estimations[costEstimateID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - logfile, ok := m.logs[ce.ID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - if _, err := os.Stat(logfile); os.IsNotExist(err) { - return bytes.NewBufferString("logfile does not exist"), nil - } - - logs, err := ioutil.ReadFile(logfile) - if err != nil { - return nil, err - } - - ce.Status = tfe.CostEstimateFinished - - return bytes.NewBuffer(logs), nil -} - -// mockInput is a mock implementation of terraform.UIInput. -type mockInput struct { - answers map[string]string -} - -func (m *mockInput) Input(ctx context.Context, opts *terraform.InputOpts) (string, error) { - v, ok := m.answers[opts.Id] - if !ok { - return "", fmt.Errorf("unexpected input request in test: %s", opts.Id) - } - if v == "wait-for-external-update" { - select { - case <-ctx.Done(): - case <-time.After(time.Minute): - } - } - delete(m.answers, opts.Id) - return v, nil -} - -type mockOrganizations struct { - client *mockClient - organizations map[string]*tfe.Organization -} - -func newMockOrganizations(client *mockClient) *mockOrganizations { - return &mockOrganizations{ - client: client, - organizations: make(map[string]*tfe.Organization), - } -} - -func (m *mockOrganizations) List(ctx context.Context, options tfe.OrganizationListOptions) (*tfe.OrganizationList, error) { - orgl := &tfe.OrganizationList{} - for _, org := range m.organizations { - orgl.Items = append(orgl.Items, org) - } - - orgl.Pagination = &tfe.Pagination{ - CurrentPage: 1, - NextPage: 1, - PreviousPage: 1, - TotalPages: 1, - TotalCount: len(orgl.Items), - } - - return orgl, nil -} - -// mockLogReader is a mock logreader that enables testing queued runs. -type mockLogReader struct { - done func() (bool, error) - logs *bytes.Buffer -} - -func (m *mockLogReader) Read(l []byte) (int, error) { - for { - if written, err := m.read(l); err != io.ErrNoProgress { - return written, err - } - time.Sleep(1 * time.Millisecond) - } -} - -func (m *mockLogReader) read(l []byte) (int, error) { - done, err := m.done() - if err != nil { - return 0, err - } - if !done { - return 0, io.ErrNoProgress - } - return m.logs.Read(l) -} - -func (m *mockOrganizations) Create(ctx context.Context, options tfe.OrganizationCreateOptions) (*tfe.Organization, error) { - org := &tfe.Organization{Name: *options.Name} - m.organizations[org.Name] = org - return org, nil -} - -func (m *mockOrganizations) Read(ctx context.Context, name string) (*tfe.Organization, error) { - org, ok := m.organizations[name] - if !ok { - return nil, tfe.ErrResourceNotFound - } - return org, nil -} - -func (m *mockOrganizations) Update(ctx context.Context, name string, options tfe.OrganizationUpdateOptions) (*tfe.Organization, error) { - org, ok := m.organizations[name] - if !ok { - return nil, tfe.ErrResourceNotFound - } - org.Name = *options.Name - return org, nil - -} - -func (m *mockOrganizations) Delete(ctx context.Context, name string) error { - delete(m.organizations, name) - return nil -} - -func (m *mockOrganizations) Capacity(ctx context.Context, name string) (*tfe.Capacity, error) { - var pending, running int - for _, r := range m.client.Runs.runs { - if r.Status == tfe.RunPending { - pending++ - continue - } - running++ - } - return &tfe.Capacity{Pending: pending, Running: running}, nil -} - -func (m *mockOrganizations) Entitlements(ctx context.Context, name string) (*tfe.Entitlements, error) { - return &tfe.Entitlements{ - Operations: true, - PrivateModuleRegistry: true, - Sentinel: true, - StateStorage: true, - Teams: true, - VCSIntegrations: true, - }, nil -} - -func (m *mockOrganizations) RunQueue(ctx context.Context, name string, options tfe.RunQueueOptions) (*tfe.RunQueue, error) { - rq := &tfe.RunQueue{} - - for _, r := range m.client.Runs.runs { - rq.Items = append(rq.Items, r) - } - - rq.Pagination = &tfe.Pagination{ - CurrentPage: 1, - NextPage: 1, - PreviousPage: 1, - TotalPages: 1, - TotalCount: len(rq.Items), - } - - return rq, nil -} - -type mockPlans struct { - client *mockClient - logs map[string]string - planOutputs map[string]string - plans map[string]*tfe.Plan -} - -func newMockPlans(client *mockClient) *mockPlans { - return &mockPlans{ - client: client, - logs: make(map[string]string), - planOutputs: make(map[string]string), - plans: make(map[string]*tfe.Plan), - } -} - -// create is a helper function to create a mock plan that uses the configured -// working directory to find the logfile. -func (m *mockPlans) create(cvID, workspaceID string) (*tfe.Plan, error) { - id := generateID("plan-") - url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) - - p := &tfe.Plan{ - ID: id, - LogReadURL: url, - Status: tfe.PlanPending, - } - - w, ok := m.client.Workspaces.workspaceIDs[workspaceID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - m.logs[url] = filepath.Join( - m.client.ConfigurationVersions.uploadPaths[cvID], - w.WorkingDirectory, - "plan.log", - ) - m.plans[p.ID] = p - - return p, nil -} - -func (m *mockPlans) Read(ctx context.Context, planID string) (*tfe.Plan, error) { - p, ok := m.plans[planID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - // Together with the mockLogReader this allows testing queued runs. - if p.Status == tfe.PlanRunning { - p.Status = tfe.PlanFinished - } - return p, nil -} - -func (m *mockPlans) Logs(ctx context.Context, planID string) (io.Reader, error) { - p, err := m.Read(ctx, planID) - if err != nil { - return nil, err - } - - logfile, ok := m.logs[p.LogReadURL] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - if _, err := os.Stat(logfile); os.IsNotExist(err) { - return bytes.NewBufferString("logfile does not exist"), nil - } - - logs, err := ioutil.ReadFile(logfile) - if err != nil { - return nil, err - } - - done := func() (bool, error) { - p, err := m.Read(ctx, planID) - if err != nil { - return false, err - } - if p.Status != tfe.PlanFinished { - return false, nil - } - return true, nil - } - - return &mockLogReader{ - done: done, - logs: bytes.NewBuffer(logs), - }, nil -} - -func (m *mockPlans) JSONOutput(ctx context.Context, planID string) ([]byte, error) { - planOutput, ok := m.planOutputs[planID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - return []byte(planOutput), nil -} - -type mockPolicyChecks struct { - client *mockClient - checks map[string]*tfe.PolicyCheck - logs map[string]string -} - -func newMockPolicyChecks(client *mockClient) *mockPolicyChecks { - return &mockPolicyChecks{ - client: client, - checks: make(map[string]*tfe.PolicyCheck), - logs: make(map[string]string), - } -} - -// create is a helper function to create a mock policy check that uses the -// configured working directory to find the logfile. -func (m *mockPolicyChecks) create(cvID, workspaceID string) (*tfe.PolicyCheck, error) { - id := generateID("pc-") - - pc := &tfe.PolicyCheck{ - ID: id, - Actions: &tfe.PolicyActions{}, - Permissions: &tfe.PolicyPermissions{}, - Scope: tfe.PolicyScopeOrganization, - Status: tfe.PolicyPending, - } - - w, ok := m.client.Workspaces.workspaceIDs[workspaceID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - logfile := filepath.Join( - m.client.ConfigurationVersions.uploadPaths[cvID], - w.WorkingDirectory, - "policy.log", - ) - - if _, err := os.Stat(logfile); os.IsNotExist(err) { - return nil, nil - } - - m.logs[pc.ID] = logfile - m.checks[pc.ID] = pc - - return pc, nil -} - -func (m *mockPolicyChecks) List(ctx context.Context, runID string, options tfe.PolicyCheckListOptions) (*tfe.PolicyCheckList, error) { - _, ok := m.client.Runs.runs[runID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - pcl := &tfe.PolicyCheckList{} - for _, pc := range m.checks { - pcl.Items = append(pcl.Items, pc) - } - - pcl.Pagination = &tfe.Pagination{ - CurrentPage: 1, - NextPage: 1, - PreviousPage: 1, - TotalPages: 1, - TotalCount: len(pcl.Items), - } - - return pcl, nil -} - -func (m *mockPolicyChecks) Read(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) { - pc, ok := m.checks[policyCheckID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - logfile, ok := m.logs[pc.ID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - if _, err := os.Stat(logfile); os.IsNotExist(err) { - return nil, fmt.Errorf("logfile does not exist") - } - - logs, err := ioutil.ReadFile(logfile) - if err != nil { - return nil, err - } - - switch { - case bytes.Contains(logs, []byte("Sentinel Result: true")): - pc.Status = tfe.PolicyPasses - case bytes.Contains(logs, []byte("Sentinel Result: false")): - switch { - case bytes.Contains(logs, []byte("hard-mandatory")): - pc.Status = tfe.PolicyHardFailed - case bytes.Contains(logs, []byte("soft-mandatory")): - pc.Actions.IsOverridable = true - pc.Permissions.CanOverride = true - pc.Status = tfe.PolicySoftFailed - } - default: - // As this is an unexpected state, we say the policy errored. - pc.Status = tfe.PolicyErrored - } - - return pc, nil -} - -func (m *mockPolicyChecks) Override(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) { - pc, ok := m.checks[policyCheckID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - pc.Status = tfe.PolicyOverridden - return pc, nil -} - -func (m *mockPolicyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reader, error) { - pc, ok := m.checks[policyCheckID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - logfile, ok := m.logs[pc.ID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - if _, err := os.Stat(logfile); os.IsNotExist(err) { - return bytes.NewBufferString("logfile does not exist"), nil - } - - logs, err := ioutil.ReadFile(logfile) - if err != nil { - return nil, err - } - - switch { - case bytes.Contains(logs, []byte("Sentinel Result: true")): - pc.Status = tfe.PolicyPasses - case bytes.Contains(logs, []byte("Sentinel Result: false")): - switch { - case bytes.Contains(logs, []byte("hard-mandatory")): - pc.Status = tfe.PolicyHardFailed - case bytes.Contains(logs, []byte("soft-mandatory")): - pc.Actions.IsOverridable = true - pc.Permissions.CanOverride = true - pc.Status = tfe.PolicySoftFailed - } - default: - // As this is an unexpected state, we say the policy errored. - pc.Status = tfe.PolicyErrored - } - - return bytes.NewBuffer(logs), nil -} - -type mockRuns struct { - sync.Mutex - - client *mockClient - runs map[string]*tfe.Run - workspaces map[string][]*tfe.Run - - // If modifyNewRun is non-nil, the create method will call it just before - // saving a new run in the runs map, so that a calling test can mimic - // side-effects that a real server might apply in certain situations. - modifyNewRun func(client *mockClient, options tfe.RunCreateOptions, run *tfe.Run) -} - -func newMockRuns(client *mockClient) *mockRuns { - return &mockRuns{ - client: client, - runs: make(map[string]*tfe.Run), - workspaces: make(map[string][]*tfe.Run), - } -} - -func (m *mockRuns) List(ctx context.Context, workspaceID string, options tfe.RunListOptions) (*tfe.RunList, error) { - m.Lock() - defer m.Unlock() - - w, ok := m.client.Workspaces.workspaceIDs[workspaceID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - rl := &tfe.RunList{} - for _, run := range m.workspaces[w.ID] { - rc, err := copystructure.Copy(run) - if err != nil { - panic(err) - } - rl.Items = append(rl.Items, rc.(*tfe.Run)) - } - - rl.Pagination = &tfe.Pagination{ - CurrentPage: 1, - NextPage: 1, - PreviousPage: 1, - TotalPages: 1, - TotalCount: len(rl.Items), - } - - return rl, nil -} - -func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*tfe.Run, error) { - m.Lock() - defer m.Unlock() - - a, err := m.client.Applies.create(options.ConfigurationVersion.ID, options.Workspace.ID) - if err != nil { - return nil, err - } - - ce, err := m.client.CostEstimates.create(options.ConfigurationVersion.ID, options.Workspace.ID) - if err != nil { - return nil, err - } - - p, err := m.client.Plans.create(options.ConfigurationVersion.ID, options.Workspace.ID) - if err != nil { - return nil, err - } - - pc, err := m.client.PolicyChecks.create(options.ConfigurationVersion.ID, options.Workspace.ID) - if err != nil { - return nil, err - } - - r := &tfe.Run{ - ID: generateID("run-"), - Actions: &tfe.RunActions{IsCancelable: true}, - Apply: a, - CostEstimate: ce, - HasChanges: false, - Permissions: &tfe.RunPermissions{}, - Plan: p, - ReplaceAddrs: options.ReplaceAddrs, - Status: tfe.RunPending, - TargetAddrs: options.TargetAddrs, - } - - if options.Message != nil { - r.Message = *options.Message - } - - if pc != nil { - r.PolicyChecks = []*tfe.PolicyCheck{pc} - } - - if options.IsDestroy != nil { - r.IsDestroy = *options.IsDestroy - } - - if options.Refresh != nil { - r.Refresh = *options.Refresh - } - - if options.RefreshOnly != nil { - r.RefreshOnly = *options.RefreshOnly - } - - w, ok := m.client.Workspaces.workspaceIDs[options.Workspace.ID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - if w.CurrentRun == nil { - w.CurrentRun = r - } - - if m.modifyNewRun != nil { - // caller-provided callback may modify the run in-place to mimic - // side-effects that a real server might take in some situations. - m.modifyNewRun(m.client, options, r) - } - - m.runs[r.ID] = r - m.workspaces[options.Workspace.ID] = append(m.workspaces[options.Workspace.ID], r) - - return r, nil -} - -func (m *mockRuns) Read(ctx context.Context, runID string) (*tfe.Run, error) { - return m.ReadWithOptions(ctx, runID, nil) -} - -func (m *mockRuns) ReadWithOptions(ctx context.Context, runID string, _ *tfe.RunReadOptions) (*tfe.Run, error) { - m.Lock() - defer m.Unlock() - - r, ok := m.runs[runID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - pending := false - for _, r := range m.runs { - if r.ID != runID && r.Status == tfe.RunPending { - pending = true - break - } - } - - if !pending && r.Status == tfe.RunPending { - // Only update the status if there are no other pending runs. - r.Status = tfe.RunPlanning - r.Plan.Status = tfe.PlanRunning - } - - logs, _ := ioutil.ReadFile(m.client.Plans.logs[r.Plan.LogReadURL]) - if r.Status == tfe.RunPlanning && r.Plan.Status == tfe.PlanFinished { - if r.IsDestroy || bytes.Contains(logs, []byte("1 to add, 0 to change, 0 to destroy")) { - r.Actions.IsCancelable = false - r.Actions.IsConfirmable = true - r.HasChanges = true - r.Permissions.CanApply = true - } - - if bytes.Contains(logs, []byte("null_resource.foo: 1 error")) { - r.Actions.IsCancelable = false - r.HasChanges = false - r.Status = tfe.RunErrored - } - } - - // we must return a copy for the client - rc, err := copystructure.Copy(r) - if err != nil { - panic(err) - } - - return rc.(*tfe.Run), nil -} - -func (m *mockRuns) Apply(ctx context.Context, runID string, options tfe.RunApplyOptions) error { - m.Lock() - defer m.Unlock() - - r, ok := m.runs[runID] - if !ok { - return tfe.ErrResourceNotFound - } - if r.Status != tfe.RunPending { - // Only update the status if the run is not pending anymore. - r.Status = tfe.RunApplying - r.Actions.IsConfirmable = false - r.Apply.Status = tfe.ApplyRunning - } - return nil -} - -func (m *mockRuns) Cancel(ctx context.Context, runID string, options tfe.RunCancelOptions) error { - panic("not implemented") -} - -func (m *mockRuns) ForceCancel(ctx context.Context, runID string, options tfe.RunForceCancelOptions) error { - panic("not implemented") -} - -func (m *mockRuns) Discard(ctx context.Context, runID string, options tfe.RunDiscardOptions) error { - m.Lock() - defer m.Unlock() - - r, ok := m.runs[runID] - if !ok { - return tfe.ErrResourceNotFound - } - r.Status = tfe.RunDiscarded - r.Actions.IsConfirmable = false - return nil -} - -type mockStateVersions struct { - client *mockClient - states map[string][]byte - stateVersions map[string]*tfe.StateVersion - workspaces map[string][]string -} - -func newMockStateVersions(client *mockClient) *mockStateVersions { - return &mockStateVersions{ - client: client, - states: make(map[string][]byte), - stateVersions: make(map[string]*tfe.StateVersion), - workspaces: make(map[string][]string), - } -} - -func (m *mockStateVersions) List(ctx context.Context, options tfe.StateVersionListOptions) (*tfe.StateVersionList, error) { - svl := &tfe.StateVersionList{} - for _, sv := range m.stateVersions { - svl.Items = append(svl.Items, sv) - } - - svl.Pagination = &tfe.Pagination{ - CurrentPage: 1, - NextPage: 1, - PreviousPage: 1, - TotalPages: 1, - TotalCount: len(svl.Items), - } - - return svl, nil -} - -func (m *mockStateVersions) Create(ctx context.Context, workspaceID string, options tfe.StateVersionCreateOptions) (*tfe.StateVersion, error) { - id := generateID("sv-") - runID := os.Getenv("TFE_RUN_ID") - url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) - - if runID != "" && (options.Run == nil || runID != options.Run.ID) { - return nil, fmt.Errorf("option.Run.ID does not contain the ID exported by TFE_RUN_ID") - } - - sv := &tfe.StateVersion{ - ID: id, - DownloadURL: url, - Serial: *options.Serial, - } - - state, err := base64.StdEncoding.DecodeString(*options.State) - if err != nil { - return nil, err - } - - m.states[sv.DownloadURL] = state - m.stateVersions[sv.ID] = sv - m.workspaces[workspaceID] = append(m.workspaces[workspaceID], sv.ID) - - return sv, nil -} - -func (m *mockStateVersions) Read(ctx context.Context, svID string) (*tfe.StateVersion, error) { - return m.ReadWithOptions(ctx, svID, nil) -} - -func (m *mockStateVersions) ReadWithOptions(ctx context.Context, svID string, options *tfe.StateVersionReadOptions) (*tfe.StateVersion, error) { - sv, ok := m.stateVersions[svID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - return sv, nil -} - -func (m *mockStateVersions) Current(ctx context.Context, workspaceID string) (*tfe.StateVersion, error) { - return m.CurrentWithOptions(ctx, workspaceID, nil) -} - -func (m *mockStateVersions) CurrentWithOptions(ctx context.Context, workspaceID string, options *tfe.StateVersionCurrentOptions) (*tfe.StateVersion, error) { - w, ok := m.client.Workspaces.workspaceIDs[workspaceID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - svs, ok := m.workspaces[w.ID] - if !ok || len(svs) == 0 { - return nil, tfe.ErrResourceNotFound - } - - sv, ok := m.stateVersions[svs[len(svs)-1]] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - return sv, nil -} - -func (m *mockStateVersions) Download(ctx context.Context, url string) ([]byte, error) { - state, ok := m.states[url] - if !ok { - return nil, tfe.ErrResourceNotFound - } - return state, nil -} - -type mockVariables struct { - client *mockClient - workspaces map[string]*tfe.VariableList -} - -var _ tfe.Variables = (*mockVariables)(nil) - -func newMockVariables(client *mockClient) *mockVariables { - return &mockVariables{ - client: client, - workspaces: make(map[string]*tfe.VariableList), - } -} - -func (m *mockVariables) List(ctx context.Context, workspaceID string, options tfe.VariableListOptions) (*tfe.VariableList, error) { - vl := m.workspaces[workspaceID] - return vl, nil -} - -func (m *mockVariables) Create(ctx context.Context, workspaceID string, options tfe.VariableCreateOptions) (*tfe.Variable, error) { - v := &tfe.Variable{ - ID: generateID("var-"), - Key: *options.Key, - Category: *options.Category, - } - if options.Value != nil { - v.Value = *options.Value - } - if options.HCL != nil { - v.HCL = *options.HCL - } - if options.Sensitive != nil { - v.Sensitive = *options.Sensitive - } - - workspace := workspaceID - - if m.workspaces[workspace] == nil { - m.workspaces[workspace] = &tfe.VariableList{} - } - - vl := m.workspaces[workspace] - vl.Items = append(vl.Items, v) - - return v, nil -} - -func (m *mockVariables) Read(ctx context.Context, workspaceID string, variableID string) (*tfe.Variable, error) { - panic("not implemented") -} - -func (m *mockVariables) Update(ctx context.Context, workspaceID string, variableID string, options tfe.VariableUpdateOptions) (*tfe.Variable, error) { - panic("not implemented") -} - -func (m *mockVariables) Delete(ctx context.Context, workspaceID string, variableID string) error { - panic("not implemented") -} - -type mockWorkspaces struct { - client *mockClient - workspaceIDs map[string]*tfe.Workspace - workspaceNames map[string]*tfe.Workspace -} - -func newMockWorkspaces(client *mockClient) *mockWorkspaces { - return &mockWorkspaces{ - client: client, - workspaceIDs: make(map[string]*tfe.Workspace), - workspaceNames: make(map[string]*tfe.Workspace), - } -} - -func (m *mockWorkspaces) List(ctx context.Context, organization string, options tfe.WorkspaceListOptions) (*tfe.WorkspaceList, error) { - dummyWorkspaces := 10 - wl := &tfe.WorkspaceList{} - - // Get the prefix from the search options. - prefix := "" - if options.Search != nil { - prefix = *options.Search - } - - // Get all the workspaces that match the prefix. - var ws []*tfe.Workspace - for _, w := range m.workspaceIDs { - if strings.HasPrefix(w.Name, prefix) { - ws = append(ws, w) - } - } - - // Return an empty result if we have no matches. - if len(ws) == 0 { - wl.Pagination = &tfe.Pagination{ - CurrentPage: 1, - } - return wl, nil - } - - // Return dummy workspaces for the first page to test pagination. - if options.PageNumber <= 1 { - for i := 0; i < dummyWorkspaces; i++ { - wl.Items = append(wl.Items, &tfe.Workspace{ - ID: generateID("ws-"), - Name: fmt.Sprintf("dummy-workspace-%d", i), - }) - } - - wl.Pagination = &tfe.Pagination{ - CurrentPage: 1, - NextPage: 2, - TotalPages: 2, - TotalCount: len(wl.Items) + len(ws), - } - - return wl, nil - } - - // Return the actual workspaces that matched as the second page. - wl.Items = ws - wl.Pagination = &tfe.Pagination{ - CurrentPage: 2, - PreviousPage: 1, - TotalPages: 2, - TotalCount: len(wl.Items) + dummyWorkspaces, - } - - return wl, nil -} - -func (m *mockWorkspaces) Create(ctx context.Context, organization string, options tfe.WorkspaceCreateOptions) (*tfe.Workspace, error) { - if strings.HasSuffix(*options.Name, "no-operations") { - options.Operations = tfe.Bool(false) - } else if options.Operations == nil { - options.Operations = tfe.Bool(true) - } - w := &tfe.Workspace{ - ID: generateID("ws-"), - Name: *options.Name, - Operations: *options.Operations, - Permissions: &tfe.WorkspacePermissions{ - CanQueueApply: true, - CanQueueRun: true, - }, - } - if options.AutoApply != nil { - w.AutoApply = *options.AutoApply - } - if options.VCSRepo != nil { - w.VCSRepo = &tfe.VCSRepo{} - } - if options.TerraformVersion != nil { - w.TerraformVersion = *options.TerraformVersion - } else { - w.TerraformVersion = tfversion.String() - } - m.workspaceIDs[w.ID] = w - m.workspaceNames[w.Name] = w - return w, nil -} - -func (m *mockWorkspaces) Read(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) { - // custom error for TestRemote_plan500 in backend_plan_test.go - if workspace == "network-error" { - return nil, errors.New("I'm a little teacup") - } - - w, ok := m.workspaceNames[workspace] - if !ok { - return nil, tfe.ErrResourceNotFound - } - return w, nil -} - -func (m *mockWorkspaces) ReadByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { - w, ok := m.workspaceIDs[workspaceID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - return w, nil -} - -func (m *mockWorkspaces) Update(ctx context.Context, organization, workspace string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) { - w, ok := m.workspaceNames[workspace] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - if options.Operations != nil { - w.Operations = *options.Operations - } - if options.Name != nil { - w.Name = *options.Name - } - if options.TerraformVersion != nil { - w.TerraformVersion = *options.TerraformVersion - } - if options.WorkingDirectory != nil { - w.WorkingDirectory = *options.WorkingDirectory - } - - delete(m.workspaceNames, workspace) - m.workspaceNames[w.Name] = w - - return w, nil -} - -func (m *mockWorkspaces) UpdateByID(ctx context.Context, workspaceID string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) { - w, ok := m.workspaceIDs[workspaceID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - - if options.Name != nil { - w.Name = *options.Name - } - if options.TerraformVersion != nil { - w.TerraformVersion = *options.TerraformVersion - } - if options.WorkingDirectory != nil { - w.WorkingDirectory = *options.WorkingDirectory - } - - delete(m.workspaceNames, w.Name) - m.workspaceNames[w.Name] = w - - return w, nil -} - -func (m *mockWorkspaces) Delete(ctx context.Context, organization, workspace string) error { - if w, ok := m.workspaceNames[workspace]; ok { - delete(m.workspaceIDs, w.ID) - } - delete(m.workspaceNames, workspace) - return nil -} - -func (m *mockWorkspaces) DeleteByID(ctx context.Context, workspaceID string) error { - if w, ok := m.workspaceIDs[workspaceID]; ok { - delete(m.workspaceIDs, w.Name) - } - delete(m.workspaceIDs, workspaceID) - return nil -} - -func (m *mockWorkspaces) RemoveVCSConnection(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) { - w, ok := m.workspaceNames[workspace] - if !ok { - return nil, tfe.ErrResourceNotFound - } - w.VCSRepo = nil - return w, nil -} - -func (m *mockWorkspaces) RemoveVCSConnectionByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { - w, ok := m.workspaceIDs[workspaceID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - w.VCSRepo = nil - return w, nil -} - -func (m *mockWorkspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) { - w, ok := m.workspaceIDs[workspaceID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - if w.Locked { - return nil, tfe.ErrWorkspaceLocked - } - w.Locked = true - return w, nil -} - -func (m *mockWorkspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { - w, ok := m.workspaceIDs[workspaceID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - if !w.Locked { - return nil, tfe.ErrWorkspaceNotLocked - } - w.Locked = false - return w, nil -} - -func (m *mockWorkspaces) ForceUnlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { - w, ok := m.workspaceIDs[workspaceID] - if !ok { - return nil, tfe.ErrResourceNotFound - } - if !w.Locked { - return nil, tfe.ErrWorkspaceNotLocked - } - w.Locked = false - return w, nil -} - -func (m *mockWorkspaces) AssignSSHKey(ctx context.Context, workspaceID string, options tfe.WorkspaceAssignSSHKeyOptions) (*tfe.Workspace, error) { - panic("not implemented") -} - -func (m *mockWorkspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { - panic("not implemented") -} - -func (m *mockWorkspaces) RemoteStateConsumers(ctx context.Context, workspaceID string) (*tfe.WorkspaceList, error) { - panic("not implemented") -} - -func (m *mockWorkspaces) AddRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceAddRemoteStateConsumersOptions) error { - panic("not implemented") -} - -func (m *mockWorkspaces) RemoveRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceRemoveRemoteStateConsumersOptions) error { - panic("not implemented") -} - -func (m *mockWorkspaces) UpdateRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceUpdateRemoteStateConsumersOptions) error { - panic("not implemented") -} - -func (m *mockWorkspaces) Readme(ctx context.Context, workspaceID string) (io.Reader, error) { - panic("not implemented") -} - -const alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - -func generateID(s string) string { - b := make([]byte, 16) - for i := range b { - b[i] = alphanumeric[rand.Intn(len(alphanumeric))] - } - return s + string(b) -} diff --git a/internal/backend/remote/backend_plan_test.go b/internal/backend/remote/backend_plan_test.go index 147c68e9d..95aacea97 100644 --- a/internal/backend/remote/backend_plan_test.go +++ b/internal/backend/remote/backend_plan_test.go @@ -13,6 +13,7 @@ import ( tfe "github.com/hashicorp/go-tfe" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/cloud" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" @@ -313,11 +314,11 @@ func TestRemote_planWithoutRefresh(t *testing.T) { // We should find a run inside the mock client that has refresh set // to false. - runsAPI := b.client.Runs.(*mockRuns) - if got, want := len(runsAPI.runs), 1; got != want { + runsAPI := b.client.Runs.(*cloud.MockRuns) + if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } - for _, run := range runsAPI.runs { + for _, run := range runsAPI.Runs { if diff := cmp.Diff(false, run.Refresh); diff != "" { t.Errorf("wrong Refresh setting in the created run\n%s", diff) } @@ -382,11 +383,11 @@ func TestRemote_planWithRefreshOnly(t *testing.T) { // We should find a run inside the mock client that has refresh-only set // to true. - runsAPI := b.client.Runs.(*mockRuns) - if got, want := len(runsAPI.runs), 1; got != want { + runsAPI := b.client.Runs.(*cloud.MockRuns) + if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } - for _, run := range runsAPI.runs { + for _, run := range runsAPI.Runs { if diff := cmp.Diff(true, run.RefreshOnly); diff != "" { t.Errorf("wrong RefreshOnly setting in the created run\n%s", diff) } @@ -432,7 +433,7 @@ func TestRemote_planWithTarget(t *testing.T) { // When the backend code creates a new run, we'll tweak it so that it // has a cost estimation object with the "skipped_due_to_targeting" status, // emulating how a real server is expected to behave in that case. - b.client.Runs.(*mockRuns).modifyNewRun = func(client *mockClient, options tfe.RunCreateOptions, run *tfe.Run) { + b.client.Runs.(*cloud.MockRuns).ModifyNewRun = func(client *cloud.MockClient, options tfe.RunCreateOptions, run *tfe.Run) { const fakeID = "fake" // This is the cost estimate object embedded in the run itself which // the backend will use to learn the ID to request from the cost @@ -446,7 +447,7 @@ func TestRemote_planWithTarget(t *testing.T) { // the same ID indicated in the object above, where we'll then return // the status "skipped_due_to_targeting" to trigger the special skip // message in the backend output. - client.CostEstimates.estimations[fakeID] = &tfe.CostEstimate{ + client.CostEstimates.Estimations[fakeID] = &tfe.CostEstimate{ ID: fakeID, Status: "skipped_due_to_targeting", } @@ -483,11 +484,11 @@ func TestRemote_planWithTarget(t *testing.T) { // We should find a run inside the mock client that has the same // target address we requested above. - runsAPI := b.client.Runs.(*mockRuns) - if got, want := len(runsAPI.runs), 1; got != want { + runsAPI := b.client.Runs.(*cloud.MockRuns) + if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } - for _, run := range runsAPI.runs { + for _, run := range runsAPI.Runs { if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" { t.Errorf("wrong TargetAddrs in the created run\n%s", diff) } @@ -558,11 +559,11 @@ func TestRemote_planWithReplace(t *testing.T) { // We should find a run inside the mock client that has the same // refresh address we requested above. - runsAPI := b.client.Runs.(*mockRuns) - if got, want := len(runsAPI.runs), 1; got != want { + runsAPI := b.client.Runs.(*cloud.MockRuns) + if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } - for _, run := range runsAPI.runs { + for _, run := range runsAPI.Runs { if diff := cmp.Diff([]string{"null_resource.foo"}, run.ReplaceAddrs); diff != "" { t.Errorf("wrong ReplaceAddrs in the created run\n%s", diff) } diff --git a/internal/backend/remote/backend_state_test.go b/internal/backend/remote/backend_state_test.go index 3a1769c30..0503936b8 100644 --- a/internal/backend/remote/backend_state_test.go +++ b/internal/backend/remote/backend_state_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/cloud" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statefile" @@ -39,7 +40,7 @@ func TestRemoteClient_stateLock(t *testing.T) { func TestRemoteClient_withRunID(t *testing.T) { // Set the TFE_RUN_ID environment variable before creating the client! - if err := os.Setenv("TFE_RUN_ID", generateID("run-")); err != nil { + if err := os.Setenv("TFE_RUN_ID", cloud.GenerateID("run-")); err != nil { t.Fatalf("error setting env var TFE_RUN_ID: %v", err) } diff --git a/internal/backend/remote/testing.go b/internal/backend/remote/testing.go index f3c66941a..7dbb9e9b2 100644 --- a/internal/backend/remote/testing.go +++ b/internal/backend/remote/testing.go @@ -8,12 +8,14 @@ import ( "net/http/httptest" "path" "testing" + "time" tfe "github.com/hashicorp/go-tfe" svchost "github.com/hashicorp/terraform-svchost" "github.com/hashicorp/terraform-svchost/auth" "github.com/hashicorp/terraform-svchost/disco" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/cloud" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/httpclient" @@ -39,6 +41,26 @@ var ( }) ) +// mockInput is a mock implementation of terraform.UIInput. +type mockInput struct { + answers map[string]string +} + +func (m *mockInput) Input(ctx context.Context, opts *terraform.InputOpts) (string, error) { + v, ok := m.answers[opts.Id] + if !ok { + return "", fmt.Errorf("unexpected input request in test: %s", opts.Id) + } + if v == "wait-for-external-update" { + select { + case <-ctx.Done(): + case <-time.After(time.Minute): + } + } + delete(m.answers, opts.Id) + return v, nil +} + func testInput(t *testing.T, answers map[string]string) *mockInput { return &mockInput{answers: answers} } @@ -111,7 +133,7 @@ func testBackend(t *testing.T, obj cty.Value) (*Remote, func()) { } // Get a new mock client. - mc := newMockClient() + mc := cloud.NewMockClient() // Replace the services we use with our mock services. b.CLI = cli.NewMockUi() diff --git a/internal/cloud/backend_apply_test.go b/internal/cloud/backend_apply_test.go index 140dafa8a..1100c31dc 100644 --- a/internal/cloud/backend_apply_test.go +++ b/internal/cloud/backend_apply_test.go @@ -299,11 +299,11 @@ func TestCloud_applyWithoutRefresh(t *testing.T) { // We should find a run inside the mock client that has refresh set // to false. - runsAPI := b.client.Runs.(*mockRuns) - if got, want := len(runsAPI.runs), 1; got != want { + runsAPI := b.client.Runs.(*MockRuns) + if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } - for _, run := range runsAPI.runs { + for _, run := range runsAPI.Runs { if diff := cmp.Diff(false, run.Refresh); diff != "" { t.Errorf("wrong Refresh setting in the created run\n%s", diff) } @@ -368,11 +368,11 @@ func TestCloud_applyWithRefreshOnly(t *testing.T) { // We should find a run inside the mock client that has refresh-only set // to true. - runsAPI := b.client.Runs.(*mockRuns) - if got, want := len(runsAPI.runs), 1; got != want { + runsAPI := b.client.Runs.(*MockRuns) + if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } - for _, run := range runsAPI.runs { + for _, run := range runsAPI.Runs { if diff := cmp.Diff(true, run.RefreshOnly); diff != "" { t.Errorf("wrong RefreshOnly setting in the created run\n%s", diff) } @@ -439,11 +439,11 @@ func TestCloud_applyWithTarget(t *testing.T) { // We should find a run inside the mock client that has the same // target address we requested above. - runsAPI := b.client.Runs.(*mockRuns) - if got, want := len(runsAPI.runs), 1; got != want { + runsAPI := b.client.Runs.(*MockRuns) + if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } - for _, run := range runsAPI.runs { + for _, run := range runsAPI.Runs { if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" { t.Errorf("wrong TargetAddrs in the created run\n%s", diff) } @@ -514,11 +514,11 @@ func TestCloud_applyWithReplace(t *testing.T) { // We should find a run inside the mock client that has the same // refresh address we requested above. - runsAPI := b.client.Runs.(*mockRuns) - if got, want := len(runsAPI.runs), 1; got != want { + runsAPI := b.client.Runs.(*MockRuns) + if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } - for _, run := range runsAPI.runs { + for _, run := range runsAPI.Runs { if diff := cmp.Diff([]string{"null_resource.foo"}, run.ReplaceAddrs); diff != "" { t.Errorf("wrong ReplaceAddrs in the created run\n%s", diff) } diff --git a/internal/cloud/backend_plan_test.go b/internal/cloud/backend_plan_test.go index 790c8cc03..cd836b46d 100644 --- a/internal/cloud/backend_plan_test.go +++ b/internal/cloud/backend_plan_test.go @@ -304,11 +304,11 @@ func TestCloud_planWithoutRefresh(t *testing.T) { // We should find a run inside the mock client that has refresh set // to false. - runsAPI := b.client.Runs.(*mockRuns) - if got, want := len(runsAPI.runs), 1; got != want { + runsAPI := b.client.Runs.(*MockRuns) + if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } - for _, run := range runsAPI.runs { + for _, run := range runsAPI.Runs { if diff := cmp.Diff(false, run.Refresh); diff != "" { t.Errorf("wrong Refresh setting in the created run\n%s", diff) } @@ -373,11 +373,11 @@ func TestCloud_planWithRefreshOnly(t *testing.T) { // We should find a run inside the mock client that has refresh-only set // to true. - runsAPI := b.client.Runs.(*mockRuns) - if got, want := len(runsAPI.runs), 1; got != want { + runsAPI := b.client.Runs.(*MockRuns) + if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } - for _, run := range runsAPI.runs { + for _, run := range runsAPI.Runs { if diff := cmp.Diff(true, run.RefreshOnly); diff != "" { t.Errorf("wrong RefreshOnly setting in the created run\n%s", diff) } @@ -423,7 +423,7 @@ func TestCloud_planWithTarget(t *testing.T) { // When the backend code creates a new run, we'll tweak it so that it // has a cost estimation object with the "skipped_due_to_targeting" status, // emulating how a real server is expected to behave in that case. - b.client.Runs.(*mockRuns).modifyNewRun = func(client *mockClient, options tfe.RunCreateOptions, run *tfe.Run) { + b.client.Runs.(*MockRuns).ModifyNewRun = func(client *MockClient, options tfe.RunCreateOptions, run *tfe.Run) { const fakeID = "fake" // This is the cost estimate object embedded in the run itself which // the backend will use to learn the ID to request from the cost @@ -437,7 +437,7 @@ func TestCloud_planWithTarget(t *testing.T) { // the same ID indicated in the object above, where we'll then return // the status "skipped_due_to_targeting" to trigger the special skip // message in the backend output. - client.CostEstimates.estimations[fakeID] = &tfe.CostEstimate{ + client.CostEstimates.Estimations[fakeID] = &tfe.CostEstimate{ ID: fakeID, Status: "skipped_due_to_targeting", } @@ -474,11 +474,11 @@ func TestCloud_planWithTarget(t *testing.T) { // We should find a run inside the mock client that has the same // target address we requested above. - runsAPI := b.client.Runs.(*mockRuns) - if got, want := len(runsAPI.runs), 1; got != want { + runsAPI := b.client.Runs.(*MockRuns) + if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } - for _, run := range runsAPI.runs { + for _, run := range runsAPI.Runs { if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" { t.Errorf("wrong TargetAddrs in the created run\n%s", diff) } @@ -549,11 +549,11 @@ func TestCloud_planWithReplace(t *testing.T) { // We should find a run inside the mock client that has the same // refresh address we requested above. - runsAPI := b.client.Runs.(*mockRuns) - if got, want := len(runsAPI.runs), 1; got != want { + runsAPI := b.client.Runs.(*MockRuns) + if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } - for _, run := range runsAPI.runs { + for _, run := range runsAPI.Runs { if diff := cmp.Diff([]string{"null_resource.foo"}, run.ReplaceAddrs); diff != "" { t.Errorf("wrong ReplaceAddrs in the created run\n%s", diff) } diff --git a/internal/cloud/backend_state_test.go b/internal/cloud/backend_state_test.go index dc2a45723..a94c60c6e 100644 --- a/internal/cloud/backend_state_test.go +++ b/internal/cloud/backend_state_test.go @@ -39,7 +39,7 @@ func TestRemoteClient_stateLock(t *testing.T) { func TestRemoteClient_withRunID(t *testing.T) { // Set the TFE_RUN_ID environment variable before creating the client! - if err := os.Setenv("TFE_RUN_ID", generateID("run-")); err != nil { + if err := os.Setenv("TFE_RUN_ID", GenerateID("run-")); err != nil { t.Fatalf("error setting env var TFE_RUN_ID: %v", err) } diff --git a/internal/cloud/testing.go b/internal/cloud/testing.go index 73668b2b0..7bb145b3c 100644 --- a/internal/cloud/testing.go +++ b/internal/cloud/testing.go @@ -8,6 +8,7 @@ import ( "net/http/httptest" "path" "testing" + "time" tfe "github.com/hashicorp/go-tfe" svchost "github.com/hashicorp/terraform-svchost" @@ -39,6 +40,26 @@ var ( }) ) +// mockInput is a mock implementation of terraform.UIInput. +type mockInput struct { + answers map[string]string +} + +func (m *mockInput) Input(ctx context.Context, opts *terraform.InputOpts) (string, error) { + v, ok := m.answers[opts.Id] + if !ok { + return "", fmt.Errorf("unexpected input request in test: %s", opts.Id) + } + if v == "wait-for-external-update" { + select { + case <-ctx.Done(): + case <-time.After(time.Minute): + } + } + delete(m.answers, opts.Id) + return v, nil +} + func testInput(t *testing.T, answers map[string]string) *mockInput { return &mockInput{answers: answers} } @@ -111,7 +132,7 @@ func testBackend(t *testing.T, obj cty.Value) (*Cloud, func()) { } // Get a new mock client. - mc := newMockClient() + mc := NewMockClient() // Replace the services we use with our mock services. b.CLI = cli.NewMockUi() diff --git a/internal/cloud/backend_mock.go b/internal/cloud/tfe_client_mock.go similarity index 75% rename from internal/cloud/backend_mock.go rename to internal/cloud/tfe_client_mock.go index 0ee036bc5..d575d82e8 100644 --- a/internal/cloud/backend_mock.go +++ b/internal/cloud/tfe_client_mock.go @@ -16,26 +16,25 @@ import ( "time" tfe "github.com/hashicorp/go-tfe" - "github.com/hashicorp/terraform/internal/terraform" tfversion "github.com/hashicorp/terraform/version" "github.com/mitchellh/copystructure" ) -type mockClient struct { - Applies *mockApplies - ConfigurationVersions *mockConfigurationVersions - CostEstimates *mockCostEstimates - Organizations *mockOrganizations - Plans *mockPlans - PolicyChecks *mockPolicyChecks - Runs *mockRuns - StateVersions *mockStateVersions - Variables *mockVariables - Workspaces *mockWorkspaces +type MockClient struct { + Applies *MockApplies + ConfigurationVersions *MockConfigurationVersions + CostEstimates *MockCostEstimates + Organizations *MockOrganizations + Plans *MockPlans + PolicyChecks *MockPolicyChecks + Runs *MockRuns + StateVersions *MockStateVersions + Variables *MockVariables + Workspaces *MockWorkspaces } -func newMockClient() *mockClient { - c := &mockClient{} +func NewMockClient() *MockClient { + c := &MockClient{} c.Applies = newMockApplies(c) c.ConfigurationVersions = newMockConfigurationVersions(c) c.CostEstimates = newMockCostEstimates(c) @@ -49,14 +48,14 @@ func newMockClient() *mockClient { return c } -type mockApplies struct { - client *mockClient +type MockApplies struct { + client *MockClient applies map[string]*tfe.Apply logs map[string]string } -func newMockApplies(client *mockClient) *mockApplies { - return &mockApplies{ +func newMockApplies(client *MockClient) *MockApplies { + return &MockApplies{ client: client, applies: make(map[string]*tfe.Apply), logs: make(map[string]string), @@ -65,7 +64,7 @@ func newMockApplies(client *mockClient) *mockApplies { // create is a helper function to create a mock apply that uses the configured // working directory to find the logfile. -func (m *mockApplies) create(cvID, workspaceID string) (*tfe.Apply, error) { +func (m *MockApplies) create(cvID, workspaceID string) (*tfe.Apply, error) { c, ok := m.client.ConfigurationVersions.configVersions[cvID] if !ok { return nil, tfe.ErrResourceNotFound @@ -75,7 +74,7 @@ func (m *mockApplies) create(cvID, workspaceID string) (*tfe.Apply, error) { return nil, nil } - id := generateID("apply-") + id := GenerateID("apply-") url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) a := &tfe.Apply{ @@ -103,7 +102,7 @@ func (m *mockApplies) create(cvID, workspaceID string) (*tfe.Apply, error) { return a, nil } -func (m *mockApplies) Read(ctx context.Context, applyID string) (*tfe.Apply, error) { +func (m *MockApplies) Read(ctx context.Context, applyID string) (*tfe.Apply, error) { a, ok := m.applies[applyID] if !ok { return nil, tfe.ErrResourceNotFound @@ -115,7 +114,7 @@ func (m *mockApplies) Read(ctx context.Context, applyID string) (*tfe.Apply, err return a, nil } -func (m *mockApplies) Logs(ctx context.Context, applyID string) (io.Reader, error) { +func (m *MockApplies) Logs(ctx context.Context, applyID string) (io.Reader, error) { a, err := m.Read(ctx, applyID) if err != nil { return nil, err @@ -152,15 +151,15 @@ func (m *mockApplies) Logs(ctx context.Context, applyID string) (io.Reader, erro }, nil } -type mockConfigurationVersions struct { - client *mockClient +type MockConfigurationVersions struct { + client *MockClient configVersions map[string]*tfe.ConfigurationVersion uploadPaths map[string]string uploadURLs map[string]*tfe.ConfigurationVersion } -func newMockConfigurationVersions(client *mockClient) *mockConfigurationVersions { - return &mockConfigurationVersions{ +func newMockConfigurationVersions(client *MockClient) *MockConfigurationVersions { + return &MockConfigurationVersions{ client: client, configVersions: make(map[string]*tfe.ConfigurationVersion), uploadPaths: make(map[string]string), @@ -168,7 +167,7 @@ func newMockConfigurationVersions(client *mockClient) *mockConfigurationVersions } } -func (m *mockConfigurationVersions) List(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionListOptions) (*tfe.ConfigurationVersionList, error) { +func (m *MockConfigurationVersions) List(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionListOptions) (*tfe.ConfigurationVersionList, error) { cvl := &tfe.ConfigurationVersionList{} for _, cv := range m.configVersions { cvl.Items = append(cvl.Items, cv) @@ -185,8 +184,8 @@ func (m *mockConfigurationVersions) List(ctx context.Context, workspaceID string return cvl, nil } -func (m *mockConfigurationVersions) Create(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionCreateOptions) (*tfe.ConfigurationVersion, error) { - id := generateID("cv-") +func (m *MockConfigurationVersions) Create(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionCreateOptions) (*tfe.ConfigurationVersion, error) { + id := GenerateID("cv-") url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) cv := &tfe.ConfigurationVersion{ @@ -201,7 +200,7 @@ func (m *mockConfigurationVersions) Create(ctx context.Context, workspaceID stri return cv, nil } -func (m *mockConfigurationVersions) Read(ctx context.Context, cvID string) (*tfe.ConfigurationVersion, error) { +func (m *MockConfigurationVersions) Read(ctx context.Context, cvID string) (*tfe.ConfigurationVersion, error) { cv, ok := m.configVersions[cvID] if !ok { return nil, tfe.ErrResourceNotFound @@ -209,7 +208,15 @@ func (m *mockConfigurationVersions) Read(ctx context.Context, cvID string) (*tfe return cv, nil } -func (m *mockConfigurationVersions) Upload(ctx context.Context, url, path string) error { +func (m *MockConfigurationVersions) ReadWithOptions(ctx context.Context, cvID string, options *tfe.ConfigurationVersionReadOptions) (*tfe.ConfigurationVersion, error) { + cv, ok := m.configVersions[cvID] + if !ok { + return nil, tfe.ErrResourceNotFound + } + return cv, nil +} + +func (m *MockConfigurationVersions) Upload(ctx context.Context, url, path string) error { cv, ok := m.uploadURLs[url] if !ok { return errors.New("404 not found") @@ -219,24 +226,24 @@ func (m *mockConfigurationVersions) Upload(ctx context.Context, url, path string return nil } -type mockCostEstimates struct { - client *mockClient - estimations map[string]*tfe.CostEstimate +type MockCostEstimates struct { + client *MockClient + Estimations map[string]*tfe.CostEstimate logs map[string]string } -func newMockCostEstimates(client *mockClient) *mockCostEstimates { - return &mockCostEstimates{ +func newMockCostEstimates(client *MockClient) *MockCostEstimates { + return &MockCostEstimates{ client: client, - estimations: make(map[string]*tfe.CostEstimate), + Estimations: make(map[string]*tfe.CostEstimate), logs: make(map[string]string), } } // create is a helper function to create a mock cost estimation that uses the // configured working directory to find the logfile. -func (m *mockCostEstimates) create(cvID, workspaceID string) (*tfe.CostEstimate, error) { - id := generateID("ce-") +func (m *MockCostEstimates) create(cvID, workspaceID string) (*tfe.CostEstimate, error) { + id := GenerateID("ce-") ce := &tfe.CostEstimate{ ID: id, @@ -263,21 +270,21 @@ func (m *mockCostEstimates) create(cvID, workspaceID string) (*tfe.CostEstimate, } m.logs[ce.ID] = logfile - m.estimations[ce.ID] = ce + m.Estimations[ce.ID] = ce return ce, nil } -func (m *mockCostEstimates) Read(ctx context.Context, costEstimateID string) (*tfe.CostEstimate, error) { - ce, ok := m.estimations[costEstimateID] +func (m *MockCostEstimates) Read(ctx context.Context, costEstimateID string) (*tfe.CostEstimate, error) { + ce, ok := m.Estimations[costEstimateID] if !ok { return nil, tfe.ErrResourceNotFound } return ce, nil } -func (m *mockCostEstimates) Logs(ctx context.Context, costEstimateID string) (io.Reader, error) { - ce, ok := m.estimations[costEstimateID] +func (m *MockCostEstimates) Logs(ctx context.Context, costEstimateID string) (io.Reader, error) { + ce, ok := m.Estimations[costEstimateID] if !ok { return nil, tfe.ErrResourceNotFound } @@ -301,39 +308,19 @@ func (m *mockCostEstimates) Logs(ctx context.Context, costEstimateID string) (io return bytes.NewBuffer(logs), nil } -// mockInput is a mock implementation of terraform.UIInput. -type mockInput struct { - answers map[string]string -} - -func (m *mockInput) Input(ctx context.Context, opts *terraform.InputOpts) (string, error) { - v, ok := m.answers[opts.Id] - if !ok { - return "", fmt.Errorf("unexpected input request in test: %s", opts.Id) - } - if v == "wait-for-external-update" { - select { - case <-ctx.Done(): - case <-time.After(time.Minute): - } - } - delete(m.answers, opts.Id) - return v, nil -} - -type mockOrganizations struct { - client *mockClient +type MockOrganizations struct { + client *MockClient organizations map[string]*tfe.Organization } -func newMockOrganizations(client *mockClient) *mockOrganizations { - return &mockOrganizations{ +func newMockOrganizations(client *MockClient) *MockOrganizations { + return &MockOrganizations{ client: client, organizations: make(map[string]*tfe.Organization), } } -func (m *mockOrganizations) List(ctx context.Context, options tfe.OrganizationListOptions) (*tfe.OrganizationList, error) { +func (m *MockOrganizations) List(ctx context.Context, options tfe.OrganizationListOptions) (*tfe.OrganizationList, error) { orgl := &tfe.OrganizationList{} for _, org := range m.organizations { orgl.Items = append(orgl.Items, org) @@ -376,13 +363,13 @@ func (m *mockLogReader) read(l []byte) (int, error) { return m.logs.Read(l) } -func (m *mockOrganizations) Create(ctx context.Context, options tfe.OrganizationCreateOptions) (*tfe.Organization, error) { +func (m *MockOrganizations) Create(ctx context.Context, options tfe.OrganizationCreateOptions) (*tfe.Organization, error) { org := &tfe.Organization{Name: *options.Name} m.organizations[org.Name] = org return org, nil } -func (m *mockOrganizations) Read(ctx context.Context, name string) (*tfe.Organization, error) { +func (m *MockOrganizations) Read(ctx context.Context, name string) (*tfe.Organization, error) { org, ok := m.organizations[name] if !ok { return nil, tfe.ErrResourceNotFound @@ -390,7 +377,7 @@ func (m *mockOrganizations) Read(ctx context.Context, name string) (*tfe.Organiz return org, nil } -func (m *mockOrganizations) Update(ctx context.Context, name string, options tfe.OrganizationUpdateOptions) (*tfe.Organization, error) { +func (m *MockOrganizations) Update(ctx context.Context, name string, options tfe.OrganizationUpdateOptions) (*tfe.Organization, error) { org, ok := m.organizations[name] if !ok { return nil, tfe.ErrResourceNotFound @@ -400,14 +387,14 @@ func (m *mockOrganizations) Update(ctx context.Context, name string, options tfe } -func (m *mockOrganizations) Delete(ctx context.Context, name string) error { +func (m *MockOrganizations) Delete(ctx context.Context, name string) error { delete(m.organizations, name) return nil } -func (m *mockOrganizations) Capacity(ctx context.Context, name string) (*tfe.Capacity, error) { +func (m *MockOrganizations) Capacity(ctx context.Context, name string) (*tfe.Capacity, error) { var pending, running int - for _, r := range m.client.Runs.runs { + for _, r := range m.client.Runs.Runs { if r.Status == tfe.RunPending { pending++ continue @@ -417,7 +404,7 @@ func (m *mockOrganizations) Capacity(ctx context.Context, name string) (*tfe.Cap return &tfe.Capacity{Pending: pending, Running: running}, nil } -func (m *mockOrganizations) Entitlements(ctx context.Context, name string) (*tfe.Entitlements, error) { +func (m *MockOrganizations) Entitlements(ctx context.Context, name string) (*tfe.Entitlements, error) { return &tfe.Entitlements{ Operations: true, PrivateModuleRegistry: true, @@ -428,10 +415,10 @@ func (m *mockOrganizations) Entitlements(ctx context.Context, name string) (*tfe }, nil } -func (m *mockOrganizations) RunQueue(ctx context.Context, name string, options tfe.RunQueueOptions) (*tfe.RunQueue, error) { +func (m *MockOrganizations) RunQueue(ctx context.Context, name string, options tfe.RunQueueOptions) (*tfe.RunQueue, error) { rq := &tfe.RunQueue{} - for _, r := range m.client.Runs.runs { + for _, r := range m.client.Runs.Runs { rq.Items = append(rq.Items, r) } @@ -446,15 +433,15 @@ func (m *mockOrganizations) RunQueue(ctx context.Context, name string, options t return rq, nil } -type mockPlans struct { - client *mockClient +type MockPlans struct { + client *MockClient logs map[string]string planOutputs map[string]string plans map[string]*tfe.Plan } -func newMockPlans(client *mockClient) *mockPlans { - return &mockPlans{ +func newMockPlans(client *MockClient) *MockPlans { + return &MockPlans{ client: client, logs: make(map[string]string), planOutputs: make(map[string]string), @@ -464,8 +451,8 @@ func newMockPlans(client *mockClient) *mockPlans { // create is a helper function to create a mock plan that uses the configured // working directory to find the logfile. -func (m *mockPlans) create(cvID, workspaceID string) (*tfe.Plan, error) { - id := generateID("plan-") +func (m *MockPlans) create(cvID, workspaceID string) (*tfe.Plan, error) { + id := GenerateID("plan-") url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) p := &tfe.Plan{ @@ -489,7 +476,7 @@ func (m *mockPlans) create(cvID, workspaceID string) (*tfe.Plan, error) { return p, nil } -func (m *mockPlans) Read(ctx context.Context, planID string) (*tfe.Plan, error) { +func (m *MockPlans) Read(ctx context.Context, planID string) (*tfe.Plan, error) { p, ok := m.plans[planID] if !ok { return nil, tfe.ErrResourceNotFound @@ -501,7 +488,7 @@ func (m *mockPlans) Read(ctx context.Context, planID string) (*tfe.Plan, error) return p, nil } -func (m *mockPlans) Logs(ctx context.Context, planID string) (io.Reader, error) { +func (m *MockPlans) Logs(ctx context.Context, planID string) (io.Reader, error) { p, err := m.Read(ctx, planID) if err != nil { return nil, err @@ -538,7 +525,7 @@ func (m *mockPlans) Logs(ctx context.Context, planID string) (io.Reader, error) }, nil } -func (m *mockPlans) JSONOutput(ctx context.Context, planID string) ([]byte, error) { +func (m *MockPlans) JSONOutput(ctx context.Context, planID string) ([]byte, error) { planOutput, ok := m.planOutputs[planID] if !ok { return nil, tfe.ErrResourceNotFound @@ -547,14 +534,14 @@ func (m *mockPlans) JSONOutput(ctx context.Context, planID string) ([]byte, erro return []byte(planOutput), nil } -type mockPolicyChecks struct { - client *mockClient +type MockPolicyChecks struct { + client *MockClient checks map[string]*tfe.PolicyCheck logs map[string]string } -func newMockPolicyChecks(client *mockClient) *mockPolicyChecks { - return &mockPolicyChecks{ +func newMockPolicyChecks(client *MockClient) *MockPolicyChecks { + return &MockPolicyChecks{ client: client, checks: make(map[string]*tfe.PolicyCheck), logs: make(map[string]string), @@ -563,8 +550,8 @@ func newMockPolicyChecks(client *mockClient) *mockPolicyChecks { // create is a helper function to create a mock policy check that uses the // configured working directory to find the logfile. -func (m *mockPolicyChecks) create(cvID, workspaceID string) (*tfe.PolicyCheck, error) { - id := generateID("pc-") +func (m *MockPolicyChecks) create(cvID, workspaceID string) (*tfe.PolicyCheck, error) { + id := GenerateID("pc-") pc := &tfe.PolicyCheck{ ID: id, @@ -595,8 +582,8 @@ func (m *mockPolicyChecks) create(cvID, workspaceID string) (*tfe.PolicyCheck, e return pc, nil } -func (m *mockPolicyChecks) List(ctx context.Context, runID string, options tfe.PolicyCheckListOptions) (*tfe.PolicyCheckList, error) { - _, ok := m.client.Runs.runs[runID] +func (m *MockPolicyChecks) List(ctx context.Context, runID string, options tfe.PolicyCheckListOptions) (*tfe.PolicyCheckList, error) { + _, ok := m.client.Runs.Runs[runID] if !ok { return nil, tfe.ErrResourceNotFound } @@ -617,7 +604,7 @@ func (m *mockPolicyChecks) List(ctx context.Context, runID string, options tfe.P return pcl, nil } -func (m *mockPolicyChecks) Read(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) { +func (m *MockPolicyChecks) Read(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) { pc, ok := m.checks[policyCheckID] if !ok { return nil, tfe.ErrResourceNotFound @@ -657,7 +644,7 @@ func (m *mockPolicyChecks) Read(ctx context.Context, policyCheckID string) (*tfe return pc, nil } -func (m *mockPolicyChecks) Override(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) { +func (m *MockPolicyChecks) Override(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) { pc, ok := m.checks[policyCheckID] if !ok { return nil, tfe.ErrResourceNotFound @@ -666,7 +653,7 @@ func (m *mockPolicyChecks) Override(ctx context.Context, policyCheckID string) ( return pc, nil } -func (m *mockPolicyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reader, error) { +func (m *MockPolicyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reader, error) { pc, ok := m.checks[policyCheckID] if !ok { return nil, tfe.ErrResourceNotFound @@ -706,28 +693,28 @@ func (m *mockPolicyChecks) Logs(ctx context.Context, policyCheckID string) (io.R return bytes.NewBuffer(logs), nil } -type mockRuns struct { +type MockRuns struct { sync.Mutex - client *mockClient - runs map[string]*tfe.Run + client *MockClient + Runs map[string]*tfe.Run workspaces map[string][]*tfe.Run - // If modifyNewRun is non-nil, the create method will call it just before + // If ModifyNewRun is non-nil, the create method will call it just before // saving a new run in the runs map, so that a calling test can mimic // side-effects that a real server might apply in certain situations. - modifyNewRun func(client *mockClient, options tfe.RunCreateOptions, run *tfe.Run) + ModifyNewRun func(client *MockClient, options tfe.RunCreateOptions, run *tfe.Run) } -func newMockRuns(client *mockClient) *mockRuns { - return &mockRuns{ +func newMockRuns(client *MockClient) *MockRuns { + return &MockRuns{ client: client, - runs: make(map[string]*tfe.Run), + Runs: make(map[string]*tfe.Run), workspaces: make(map[string][]*tfe.Run), } } -func (m *mockRuns) List(ctx context.Context, workspaceID string, options tfe.RunListOptions) (*tfe.RunList, error) { +func (m *MockRuns) List(ctx context.Context, workspaceID string, options tfe.RunListOptions) (*tfe.RunList, error) { m.Lock() defer m.Unlock() @@ -756,7 +743,7 @@ func (m *mockRuns) List(ctx context.Context, workspaceID string, options tfe.Run return rl, nil } -func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*tfe.Run, error) { +func (m *MockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*tfe.Run, error) { m.Lock() defer m.Unlock() @@ -781,7 +768,7 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t } r := &tfe.Run{ - ID: generateID("run-"), + ID: GenerateID("run-"), Actions: &tfe.RunActions{IsCancelable: true}, Apply: a, CostEstimate: ce, @@ -821,33 +808,33 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t w.CurrentRun = r } - if m.modifyNewRun != nil { + if m.ModifyNewRun != nil { // caller-provided callback may modify the run in-place to mimic // side-effects that a real server might take in some situations. - m.modifyNewRun(m.client, options, r) + m.ModifyNewRun(m.client, options, r) } - m.runs[r.ID] = r + m.Runs[r.ID] = r m.workspaces[options.Workspace.ID] = append(m.workspaces[options.Workspace.ID], r) return r, nil } -func (m *mockRuns) Read(ctx context.Context, runID string) (*tfe.Run, error) { +func (m *MockRuns) Read(ctx context.Context, runID string) (*tfe.Run, error) { return m.ReadWithOptions(ctx, runID, nil) } -func (m *mockRuns) ReadWithOptions(ctx context.Context, runID string, _ *tfe.RunReadOptions) (*tfe.Run, error) { +func (m *MockRuns) ReadWithOptions(ctx context.Context, runID string, _ *tfe.RunReadOptions) (*tfe.Run, error) { m.Lock() defer m.Unlock() - r, ok := m.runs[runID] + r, ok := m.Runs[runID] if !ok { return nil, tfe.ErrResourceNotFound } pending := false - for _, r := range m.runs { + for _, r := range m.Runs { if r.ID != runID && r.Status == tfe.RunPending { pending = true break @@ -885,11 +872,11 @@ func (m *mockRuns) ReadWithOptions(ctx context.Context, runID string, _ *tfe.Run return rc.(*tfe.Run), nil } -func (m *mockRuns) Apply(ctx context.Context, runID string, options tfe.RunApplyOptions) error { +func (m *MockRuns) Apply(ctx context.Context, runID string, options tfe.RunApplyOptions) error { m.Lock() defer m.Unlock() - r, ok := m.runs[runID] + r, ok := m.Runs[runID] if !ok { return tfe.ErrResourceNotFound } @@ -902,19 +889,19 @@ func (m *mockRuns) Apply(ctx context.Context, runID string, options tfe.RunApply return nil } -func (m *mockRuns) Cancel(ctx context.Context, runID string, options tfe.RunCancelOptions) error { +func (m *MockRuns) Cancel(ctx context.Context, runID string, options tfe.RunCancelOptions) error { panic("not implemented") } -func (m *mockRuns) ForceCancel(ctx context.Context, runID string, options tfe.RunForceCancelOptions) error { +func (m *MockRuns) ForceCancel(ctx context.Context, runID string, options tfe.RunForceCancelOptions) error { panic("not implemented") } -func (m *mockRuns) Discard(ctx context.Context, runID string, options tfe.RunDiscardOptions) error { +func (m *MockRuns) Discard(ctx context.Context, runID string, options tfe.RunDiscardOptions) error { m.Lock() defer m.Unlock() - r, ok := m.runs[runID] + r, ok := m.Runs[runID] if !ok { return tfe.ErrResourceNotFound } @@ -923,15 +910,15 @@ func (m *mockRuns) Discard(ctx context.Context, runID string, options tfe.RunDis return nil } -type mockStateVersions struct { - client *mockClient +type MockStateVersions struct { + client *MockClient states map[string][]byte stateVersions map[string]*tfe.StateVersion workspaces map[string][]string } -func newMockStateVersions(client *mockClient) *mockStateVersions { - return &mockStateVersions{ +func newMockStateVersions(client *MockClient) *MockStateVersions { + return &MockStateVersions{ client: client, states: make(map[string][]byte), stateVersions: make(map[string]*tfe.StateVersion), @@ -939,7 +926,7 @@ func newMockStateVersions(client *mockClient) *mockStateVersions { } } -func (m *mockStateVersions) List(ctx context.Context, options tfe.StateVersionListOptions) (*tfe.StateVersionList, error) { +func (m *MockStateVersions) List(ctx context.Context, options tfe.StateVersionListOptions) (*tfe.StateVersionList, error) { svl := &tfe.StateVersionList{} for _, sv := range m.stateVersions { svl.Items = append(svl.Items, sv) @@ -956,8 +943,8 @@ func (m *mockStateVersions) List(ctx context.Context, options tfe.StateVersionLi return svl, nil } -func (m *mockStateVersions) Create(ctx context.Context, workspaceID string, options tfe.StateVersionCreateOptions) (*tfe.StateVersion, error) { - id := generateID("sv-") +func (m *MockStateVersions) Create(ctx context.Context, workspaceID string, options tfe.StateVersionCreateOptions) (*tfe.StateVersion, error) { + id := GenerateID("sv-") runID := os.Getenv("TFE_RUN_ID") url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) @@ -983,11 +970,11 @@ func (m *mockStateVersions) Create(ctx context.Context, workspaceID string, opti return sv, nil } -func (m *mockStateVersions) Read(ctx context.Context, svID string) (*tfe.StateVersion, error) { +func (m *MockStateVersions) Read(ctx context.Context, svID string) (*tfe.StateVersion, error) { return m.ReadWithOptions(ctx, svID, nil) } -func (m *mockStateVersions) ReadWithOptions(ctx context.Context, svID string, options *tfe.StateVersionReadOptions) (*tfe.StateVersion, error) { +func (m *MockStateVersions) ReadWithOptions(ctx context.Context, svID string, options *tfe.StateVersionReadOptions) (*tfe.StateVersion, error) { sv, ok := m.stateVersions[svID] if !ok { return nil, tfe.ErrResourceNotFound @@ -995,11 +982,11 @@ func (m *mockStateVersions) ReadWithOptions(ctx context.Context, svID string, op return sv, nil } -func (m *mockStateVersions) Current(ctx context.Context, workspaceID string) (*tfe.StateVersion, error) { +func (m *MockStateVersions) Current(ctx context.Context, workspaceID string) (*tfe.StateVersion, error) { return m.CurrentWithOptions(ctx, workspaceID, nil) } -func (m *mockStateVersions) CurrentWithOptions(ctx context.Context, workspaceID string, options *tfe.StateVersionCurrentOptions) (*tfe.StateVersion, error) { +func (m *MockStateVersions) CurrentWithOptions(ctx context.Context, workspaceID string, options *tfe.StateVersionCurrentOptions) (*tfe.StateVersion, error) { w, ok := m.client.Workspaces.workspaceIDs[workspaceID] if !ok { return nil, tfe.ErrResourceNotFound @@ -1018,7 +1005,7 @@ func (m *mockStateVersions) CurrentWithOptions(ctx context.Context, workspaceID return sv, nil } -func (m *mockStateVersions) Download(ctx context.Context, url string) ([]byte, error) { +func (m *MockStateVersions) Download(ctx context.Context, url string) ([]byte, error) { state, ok := m.states[url] if !ok { return nil, tfe.ErrResourceNotFound @@ -1026,28 +1013,32 @@ func (m *mockStateVersions) Download(ctx context.Context, url string) ([]byte, e return state, nil } -type mockVariables struct { - client *mockClient +func (m *MockStateVersions) Outputs(ctx context.Context, svID string, options tfe.StateVersionOutputsListOptions) ([]*tfe.StateVersionOutput, error) { + panic("not implemented") +} + +type MockVariables struct { + client *MockClient workspaces map[string]*tfe.VariableList } -var _ tfe.Variables = (*mockVariables)(nil) +var _ tfe.Variables = (*MockVariables)(nil) -func newMockVariables(client *mockClient) *mockVariables { - return &mockVariables{ +func newMockVariables(client *MockClient) *MockVariables { + return &MockVariables{ client: client, workspaces: make(map[string]*tfe.VariableList), } } -func (m *mockVariables) List(ctx context.Context, workspaceID string, options tfe.VariableListOptions) (*tfe.VariableList, error) { +func (m *MockVariables) List(ctx context.Context, workspaceID string, options tfe.VariableListOptions) (*tfe.VariableList, error) { vl := m.workspaces[workspaceID] return vl, nil } -func (m *mockVariables) Create(ctx context.Context, workspaceID string, options tfe.VariableCreateOptions) (*tfe.Variable, error) { +func (m *MockVariables) Create(ctx context.Context, workspaceID string, options tfe.VariableCreateOptions) (*tfe.Variable, error) { v := &tfe.Variable{ - ID: generateID("var-"), + ID: GenerateID("var-"), Key: *options.Key, Category: *options.Category, } @@ -1073,33 +1064,33 @@ func (m *mockVariables) Create(ctx context.Context, workspaceID string, options return v, nil } -func (m *mockVariables) Read(ctx context.Context, workspaceID string, variableID string) (*tfe.Variable, error) { +func (m *MockVariables) Read(ctx context.Context, workspaceID string, variableID string) (*tfe.Variable, error) { panic("not implemented") } -func (m *mockVariables) Update(ctx context.Context, workspaceID string, variableID string, options tfe.VariableUpdateOptions) (*tfe.Variable, error) { +func (m *MockVariables) Update(ctx context.Context, workspaceID string, variableID string, options tfe.VariableUpdateOptions) (*tfe.Variable, error) { panic("not implemented") } -func (m *mockVariables) Delete(ctx context.Context, workspaceID string, variableID string) error { +func (m *MockVariables) Delete(ctx context.Context, workspaceID string, variableID string) error { panic("not implemented") } -type mockWorkspaces struct { - client *mockClient +type MockWorkspaces struct { + client *MockClient workspaceIDs map[string]*tfe.Workspace workspaceNames map[string]*tfe.Workspace } -func newMockWorkspaces(client *mockClient) *mockWorkspaces { - return &mockWorkspaces{ +func newMockWorkspaces(client *MockClient) *MockWorkspaces { + return &MockWorkspaces{ client: client, workspaceIDs: make(map[string]*tfe.Workspace), workspaceNames: make(map[string]*tfe.Workspace), } } -func (m *mockWorkspaces) List(ctx context.Context, organization string, options tfe.WorkspaceListOptions) (*tfe.WorkspaceList, error) { +func (m *MockWorkspaces) List(ctx context.Context, organization string, options tfe.WorkspaceListOptions) (*tfe.WorkspaceList, error) { dummyWorkspaces := 10 wl := &tfe.WorkspaceList{} @@ -1129,7 +1120,7 @@ func (m *mockWorkspaces) List(ctx context.Context, organization string, options if options.PageNumber <= 1 { for i := 0; i < dummyWorkspaces; i++ { wl.Items = append(wl.Items, &tfe.Workspace{ - ID: generateID("ws-"), + ID: GenerateID("ws-"), Name: fmt.Sprintf("dummy-workspace-%d", i), }) } @@ -1156,14 +1147,14 @@ func (m *mockWorkspaces) List(ctx context.Context, organization string, options return wl, nil } -func (m *mockWorkspaces) Create(ctx context.Context, organization string, options tfe.WorkspaceCreateOptions) (*tfe.Workspace, error) { +func (m *MockWorkspaces) Create(ctx context.Context, organization string, options tfe.WorkspaceCreateOptions) (*tfe.Workspace, error) { if strings.HasSuffix(*options.Name, "no-operations") { options.Operations = tfe.Bool(false) } else if options.Operations == nil { options.Operations = tfe.Bool(true) } w := &tfe.Workspace{ - ID: generateID("ws-"), + ID: GenerateID("ws-"), Name: *options.Name, Operations: *options.Operations, Permissions: &tfe.WorkspacePermissions{ @@ -1187,7 +1178,7 @@ func (m *mockWorkspaces) Create(ctx context.Context, organization string, option return w, nil } -func (m *mockWorkspaces) Read(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) { +func (m *MockWorkspaces) Read(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) { // custom error for TestCloud_plan500 in backend_plan_test.go if workspace == "network-error" { return nil, errors.New("I'm a little teacup") @@ -1200,7 +1191,7 @@ func (m *mockWorkspaces) Read(ctx context.Context, organization, workspace strin return w, nil } -func (m *mockWorkspaces) ReadByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { +func (m *MockWorkspaces) ReadByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { w, ok := m.workspaceIDs[workspaceID] if !ok { return nil, tfe.ErrResourceNotFound @@ -1208,7 +1199,19 @@ func (m *mockWorkspaces) ReadByID(ctx context.Context, workspaceID string) (*tfe return w, nil } -func (m *mockWorkspaces) Update(ctx context.Context, organization, workspace string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) { +func (m *MockWorkspaces) ReadWithOptions(ctx context.Context, organization string, workspace string, options *tfe.WorkspaceReadOptions) (*tfe.Workspace, error) { + panic("not implemented") +} + +func (m *MockWorkspaces) ReadByIDWithOptions(ctx context.Context, workspaceID string, options *tfe.WorkspaceReadOptions) (*tfe.Workspace, error) { + w, ok := m.workspaceIDs[workspaceID] + if !ok { + return nil, tfe.ErrResourceNotFound + } + return w, nil +} + +func (m *MockWorkspaces) Update(ctx context.Context, organization, workspace string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) { w, ok := m.workspaceNames[workspace] if !ok { return nil, tfe.ErrResourceNotFound @@ -1233,7 +1236,7 @@ func (m *mockWorkspaces) Update(ctx context.Context, organization, workspace str return w, nil } -func (m *mockWorkspaces) UpdateByID(ctx context.Context, workspaceID string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) { +func (m *MockWorkspaces) UpdateByID(ctx context.Context, workspaceID string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) { w, ok := m.workspaceIDs[workspaceID] if !ok { return nil, tfe.ErrResourceNotFound @@ -1255,7 +1258,7 @@ func (m *mockWorkspaces) UpdateByID(ctx context.Context, workspaceID string, opt return w, nil } -func (m *mockWorkspaces) Delete(ctx context.Context, organization, workspace string) error { +func (m *MockWorkspaces) Delete(ctx context.Context, organization, workspace string) error { if w, ok := m.workspaceNames[workspace]; ok { delete(m.workspaceIDs, w.ID) } @@ -1263,7 +1266,7 @@ func (m *mockWorkspaces) Delete(ctx context.Context, organization, workspace str return nil } -func (m *mockWorkspaces) DeleteByID(ctx context.Context, workspaceID string) error { +func (m *MockWorkspaces) DeleteByID(ctx context.Context, workspaceID string) error { if w, ok := m.workspaceIDs[workspaceID]; ok { delete(m.workspaceIDs, w.Name) } @@ -1271,7 +1274,7 @@ func (m *mockWorkspaces) DeleteByID(ctx context.Context, workspaceID string) err return nil } -func (m *mockWorkspaces) RemoveVCSConnection(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) { +func (m *MockWorkspaces) RemoveVCSConnection(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) { w, ok := m.workspaceNames[workspace] if !ok { return nil, tfe.ErrResourceNotFound @@ -1280,7 +1283,7 @@ func (m *mockWorkspaces) RemoveVCSConnection(ctx context.Context, organization, return w, nil } -func (m *mockWorkspaces) RemoveVCSConnectionByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { +func (m *MockWorkspaces) RemoveVCSConnectionByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { w, ok := m.workspaceIDs[workspaceID] if !ok { return nil, tfe.ErrResourceNotFound @@ -1289,7 +1292,7 @@ func (m *mockWorkspaces) RemoveVCSConnectionByID(ctx context.Context, workspaceI return w, nil } -func (m *mockWorkspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) { +func (m *MockWorkspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) { w, ok := m.workspaceIDs[workspaceID] if !ok { return nil, tfe.ErrResourceNotFound @@ -1301,7 +1304,7 @@ func (m *mockWorkspaces) Lock(ctx context.Context, workspaceID string, options t return w, nil } -func (m *mockWorkspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { +func (m *MockWorkspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { w, ok := m.workspaceIDs[workspaceID] if !ok { return nil, tfe.ErrResourceNotFound @@ -1313,7 +1316,7 @@ func (m *mockWorkspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.W return w, nil } -func (m *mockWorkspaces) ForceUnlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { +func (m *MockWorkspaces) ForceUnlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { w, ok := m.workspaceIDs[workspaceID] if !ok { return nil, tfe.ErrResourceNotFound @@ -1325,37 +1328,49 @@ func (m *mockWorkspaces) ForceUnlock(ctx context.Context, workspaceID string) (* return w, nil } -func (m *mockWorkspaces) AssignSSHKey(ctx context.Context, workspaceID string, options tfe.WorkspaceAssignSSHKeyOptions) (*tfe.Workspace, error) { +func (m *MockWorkspaces) AssignSSHKey(ctx context.Context, workspaceID string, options tfe.WorkspaceAssignSSHKeyOptions) (*tfe.Workspace, error) { panic("not implemented") } -func (m *mockWorkspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { +func (m *MockWorkspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { panic("not implemented") } -func (m *mockWorkspaces) RemoteStateConsumers(ctx context.Context, workspaceID string) (*tfe.WorkspaceList, error) { +func (m *MockWorkspaces) RemoteStateConsumers(ctx context.Context, workspaceID string) (*tfe.WorkspaceList, error) { panic("not implemented") } -func (m *mockWorkspaces) AddRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceAddRemoteStateConsumersOptions) error { +func (m *MockWorkspaces) AddRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceAddRemoteStateConsumersOptions) error { panic("not implemented") } -func (m *mockWorkspaces) RemoveRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceRemoveRemoteStateConsumersOptions) error { +func (m *MockWorkspaces) RemoveRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceRemoveRemoteStateConsumersOptions) error { panic("not implemented") } -func (m *mockWorkspaces) UpdateRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceUpdateRemoteStateConsumersOptions) error { +func (m *MockWorkspaces) UpdateRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceUpdateRemoteStateConsumersOptions) error { panic("not implemented") } -func (m *mockWorkspaces) Readme(ctx context.Context, workspaceID string) (io.Reader, error) { +func (m *MockWorkspaces) Readme(ctx context.Context, workspaceID string) (io.Reader, error) { + panic("not implemented") +} + +func (m *MockWorkspaces) Tags(ctx context.Context, workspaceID string, options tfe.WorkspaceTagListOptions) (*tfe.TagList, error) { + panic("not implemented") +} + +func (m *MockWorkspaces) AddTags(ctx context.Context, workspaceID string, options tfe.WorkspaceAddTagsOptions) error { + panic("not implemented") +} + +func (m *MockWorkspaces) RemoveTags(ctx context.Context, workspaceID string, options tfe.WorkspaceRemoveTagsOptions) error { panic("not implemented") } const alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" -func generateID(s string) string { +func GenerateID(s string) string { b := make([]byte, 16) for i := range b { b[i] = alphanumeric[rand.Intn(len(alphanumeric))]