github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/cloud/backend_state.go (about)

     1  package cloud
     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/cycloidio/terraform/states/remote"
    12  	"github.com/cycloidio/terraform/states/statefile"
    13  	"github.com/cycloidio/terraform/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  }