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