opentofu/internal/backend/local/testing.go
Christian Mesh 2f5dcd5c0a
Integrate Encryption into State Backends (#1288)
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
2024-03-04 09:25:14 -05:00

236 lines
7.3 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package local
import (
"path/filepath"
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/backend"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/states"
"github.com/opentofu/opentofu/internal/states/statemgr"
"github.com/opentofu/opentofu/internal/tofu"
)
// TestLocal returns a configured Local struct with temporary paths and
// in-memory ContextOpts.
//
// No operations will be called on the returned value, so you can still set
// public fields without any locks.
func TestLocal(t *testing.T) *Local {
t.Helper()
tempDir, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatal(err)
}
local := New(encryption.StateEncryptionDisabled())
local.StatePath = filepath.Join(tempDir, "state.tfstate")
local.StateOutPath = filepath.Join(tempDir, "state.tfstate")
local.StateBackupPath = filepath.Join(tempDir, "state.tfstate.bak")
local.StateWorkspaceDir = filepath.Join(tempDir, "state.tfstate.d")
local.ContextOpts = &tofu.ContextOpts{}
return local
}
// TestLocalProvider modifies the ContextOpts of the *Local parameter to
// have a provider with the given name.
func TestLocalProvider(t *testing.T, b *Local, name string, schema providers.ProviderSchema) *tofu.MockProvider {
// Build a mock resource provider for in-memory operations
p := new(tofu.MockProvider)
p.GetProviderSchemaResponse = &schema
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
// this is a destroy plan,
if req.ProposedNewState.IsNull() {
resp.PlannedState = req.ProposedNewState
resp.PlannedPrivate = req.PriorPrivate
return resp
}
rSchema, _ := schema.SchemaForResourceType(addrs.ManagedResourceMode, req.TypeName)
if rSchema == nil {
rSchema = &configschema.Block{} // default schema is empty
}
plannedVals := map[string]cty.Value{}
for name, attrS := range rSchema.Attributes {
val := req.ProposedNewState.GetAttr(name)
if attrS.Computed && val.IsNull() {
val = cty.UnknownVal(attrS.Type)
}
plannedVals[name] = val
}
for name := range rSchema.BlockTypes {
// For simplicity's sake we just copy the block attributes over
// verbatim, since this package's mock providers are all relatively
// simple -- we're testing the backend, not esoteric provider features.
plannedVals[name] = req.ProposedNewState.GetAttr(name)
}
return providers.PlanResourceChangeResponse{
PlannedState: cty.ObjectVal(plannedVals),
PlannedPrivate: req.PriorPrivate,
}
}
p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse {
return providers.ReadResourceResponse{NewState: req.PriorState}
}
p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
return providers.ReadDataSourceResponse{State: req.Config}
}
// Initialize the opts
if b.ContextOpts == nil {
b.ContextOpts = &tofu.ContextOpts{}
}
// Set up our provider
b.ContextOpts.Providers = map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider(name): providers.FactoryFixed(p),
}
return p
}
// TestLocalSingleState is a backend implementation that wraps Local
// and modifies it to only support single states (returns
// ErrWorkspacesNotSupported for multi-state operations).
//
// This isn't an actual use case, this is exported just to provide a
// easy way to test that behavior.
type TestLocalSingleState struct {
*Local
}
// TestNewLocalSingle is a factory for creating a TestLocalSingleState.
// This function matches the signature required for backend/init.
func TestNewLocalSingle(enc encryption.StateEncryption) backend.Backend {
return &TestLocalSingleState{Local: New(encryption.StateEncryptionDisabled())}
}
func (b *TestLocalSingleState) Workspaces() ([]string, error) {
return nil, backend.ErrWorkspacesNotSupported
}
func (b *TestLocalSingleState) DeleteWorkspace(string, bool) error {
return backend.ErrWorkspacesNotSupported
}
func (b *TestLocalSingleState) StateMgr(name string) (statemgr.Full, error) {
if name != backend.DefaultStateName {
return nil, backend.ErrWorkspacesNotSupported
}
return b.Local.StateMgr(name)
}
// TestLocalNoDefaultState is a backend implementation that wraps
// Local and modifies it to support named states, but not the
// default state. It returns ErrDefaultWorkspaceNotSupported when
// the DefaultStateName is used.
type TestLocalNoDefaultState struct {
*Local
}
// TestNewLocalNoDefault is a factory for creating a TestLocalNoDefaultState.
// This function matches the signature required for backend/init.
func TestNewLocalNoDefault(enc encryption.StateEncryption) backend.Backend {
return &TestLocalNoDefaultState{Local: New(encryption.StateEncryptionDisabled())}
}
func (b *TestLocalNoDefaultState) Workspaces() ([]string, error) {
workspaces, err := b.Local.Workspaces()
if err != nil {
return nil, err
}
filtered := workspaces[:0]
for _, name := range workspaces {
if name != backend.DefaultStateName {
filtered = append(filtered, name)
}
}
return filtered, nil
}
func (b *TestLocalNoDefaultState) DeleteWorkspace(name string, force bool) error {
if name == backend.DefaultStateName {
return backend.ErrDefaultWorkspaceNotSupported
}
return b.Local.DeleteWorkspace(name, force)
}
func (b *TestLocalNoDefaultState) StateMgr(name string) (statemgr.Full, error) {
if name == backend.DefaultStateName {
return nil, backend.ErrDefaultWorkspaceNotSupported
}
return b.Local.StateMgr(name)
}
func testStateFile(t *testing.T, path string, s *states.State) {
t.Helper()
if err := statemgr.WriteAndPersist(statemgr.NewFilesystem(path, encryption.StateEncryptionDisabled()), s, nil); err != nil {
t.Fatal(err)
}
}
func mustProviderConfig(s string) addrs.AbsProviderConfig {
p, diags := addrs.ParseAbsProviderConfigStr(s)
if diags.HasErrors() {
panic(diags.Err())
}
return p
}
func mustResourceInstanceAddr(s string) addrs.AbsResourceInstance {
addr, diags := addrs.ParseAbsResourceInstanceStr(s)
if diags.HasErrors() {
panic(diags.Err())
}
return addr
}
// assertBackendStateUnlocked attempts to lock the backend state for a test.
// Failure indicates that the state was locked and false is returned.
// True is returned if a lock was obtained.
func assertBackendStateUnlocked(t *testing.T, b *Local) bool {
t.Helper()
stateMgr, _ := b.StateMgr(backend.DefaultStateName)
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
t.Errorf("state is already locked: %s", err.Error())
// lock was obtained
return false
}
// lock was not obtained
return true
}
// assertBackendStateLocked attempts to lock the backend state for a test.
// Failure indicates that the state was not locked and false is returned.
// True is returned if a lock was not obtained.
func assertBackendStateLocked(t *testing.T, b *Local) bool {
t.Helper()
stateMgr, _ := b.StateMgr(backend.DefaultStateName)
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
// lock was not obtained
return true
}
t.Error("unexpected success locking state")
// lock was obtained
return false
}