2018-10-31 10:45:03 -05:00
|
|
|
package remote
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"encoding/base64"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"math/rand"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
2020-09-30 12:36:04 -05:00
|
|
|
"sync"
|
2018-10-31 10:45:03 -05:00
|
|
|
"time"
|
|
|
|
|
|
|
|
tfe "github.com/hashicorp/go-tfe"
|
2021-05-17 14:46:19 -05:00
|
|
|
"github.com/hashicorp/terraform/internal/terraform"
|
backend: Validate remote backend Terraform version
When using the enhanced remote backend, a subset of all Terraform
operations are supported. Of these, only plan and apply can be executed
on the remote infrastructure (e.g. Terraform Cloud). Other operations
run locally and use the remote backend for state storage.
This causes problems when the local version of Terraform does not match
the configured version from the remote workspace. If the two versions
are incompatible, an `import` or `state mv` operation can cause the
remote workspace to be unusable until a manual fix is applied.
To prevent this from happening accidentally, this commit introduces a
check that the local Terraform version and the configured remote
workspace Terraform version are compatible. This check is skipped for
commands which do not write state, and can also be disabled by the use
of a new command-line flag, `-ignore-remote-version`.
Terraform version compatibility is defined as:
- For all releases before 0.14.0, local must exactly equal remote, as
two different versions cannot share state;
- 0.14.0 to 1.0.x are compatible, as we will not change the state
version number until at least Terraform 1.1.0;
- Versions after 1.1.0 must have the same major and minor versions, as
we will not change the state version number in a patch release.
If the two versions are incompatible, a diagnostic is displayed,
advising that the error can be suppressed with `-ignore-remote-version`.
When this flag is used, the diagnostic is still displayed, but as a
warning instead of an error.
Commands which will not write state can assert this fact by calling the
helper `meta.ignoreRemoteBackendVersionConflict`, which will disable the
checks. Those which can write state should instead call the helper
`meta.remoteBackendVersionCheck`, which will return diagnostics for
display.
In addition to these explicit paths for managing the version check, we
have an implicit check in the remote backend's state manager
initialization method. Both of the above helpers will disable this
check. This fallback is in place to ensure that future code paths which
access state cannot accidentally skip the remote version check.
2020-11-13 15:43:56 -06:00
|
|
|
tfversion "github.com/hashicorp/terraform/version"
|
2020-09-30 12:36:04 -05:00
|
|
|
"github.com/mitchellh/copystructure"
|
2018-10-31 10:45:03 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
type mockClient struct {
|
|
|
|
Applies *mockApplies
|
|
|
|
ConfigurationVersions *mockConfigurationVersions
|
2019-09-27 16:22:34 -05:00
|
|
|
CostEstimates *mockCostEstimates
|
2018-10-31 10:45:03 -05:00
|
|
|
Organizations *mockOrganizations
|
|
|
|
Plans *mockPlans
|
|
|
|
PolicyChecks *mockPolicyChecks
|
|
|
|
Runs *mockRuns
|
|
|
|
StateVersions *mockStateVersions
|
2019-11-13 10:34:09 -06:00
|
|
|
Variables *mockVariables
|
2018-10-31 10:45:03 -05:00
|
|
|
Workspaces *mockWorkspaces
|
|
|
|
}
|
|
|
|
|
|
|
|
func newMockClient() *mockClient {
|
|
|
|
c := &mockClient{}
|
|
|
|
c.Applies = newMockApplies(c)
|
|
|
|
c.ConfigurationVersions = newMockConfigurationVersions(c)
|
2019-09-16 13:30:24 -05:00
|
|
|
c.CostEstimates = newMockCostEstimates(c)
|
2018-10-31 10:45:03 -05:00
|
|
|
c.Organizations = newMockOrganizations(c)
|
|
|
|
c.Plans = newMockPlans(c)
|
|
|
|
c.PolicyChecks = newMockPolicyChecks(c)
|
|
|
|
c.Runs = newMockRuns(c)
|
|
|
|
c.StateVersions = newMockStateVersions(c)
|
2019-11-13 10:34:09 -06:00
|
|
|
c.Variables = newMockVariables(c)
|
2018-10-31 10:45:03 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-09-26 16:35:33 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
// mockInput is a mock implementation of terraform.UIInput.
|
|
|
|
type mockInput struct {
|
|
|
|
answers map[string]string
|
|
|
|
}
|
|
|
|
|
2019-03-07 14:07:13 -06:00
|
|
|
func (m *mockInput) Input(ctx context.Context, opts *terraform.InputOpts) (string, error) {
|
2018-10-31 10:45:03 -05:00
|
|
|
v, ok := m.answers[opts.Id]
|
|
|
|
if !ok {
|
|
|
|
return "", fmt.Errorf("unexpected input request in test: %s", opts.Id)
|
|
|
|
}
|
2019-03-08 03:37:25 -06:00
|
|
|
if v == "wait-for-external-update" {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
case <-time.After(time.Minute):
|
|
|
|
}
|
|
|
|
}
|
2018-10-31 10:45:03 -05:00
|
|
|
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
|
|
|
|
}
|
2020-11-18 14:35:48 -06:00
|
|
|
time.Sleep(1 * time.Millisecond)
|
2018-10-31 10:45:03 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-12-05 05:29:08 -06:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
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 {
|
2021-05-03 22:25:56 -05:00
|
|
|
client *mockClient
|
|
|
|
logs map[string]string
|
|
|
|
planOutputs map[string]string
|
|
|
|
plans map[string]*tfe.Plan
|
2018-10-31 10:45:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func newMockPlans(client *mockClient) *mockPlans {
|
|
|
|
return &mockPlans{
|
2021-05-03 22:25:56 -05:00
|
|
|
client: client,
|
|
|
|
logs: make(map[string]string),
|
|
|
|
planOutputs: make(map[string]string),
|
|
|
|
plans: make(map[string]*tfe.Plan),
|
2018-10-31 10:45:03 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2021-05-03 22:25:56 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
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 {
|
2020-09-30 12:36:04 -05:00
|
|
|
sync.Mutex
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
client *mockClient
|
|
|
|
runs map[string]*tfe.Run
|
|
|
|
workspaces map[string][]*tfe.Run
|
2020-05-18 17:12:44 -05:00
|
|
|
|
|
|
|
// 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)
|
2018-10-31 10:45:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2020-09-30 12:36:04 -05:00
|
|
|
m.Lock()
|
|
|
|
defer m.Unlock()
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
|
|
|
|
if !ok {
|
|
|
|
return nil, tfe.ErrResourceNotFound
|
|
|
|
}
|
|
|
|
|
2020-09-30 12:36:04 -05:00
|
|
|
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))
|
2018-10-31 10:45:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2020-09-30 12:36:04 -05:00
|
|
|
m.Lock()
|
|
|
|
defer m.Unlock()
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
a, err := m.client.Applies.create(options.ConfigurationVersion.ID, options.Workspace.ID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-09-27 16:22:34 -05:00
|
|
|
ce, err := m.client.CostEstimates.create(options.ConfigurationVersion.ID, options.Workspace.ID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
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{
|
2019-09-27 16:22:34 -05:00
|
|
|
ID: generateID("run-"),
|
|
|
|
Actions: &tfe.RunActions{IsCancelable: true},
|
|
|
|
Apply: a,
|
|
|
|
CostEstimate: ce,
|
|
|
|
HasChanges: false,
|
|
|
|
Permissions: &tfe.RunPermissions{},
|
|
|
|
Plan: p,
|
2021-05-13 23:28:16 -05:00
|
|
|
ReplaceAddrs: options.ReplaceAddrs,
|
2019-09-27 16:22:34 -05:00
|
|
|
Status: tfe.RunPending,
|
2020-05-01 18:01:36 -05:00
|
|
|
TargetAddrs: options.TargetAddrs,
|
2018-10-31 10:45:03 -05:00
|
|
|
}
|
|
|
|
|
2020-05-13 10:05:22 -05:00
|
|
|
if options.Message != nil {
|
|
|
|
r.Message = *options.Message
|
|
|
|
}
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
if pc != nil {
|
|
|
|
r.PolicyChecks = []*tfe.PolicyCheck{pc}
|
|
|
|
}
|
|
|
|
|
|
|
|
if options.IsDestroy != nil {
|
|
|
|
r.IsDestroy = *options.IsDestroy
|
|
|
|
}
|
|
|
|
|
2021-05-13 23:28:16 -05:00
|
|
|
if options.Refresh != nil {
|
|
|
|
r.Refresh = *options.Refresh
|
|
|
|
}
|
|
|
|
|
|
|
|
if options.RefreshOnly != nil {
|
|
|
|
r.RefreshOnly = *options.RefreshOnly
|
|
|
|
}
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
w, ok := m.client.Workspaces.workspaceIDs[options.Workspace.ID]
|
|
|
|
if !ok {
|
|
|
|
return nil, tfe.ErrResourceNotFound
|
|
|
|
}
|
|
|
|
if w.CurrentRun == nil {
|
|
|
|
w.CurrentRun = r
|
|
|
|
}
|
|
|
|
|
2020-05-18 17:12:44 -05:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
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) {
|
2021-05-03 22:25:56 -05:00
|
|
|
return m.ReadWithOptions(ctx, runID, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockRuns) ReadWithOptions(ctx context.Context, runID string, _ *tfe.RunReadOptions) (*tfe.Run, error) {
|
2020-09-30 12:36:04 -05:00
|
|
|
m.Lock()
|
|
|
|
defer m.Unlock()
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
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])
|
2019-03-08 03:37:25 -06:00
|
|
|
if r.Status == tfe.RunPlanning && r.Plan.Status == tfe.PlanFinished {
|
2018-10-31 10:45:03 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-30 12:36:04 -05:00
|
|
|
// we must return a copy for the client
|
|
|
|
rc, err := copystructure.Copy(r)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return rc.(*tfe.Run), nil
|
2018-10-31 10:45:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockRuns) Apply(ctx context.Context, runID string, options tfe.RunApplyOptions) error {
|
2020-09-30 12:36:04 -05:00
|
|
|
m.Lock()
|
|
|
|
defer m.Unlock()
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
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
|
2019-03-08 03:37:25 -06:00
|
|
|
r.Actions.IsConfirmable = false
|
2018-10-31 10:45:03 -05:00
|
|
|
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 {
|
2020-09-30 12:36:04 -05:00
|
|
|
m.Lock()
|
|
|
|
defer m.Unlock()
|
|
|
|
|
2019-03-08 03:37:25 -06:00
|
|
|
r, ok := m.runs[runID]
|
|
|
|
if !ok {
|
|
|
|
return tfe.ErrResourceNotFound
|
|
|
|
}
|
|
|
|
r.Status = tfe.RunDiscarded
|
|
|
|
r.Actions.IsConfirmable = false
|
|
|
|
return nil
|
2018-10-31 10:45:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2021-05-03 22:25:56 -05:00
|
|
|
return m.ReadWithOptions(ctx, svID, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockStateVersions) ReadWithOptions(ctx context.Context, svID string, options *tfe.StateVersionReadOptions) (*tfe.StateVersion, error) {
|
2018-10-31 10:45:03 -05:00
|
|
|
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) {
|
2021-05-03 22:25:56 -05:00
|
|
|
return m.CurrentWithOptions(ctx, workspaceID, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockStateVersions) CurrentWithOptions(ctx context.Context, workspaceID string, options *tfe.StateVersionCurrentOptions) (*tfe.StateVersion, error) {
|
2018-10-31 10:45:03 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-11-13 10:34:09 -06:00
|
|
|
type mockVariables struct {
|
|
|
|
client *mockClient
|
|
|
|
workspaces map[string]*tfe.VariableList
|
|
|
|
}
|
|
|
|
|
2020-04-30 16:56:32 -05:00
|
|
|
var _ tfe.Variables = (*mockVariables)(nil)
|
|
|
|
|
2019-11-13 10:34:09 -06:00
|
|
|
func newMockVariables(client *mockClient) *mockVariables {
|
|
|
|
return &mockVariables{
|
|
|
|
client: client,
|
|
|
|
workspaces: make(map[string]*tfe.VariableList),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-30 16:56:32 -05:00
|
|
|
func (m *mockVariables) List(ctx context.Context, workspaceID string, options tfe.VariableListOptions) (*tfe.VariableList, error) {
|
|
|
|
vl := m.workspaces[workspaceID]
|
2019-11-13 10:34:09 -06:00
|
|
|
return vl, nil
|
|
|
|
}
|
|
|
|
|
2020-04-30 16:56:32 -05:00
|
|
|
func (m *mockVariables) Create(ctx context.Context, workspaceID string, options tfe.VariableCreateOptions) (*tfe.Variable, error) {
|
2019-11-13 10:34:09 -06:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-04-30 16:56:32 -05:00
|
|
|
workspace := workspaceID
|
2019-11-13 10:34:09 -06:00
|
|
|
|
|
|
|
if m.workspaces[workspace] == nil {
|
|
|
|
m.workspaces[workspace] = &tfe.VariableList{}
|
|
|
|
}
|
|
|
|
|
|
|
|
vl := m.workspaces[workspace]
|
|
|
|
vl.Items = append(vl.Items, v)
|
|
|
|
|
|
|
|
return v, nil
|
|
|
|
}
|
|
|
|
|
2020-04-30 16:56:32 -05:00
|
|
|
func (m *mockVariables) Read(ctx context.Context, workspaceID string, variableID string) (*tfe.Variable, error) {
|
2019-11-13 10:34:09 -06:00
|
|
|
panic("not implemented")
|
|
|
|
}
|
|
|
|
|
2020-04-30 16:56:32 -05:00
|
|
|
func (m *mockVariables) Update(ctx context.Context, workspaceID string, variableID string, options tfe.VariableUpdateOptions) (*tfe.Variable, error) {
|
2019-11-13 10:34:09 -06:00
|
|
|
panic("not implemented")
|
|
|
|
}
|
|
|
|
|
2020-04-30 16:56:32 -05:00
|
|
|
func (m *mockVariables) Delete(ctx context.Context, workspaceID string, variableID string) error {
|
2019-11-13 10:34:09 -06:00
|
|
|
panic("not implemented")
|
|
|
|
}
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
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) {
|
backend: Validate remote backend Terraform version
When using the enhanced remote backend, a subset of all Terraform
operations are supported. Of these, only plan and apply can be executed
on the remote infrastructure (e.g. Terraform Cloud). Other operations
run locally and use the remote backend for state storage.
This causes problems when the local version of Terraform does not match
the configured version from the remote workspace. If the two versions
are incompatible, an `import` or `state mv` operation can cause the
remote workspace to be unusable until a manual fix is applied.
To prevent this from happening accidentally, this commit introduces a
check that the local Terraform version and the configured remote
workspace Terraform version are compatible. This check is skipped for
commands which do not write state, and can also be disabled by the use
of a new command-line flag, `-ignore-remote-version`.
Terraform version compatibility is defined as:
- For all releases before 0.14.0, local must exactly equal remote, as
two different versions cannot share state;
- 0.14.0 to 1.0.x are compatible, as we will not change the state
version number until at least Terraform 1.1.0;
- Versions after 1.1.0 must have the same major and minor versions, as
we will not change the state version number in a patch release.
If the two versions are incompatible, a diagnostic is displayed,
advising that the error can be suppressed with `-ignore-remote-version`.
When this flag is used, the diagnostic is still displayed, but as a
warning instead of an error.
Commands which will not write state can assert this fact by calling the
helper `meta.ignoreRemoteBackendVersionConflict`, which will disable the
checks. Those which can write state should instead call the helper
`meta.remoteBackendVersionCheck`, which will return diagnostics for
display.
In addition to these explicit paths for managing the version check, we
have an implicit check in the remote backend's state manager
initialization method. Both of the above helpers will disable this
check. This fallback is in place to ensure that future code paths which
access state cannot accidentally skip the remote version check.
2020-11-13 15:43:56 -06:00
|
|
|
if strings.HasSuffix(*options.Name, "no-operations") {
|
|
|
|
options.Operations = tfe.Bool(false)
|
|
|
|
} else if options.Operations == nil {
|
|
|
|
options.Operations = tfe.Bool(true)
|
|
|
|
}
|
2018-10-31 10:45:03 -05:00
|
|
|
w := &tfe.Workspace{
|
2018-11-15 13:26:46 -06:00
|
|
|
ID: generateID("ws-"),
|
|
|
|
Name: *options.Name,
|
backend: Validate remote backend Terraform version
When using the enhanced remote backend, a subset of all Terraform
operations are supported. Of these, only plan and apply can be executed
on the remote infrastructure (e.g. Terraform Cloud). Other operations
run locally and use the remote backend for state storage.
This causes problems when the local version of Terraform does not match
the configured version from the remote workspace. If the two versions
are incompatible, an `import` or `state mv` operation can cause the
remote workspace to be unusable until a manual fix is applied.
To prevent this from happening accidentally, this commit introduces a
check that the local Terraform version and the configured remote
workspace Terraform version are compatible. This check is skipped for
commands which do not write state, and can also be disabled by the use
of a new command-line flag, `-ignore-remote-version`.
Terraform version compatibility is defined as:
- For all releases before 0.14.0, local must exactly equal remote, as
two different versions cannot share state;
- 0.14.0 to 1.0.x are compatible, as we will not change the state
version number until at least Terraform 1.1.0;
- Versions after 1.1.0 must have the same major and minor versions, as
we will not change the state version number in a patch release.
If the two versions are incompatible, a diagnostic is displayed,
advising that the error can be suppressed with `-ignore-remote-version`.
When this flag is used, the diagnostic is still displayed, but as a
warning instead of an error.
Commands which will not write state can assert this fact by calling the
helper `meta.ignoreRemoteBackendVersionConflict`, which will disable the
checks. Those which can write state should instead call the helper
`meta.remoteBackendVersionCheck`, which will return diagnostics for
display.
In addition to these explicit paths for managing the version check, we
have an implicit check in the remote backend's state manager
initialization method. Both of the above helpers will disable this
check. This fallback is in place to ensure that future code paths which
access state cannot accidentally skip the remote version check.
2020-11-13 15:43:56 -06:00
|
|
|
Operations: *options.Operations,
|
2018-10-31 10:45:03 -05:00
|
|
|
Permissions: &tfe.WorkspacePermissions{
|
2019-02-26 12:12:53 -06:00
|
|
|
CanQueueApply: true,
|
|
|
|
CanQueueRun: true,
|
2018-10-31 10:45:03 -05:00
|
|
|
},
|
|
|
|
}
|
|
|
|
if options.AutoApply != nil {
|
|
|
|
w.AutoApply = *options.AutoApply
|
|
|
|
}
|
|
|
|
if options.VCSRepo != nil {
|
|
|
|
w.VCSRepo = &tfe.VCSRepo{}
|
|
|
|
}
|
backend: Validate remote backend Terraform version
When using the enhanced remote backend, a subset of all Terraform
operations are supported. Of these, only plan and apply can be executed
on the remote infrastructure (e.g. Terraform Cloud). Other operations
run locally and use the remote backend for state storage.
This causes problems when the local version of Terraform does not match
the configured version from the remote workspace. If the two versions
are incompatible, an `import` or `state mv` operation can cause the
remote workspace to be unusable until a manual fix is applied.
To prevent this from happening accidentally, this commit introduces a
check that the local Terraform version and the configured remote
workspace Terraform version are compatible. This check is skipped for
commands which do not write state, and can also be disabled by the use
of a new command-line flag, `-ignore-remote-version`.
Terraform version compatibility is defined as:
- For all releases before 0.14.0, local must exactly equal remote, as
two different versions cannot share state;
- 0.14.0 to 1.0.x are compatible, as we will not change the state
version number until at least Terraform 1.1.0;
- Versions after 1.1.0 must have the same major and minor versions, as
we will not change the state version number in a patch release.
If the two versions are incompatible, a diagnostic is displayed,
advising that the error can be suppressed with `-ignore-remote-version`.
When this flag is used, the diagnostic is still displayed, but as a
warning instead of an error.
Commands which will not write state can assert this fact by calling the
helper `meta.ignoreRemoteBackendVersionConflict`, which will disable the
checks. Those which can write state should instead call the helper
`meta.remoteBackendVersionCheck`, which will return diagnostics for
display.
In addition to these explicit paths for managing the version check, we
have an implicit check in the remote backend's state manager
initialization method. Both of the above helpers will disable this
check. This fallback is in place to ensure that future code paths which
access state cannot accidentally skip the remote version check.
2020-11-13 15:43:56 -06:00
|
|
|
if options.TerraformVersion != nil {
|
|
|
|
w.TerraformVersion = *options.TerraformVersion
|
|
|
|
} else {
|
|
|
|
w.TerraformVersion = tfversion.String()
|
|
|
|
}
|
2018-10-31 10:45:03 -05:00
|
|
|
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) {
|
2019-07-22 08:06:39 -05:00
|
|
|
// custom error for TestRemote_plan500 in backend_plan_test.go
|
|
|
|
if workspace == "network-error" {
|
|
|
|
return nil, errors.New("I'm a little teacup")
|
|
|
|
}
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
w, ok := m.workspaceNames[workspace]
|
|
|
|
if !ok {
|
|
|
|
return nil, tfe.ErrResourceNotFound
|
|
|
|
}
|
|
|
|
return w, nil
|
|
|
|
}
|
|
|
|
|
2019-09-27 16:22:34 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
backend: Validate remote backend Terraform version
When using the enhanced remote backend, a subset of all Terraform
operations are supported. Of these, only plan and apply can be executed
on the remote infrastructure (e.g. Terraform Cloud). Other operations
run locally and use the remote backend for state storage.
This causes problems when the local version of Terraform does not match
the configured version from the remote workspace. If the two versions
are incompatible, an `import` or `state mv` operation can cause the
remote workspace to be unusable until a manual fix is applied.
To prevent this from happening accidentally, this commit introduces a
check that the local Terraform version and the configured remote
workspace Terraform version are compatible. This check is skipped for
commands which do not write state, and can also be disabled by the use
of a new command-line flag, `-ignore-remote-version`.
Terraform version compatibility is defined as:
- For all releases before 0.14.0, local must exactly equal remote, as
two different versions cannot share state;
- 0.14.0 to 1.0.x are compatible, as we will not change the state
version number until at least Terraform 1.1.0;
- Versions after 1.1.0 must have the same major and minor versions, as
we will not change the state version number in a patch release.
If the two versions are incompatible, a diagnostic is displayed,
advising that the error can be suppressed with `-ignore-remote-version`.
When this flag is used, the diagnostic is still displayed, but as a
warning instead of an error.
Commands which will not write state can assert this fact by calling the
helper `meta.ignoreRemoteBackendVersionConflict`, which will disable the
checks. Those which can write state should instead call the helper
`meta.remoteBackendVersionCheck`, which will return diagnostics for
display.
In addition to these explicit paths for managing the version check, we
have an implicit check in the remote backend's state manager
initialization method. Both of the above helpers will disable this
check. This fallback is in place to ensure that future code paths which
access state cannot accidentally skip the remote version check.
2020-11-13 15:43:56 -06:00
|
|
|
if options.Operations != nil {
|
|
|
|
w.Operations = *options.Operations
|
|
|
|
}
|
2018-10-31 10:45:03 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-09-27 16:22:34 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-09-27 16:22:34 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-02-25 07:37:58 -06:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-09-27 16:22:34 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
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
|
|
|
|
}
|
2018-11-30 12:31:58 -06:00
|
|
|
if w.Locked {
|
|
|
|
return nil, tfe.ErrWorkspaceLocked
|
|
|
|
}
|
2018-10-31 10:45:03 -05:00
|
|
|
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
|
|
|
|
}
|
2018-11-30 12:31:58 -06:00
|
|
|
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
|
|
|
|
}
|
2018-10-31 10:45:03 -05:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2021-05-03 22:25:56 -05:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2018-10-31 10:45:03 -05:00
|
|
|
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)
|
|
|
|
}
|