308 lines
8.5 KiB
Go
308 lines
8.5 KiB
Go
package configupgrade
|
|
|
|
import (
|
|
"bytes"
|
|
"flag"
|
|
"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/provisioners"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
)
|
|
|
|
func TestUpgradeValid(t *testing.T) {
|
|
// This test uses the contents of the testdata/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 := "testdata/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),
|
|
Provisioners: testProvisioners,
|
|
}
|
|
|
|
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, inputDir)
|
|
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("testdata/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, inputDir)
|
|
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},
|
|
"list_of_obj": {Type: cty.List(cty.EmptyObject), 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
|
|
}),
|
|
"aws": providers.Factory(func() (providers.Interface, error) {
|
|
// This is here only so we can test the provisioner connection info
|
|
// migration behavior, which is resource-type specific. Do not use
|
|
// it in any other tests.
|
|
p := &terraform.MockProvider{}
|
|
p.GetSchemaReturn = &terraform.ProviderSchema{
|
|
ResourceTypes: map[string]*configschema.Block{
|
|
"aws_instance": {},
|
|
},
|
|
}
|
|
return p, nil
|
|
}),
|
|
}
|
|
|
|
var testProvisioners = map[string]provisioners.Factory{
|
|
"test": provisioners.Factory(func() (provisioners.Interface, error) {
|
|
p := &terraform.MockProvisioner{}
|
|
p.GetSchemaResponse = provisioners.GetSchemaResponse{
|
|
Provisioner: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"commands": {Type: cty.List(cty.String), Optional: true},
|
|
"interpreter": {Type: cty.String, Optional: true},
|
|
},
|
|
},
|
|
}
|
|
return p, nil
|
|
}),
|
|
}
|
|
|
|
func init() {
|
|
// Initialize the backends
|
|
backendinit.Init(nil)
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
flag.Parse()
|
|
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())
|
|
}
|