github.com/kevinklinger/open_terraform@v1.3.6/noninternal/backend/local/backend_local_test.go (about)

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