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