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 }