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