github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/consul/backend_state.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package consul
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/terramate-io/tf/backend"
    11  	"github.com/terramate-io/tf/states"
    12  	"github.com/terramate-io/tf/states/remote"
    13  	"github.com/terramate-io/tf/states/statemgr"
    14  )
    15  
    16  const (
    17  	keyEnvPrefix = "-env:"
    18  )
    19  
    20  func (b *Backend) Workspaces() ([]string, error) {
    21  	// List our raw path
    22  	prefix := b.configData.Get("path").(string) + keyEnvPrefix
    23  	keys, _, err := b.client.KV().Keys(prefix, "/", nil)
    24  	if err != nil {
    25  		return nil, err
    26  	}
    27  
    28  	// Find the envs, we use a map since we can get duplicates with
    29  	// path suffixes.
    30  	envs := map[string]struct{}{}
    31  	for _, key := range keys {
    32  		// Consul should ensure this but it doesn't hurt to check again
    33  		if strings.HasPrefix(key, prefix) {
    34  			key = strings.TrimPrefix(key, prefix)
    35  
    36  			// Ignore anything with a "/" in it since we store the state
    37  			// directly in a key not a directory.
    38  			if idx := strings.IndexRune(key, '/'); idx >= 0 {
    39  				continue
    40  			}
    41  
    42  			envs[key] = struct{}{}
    43  		}
    44  	}
    45  
    46  	result := make([]string, 1, len(envs)+1)
    47  	result[0] = backend.DefaultStateName
    48  	for k, _ := range envs {
    49  		result = append(result, k)
    50  	}
    51  
    52  	return result, nil
    53  }
    54  
    55  func (b *Backend) DeleteWorkspace(name string, _ bool) error {
    56  	if name == backend.DefaultStateName || name == "" {
    57  		return fmt.Errorf("can't delete default state")
    58  	}
    59  
    60  	// Determine the path of the data
    61  	path := b.path(name)
    62  
    63  	// Delete it. We just delete it without any locking since
    64  	// the DeleteState API is documented as such.
    65  	_, err := b.client.KV().Delete(path, nil)
    66  	return err
    67  }
    68  
    69  func (b *Backend) StateMgr(name string) (statemgr.Full, error) {
    70  	// Determine the path of the data
    71  	path := b.path(name)
    72  
    73  	// Determine whether to gzip or not
    74  	gzip := b.configData.Get("gzip").(bool)
    75  
    76  	// Build the state client
    77  	var stateMgr = &remote.State{
    78  		Client: &RemoteClient{
    79  			Client:    b.client,
    80  			Path:      path,
    81  			GZip:      gzip,
    82  			lockState: b.lock,
    83  		},
    84  	}
    85  
    86  	if !b.lock {
    87  		stateMgr.DisableLocks()
    88  	}
    89  
    90  	// the default state always exists
    91  	if name == backend.DefaultStateName {
    92  		return stateMgr, nil
    93  	}
    94  
    95  	// Grab a lock, we use this to write an empty state if one doesn't
    96  	// exist already. We have to write an empty state as a sentinel value
    97  	// so States() knows it exists.
    98  	lockInfo := statemgr.NewLockInfo()
    99  	lockInfo.Operation = "init"
   100  	lockId, err := stateMgr.Lock(lockInfo)
   101  	if err != nil {
   102  		return nil, fmt.Errorf("failed to lock state in Consul: %s", err)
   103  	}
   104  
   105  	// Local helper function so we can call it multiple places
   106  	lockUnlock := func(parent error) error {
   107  		if err := stateMgr.Unlock(lockId); err != nil {
   108  			return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err)
   109  		}
   110  
   111  		return parent
   112  	}
   113  
   114  	// Grab the value
   115  	if err := stateMgr.RefreshState(); err != nil {
   116  		err = lockUnlock(err)
   117  		return nil, err
   118  	}
   119  
   120  	// If we have no state, we have to create an empty state
   121  	if v := stateMgr.State(); v == nil {
   122  		if err := stateMgr.WriteState(states.NewState()); err != nil {
   123  			err = lockUnlock(err)
   124  			return nil, err
   125  		}
   126  		if err := stateMgr.PersistState(nil); err != nil {
   127  			err = lockUnlock(err)
   128  			return nil, err
   129  		}
   130  	}
   131  
   132  	// Unlock, the state should now be initialized
   133  	if err := lockUnlock(nil); err != nil {
   134  		return nil, err
   135  	}
   136  
   137  	return stateMgr, nil
   138  }
   139  
   140  func (b *Backend) path(name string) string {
   141  	path := b.configData.Get("path").(string)
   142  	if name != backend.DefaultStateName {
   143  		path += fmt.Sprintf("%s%s", keyEnvPrefix, name)
   144  	}
   145  
   146  	return path
   147  }
   148  
   149  const errStateUnlock = `
   150  Error unlocking Consul state. Lock ID: %s
   151  
   152  Error: %s
   153  
   154  You may have to force-unlock this state in order to use it again.
   155  The Consul backend acquires a lock during initialization to ensure
   156  the minimum required key/values are prepared.
   157  `