mirror of https://github.com/hashicorp/terraform
PSS : Add `fs` and `inmem` state storage implementations to the builtin `simplev6` provider, update `grpcwrap` package, use PSS implementation in E2E test (#37790)
* feat: Implement `inmem` state store in provider-simple-v6 * feat: Add filesystem state store `fs` in provider-simple-v6, no locking implemented * refactor: Move PSS chunking-related constants into the `pluggable` package, so they can be reused. * feat: Implement PSS-related methods in grpcwrap package * test: Add E2E test checking an init and apply (no plan) workflow is usable with both PSS implementations * fix: Ensure state stores are configured with a suggested chunk size from Core --------- Co-authored-by: Radek Simko <radeksimko@users.noreply.github.com>pull/37884/head
parent
078ac7cb21
commit
f2818db795
@ -0,0 +1,18 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package pluggable
|
||||
|
||||
const (
|
||||
// DefaultStateStoreChunkSize is the default chunk size proposed
|
||||
// to the provider.
|
||||
// This can be tweaked but should provide reasonable performance
|
||||
// trade-offs for average network conditions and state file sizes.
|
||||
DefaultStateStoreChunkSize int64 = 8 << 20 // 8 MB
|
||||
|
||||
// MaxStateStoreChunkSize is the highest chunk size provider may choose
|
||||
// which we still consider reasonable/safe.
|
||||
// This reflects terraform-plugin-go's max. RPC message size of 256MB
|
||||
// and leaves plenty of space for other variable data like diagnostics.
|
||||
MaxStateStoreChunkSize int64 = 128 << 20 // 128 MB
|
||||
)
|
||||
@ -0,0 +1,23 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
simple6 = {
|
||||
source = "registry.terraform.io/hashicorp/simple6"
|
||||
}
|
||||
}
|
||||
|
||||
state_store "simple6_fs" {
|
||||
provider "simple6" {}
|
||||
}
|
||||
}
|
||||
|
||||
variable "name" {
|
||||
default = "world"
|
||||
}
|
||||
|
||||
resource "terraform_data" "my-data" {
|
||||
input = "hello ${var.name}"
|
||||
}
|
||||
|
||||
output "greeting" {
|
||||
value = resource.terraform_data.my-data.output
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
simple6 = {
|
||||
source = "registry.terraform.io/hashicorp/simple6"
|
||||
}
|
||||
}
|
||||
|
||||
state_store "simple6_inmem" {
|
||||
provider "simple6" {}
|
||||
}
|
||||
}
|
||||
|
||||
variable "name" {
|
||||
default = "world"
|
||||
}
|
||||
|
||||
resource "terraform_data" "my-data" {
|
||||
input = "hello ${var.name}"
|
||||
}
|
||||
|
||||
output "greeting" {
|
||||
value = resource.terraform_data.my-data.output
|
||||
}
|
||||
@ -0,0 +1,263 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package simple
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/backend"
|
||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||
"github.com/hashicorp/terraform/internal/providers"
|
||||
"github.com/hashicorp/terraform/internal/states/statemgr"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
const fsStoreName = "simple6_fs"
|
||||
const defaultStatesDir = "terraform.tfstate.d"
|
||||
|
||||
// FsStore allows storing state in the local filesystem.
|
||||
//
|
||||
// This state storage implementation differs from the old "local" backend in core,
|
||||
// by storing all states in the custom, or default, states directory. In the "local"
|
||||
// backend the default state was a special case and was handled differently to custom states.
|
||||
type FsStore struct {
|
||||
// Configured values
|
||||
statesDir string
|
||||
chunkSize int64
|
||||
|
||||
states map[string]*statemgr.Filesystem
|
||||
}
|
||||
|
||||
func stateStoreFsGetSchema() providers.Schema {
|
||||
return providers.Schema{
|
||||
Body: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
// Named workspace_dir to match what's present in the local backend
|
||||
"workspace_dir": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "The directory where state files will be created. When unset the value will default to terraform.tfstate.d",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FsStore) ValidateStateStoreConfig(req providers.ValidateStateStoreConfigRequest) providers.ValidateStateStoreConfigResponse {
|
||||
var resp providers.ValidateStateStoreConfigResponse
|
||||
|
||||
attrs := req.Config.AsValueMap()
|
||||
if v, ok := attrs["workspace_dir"]; ok {
|
||||
if !v.IsKnown() {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(errors.New("the attribute \"workspace_dir\" cannot be an unknown value"))
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (f *FsStore) ConfigureStateStore(req providers.ConfigureStateStoreRequest) providers.ConfigureStateStoreResponse {
|
||||
resp := providers.ConfigureStateStoreResponse{}
|
||||
|
||||
configVal := req.Config
|
||||
if v := configVal.GetAttr("workspace_dir"); !v.IsNull() {
|
||||
f.statesDir = v.AsString()
|
||||
} else {
|
||||
f.statesDir = defaultStatesDir
|
||||
}
|
||||
|
||||
if f.states == nil {
|
||||
f.states = make(map[string]*statemgr.Filesystem)
|
||||
}
|
||||
|
||||
// We need to select return a suggested chunk size; use the value suggested by Core
|
||||
resp.Capabilities.ChunkSize = req.Capabilities.ChunkSize
|
||||
f.chunkSize = req.Capabilities.ChunkSize
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (f *FsStore) LockState(req providers.LockStateRequest) providers.LockStateResponse {
|
||||
resp := providers.LockStateResponse{}
|
||||
resp.Diagnostics = resp.Diagnostics.Append(tfdiags.Sourceless(
|
||||
tfdiags.Warning,
|
||||
"Locking not implemented",
|
||||
fmt.Sprintf("Could not lock state %q; state locking isn't implemented", req.StateId),
|
||||
))
|
||||
return resp
|
||||
}
|
||||
|
||||
func (f *FsStore) UnlockState(req providers.UnlockStateRequest) providers.UnlockStateResponse {
|
||||
resp := providers.UnlockStateResponse{}
|
||||
resp.Diagnostics = resp.Diagnostics.Append(tfdiags.Sourceless(
|
||||
tfdiags.Warning,
|
||||
"Unlocking not implemented",
|
||||
fmt.Sprintf("Could not unlock state %q; state locking isn't implemented", req.StateId),
|
||||
))
|
||||
return resp
|
||||
}
|
||||
|
||||
func (f *FsStore) GetStates(req providers.GetStatesRequest) providers.GetStatesResponse {
|
||||
resp := providers.GetStatesResponse{}
|
||||
|
||||
entries, err := os.ReadDir(f.statesDir)
|
||||
// no error if there's no envs configured
|
||||
if os.IsNotExist(err) {
|
||||
return resp
|
||||
}
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
|
||||
var envs []string
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
envs = append(envs, filepath.Base(entry.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(envs)
|
||||
resp.States = envs
|
||||
return resp
|
||||
}
|
||||
|
||||
func (f *FsStore) DeleteState(req providers.DeleteStateRequest) providers.DeleteStateResponse {
|
||||
resp := providers.DeleteStateResponse{}
|
||||
|
||||
if req.StateId == "" {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(errors.New("empty state name"))
|
||||
return resp
|
||||
}
|
||||
|
||||
if req.StateId == backend.DefaultStateName {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(errors.New("cannot delete default state"))
|
||||
return resp
|
||||
}
|
||||
|
||||
delete(f.states, req.StateId)
|
||||
err := os.RemoveAll(filepath.Join(f.statesDir, req.StateId))
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error deleting state %q: %w", req.StateId, err))
|
||||
return resp
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (f *FsStore) getStatePath(stateId string) string {
|
||||
return path.Join(f.statesDir, stateId, "terraform.tfstate")
|
||||
}
|
||||
|
||||
func (f *FsStore) getStateDir(stateId string) string {
|
||||
return path.Join(f.statesDir, stateId)
|
||||
}
|
||||
|
||||
func (f *FsStore) ReadStateBytes(req providers.ReadStateBytesRequest) providers.ReadStateBytesResponse {
|
||||
log.Printf("[DEBUG] ReadStateBytes: reading data from the %q state", req.StateId)
|
||||
resp := providers.ReadStateBytesResponse{}
|
||||
|
||||
// E.g. terraform.tfstate.d/foobar/terraform.tfstate
|
||||
path := f.getStatePath(req.StateId)
|
||||
file, err := os.Open(path)
|
||||
|
||||
fileExists := true
|
||||
if err != nil {
|
||||
if _, ok := err.(*os.PathError); !ok {
|
||||
// Error other than the file not existing
|
||||
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error opening state file %q: %w", path, err))
|
||||
return resp
|
||||
}
|
||||
fileExists = false
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
var processedBytes int
|
||||
|
||||
if fileExists {
|
||||
for {
|
||||
b := make([]byte, f.chunkSize)
|
||||
n, err := file.Read(b)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error reading from state file %q: %w", path, err))
|
||||
return resp
|
||||
}
|
||||
buf.Write(b[0:n])
|
||||
processedBytes += n
|
||||
}
|
||||
}
|
||||
log.Printf("[DEBUG] ReadStateBytes: read %d bytes of data from state file %q", processedBytes, path)
|
||||
|
||||
if processedBytes == 0 {
|
||||
// Does not exist, so return no bytes
|
||||
resp.Diagnostics = resp.Diagnostics.Append(tfdiags.Sourceless(
|
||||
tfdiags.Warning,
|
||||
"State doesn't exist",
|
||||
fmt.Sprintf("The %q state does not exist", req.StateId),
|
||||
))
|
||||
}
|
||||
|
||||
resp.Bytes = buf.Bytes()
|
||||
return resp
|
||||
}
|
||||
|
||||
func (f *FsStore) WriteStateBytes(req providers.WriteStateBytesRequest) providers.WriteStateBytesResponse {
|
||||
log.Printf("[DEBUG] WriteStateBytes: writing data to the %q state", req.StateId)
|
||||
resp := providers.WriteStateBytesResponse{}
|
||||
|
||||
// E.g. terraform.tfstate.d/foobar/terraform.tfstate
|
||||
path := f.getStatePath(req.StateId)
|
||||
|
||||
// Create or open state file
|
||||
dir := f.getStateDir(req.StateId)
|
||||
err := os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error creating state file directory %q: %w", dir, err))
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error opening state file %q: %w", path, err))
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(req.Bytes)
|
||||
var processedBytes int
|
||||
if f.chunkSize == 0 {
|
||||
panic("WriteStateBytes: chunk size zero. This is an error in Terraform and should be reported")
|
||||
}
|
||||
for {
|
||||
data := buf.Next(int(f.chunkSize))
|
||||
if len(data) == 0 {
|
||||
break
|
||||
}
|
||||
n, err := file.Write(data)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error writing to state file %q: %w", path, err))
|
||||
return resp
|
||||
}
|
||||
|
||||
processedBytes += n
|
||||
}
|
||||
log.Printf("[DEBUG] WriteStateBytes: wrote %d bytes of data to state file %q", processedBytes, path)
|
||||
|
||||
if processedBytes == 0 {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("missing state data: write action wrote %d bytes of data to file %q.", processedBytes, path))
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package simple
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/backend"
|
||||
"github.com/hashicorp/terraform/internal/backend/pluggable"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// TODO: Testing of locking with 2 clients once locking is fully implemented.
|
||||
|
||||
func TestFsStoreRemoteState(t *testing.T) {
|
||||
td := t.TempDir()
|
||||
t.Chdir(td)
|
||||
|
||||
provider := Provider()
|
||||
|
||||
plug, err := pluggable.NewPluggable(provider, fsStoreName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b := backend.TestBackendConfig(t, plug, hcl.EmptyBody())
|
||||
|
||||
// The "default" state doesn't exist by default
|
||||
// (Note that this depends on the factory method used to get the provider above)
|
||||
stateIds, wDiags := b.Workspaces()
|
||||
if wDiags.HasErrors() {
|
||||
t.Fatal(wDiags.Err())
|
||||
}
|
||||
if len(stateIds) != 0 {
|
||||
t.Fatalf("unexpected response from Workspaces method: %#v", stateIds)
|
||||
}
|
||||
|
||||
// create a new state using this backend
|
||||
newStateId := "foobar"
|
||||
emptyState := states.NewState()
|
||||
|
||||
sMgr, sDiags := b.StateMgr(newStateId)
|
||||
if sDiags.HasErrors() {
|
||||
t.Fatal(sDiags.Err())
|
||||
}
|
||||
if err := sMgr.WriteState(emptyState); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := sMgr.PersistState(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// force overwriting the remote state
|
||||
newState := states.NewState()
|
||||
newState.SetOutputValue(
|
||||
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
|
||||
cty.StringVal("bar"),
|
||||
false)
|
||||
|
||||
if err := sMgr.WriteState(newState); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := sMgr.PersistState(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := sMgr.RefreshState(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,226 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package simple
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/hashicorp/terraform/internal/backend"
|
||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||
"github.com/hashicorp/terraform/internal/providers"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
const inMemStoreName = "simple6_inmem"
|
||||
|
||||
// InMemStoreSingle allows 'storing' state in memory for the purpose of testing.
|
||||
//
|
||||
// "Single" reflects the fact that this implementation does not use any global scope vars
|
||||
// in its implementation, unlike the current inmem backend. HOWEVER, you can test whether locking
|
||||
// blocks multiple clients trying to access the same state at once by creating multiple instances
|
||||
// of backend.Backend that wrap the same provider.Interface instance.
|
||||
type InMemStoreSingle struct {
|
||||
states stateMap
|
||||
locks lockMap
|
||||
}
|
||||
|
||||
func stateStoreInMemGetSchema() providers.Schema {
|
||||
return providers.Schema{
|
||||
Body: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"lock_id": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Description: "initializes the state in a locked configuration",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *InMemStoreSingle) ValidateStateStoreConfig(req providers.ValidateStateStoreConfigRequest) providers.ValidateStateStoreConfigResponse {
|
||||
var resp providers.ValidateStateStoreConfigResponse
|
||||
|
||||
attrs := req.Config.AsValueMap()
|
||||
|
||||
// This is completely arbitrary validation included here to avoid this method being empty. It is not here for a purpose,
|
||||
// but could be used if an E2E test wants to trigger a validation error.
|
||||
if v, ok := attrs["lock_id"]; ok && !v.IsNull() {
|
||||
cutoff := cty.NumberVal(big.NewFloat(3))
|
||||
if v.Length().LessThan(cutoff) == cty.True {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("when set, the attribute \"lock_id\" must have a length equal or greater than %s", cutoff.AsString()))
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (m *InMemStoreSingle) ConfigureStateStore(req providers.ConfigureStateStoreRequest) providers.ConfigureStateStoreResponse {
|
||||
resp := providers.ConfigureStateStoreResponse{}
|
||||
|
||||
m.states.Lock()
|
||||
defer m.states.Unlock()
|
||||
|
||||
// set the default client lock info per the test config
|
||||
configVal := req.Config
|
||||
if v := configVal.GetAttr("lock_id"); !v.IsNull() {
|
||||
m.locks.lock(backend.DefaultStateName, v.AsString())
|
||||
}
|
||||
|
||||
// We need to return a suggested chunk size; use the value suggested by Core
|
||||
resp.Capabilities.ChunkSize = req.Capabilities.ChunkSize
|
||||
return resp
|
||||
}
|
||||
|
||||
func (m *InMemStoreSingle) ReadStateBytes(req providers.ReadStateBytesRequest) providers.ReadStateBytesResponse {
|
||||
resp := providers.ReadStateBytesResponse{}
|
||||
|
||||
v, ok := m.states.m[req.StateId]
|
||||
if !ok {
|
||||
// Does not exist, so return no bytes
|
||||
|
||||
resp.Diagnostics = resp.Diagnostics.Append(tfdiags.Sourceless(
|
||||
tfdiags.Warning,
|
||||
"State doesn't exist",
|
||||
fmt.Sprintf("The %q state does not exist", req.StateId),
|
||||
))
|
||||
return resp
|
||||
}
|
||||
|
||||
resp.Bytes = v
|
||||
return resp
|
||||
}
|
||||
|
||||
func (m *InMemStoreSingle) WriteStateBytes(req providers.WriteStateBytesRequest) providers.WriteStateBytesResponse {
|
||||
resp := providers.WriteStateBytesResponse{}
|
||||
|
||||
if m.states.m == nil {
|
||||
m.states.m = make(map[string][]byte, 1)
|
||||
}
|
||||
|
||||
m.states.m[req.StateId] = req.Bytes
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (m *InMemStoreSingle) LockState(req providers.LockStateRequest) providers.LockStateResponse {
|
||||
resp := providers.LockStateResponse{}
|
||||
|
||||
lockIdBytes, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error creating random lock uuid: %w", err))
|
||||
return resp
|
||||
}
|
||||
|
||||
lockId := string(lockIdBytes)
|
||||
returnedLockId, err := m.locks.lock(req.StateId, lockId)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
}
|
||||
|
||||
resp.LockId = string(returnedLockId)
|
||||
return resp
|
||||
}
|
||||
|
||||
func (m *InMemStoreSingle) UnlockState(req providers.UnlockStateRequest) providers.UnlockStateResponse {
|
||||
resp := providers.UnlockStateResponse{}
|
||||
|
||||
err := m.locks.unlock(req.StateId, req.LockId)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error when unlocking state %q: %w", req.StateId, err))
|
||||
return resp
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (m *InMemStoreSingle) GetStates(req providers.GetStatesRequest) providers.GetStatesResponse {
|
||||
m.states.Lock()
|
||||
defer m.states.Unlock()
|
||||
|
||||
resp := providers.GetStatesResponse{}
|
||||
|
||||
var stateIds []string
|
||||
|
||||
for s := range m.states.m {
|
||||
stateIds = append(stateIds, s)
|
||||
}
|
||||
|
||||
sort.Strings(stateIds)
|
||||
resp.States = stateIds
|
||||
return resp
|
||||
}
|
||||
|
||||
func (m *InMemStoreSingle) DeleteState(req providers.DeleteStateRequest) providers.DeleteStateResponse {
|
||||
m.states.Lock()
|
||||
defer m.states.Unlock()
|
||||
|
||||
resp := providers.DeleteStateResponse{}
|
||||
|
||||
if req.StateId == backend.DefaultStateName || req.StateId == "" {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("can't delete default state"))
|
||||
return resp
|
||||
}
|
||||
|
||||
delete(m.states.m, req.StateId)
|
||||
return resp
|
||||
}
|
||||
|
||||
type stateMap struct {
|
||||
sync.Mutex
|
||||
m map[string][]byte // key=state id, value=state
|
||||
}
|
||||
|
||||
type lockMap struct {
|
||||
sync.Mutex
|
||||
m map[string]string // key=state id, value=lock_id
|
||||
}
|
||||
|
||||
func (l *lockMap) lock(name string, lockId string) (string, error) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
|
||||
lock, ok := l.m[name]
|
||||
if ok {
|
||||
// Error; lock already exists for that state id
|
||||
return "", fmt.Errorf("state %q is already locked with lock id %q", name, lock)
|
||||
}
|
||||
|
||||
if l.m == nil {
|
||||
l.m = make(map[string]string, 1)
|
||||
}
|
||||
|
||||
l.m[name] = lockId
|
||||
|
||||
return lockId, nil
|
||||
}
|
||||
|
||||
func (l *lockMap) unlock(name, id string) error {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
|
||||
lockId, ok := l.m[name]
|
||||
|
||||
if !ok {
|
||||
return errors.New("state not locked")
|
||||
}
|
||||
|
||||
if id != lockId {
|
||||
return fmt.Errorf("invalid lock id: state %q was locked with lock id %q, but tried to unlock with lock id %q",
|
||||
name,
|
||||
lockId,
|
||||
id,
|
||||
)
|
||||
}
|
||||
|
||||
delete(l.m, name)
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package simple
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/backend"
|
||||
"github.com/hashicorp/terraform/internal/backend/pluggable"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestInMemStoreLocked(t *testing.T) {
|
||||
// backend.TestBackendStateLocks assumes the "default" state exists
|
||||
// by default, so we need to make it exist using the method below.
|
||||
provider := ProviderWithDefaultState()
|
||||
|
||||
plug1, err := pluggable.NewPluggable(provider, inMemStoreName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plug2, err := pluggable.NewPluggable(provider, inMemStoreName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b1 := backend.TestBackendConfig(t, plug1, nil)
|
||||
b2 := backend.TestBackendConfig(t, plug2, nil)
|
||||
|
||||
backend.TestBackendStateLocks(t, b1, b2)
|
||||
}
|
||||
|
||||
func TestInMemStoreRemoteState(t *testing.T) {
|
||||
provider := Provider()
|
||||
|
||||
plug, err := pluggable.NewPluggable(provider, inMemStoreName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b := backend.TestBackendConfig(t, plug, hcl.EmptyBody())
|
||||
|
||||
// The "default" state doesn't exist by default
|
||||
// (Note that this depends on the factory method used to get the provider above)
|
||||
stateIds, wDiags := b.Workspaces()
|
||||
if wDiags.HasErrors() {
|
||||
t.Fatal(wDiags.Err())
|
||||
}
|
||||
if len(stateIds) != 0 {
|
||||
t.Fatalf("unexpected response from Workspaces method: %#v", stateIds)
|
||||
}
|
||||
|
||||
// create a new state using this backend
|
||||
newStateId := "foobar"
|
||||
emptyState := states.NewState()
|
||||
|
||||
sMgr, sDiags := b.StateMgr(newStateId)
|
||||
if sDiags.HasErrors() {
|
||||
t.Fatal(sDiags.Err())
|
||||
}
|
||||
if err := sMgr.WriteState(emptyState); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := sMgr.PersistState(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// force overwriting the remote state
|
||||
newState := states.NewState()
|
||||
newState.SetOutputValue(
|
||||
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
|
||||
cty.StringVal("bar"),
|
||||
false)
|
||||
|
||||
if err := sMgr.WriteState(newState); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := sMgr.PersistState(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := sMgr.RefreshState(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue