terraform/configs/configupgrade/upgrade_test.go

275 lines
7.4 KiB
Go
Raw Normal View History

package configupgrade
import (
"bytes"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/zclconf/go-cty/cty"
backendinit "github.com/hashicorp/terraform/backend/init"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/helper/logging"
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/terraform"
)
func TestUpgradeValid(t *testing.T) {
// This test uses the contents of the test-fixtures/valid directory as
// a table of tests. Every directory there must have both "input" and
// "want" subdirectories, where "input" is the configuration to be
// upgraded and "want" is the expected result.
fixtureDir := "test-fixtures/valid"
testDirs, err := ioutil.ReadDir(fixtureDir)
if err != nil {
t.Fatal(err)
}
for _, entry := range testDirs {
if !entry.IsDir() {
continue
}
t.Run(entry.Name(), func(t *testing.T) {
inputDir := filepath.Join(fixtureDir, entry.Name(), "input")
wantDir := filepath.Join(fixtureDir, entry.Name(), "want")
u := &Upgrader{
Providers: providers.ResolverFixed(testProviders),
}
inputSrc, err := LoadModule(inputDir)
if err != nil {
t.Fatal(err)
}
wantSrc, err := LoadModule(wantDir)
if err != nil {
t.Fatal(err)
}
gotSrc, diags := u.Upgrade(inputSrc)
if diags.HasErrors() {
t.Error(diags.Err())
}
// Upgrade uses a nil entry as a signal to delete a file, which
// we can't test here because we aren't modifying an existing
// dir in place, so we'll just ignore those and leave that mechanism
// to be tested elsewhere.
for name, got := range gotSrc {
if gotSrc[name] == nil {
delete(gotSrc, name)
continue
}
want, wanted := wantSrc[name]
if !wanted {
t.Errorf("unexpected extra output file %q\n=== GOT ===\n%s", name, got)
continue
}
got = bytes.TrimSpace(got)
want = bytes.TrimSpace(want)
if !bytes.Equal(got, want) {
diff := diffSourceFiles(got, want)
t.Errorf("wrong content in %q\n%s", name, diff)
}
}
for name, want := range wantSrc {
if _, present := gotSrc[name]; !present {
t.Errorf("missing output file %q\n=== WANT ===\n%s", name, want)
}
}
})
}
}
func TestUpgradeRenameJSON(t *testing.T) {
inputDir := filepath.Join("test-fixtures/valid/rename-json/input")
inputSrc, err := LoadModule(inputDir)
if err != nil {
t.Fatal(err)
}
u := &Upgrader{
Providers: providers.ResolverFixed(testProviders),
}
gotSrc, diags := u.Upgrade(inputSrc)
if diags.HasErrors() {
t.Error(diags.Err())
}
// This test fixture is also fully covered by TestUpgradeValid, so
// we're just testing that the file was renamed here.
src, exists := gotSrc["misnamed-json.tf"]
if src != nil {
t.Errorf("misnamed-json.tf still has content")
} else if !exists {
t.Errorf("misnamed-json.tf not marked for deletion")
}
src, exists = gotSrc["misnamed-json.tf.json"]
if src == nil || !exists {
t.Errorf("misnamed-json.tf.json was not created")
}
}
func diffSourceFiles(got, want []byte) []byte {
// We'll try to run "diff -u" here to get nice output, but if that fails
// (e.g. because we're running on a machine without diff installed) then
// we'll fall back on just printing out the before and after in full.
gotR, gotW, err := os.Pipe()
if err != nil {
return diffSourceFilesFallback(got, want)
}
defer gotR.Close()
defer gotW.Close()
wantR, wantW, err := os.Pipe()
if err != nil {
return diffSourceFilesFallback(got, want)
}
defer wantR.Close()
defer wantW.Close()
cmd := exec.Command("diff", "-u", "--label=GOT", "--label=WANT", "/dev/fd/3", "/dev/fd/4")
cmd.ExtraFiles = []*os.File{gotR, wantR}
stdout, err := cmd.StdoutPipe()
stderr, err := cmd.StderrPipe()
if err != nil {
return diffSourceFilesFallback(got, want)
}
go func() {
wantW.Write(want)
wantW.Close()
}()
go func() {
gotW.Write(got)
gotW.Close()
}()
err = cmd.Start()
if err != nil {
return diffSourceFilesFallback(got, want)
}
outR := io.MultiReader(stdout, stderr)
out, err := ioutil.ReadAll(outR)
if err != nil {
return diffSourceFilesFallback(got, want)
}
cmd.Wait() // not checking errors here because on failure we'll have stderr captured to return
const noNewline = "\\ No newline at end of file\n"
if bytes.HasSuffix(out, []byte(noNewline)) {
out = out[:len(out)-len(noNewline)]
}
return out
}
func diffSourceFilesFallback(got, want []byte) []byte {
var buf bytes.Buffer
buf.WriteString("=== GOT ===\n")
buf.Write(got)
buf.WriteString("\n=== WANT ===\n")
buf.Write(want)
buf.WriteString("\n")
return buf.Bytes()
}
var testProviders = map[string]providers.Factory{
"test": providers.Factory(func() (providers.Interface, error) {
p := &terraform.MockProvider{}
p.GetSchemaReturn = &terraform.ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_instance": {
Attributes: map[string]*configschema.Attribute{
configs/configupgrade: Remove redundant list brackets In early versions of Terraform where the interpolation language didn't have any real list support, list brackets around a single string was the signal to split the string on a special uuid separator to produce a list just in time for processing, giving expressions like this: foo = ["${test_instance.foo.*.id}"] Logically this is weird because it looks like it should produce a list of lists of strings. When we added real list support in Terraform 0.7 we retained support for this behavior by trimming off extra levels of list during evaluation, and inadvertently continued relying on this notation for correct type checking. During the Terraform 0.10 line we fixed the type checker bugs (a few remaining issues notwithstanding) so that it was finally possible to use the more intuitive form: foo = "${test_instance.foo.*.id}" ...but we continued trimming off extra levels of list for backward compatibility. Terraform 0.12 finally removes that compatibility shim, causing redundant list brackets to be interpreted as a list of lists. This upgrade rule attempts to identify situations that are relying on the old compatibility behavior and trim off the redundant extra brackets. It's not possible to do this fully-generally using only static analysis, but we can gather enough information through or partial type inference mechanism here to deal with the most common situations automatically and produce a TF-UPGRADE-TODO comment for more complex scenarios where the user intent isn't decidable with only static analysis. In particular, this handles by far the most common situation of wrapping list brackets around a splat expression like the first example above. After this and the other upgrade rules are applied, the first example above will become: foo = test_instance.foo.*.id
2018-12-06 20:56:43 +01:00
"id": {Type: cty.String, Computed: true},
"type": {Type: cty.String, Optional: true},
"image": {Type: cty.String, Optional: true},
"tags": {Type: cty.Map(cty.String), Optional: true},
"security_groups": {Type: cty.List(cty.String), Optional: true},
},
BlockTypes: map[string]*configschema.NestedBlock{
"network": {
Nesting: configschema.NestingSet,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"cidr_block": {Type: cty.String, Optional: true},
"subnet_cidrs": {Type: cty.Map(cty.String), Computed: true},
},
BlockTypes: map[string]*configschema.NestedBlock{
"subnet": {
Nesting: configschema.NestingSet,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"number": {Type: cty.Number, Required: true},
},
},
},
},
},
},
"addresses": {
Nesting: configschema.NestingSingle,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"ipv4": {Type: cty.String, Computed: true},
"ipv6": {Type: cty.String, Computed: true},
},
},
},
},
},
},
}
return p, nil
}),
"terraform": providers.Factory(func() (providers.Interface, error) {
p := &terraform.MockProvider{}
p.GetSchemaReturn = &terraform.ProviderSchema{
DataSources: map[string]*configschema.Block{
"terraform_remote_state": {
// This is just enough an approximation of the remote state
// schema to check out reference upgrade logic. It is
// intentionally not fully-comprehensive.
Attributes: map[string]*configschema.Attribute{
"backend": {Type: cty.String, Optional: true},
},
},
},
}
return p, nil
}),
}
func init() {
// Initialize the backends
backendinit.Init(nil)
}
func TestMain(m *testing.M) {
if testing.Verbose() {
// if we're verbose, use the logging requested by TF_LOG
logging.SetOutput()
} else {
// otherwise silence all logs
log.SetOutput(ioutil.Discard)
}
// We have fmt.Stringer implementations on lots of objects that hide
// details that we very often want to see in tests, so we just disable
// spew's use of String methods globally on the assumption that spew
// usage implies an intent to see the raw values and ignore any
// abstractions.
spew.Config.DisableMethods = true
os.Exit(m.Run())
}