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  `