github.com/opentofu/opentofu@v1.7.1/internal/backend/local/backend_local_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package local
     7  
     8  import (
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"testing"
    13  
    14  	"github.com/zclconf/go-cty/cty"
    15  
    16  	"github.com/opentofu/opentofu/internal/backend"
    17  	"github.com/opentofu/opentofu/internal/command/arguments"
    18  	"github.com/opentofu/opentofu/internal/command/clistate"
    19  	"github.com/opentofu/opentofu/internal/command/views"
    20  	"github.com/opentofu/opentofu/internal/configs/configload"
    21  	"github.com/opentofu/opentofu/internal/configs/configschema"
    22  	"github.com/opentofu/opentofu/internal/encryption"
    23  	"github.com/opentofu/opentofu/internal/initwd"
    24  	"github.com/opentofu/opentofu/internal/plans"
    25  	"github.com/opentofu/opentofu/internal/plans/planfile"
    26  	"github.com/opentofu/opentofu/internal/states"
    27  	"github.com/opentofu/opentofu/internal/states/statefile"
    28  	"github.com/opentofu/opentofu/internal/states/statemgr"
    29  	"github.com/opentofu/opentofu/internal/terminal"
    30  	"github.com/opentofu/opentofu/internal/tfdiags"
    31  	"github.com/opentofu/opentofu/internal/tofu"
    32  )
    33  
    34  func TestLocalRun(t *testing.T) {
    35  	configDir := "./testdata/empty"
    36  	b := TestLocal(t)
    37  
    38  	_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
    39  	defer configCleanup()
    40  
    41  	streams, _ := terminal.StreamsForTesting(t)
    42  	view := views.NewView(streams)
    43  	stateLocker := clistate.NewLocker(0, views.NewStateLocker(arguments.ViewHuman, view))
    44  
    45  	op := &backend.Operation{
    46  		ConfigDir:    configDir,
    47  		ConfigLoader: configLoader,
    48  		Workspace:    backend.DefaultStateName,
    49  		StateLocker:  stateLocker,
    50  	}
    51  
    52  	_, _, diags := b.LocalRun(op)
    53  	if diags.HasErrors() {
    54  		t.Fatalf("unexpected error: %s", diags.Err().Error())
    55  	}
    56  
    57  	// LocalRun() retains a lock on success
    58  	assertBackendStateLocked(t, b)
    59  }
    60  
    61  func TestLocalRun_error(t *testing.T) {
    62  	configDir := "./testdata/invalid"
    63  	b := TestLocal(t)
    64  
    65  	// This backend will return an error when asked to RefreshState, which
    66  	// should then cause LocalRun to return with the state unlocked.
    67  	b.Backend = backendWithStateStorageThatFailsRefresh{}
    68  
    69  	_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
    70  	defer configCleanup()
    71  
    72  	streams, _ := terminal.StreamsForTesting(t)
    73  	view := views.NewView(streams)
    74  	stateLocker := clistate.NewLocker(0, views.NewStateLocker(arguments.ViewHuman, view))
    75  
    76  	op := &backend.Operation{
    77  		ConfigDir:    configDir,
    78  		ConfigLoader: configLoader,
    79  		Workspace:    backend.DefaultStateName,
    80  		StateLocker:  stateLocker,
    81  	}
    82  
    83  	_, _, diags := b.LocalRun(op)
    84  	if !diags.HasErrors() {
    85  		t.Fatal("unexpected success")
    86  	}
    87  
    88  	// LocalRun() unlocks the state on failure
    89  	assertBackendStateUnlocked(t, b)
    90  }
    91  
    92  func TestLocalRun_cloudPlan(t *testing.T) {
    93  	configDir := "./testdata/apply"
    94  	b := TestLocal(t)
    95  
    96  	_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
    97  	defer configCleanup()
    98  
    99  	planPath := "./testdata/plan-bookmark/bookmark.json"
   100  
   101  	planFile, err := planfile.OpenWrapped(planPath, encryption.PlanEncryptionDisabled())
   102  	if err != nil {
   103  		t.Fatalf("unexpected error reading planfile: %s", err)
   104  	}
   105  
   106  	streams, _ := terminal.StreamsForTesting(t)
   107  	view := views.NewView(streams)
   108  	stateLocker := clistate.NewLocker(0, views.NewStateLocker(arguments.ViewHuman, view))
   109  
   110  	op := &backend.Operation{
   111  		ConfigDir:    configDir,
   112  		ConfigLoader: configLoader,
   113  		PlanFile:     planFile,
   114  		Workspace:    backend.DefaultStateName,
   115  		StateLocker:  stateLocker,
   116  	}
   117  
   118  	_, _, diags := b.LocalRun(op)
   119  	if !diags.HasErrors() {
   120  		t.Fatal("unexpected success")
   121  	}
   122  
   123  	// LocalRun() unlocks the state on failure
   124  	assertBackendStateUnlocked(t, b)
   125  }
   126  
   127  func TestLocalRun_stalePlan(t *testing.T) {
   128  	configDir := "./testdata/apply"
   129  	b := TestLocal(t)
   130  
   131  	_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
   132  	defer configCleanup()
   133  
   134  	// Write an empty state file with serial 3
   135  	sf, err := os.Create(b.StatePath)
   136  	if err != nil {
   137  		t.Fatalf("unexpected error creating state file %s: %s", b.StatePath, err)
   138  	}
   139  	if err := statefile.Write(statefile.New(states.NewState(), "boop", 3), sf, encryption.StateEncryptionDisabled()); err != nil {
   140  		t.Fatalf("unexpected error writing state file: %s", err)
   141  	}
   142  
   143  	// Refresh the state
   144  	sm, err := b.StateMgr("")
   145  	if err != nil {
   146  		t.Fatalf("unexpected error: %s", err)
   147  	}
   148  	if err := sm.RefreshState(); err != nil {
   149  		t.Fatalf("unexpected error refreshing state: %s", err)
   150  	}
   151  
   152  	// Create a minimal plan which also has state file serial 2, so is stale
   153  	backendConfig := cty.ObjectVal(map[string]cty.Value{
   154  		"path":          cty.NullVal(cty.String),
   155  		"workspace_dir": cty.NullVal(cty.String),
   156  	})
   157  	backendConfigRaw, err := plans.NewDynamicValue(backendConfig, backendConfig.Type())
   158  	if err != nil {
   159  		t.Fatal(err)
   160  	}
   161  	plan := &plans.Plan{
   162  		UIMode:  plans.NormalMode,
   163  		Changes: plans.NewChanges(),
   164  		Backend: plans.Backend{
   165  			Type:   "local",
   166  			Config: backendConfigRaw,
   167  		},
   168  		PrevRunState: states.NewState(),
   169  		PriorState:   states.NewState(),
   170  	}
   171  	prevStateFile := statefile.New(plan.PrevRunState, "boop", 1)
   172  	stateFile := statefile.New(plan.PriorState, "boop", 2)
   173  
   174  	// Roundtrip through serialization as expected by the operation
   175  	outDir := t.TempDir()
   176  	defer os.RemoveAll(outDir)
   177  	planPath := filepath.Join(outDir, "plan.tfplan")
   178  	planfileArgs := planfile.CreateArgs{
   179  		ConfigSnapshot:       configload.NewEmptySnapshot(),
   180  		PreviousRunStateFile: prevStateFile,
   181  		StateFile:            stateFile,
   182  		Plan:                 plan,
   183  	}
   184  	if err := planfile.Create(planPath, planfileArgs, encryption.PlanEncryptionDisabled()); err != nil {
   185  		t.Fatalf("unexpected error writing planfile: %s", err)
   186  	}
   187  	planFile, err := planfile.OpenWrapped(planPath, encryption.PlanEncryptionDisabled())
   188  	if err != nil {
   189  		t.Fatalf("unexpected error reading planfile: %s", err)
   190  	}
   191  
   192  	streams, _ := terminal.StreamsForTesting(t)
   193  	view := views.NewView(streams)
   194  	stateLocker := clistate.NewLocker(0, views.NewStateLocker(arguments.ViewHuman, view))
   195  
   196  	op := &backend.Operation{
   197  		ConfigDir:    configDir,
   198  		ConfigLoader: configLoader,
   199  		PlanFile:     planFile,
   200  		Workspace:    backend.DefaultStateName,
   201  		StateLocker:  stateLocker,
   202  	}
   203  
   204  	_, _, diags := b.LocalRun(op)
   205  	if !diags.HasErrors() {
   206  		t.Fatal("unexpected success")
   207  	}
   208  
   209  	// LocalRun() unlocks the state on failure
   210  	assertBackendStateUnlocked(t, b)
   211  }
   212  
   213  type backendWithStateStorageThatFailsRefresh struct {
   214  }
   215  
   216  var _ backend.Backend = backendWithStateStorageThatFailsRefresh{}
   217  
   218  func (b backendWithStateStorageThatFailsRefresh) StateMgr(workspace string) (statemgr.Full, error) {
   219  	return &stateStorageThatFailsRefresh{}, nil
   220  }
   221  
   222  func (b backendWithStateStorageThatFailsRefresh) ConfigSchema() *configschema.Block {
   223  	return &configschema.Block{}
   224  }
   225  
   226  func (b backendWithStateStorageThatFailsRefresh) PrepareConfig(in cty.Value) (cty.Value, tfdiags.Diagnostics) {
   227  	return in, nil
   228  }
   229  
   230  func (b backendWithStateStorageThatFailsRefresh) Configure(cty.Value) tfdiags.Diagnostics {
   231  	return nil
   232  }
   233  
   234  func (b backendWithStateStorageThatFailsRefresh) DeleteWorkspace(name string, force bool) error {
   235  	return fmt.Errorf("unimplemented")
   236  }
   237  
   238  func (b backendWithStateStorageThatFailsRefresh) Workspaces() ([]string, error) {
   239  	return []string{"default"}, nil
   240  }
   241  
   242  type stateStorageThatFailsRefresh struct {
   243  	locked bool
   244  }
   245  
   246  func (s *stateStorageThatFailsRefresh) Lock(info *statemgr.LockInfo) (string, error) {
   247  	if s.locked {
   248  		return "", fmt.Errorf("already locked")
   249  	}
   250  	s.locked = true
   251  	return "locked", nil
   252  }
   253  
   254  func (s *stateStorageThatFailsRefresh) Unlock(id string) error {
   255  	if !s.locked {
   256  		return fmt.Errorf("not locked")
   257  	}
   258  	s.locked = false
   259  	return nil
   260  }
   261  
   262  func (s *stateStorageThatFailsRefresh) State() *states.State {
   263  	return nil
   264  }
   265  
   266  func (s *stateStorageThatFailsRefresh) GetRootOutputValues() (map[string]*states.OutputValue, error) {
   267  	return nil, fmt.Errorf("unimplemented")
   268  }
   269  
   270  func (s *stateStorageThatFailsRefresh) WriteState(*states.State) error {
   271  	return fmt.Errorf("unimplemented")
   272  }
   273  
   274  func (s *stateStorageThatFailsRefresh) RefreshState() error {
   275  	return fmt.Errorf("intentionally failing for testing purposes")
   276  }
   277  
   278  func (s *stateStorageThatFailsRefresh) PersistState(schemas *tofu.Schemas) error {
   279  	return fmt.Errorf("unimplemented")
   280  }