| package remote |
| |
| import ( |
| "bytes" |
| "context" |
| "crypto/md5" |
| "encoding/base64" |
| "fmt" |
| |
| tfe "github.com/hashicorp/go-tfe" |
| "github.com/hashicorp/terraform/internal/states/remote" |
| "github.com/hashicorp/terraform/internal/states/statefile" |
| "github.com/hashicorp/terraform/internal/states/statemgr" |
| ) |
| |
| type remoteClient struct { |
| client *tfe.Client |
| lockInfo *statemgr.LockInfo |
| organization string |
| runID string |
| stateUploadErr bool |
| workspace *tfe.Workspace |
| forcePush bool |
| } |
| |
| // Get the remote state. |
| func (r *remoteClient) Get() (*remote.Payload, error) { |
| ctx := context.Background() |
| |
| sv, err := r.client.StateVersions.ReadCurrent(ctx, r.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 := r.client.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 |
| } |
| |
| // Put the remote state. |
| func (r *remoteClient) Put(state []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("Error reading state: %s", err) |
| } |
| |
| options := tfe.StateVersionCreateOptions{ |
| Lineage: tfe.String(stateFile.Lineage), |
| Serial: tfe.Int64(int64(stateFile.Serial)), |
| MD5: tfe.String(fmt.Sprintf("%x", md5.Sum(state))), |
| State: tfe.String(base64.StdEncoding.EncodeToString(state)), |
| Force: tfe.Bool(r.forcePush), |
| } |
| |
| // If we have a run ID, make sure to add it to the options |
| // so the state will be properly associated with the run. |
| if r.runID != "" { |
| options.Run = &tfe.Run{ID: r.runID} |
| } |
| |
| // Create the new state. |
| _, err = r.client.StateVersions.Create(ctx, r.workspace.ID, options) |
| if err != nil { |
| r.stateUploadErr = true |
| return fmt.Errorf("Error uploading state: %v", err) |
| } |
| |
| return nil |
| } |
| |
| // Delete the remote state. |
| func (r *remoteClient) Delete() error { |
| err := r.client.Workspaces.Delete(context.Background(), r.organization, r.workspace.Name) |
| if err != nil && err != tfe.ErrResourceNotFound { |
| return fmt.Errorf("Error deleting workspace %s: %v", r.workspace.Name, err) |
| } |
| |
| return nil |
| } |
| |
| // EnableForcePush to allow the remote client to overwrite state |
| // by implementing remote.ClientForcePusher |
| func (r *remoteClient) EnableForcePush() { |
| r.forcePush = true |
| } |
| |
| // Lock the remote state. |
| func (r *remoteClient) Lock(info *statemgr.LockInfo) (string, error) { |
| ctx := context.Background() |
| |
| lockErr := &statemgr.LockError{Info: r.lockInfo} |
| |
| // Lock the workspace. |
| _, err := r.client.Workspaces.Lock(ctx, r.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, r.organization, r.workspace.Name) |
| } |
| lockErr.Err = err |
| return "", lockErr |
| } |
| |
| r.lockInfo = info |
| |
| return r.lockInfo.ID, nil |
| } |
| |
| // Unlock the remote state. |
| func (r *remoteClient) Unlock(id string) error { |
| 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 r.stateUploadErr { |
| return nil |
| } |
| |
| lockErr := &statemgr.LockError{Info: r.lockInfo} |
| |
| // With lock info this should be treated as a normal unlock. |
| if r.lockInfo != nil { |
| // Verify the expected lock ID. |
| if r.lockInfo.ID != id { |
| lockErr.Err = fmt.Errorf("lock ID does not match existing lock") |
| return lockErr |
| } |
| |
| // Unlock the workspace. |
| _, err := r.client.Workspaces.Unlock(ctx, r.workspace.ID) |
| if err != nil { |
| lockErr.Err = err |
| return lockErr |
| } |
| |
| return nil |
| } |
| |
| // Verify the optional force-unlock lock ID. |
| if r.organization+"/"+r.workspace.Name != id { |
| lockErr.Err = fmt.Errorf( |
| "lock ID %q does not match existing lock ID \"%s/%s\"", |
| id, |
| r.organization, |
| r.workspace.Name, |
| ) |
| return lockErr |
| } |
| |
| // Force unlock the workspace. |
| _, err := r.client.Workspaces.ForceUnlock(ctx, r.workspace.ID) |
| if err != nil { |
| lockErr.Err = err |
| return lockErr |
| } |
| |
| return nil |
| } |