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