github.com/hugorut/terraform@v1.1.3/src/backend/local/backend_apply_test.go (about) 1 package local 2 3 import ( 4 "context" 5 "errors" 6 "os" 7 "path/filepath" 8 "strings" 9 "sync" 10 "testing" 11 12 "github.com/zclconf/go-cty/cty" 13 14 "github.com/hugorut/terraform/src/addrs" 15 "github.com/hugorut/terraform/src/backend" 16 "github.com/hugorut/terraform/src/command/arguments" 17 "github.com/hugorut/terraform/src/command/clistate" 18 "github.com/hugorut/terraform/src/command/views" 19 "github.com/hugorut/terraform/src/configs/configschema" 20 "github.com/hugorut/terraform/src/depsfile" 21 "github.com/hugorut/terraform/src/initwd" 22 "github.com/hugorut/terraform/src/plans" 23 "github.com/hugorut/terraform/src/providers" 24 "github.com/hugorut/terraform/src/states" 25 "github.com/hugorut/terraform/src/states/statemgr" 26 "github.com/hugorut/terraform/src/terminal" 27 "github.com/hugorut/terraform/src/terraform" 28 "github.com/hugorut/terraform/src/tfdiags" 29 ) 30 31 func TestLocal_applyBasic(t *testing.T) { 32 b := TestLocal(t) 33 34 p := TestLocalProvider(t, b, "test", applyFixtureSchema()) 35 p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{ 36 "id": cty.StringVal("yes"), 37 "ami": cty.StringVal("bar"), 38 })} 39 40 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 41 defer configCleanup() 42 43 run, err := b.Operation(context.Background(), op) 44 if err != nil { 45 t.Fatalf("bad: %s", err) 46 } 47 <-run.Done() 48 if run.Result != backend.OperationSuccess { 49 t.Fatal("operation failed") 50 } 51 52 if p.ReadResourceCalled { 53 t.Fatal("ReadResource should not be called") 54 } 55 56 if !p.PlanResourceChangeCalled { 57 t.Fatal("diff should be called") 58 } 59 60 if !p.ApplyResourceChangeCalled { 61 t.Fatal("apply should be called") 62 } 63 64 checkState(t, b.StateOutPath, ` 65 test_instance.foo: 66 ID = yes 67 provider = provider["registry.terraform.io/hashicorp/test"] 68 ami = bar 69 `) 70 71 if errOutput := done(t).Stderr(); errOutput != "" { 72 t.Fatalf("unexpected error output:\n%s", errOutput) 73 } 74 } 75 76 func TestLocal_applyEmptyDir(t *testing.T) { 77 b := TestLocal(t) 78 79 p := TestLocalProvider(t, b, "test", &terraform.ProviderSchema{}) 80 p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{"id": cty.StringVal("yes")})} 81 82 op, configCleanup, done := testOperationApply(t, "./testdata/empty") 83 defer configCleanup() 84 85 run, err := b.Operation(context.Background(), op) 86 if err != nil { 87 t.Fatalf("bad: %s", err) 88 } 89 <-run.Done() 90 if run.Result == backend.OperationSuccess { 91 t.Fatal("operation succeeded; want error") 92 } 93 94 if p.ApplyResourceChangeCalled { 95 t.Fatal("apply should not be called") 96 } 97 98 if _, err := os.Stat(b.StateOutPath); err == nil { 99 t.Fatal("should not exist") 100 } 101 102 // the backend should be unlocked after a run 103 assertBackendStateUnlocked(t, b) 104 105 if got, want := done(t).Stderr(), "Error: No configuration files"; !strings.Contains(got, want) { 106 t.Fatalf("unexpected error output:\n%s\nwant: %s", got, want) 107 } 108 } 109 110 func TestLocal_applyEmptyDirDestroy(t *testing.T) { 111 b := TestLocal(t) 112 113 p := TestLocalProvider(t, b, "test", &terraform.ProviderSchema{}) 114 p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{} 115 116 op, configCleanup, done := testOperationApply(t, "./testdata/empty") 117 defer configCleanup() 118 op.PlanMode = plans.DestroyMode 119 120 run, err := b.Operation(context.Background(), op) 121 if err != nil { 122 t.Fatalf("bad: %s", err) 123 } 124 <-run.Done() 125 if run.Result != backend.OperationSuccess { 126 t.Fatalf("apply operation failed") 127 } 128 129 if p.ApplyResourceChangeCalled { 130 t.Fatal("apply should not be called") 131 } 132 133 checkState(t, b.StateOutPath, `<no state>`) 134 135 if errOutput := done(t).Stderr(); errOutput != "" { 136 t.Fatalf("unexpected error output:\n%s", errOutput) 137 } 138 } 139 140 func TestLocal_applyError(t *testing.T) { 141 b := TestLocal(t) 142 143 schema := &terraform.ProviderSchema{ 144 ResourceTypes: map[string]*configschema.Block{ 145 "test_instance": { 146 Attributes: map[string]*configschema.Attribute{ 147 "ami": {Type: cty.String, Optional: true}, 148 "id": {Type: cty.String, Computed: true}, 149 }, 150 }, 151 }, 152 } 153 p := TestLocalProvider(t, b, "test", schema) 154 155 var lock sync.Mutex 156 errored := false 157 p.ApplyResourceChangeFn = func( 158 r providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { 159 160 lock.Lock() 161 defer lock.Unlock() 162 var diags tfdiags.Diagnostics 163 164 ami := r.Config.GetAttr("ami").AsString() 165 if !errored && ami == "error" { 166 errored = true 167 diags = diags.Append(errors.New("ami error")) 168 return providers.ApplyResourceChangeResponse{ 169 Diagnostics: diags, 170 } 171 } 172 return providers.ApplyResourceChangeResponse{ 173 Diagnostics: diags, 174 NewState: cty.ObjectVal(map[string]cty.Value{ 175 "id": cty.StringVal("foo"), 176 "ami": cty.StringVal("bar"), 177 }), 178 } 179 } 180 181 op, configCleanup, done := testOperationApply(t, "./testdata/apply-error") 182 defer configCleanup() 183 184 run, err := b.Operation(context.Background(), op) 185 if err != nil { 186 t.Fatalf("bad: %s", err) 187 } 188 <-run.Done() 189 if run.Result == backend.OperationSuccess { 190 t.Fatal("operation succeeded; want failure") 191 } 192 193 checkState(t, b.StateOutPath, ` 194 test_instance.foo: 195 ID = foo 196 provider = provider["registry.terraform.io/hashicorp/test"] 197 ami = bar 198 `) 199 200 // the backend should be unlocked after a run 201 assertBackendStateUnlocked(t, b) 202 203 if got, want := done(t).Stderr(), "Error: ami error"; !strings.Contains(got, want) { 204 t.Fatalf("unexpected error output:\n%s\nwant: %s", got, want) 205 } 206 } 207 208 func TestLocal_applyBackendFail(t *testing.T) { 209 b := TestLocal(t) 210 211 p := TestLocalProvider(t, b, "test", applyFixtureSchema()) 212 213 p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{ 214 NewState: cty.ObjectVal(map[string]cty.Value{ 215 "id": cty.StringVal("yes"), 216 "ami": cty.StringVal("bar"), 217 }), 218 Diagnostics: tfdiags.Diagnostics.Append(nil, errors.New("error before backend failure")), 219 } 220 221 wd, err := os.Getwd() 222 if err != nil { 223 t.Fatalf("failed to get current working directory") 224 } 225 err = os.Chdir(filepath.Dir(b.StatePath)) 226 if err != nil { 227 t.Fatalf("failed to set temporary working directory") 228 } 229 defer os.Chdir(wd) 230 231 op, configCleanup, done := testOperationApply(t, wd+"/testdata/apply") 232 defer configCleanup() 233 234 b.Backend = &backendWithFailingState{} 235 236 run, err := b.Operation(context.Background(), op) 237 if err != nil { 238 t.Fatalf("bad: %s", err) 239 } 240 <-run.Done() 241 242 output := done(t) 243 244 if run.Result == backend.OperationSuccess { 245 t.Fatalf("apply succeeded; want error") 246 } 247 248 diagErr := output.Stderr() 249 250 if !strings.Contains(diagErr, "Error saving state: fake failure") { 251 t.Fatalf("missing \"fake failure\" message in diags:\n%s", diagErr) 252 } 253 254 if !strings.Contains(diagErr, "error before backend failure") { 255 t.Fatalf("missing 'error before backend failure' diagnostic from apply") 256 } 257 258 // The fallback behavior should've created a file errored.tfstate in the 259 // current working directory. 260 checkState(t, "errored.tfstate", ` 261 test_instance.foo: (tainted) 262 ID = yes 263 provider = provider["registry.terraform.io/hashicorp/test"] 264 ami = bar 265 `) 266 267 // the backend should be unlocked after a run 268 assertBackendStateUnlocked(t, b) 269 } 270 271 func TestLocal_applyRefreshFalse(t *testing.T) { 272 b := TestLocal(t) 273 274 p := TestLocalProvider(t, b, "test", planFixtureSchema()) 275 testStateFile(t, b.StatePath, testPlanState()) 276 277 op, configCleanup, done := testOperationApply(t, "./testdata/plan") 278 defer configCleanup() 279 280 run, err := b.Operation(context.Background(), op) 281 if err != nil { 282 t.Fatalf("bad: %s", err) 283 } 284 <-run.Done() 285 if run.Result != backend.OperationSuccess { 286 t.Fatalf("plan operation failed") 287 } 288 289 if p.ReadResourceCalled { 290 t.Fatal("ReadResource should not be called") 291 } 292 293 if errOutput := done(t).Stderr(); errOutput != "" { 294 t.Fatalf("unexpected error output:\n%s", errOutput) 295 } 296 } 297 298 type backendWithFailingState struct { 299 Local 300 } 301 302 func (b *backendWithFailingState) StateMgr(name string) (statemgr.Full, error) { 303 return &failingState{ 304 statemgr.NewFilesystem("failing-state.tfstate"), 305 }, nil 306 } 307 308 type failingState struct { 309 *statemgr.Filesystem 310 } 311 312 func (s failingState) WriteState(state *states.State) error { 313 return errors.New("fake failure") 314 } 315 316 func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { 317 t.Helper() 318 319 _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) 320 321 streams, done := terminal.StreamsForTesting(t) 322 view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) 323 324 // Many of our tests use an overridden "test" provider that's just in-memory 325 // inside the test process, not a separate plugin on disk. 326 depLocks := depsfile.NewLocks() 327 depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test")) 328 329 return &backend.Operation{ 330 Type: backend.OperationTypeApply, 331 ConfigDir: configDir, 332 ConfigLoader: configLoader, 333 StateLocker: clistate.NewNoopLocker(), 334 View: view, 335 DependencyLocks: depLocks, 336 }, configCleanup, done 337 } 338 339 // applyFixtureSchema returns a schema suitable for processing the 340 // configuration in testdata/apply . This schema should be 341 // assigned to a mock provider named "test". 342 func applyFixtureSchema() *terraform.ProviderSchema { 343 return &terraform.ProviderSchema{ 344 ResourceTypes: map[string]*configschema.Block{ 345 "test_instance": { 346 Attributes: map[string]*configschema.Attribute{ 347 "ami": {Type: cty.String, Optional: true}, 348 "id": {Type: cty.String, Computed: true}, 349 }, 350 }, 351 }, 352 } 353 }