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  }