github.com/hugorut/terraform@v1.1.3/src/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/hugorut/terraform/src/states/remote" 12 "github.com/hugorut/terraform/src/states/statefile" 13 "github.com/hugorut/terraform/src/states/statemgr" 14 ) 15 16 type remoteClient struct { 17 client *tfe.Client 18 lockInfo *statemgr.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 68 options := tfe.StateVersionCreateOptions{ 69 Lineage: tfe.String(stateFile.Lineage), 70 Serial: tfe.Int64(int64(stateFile.Serial)), 71 MD5: tfe.String(fmt.Sprintf("%x", md5.Sum(state))), 72 State: tfe.String(base64.StdEncoding.EncodeToString(state)), 73 Force: tfe.Bool(r.forcePush), 74 } 75 76 // If we have a run ID, make sure to add it to the options 77 // so the state will be properly associated with the run. 78 if r.runID != "" { 79 options.Run = &tfe.Run{ID: r.runID} 80 } 81 82 // Create the new state. 83 _, err = r.client.StateVersions.Create(ctx, r.workspace.ID, options) 84 if err != nil { 85 r.stateUploadErr = true 86 return fmt.Errorf("Error uploading state: %v", err) 87 } 88 89 return nil 90 } 91 92 // Delete the remote state. 93 func (r *remoteClient) Delete() error { 94 err := r.client.Workspaces.Delete(context.Background(), r.organization, r.workspace.Name) 95 if err != nil && err != tfe.ErrResourceNotFound { 96 return fmt.Errorf("Error deleting workspace %s: %v", r.workspace.Name, err) 97 } 98 99 return nil 100 } 101 102 // EnableForcePush to allow the remote client to overwrite state 103 // by implementing remote.ClientForcePusher 104 func (r *remoteClient) EnableForcePush() { 105 r.forcePush = true 106 } 107 108 // Lock the remote state. 109 func (r *remoteClient) Lock(info *statemgr.LockInfo) (string, error) { 110 ctx := context.Background() 111 112 lockErr := &statemgr.LockError{Info: r.lockInfo} 113 114 // Lock the workspace. 115 _, err := r.client.Workspaces.Lock(ctx, r.workspace.ID, tfe.WorkspaceLockOptions{ 116 Reason: tfe.String("Locked by Terraform"), 117 }) 118 if err != nil { 119 if err == tfe.ErrWorkspaceLocked { 120 lockErr.Info = info 121 err = fmt.Errorf("%s (lock ID: \"%s/%s\")", err, r.organization, r.workspace.Name) 122 } 123 lockErr.Err = err 124 return "", lockErr 125 } 126 127 r.lockInfo = info 128 129 return r.lockInfo.ID, nil 130 } 131 132 // Unlock the remote state. 133 func (r *remoteClient) Unlock(id string) error { 134 ctx := context.Background() 135 136 // We first check if there was an error while uploading the latest 137 // state. If so, we will not unlock the workspace to prevent any 138 // changes from being applied until the correct state is uploaded. 139 if r.stateUploadErr { 140 return nil 141 } 142 143 lockErr := &statemgr.LockError{Info: r.lockInfo} 144 145 // With lock info this should be treated as a normal unlock. 146 if r.lockInfo != nil { 147 // Verify the expected lock ID. 148 if r.lockInfo.ID != id { 149 lockErr.Err = fmt.Errorf("lock ID does not match existing lock") 150 return lockErr 151 } 152 153 // Unlock the workspace. 154 _, err := r.client.Workspaces.Unlock(ctx, r.workspace.ID) 155 if err != nil { 156 lockErr.Err = err 157 return lockErr 158 } 159 160 return nil 161 } 162 163 // Verify the optional force-unlock lock ID. 164 if r.organization+"/"+r.workspace.Name != id { 165 lockErr.Err = fmt.Errorf( 166 "lock ID %q does not match existing lock ID \"%s/%s\"", 167 id, 168 r.organization, 169 r.workspace.Name, 170 ) 171 return lockErr 172 } 173 174 // Force unlock the workspace. 175 _, err := r.client.Workspaces.ForceUnlock(ctx, r.workspace.ID) 176 if err != nil { 177 lockErr.Err = err 178 return lockErr 179 } 180 181 return nil 182 }