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  `