github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/remote-state/s3/backend_state.go (about) 1 package s3 2 3 import ( 4 "errors" 5 "fmt" 6 "path" 7 "sort" 8 "strings" 9 10 "github.com/aws/aws-sdk-go/aws" 11 "github.com/aws/aws-sdk-go/aws/awserr" 12 "github.com/aws/aws-sdk-go/service/s3" 13 14 "github.com/hashicorp/terraform/backend" 15 "github.com/hashicorp/terraform/state" 16 "github.com/hashicorp/terraform/state/remote" 17 "github.com/hashicorp/terraform/states" 18 ) 19 20 func (b *Backend) Workspaces() ([]string, error) { 21 const maxKeys = 1000 22 23 prefix := "" 24 25 if b.workspaceKeyPrefix != "" { 26 prefix = b.workspaceKeyPrefix + "/" 27 } 28 29 params := &s3.ListObjectsInput{ 30 Bucket: &b.bucketName, 31 Prefix: aws.String(prefix), 32 MaxKeys: aws.Int64(maxKeys), 33 } 34 35 wss := []string{backend.DefaultStateName} 36 err := b.s3Client.ListObjectsPages(params, func(page *s3.ListObjectsOutput, lastPage bool) bool { 37 for _, obj := range page.Contents { 38 ws := b.keyEnv(*obj.Key) 39 if ws != "" { 40 wss = append(wss, ws) 41 } 42 } 43 return !lastPage 44 }) 45 46 if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == s3.ErrCodeNoSuchBucket { 47 return nil, fmt.Errorf(errS3NoSuchBucket, err) 48 } 49 50 sort.Strings(wss[1:]) 51 return wss, nil 52 } 53 54 func (b *Backend) keyEnv(key string) string { 55 prefix := b.workspaceKeyPrefix 56 57 if prefix == "" { 58 parts := strings.SplitN(key, "/", 2) 59 if len(parts) > 1 && parts[1] == b.keyName { 60 return parts[0] 61 } else { 62 return "" 63 } 64 } 65 66 // add a slash to treat this as a directory 67 prefix += "/" 68 69 parts := strings.SplitAfterN(key, prefix, 2) 70 if len(parts) < 2 { 71 return "" 72 } 73 74 // shouldn't happen since we listed by prefix 75 if parts[0] != prefix { 76 return "" 77 } 78 79 parts = strings.SplitN(parts[1], "/", 2) 80 81 if len(parts) < 2 { 82 return "" 83 } 84 85 // not our key, so don't include it in our listing 86 if parts[1] != b.keyName { 87 return "" 88 } 89 90 return parts[0] 91 } 92 93 func (b *Backend) DeleteWorkspace(name string) error { 94 if name == backend.DefaultStateName || name == "" { 95 return fmt.Errorf("can't delete default state") 96 } 97 98 client, err := b.remoteClient(name) 99 if err != nil { 100 return err 101 } 102 103 return client.Delete() 104 } 105 106 // get a remote client configured for this state 107 func (b *Backend) remoteClient(name string) (*RemoteClient, error) { 108 if name == "" { 109 return nil, errors.New("missing state name") 110 } 111 112 client := &RemoteClient{ 113 s3Client: b.s3Client, 114 dynClient: b.dynClient, 115 bucketName: b.bucketName, 116 path: b.path(name), 117 serverSideEncryption: b.serverSideEncryption, 118 customerEncryptionKey: b.customerEncryptionKey, 119 acl: b.acl, 120 kmsKeyID: b.kmsKeyID, 121 ddbTable: b.ddbTable, 122 } 123 124 return client, nil 125 } 126 127 func (b *Backend) StateMgr(name string) (state.State, error) { 128 return b.stateMgr(name, true) 129 } 130 131 func (b *Backend) StateMgrWithoutCheckVersion(name string) (state.State, error) { 132 return b.stateMgr(name, false) 133 } 134 135 func (b *Backend) stateMgr(name string, checkVersion bool) (state.State, error) { 136 client, err := b.remoteClient(name) 137 if err != nil { 138 return nil, err 139 } 140 141 stateMgr := &remote.State{Client: client} 142 // Check to see if this state already exists. 143 // If we're trying to force-unlock a state, we can't take the lock before 144 // fetching the state. If the state doesn't exist, we have to assume this 145 // is a normal create operation, and take the lock at that point. 146 // 147 // If we need to force-unlock, but for some reason the state no longer 148 // exists, the user will have to use aws tools to manually fix the 149 // situation. 150 existing, err := b.Workspaces() 151 if err != nil { 152 return nil, err 153 } 154 155 exists := false 156 for _, s := range existing { 157 if s == name { 158 exists = true 159 break 160 } 161 } 162 163 // We need to create the object so it's listed by States. 164 if !exists { 165 // take a lock on this state while we write it 166 lockInfo := state.NewLockInfo() 167 lockInfo.Operation = "init" 168 lockId, err := client.Lock(lockInfo) 169 if err != nil { 170 return nil, fmt.Errorf("failed to lock s3 state: %s", err) 171 } 172 173 // Local helper function so we can call it multiple places 174 lockUnlock := func(parent error) error { 175 if err := stateMgr.Unlock(lockId); err != nil { 176 return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err) 177 } 178 return parent 179 } 180 181 // Grab the value 182 // This is to ensure that no one beat us to writing a state between 183 // the `exists` check and taking the lock. 184 if checkVersion { 185 if err := stateMgr.RefreshState(); err != nil { 186 err = lockUnlock(err) 187 return nil, err 188 } 189 } else { 190 if err := stateMgr.RefreshStateWithoutCheckVersion(); err != nil { 191 err = lockUnlock(err) 192 return nil, err 193 } 194 } 195 196 // If we have no state, we have to create an empty state 197 if v := stateMgr.State(); v == nil { 198 if err := stateMgr.WriteState(states.NewState()); err != nil { 199 err = lockUnlock(err) 200 return nil, err 201 } 202 if err := stateMgr.PersistState(); err != nil { 203 err = lockUnlock(err) 204 return nil, err 205 } 206 } 207 208 // Unlock, the state should now be initialized 209 if err := lockUnlock(nil); err != nil { 210 return nil, err 211 } 212 213 } 214 215 return stateMgr, nil 216 } 217 218 func (b *Backend) client() *RemoteClient { 219 return &RemoteClient{} 220 } 221 222 func (b *Backend) path(name string) string { 223 if name == backend.DefaultStateName { 224 return b.keyName 225 } 226 227 return path.Join(b.workspaceKeyPrefix, name, b.keyName) 228 } 229 230 const errStateUnlock = ` 231 Error unlocking S3 state. Lock ID: %s 232 233 Error: %s 234 235 You may have to force-unlock this state in order to use it again. 236 `