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