mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-29 10:21:01 -06:00
925db6dbc5
Signed-off-by: Marcin Wyszynski <marcin.pixie@gmail.com>
212 lines
4.3 KiB
Go
212 lines
4.3 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package inmem
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/opentofu/opentofu/internal/backend"
|
|
"github.com/opentofu/opentofu/internal/legacy/helper/schema"
|
|
statespkg "github.com/opentofu/opentofu/internal/states"
|
|
"github.com/opentofu/opentofu/internal/states/remote"
|
|
"github.com/opentofu/opentofu/internal/states/statemgr"
|
|
)
|
|
|
|
// we keep the states and locks in package-level variables, so that they can be
|
|
// accessed from multiple instances of the backend. This better emulates
|
|
// backend instances accessing a single remote data store.
|
|
var (
|
|
states stateMap
|
|
locks lockMap
|
|
)
|
|
|
|
func init() {
|
|
Reset()
|
|
}
|
|
|
|
// Reset clears out all existing state and lock data.
|
|
// This is used to initialize the package during init, as well as between
|
|
// tests.
|
|
func Reset() {
|
|
states = stateMap{
|
|
m: map[string]*remote.State{},
|
|
}
|
|
|
|
locks = lockMap{
|
|
m: map[string]*statemgr.LockInfo{},
|
|
}
|
|
}
|
|
|
|
// New creates a new backend for Inmem remote state.
|
|
func New() backend.Backend {
|
|
// Set the schema
|
|
s := &schema.Backend{
|
|
Schema: map[string]*schema.Schema{
|
|
"lock_id": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Description: "initializes the state in a locked configuration",
|
|
},
|
|
},
|
|
}
|
|
backend := &Backend{Backend: s}
|
|
backend.Backend.ConfigureFunc = backend.configure
|
|
return backend
|
|
}
|
|
|
|
type Backend struct {
|
|
*schema.Backend
|
|
}
|
|
|
|
func (b *Backend) configure(ctx context.Context) error {
|
|
states.Lock()
|
|
defer states.Unlock()
|
|
|
|
defaultClient := &RemoteClient{
|
|
Name: backend.DefaultStateName,
|
|
}
|
|
|
|
states.m[backend.DefaultStateName] = &remote.State{
|
|
Client: defaultClient,
|
|
}
|
|
|
|
// set the default client lock info per the test config
|
|
data := schema.FromContextBackendConfig(ctx)
|
|
if v, ok := data.GetOk("lock_id"); ok && v.(string) != "" {
|
|
info := statemgr.NewLockInfo()
|
|
info.ID = v.(string)
|
|
info.Operation = "test"
|
|
info.Info = "test config"
|
|
|
|
locks.lock(backend.DefaultStateName, info)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Backend) Workspaces() ([]string, error) {
|
|
states.Lock()
|
|
defer states.Unlock()
|
|
|
|
var workspaces []string
|
|
|
|
for s := range states.m {
|
|
workspaces = append(workspaces, s)
|
|
}
|
|
|
|
sort.Strings(workspaces)
|
|
return workspaces, nil
|
|
}
|
|
|
|
func (b *Backend) DeleteWorkspace(_ context.Context, name string, _ bool) error {
|
|
states.Lock()
|
|
defer states.Unlock()
|
|
|
|
if name == backend.DefaultStateName || name == "" {
|
|
return fmt.Errorf("can't delete default state")
|
|
}
|
|
|
|
delete(states.m, name)
|
|
return nil
|
|
}
|
|
|
|
func (b *Backend) StateMgr(ctx context.Context, name string) (statemgr.Full, error) {
|
|
states.Lock()
|
|
defer states.Unlock()
|
|
|
|
s := states.m[name]
|
|
if s == nil {
|
|
s = &remote.State{
|
|
Client: &RemoteClient{
|
|
Name: name,
|
|
},
|
|
}
|
|
states.m[name] = s
|
|
|
|
// to most closely replicate other implementations, we are going to
|
|
// take a lock and create a new state if it doesn't exist.
|
|
lockInfo := statemgr.NewLockInfo()
|
|
lockInfo.Operation = "init"
|
|
lockID, err := s.Lock(lockInfo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to lock inmem state: %w", err)
|
|
}
|
|
defer s.Unlock(lockID)
|
|
|
|
// If we have no state, we have to create an empty state
|
|
if v := s.State(); v == nil {
|
|
if err := s.WriteState(statespkg.NewState()); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.PersistState(nil); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
type stateMap struct {
|
|
sync.Mutex
|
|
m map[string]*remote.State
|
|
}
|
|
|
|
// Global level locks for inmem backends.
|
|
type lockMap struct {
|
|
sync.Mutex
|
|
m map[string]*statemgr.LockInfo
|
|
}
|
|
|
|
func (l *lockMap) lock(name string, info *statemgr.LockInfo) (string, error) {
|
|
l.Lock()
|
|
defer l.Unlock()
|
|
|
|
lockInfo := l.m[name]
|
|
if lockInfo != nil {
|
|
lockErr := &statemgr.LockError{
|
|
Info: lockInfo,
|
|
}
|
|
|
|
lockErr.Err = errors.New("state locked")
|
|
// make a copy of the lock info to avoid any testing shenanigans
|
|
*lockErr.Info = *lockInfo
|
|
return "", lockErr
|
|
}
|
|
|
|
info.Created = time.Now().UTC()
|
|
l.m[name] = info
|
|
|
|
return info.ID, nil
|
|
}
|
|
|
|
func (l *lockMap) unlock(name, id string) error {
|
|
l.Lock()
|
|
defer l.Unlock()
|
|
|
|
lockInfo := l.m[name]
|
|
|
|
if lockInfo == nil {
|
|
return errors.New("state not locked")
|
|
}
|
|
|
|
lockErr := &statemgr.LockError{
|
|
Info: &statemgr.LockInfo{},
|
|
}
|
|
|
|
if id != lockInfo.ID {
|
|
lockErr.Err = errors.New("invalid lock id")
|
|
*lockErr.Info = *lockInfo
|
|
return lockErr
|
|
}
|
|
|
|
delete(l.m, name)
|
|
return nil
|
|
}
|