mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-09 07:33:58 -06:00
275a44f552
We used to treat the "id" attribute of a resource as special and elevate it into its own struct field "ID" in the state, but the new state format and provider protocol treats it just as any other attribute. However, it's still useful to show the value of a single identifying attribute when there isn't room in the UI for showing all of the attributes, and so here we take a new strategy of considering "id" along with some other conventional names as special only in the UI layer. This new heuristic approach can be adjusted over time as new provider patterns emerge, but for now it covers some common conventions we've seen in real providers. With that said, since all existing providers made for Terraform versions prior to v0.12 were forced to set "id", we won't see any use of other attributes here until providers are updated to remove the placeholder ids they were generating in cases where an id was not actually relevant but was forced by the old protocol. At that point the UX should be improved by showing a more relevant attribute instead. We now also allow for the possibility of no id at all, since that is valid for resources that exist only within the Terraform state, like the ones from the "random" and "tls" providers.
267 lines
6.7 KiB
Go
267 lines
6.7 KiB
Go
package command
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mitchellh/cli"
|
|
"github.com/mitchellh/colorstring"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/addrs"
|
|
"github.com/hashicorp/terraform/plans"
|
|
"github.com/hashicorp/terraform/states"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
)
|
|
|
|
func TestUiHookPreApply_periodicTimer(t *testing.T) {
|
|
ui := cli.NewMockUi()
|
|
h := &UiHook{
|
|
Colorize: &colorstring.Colorize{
|
|
Colors: colorstring.DefaultColors,
|
|
Disable: true,
|
|
Reset: true,
|
|
},
|
|
Ui: ui,
|
|
PeriodicUiTimer: 1 * time.Second,
|
|
}
|
|
h.init()
|
|
h.resources = map[string]uiResourceState{
|
|
"data.aws_availability_zones.available": uiResourceState{
|
|
Op: uiResourceDestroy,
|
|
Start: time.Now(),
|
|
},
|
|
}
|
|
|
|
addr := addrs.Resource{
|
|
Mode: addrs.DataResourceMode,
|
|
Type: "aws_availability_zones",
|
|
Name: "available",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
|
|
|
priorState := cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("2017-03-05 10:56:59.298784526 +0000 UTC"),
|
|
"names": cty.ListVal([]cty.Value{
|
|
cty.StringVal("us-east-1a"),
|
|
cty.StringVal("us-east-1b"),
|
|
cty.StringVal("us-east-1c"),
|
|
cty.StringVal("us-east-1d"),
|
|
}),
|
|
})
|
|
plannedNewState := cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"id": cty.String,
|
|
"names": cty.List(cty.String),
|
|
}))
|
|
|
|
action, err := h.PreApply(addr, states.CurrentGen, plans.Delete, priorState, plannedNewState)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if action != terraform.HookActionContinue {
|
|
t.Fatalf("Expected hook to continue, given: %#v", action)
|
|
}
|
|
|
|
time.Sleep(3100 * time.Millisecond)
|
|
|
|
// stop the background writer
|
|
uiState := h.resources[addr.String()]
|
|
close(uiState.DoneCh)
|
|
<-uiState.done
|
|
|
|
expectedOutput := `data.aws_availability_zones.available: Destroying... [id=2017-03-05 10:56:59.298784526 +0000 UTC]
|
|
data.aws_availability_zones.available: Still destroying... [id=2017-03-05 10:56:59.298784526 +0000 UTC, 1s elapsed]
|
|
data.aws_availability_zones.available: Still destroying... [id=2017-03-05 10:56:59.298784526 +0000 UTC, 2s elapsed]
|
|
data.aws_availability_zones.available: Still destroying... [id=2017-03-05 10:56:59.298784526 +0000 UTC, 3s elapsed]
|
|
`
|
|
output := ui.OutputWriter.String()
|
|
if output != expectedOutput {
|
|
t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output)
|
|
}
|
|
|
|
expectedErrOutput := ""
|
|
errOutput := ui.ErrorWriter.String()
|
|
if errOutput != expectedErrOutput {
|
|
t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
|
|
}
|
|
}
|
|
|
|
func TestUiHookPreApply_destroy(t *testing.T) {
|
|
ui := cli.NewMockUi()
|
|
h := &UiHook{
|
|
Colorize: &colorstring.Colorize{
|
|
Colors: colorstring.DefaultColors,
|
|
Disable: true,
|
|
Reset: true,
|
|
},
|
|
Ui: ui,
|
|
}
|
|
h.init()
|
|
h.resources = map[string]uiResourceState{
|
|
"data.aws_availability_zones.available": uiResourceState{
|
|
Op: uiResourceDestroy,
|
|
Start: time.Now(),
|
|
},
|
|
}
|
|
|
|
addr := addrs.Resource{
|
|
Mode: addrs.DataResourceMode,
|
|
Type: "aws_availability_zones",
|
|
Name: "available",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
|
|
|
priorState := cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("2017-03-05 10:56:59.298784526 +0000 UTC"),
|
|
"names": cty.ListVal([]cty.Value{
|
|
cty.StringVal("us-east-1a"),
|
|
cty.StringVal("us-east-1b"),
|
|
cty.StringVal("us-east-1c"),
|
|
cty.StringVal("us-east-1d"),
|
|
}),
|
|
})
|
|
plannedNewState := cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"id": cty.String,
|
|
"names": cty.List(cty.String),
|
|
}))
|
|
|
|
action, err := h.PreApply(addr, states.CurrentGen, plans.Delete, priorState, plannedNewState)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if action != terraform.HookActionContinue {
|
|
t.Fatalf("Expected hook to continue, given: %#v", action)
|
|
}
|
|
|
|
// stop the background writer
|
|
uiState := h.resources[addr.String()]
|
|
close(uiState.DoneCh)
|
|
<-uiState.done
|
|
|
|
expectedOutput := "data.aws_availability_zones.available: Destroying... [id=2017-03-05 10:56:59.298784526 +0000 UTC]\n"
|
|
output := ui.OutputWriter.String()
|
|
if output != expectedOutput {
|
|
t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output)
|
|
}
|
|
|
|
expectedErrOutput := ""
|
|
errOutput := ui.ErrorWriter.String()
|
|
if errOutput != expectedErrOutput {
|
|
t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
|
|
}
|
|
}
|
|
|
|
func TestUiHookPostApply_emptyState(t *testing.T) {
|
|
ui := cli.NewMockUi()
|
|
h := &UiHook{
|
|
Colorize: &colorstring.Colorize{
|
|
Colors: colorstring.DefaultColors,
|
|
Disable: true,
|
|
Reset: true,
|
|
},
|
|
Ui: ui,
|
|
}
|
|
h.init()
|
|
h.resources = map[string]uiResourceState{
|
|
"data.google_compute_zones.available": uiResourceState{
|
|
Op: uiResourceDestroy,
|
|
Start: time.Now(),
|
|
},
|
|
}
|
|
|
|
addr := addrs.Resource{
|
|
Mode: addrs.DataResourceMode,
|
|
Type: "google_compute_zones",
|
|
Name: "available",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
|
|
|
newState := cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"id": cty.String,
|
|
"names": cty.List(cty.String),
|
|
}))
|
|
|
|
action, err := h.PostApply(addr, states.CurrentGen, newState, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if action != terraform.HookActionContinue {
|
|
t.Fatalf("Expected hook to continue, given: %#v", action)
|
|
}
|
|
|
|
expectedRegexp := "^data.google_compute_zones.available: Destruction complete after -?[a-z0-9.]+\n$"
|
|
output := ui.OutputWriter.String()
|
|
if matched, _ := regexp.MatchString(expectedRegexp, output); !matched {
|
|
t.Fatalf("Output didn't match regexp.\nExpected: %q\nGiven: %q", expectedRegexp, output)
|
|
}
|
|
|
|
expectedErrOutput := ""
|
|
errOutput := ui.ErrorWriter.String()
|
|
if errOutput != expectedErrOutput {
|
|
t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
|
|
}
|
|
}
|
|
|
|
func TestTruncateId(t *testing.T) {
|
|
testCases := []struct {
|
|
Input string
|
|
Expected string
|
|
MaxLen int
|
|
}{
|
|
{
|
|
Input: "Hello world",
|
|
Expected: "H...d",
|
|
MaxLen: 3,
|
|
},
|
|
{
|
|
Input: "Hello world",
|
|
Expected: "H...d",
|
|
MaxLen: 5,
|
|
},
|
|
{
|
|
Input: "Hello world",
|
|
Expected: "He...d",
|
|
MaxLen: 6,
|
|
},
|
|
{
|
|
Input: "Hello world",
|
|
Expected: "He...ld",
|
|
MaxLen: 7,
|
|
},
|
|
{
|
|
Input: "Hello world",
|
|
Expected: "Hel...ld",
|
|
MaxLen: 8,
|
|
},
|
|
{
|
|
Input: "Hello world",
|
|
Expected: "Hel...rld",
|
|
MaxLen: 9,
|
|
},
|
|
{
|
|
Input: "Hello world",
|
|
Expected: "Hell...rld",
|
|
MaxLen: 10,
|
|
},
|
|
{
|
|
Input: "Hello world",
|
|
Expected: "Hello world",
|
|
MaxLen: 11,
|
|
},
|
|
{
|
|
Input: "Hello world",
|
|
Expected: "Hello world",
|
|
MaxLen: 12,
|
|
},
|
|
}
|
|
for i, tc := range testCases {
|
|
testName := fmt.Sprintf("%d", i)
|
|
t.Run(testName, func(t *testing.T) {
|
|
out := truncateId(tc.Input, tc.MaxLen)
|
|
if out != tc.Expected {
|
|
t.Fatalf("Expected %q to be shortened to %d as %q (given: %q)",
|
|
tc.Input, tc.MaxLen, tc.Expected, out)
|
|
}
|
|
})
|
|
}
|
|
}
|