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  }