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