mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Support named states in inmeme backend
Used to expand test coverage
This commit is contained in:
parent
470448d8ce
commit
ac60ddcd40
@ -2,40 +2,172 @@ package inmem
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/backend"
|
"github.com/hashicorp/terraform/backend"
|
||||||
"github.com/hashicorp/terraform/backend/remote-state"
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
"github.com/hashicorp/terraform/state"
|
"github.com/hashicorp/terraform/state"
|
||||||
"github.com/hashicorp/terraform/state/remote"
|
"github.com/hashicorp/terraform/state/remote"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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{
|
||||||
|
m: map[string]*remote.State{},
|
||||||
|
}
|
||||||
|
|
||||||
|
var locks = lockMap{
|
||||||
|
m: map[string]*state.LockInfo{},
|
||||||
|
}
|
||||||
|
|
||||||
// New creates a new backend for Inmem remote state.
|
// New creates a new backend for Inmem remote state.
|
||||||
func New() backend.Backend {
|
func New() backend.Backend {
|
||||||
return &remotestate.Backend{
|
// Set the schema
|
||||||
ConfigureFunc: configure,
|
s := &schema.Backend{
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
// Set the schema
|
"lock_id": &schema.Schema{
|
||||||
Backend: &schema.Backend{
|
Type: schema.TypeString,
|
||||||
Schema: map[string]*schema.Schema{
|
Optional: true,
|
||||||
"lock_id": &schema.Schema{
|
Description: "initializes the state in a locked configuration",
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
Description: "initializes the state in a locked configuration",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
backend := &Backend{Backend: s}
|
||||||
|
backend.Backend.ConfigureFunc = backend.configure
|
||||||
|
return backend
|
||||||
}
|
}
|
||||||
|
|
||||||
func configure(ctx context.Context) (remote.Client, error) {
|
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)
|
data := schema.FromContextBackendConfig(ctx)
|
||||||
if v, ok := data.GetOk("lock_id"); ok && v.(string) != "" {
|
if v, ok := data.GetOk("lock_id"); ok && v.(string) != "" {
|
||||||
info := state.NewLockInfo()
|
info := state.NewLockInfo()
|
||||||
info.ID = v.(string)
|
info.ID = v.(string)
|
||||||
info.Operation = "test"
|
info.Operation = "test"
|
||||||
info.Info = "test config"
|
info.Info = "test config"
|
||||||
return &RemoteClient{LockInfo: info}, nil
|
|
||||||
|
locks.lock(backend.DefaultStateName, info)
|
||||||
}
|
}
|
||||||
return &RemoteClient{}, nil
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Backend) States() ([]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) DeleteState(name string) 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) State(name string) (state.State, error) {
|
||||||
|
states.Lock()
|
||||||
|
defer states.Unlock()
|
||||||
|
|
||||||
|
s := states.m[name]
|
||||||
|
if s == nil {
|
||||||
|
s = &remote.State{
|
||||||
|
Client: &RemoteClient{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
states.m[name] = s
|
||||||
|
}
|
||||||
|
|
||||||
|
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]*state.LockInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lockMap) lock(name string, info *state.LockInfo) (string, error) {
|
||||||
|
l.Lock()
|
||||||
|
defer l.Unlock()
|
||||||
|
|
||||||
|
lockInfo := l.m[name]
|
||||||
|
if lockInfo != nil {
|
||||||
|
lockErr := &state.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 := &state.LockError{
|
||||||
|
Info: &state.LockInfo{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if id != lockInfo.ID {
|
||||||
|
lockErr.Err = errors.New("invalid lock id")
|
||||||
|
*lockErr.Info = *lockInfo
|
||||||
|
return lockErr
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(l.m, name)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
63
backend/remote-state/inmem/backend_test.go
Normal file
63
backend/remote-state/inmem/backend_test.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package inmem
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
"github.com/hashicorp/terraform/state"
|
||||||
|
"github.com/hashicorp/terraform/state/remote"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBackend_impl(t *testing.T) {
|
||||||
|
var _ backend.Backend = new(Backend)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset the states and locks between tests
|
||||||
|
func reset() {
|
||||||
|
states = stateMap{
|
||||||
|
m: map[string]*remote.State{},
|
||||||
|
}
|
||||||
|
|
||||||
|
locks = lockMap{
|
||||||
|
m: map[string]*state.LockInfo{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackendConfig(t *testing.T) {
|
||||||
|
defer reset()
|
||||||
|
testID := "test_lock_id"
|
||||||
|
|
||||||
|
config := map[string]interface{}{
|
||||||
|
"lock_id": testID,
|
||||||
|
}
|
||||||
|
|
||||||
|
b := backend.TestBackendConfig(t, New(), config).(*Backend)
|
||||||
|
|
||||||
|
s, err := b.State(backend.DefaultStateName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := s.(*remote.State).Client.(*RemoteClient)
|
||||||
|
if c.Name != backend.DefaultStateName {
|
||||||
|
t.Fatal("client name is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := locks.unlock(backend.DefaultStateName, testID); err != nil {
|
||||||
|
t.Fatalf("default state should have been locked: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackend(t *testing.T) {
|
||||||
|
defer reset()
|
||||||
|
b := backend.TestBackendConfig(t, New(), nil).(*Backend)
|
||||||
|
backend.TestBackend(t, b, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackendLocked(t *testing.T) {
|
||||||
|
defer reset()
|
||||||
|
b1 := backend.TestBackendConfig(t, New(), nil).(*Backend)
|
||||||
|
b2 := backend.TestBackendConfig(t, New(), nil).(*Backend)
|
||||||
|
|
||||||
|
backend.TestBackend(t, b1, b2)
|
||||||
|
}
|
@ -2,8 +2,6 @@ package inmem
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/state"
|
"github.com/hashicorp/terraform/state"
|
||||||
"github.com/hashicorp/terraform/state/remote"
|
"github.com/hashicorp/terraform/state/remote"
|
||||||
@ -13,8 +11,7 @@ import (
|
|||||||
type RemoteClient struct {
|
type RemoteClient struct {
|
||||||
Data []byte
|
Data []byte
|
||||||
MD5 []byte
|
MD5 []byte
|
||||||
|
Name string
|
||||||
LockInfo *state.LockInfo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RemoteClient) Get() (*remote.Payload, error) {
|
func (c *RemoteClient) Get() (*remote.Payload, error) {
|
||||||
@ -43,37 +40,8 @@ func (c *RemoteClient) Delete() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) {
|
func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) {
|
||||||
lockErr := &state.LockError{
|
return locks.lock(c.Name, info)
|
||||||
Info: &state.LockInfo{},
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.LockInfo != nil {
|
|
||||||
lockErr.Err = errors.New("state locked")
|
|
||||||
// make a copy of the lock info to avoid any testing shenanigans
|
|
||||||
*lockErr.Info = *c.LockInfo
|
|
||||||
return "", lockErr
|
|
||||||
}
|
|
||||||
|
|
||||||
info.Created = time.Now().UTC()
|
|
||||||
c.LockInfo = info
|
|
||||||
|
|
||||||
return c.LockInfo.ID, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RemoteClient) Unlock(id string) error {
|
func (c *RemoteClient) Unlock(id string) error {
|
||||||
if c.LockInfo == nil {
|
return locks.unlock(c.Name, id)
|
||||||
return errors.New("state not locked")
|
|
||||||
}
|
|
||||||
|
|
||||||
lockErr := &state.LockError{
|
|
||||||
Info: &state.LockInfo{},
|
|
||||||
}
|
|
||||||
if id != c.LockInfo.ID {
|
|
||||||
lockErr.Err = errors.New("invalid lock id")
|
|
||||||
*lockErr.Info = *c.LockInfo
|
|
||||||
return lockErr
|
|
||||||
}
|
|
||||||
|
|
||||||
c.LockInfo = nil
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/backend"
|
"github.com/hashicorp/terraform/backend"
|
||||||
remotestate "github.com/hashicorp/terraform/backend/remote-state"
|
|
||||||
"github.com/hashicorp/terraform/state/remote"
|
"github.com/hashicorp/terraform/state/remote"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -14,11 +13,19 @@ func TestRemoteClient_impl(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRemoteClient(t *testing.T) {
|
func TestRemoteClient(t *testing.T) {
|
||||||
|
defer reset()
|
||||||
b := backend.TestBackendConfig(t, New(), nil)
|
b := backend.TestBackendConfig(t, New(), nil)
|
||||||
remotestate.TestClient(t, b)
|
|
||||||
|
s, err := b.State(backend.DefaultStateName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remote.TestClient(t, s.(*remote.State).Client)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInmemLocks(t *testing.T) {
|
func TestInmemLocks(t *testing.T) {
|
||||||
|
defer reset()
|
||||||
s, err := backend.TestBackendConfig(t, New(), nil).State(backend.DefaultStateName)
|
s, err := backend.TestBackendConfig(t, New(), nil).State(backend.DefaultStateName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
Loading…
Reference in New Issue
Block a user