github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/remote/backend_state.go (about) 1 package remote 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/md5" 7 "encoding/base64" 8 "fmt" 9 10 tfe "github.com/hashicorp/go-tfe" 11 "github.com/hashicorp/terraform/state" 12 "github.com/hashicorp/terraform/state/remote" 13 "github.com/hashicorp/terraform/states/statefile" 14 ) 15 16 type remoteClient struct { 17 client *tfe.Client 18 lockInfo *state.LockInfo 19 organization string 20 runID string 21 stateUploadErr bool 22 workspace *tfe.Workspace 23 forcePush bool 24 } 25 26 // Get the remote state. 27 func (r *remoteClient) Get() (*remote.Payload, error) { 28 ctx := context.Background() 29 30 sv, err := r.client.StateVersions.Current(ctx, r.workspace.ID) 31 if err != nil { 32 if err == tfe.ErrResourceNotFound { 33 // If no state exists, then return nil. 34 return nil, nil 35 } 36 return nil, fmt.Errorf("Error retrieving state: %v", err) 37 } 38 39 state, err := r.client.StateVersions.Download(ctx, sv.DownloadURL) 40 if err != nil { 41 return nil, fmt.Errorf("Error downloading state: %v", err) 42 } 43 44 // If the state is empty, then return nil. 45 if len(state) == 0 { 46 return nil, nil 47 } 48 49 // Get the MD5 checksum of the state. 50 sum := md5.Sum(state) 51 52 return &remote.Payload{ 53 Data: state, 54 MD5: sum[:], 55 }, nil 56 } 57 58 // Put the remote state. 59 func (r *remoteClient) Put(state []byte) error { 60 ctx := context.Background() 61 62 // Read the raw state into a Terraform state. 63 stateFile, err := statefile.Read(bytes.NewReader(state)) 64 if err != nil { 65 return fmt.Errorf("Error reading state: %s", err) 66 } 67 if err := stateFile.CheckTerraformVersion(); err != nil { 68 return fmt.Errorf("Incompatible statefile: %s", err) 69 } 70 71 options := tfe.StateVersionCreateOptions{ 72 Lineage: tfe.String(stateFile.Lineage), 73 Serial: tfe.Int64(int64(stateFile.Serial)), 74 MD5: tfe.String(fmt.Sprintf("%x", md5.Sum(state))), 75 State: tfe.String(base64.StdEncoding.EncodeToString(state)), 76 Force: tfe.Bool(r.forcePush), 77 } 78 79 // If we have a run ID, make sure to add it to the options 80 // so the state will be properly associated with the run. 81 if r.runID != "" { 82 options.Run = &tfe.Run{ID: r.runID} 83 } 84 85 // Create the new state. 86 _, err = r.client.StateVersions.Create(ctx, r.workspace.ID, options) 87 if err != nil { 88 r.stateUploadErr = true 89 return fmt.Errorf("Error uploading state: %v", err) 90 } 91 92 return nil 93 } 94 95 // Delete the remote state. 96 func (r *remoteClient) Delete() error { 97 err := r.client.Workspaces.Delete(context.Background(), r.organization, r.workspace.Name) 98 if err != nil && err != tfe.ErrResourceNotFound { 99 return fmt.Errorf("Error deleting workspace %s: %v", r.workspace.Name, err) 100 } 101 102 return nil 103 } 104 105 // EnableForcePush to allow the remote client to overwrite state 106 // by implementing remote.ClientForcePusher 107 func (r *remoteClient) EnableForcePush() { 108 r.forcePush = true 109 } 110 111 // Lock the remote state. 112 func (r *remoteClient) Lock(info *state.LockInfo) (string, error) { 113 ctx := context.Background() 114 115 lockErr := &state.LockError{Info: r.lockInfo} 116 117 // Lock the workspace. 118 _, err := r.client.Workspaces.Lock(ctx, r.workspace.ID, tfe.WorkspaceLockOptions{ 119 Reason: tfe.String("Locked by Terraform"), 120 }) 121 if err != nil { 122 if err == tfe.ErrWorkspaceLocked { 123 err = fmt.Errorf("%s (lock ID: \"%s/%s\")", err, r.organization, r.workspace.Name) 124 } 125 lockErr.Err = err 126 return "", lockErr 127 } 128 129 r.lockInfo = info 130 131 return r.lockInfo.ID, nil 132 } 133 134 // Unlock the remote state. 135 func (r *remoteClient) Unlock(id string) error { 136 ctx := context.Background() 137 138 // We first check if there was an error while uploading the latest 139 // state. If so, we will not unlock the workspace to prevent any 140 // changes from being applied until the correct state is uploaded. 141 if r.stateUploadErr { 142 return nil 143 } 144 145 lockErr := &state.LockError{Info: r.lockInfo} 146 147 // With lock info this should be treated as a normal unlock. 148 if r.lockInfo != nil { 149 // Verify the expected lock ID. 150 if r.lockInfo.ID != id { 151 lockErr.Err = fmt.Errorf("lock ID does not match existing lock") 152 return lockErr 153 } 154 155 // Unlock the workspace. 156 _, err := r.client.Workspaces.Unlock(ctx, r.workspace.ID) 157 if err != nil { 158 lockErr.Err = err 159 return lockErr 160 } 161 162 return nil 163 } 164 165 // Verify the optional force-unlock lock ID. 166 if r.organization+"/"+r.workspace.Name != id { 167 lockErr.Err = fmt.Errorf( 168 "lock ID %q does not match existing lock ID \"%s/%s\"", 169 id, 170 r.organization, 171 r.workspace.Name, 172 ) 173 return lockErr 174 } 175 176 // Force unlock the workspace. 177 _, err := r.client.Workspaces.ForceUnlock(ctx, r.workspace.ID) 178 if err != nil { 179 lockErr.Err = err 180 return lockErr 181 } 182 183 return nil 184 }