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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package gcs
     5  
     6  import (
     7  	"fmt"
     8  	"path"
     9  	"sort"
    10  	"strings"
    11  
    12  	"cloud.google.com/go/storage"
    13  	"google.golang.org/api/iterator"
    14  
    15  	"github.com/terramate-io/tf/backend"
    16  	"github.com/terramate-io/tf/states"
    17  	"github.com/terramate-io/tf/states/remote"
    18  	"github.com/terramate-io/tf/states/statemgr"
    19  )
    20  
    21  const (
    22  	stateFileSuffix = ".tfstate"
    23  	lockFileSuffix  = ".tflock"
    24  )
    25  
    26  // Workspaces returns a list of names for the workspaces found on GCS. The default
    27  // state is always returned as the first element in the slice.
    28  func (b *Backend) Workspaces() ([]string, error) {
    29  	states := []string{backend.DefaultStateName}
    30  
    31  	bucket := b.storageClient.Bucket(b.bucketName)
    32  	objs := bucket.Objects(b.storageContext, &storage.Query{
    33  		Delimiter: "/",
    34  		Prefix:    b.prefix,
    35  	})
    36  	for {
    37  		attrs, err := objs.Next()
    38  		if err == iterator.Done {
    39  			break
    40  		}
    41  		if err != nil {
    42  			return nil, fmt.Errorf("querying Cloud Storage failed: %v", err)
    43  		}
    44  
    45  		name := path.Base(attrs.Name)
    46  		if !strings.HasSuffix(name, stateFileSuffix) {
    47  			continue
    48  		}
    49  		st := strings.TrimSuffix(name, stateFileSuffix)
    50  
    51  		if st != backend.DefaultStateName {
    52  			states = append(states, st)
    53  		}
    54  	}
    55  
    56  	sort.Strings(states[1:])
    57  	return states, nil
    58  }
    59  
    60  // DeleteWorkspace deletes the named workspaces. The "default" state cannot be deleted.
    61  func (b *Backend) DeleteWorkspace(name string, _ bool) error {
    62  	if name == backend.DefaultStateName {
    63  		return fmt.Errorf("cowardly refusing to delete the %q state", name)
    64  	}
    65  
    66  	c, err := b.client(name)
    67  	if err != nil {
    68  		return err
    69  	}
    70  
    71  	return c.Delete()
    72  }
    73  
    74  // client returns a remoteClient for the named state.
    75  func (b *Backend) client(name string) (*remoteClient, error) {
    76  	if name == "" {
    77  		return nil, fmt.Errorf("%q is not a valid state name", name)
    78  	}
    79  
    80  	return &remoteClient{
    81  		storageContext: b.storageContext,
    82  		storageClient:  b.storageClient,
    83  		bucketName:     b.bucketName,
    84  		stateFilePath:  b.stateFile(name),
    85  		lockFilePath:   b.lockFile(name),
    86  		encryptionKey:  b.encryptionKey,
    87  		kmsKeyName:     b.kmsKeyName,
    88  	}, nil
    89  }
    90  
    91  // StateMgr reads and returns the named state from GCS. If the named state does
    92  // not yet exist, a new state file is created.
    93  func (b *Backend) StateMgr(name string) (statemgr.Full, error) {
    94  	c, err := b.client(name)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	st := &remote.State{Client: c}
   100  
   101  	// Grab the value
   102  	if err := st.RefreshState(); err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	// If we have no state, we have to create an empty state
   107  	if v := st.State(); v == nil {
   108  
   109  		lockInfo := statemgr.NewLockInfo()
   110  		lockInfo.Operation = "init"
   111  		lockID, err := st.Lock(lockInfo)
   112  		if err != nil {
   113  			return nil, err
   114  		}
   115  
   116  		// Local helper function so we can call it multiple places
   117  		unlock := func(baseErr error) error {
   118  			if err := st.Unlock(lockID); err != nil {
   119  				const unlockErrMsg = `%v
   120  				Additionally, unlocking the state file on Google Cloud Storage failed:
   121  
   122  				Error message: %q
   123  				Lock ID (gen): %v
   124  				Lock file URL: %v
   125  
   126  				You may have to force-unlock this state in order to use it again.
   127  				The GCloud backend acquires a lock during initialization to ensure
   128  				the initial state file is created.`
   129  				return fmt.Errorf(unlockErrMsg, baseErr, err.Error(), lockID, c.lockFileURL())
   130  			}
   131  
   132  			return baseErr
   133  		}
   134  
   135  		if err := st.WriteState(states.NewState()); err != nil {
   136  			return nil, unlock(err)
   137  		}
   138  		if err := st.PersistState(nil); err != nil {
   139  			return nil, unlock(err)
   140  		}
   141  
   142  		// Unlock, the state should now be initialized
   143  		if err := unlock(nil); err != nil {
   144  			return nil, err
   145  		}
   146  
   147  	}
   148  
   149  	return st, nil
   150  }
   151  
   152  func (b *Backend) stateFile(name string) string {
   153  	return path.Join(b.prefix, name+stateFileSuffix)
   154  }
   155  
   156  func (b *Backend) lockFile(name string) string {
   157  	return path.Join(b.prefix, name+lockFileSuffix)
   158  }