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