github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/gcs/client.go (about)

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