From bddf6a9b34e8202441669b734463f40712f9fe4a Mon Sep 17 00:00:00 2001 From: Megan Bang Date: Mon, 29 Aug 2022 14:13:18 -0500 Subject: [PATCH] updating to use the latest version of cloud/state.go and just pass schemas along to PersistState in the remote state --- .../remote-state/inmem/backend_test.go | 2 + internal/cloud/backend.go | 24 +- internal/cloud/backend_state_test.go | 115 +++-- internal/cloud/state.go | 446 ++---------------- internal/cloud/state_test.go | 11 +- internal/cloud/testing.go | 101 +--- .../command/state_replace_provider_test.go | 19 + 7 files changed, 158 insertions(+), 560 deletions(-) diff --git a/internal/backend/remote-state/inmem/backend_test.go b/internal/backend/remote-state/inmem/backend_test.go index 204858824d..b7e9a555a9 100644 --- a/internal/backend/remote-state/inmem/backend_test.go +++ b/internal/backend/remote-state/inmem/backend_test.go @@ -5,6 +5,8 @@ import ( "os" "testing" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/backend" statespkg "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/remote" diff --git a/internal/cloud/backend.go b/internal/cloud/backend.go index ded38f75f2..669cc37227 100644 --- a/internal/cloud/backend.go +++ b/internal/cloud/backend.go @@ -526,10 +526,15 @@ func (b *Cloud) DeleteWorkspace(name string) error { } // Configure the remote workspace name. - State := &State{tfeClient: b.client, organization: b.organization, workspace: &tfe.Workspace{ - Name: name, - }} - return State.Delete() + client := &remoteClient{ + client: b.client, + organization: b.organization, + workspace: &tfe.Workspace{ + Name: name, + }, + } + + return client.Delete() } // StateMgr implements backend.Enhanced. @@ -614,7 +619,16 @@ func (b *Cloud) StateMgr(name string) (statemgr.Full, error) { } } - return &State{tfeClient: b.client, organization: b.organization, workspace: workspace}, nil + client := &remoteClient{ + client: b.client, + organization: b.organization, + workspace: workspace, + + // This is optionally set during Terraform Enterprise runs. + runID: os.Getenv("TFE_RUN_ID"), + } + + return NewState(client), nil } // Operation implements backend.Enhanced. diff --git a/internal/cloud/backend_state_test.go b/internal/cloud/backend_state_test.go index 223ddb9153..3b9833c38e 100644 --- a/internal/cloud/backend_state_test.go +++ b/internal/cloud/backend_state_test.go @@ -2,68 +2,69 @@ package cloud import ( "bytes" - "io/ioutil" "os" "testing" + tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statefile" ) -func TestCloudState_impl(t *testing.T) { +func TestRemoteClient_impl(t *testing.T) { var _ remote.Client = new(remoteClient) } -func TestCloudState(t *testing.T) { - state := testCloudState(t) - TestState(t, state) +func TestRemoteClient(t *testing.T) { + client := testRemoteClient(t) + remote.TestClient(t, client) } -//func TestRemoteClient_stateVersionCreated(t *testing.T) { -// b, bCleanup := testBackendWithName(t) -// defer bCleanup() -// -// raw, err := b.StateMgr(testBackendSingleWorkspaceName) -// if err != nil { -// t.Fatalf("error: %v", err) -// } -// -// state := raw.(*State) -// -// err = state.WriteState(([]byte)(` -//{ -// "version": 4, -// "terraform_version": "1.3.0", -// "serial": 1, -// "lineage": "backend-change", -// "outputs": { -// "foo": { -// "type": "string", -// "value": "bar" -// } -// } -//}`)) -// if err != nil { -// t.Fatalf("expected no error, got %v", err) -// } -// -// stateVersionsAPI := b.client.StateVersions.(*MockStateVersions) -// if got, want := len(stateVersionsAPI.stateVersions), 1; got != want { -// t.Fatalf("wrong number of state versions in the mock client %d; want %d", got, want) -// } -// -// var stateVersion *tfe.StateVersion -// for _, sv := range stateVersionsAPI.stateVersions { -// stateVersion = sv -// } -// -// if stateVersionsAPI.outputStates[stateVersion.ID] == nil || len(stateVersionsAPI.outputStates[stateVersion.ID]) == 0 { -// t.Fatal("no state version outputs in the mock client") -// } -//} +func TestRemoteClient_stateVersionCreated(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() -func TestCLoudState_TestRemoteLocks(t *testing.T) { + raw, err := b.StateMgr(testBackendSingleWorkspaceName) + if err != nil { + t.Fatalf("error: %v", err) + } + + client := raw.(*State).Client + + err = client.Put(([]byte)(` +{ + "version": 4, + "terraform_version": "1.3.0", + "serial": 1, + "lineage": "backend-change", + "outputs": { + "foo": { + "type": "string", + "value": "bar" + } + } +}`)) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + stateVersionsAPI := b.client.StateVersions.(*MockStateVersions) + if got, want := len(stateVersionsAPI.stateVersions), 1; got != want { + t.Fatalf("wrong number of state versions in the mock client %d; want %d", got, want) + } + + var stateVersion *tfe.StateVersion + for _, sv := range stateVersionsAPI.stateVersions { + stateVersion = sv + } + + if stateVersionsAPI.outputStates[stateVersion.ID] == nil || len(stateVersionsAPI.outputStates[stateVersion.ID]) == 0 { + t.Fatal("no state version outputs in the mock client") + } +} + +func TestRemoteClient_TestRemoteLocks(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() @@ -77,30 +78,26 @@ func TestCLoudState_TestRemoteLocks(t *testing.T) { t.Fatalf("expected no error, got %v", err) } - TestCloudLocks(t, s1, s2) + remote.TestRemoteLocks(t, s1.(*State).Client, s2.(*State).Client) } -func TestCloudState_withRunID(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 { t.Fatalf("error setting env var TFE_RUN_ID: %v", err) } // Create a new test client. - state := testCloudState(t) + client := testRemoteClient(t) // Create a new empty state. sf := statefile.New(states.NewState(), "", 0) var buf bytes.Buffer statefile.Write(sf, &buf) - jsonState, err := ioutil.ReadFile("../command/testdata/show-json-state/sensitive-variables/output.json") - - if err != nil { - t.Fatal(err) - } - - if err := state.uploadState(state.lineage, state.serial, state.forcePush, buf.Bytes(), jsonState); err != nil { - t.Fatalf("put: %s", err) + // Store the new state to verify (this will be done + // by the mock that is used) that the run ID is set. + if err := client.Put(buf.Bytes()); err != nil { + t.Fatalf("expected no error, got %v", err) } } diff --git a/internal/cloud/state.go b/internal/cloud/state.go index 44f5faa1e1..fac19add6f 100644 --- a/internal/cloud/state.go +++ b/internal/cloud/state.go @@ -1,59 +1,30 @@ package cloud import ( - "bytes" "context" - "crypto/md5" - "encoding/base64" "encoding/json" "errors" "fmt" "log" - "os" "strings" - "sync" - tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/go-tfe" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/gocty" - uuid "github.com/hashicorp/go-uuid" - "github.com/hashicorp/terraform/internal/command/jsonstate" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/remote" - "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" ) -// State implements the State interfaces in the state package to handle -// reading and writing the remote state to TFC. This State on its own does no -// local caching so every persist will go to the remote storage and local -// writes will go to memory. +// State is similar to remote State and delegates to it, except in the case of output values, +// which use a separate methodology that ensures the caller is authorized to read cloud +// workspace outputs. type State struct { - mu sync.Mutex + Client *remoteClient - // Client Client - - // We track two pieces of meta data in addition to the state itself: - // - // lineage - the state's unique ID - // serial - the monotonic counter of "versions" of the state - // - // Both of these (along with state) have a sister field - // that represents the values read in from an existing source. - // All three of these values are used to determine if the new - // state has changed from an existing state we read in. - lineage, readLineage string - serial, readSerial uint64 - state, readState *states.State - disableLocks bool - tfeClient *tfe.Client - organization string - workspace *tfe.Workspace - stateUploadErr bool - forcePush bool - lockInfo *statemgr.LockInfo + delegate remote.State } var ErrStateVersionUnauthorizedUpgradeState = errors.New(strings.TrimSpace(` @@ -63,371 +34,70 @@ of authorization and therefore this error can usually be fixed by upgrading the remote state version. `)) -var _ statemgr.Full = (*State)(nil) -var _ statemgr.Migrator = (*State)(nil) +// Proof that cloud State is a statemgr.Persistent interface +var _ statemgr.Persistent = (*State)(nil) -// statemgr.Reader impl. +func NewState(client *remoteClient) *State { + return &State{ + Client: client, + delegate: remote.State{Client: client}, + } +} + +// State delegates calls to read State to the remote State func (s *State) State() *states.State { - s.mu.Lock() - defer s.mu.Unlock() - - return s.state.DeepCopy() + return s.delegate.State() } -// StateForMigration is part of our implementation of statemgr.Migrator. -func (s *State) StateForMigration() *statefile.File { - s.mu.Lock() - defer s.mu.Unlock() - - return statefile.New(s.state.DeepCopy(), s.lineage, s.serial) -} - -// WriteStateForMigration is part of our implementation of statemgr.Migrator. -func (s *State) WriteStateForMigration(f *statefile.File, force bool) error { - s.mu.Lock() - defer s.mu.Unlock() - - if !force { - checkFile := statefile.New(s.state, s.lineage, s.serial) - if err := statemgr.CheckValidImport(f, checkFile); err != nil { - return err - } - } - - // The remote backend needs to pass the `force` flag through to its client. - // For backends that support such operations, inform the client - // that a force push has been requested - if force { - s.EnableForcePush() - } - - // We create a deep copy of the state here, because the caller also has - // a reference to the given object and can potentially go on to mutate - // it after we return, but we want the snapshot at this point in time. - s.state = f.State.DeepCopy() - s.lineage = f.Lineage - s.serial = f.Serial - - return nil -} - -// DisableLocks turns the Lock and Unlock methods into no-ops. This is intended -// to be called during initialization of a state manager and should not be -// called after any of the statemgr.Full interface methods have been called. -func (s *State) DisableLocks() { - s.disableLocks = true -} - -// StateSnapshotMeta returns the metadata from the most recently persisted -// or refreshed persistent state snapshot. -// -// This is an implementation of statemgr.PersistentMeta. -func (s *State) StateSnapshotMeta() statemgr.SnapshotMeta { - return statemgr.SnapshotMeta{ - Lineage: s.lineage, - Serial: s.serial, - } -} - -// statemgr.Writer impl. -func (s *State) WriteState(state *states.State) error { - s.mu.Lock() - defer s.mu.Unlock() - - // We create a deep copy of the state here, because the caller also has - // a reference to the given object and can potentially go on to mutate - // it after we return, but we want the snapshot at this point in time. - s.state = state.DeepCopy() - - return nil -} - -// statemgr.Persister impl. -func (s *State) PersistState(schemas *terraform.Schemas) error { - s.mu.Lock() - defer s.mu.Unlock() - - if s.readState != nil { - lineageUnchanged := s.readLineage != "" && s.lineage == s.readLineage - serialUnchanged := s.readSerial != 0 && s.serial == s.readSerial - stateUnchanged := statefile.StatesMarshalEqual(s.state, s.readState) - if stateUnchanged && lineageUnchanged && serialUnchanged { - // If the state, lineage or serial haven't changed at all then we have nothing to do. - return nil - } - s.serial++ - } else { - // We might be writing a new state altogether, but before we do that - // we'll check to make sure there isn't already a snapshot present - // that we ought to be updating. - err := s.refreshState() - if err != nil { - return fmt.Errorf("failed checking for existing remote state: %s", err) - } - if s.lineage == "" { // indicates that no state snapshot is present yet - lineage, err := uuid.GenerateUUID() - if err != nil { - return fmt.Errorf("failed to generate initial lineage: %v", err) - } - s.lineage = lineage - s.serial = 0 - } - } - - f := statefile.New(s.state, s.lineage, s.serial) - - var buf bytes.Buffer - err := statefile.Write(f, &buf) - if err != nil { - return err - } - - var jsonState []byte - if schemas != nil { - jsonState, err = jsonstate.Marshal(f, schemas) - if err != nil { - return err - } - } - - err = s.uploadState(s.lineage, s.serial, s.forcePush, buf.Bytes(), jsonState) - if err != nil { - s.stateUploadErr = true - return fmt.Errorf("error uploading state: %w", err) - } - // After we've successfully persisted, what we just wrote is our new - // reference state until someone calls RefreshState again. - // We've potentially overwritten (via force) the state, lineage - // and / or serial (and serial was incremented) so we copy over all - // three fields so everything matches the new state and a subsequent - // operation would correctly detect no changes to the lineage, serial or state. - s.readState = s.state.DeepCopy() - s.readLineage = s.lineage - s.readSerial = s.serial - return nil -} - -func (s *State) uploadState(lineage string, serial uint64, isForcePush bool, state, jsonState []byte) error { - ctx := context.Background() - - // Read the raw state into a Terraform state. - stateFile, err := statefile.Read(bytes.NewReader(state)) - if err != nil { - return fmt.Errorf("failed to read state: %w", err) - } - - ov, err := jsonstate.MarshalOutputs(stateFile.State.RootModule().OutputValues) - if err != nil { - return fmt.Errorf("failed to translate outputs: %w", err) - } - o, err := json.Marshal(ov) - if err != nil { - return fmt.Errorf("failed to marshal outputs to json: %w", err) - } - - options := tfe.StateVersionCreateOptions{ - Lineage: tfe.String(lineage), - Serial: tfe.Int64(int64(serial)), - MD5: tfe.String(fmt.Sprintf("%x", md5.Sum(state))), - State: tfe.String(base64.StdEncoding.EncodeToString(state)), - Force: tfe.Bool(isForcePush), - JSONState: tfe.String(base64.StdEncoding.EncodeToString(jsonState)), - JSONStateOutputs: tfe.String(base64.StdEncoding.EncodeToString(o)), - } - - // If we have a run ID, make sure to add it to the options - // so the state will be properly associated with the run. - runID := os.Getenv("TFE_RUN_ID") - if runID != "" { - options.Run = &tfe.Run{ID: runID} - } - // Create the new state. - _, err = s.tfeClient.StateVersions.Create(ctx, s.workspace.ID, options) - return err -} - -// Lock calls the Client's Lock method if it's implemented. +// Lock delegates calls to lock state to the remote State func (s *State) Lock(info *statemgr.LockInfo) (string, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if s.disableLocks { - return "", nil - } - ctx := context.Background() - - lockErr := &statemgr.LockError{Info: s.lockInfo} - - // Lock the workspace. - _, err := s.tfeClient.Workspaces.Lock(ctx, s.workspace.ID, tfe.WorkspaceLockOptions{ - Reason: tfe.String("Locked by Terraform"), - }) - if err != nil { - if err == tfe.ErrWorkspaceLocked { - lockErr.Info = info - err = fmt.Errorf("%s (lock ID: \"%s/%s\")", err, s.organization, s.workspace.Name) - } - lockErr.Err = err - return "", lockErr - } - - s.lockInfo = info - - return s.lockInfo.ID, nil + return s.delegate.Lock(info) } -// statemgr.Refresher impl. -func (s *State) RefreshState() error { - s.mu.Lock() - defer s.mu.Unlock() - return s.refreshState() -} - -// refreshState is the main implementation of RefreshState, but split out so -// that we can make internal calls to it from methods that are already holding -// the s.mu lock. -func (s *State) refreshState() error { - payload, err := s.getStatePayload() - if err != nil { - return err - } - - // no remote state is OK - if payload == nil { - s.readState = nil - s.lineage = "" - s.serial = 0 - return nil - } - - stateFile, err := statefile.Read(bytes.NewReader(payload.Data)) - if err != nil { - return err - } - - s.lineage = stateFile.Lineage - s.serial = stateFile.Serial - s.state = stateFile.State - - // Properties from the remote must be separate so we can - // track changes as lineage, serial and/or state are mutated - s.readLineage = stateFile.Lineage - s.readSerial = stateFile.Serial - s.readState = s.state.DeepCopy() - return nil -} - -func (s *State) getStatePayload() (*remote.Payload, error) { - ctx := context.Background() - - sv, err := s.tfeClient.StateVersions.ReadCurrent(ctx, s.workspace.ID) - if err != nil { - if err == tfe.ErrResourceNotFound { - // If no state exists, then return nil. - return nil, nil - } - return nil, fmt.Errorf("error retrieving state: %v", err) - } - - state, err := s.tfeClient.StateVersions.Download(ctx, sv.DownloadURL) - if err != nil { - return nil, fmt.Errorf("error downloading state: %v", err) - } - - // If the state is empty, then return nil. - if len(state) == 0 { - return nil, nil - } - - // Get the MD5 checksum of the state. - sum := md5.Sum(state) - - return &remote.Payload{ - Data: state, - MD5: sum[:], - }, nil -} - -// Unlock calls the Client's Unlock method if it's implemented. +// Unlock delegates calls to unlock state to the remote State func (s *State) Unlock(id string) error { - s.mu.Lock() - defer s.mu.Unlock() + return s.delegate.Unlock(id) +} - if s.disableLocks { - return nil - } - - ctx := context.Background() - - // We first check if there was an error while uploading the latest - // state. If so, we will not unlock the workspace to prevent any - // changes from being applied until the correct state is uploaded. - if s.stateUploadErr { - return nil - } - - lockErr := &statemgr.LockError{Info: s.lockInfo} - - // With lock info this should be treated as a normal unlock. - if s.lockInfo != nil { - // Verify the expected lock ID. - if s.lockInfo.ID != id { - lockErr.Err = fmt.Errorf("lock ID does not match existing lock") - return lockErr - } - - // Unlock the workspace. - _, err := s.tfeClient.Workspaces.Unlock(ctx, s.workspace.ID) - if err != nil { - lockErr.Err = err - return lockErr - } - - return nil - } - - // Verify the optional force-unlock lock ID. - if s.organization+"/"+s.workspace.Name != id { - lockErr.Err = fmt.Errorf( - "lock ID %q does not match existing lock ID \"%s/%s\"", - id, - s.organization, - s.workspace.Name, - ) - return lockErr - } - - // Force unlock the workspace. - _, err := s.tfeClient.Workspaces.ForceUnlock(ctx, s.workspace.ID) - if err != nil { - lockErr.Err = err - return lockErr - } +// RefreshState delegates calls to refresh State to the remote State +func (s *State) RefreshState() error { + return s.delegate.RefreshState() +} +// RefreshState delegates calls to refresh State to the remote State +func (s *State) PersistState(schemas *terraform.Schemas) error { + s.delegate.PersistState(schemas) return nil } -// Delete the remote state. -func (s *State) Delete() error { - err := s.tfeClient.Workspaces.Delete(context.Background(), s.organization, s.workspace.Name) - if err != nil && err != tfe.ErrResourceNotFound { - return fmt.Errorf("error deleting workspace %s: %v", s.workspace.Name, err) - } - - return nil +// WriteState delegates calls to write State to the remote State +func (s *State) WriteState(state *states.State) error { + return s.delegate.WriteState(state) } -// EnableForcePush to allow the remote client to overwrite state -// by implementing remote.ClientForcePusher -func (s *State) EnableForcePush() { - s.forcePush = true +func (s *State) fallbackReadOutputsFromFullState() (map[string]*states.OutputValue, error) { + log.Printf("[DEBUG] falling back to reading full state") + + if err := s.RefreshState(); err != nil { + return nil, fmt.Errorf("failed to load state: %w", err) + } + + state := s.State() + if state == nil { + // We know that there is supposed to be state (and this is not simply a new workspace + // without state) because the fallback is only invoked when outputs are present but + // detailed types are not available. + return nil, ErrStateVersionUnauthorizedUpgradeState + } + + return state.RootModule().OutputValues, nil } // GetRootOutputValues fetches output values from Terraform Cloud func (s *State) GetRootOutputValues() (map[string]*states.OutputValue, error) { ctx := context.Background() - so, err := s.tfeClient.StateVersionOutputs.ReadCurrent(ctx, s.workspace.ID) + so, err := s.Client.client.StateVersionOutputs.ReadCurrent(ctx, s.Client.workspace.ID) if err != nil { return nil, fmt.Errorf("could not read state version outputs: %w", err) @@ -441,27 +111,13 @@ func (s *State) GetRootOutputValues() (map[string]*states.OutputValue, error) { // with a version of terraform < 1.3.0. In this case, we'll eject completely from this // function and fall back to the old behavior of reading the entire state file, which // requires a higher level of authorization. - log.Printf("[DEBUG] falling back to reading full state") - - if err := s.RefreshState(); err != nil { - return nil, fmt.Errorf("failed to load state: %w", err) - } - - state := s.State() - if state == nil { - // We know that there is supposed to be state (and this is not simply a new workspace - // without state) because the fallback is only invoked when outputs are present but - // detailed types are not available. - return nil, ErrStateVersionUnauthorizedUpgradeState - } - - return state.RootModule().OutputValues, nil + return s.fallbackReadOutputsFromFullState() } if output.Sensitive { // Since this is a sensitive value, the output must be requested explicitly in order to // read its value, which is assumed to be present by callers - sensitiveOutput, err := s.tfeClient.StateVersionOutputs.Read(ctx, output.ID) + sensitiveOutput, err := s.Client.client.StateVersionOutputs.Read(ctx, output.ID) if err != nil { return nil, fmt.Errorf("could not read state version output %s: %w", output.ID, err) } diff --git a/internal/cloud/state_test.go b/internal/cloud/state_test.go index 72e8cf5cee..738ae721a4 100644 --- a/internal/cloud/state_test.go +++ b/internal/cloud/state_test.go @@ -27,9 +27,14 @@ func TestState_GetRootOutputValues(t *testing.T) { b, bCleanup := testBackendWithOutputs(t) defer bCleanup() - state := &State{tfeClient: b.client, organization: b.organization, workspace: &tfe.Workspace{ - ID: "ws-abcd", - }} + client := &remoteClient{ + client: b.client, + workspace: &tfe.Workspace{ + ID: "ws-abcd", + }, + } + + state := NewState(client) outputs, err := state.GetRootOutputValues() if err != nil { diff --git a/internal/cloud/testing.go b/internal/cloud/testing.go index 95ac257c6d..cfb49cf9b3 100644 --- a/internal/cloud/testing.go +++ b/internal/cloud/testing.go @@ -1,14 +1,10 @@ package cloud import ( - "bytes" "context" "encoding/json" "fmt" - "github.com/hashicorp/terraform/internal/states/statefile" - "github.com/hashicorp/terraform/internal/states/statemgr" "io" - "io/ioutil" "net/http" "net/http/httptest" "path" @@ -27,6 +23,7 @@ import ( "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/httpclient" "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/hashicorp/terraform/version" @@ -113,7 +110,7 @@ func testBackendNoOperations(t *testing.T) (*Cloud, func()) { return testBackend(t, obj) } -func testCloudState(t *testing.T) *State { +func testRemoteClient(t *testing.T) remote.Client { b, bCleanup := testBackendWithName(t) defer bCleanup() @@ -122,7 +119,7 @@ func testCloudState(t *testing.T) *State { t.Fatalf("error: %v", err) } - return raw.(*State) + return raw.(*State).Client } func testBackendWithOutputs(t *testing.T) (*Cloud, func()) { @@ -446,95 +443,3 @@ func testVariables(s terraform.ValueSourceType, vs ...string) map[string]backend } return vars } - -func TestState(t *testing.T, state *State) { - var buf bytes.Buffer - s := statemgr.TestFullInitialState() - sf := statefile.New(s, "stub-lineage", 2) - err := statefile.Write(sf, &buf) - if err != nil { - t.Fatalf("err: %s", err) - } - data := buf.Bytes() - - jsonState, err := ioutil.ReadFile("../command/testdata/show-json-state/sensitive-variables/output.json") - - if err != nil { - t.Fatal(err) - } - - if err := state.uploadState(state.lineage, state.serial, state.forcePush, data, jsonState); err != nil { - t.Fatalf("put: %s", err) - } - - payload, err := state.getStatePayload() - if err != nil { - t.Fatalf("get: %s", err) - } - if !bytes.Equal(payload.Data, data) { - t.Fatalf("expected full state %q\n\ngot: %q", string(payload.Data), string(data)) - } - - if err := state.Delete(); err != nil { - t.Fatalf("delete: %s", err) - } - - p, err := state.getStatePayload() - if err != nil { - t.Fatalf("get: %s", err) - } - if p != nil { - t.Fatalf("expected empty state, got: %q", string(p.Data)) - } -} - -func TestCloudLocks(t *testing.T, a, b statemgr.Full) { - lockerA, ok := a.(statemgr.Locker) - if !ok { - t.Fatal("client A not a statemgr.Locker") - } - - lockerB, ok := b.(statemgr.Locker) - if !ok { - t.Fatal("client B not a statemgr.Locker") - } - - infoA := statemgr.NewLockInfo() - infoA.Operation = "test" - infoA.Who = "clientA" - - infoB := statemgr.NewLockInfo() - infoB.Operation = "test" - infoB.Who = "clientB" - - lockIDA, err := lockerA.Lock(infoA) - if err != nil { - t.Fatal("unable to get initial lock:", err) - } - - _, err = lockerB.Lock(infoB) - if err == nil { - lockerA.Unlock(lockIDA) - t.Fatal("client B obtained lock while held by client A") - } - if _, ok := err.(*statemgr.LockError); !ok { - t.Errorf("expected a LockError, but was %t: %s", err, err) - } - - if err := lockerA.Unlock(lockIDA); err != nil { - t.Fatal("error unlocking client A", err) - } - - lockIDB, err := lockerB.Lock(infoB) - if err != nil { - t.Fatal("unable to obtain lock from client B") - } - - if lockIDB == lockIDA { - t.Fatalf("duplicate lock IDs: %q", lockIDB) - } - - if err = lockerB.Unlock(lockIDB); err != nil { - t.Fatal("error unlocking client B:", err) - } -} diff --git a/internal/command/state_replace_provider_test.go b/internal/command/state_replace_provider_test.go index e86e5d669f..397a895ab6 100644 --- a/internal/command/state_replace_provider_test.go +++ b/internal/command/state_replace_provider_test.go @@ -3,6 +3,8 @@ package command import ( "bytes" "fmt" + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform/internal/states/statefile" "path/filepath" "regexp" "strings" @@ -64,6 +66,23 @@ func TestStateReplaceProvider(t *testing.T) { }) t.Run("Schemas not initialized and JSON output not generated", func(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("init-cloud-simple"), td) + defer testChdir(t, td)() + + // Some of the tests need a non-empty placeholder state file to work + // with. + fakeStateFile := &statefile.File{ + Lineage: "boop", + Serial: 4, + TerraformVersion: version.Must(version.NewVersion("1.0.0")), + State: state, + } + var fakeStateBuf bytes.Buffer + err := statefile.WriteForTest(fakeStateFile, &fakeStateBuf) + if err != nil { + t.Error(err) + } statePath := testStateFile(t, state) ui := new(cli.MockUi)