github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/cos/backend_state.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package cos 5 6 import ( 7 "fmt" 8 "log" 9 "path" 10 "sort" 11 "strings" 12 13 "github.com/terramate-io/tf/backend" 14 "github.com/terramate-io/tf/states" 15 "github.com/terramate-io/tf/states/remote" 16 "github.com/terramate-io/tf/states/statemgr" 17 ) 18 19 // Define file suffix 20 const ( 21 stateFileSuffix = ".tfstate" 22 lockFileSuffix = ".tflock" 23 ) 24 25 // Workspaces returns a list of names for the workspaces 26 func (b *Backend) Workspaces() ([]string, error) { 27 c, err := b.client("tencentcloud") 28 if err != nil { 29 return nil, err 30 } 31 32 obs, err := c.getBucket(b.prefix) 33 log.Printf("[DEBUG] list all workspaces, objects: %v, error: %v", obs, err) 34 if err != nil { 35 return nil, err 36 } 37 38 ws := []string{backend.DefaultStateName} 39 for _, vv := range obs { 40 // <name>.tfstate 41 if !strings.HasSuffix(vv.Key, stateFileSuffix) { 42 continue 43 } 44 // default worksapce 45 if path.Join(b.prefix, b.key) == vv.Key { 46 continue 47 } 48 // <prefix>/<worksapce>/<key> 49 prefix := strings.TrimRight(b.prefix, "/") + "/" 50 parts := strings.Split(strings.TrimPrefix(vv.Key, prefix), "/") 51 if len(parts) > 0 && parts[0] != "" { 52 ws = append(ws, parts[0]) 53 } 54 } 55 56 sort.Strings(ws[1:]) 57 log.Printf("[DEBUG] list all workspaces, workspaces: %v", ws) 58 59 return ws, nil 60 } 61 62 // DeleteWorkspace deletes the named workspaces. The "default" state cannot be deleted. 63 func (b *Backend) DeleteWorkspace(name string, _ bool) error { 64 log.Printf("[DEBUG] delete workspace, workspace: %v", name) 65 66 if name == backend.DefaultStateName || name == "" { 67 return fmt.Errorf("default state is not allow to delete") 68 } 69 70 c, err := b.client(name) 71 if err != nil { 72 return err 73 } 74 75 return c.Delete() 76 } 77 78 // StateMgr manage the state, if the named state not exists, a new file will created 79 func (b *Backend) StateMgr(name string) (statemgr.Full, error) { 80 log.Printf("[DEBUG] state manager, current workspace: %v", name) 81 82 c, err := b.client(name) 83 if err != nil { 84 return nil, err 85 } 86 stateMgr := &remote.State{Client: c} 87 88 ws, err := b.Workspaces() 89 if err != nil { 90 return nil, err 91 } 92 93 exists := false 94 for _, candidate := range ws { 95 if candidate == name { 96 exists = true 97 break 98 } 99 } 100 101 if !exists { 102 log.Printf("[DEBUG] workspace %v not exists", name) 103 104 // take a lock on this state while we write it 105 lockInfo := statemgr.NewLockInfo() 106 lockInfo.Operation = "init" 107 lockId, err := c.Lock(lockInfo) 108 if err != nil { 109 return nil, fmt.Errorf("Failed to lock cos state: %s", err) 110 } 111 112 // Local helper function so we can call it multiple places 113 lockUnlock := func(e error) error { 114 if err := stateMgr.Unlock(lockId); err != nil { 115 return fmt.Errorf(unlockErrMsg, err, lockId) 116 } 117 return e 118 } 119 120 // Grab the value 121 if err := stateMgr.RefreshState(); err != nil { 122 err = lockUnlock(err) 123 return nil, err 124 } 125 126 // If we have no state, we have to create an empty state 127 if v := stateMgr.State(); v == nil { 128 if err := stateMgr.WriteState(states.NewState()); err != nil { 129 err = lockUnlock(err) 130 return nil, err 131 } 132 if err := stateMgr.PersistState(nil); err != nil { 133 err = lockUnlock(err) 134 return nil, err 135 } 136 } 137 138 // Unlock, the state should now be initialized 139 if err := lockUnlock(nil); err != nil { 140 return nil, err 141 } 142 } 143 144 return stateMgr, nil 145 } 146 147 // client returns a remoteClient for the named state. 148 func (b *Backend) client(name string) (*remoteClient, error) { 149 if strings.TrimSpace(name) == "" { 150 return nil, fmt.Errorf("state name not allow to be empty") 151 } 152 153 return &remoteClient{ 154 cosContext: b.cosContext, 155 cosClient: b.cosClient, 156 tagClient: b.tagClient, 157 bucket: b.bucket, 158 stateFile: b.stateFile(name), 159 lockFile: b.lockFile(name), 160 encrypt: b.encrypt, 161 acl: b.acl, 162 }, nil 163 } 164 165 // stateFile returns state file path by name 166 func (b *Backend) stateFile(name string) string { 167 if name == backend.DefaultStateName { 168 return path.Join(b.prefix, b.key) 169 } 170 return path.Join(b.prefix, name, b.key) 171 } 172 173 // lockFile returns lock file path by name 174 func (b *Backend) lockFile(name string) string { 175 return b.stateFile(name) + lockFileSuffix 176 } 177 178 // unlockErrMsg is error msg for unlock failed 179 const unlockErrMsg = ` 180 Unlocking the state file on TencentCloud cos backend failed: 181 182 Error message: %v 183 Lock ID (gen): %s 184 185 You may have to force-unlock this state in order to use it again. 186 The TencentCloud backend acquires a lock during initialization 187 to ensure the initial state file is created. 188 `