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 }