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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package inmem
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"sort"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/terramate-io/tf/backend"
    15  	"github.com/terramate-io/tf/legacy/helper/schema"
    16  	statespkg "github.com/terramate-io/tf/states"
    17  	"github.com/terramate-io/tf/states/remote"
    18  	"github.com/terramate-io/tf/states/statemgr"
    19  )
    20  
    21  // we keep the states and locks in package-level variables, so that they can be
    22  // accessed from multiple instances of the backend. This better emulates
    23  // backend instances accessing a single remote data store.
    24  var (
    25  	states stateMap
    26  	locks  lockMap
    27  )
    28  
    29  func init() {
    30  	Reset()
    31  }
    32  
    33  // Reset clears out all existing state and lock data.
    34  // This is used to initialize the package during init, as well as between
    35  // tests.
    36  func Reset() {
    37  	states = stateMap{
    38  		m: map[string]*remote.State{},
    39  	}
    40  
    41  	locks = lockMap{
    42  		m: map[string]*statemgr.LockInfo{},
    43  	}
    44  }
    45  
    46  // New creates a new backend for Inmem remote state.
    47  func New() backend.Backend {
    48  	// Set the schema
    49  	s := &schema.Backend{
    50  		Schema: map[string]*schema.Schema{
    51  			"lock_id": &schema.Schema{
    52  				Type:        schema.TypeString,
    53  				Optional:    true,
    54  				Description: "initializes the state in a locked configuration",
    55  			},
    56  		},
    57  	}
    58  	backend := &Backend{Backend: s}
    59  	backend.Backend.ConfigureFunc = backend.configure
    60  	return backend
    61  }
    62  
    63  type Backend struct {
    64  	*schema.Backend
    65  }
    66  
    67  func (b *Backend) configure(ctx context.Context) error {
    68  	states.Lock()
    69  	defer states.Unlock()
    70  
    71  	defaultClient := &RemoteClient{
    72  		Name: backend.DefaultStateName,
    73  	}
    74  
    75  	states.m[backend.DefaultStateName] = &remote.State{
    76  		Client: defaultClient,
    77  	}
    78  
    79  	// set the default client lock info per the test config
    80  	data := schema.FromContextBackendConfig(ctx)
    81  	if v, ok := data.GetOk("lock_id"); ok && v.(string) != "" {
    82  		info := statemgr.NewLockInfo()
    83  		info.ID = v.(string)
    84  		info.Operation = "test"
    85  		info.Info = "test config"
    86  
    87  		locks.lock(backend.DefaultStateName, info)
    88  	}
    89  
    90  	return nil
    91  }
    92  
    93  func (b *Backend) Workspaces() ([]string, error) {
    94  	states.Lock()
    95  	defer states.Unlock()
    96  
    97  	var workspaces []string
    98  
    99  	for s := range states.m {
   100  		workspaces = append(workspaces, s)
   101  	}
   102  
   103  	sort.Strings(workspaces)
   104  	return workspaces, nil
   105  }
   106  
   107  func (b *Backend) DeleteWorkspace(name string, _ bool) error {
   108  	states.Lock()
   109  	defer states.Unlock()
   110  
   111  	if name == backend.DefaultStateName || name == "" {
   112  		return fmt.Errorf("can't delete default state")
   113  	}
   114  
   115  	delete(states.m, name)
   116  	return nil
   117  }
   118  
   119  func (b *Backend) StateMgr(name string) (statemgr.Full, error) {
   120  	states.Lock()
   121  	defer states.Unlock()
   122  
   123  	s := states.m[name]
   124  	if s == nil {
   125  		s = &remote.State{
   126  			Client: &RemoteClient{
   127  				Name: name,
   128  			},
   129  		}
   130  		states.m[name] = s
   131  
   132  		// to most closely replicate other implementations, we are going to
   133  		// take a lock and create a new state if it doesn't exist.
   134  		lockInfo := statemgr.NewLockInfo()
   135  		lockInfo.Operation = "init"
   136  		lockID, err := s.Lock(lockInfo)
   137  		if err != nil {
   138  			return nil, fmt.Errorf("failed to lock inmem state: %s", err)
   139  		}
   140  		defer s.Unlock(lockID)
   141  
   142  		// If we have no state, we have to create an empty state
   143  		if v := s.State(); v == nil {
   144  			if err := s.WriteState(statespkg.NewState()); err != nil {
   145  				return nil, err
   146  			}
   147  			if err := s.PersistState(nil); err != nil {
   148  				return nil, err
   149  			}
   150  		}
   151  	}
   152  
   153  	return s, nil
   154  }
   155  
   156  type stateMap struct {
   157  	sync.Mutex
   158  	m map[string]*remote.State
   159  }
   160  
   161  // Global level locks for inmem backends.
   162  type lockMap struct {
   163  	sync.Mutex
   164  	m map[string]*statemgr.LockInfo
   165  }
   166  
   167  func (l *lockMap) lock(name string, info *statemgr.LockInfo) (string, error) {
   168  	l.Lock()
   169  	defer l.Unlock()
   170  
   171  	lockInfo := l.m[name]
   172  	if lockInfo != nil {
   173  		lockErr := &statemgr.LockError{
   174  			Info: lockInfo,
   175  		}
   176  
   177  		lockErr.Err = errors.New("state locked")
   178  		// make a copy of the lock info to avoid any testing shenanigans
   179  		*lockErr.Info = *lockInfo
   180  		return "", lockErr
   181  	}
   182  
   183  	info.Created = time.Now().UTC()
   184  	l.m[name] = info
   185  
   186  	return info.ID, nil
   187  }
   188  
   189  func (l *lockMap) unlock(name, id string) error {
   190  	l.Lock()
   191  	defer l.Unlock()
   192  
   193  	lockInfo := l.m[name]
   194  
   195  	if lockInfo == nil {
   196  		return errors.New("state not locked")
   197  	}
   198  
   199  	lockErr := &statemgr.LockError{
   200  		Info: &statemgr.LockInfo{},
   201  	}
   202  
   203  	if id != lockInfo.ID {
   204  		lockErr.Err = errors.New("invalid lock id")
   205  		*lockErr.Info = *lockInfo
   206  		return lockErr
   207  	}
   208  
   209  	delete(l.m, name)
   210  	return nil
   211  }