mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 18:01:01 -06:00
c33c8b013f
Normally, `terraform output` refreshes and reads the entire state in the command package before pulling output values out of it. This doesn't give Terraform Cloud the opportunity to apply the read state outputs org permission and instead applies the read state versions permission. I decided to expand the state manager interface to provide a separate GetRootOutputValues function in order to give the cloud backend a more nuanced opportunity to fetch just the outputs. This required moving state Refresh/Read code that was previously in the command into the shared backend state as well as the filesystem state packages.
238 lines
6.7 KiB
Go
238 lines
6.7 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 := TestLocal(t)
|
|
|
|
_, 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 := TestLocal(t)
|
|
|
|
// 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 := TestLocal(t)
|
|
|
|
_, 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")
|
|
planfileArgs := planfile.CreateArgs{
|
|
ConfigSnapshot: configload.NewEmptySnapshot(),
|
|
PreviousRunStateFile: prevStateFile,
|
|
StateFile: stateFile,
|
|
Plan: plan,
|
|
}
|
|
if err := planfile.Create(planPath, planfileArgs); 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) GetRootOutputValues() (map[string]*states.OutputValue, error) {
|
|
return nil, fmt.Errorf("unimplemented")
|
|
}
|
|
|
|
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")
|
|
}
|