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