github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/gcs/client.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package gcs
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"strconv"
    11  
    12  	"cloud.google.com/go/storage"
    13  	multierror "github.com/hashicorp/go-multierror"
    14  	"github.com/terramate-io/tf/states/remote"
    15  	"github.com/terramate-io/tf/states/statemgr"
    16  	"golang.org/x/net/context"
    17  )
    18  
    19  // remoteClient is used by "state/remote".State to read and write
    20  // blobs representing state.
    21  // Implements "state/remote".ClientLocker
    22  type remoteClient struct {
    23  	storageContext context.Context
    24  	storageClient  *storage.Client
    25  	bucketName     string
    26  	stateFilePath  string
    27  	lockFilePath   string
    28  	encryptionKey  []byte
    29  	kmsKeyName     string
    30  }
    31  
    32  func (c *remoteClient) Get() (payload *remote.Payload, err error) {
    33  	stateFileReader, err := c.stateFile().NewReader(c.storageContext)
    34  	if err != nil {
    35  		if err == storage.ErrObjectNotExist {
    36  			return nil, nil
    37  		} else {
    38  			return nil, fmt.Errorf("Failed to open state file at %v: %v", c.stateFileURL(), err)
    39  		}
    40  	}
    41  	defer stateFileReader.Close()
    42  
    43  	stateFileContents, err := ioutil.ReadAll(stateFileReader)
    44  	if err != nil {
    45  		return nil, fmt.Errorf("Failed to read state file from %v: %v", c.stateFileURL(), err)
    46  	}
    47  
    48  	stateFileAttrs, err := c.stateFile().Attrs(c.storageContext)
    49  	if err != nil {
    50  		return nil, fmt.Errorf("Failed to read state file attrs from %v: %v", c.stateFileURL(), err)
    51  	}
    52  
    53  	result := &remote.Payload{
    54  		Data: stateFileContents,
    55  		MD5:  stateFileAttrs.MD5,
    56  	}
    57  
    58  	return result, nil
    59  }
    60  
    61  func (c *remoteClient) Put(data []byte) error {
    62  	err := func() error {
    63  		stateFileWriter := c.stateFile().NewWriter(c.storageContext)
    64  		if len(c.kmsKeyName) > 0 {
    65  			stateFileWriter.KMSKeyName = c.kmsKeyName
    66  		}
    67  		if _, err := stateFileWriter.Write(data); err != nil {
    68  			return err
    69  		}
    70  		return stateFileWriter.Close()
    71  	}()
    72  	if err != nil {
    73  		return fmt.Errorf("Failed to upload state to %v: %v", c.stateFileURL(), err)
    74  	}
    75  
    76  	return nil
    77  }
    78  
    79  func (c *remoteClient) Delete() error {
    80  	if err := c.stateFile().Delete(c.storageContext); err != nil {
    81  		return fmt.Errorf("Failed to delete state file %v: %v", c.stateFileURL(), err)
    82  	}
    83  
    84  	return nil
    85  }
    86  
    87  // Lock writes to a lock file, ensuring file creation. Returns the generation
    88  // number, which must be passed to Unlock().
    89  func (c *remoteClient) Lock(info *statemgr.LockInfo) (string, error) {
    90  	// update the path we're using
    91  	// we can't set the ID until the info is written
    92  	info.Path = c.lockFileURL()
    93  
    94  	infoJson, err := json.Marshal(info)
    95  	if err != nil {
    96  		return "", err
    97  	}
    98  
    99  	lockFile := c.lockFile()
   100  	w := lockFile.If(storage.Conditions{DoesNotExist: true}).NewWriter(c.storageContext)
   101  	err = func() error {
   102  		if _, err := w.Write(infoJson); err != nil {
   103  			return err
   104  		}
   105  		return w.Close()
   106  	}()
   107  
   108  	if err != nil {
   109  		return "", c.lockError(fmt.Errorf("writing %q failed: %v", c.lockFileURL(), err))
   110  	}
   111  
   112  	info.ID = strconv.FormatInt(w.Attrs().Generation, 10)
   113  
   114  	return info.ID, nil
   115  }
   116  
   117  func (c *remoteClient) Unlock(id string) error {
   118  	gen, err := strconv.ParseInt(id, 10, 64)
   119  	if err != nil {
   120  		return fmt.Errorf("Lock ID should be numerical value, got '%s'", id)
   121  	}
   122  
   123  	if err := c.lockFile().If(storage.Conditions{GenerationMatch: gen}).Delete(c.storageContext); err != nil {
   124  		return c.lockError(err)
   125  	}
   126  
   127  	return nil
   128  }
   129  
   130  func (c *remoteClient) lockError(err error) *statemgr.LockError {
   131  	lockErr := &statemgr.LockError{
   132  		Err: err,
   133  	}
   134  
   135  	info, infoErr := c.lockInfo()
   136  	if infoErr != nil {
   137  		lockErr.Err = multierror.Append(lockErr.Err, infoErr)
   138  	} else {
   139  		lockErr.Info = info
   140  	}
   141  	return lockErr
   142  }
   143  
   144  // lockInfo reads the lock file, parses its contents and returns the parsed
   145  // LockInfo struct.
   146  func (c *remoteClient) lockInfo() (*statemgr.LockInfo, error) {
   147  	r, err := c.lockFile().NewReader(c.storageContext)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  	defer r.Close()
   152  
   153  	rawData, err := ioutil.ReadAll(r)
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	info := &statemgr.LockInfo{}
   159  	if err := json.Unmarshal(rawData, info); err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	// We use the Generation as the ID, so overwrite the ID in the json.
   164  	// This can't be written into the Info, since the generation isn't known
   165  	// until it's written.
   166  	attrs, err := c.lockFile().Attrs(c.storageContext)
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  	info.ID = strconv.FormatInt(attrs.Generation, 10)
   171  
   172  	return info, nil
   173  }
   174  
   175  func (c *remoteClient) stateFile() *storage.ObjectHandle {
   176  	h := c.storageClient.Bucket(c.bucketName).Object(c.stateFilePath)
   177  	if len(c.encryptionKey) > 0 {
   178  		return h.Key(c.encryptionKey)
   179  	}
   180  	return h
   181  }
   182  
   183  func (c *remoteClient) stateFileURL() string {
   184  	return fmt.Sprintf("gs://%v/%v", c.bucketName, c.stateFilePath)
   185  }
   186  
   187  func (c *remoteClient) lockFile() *storage.ObjectHandle {
   188  	return c.storageClient.Bucket(c.bucketName).Object(c.lockFilePath)
   189  }
   190  
   191  func (c *remoteClient) lockFileURL() string {
   192  	return fmt.Sprintf("gs://%v/%v", c.bucketName, c.lockFilePath)
   193  }