terraform/configs/configupgrade/upgrade_test.go

276 lines
7.5 KiB
Go

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{
"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},
"subnet_ids": {Type: cty.Set(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())
}