github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/oss/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 oss 7 8 import ( 9 "errors" 10 "fmt" 11 "log" 12 "path" 13 "sort" 14 "strings" 15 16 "github.com/aliyun/aliyun-oss-go-sdk/oss" 17 "github.com/aliyun/aliyun-tablestore-go-sdk/tablestore" 18 19 "github.com/opentofu/opentofu/internal/backend" 20 "github.com/opentofu/opentofu/internal/states" 21 "github.com/opentofu/opentofu/internal/states/remote" 22 "github.com/opentofu/opentofu/internal/states/statemgr" 23 ) 24 25 const ( 26 lockFileSuffix = ".tflock" 27 ) 28 29 // get a remote client configured for this state 30 func (b *Backend) remoteClient(name string) (*RemoteClient, error) { 31 if name == "" { 32 return nil, errors.New("missing state name") 33 } 34 35 client := &RemoteClient{ 36 ossClient: b.ossClient, 37 bucketName: b.bucketName, 38 stateFile: b.stateFile(name), 39 lockFile: b.lockFile(name), 40 serverSideEncryption: b.serverSideEncryption, 41 acl: b.acl, 42 otsTable: b.otsTable, 43 otsClient: b.otsClient, 44 } 45 if b.otsEndpoint != "" && b.otsTable != "" { 46 _, err := b.otsClient.DescribeTable(&tablestore.DescribeTableRequest{ 47 TableName: b.otsTable, 48 }) 49 if err != nil { 50 return client, fmt.Errorf("error describing table store %s: %w", b.otsTable, err) 51 } 52 } 53 54 return client, nil 55 } 56 57 func (b *Backend) Workspaces() ([]string, error) { 58 bucket, err := b.ossClient.Bucket(b.bucketName) 59 if err != nil { 60 return []string{""}, fmt.Errorf("error getting bucket: %w", err) 61 } 62 63 var options []oss.Option 64 options = append(options, oss.Prefix(b.statePrefix+"/"), oss.MaxKeys(1000)) 65 resp, err := bucket.ListObjects(options...) 66 if err != nil { 67 return nil, err 68 } 69 70 result := []string{backend.DefaultStateName} 71 prefix := b.statePrefix 72 lastObj := "" 73 for { 74 for _, obj := range resp.Objects { 75 // we have 3 parts, the state prefix, the workspace name, and the state file: <prefix>/<worksapce-name>/<key> 76 if path.Join(b.statePrefix, b.stateKey) == obj.Key { 77 // filter the default workspace 78 continue 79 } 80 lastObj = obj.Key 81 parts := strings.Split(strings.TrimPrefix(obj.Key, prefix+"/"), "/") 82 if len(parts) > 0 && parts[0] != "" { 83 result = append(result, parts[0]) 84 } 85 } 86 if resp.IsTruncated { 87 if len(options) == 3 { 88 options[2] = oss.Marker(lastObj) 89 } else { 90 options = append(options, oss.Marker(lastObj)) 91 } 92 resp, err = bucket.ListObjects(options...) 93 if err != nil { 94 return nil, err 95 } 96 } else { 97 break 98 } 99 } 100 sort.Strings(result[1:]) 101 return result, nil 102 } 103 104 func (b *Backend) DeleteWorkspace(name string, _ bool) error { 105 if name == backend.DefaultStateName || name == "" { 106 return fmt.Errorf("can't delete default state") 107 } 108 109 client, err := b.remoteClient(name) 110 if err != nil { 111 return err 112 } 113 return client.Delete() 114 } 115 116 func (b *Backend) StateMgr(name string) (statemgr.Full, error) { 117 client, err := b.remoteClient(name) 118 if err != nil { 119 return nil, err 120 } 121 stateMgr := remote.NewState(client, b.encryption) 122 123 // Check to see if this state already exists. 124 existing, err := b.Workspaces() 125 if err != nil { 126 return nil, err 127 } 128 129 log.Printf("[DEBUG] Current workspace name: %s. All workspaces:%#v", name, existing) 130 131 exists := false 132 for _, s := range existing { 133 if s == name { 134 exists = true 135 break 136 } 137 } 138 // We need to create the object so it's listed by States. 139 if !exists { 140 // take a lock on this state while we write it 141 lockInfo := statemgr.NewLockInfo() 142 lockInfo.Operation = "init" 143 lockId, err := client.Lock(lockInfo) 144 if err != nil { 145 return nil, fmt.Errorf("failed to lock OSS state: %w", err) 146 } 147 148 // Local helper function so we can call it multiple places 149 lockUnlock := func(e error) error { 150 if err := stateMgr.Unlock(lockId); err != nil { 151 return fmt.Errorf(strings.TrimSpace(stateUnlockError), lockId, err) 152 } 153 return e 154 } 155 156 // Grab the value 157 if err := stateMgr.RefreshState(); err != nil { 158 err = lockUnlock(err) 159 return nil, err 160 } 161 162 // If we have no state, we have to create an empty state 163 if v := stateMgr.State(); v == nil { 164 if err := stateMgr.WriteState(states.NewState()); err != nil { 165 err = lockUnlock(err) 166 return nil, err 167 } 168 if err := stateMgr.PersistState(nil); err != nil { 169 err = lockUnlock(err) 170 return nil, err 171 } 172 } 173 174 // Unlock, the state should now be initialized 175 if err := lockUnlock(nil); err != nil { 176 return nil, err 177 } 178 179 } 180 return stateMgr, nil 181 } 182 183 func (b *Backend) stateFile(name string) string { 184 if name == backend.DefaultStateName { 185 return path.Join(b.statePrefix, b.stateKey) 186 } 187 return path.Join(b.statePrefix, name, b.stateKey) 188 } 189 190 func (b *Backend) lockFile(name string) string { 191 return b.stateFile(name) + lockFileSuffix 192 } 193 194 const stateUnlockError = ` 195 Error unlocking Alibaba Cloud OSS state file: 196 197 Lock ID: %s 198 Error message: %w 199 200 You may have to force-unlock this state in order to use it again. 201 The Alibaba Cloud backend acquires a lock during initialization to ensure the initial state file is created. 202 `