opentofu/command/command_test.go
James Bardin cdb80f68a8 Ensure better state normalization
Fix checksum issue with remote state

If we read a state file with "null" objects in a module and they become
initialized to an empty map the state file may be written out with empty
objects rather than "null", changing the checksum. If we can detect
this, increment the serial number to prevent a conflict in atlas.

Our fakeAtlas test server now needs to decode the state directly rather
than using the ReadState function, so as to be able to read the state
unaltered.

The terraform.State data structures have initialization spread out
throughout the package. More thoroughly initialize State during
ReadState, and add a call to init() during WriteState as another
normalization safeguard.

Expose State.init through an exported Init() method, so that a new State
can be completely realized outside of the terraform package.
Additionally, the internal init now completely walks all internal state
structures ensuring that all maps and slices are initialized.  While it
was mentioned before that the `init()` methods are problematic with too
many call sites, expanding this out better exposes the entry points that
will need to be refactored later for improved concurrency handling.

The State structures had a mix of `omitempty` fields. Remove omitempty
for all maps and slices as part of this normalization process. Make
Lineage mandatory, which is now explicitly set in some tests.
2016-08-12 11:09:50 -04:00

289 lines
6.1 KiB
Go

package command
import (
"flag"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"testing"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/helper/logging"
"github.com/hashicorp/terraform/terraform"
)
// This is the directory where our test fixtures are.
var fixtureDir = "./test-fixtures"
func init() {
test = true
// Expand the fixture dir on init because we change the working
// directory in some tests.
var err error
fixtureDir, err = filepath.Abs(fixtureDir)
if err != nil {
panic(err)
}
}
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)
}
os.Exit(m.Run())
}
func tempDir(t *testing.T) string {
dir, err := ioutil.TempDir("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.RemoveAll(dir); err != nil {
t.Fatalf("err: %s", err)
}
return dir
}
func testFixturePath(name string) string {
return filepath.Join(fixtureDir, name)
}
func testCtxConfig(p terraform.ResourceProvider) *terraform.ContextOpts {
return &terraform.ContextOpts{
Providers: map[string]terraform.ResourceProviderFactory{
"test": func() (terraform.ResourceProvider, error) {
return p, nil
},
},
}
}
func testCtxConfigWithShell(p terraform.ResourceProvider, pr terraform.ResourceProvisioner) *terraform.ContextOpts {
return &terraform.ContextOpts{
Providers: map[string]terraform.ResourceProviderFactory{
"test": func() (terraform.ResourceProvider, error) {
return p, nil
},
},
Provisioners: map[string]terraform.ResourceProvisionerFactory{
"shell": func() (terraform.ResourceProvisioner, error) {
return pr, nil
},
},
}
}
func testModule(t *testing.T, name string) *module.Tree {
mod, err := module.NewTreeModule("", filepath.Join(fixtureDir, name))
if err != nil {
t.Fatalf("err: %s", err)
}
s := &getter.FolderStorage{StorageDir: tempDir(t)}
if err := mod.Load(s, module.GetModeGet); err != nil {
t.Fatalf("err: %s", err)
}
return mod
}
func testPlanFile(t *testing.T, plan *terraform.Plan) string {
path := testTempFile(t)
f, err := os.Create(path)
if err != nil {
t.Fatalf("err: %s", err)
}
defer f.Close()
if err := terraform.WritePlan(plan, f); err != nil {
t.Fatalf("err: %s", err)
}
return path
}
func testReadPlan(t *testing.T, path string) *terraform.Plan {
f, err := os.Open(path)
if err != nil {
t.Fatalf("err: %s", err)
}
defer f.Close()
p, err := terraform.ReadPlan(f)
if err != nil {
t.Fatalf("err: %s", err)
}
return p
}
// testState returns a test State structure that we use for a lot of tests.
func testState() *terraform.State {
state := &terraform.State{
Version: 2,
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "bar",
},
},
},
Outputs: map[string]*terraform.OutputState{},
},
},
}
state.Init()
return state
}
func testStateFile(t *testing.T, s *terraform.State) string {
path := testTempFile(t)
f, err := os.Create(path)
if err != nil {
t.Fatalf("err: %s", err)
}
defer f.Close()
if err := terraform.WriteState(s, f); err != nil {
t.Fatalf("err: %s", err)
}
return path
}
// testStateFileDefault writes the state out to the default statefile
// in the cwd. Use `testCwd` to change into a temp cwd.
func testStateFileDefault(t *testing.T, s *terraform.State) string {
f, err := os.Create(DefaultStateFilename)
if err != nil {
t.Fatalf("err: %s", err)
}
defer f.Close()
if err := terraform.WriteState(s, f); err != nil {
t.Fatalf("err: %s", err)
}
return DefaultStateFilename
}
// testStateFileRemote writes the state out to the remote statefile
// in the cwd. Use `testCwd` to change into a temp cwd.
func testStateFileRemote(t *testing.T, s *terraform.State) string {
path := filepath.Join(DefaultDataDir, DefaultStateFilename)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatalf("err: %s", err)
}
f, err := os.Create(path)
if err != nil {
t.Fatalf("err: %s", err)
}
defer f.Close()
if err := terraform.WriteState(s, f); err != nil {
t.Fatalf("err: %s", err)
}
return path
}
// testStateOutput tests that the state at the given path contains
// the expected state string.
func testStateOutput(t *testing.T, path string, expected string) {
f, err := os.Open(path)
if err != nil {
t.Fatalf("err: %s", err)
}
newState, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(newState.String())
expected = strings.TrimSpace(expected)
if actual != expected {
t.Fatalf("expected:\n%s\nactual:\n%s", expected, actual)
}
}
func testProvider() *terraform.MockResourceProvider {
p := new(terraform.MockResourceProvider)
p.DiffReturn = &terraform.InstanceDiff{}
p.RefreshFn = func(
info *terraform.InstanceInfo,
s *terraform.InstanceState) (*terraform.InstanceState, error) {
return s, nil
}
p.ResourcesReturn = []terraform.ResourceType{
terraform.ResourceType{
Name: "test_instance",
},
}
return p
}
func testTempFile(t *testing.T) string {
return filepath.Join(testTempDir(t), "state.tfstate")
}
func testTempDir(t *testing.T) string {
d, err := ioutil.TempDir("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
return d
}
// testCwd is used to change the current working directory
// into a test directory that should be remoted after
func testCwd(t *testing.T) (string, string) {
tmp, err := ioutil.TempDir("", "tf")
if err != nil {
t.Fatalf("err: %v", err)
}
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %v", err)
}
if err := os.Chdir(tmp); err != nil {
t.Fatalf("err: %v", err)
}
return tmp, cwd
}
// testFixCwd is used to as a defer to testDir
func testFixCwd(t *testing.T, tmp, cwd string) {
if err := os.Chdir(cwd); err != nil {
t.Fatalf("err: %v", err)
}
if err := os.RemoveAll(tmp); err != nil {
t.Fatalf("err: %v", err)
}
}