terraform/backend/local/testing.go

288 lines
8.8 KiB
Go

package local
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
)
// TestLocal returns a configured Local struct with temporary paths and
// in-memory ContextOpts.
//
// No operations will be called on the returned value, so you can still set
// public fields without any locks.
func TestLocal(t *testing.T) (*Local, func()) {
t.Helper()
tempDir := testTempDir(t)
local := New()
local.StatePath = filepath.Join(tempDir, "state.tfstate")
local.StateOutPath = filepath.Join(tempDir, "state.tfstate")
local.StateBackupPath = filepath.Join(tempDir, "state.tfstate.bak")
local.StateWorkspaceDir = filepath.Join(tempDir, "state.tfstate.d")
local.ContextOpts = &terraform.ContextOpts{}
cleanup := func() {
if err := os.RemoveAll(tempDir); err != nil {
t.Fatal("error cleanup up test:", err)
}
}
return local, cleanup
}
// TestLocalProvider modifies the ContextOpts of the *Local parameter to
// have a provider with the given name.
func TestLocalProvider(t *testing.T, b *Local, name string, schema *terraform.ProviderSchema) *terraform.MockProvider {
// Build a mock resource provider for in-memory operations
p := new(terraform.MockProvider)
if schema == nil {
schema = &terraform.ProviderSchema{} // default schema is empty
}
p.GetSchemaResponse = &providers.GetSchemaResponse{
Provider: providers.Schema{Block: schema.Provider},
ProviderMeta: providers.Schema{Block: schema.ProviderMeta},
ResourceTypes: map[string]providers.Schema{},
DataSources: map[string]providers.Schema{},
}
for name, res := range schema.ResourceTypes {
p.GetSchemaResponse.ResourceTypes[name] = providers.Schema{
Block: res,
Version: int64(schema.ResourceTypeSchemaVersions[name]),
}
}
for name, dat := range schema.DataSources {
p.GetSchemaResponse.DataSources[name] = providers.Schema{Block: dat}
}
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
rSchema, _ := schema.SchemaForResourceType(addrs.ManagedResourceMode, req.TypeName)
if rSchema == nil {
rSchema = &configschema.Block{} // default schema is empty
}
plannedVals := map[string]cty.Value{}
for name, attrS := range rSchema.Attributes {
val := req.ProposedNewState.GetAttr(name)
if attrS.Computed && val.IsNull() {
val = cty.UnknownVal(attrS.Type)
}
plannedVals[name] = val
}
for name := range rSchema.BlockTypes {
// For simplicity's sake we just copy the block attributes over
// verbatim, since this package's mock providers are all relatively
// simple -- we're testing the backend, not esoteric provider features.
plannedVals[name] = req.ProposedNewState.GetAttr(name)
}
return providers.PlanResourceChangeResponse{
PlannedState: cty.ObjectVal(plannedVals),
PlannedPrivate: req.PriorPrivate,
}
}
p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse {
return providers.ReadResourceResponse{NewState: req.PriorState}
}
p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
return providers.ReadDataSourceResponse{State: req.Config}
}
// Initialize the opts
if b.ContextOpts == nil {
b.ContextOpts = &terraform.ContextOpts{}
}
// Set up our provider
b.ContextOpts.Providers = map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider(name): providers.FactoryFixed(p),
}
return p
}
// TestLocalSingleState is a backend implementation that wraps Local
// and modifies it to only support single states (returns
// ErrWorkspacesNotSupported for multi-state operations).
//
// This isn't an actual use case, this is exported just to provide a
// easy way to test that behavior.
type TestLocalSingleState struct {
*Local
}
// TestNewLocalSingle is a factory for creating a TestLocalSingleState.
// This function matches the signature required for backend/init.
func TestNewLocalSingle() backend.Backend {
return &TestLocalSingleState{Local: New()}
}
func (b *TestLocalSingleState) Workspaces() ([]string, error) {
return nil, backend.ErrWorkspacesNotSupported
}
func (b *TestLocalSingleState) DeleteWorkspace(string) error {
return backend.ErrWorkspacesNotSupported
}
func (b *TestLocalSingleState) StateMgr(name string) (statemgr.Full, error) {
if name != backend.DefaultStateName {
return nil, backend.ErrWorkspacesNotSupported
}
return b.Local.StateMgr(name)
}
// TestLocalNoDefaultState is a backend implementation that wraps
// Local and modifies it to support named states, but not the
// default state. It returns ErrDefaultWorkspaceNotSupported when
// the DefaultStateName is used.
type TestLocalNoDefaultState struct {
*Local
}
// TestNewLocalNoDefault is a factory for creating a TestLocalNoDefaultState.
// This function matches the signature required for backend/init.
func TestNewLocalNoDefault() backend.Backend {
return &TestLocalNoDefaultState{Local: New()}
}
func (b *TestLocalNoDefaultState) Workspaces() ([]string, error) {
workspaces, err := b.Local.Workspaces()
if err != nil {
return nil, err
}
filtered := workspaces[:0]
for _, name := range workspaces {
if name != backend.DefaultStateName {
filtered = append(filtered, name)
}
}
return filtered, nil
}
func (b *TestLocalNoDefaultState) DeleteWorkspace(name string) error {
if name == backend.DefaultStateName {
return backend.ErrDefaultWorkspaceNotSupported
}
return b.Local.DeleteWorkspace(name)
}
func (b *TestLocalNoDefaultState) StateMgr(name string) (statemgr.Full, error) {
if name == backend.DefaultStateName {
return nil, backend.ErrDefaultWorkspaceNotSupported
}
return b.Local.StateMgr(name)
}
func testTempDir(t *testing.T) string {
d, err := ioutil.TempDir("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
return d
}
func testStateFile(t *testing.T, path string, s *states.State) {
stateFile := statemgr.NewFilesystem(path)
stateFile.WriteState(s)
}
func mustProviderConfig(s string) addrs.AbsProviderConfig {
p, diags := addrs.ParseAbsProviderConfigStr(s)
if diags.HasErrors() {
panic(diags.Err())
}
return p
}
func mustResourceInstanceAddr(s string) addrs.AbsResourceInstance {
addr, diags := addrs.ParseAbsResourceInstanceStr(s)
if diags.HasErrors() {
panic(diags.Err())
}
return addr
}
// assertBackendStateUnlocked attempts to lock the backend state. Failure
// indicates that the state was indeed locked and therefore this function will
// return true.
func assertBackendStateUnlocked(t *testing.T, b *Local) bool {
t.Helper()
stateMgr, _ := b.StateMgr(backend.DefaultStateName)
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
t.Errorf("state is already locked: %s", err.Error())
return false
}
return true
}
// assertBackendStateLocked attempts to lock the backend state. Failure
// indicates that the state was already locked and therefore this function will
// return false.
func assertBackendStateLocked(t *testing.T, b *Local) bool {
t.Helper()
stateMgr, _ := b.StateMgr(backend.DefaultStateName)
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
return true
}
t.Error("unexpected success locking state")
return true
}
// testRecordDiagnostics allows tests to record and later inspect diagnostics
// emitted during an Operation. It returns a record function which can be set
// as the ShowDiagnostics value of an Operation, and a playback function which
// returns the recorded diagnostics for inspection.
func testRecordDiagnostics(t *testing.T) (record func(vals ...interface{}), playback func() tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
record = func(vals ...interface{}) {
diags = diags.Append(vals...)
}
playback = func() tfdiags.Diagnostics {
diags.Sort()
return diags
}
return
}
// testLogDiagnostics returns a function which can be used as the
// ShowDiagnostics value for an Operation, in order to help debugging during
// tests. Any calls to this function result in test logs.
func testLogDiagnostics(t *testing.T) func(vals ...interface{}) {
return func(vals ...interface{}) {
var diags tfdiags.Diagnostics
diags = diags.Append(vals...)
diags.Sort()
for _, diag := range diags {
// NOTE: Since the caller here is not directly the TestLocal
// function, t.Helper doesn't apply and so the log source
// isn't correctly shown in the test log output. This seems
// unavoidable as long as this is happening so indirectly.
desc := diag.Description()
if desc.Detail != "" {
t.Logf("%s: %s", desc.Summary, desc.Detail)
} else {
t.Log(desc.Summary)
}
}
}
}