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