opentofu/internal/backend/local/backend_local_test.go
James Bardin d0993b0e80 fix temp directory handling in some tests
Cleanup some more test fixtures to use t.TempDir

Use EvalSymlinks with temp dir paths to help with MacOS errors from
various terraform components.
2021-09-13 13:45:04 -04:00

231 lines
6.5 KiB
Go

package local
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/clistate"
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/configs/configload"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/plans/planfile"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/states/statefile"
"github.com/hashicorp/terraform/internal/states/statemgr"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
func TestLocalRun(t *testing.T) {
configDir := "./testdata/empty"
b, cleanup := TestLocal(t)
defer cleanup()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
defer configCleanup()
streams, _ := terminal.StreamsForTesting(t)
view := views.NewView(streams)
stateLocker := clistate.NewLocker(0, views.NewStateLocker(arguments.ViewHuman, view))
op := &backend.Operation{
ConfigDir: configDir,
ConfigLoader: configLoader,
Workspace: backend.DefaultStateName,
StateLocker: stateLocker,
}
_, _, diags := b.LocalRun(op)
if diags.HasErrors() {
t.Fatalf("unexpected error: %s", diags.Err().Error())
}
// LocalRun() retains a lock on success
assertBackendStateLocked(t, b)
}
func TestLocalRun_error(t *testing.T) {
configDir := "./testdata/invalid"
b, cleanup := TestLocal(t)
defer cleanup()
// This backend will return an error when asked to RefreshState, which
// should then cause LocalRun to return with the state unlocked.
b.Backend = backendWithStateStorageThatFailsRefresh{}
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
defer configCleanup()
streams, _ := terminal.StreamsForTesting(t)
view := views.NewView(streams)
stateLocker := clistate.NewLocker(0, views.NewStateLocker(arguments.ViewHuman, view))
op := &backend.Operation{
ConfigDir: configDir,
ConfigLoader: configLoader,
Workspace: backend.DefaultStateName,
StateLocker: stateLocker,
}
_, _, diags := b.LocalRun(op)
if !diags.HasErrors() {
t.Fatal("unexpected success")
}
// LocalRun() unlocks the state on failure
assertBackendStateUnlocked(t, b)
}
func TestLocalRun_stalePlan(t *testing.T) {
configDir := "./testdata/apply"
b, cleanup := TestLocal(t)
defer cleanup()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
defer configCleanup()
// Write an empty state file with serial 3
sf, err := os.Create(b.StatePath)
if err != nil {
t.Fatalf("unexpected error creating state file %s: %s", b.StatePath, err)
}
if err := statefile.Write(statefile.New(states.NewState(), "boop", 3), sf); err != nil {
t.Fatalf("unexpected error writing state file: %s", err)
}
// Refresh the state
sm, err := b.StateMgr("")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if err := sm.RefreshState(); err != nil {
t.Fatalf("unexpected error refreshing state: %s", err)
}
// Create a minimal plan which also has state file serial 2, so is stale
backendConfig := cty.ObjectVal(map[string]cty.Value{
"path": cty.NullVal(cty.String),
"workspace_dir": cty.NullVal(cty.String),
})
backendConfigRaw, err := plans.NewDynamicValue(backendConfig, backendConfig.Type())
if err != nil {
t.Fatal(err)
}
plan := &plans.Plan{
UIMode: plans.NormalMode,
Changes: plans.NewChanges(),
Backend: plans.Backend{
Type: "local",
Config: backendConfigRaw,
},
PrevRunState: states.NewState(),
PriorState: states.NewState(),
}
prevStateFile := statefile.New(plan.PrevRunState, "boop", 1)
stateFile := statefile.New(plan.PriorState, "boop", 2)
// Roundtrip through serialization as expected by the operation
outDir := t.TempDir()
defer os.RemoveAll(outDir)
planPath := filepath.Join(outDir, "plan.tfplan")
if err := planfile.Create(planPath, configload.NewEmptySnapshot(), prevStateFile, stateFile, plan); err != nil {
t.Fatalf("unexpected error writing planfile: %s", err)
}
planFile, err := planfile.Open(planPath)
if err != nil {
t.Fatalf("unexpected error reading planfile: %s", err)
}
streams, _ := terminal.StreamsForTesting(t)
view := views.NewView(streams)
stateLocker := clistate.NewLocker(0, views.NewStateLocker(arguments.ViewHuman, view))
op := &backend.Operation{
ConfigDir: configDir,
ConfigLoader: configLoader,
PlanFile: planFile,
Workspace: backend.DefaultStateName,
StateLocker: stateLocker,
}
_, _, diags := b.LocalRun(op)
if !diags.HasErrors() {
t.Fatal("unexpected success")
}
// LocalRun() unlocks the state on failure
assertBackendStateUnlocked(t, b)
}
type backendWithStateStorageThatFailsRefresh struct {
}
var _ backend.Backend = backendWithStateStorageThatFailsRefresh{}
func (b backendWithStateStorageThatFailsRefresh) StateMgr(workspace string) (statemgr.Full, error) {
return &stateStorageThatFailsRefresh{}, nil
}
func (b backendWithStateStorageThatFailsRefresh) ConfigSchema() *configschema.Block {
return &configschema.Block{}
}
func (b backendWithStateStorageThatFailsRefresh) PrepareConfig(in cty.Value) (cty.Value, tfdiags.Diagnostics) {
return in, nil
}
func (b backendWithStateStorageThatFailsRefresh) Configure(cty.Value) tfdiags.Diagnostics {
return nil
}
func (b backendWithStateStorageThatFailsRefresh) DeleteWorkspace(name string) error {
return fmt.Errorf("unimplemented")
}
func (b backendWithStateStorageThatFailsRefresh) Workspaces() ([]string, error) {
return []string{"default"}, nil
}
type stateStorageThatFailsRefresh struct {
locked bool
}
func (s *stateStorageThatFailsRefresh) Lock(info *statemgr.LockInfo) (string, error) {
if s.locked {
return "", fmt.Errorf("already locked")
}
s.locked = true
return "locked", nil
}
func (s *stateStorageThatFailsRefresh) Unlock(id string) error {
if !s.locked {
return fmt.Errorf("not locked")
}
s.locked = false
return nil
}
func (s *stateStorageThatFailsRefresh) State() *states.State {
return nil
}
func (s *stateStorageThatFailsRefresh) WriteState(*states.State) error {
return fmt.Errorf("unimplemented")
}
func (s *stateStorageThatFailsRefresh) RefreshState() error {
return fmt.Errorf("intentionally failing for testing purposes")
}
func (s *stateStorageThatFailsRefresh) PersistState() error {
return fmt.Errorf("unimplemented")
}