package remote import ( "bytes" "context" "crypto/md5" "encoding/base64" "fmt" tfe "github.com/hashicorp/go-tfe" "github.com/hashicorp/terraform/states/remote" "github.com/hashicorp/terraform/states/statefile" "github.com/hashicorp/terraform/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.Current(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 { 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 }