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