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 }