github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/local/backend_local_test.go (about)

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