github.com/rvichery/terraform@v0.11.10/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/terraform"
    14  )
    15  
    16  type remoteClient struct {
    17  	client       *tfe.Client
    18  	lockInfo     *state.LockInfo
    19  	organization string
    20  	runID        string
    21  	workspace    string
    22  }
    23  
    24  // Get the remote state.
    25  func (r *remoteClient) Get() (*remote.Payload, error) {
    26  	ctx := context.Background()
    27  
    28  	// Retrieve the workspace for which to create a new state.
    29  	w, err := r.client.Workspaces.Read(ctx, r.organization, r.workspace)
    30  	if err != nil {
    31  		if err == tfe.ErrResourceNotFound {
    32  			// If no state exists, then return nil.
    33  			return nil, nil
    34  		}
    35  		return nil, fmt.Errorf("Error retrieving workspace: %v", err)
    36  	}
    37  
    38  	sv, err := r.client.StateVersions.Current(ctx, w.ID)
    39  	if err != nil {
    40  		if err == tfe.ErrResourceNotFound {
    41  			// If no state exists, then return nil.
    42  			return nil, nil
    43  		}
    44  		return nil, fmt.Errorf("Error retrieving remote state: %v", err)
    45  	}
    46  
    47  	state, err := r.client.StateVersions.Download(ctx, sv.DownloadURL)
    48  	if err != nil {
    49  		return nil, fmt.Errorf("Error downloading remote state: %v", err)
    50  	}
    51  
    52  	// If the state is empty, then return nil.
    53  	if len(state) == 0 {
    54  		return nil, nil
    55  	}
    56  
    57  	// Get the MD5 checksum of the state.
    58  	sum := md5.Sum(state)
    59  
    60  	return &remote.Payload{
    61  		Data: state,
    62  		MD5:  sum[:],
    63  	}, nil
    64  }
    65  
    66  // Put the remote state.
    67  func (r *remoteClient) Put(state []byte) error {
    68  	ctx := context.Background()
    69  
    70  	// Retrieve the workspace for which to create a new state.
    71  	w, err := r.client.Workspaces.Read(ctx, r.organization, r.workspace)
    72  	if err != nil {
    73  		return fmt.Errorf("Error retrieving workspace: %v", err)
    74  	}
    75  
    76  	// Read the raw state into a Terraform state.
    77  	tfState, err := terraform.ReadState(bytes.NewReader(state))
    78  	if err != nil {
    79  		return fmt.Errorf("Error reading state: %s", err)
    80  	}
    81  
    82  	options := tfe.StateVersionCreateOptions{
    83  		Lineage: tfe.String(tfState.Lineage),
    84  		Serial:  tfe.Int64(tfState.Serial),
    85  		MD5:     tfe.String(fmt.Sprintf("%x", md5.Sum(state))),
    86  		State:   tfe.String(base64.StdEncoding.EncodeToString(state)),
    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, w.ID, options)
    97  	if err != nil {
    98  		return fmt.Errorf("Error creating remote state: %v", err)
    99  	}
   100  
   101  	return nil
   102  }
   103  
   104  // Delete the remote state.
   105  func (r *remoteClient) Delete() error {
   106  	err := r.client.Workspaces.Delete(context.Background(), r.organization, r.workspace)
   107  	if err != nil && err != tfe.ErrResourceNotFound {
   108  		return fmt.Errorf("Error deleting workspace %s: %v", r.workspace, err)
   109  	}
   110  
   111  	return nil
   112  }
   113  
   114  // Lock the remote state.
   115  func (r *remoteClient) Lock(info *state.LockInfo) (string, error) {
   116  	ctx := context.Background()
   117  
   118  	lockErr := &state.LockError{Info: r.lockInfo}
   119  
   120  	// Retrieve the workspace to lock.
   121  	w, err := r.client.Workspaces.Read(ctx, r.organization, r.workspace)
   122  	if err != nil {
   123  		lockErr.Err = err
   124  		return "", lockErr
   125  	}
   126  
   127  	// Check if the workspace is already locked.
   128  	if w.Locked {
   129  		lockErr.Err = fmt.Errorf(
   130  			"remote state already\nlocked (lock ID: \"%s/%s\")", r.organization, r.workspace)
   131  		return "", lockErr
   132  	}
   133  
   134  	// Lock the workspace.
   135  	w, err = r.client.Workspaces.Lock(ctx, w.ID, tfe.WorkspaceLockOptions{
   136  		Reason: tfe.String("Locked by Terraform"),
   137  	})
   138  	if err != nil {
   139  		lockErr.Err = err
   140  		return "", lockErr
   141  	}
   142  
   143  	r.lockInfo = info
   144  
   145  	return r.lockInfo.ID, nil
   146  }
   147  
   148  // Unlock the remote state.
   149  func (r *remoteClient) Unlock(id string) error {
   150  	ctx := context.Background()
   151  
   152  	lockErr := &state.LockError{Info: r.lockInfo}
   153  
   154  	// Verify the expected lock ID.
   155  	if r.lockInfo != nil && r.lockInfo.ID != id {
   156  		lockErr.Err = fmt.Errorf("lock ID does not match existing lock")
   157  		return lockErr
   158  	}
   159  
   160  	// Verify the optional force-unlock lock ID.
   161  	if r.lockInfo == nil && r.organization+"/"+r.workspace != id {
   162  		lockErr.Err = fmt.Errorf("lock ID does not match existing lock")
   163  		return lockErr
   164  	}
   165  
   166  	// Retrieve the workspace to lock.
   167  	w, err := r.client.Workspaces.Read(ctx, r.organization, r.workspace)
   168  	if err != nil {
   169  		lockErr.Err = err
   170  		return lockErr
   171  	}
   172  
   173  	// Unlock the workspace.
   174  	w, err = r.client.Workspaces.Unlock(ctx, w.ID)
   175  	if err != nil {
   176  		lockErr.Err = err
   177  		return lockErr
   178  	}
   179  
   180  	return nil
   181  }