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 }