github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/command/show_test.go (about) 1 package command 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "strings" 10 "testing" 11 12 "github.com/google/go-cmp/cmp" 13 "github.com/hashicorp/terraform/addrs" 14 "github.com/hashicorp/terraform/configs/configschema" 15 "github.com/hashicorp/terraform/helper/copy" 16 "github.com/hashicorp/terraform/plans" 17 "github.com/hashicorp/terraform/providers" 18 "github.com/hashicorp/terraform/states" 19 "github.com/hashicorp/terraform/terraform" 20 "github.com/mitchellh/cli" 21 "github.com/zclconf/go-cty/cty" 22 ) 23 24 func TestShow(t *testing.T) { 25 ui := new(cli.MockUi) 26 c := &ShowCommand{ 27 Meta: Meta{ 28 testingOverrides: metaOverridesForProvider(testProvider()), 29 Ui: ui, 30 }, 31 } 32 33 args := []string{ 34 "bad", 35 "bad", 36 } 37 if code := c.Run(args); code != 1 { 38 t.Fatalf("bad: \n%s", ui.OutputWriter.String()) 39 } 40 } 41 42 func TestShow_noArgs(t *testing.T) { 43 // Create the default state 44 statePath := testStateFile(t, testState()) 45 stateDir := filepath.Dir(statePath) 46 defer os.RemoveAll(stateDir) 47 defer testChdir(t, stateDir)() 48 49 ui := new(cli.MockUi) 50 c := &ShowCommand{ 51 Meta: Meta{ 52 testingOverrides: metaOverridesForProvider(testProvider()), 53 Ui: ui, 54 }, 55 } 56 57 // the statefile created by testStateFile is named state.tfstate 58 // so one arg is required 59 args := []string{"state.tfstate"} 60 if code := c.Run(args); code != 0 { 61 t.Fatalf("bad: \n%s", ui.OutputWriter.String()) 62 } 63 64 if !strings.Contains(ui.OutputWriter.String(), "# test_instance.foo:") { 65 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 66 } 67 } 68 69 // https://github.com/hashicorp/terraform/issues/21462 70 func TestShow_aliasedProvider(t *testing.T) { 71 // Create the default state with aliased resource 72 testState := states.BuildState(func(s *states.SyncState) { 73 s.SetResourceInstanceCurrent( 74 addrs.Resource{ 75 Mode: addrs.ManagedResourceMode, 76 Type: "test_instance", 77 Name: "foo", 78 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 79 &states.ResourceInstanceObjectSrc{ 80 // The weird whitespace here is reflective of how this would 81 // get written out in a real state file, due to the indentation 82 // of all of the containing wrapping objects and arrays. 83 AttrsJSON: []byte("{\n \"id\": \"bar\"\n }"), 84 Status: states.ObjectReady, 85 Dependencies: []addrs.AbsResource{}, 86 DependsOn: []addrs.Referenceable{}, 87 }, 88 addrs.RootModuleInstance.ProviderConfigAliased("test", "alias"), 89 ) 90 }) 91 92 statePath := testStateFile(t, testState) 93 stateDir := filepath.Dir(statePath) 94 defer os.RemoveAll(stateDir) 95 defer testChdir(t, stateDir)() 96 97 ui := new(cli.MockUi) 98 c := &ShowCommand{ 99 Meta: Meta{ 100 testingOverrides: metaOverridesForProvider(testProvider()), 101 Ui: ui, 102 }, 103 } 104 105 fmt.Println(os.Getwd()) 106 107 // the statefile created by testStateFile is named state.tfstate 108 args := []string{"state.tfstate"} 109 if code := c.Run(args); code != 0 { 110 t.Fatalf("bad exit code: \n%s", ui.OutputWriter.String()) 111 } 112 113 if strings.Contains(ui.OutputWriter.String(), "# missing schema for provider \"test.alias\"") { 114 t.Fatalf("bad output: \n%s", ui.OutputWriter.String()) 115 } 116 } 117 118 func TestShow_noArgsNoState(t *testing.T) { 119 // Create the default state 120 statePath := testStateFile(t, testState()) 121 stateDir := filepath.Dir(statePath) 122 defer os.RemoveAll(stateDir) 123 defer testChdir(t, stateDir)() 124 125 ui := new(cli.MockUi) 126 c := &ShowCommand{ 127 Meta: Meta{ 128 testingOverrides: metaOverridesForProvider(testProvider()), 129 Ui: ui, 130 }, 131 } 132 133 // the statefile created by testStateFile is named state.tfstate 134 args := []string{"state.tfstate"} 135 if code := c.Run(args); code != 0 { 136 t.Fatalf("bad: \n%s", ui.OutputWriter.String()) 137 } 138 } 139 140 func TestShow_plan(t *testing.T) { 141 planPath := testPlanFileNoop(t) 142 143 ui := cli.NewMockUi() 144 c := &ShowCommand{ 145 Meta: Meta{ 146 testingOverrides: metaOverridesForProvider(testProvider()), 147 Ui: ui, 148 }, 149 } 150 151 args := []string{ 152 planPath, 153 } 154 if code := c.Run(args); code != 0 { 155 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 156 } 157 158 want := `Terraform will perform the following actions` 159 got := ui.OutputWriter.String() 160 if !strings.Contains(got, want) { 161 t.Errorf("missing expected output\nwant: %s\ngot:\n%s", want, got) 162 } 163 } 164 165 func TestShow_planWithChanges(t *testing.T) { 166 planPathWithChanges := showFixturePlanFile(t, plans.DeleteThenCreate) 167 168 ui := cli.NewMockUi() 169 c := &ShowCommand{ 170 Meta: Meta{ 171 testingOverrides: metaOverridesForProvider(showFixtureProvider()), 172 Ui: ui, 173 }, 174 } 175 176 args := []string{ 177 planPathWithChanges, 178 } 179 180 if code := c.Run(args); code != 0 { 181 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 182 } 183 184 want := `test_instance.foo must be replaced` 185 got := ui.OutputWriter.String() 186 if !strings.Contains(got, want) { 187 t.Errorf("missing expected output\nwant: %s\ngot:\n%s", want, got) 188 } 189 } 190 191 func TestShow_plan_json(t *testing.T) { 192 planPath := showFixturePlanFile(t, plans.Create) 193 194 ui := new(cli.MockUi) 195 c := &ShowCommand{ 196 Meta: Meta{ 197 testingOverrides: metaOverridesForProvider(showFixtureProvider()), 198 Ui: ui, 199 }, 200 } 201 202 args := []string{ 203 "-json", 204 planPath, 205 } 206 if code := c.Run(args); code != 0 { 207 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 208 } 209 } 210 211 func TestShow_state(t *testing.T) { 212 originalState := testState() 213 statePath := testStateFile(t, originalState) 214 defer os.RemoveAll(filepath.Dir(statePath)) 215 216 ui := new(cli.MockUi) 217 c := &ShowCommand{ 218 Meta: Meta{ 219 testingOverrides: metaOverridesForProvider(testProvider()), 220 Ui: ui, 221 }, 222 } 223 224 args := []string{ 225 statePath, 226 } 227 if code := c.Run(args); code != 0 { 228 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 229 } 230 } 231 232 func TestShow_json_output(t *testing.T) { 233 fixtureDir := "testdata/show-json" 234 testDirs, err := ioutil.ReadDir(fixtureDir) 235 if err != nil { 236 t.Fatal(err) 237 } 238 239 for _, entry := range testDirs { 240 if !entry.IsDir() { 241 continue 242 } 243 244 t.Run(entry.Name(), func(t *testing.T) { 245 td := tempDir(t) 246 inputDir := filepath.Join(fixtureDir, entry.Name()) 247 copy.CopyDir(inputDir, td) 248 defer os.RemoveAll(td) 249 defer testChdir(t, td)() 250 251 expectError := strings.Contains(entry.Name(), "error") 252 253 p := showFixtureProvider() 254 ui := new(cli.MockUi) 255 m := Meta{ 256 testingOverrides: metaOverridesForProvider(p), 257 Ui: ui, 258 } 259 260 // init 261 ic := &InitCommand{ 262 Meta: m, 263 providerInstaller: &mockProviderInstaller{ 264 Providers: map[string][]string{ 265 "test": []string{"1.2.3"}, 266 }, 267 Dir: m.pluginDir(), 268 }, 269 } 270 if code := ic.Run([]string{}); code != 0 { 271 if expectError { 272 // this should error, but not panic. 273 return 274 } 275 t.Fatalf("init failed\n%s", ui.ErrorWriter) 276 } 277 278 pc := &PlanCommand{ 279 Meta: m, 280 } 281 282 args := []string{ 283 "-out=terraform.plan", 284 } 285 286 if code := pc.Run(args); code != 0 { 287 t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String()) 288 } 289 290 // flush the plan output from the mock ui 291 ui.OutputWriter.Reset() 292 sc := &ShowCommand{ 293 Meta: m, 294 } 295 296 args = []string{ 297 "-json", 298 "terraform.plan", 299 } 300 defer os.Remove("terraform.plan") 301 302 if code := sc.Run(args); code != 0 { 303 t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String()) 304 } 305 306 // compare ui output to wanted output 307 var got, want plan 308 309 gotString := ui.OutputWriter.String() 310 json.Unmarshal([]byte(gotString), &got) 311 312 wantFile, err := os.Open("output.json") 313 if err != nil { 314 t.Fatalf("err: %s", err) 315 } 316 defer wantFile.Close() 317 byteValue, err := ioutil.ReadAll(wantFile) 318 if err != nil { 319 t.Fatalf("err: %s", err) 320 } 321 json.Unmarshal([]byte(byteValue), &want) 322 323 if !cmp.Equal(got, want) { 324 t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want)) 325 } 326 327 }) 328 329 } 330 } 331 332 // similar test as above, without the plan 333 func TestShow_json_output_state(t *testing.T) { 334 fixtureDir := "testdata/show-json-state" 335 testDirs, err := ioutil.ReadDir(fixtureDir) 336 if err != nil { 337 t.Fatal(err) 338 } 339 340 for _, entry := range testDirs { 341 if !entry.IsDir() { 342 continue 343 } 344 345 t.Run(entry.Name(), func(t *testing.T) { 346 td := tempDir(t) 347 inputDir := filepath.Join(fixtureDir, entry.Name()) 348 copy.CopyDir(inputDir, td) 349 defer os.RemoveAll(td) 350 defer testChdir(t, td)() 351 352 p := showFixtureProvider() 353 ui := new(cli.MockUi) 354 m := Meta{ 355 testingOverrides: metaOverridesForProvider(p), 356 Ui: ui, 357 } 358 359 // init 360 ic := &InitCommand{ 361 Meta: m, 362 providerInstaller: &mockProviderInstaller{ 363 Providers: map[string][]string{ 364 "test": []string{"1.2.3"}, 365 }, 366 Dir: m.pluginDir(), 367 }, 368 } 369 if code := ic.Run([]string{}); code != 0 { 370 t.Fatalf("init failed\n%s", ui.ErrorWriter) 371 } 372 373 // flush the plan output from the mock ui 374 ui.OutputWriter.Reset() 375 sc := &ShowCommand{ 376 Meta: m, 377 } 378 379 if code := sc.Run([]string{"-json"}); code != 0 { 380 t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String()) 381 } 382 383 // compare ui output to wanted output 384 type state struct { 385 FormatVersion string `json:"format_version,omitempty"` 386 TerraformVersion string `json:"terraform_version"` 387 Values map[string]interface{} `json:"values,omitempty"` 388 } 389 var got, want state 390 391 gotString := ui.OutputWriter.String() 392 json.Unmarshal([]byte(gotString), &got) 393 394 wantFile, err := os.Open("output.json") 395 if err != nil { 396 t.Fatalf("err: %s", err) 397 } 398 defer wantFile.Close() 399 byteValue, err := ioutil.ReadAll(wantFile) 400 if err != nil { 401 t.Fatalf("err: %s", err) 402 } 403 json.Unmarshal([]byte(byteValue), &want) 404 405 if !cmp.Equal(got, want) { 406 t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want)) 407 } 408 409 }) 410 411 } 412 } 413 414 // showFixtureSchema returns a schema suitable for processing the configuration 415 // in testdata/show. This schema should be assigned to a mock provider 416 // named "test". 417 func showFixtureSchema() *terraform.ProviderSchema { 418 return &terraform.ProviderSchema{ 419 ResourceTypes: map[string]*configschema.Block{ 420 "test_instance": { 421 Attributes: map[string]*configschema.Attribute{ 422 "id": {Type: cty.String, Optional: true, Computed: true}, 423 "ami": {Type: cty.String, Optional: true}, 424 }, 425 }, 426 }, 427 } 428 } 429 430 // showFixtureProvider returns a mock provider that is configured for basic 431 // operation with the configuration in testdata/show. This mock has 432 // GetSchemaReturn, PlanResourceChangeFn, and ApplyResourceChangeFn populated, 433 // with the plan/apply steps just passing through the data determined by 434 // Terraform Core. 435 func showFixtureProvider() *terraform.MockProvider { 436 p := testProvider() 437 p.GetSchemaReturn = showFixtureSchema() 438 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 439 idVal := req.ProposedNewState.GetAttr("id") 440 amiVal := req.ProposedNewState.GetAttr("ami") 441 if idVal.IsNull() { 442 idVal = cty.UnknownVal(cty.String) 443 } 444 return providers.PlanResourceChangeResponse{ 445 PlannedState: cty.ObjectVal(map[string]cty.Value{ 446 "id": idVal, 447 "ami": amiVal, 448 }), 449 } 450 } 451 p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { 452 idVal := req.PlannedState.GetAttr("id") 453 amiVal := req.PlannedState.GetAttr("ami") 454 if !idVal.IsKnown() { 455 idVal = cty.StringVal("placeholder") 456 } 457 return providers.ApplyResourceChangeResponse{ 458 NewState: cty.ObjectVal(map[string]cty.Value{ 459 "id": idVal, 460 "ami": amiVal, 461 }), 462 } 463 } 464 return p 465 } 466 467 // showFixturePlanFile creates a plan file at a temporary location containing a 468 // single change to create or update the test_instance.foo that is included in the "show" 469 // test fixture, returning the location of that plan file. 470 // `action` is the planned change you would like to elicit 471 func showFixturePlanFile(t *testing.T, action plans.Action) string { 472 _, snap := testModuleWithSnapshot(t, "show") 473 plannedVal := cty.ObjectVal(map[string]cty.Value{ 474 "id": cty.UnknownVal(cty.String), 475 "ami": cty.StringVal("bar"), 476 }) 477 priorValRaw, err := plans.NewDynamicValue(cty.NullVal(plannedVal.Type()), plannedVal.Type()) 478 if err != nil { 479 t.Fatal(err) 480 } 481 plannedValRaw, err := plans.NewDynamicValue(plannedVal, plannedVal.Type()) 482 if err != nil { 483 t.Fatal(err) 484 } 485 plan := testPlan(t) 486 plan.Changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ 487 Addr: addrs.Resource{ 488 Mode: addrs.ManagedResourceMode, 489 Type: "test_instance", 490 Name: "foo", 491 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 492 ProviderAddr: addrs.ProviderConfig{Type: addrs.NewLegacyProvider("test")}.Absolute(addrs.RootModuleInstance), 493 ChangeSrc: plans.ChangeSrc{ 494 Action: action, 495 Before: priorValRaw, 496 After: plannedValRaw, 497 }, 498 }) 499 return testPlanFile( 500 t, 501 snap, 502 states.NewState(), 503 plan, 504 ) 505 } 506 507 // this simplified plan struct allows us to preserve field order when marshaling 508 // the command output. NOTE: we are leaving "terraform_version" out of this test 509 // to avoid needing to constantly update the expected output; as a potential 510 // TODO we could write a jsonplan compare function. 511 type plan struct { 512 FormatVersion string `json:"format_version,omitempty"` 513 Variables map[string]interface{} `json:"variables,omitempty"` 514 PlannedValues map[string]interface{} `json:"planned_values,omitempty"` 515 ResourceChanges []interface{} `json:"resource_changes,omitempty"` 516 OutputChanges map[string]interface{} `json:"output_changes,omitempty"` 517 PriorState priorState `json:"prior_state,omitempty"` 518 Config map[string]interface{} `json:"configuration,omitempty"` 519 } 520 521 type priorState struct { 522 FormatVersion string `json:"format_version,omitempty"` 523 Values map[string]interface{} `json:"values,omitempty"` 524 }