backend: Update interface and implementations for new config loader

The new config loader requires some steps to happen in a different
order, particularly in regard to knowing the schema in order to
decode the configuration.

Here we lean directly on the configschema package, rather than
on helper/schema.Backend as before, because it's generally
sufficient for our needs here and this prepares us for the
helper/schema package later moving out into its own repository
to seed a "plugin SDK".
This commit is contained in:
Martin Atkins 2018-03-20 18:43:02 -07:00
parent 591aaf1e6a
commit 5782357c28
43 changed files with 1007 additions and 1160 deletions

View File

@ -1,14 +1,17 @@
package atlas
import (
"context"
"fmt"
"net/url"
"os"
"strings"
"sync"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config/configschema"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
@ -17,6 +20,9 @@ import (
"github.com/mitchellh/colorstring"
)
const EnvVarToken = "ATLAS_TOKEN"
const EnvVarAddress = "ATLAS_ADDRESS"
// Backend is an implementation of EnhancedBackend that performs all operations
// in Atlas. State must currently also be stored in Atlas, although it is worth
// investigating in the future if state storage can be external as well.
@ -44,97 +50,130 @@ type Backend struct {
opLock sync.Mutex
}
// New returns a new initialized Atlas backend.
func New() *Backend {
b := &Backend{}
b.schema = &schema.Backend{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
func (b *Backend) ConfigSchema() *configschema.Block {
return &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"name": {
Type: cty.String,
Required: true,
Description: schemaDescriptions["name"],
Description: "Full name of the environment in Terraform Enterprise, such as 'myorg/myenv'",
},
"access_token": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: schemaDescriptions["access_token"],
DefaultFunc: schema.EnvDefaultFunc("ATLAS_TOKEN", nil),
},
"address": &schema.Schema{
Type: schema.TypeString,
"access_token": {
Type: cty.String,
Optional: true,
Description: schemaDescriptions["address"],
DefaultFunc: schema.EnvDefaultFunc("ATLAS_ADDRESS", defaultAtlasServer),
Description: "Access token to use to access Terraform Enterprise; the ATLAS_TOKEN environment variable is used if this argument is not set",
},
"address": {
Type: cty.String,
Optional: true,
Description: "Base URL for your Terraform Enterprise installation; the ATLAS_ADDRESS environment variable is used if this argument is not set, finally falling back to a default of 'https://atlas.hashicorp.com/' if neither are set.",
},
},
ConfigureFunc: b.configure,
}
return b
}
func (b *Backend) configure(ctx context.Context) error {
d := schema.FromContextBackendConfig(ctx)
func (b *Backend) ValidateConfig(obj cty.Value) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
// Parse the address
addr := d.Get("address").(string)
addrUrl, err := url.Parse(addr)
if err != nil {
return fmt.Errorf("Error parsing 'address': %s", err)
name := obj.GetAttr("name").AsString()
if ct := strings.Count(name, "/"); ct != 1 {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid workspace selector",
`The "name" argument must be an organization name and a workspace name separated by a slash, such as "acme/network-production".`,
cty.Path{cty.GetAttrStep{Name: "name"}},
))
}
// Parse the org/env
name := d.Get("name").(string)
parts := strings.Split(name, "/")
if len(parts) != 2 {
return fmt.Errorf("malformed name '%s', expected format '<org>/<name>'", name)
if v := obj.GetAttr("address"); !v.IsNull() {
addr := v.AsString()
_, err := url.Parse(addr)
if err != nil {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid Terraform Enterprise URL",
fmt.Sprintf(`The "address" argument must be a valid URL: %s.`, err),
cty.Path{cty.GetAttrStep{Name: "address"}},
))
}
}
org := parts[0]
env := parts[1]
// Setup the client
b.stateClient = &stateClient{
Server: addr,
ServerURL: addrUrl,
AccessToken: d.Get("access_token").(string),
User: org,
Name: env,
return diags
}
func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
client := &stateClient{
// This is optionally set during Atlas Terraform runs.
RunId: os.Getenv("ATLAS_RUN_ID"),
}
return nil
}
name := obj.GetAttr("name").AsString() // assumed valid due to ValidateConfig method
slashIdx := strings.Index(name, "/")
client.User = name[:slashIdx]
client.Name = name[slashIdx+1:]
func (b *Backend) Input(ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
return b.schema.Input(ui, c)
}
func (b *Backend) Validate(c *terraform.ResourceConfig) ([]string, []error) {
return b.schema.Validate(c)
}
func (b *Backend) Configure(c *terraform.ResourceConfig) error {
return b.schema.Configure(c)
}
func (b *Backend) State(name string) (state.State, error) {
if name != backend.DefaultStateName {
return nil, backend.ErrNamedStatesNotSupported
if v := obj.GetAttr("access_token"); !v.IsNull() {
client.AccessToken = v.AsString()
} else {
client.AccessToken = os.Getenv(EnvVarToken)
if client.AccessToken == "" {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Missing Terraform Enterprise access token",
`The "access_token" argument must be set unless the ATLAS_TOKEN environment variable is set to provide the authentication token for Terraform Enterprise.`,
cty.Path{cty.GetAttrStep{Name: "access_token"}},
))
}
}
return &remote.State{Client: b.stateClient}, nil
if v := obj.GetAttr("address"); !v.IsNull() {
addr := v.AsString()
addrURL, err := url.Parse(addr)
if err != nil {
// We already validated the URL in ValidateConfig, so this shouldn't happen
panic(err)
}
client.Server = addr
client.ServerURL = addrURL
} else {
addr := os.Getenv(EnvVarAddress)
if addr == "" {
addr = defaultAtlasServer
}
addrURL, err := url.Parse(addr)
if err != nil {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid Terraform Enterprise URL",
fmt.Sprintf(`The ATLAS_ADDRESS environment variable must contain a valid URL: %s.`, err),
cty.Path{cty.GetAttrStep{Name: "address"}},
))
}
client.Server = addr
client.ServerURL = addrURL
}
b.stateClient = client
return diags
}
func (b *Backend) States() ([]string, error) {
return nil, backend.ErrNamedStatesNotSupported
}
func (b *Backend) DeleteState(name string) error {
return backend.ErrNamedStatesNotSupported
}
func (b *Backend) States() ([]string, error) {
return nil, backend.ErrNamedStatesNotSupported
func (b *Backend) State(name string) (state.State, error) {
if name != backend.DefaultStateName {
return nil, backend.ErrNamedStatesNotSupported
}
return &remote.State{Client: b.stateClient}, nil
}
// Colorize returns the Colorize structure that can be used for colorizing
@ -150,12 +189,3 @@ func (b *Backend) Colorize() *colorstring.Colorize {
Disable: true,
}
}
var schemaDescriptions = map[string]string{
"name": "Full name of the environment in Atlas, such as 'hashicorp/myenv'",
"access_token": "Access token to use to access Atlas. If ATLAS_TOKEN is set then\n" +
"this will override any saved value for this.",
"address": "Address to your Atlas installation. This defaults to the publicly\n" +
"hosted version at 'https://atlas.hashicorp.com/'. This address\n" +
"should contain the full HTTP scheme to use.",
}

View File

@ -4,9 +4,9 @@ import (
"os"
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
)
func TestImpl(t *testing.T) {
@ -18,16 +18,18 @@ func TestConfigure_envAddr(t *testing.T) {
defer os.Setenv("ATLAS_ADDRESS", os.Getenv("ATLAS_ADDRESS"))
os.Setenv("ATLAS_ADDRESS", "http://foo.com")
b := New()
err := b.Configure(terraform.NewResourceConfig(config.TestRawConfig(t, map[string]interface{}{
"name": "foo/bar",
})))
if err != nil {
t.Fatalf("err: %s", err)
b := &Backend{}
diags := b.Configure(cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("foo/bar"),
"address": cty.NullVal(cty.String),
"access_token": cty.StringVal("placeholder"),
}))
for _, diag := range diags {
t.Error(diag)
}
if b.stateClient.Server != "http://foo.com" {
t.Fatalf("bad: %#v", b.stateClient)
if got, want := b.stateClient.Server, "http://foo.com"; got != want {
t.Fatalf("wrong URL %#v; want %#v", got, want)
}
}
@ -35,15 +37,17 @@ func TestConfigure_envToken(t *testing.T) {
defer os.Setenv("ATLAS_TOKEN", os.Getenv("ATLAS_TOKEN"))
os.Setenv("ATLAS_TOKEN", "foo")
b := New()
err := b.Configure(terraform.NewResourceConfig(config.TestRawConfig(t, map[string]interface{}{
"name": "foo/bar",
})))
if err != nil {
t.Fatalf("err: %s", err)
b := &Backend{}
diags := b.Configure(cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("foo/bar"),
"address": cty.NullVal(cty.String),
"access_token": cty.NullVal(cty.String),
}))
for _, diag := range diags {
t.Error(diag)
}
if b.stateClient.AccessToken != "foo" {
t.Fatalf("bad: %#v", b.stateClient)
if got, want := b.stateClient.AccessToken, "foo"; got != want {
t.Fatalf("wrong access token %#v; want %#v", got, want)
}
}

View File

@ -13,14 +13,23 @@ import (
"testing"
"time"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
)
func testStateClient(t *testing.T, c map[string]interface{}) remote.Client {
b := backend.TestBackendConfig(t, New(), c)
func testStateClient(t *testing.T, c map[string]string) remote.Client {
vals := make(map[string]cty.Value)
for k, s := range c {
vals[k] = cty.StringVal(s)
}
synthBody := configs.SynthBody("<test>", vals)
b := backend.TestBackendConfig(t, &Backend{}, synthBody)
raw, err := b.State(backend.DefaultStateName)
if err != nil {
t.Fatalf("err: %s", err)
@ -42,7 +51,7 @@ func TestStateClient(t *testing.T) {
t.Skipf("skipping, ATLAS_TOKEN must be set")
}
client := testStateClient(t, map[string]interface{}{
client := testStateClient(t, map[string]string{
"access_token": token,
"name": "hashicorp/test-remote-state",
})
@ -53,7 +62,7 @@ func TestStateClient(t *testing.T) {
func TestStateClient_noRetryOnBadCerts(t *testing.T) {
acctest.RemoteTestPrecheck(t)
client := testStateClient(t, map[string]interface{}{
client := testStateClient(t, map[string]string{
"access_token": "NOT_REQUIRED",
"name": "hashicorp/test-remote-state",
})
@ -99,7 +108,7 @@ func TestStateClient_ReportedConflictEqualStates(t *testing.T) {
srv := fakeAtlas.Server()
defer srv.Close()
client := testStateClient(t, map[string]interface{}{
client := testStateClient(t, map[string]string{
"access_token": "sometoken",
"name": "someuser/some-test-remote-state",
"address": srv.URL,
@ -124,7 +133,7 @@ func TestStateClient_NoConflict(t *testing.T) {
srv := fakeAtlas.Server()
defer srv.Close()
client := testStateClient(t, map[string]interface{}{
client := testStateClient(t, map[string]string{
"access_token": "sometoken",
"name": "someuser/some-test-remote-state",
"address": srv.URL,
@ -152,7 +161,7 @@ func TestStateClient_LegitimateConflict(t *testing.T) {
srv := fakeAtlas.Server()
defer srv.Close()
client := testStateClient(t, map[string]interface{}{
client := testStateClient(t, map[string]string{
"access_token": "sometoken",
"name": "someuser/some-test-remote-state",
"address": srv.URL,
@ -191,7 +200,7 @@ func TestStateClient_UnresolvableConflict(t *testing.T) {
srv := fakeAtlas.Server()
defer srv.Close()
client := testStateClient(t, map[string]interface{}{
client := testStateClient(t, map[string]string{
"access_token": "sometoken",
"name": "someuser/some-test-remote-state",
"address": srv.URL,

View File

@ -9,10 +9,16 @@ import (
"errors"
"time"
"github.com/hashicorp/terraform/configs"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/command/clistate"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/config/configschema"
"github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
)
// DefaultStateName is the name of the default, initial state that every
@ -41,11 +47,45 @@ type InitFn func() Backend
// Backend is the minimal interface that must be implemented to enable Terraform.
type Backend interface {
// Ask for input and configure the backend. Similar to
// terraform.ResourceProvider.
Input(terraform.UIInput, *terraform.ResourceConfig) (*terraform.ResourceConfig, error)
Validate(*terraform.ResourceConfig) ([]string, []error)
Configure(*terraform.ResourceConfig) error
// ConfigSchema returns a description of the expected configuration
// structure for the receiving backend.
//
// This method does not have any side-effects for the backend and can
// be safely used before configuring.
ConfigSchema() *configschema.Block
// ValidateConfig checks the validity of the values in the given
// configuration, assuming that its structure has already been validated
// per the schema returned by ConfigSchema.
//
// This method does not have any side-effects for the backend and can
// be safely used before configuring. It also does not consult any
// external data such as environment variables, disk files, etc. Validation
// that requires such external data should be deferred until the
// Configure call.
//
// If error diagnostics are returned then the configuration is not valid
// and must not subsequently be passed to the Configure method.
//
// This method may return configuration-contextual diagnostics such
// as tfdiags.AttributeValue, and so the caller should provide the
// necessary context via the diags.InConfigBody method before returning
// diagnostics to the user.
ValidateConfig(cty.Value) tfdiags.Diagnostics
// Configure uses the provided configuration to set configuration fields
// within the backend.
//
// The given configuration is assumed to have already been validated
// against the schema returned by ConfigSchema and passed validation
// via ValidateConfig.
//
// This method may be called only once per backend instance, and must be
// called before all other methods except where otherwise stated.
//
// If error diagnostics are returned, the internal state of the instance
// is undefined and no other methods may be called.
Configure(cty.Value) tfdiags.Diagnostics
// State returns the current state for this environment. This state may
// not be loaded locally: the proper APIs should be called on state.State
@ -96,7 +136,7 @@ type Enhanced interface {
type Local interface {
// Context returns a runnable terraform Context. The operation parameter
// doesn't need a Type set but it needs other options set such as Module.
Context(*Operation) (*terraform.Context, state.State, error)
Context(*Operation) (*terraform.Context, state.State, tfdiags.Diagnostics)
}
// An operation represents an operation for Terraform to execute.
@ -128,8 +168,13 @@ type Operation struct {
PlanOutPath string // PlanOutPath is the path to save the plan
PlanOutBackend *terraform.BackendState
// Module settings specify the root module to use for operations.
Module *module.Tree
// ConfigDir is the path to the directory containing the configuration's
// root module.
ConfigDir string
// ConfigLoader is a configuration loader that can be used to load
// configuration from ConfigDir.
ConfigLoader *configload.Loader
// Plan is a plan that was passed as an argument. This is valid for
// plan and apply arguments but may not work for all backends.
@ -165,6 +210,22 @@ type Operation struct {
Workspace string
}
// HasConfig returns true if and only if the operation has a ConfigDir value
// that refers to a directory containing at least one Terraform configuration
// file.
func (o *Operation) HasConfig() bool {
return o.ConfigLoader.IsConfigDir(o.ConfigDir)
}
// Config loads the configuration that the operation applies to, using the
// ConfigDir and ConfigLoader fields within the receiving operation.
func (o *Operation) Config() (*configs.Config, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
config, hclDiags := o.ConfigLoader.LoadConfig(o.ConfigDir)
diags = diags.Append(hclDiags)
return config, diags
}
// RunningOperation is the result of starting an operation.
type RunningOperation struct {
// For implementers of a backend, this context should not wrap the
@ -184,9 +245,9 @@ type RunningOperation struct {
// to avoid running operations during process exit.
Cancel context.CancelFunc
// Err is the error of the operation. This is populated after
// the operation has completed.
Err error
// Result is the exit status of the operation, populated only after the
// operation has completed.
Result OperationResult
// ExitCode can be used to set a custom exit code. This enables enhanced
// backends to set specific exit codes that miror any remote exit codes.
@ -201,3 +262,16 @@ type RunningOperation struct {
// after the operation completes to avoid read/write races.
State *terraform.State
}
// OperationResult describes the result status of an operation.
type OperationResult int
const (
// OperationSuccess indicates that the operation completed as expected.
OperationSuccess OperationResult = 0
// OperationFailure indicates that the operation encountered some sort
// of error, and thus may have been only partially performed or not
// performed at all.
OperationFailure OperationResult = 1
)

View File

@ -48,6 +48,10 @@ type CLIOpts struct {
CLI cli.Ui
CLIColor *colorstring.Colorize
// ShowDiagnostics is a function that will format and print diagnostic
// messages to the UI.
ShowDiagnostics func(vals ...interface{})
// StatePath is the local path where state is read from.
//
// StateOutPath is the local path where the state will be written.

View File

@ -4,7 +4,7 @@ import (
"testing"
"github.com/hashicorp/terraform/backend/remote-state/inmem"
"github.com/hashicorp/terraform/terraform"
"github.com/zclconf/go-cty/cty"
)
func TestDeprecateBackend(t *testing.T) {
@ -12,21 +12,19 @@ func TestDeprecateBackend(t *testing.T) {
deprecatedBackend := deprecateBackend(
inmem.New(),
deprecateMessage,
)()
)
warns, errs := deprecatedBackend.Validate(&terraform.ResourceConfig{})
if errs != nil {
for _, err := range errs {
t.Error(err)
diags := deprecatedBackend.ValidateConfig(cty.EmptyObjectVal)
if len(diags) != 1 {
t.Errorf("got %d diagnostics; want 1", len(diags))
for _, diag := range diags {
t.Errorf("- %s", diag)
}
t.Fatal("validation errors")
return
}
if len(warns) != 1 {
t.Fatalf("expected 1 warning, got %q", warns)
}
if warns[0] != deprecateMessage {
t.Fatalf("expected %q, got %q", deprecateMessage, warns[0])
desc := diags[0].Description()
if desc.Summary != deprecateMessage {
t.Fatalf("wrong message %q; want %q", desc.Summary, deprecateMessage)
}
}

View File

@ -12,13 +12,17 @@ import (
"strings"
"sync"
"github.com/hashicorp/terraform/tfdiags"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/command/clistate"
"github.com/hashicorp/terraform/config/configschema"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
"github.com/zclconf/go-cty/cty"
)
const (
@ -37,6 +41,9 @@ type Local struct {
CLI cli.Ui
CLIColor *colorstring.Colorize
// ShowDiagnostics prints diagnostic messages to the UI.
ShowDiagnostics func(vals ...interface{})
// The State* paths are set from the backend config, and may be left blank
// to use the defaults. If the actual paths for the local backend state are
// needed, use the StatePaths method.
@ -89,106 +96,92 @@ type Local struct {
// exact commands that are being run.
RunningInAutomation bool
schema *schema.Backend
opLock sync.Mutex
}
// New returns a new initialized local backend.
func New() *Local {
return NewWithBackend(nil)
}
// NewWithBackend returns a new local backend initialized with a
// dedicated backend for non-enhanced behavior.
func NewWithBackend(backend backend.Backend) *Local {
b := &Local{
Backend: backend,
func (b *Local) ConfigSchema() *configschema.Block {
if b.Backend != nil {
return b.Backend.ConfigSchema()
}
b.schema = &schema.Backend{
Schema: map[string]*schema.Schema{
"path": &schema.Schema{
Type: schema.TypeString,
return &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"path": {
Type: cty.String,
Optional: true,
Default: "",
},
"workspace_dir": &schema.Schema{
Type: schema.TypeString,
"workspace_dir": {
Type: cty.String,
Optional: true,
Default: "",
},
"environment_dir": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "",
ConflictsWith: []string{"workspace_dir"},
Deprecated: "workspace_dir should be used instead, with the same meaning",
},
// environment_dir was previously a deprecated alias for
// workspace_dir, but now removed.
},
ConfigureFunc: b.configure,
}
return b
}
func (b *Local) configure(ctx context.Context) error {
d := schema.FromContextBackendConfig(ctx)
// Set the path if it is set
pathRaw, ok := d.GetOk("path")
if ok {
path := pathRaw.(string)
if path == "" {
return fmt.Errorf("configured path is empty")
}
b.StatePath = path
b.StateOutPath = path
func (b *Local) ValidateConfig(obj cty.Value) tfdiags.Diagnostics {
if b.Backend != nil {
return b.Backend.ValidateConfig(obj)
}
if raw, ok := d.GetOk("workspace_dir"); ok {
path := raw.(string)
if path != "" {
b.StateWorkspaceDir = path
var diags tfdiags.Diagnostics
if val := obj.GetAttr("path"); !val.IsNull() {
p := val.AsString()
if p == "" {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid local state file path",
`The "path" attribute value must not be empty.`,
cty.Path{cty.GetAttrStep{Name: "path"}},
))
}
}
// Legacy name, which ConflictsWith workspace_dir
if raw, ok := d.GetOk("environment_dir"); ok {
path := raw.(string)
if path != "" {
b.StateWorkspaceDir = path
if val := obj.GetAttr("workspace_dir"); !val.IsNull() {
p := val.AsString()
if p == "" {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid local workspace directory path",
`The "workspace_dir" attribute value must not be empty.`,
cty.Path{cty.GetAttrStep{Name: "workspace_dir"}},
))
}
}
return nil
return diags
}
func (b *Local) Input(ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
f := b.schema.Input
func (b *Local) Configure(obj cty.Value) tfdiags.Diagnostics {
if b.Backend != nil {
f = b.Backend.Input
return b.Backend.Configure(obj)
}
return f(ui, c)
}
func (b *Local) Validate(c *terraform.ResourceConfig) ([]string, []error) {
f := b.schema.Validate
if b.Backend != nil {
f = b.Backend.Validate
}
return f(c)
}
var diags tfdiags.Diagnostics
func (b *Local) Configure(c *terraform.ResourceConfig) error {
f := b.schema.Configure
if b.Backend != nil {
f = b.Backend.Configure
type Config struct {
Path string `hcl:"path,optional"`
WorkspaceDir string `hcl:"workspace_dir,optional"`
}
return f(c)
if val := obj.GetAttr("path"); !val.IsNull() {
p := val.AsString()
b.StatePath = p
b.StateOutPath = p
} else {
b.StatePath = DefaultStateFilename
b.StateOutPath = DefaultStateFilename
}
if val := obj.GetAttr("workspace_dir"); !val.IsNull() {
p := val.AsString()
b.StateWorkspaceDir = p
} else {
b.StateWorkspaceDir = DefaultWorkspaceDir
}
return diags
}
func (b *Local) State(name string) (state.State, error) {
@ -339,7 +332,11 @@ func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend.
// the state was locked during context creation, unlock the state when
// the operation completes
defer func() {
runningOp.Err = op.StateLocker.Unlock(runningOp.Err)
err := op.StateLocker.Unlock(nil)
if err != nil {
b.ShowDiagnostics(err)
}
runningOp.Result = backend.OperationFailure
}()
defer b.opLock.Unlock()
@ -397,6 +394,28 @@ func (b *Local) opWait(
return
}
// ReportResult is a helper for the common chore of setting the status of
// a running operation and showing any diagnostics produced during that
// operation.
//
// If the given diagnostics contains errors then the operation's result
// will be set to backend.OperationFailure. It will be set to
// backend.OperationSuccess otherwise. It will then use b.ShowDiagnostics
// to show the given diagnostics before returning.
//
// Callers should feel free to do each of these operations separately in
// more complex cases where e.g. diagnostics are interleaved with other
// output, but terminating immediately after reporting error diagnostics is
// common and can be expressed concisely via this method.
func (b *Local) ReportResult(op *backend.RunningOperation, diags tfdiags.Diagnostics) {
if diags.HasErrors() {
op.Result = backend.OperationFailure
} else {
op.Result = backend.OperationSuccess
}
b.ShowDiagnostics(diags)
}
// Colorize returns the Colorize structure that can be used for colorizing
// output. This is gauranteed to always return a non-nil value and so is useful
// as a helper to wrap any potentially colored strings.
@ -411,6 +430,39 @@ func (b *Local) Colorize() *colorstring.Colorize {
}
}
func (b *Local) schemaConfigure(ctx context.Context) error {
d := schema.FromContextBackendConfig(ctx)
// Set the path if it is set
pathRaw, ok := d.GetOk("path")
if ok {
path := pathRaw.(string)
if path == "" {
return fmt.Errorf("configured path is empty")
}
b.StatePath = path
b.StateOutPath = path
}
if raw, ok := d.GetOk("workspace_dir"); ok {
path := raw.(string)
if path != "" {
b.StateWorkspaceDir = path
}
}
// Legacy name, which ConflictsWith workspace_dir
if raw, ok := d.GetOk("environment_dir"); ok {
path := raw.(string)
if path != "" {
b.StateWorkspaceDir = path
}
}
return nil
}
// StatePaths returns the StatePath, StateOutPath, and StateBackupPath as
// configured from the CLI.
func (b *Local) StatePaths(name string) (string, string, string) {

View File

@ -6,15 +6,13 @@ import (
"errors"
"fmt"
"log"
"strings"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/command/format"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
)
func (b *Local) opApply(
@ -24,17 +22,18 @@ func (b *Local) opApply(
runningOp *backend.RunningOperation) {
log.Printf("[INFO] backend/local: starting Apply operation")
// If we have a nil module at this point, then set it to an empty tree
// to avoid any potential crashes.
if op.Plan == nil && op.Module == nil && !op.Destroy {
runningOp.Err = fmt.Errorf(strings.TrimSpace(applyErrNoConfig))
return
}
var diags tfdiags.Diagnostics
// If we have a nil module at this point, then set it to an empty tree
// to avoid any potential crashes.
if op.Module == nil {
op.Module = module.NewEmptyTree()
if op.Plan == nil && !op.Destroy && !op.HasConfig() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"No configuration files",
"Apply requires configuration to be present. Applying without a configuration would mark everything for destruction, which is normally not what is desired. If you would like to destroy everything, run 'terraform destroy' instead.",
))
b.ReportResult(runningOp, diags)
return
}
// Setup our count hook that keeps track of resource changes
@ -50,7 +49,8 @@ func (b *Local) opApply(
// Get our context
tfCtx, opState, err := b.context(op)
if err != nil {
runningOp.Err = err
diags = diags.Append(err)
b.ReportResult(runningOp, diags)
return
}
@ -64,7 +64,9 @@ func (b *Local) opApply(
log.Printf("[INFO] backend/local: apply calling Refresh")
_, err := tfCtx.Refresh()
if err != nil {
runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err)
diags = diags.Append(err)
runningOp.Result = backend.OperationFailure
b.ShowDiagnostics(diags)
return
}
}
@ -73,7 +75,8 @@ func (b *Local) opApply(
log.Printf("[INFO] backend/local: apply calling Plan")
plan, err := tfCtx.Plan()
if err != nil {
runningOp.Err = errwrap.Wrapf("Error running plan: {{err}}", err)
diags = diags.Append(err)
b.ReportResult(runningOp, diags)
return
}
@ -113,15 +116,17 @@ func (b *Local) opApply(
Description: desc,
})
if err != nil {
runningOp.Err = errwrap.Wrapf("Error asking for approval: {{err}}", err)
diags = diags.Append(errwrap.Wrapf("Error asking for approval: {{err}}", err))
b.ReportResult(runningOp, diags)
return
}
if v != "yes" {
if op.Destroy {
runningOp.Err = errors.New("Destroy cancelled.")
b.CLI.Info("Destroy cancelled.")
} else {
runningOp.Err = errors.New("Apply cancelled.")
b.CLI.Info("Apply cancelled.")
}
runningOp.Result = backend.OperationFailure
return
}
}
@ -150,26 +155,31 @@ func (b *Local) opApply(
// Persist the state
if err := opState.WriteState(applyState); err != nil {
runningOp.Err = b.backupStateForError(applyState, err)
diags = diags.Append(b.backupStateForError(applyState, err))
b.ReportResult(runningOp, diags)
return
}
if err := opState.PersistState(); err != nil {
runningOp.Err = b.backupStateForError(applyState, err)
diags = diags.Append(b.backupStateForError(applyState, err))
b.ReportResult(runningOp, diags)
return
}
if applyErr != nil {
runningOp.Err = fmt.Errorf(
"Error applying plan:\n\n"+
"%s\n\n"+
"Terraform does not automatically rollback in the face of errors.\n"+
"Instead, your Terraform state file has been partially updated with\n"+
"any resources that successfully completed. Please address the error\n"+
"above and apply again to incrementally change your infrastructure.",
multierror.Flatten(applyErr))
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
applyErr.Error(),
"Terraform does not automatically rollback in the face of errors. Instead, your Terraform state file has been partially updated with any resources that successfully completed. Please address the error above and apply again to incrementally change your infrastructure.",
))
b.ReportResult(runningOp, diags)
return
}
// If we've accumulated any warnings along the way then we'll show them
// here just before we show the summary and next steps. If we encountered
// errors then we would've returned early at some other point above.
b.ShowDiagnostics(diags)
// If we have a UI, output the results
if b.CLI != nil {
if op.Destroy {
@ -236,15 +246,6 @@ func (b *Local) backupStateForError(applyState *terraform.State, err error) erro
return errors.New(stateWriteBackedUpError)
}
const applyErrNoConfig = `
No configuration files found!
Apply requires configuration to be present. Applying without a configuration
would mark everything for destruction, which is normally not what is desired.
If you would like to destroy everything, please run 'terraform destroy' which
does not require any configuration files.
`
const stateWriteBackedUpError = `Failed to persist state to backend.
The error shown above has prevented Terraform from writing the updated state

View File

@ -11,7 +11,7 @@ import (
"testing"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
@ -24,19 +24,16 @@ func TestLocal_applyBasic(t *testing.T) {
p.ApplyReturn = &terraform.InstanceState{ID: "yes"}
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply")
defer modCleanup()
op := testOperationApply()
op.Module = mod
op, configCleanup := testOperationApply(t, "./test-fixtures/apply")
defer configCleanup()
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("err: %s", err)
if run.Result != backend.OperationSuccess {
t.Fatal("operation failed")
}
if p.RefreshCalled {
@ -66,16 +63,16 @@ func TestLocal_applyEmptyDir(t *testing.T) {
p.ApplyReturn = &terraform.InstanceState{ID: "yes"}
op := testOperationApply()
op.Module = nil
op, configCleanup := testOperationApply(t, "./test-fixtures/empty")
defer configCleanup()
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Err == nil {
t.Fatal("should error")
if run.Result == backend.OperationSuccess {
t.Fatal("operation succeeded; want error")
}
if p.ApplyCalled {
@ -94,8 +91,8 @@ func TestLocal_applyEmptyDirDestroy(t *testing.T) {
p.ApplyReturn = nil
op := testOperationApply()
op.Module = nil
op, configCleanup := testOperationApply(t, "./test-fixtures/empty")
defer configCleanup()
op.Destroy = true
run, err := b.Operation(context.Background(), op)
@ -103,8 +100,8 @@ func TestLocal_applyEmptyDirDestroy(t *testing.T) {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("err: %s", err)
if run.Result != backend.OperationSuccess {
t.Fatalf("apply operation failed")
}
if p.ApplyCalled {
@ -148,19 +145,16 @@ func TestLocal_applyError(t *testing.T) {
}, nil
}
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply-error")
defer modCleanup()
op := testOperationApply()
op.Module = mod
op, configCleanup := testOperationApply(t, "./test-fixtures/apply-error")
defer configCleanup()
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Err == nil {
t.Fatal("should error")
if run.Result == backend.OperationSuccess {
t.Fatal("operation succeeded; want failure")
}
checkState(t, b.StateOutPath, `
@ -171,8 +165,8 @@ test_instance.foo:
}
func TestLocal_applyBackendFail(t *testing.T) {
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply")
defer modCleanup()
op, configCleanup := testOperationApply(t, "./test-fixtures/apply")
defer configCleanup()
b, cleanup := TestLocal(t)
defer cleanup()
@ -193,23 +187,15 @@ func TestLocal_applyBackendFail(t *testing.T) {
p.ApplyReturn = &terraform.InstanceState{ID: "yes"}
op := testOperationApply()
op.Module = mod
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Err == nil {
if run.Result == backend.OperationSuccess {
t.Fatalf("apply succeeded; want error")
}
errStr := run.Err.Error()
if !strings.Contains(errStr, "terraform state push errored.tfstate") {
t.Fatalf("wrong error message:\n%s", errStr)
}
msgStr := b.CLI.(*cli.MockUi).ErrorWriter.String()
if !strings.Contains(msgStr, "Failed to save state: fake failure") {
t.Fatalf("missing original error message in output:\n%s", msgStr)
@ -244,10 +230,16 @@ func (s failingState) WriteState(state *terraform.State) error {
return errors.New("fake failure")
}
func testOperationApply() *backend.Operation {
func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func()) {
t.Helper()
_, configLoader, configCleanup := configload.MustLoadConfigForTests(t, configDir)
return &backend.Operation{
Type: backend.OperationTypeApply,
}
Type: backend.OperationTypeApply,
ConfigDir: configDir,
ConfigLoader: configLoader,
}, configCleanup
}
// testApplyState is just a common state that we use for testing refresh.

View File

@ -3,19 +3,18 @@ package local
import (
"context"
"errors"
"log"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/command/clistate"
"github.com/hashicorp/terraform/command/format"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
)
// backend.Local implementation.
func (b *Local) Context(op *backend.Operation) (*terraform.Context, state.State, error) {
func (b *Local) Context(op *backend.Operation) (*terraform.Context, state.State, tfdiags.Diagnostics) {
// Make sure the type is invalid. We use this as a way to know not
// to ask for input/validate.
op.Type = backend.OperationTypeInvalid
@ -29,19 +28,24 @@ func (b *Local) Context(op *backend.Operation) (*terraform.Context, state.State,
return b.context(op)
}
func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, error) {
func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// Get the state.
s, err := b.State(op.Workspace)
if err != nil {
return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err)
diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err))
return nil, nil, diags
}
if err := op.StateLocker.Lock(s, op.Type.String()); err != nil {
return nil, nil, errwrap.Wrapf("Error locking state: {{err}}", err)
diags = diags.Append(errwrap.Wrapf("Error locking state: {{err}}", err))
return nil, nil, diags
}
if err := s.RefreshState(); err != nil {
return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err)
diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err))
return nil, nil, diags
}
// Initialize our context options
@ -52,13 +56,18 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State,
// Copy set options from the operation
opts.Destroy = op.Destroy
opts.Module = op.Module
opts.Targets = op.Targets
opts.UIInput = op.UIIn
if op.Variables != nil {
opts.Variables = op.Variables
}
// FIXME: Configuration is temporarily stubbed out here to artificially
// create a stopping point in our work to switch to the new config loader.
// This means no backend-provided Terraform operations will actually work.
// This will be addressed in a subsequent commit.
opts.Module = nil
// Load our state
// By the time we get here, the backend creation code in "command" took
// care of making s.State() return a state compatible with our plan,
@ -79,11 +88,13 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State,
b.pluginInitRequired(rpe)
// we wrote the full UI error here, so return a generic error for flow
// control in the command.
return nil, nil, errors.New("error satisfying plugin requirements")
diags = diags.Append(errors.New("Can't satisfy plugin requirements"))
return nil, nil, diags
}
if err != nil {
return nil, nil, err
diags = diags.Append(err)
return nil, nil, diags
}
// If we have an operation, then we automatically do the input/validate
@ -96,45 +107,19 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State,
mode |= terraform.InputModeVarUnset
if err := tfCtx.Input(mode); err != nil {
return nil, nil, errwrap.Wrapf("Error asking for user input: {{err}}", err)
diags = diags.Append(errwrap.Wrapf("Error asking for user input: {{err}}", err))
return nil, nil, diags
}
}
// If validation is enabled, validate
if b.OpValidation {
diags := tfCtx.Validate()
if len(diags) > 0 {
if diags.HasErrors() {
// If there are warnings _and_ errors then we'll take this
// path and return them all together in this error.
return nil, nil, diags.Err()
}
// For now we can't propagate warnings any further without
// printing them directly to the UI, so we'll need to
// format them here ourselves.
for _, diag := range diags {
if diag.Severity() != tfdiags.Warning {
continue
}
if b.CLI != nil {
// FIXME: We don't have access to the source code cache
// in here, so we can't produce source code snippets
// from this codepath.
b.CLI.Warn(format.Diagnostic(diag, nil, b.Colorize(), 72))
} else {
desc := diag.Description()
log.Printf("[WARN] backend/local: %s", desc.Summary)
}
}
// Make a newline before continuing
b.CLI.Output("")
}
validateDiags := tfCtx.Validate()
diags = diags.Append(validateDiags)
}
}
return tfCtx, s, nil
return tfCtx, s, diags
}
const validateWarnHeader = `

View File

@ -8,10 +8,10 @@ import (
"os"
"strings"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/tfdiags"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/command/format"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/terraform"
)
@ -20,27 +20,30 @@ func (b *Local) opPlan(
cancelCtx context.Context,
op *backend.Operation,
runningOp *backend.RunningOperation) {
log.Printf("[INFO] backend/local: starting Plan operation")
if b.CLI != nil && op.Plan != nil {
b.CLI.Output(b.Colorize().Color(
"[reset][bold][yellow]" +
"The plan command received a saved plan file as input. This command\n" +
"will output the saved plan. This will not modify the already-existing\n" +
"plan. If you wish to generate a new plan, please pass in a configuration\n" +
"directory as an argument.\n\n"))
}
var diags tfdiags.Diagnostics
// A local plan requires either a plan or a module
if op.Plan == nil && op.Module == nil && !op.Destroy {
runningOp.Err = fmt.Errorf(strings.TrimSpace(planErrNoConfig))
if b.CLI != nil && op.Plan != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Can't re-plan a saved plan",
"The plan command was given a saved plan file as its input. This command generates a new plan, and so it requires a configuration directory as its argument.",
))
b.ReportResult(runningOp, diags)
return
}
// If we have a nil module at this point, then set it to an empty tree
// to avoid any potential crashes.
if op.Module == nil {
op.Module = module.NewEmptyTree()
// Local planning requires a config, unless we're planning to destroy.
if !op.Destroy && !op.HasConfig() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"No configuration files",
"Plan requires configuration to be present. Planning without a configuration would mark everything for destruction, which is normally not what is desired. If you would like to destroy everything, run plan with the -destroy option. Otherwise, create a Terraform configuration file (.tf file) and try again.",
))
b.ReportResult(runningOp, diags)
return
}
// Setup our count hook that keeps track of resource changes
@ -55,7 +58,8 @@ func (b *Local) opPlan(
// Get our context
tfCtx, opState, err := b.context(op)
if err != nil {
runningOp.Err = err
diags = diags.Append(err)
b.ReportResult(runningOp, diags)
return
}
@ -72,7 +76,8 @@ func (b *Local) opPlan(
_, err := tfCtx.Refresh()
if err != nil {
runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err)
diags = diags.Append(err)
b.ReportResult(runningOp, diags)
return
}
if b.CLI != nil {
@ -95,7 +100,8 @@ func (b *Local) opPlan(
}
if planErr != nil {
runningOp.Err = errwrap.Wrapf("Error running plan: {{err}}", planErr)
diags = diags.Append(planErr)
b.ReportResult(runningOp, diags)
return
}
// Record state
@ -119,7 +125,12 @@ func (b *Local) opPlan(
}
f.Close()
if err != nil {
runningOp.Err = fmt.Errorf("Error writing plan file: %s", err)
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to write plan file",
fmt.Sprintf("The plan file could not be written: %s.", err),
))
b.ReportResult(runningOp, diags)
return
}
}
@ -134,6 +145,11 @@ func (b *Local) opPlan(
b.renderPlan(dispPlan)
// If we've accumulated any warnings along the way then we'll show them
// here just before we show the summary and next steps. If we encountered
// errors then we would've returned early at some other point above.
b.ShowDiagnostics(diags)
// Give the user some next-steps, unless we're running in an automation
// tool which is presumed to provide its own UI for further actions.
if !b.RunningInAutomation {

View File

@ -9,7 +9,7 @@ import (
"testing"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
@ -19,11 +19,8 @@ func TestLocal_planBasic(t *testing.T) {
defer cleanup()
p := TestLocalProvider(t, b, "test")
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
defer modCleanup()
op := testOperationPlan()
op.Module = mod
op, configCleanup := testOperationPlan(t, "./test-fixtures/plan")
defer configCleanup()
op.PlanRefresh = true
run, err := b.Operation(context.Background(), op)
@ -31,8 +28,8 @@ func TestLocal_planBasic(t *testing.T) {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("err: %s", err)
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
if !p.DiffCalled {
@ -45,9 +42,6 @@ func TestLocal_planInAutomation(t *testing.T) {
defer cleanup()
TestLocalProvider(t, b, "test")
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
defer modCleanup()
const msg = `You didn't specify an "-out" parameter`
// When we're "in automation" we omit certain text from the
@ -59,8 +53,8 @@ func TestLocal_planInAutomation(t *testing.T) {
b.RunningInAutomation = false
b.CLI = cli.NewMockUi()
{
op := testOperationPlan()
op.Module = mod
op, configCleanup := testOperationPlan(t, "./test-fixtures/plan")
defer configCleanup()
op.PlanRefresh = true
run, err := b.Operation(context.Background(), op)
@ -68,8 +62,8 @@ func TestLocal_planInAutomation(t *testing.T) {
t.Fatalf("unexpected error: %s", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("unexpected error: %s", err)
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
output := b.CLI.(*cli.MockUi).OutputWriter.String()
@ -83,8 +77,8 @@ func TestLocal_planInAutomation(t *testing.T) {
b.RunningInAutomation = true
b.CLI = cli.NewMockUi()
{
op := testOperationPlan()
op.Module = mod
op, configCleanup := testOperationPlan(t, "./test-fixtures/plan")
defer configCleanup()
op.PlanRefresh = true
run, err := b.Operation(context.Background(), op)
@ -92,8 +86,8 @@ func TestLocal_planInAutomation(t *testing.T) {
t.Fatalf("unexpected error: %s", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("unexpected error: %s", err)
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
output := b.CLI.(*cli.MockUi).OutputWriter.String()
@ -109,8 +103,10 @@ func TestLocal_planNoConfig(t *testing.T) {
defer cleanup()
TestLocalProvider(t, b, "test")
op := testOperationPlan()
op.Module = nil
b.CLI = cli.NewMockUi()
op, configCleanup := testOperationPlan(t, "./test-fixtures/empty")
defer configCleanup()
op.PlanRefresh = true
run, err := b.Operation(context.Background(), op)
@ -119,11 +115,11 @@ func TestLocal_planNoConfig(t *testing.T) {
}
<-run.Done()
err = run.Err
if err == nil {
t.Fatal("should error")
if run.Result == backend.OperationSuccess {
t.Fatal("plan operation succeeded; want failure")
}
if !strings.Contains(err.Error(), "configuration") {
output := b.CLI.(*cli.MockUi).ErrorWriter.String()
if !strings.Contains(output, "configuration") {
t.Fatalf("bad: %s", err)
}
}
@ -134,19 +130,16 @@ func TestLocal_planRefreshFalse(t *testing.T) {
p := TestLocalProvider(t, b, "test")
terraform.TestStateFile(t, b.StatePath, testPlanState())
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
defer modCleanup()
op := testOperationPlan()
op.Module = mod
op, configCleanup := testOperationPlan(t, "./test-fixtures/empty")
defer configCleanup()
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("err: %s", err)
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
if p.RefreshCalled {
@ -164,17 +157,14 @@ func TestLocal_planDestroy(t *testing.T) {
p := TestLocalProvider(t, b, "test")
terraform.TestStateFile(t, b.StatePath, testPlanState())
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
defer modCleanup()
outDir := testTempDir(t)
defer os.RemoveAll(outDir)
planPath := filepath.Join(outDir, "plan.tfplan")
op := testOperationPlan()
op, configCleanup := testOperationPlan(t, "./test-fixtures/plan")
defer configCleanup()
op.Destroy = true
op.PlanRefresh = true
op.Module = mod
op.PlanOutPath = planPath
run, err := b.Operation(context.Background(), op)
@ -182,8 +172,8 @@ func TestLocal_planDestroy(t *testing.T) {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("err: %s", err)
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
if !p.RefreshCalled {
@ -210,15 +200,12 @@ func TestLocal_planOutPathNoChange(t *testing.T) {
TestLocalProvider(t, b, "test")
terraform.TestStateFile(t, b.StatePath, testPlanState())
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
defer modCleanup()
outDir := testTempDir(t)
defer os.RemoveAll(outDir)
planPath := filepath.Join(outDir, "plan.tfplan")
op := testOperationPlan()
op.Module = mod
op, configCleanup := testOperationPlan(t, "./test-fixtures/plan")
defer configCleanup()
op.PlanOutPath = planPath
run, err := b.Operation(context.Background(), op)
@ -226,8 +213,8 @@ func TestLocal_planOutPathNoChange(t *testing.T) {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("err: %s", err)
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
plan := testReadPlan(t, planPath)
@ -273,14 +260,11 @@ func TestLocal_planScaleOutNoDupeCount(t *testing.T) {
actual := new(CountHook)
b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, actual)
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan-scaleout")
defer modCleanup()
outDir := testTempDir(t)
defer os.RemoveAll(outDir)
op := testOperationPlan()
op.Module = mod
op, configCleanup := testOperationPlan(t, "./test-fixtures/plan-scaleout")
defer configCleanup()
op.PlanRefresh = true
run, err := b.Operation(context.Background(), op)
@ -288,8 +272,8 @@ func TestLocal_planScaleOutNoDupeCount(t *testing.T) {
t.Fatalf("bad: %s", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("err: %s", err)
if run.Result != backend.OperationSuccess {
t.Fatalf("plan operation failed")
}
expected := new(CountHook)
@ -304,10 +288,16 @@ func TestLocal_planScaleOutNoDupeCount(t *testing.T) {
}
}
func testOperationPlan() *backend.Operation {
func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func()) {
t.Helper()
_, configLoader, configCleanup := configload.MustLoadConfigForTests(t, configDir)
return &backend.Operation{
Type: backend.OperationTypePlan,
}
Type: backend.OperationTypePlan,
ConfigDir: configDir,
ConfigLoader: configLoader,
}, configCleanup
}
// testPlanState is just a common state that we use for testing refresh.

View File

@ -9,8 +9,8 @@ import (
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
)
func (b *Local) opRefresh(
@ -18,6 +18,9 @@ func (b *Local) opRefresh(
cancelCtx context.Context,
op *backend.Operation,
runningOp *backend.RunningOperation) {
var diags tfdiags.Diagnostics
// Check if our state exists if we're performing a refresh operation. We
// only do this if we're managing state with this backend.
if b.Backend == nil {
@ -27,26 +30,22 @@ func (b *Local) opRefresh(
}
if err != nil {
runningOp.Err = fmt.Errorf(
"There was an error reading the Terraform state that is needed\n"+
"for refreshing. The path and error are shown below.\n\n"+
"Path: %s\n\nError: %s",
b.StatePath, err)
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Cannot read state file",
fmt.Sprintf("Failed to read %s: %s", b.StatePath, err),
))
b.ReportResult(runningOp, diags)
return
}
}
}
// If we have no config module given to use, create an empty tree to
// avoid crashes when Terraform.Context is initialized.
if op.Module == nil {
op.Module = module.NewEmptyTree()
}
// Get our context
tfCtx, opState, err := b.context(op)
if err != nil {
runningOp.Err = err
tfCtx, opState, contextDiags := b.context(op)
diags = diags.Append(contextDiags)
if contextDiags.HasErrors() {
b.ReportResult(runningOp, diags)
return
}
@ -76,17 +75,20 @@ func (b *Local) opRefresh(
// write the resulting state to the running op
runningOp.State = newState
if refreshErr != nil {
runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", refreshErr)
diags = diags.Append(refreshErr)
b.ReportResult(runningOp, diags)
return
}
// Write and persist the state
if err := opState.WriteState(newState); err != nil {
runningOp.Err = errwrap.Wrapf("Error writing state: {{err}}", err)
diags = diags.Append(errwrap.Wrapf("Failed to write state: {{err}}", err))
b.ReportResult(runningOp, diags)
return
}
if err := opState.PersistState(); err != nil {
runningOp.Err = errwrap.Wrapf("Error saving state: {{err}}", err)
diags = diags.Append(errwrap.Wrapf("Failed to save state: {{err}}", err))
b.ReportResult(runningOp, diags)
return
}
}

View File

@ -6,7 +6,7 @@ import (
"testing"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/terraform"
)
@ -20,11 +20,8 @@ func TestLocal_refresh(t *testing.T) {
p.RefreshFn = nil
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
mod, modCleanup := module.TestTree(t, "./test-fixtures/refresh")
defer modCleanup()
op := testOperationRefresh()
op.Module = mod
op, configCleanup := testOperationRefresh(t, "./test-fixtures/refresh")
defer configCleanup()
run, err := b.Operation(context.Background(), op)
if err != nil {
@ -43,7 +40,7 @@ test_instance.foo:
`)
}
func TestLocal_refreshNilModule(t *testing.T) {
func TestLocal_refreshNoConfig(t *testing.T) {
b, cleanup := TestLocal(t)
defer cleanup()
p := TestLocalProvider(t, b, "test")
@ -52,8 +49,8 @@ func TestLocal_refreshNilModule(t *testing.T) {
p.RefreshFn = nil
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
op := testOperationRefresh()
op.Module = nil
op, configCleanup := testOperationRefresh(t, "./test-fixtures/empty")
defer configCleanup()
run, err := b.Operation(context.Background(), op)
if err != nil {
@ -84,8 +81,8 @@ func TestLocal_refreshNilModuleWithInput(t *testing.T) {
b.OpInput = true
op := testOperationRefresh()
op.Module = nil
op, configCleanup := testOperationRefresh(t, "./test-fixtures/empty")
defer configCleanup()
run, err := b.Operation(context.Background(), op)
if err != nil {
@ -121,15 +118,12 @@ func TestLocal_refreshInput(t *testing.T) {
p.RefreshFn = nil
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
mod, modCleanup := module.TestTree(t, "./test-fixtures/refresh-var-unset")
defer modCleanup()
// Enable input asking since it is normally disabled by default
b.OpInput = true
b.ContextOpts.UIInput = &terraform.MockUIInput{InputReturnString: "bar"}
op := testOperationRefresh()
op.Module = mod
op, configCleanup := testOperationRefresh(t, "./test-fixtures/refresh-var-unset")
defer configCleanup()
op.UIIn = b.ContextOpts.UIInput
run, err := b.Operation(context.Background(), op)
@ -158,14 +152,11 @@ func TestLocal_refreshValidate(t *testing.T) {
p.RefreshFn = nil
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
mod, modCleanup := module.TestTree(t, "./test-fixtures/refresh")
defer modCleanup()
// Enable validation
b.OpValidation = true
op := testOperationRefresh()
op.Module = mod
op, configCleanup := testOperationRefresh(t, "./test-fixtures/refresh")
defer configCleanup()
run, err := b.Operation(context.Background(), op)
if err != nil {
@ -184,10 +175,16 @@ test_instance.foo:
`)
}
func testOperationRefresh() *backend.Operation {
func testOperationRefresh(t *testing.T, configDir string) (*backend.Operation, func()) {
t.Helper()
_, configLoader, configCleanup := configload.MustLoadConfigForTests(t, configDir)
return &backend.Operation{
Type: backend.OperationTypeRefresh,
}
Type: backend.OperationTypeRefresh,
ConfigDir: configDir,
ConfigLoader: configLoader,
}, configCleanup
}
// testRefreshState is just a common state that we use for testing refresh.

View File

@ -8,6 +8,7 @@ import (
func (b *Local) CLIInit(opts *backend.CLIOpts) error {
b.CLI = opts.CLI
b.CLIColor = opts.CLIColor
b.ShowDiagnostics = opts.ShowDiagnostics
b.ContextOpts = opts.ContextOpts
b.OpInput = opts.Input
b.OpValidation = opts.Validation

View File

View File

@ -9,6 +9,7 @@ import (
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
)
// TestLocal returns a configured Local struct with temporary paths and
@ -17,15 +18,27 @@ import (
// 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{}
var local *Local
local = &Local{
StatePath: filepath.Join(tempDir, "state.tfstate"),
StateOutPath: filepath.Join(tempDir, "state.tfstate"),
StateBackupPath: filepath.Join(tempDir, "state.tfstate.bak"),
StateWorkspaceDir: filepath.Join(tempDir, "state.tfstate.d"),
ContextOpts: &terraform.ContextOpts{},
ShowDiagnostics: func(vals ...interface{}) {
var diags tfdiags.Diagnostics
diags = diags.Append(vals...)
for _, diag := range diags {
t.Log(diag.Description().Summary)
if local.CLI != nil {
local.CLI.Error(diag.Description().Summary)
}
}
},
}
cleanup := func() {
if err := os.RemoveAll(tempDir); err != nil {
t.Fatal("error clecanup up test:", err)
@ -70,7 +83,7 @@ func TestLocalProvider(t *testing.T, b *Local, name string) *terraform.MockResou
// 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()}
return &TestLocalSingleState{Local: &Local{}}
}
// TestLocalSingleState is a backend implementation that wraps Local
@ -102,7 +115,7 @@ func (b *TestLocalSingleState) DeleteState(string) error {
// 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()}
return &TestLocalNoDefaultState{Local: &Local{}}
}
// TestLocalNoDefaultState is a backend implementation that wraps

View File

@ -1,8 +1,10 @@
package backend
import (
"github.com/hashicorp/terraform/config/configschema"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// Nil is a no-op implementation of Backend.
@ -11,17 +13,15 @@ import (
// backend interface for testing.
type Nil struct{}
func (Nil) Input(
ui terraform.UIInput,
c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
return c, nil
func (Nil) ConfigSchema() *configschema.Block {
return &configschema.Block{}
}
func (Nil) Validate(*terraform.ResourceConfig) ([]string, []error) {
return nil, nil
func (Nil) ValidateConfig(cty.Value) tfdiags.Diagnostics {
return nil
}
func (Nil) Configure(*terraform.ResourceConfig) error {
func (Nil) Configure(cty.Value) tfdiags.Diagnostics {
return nil
}

View File

@ -4,7 +4,9 @@ import (
"testing"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/state/remote"
"github.com/zclconf/go-cty/cty"
)
func TestArtifactoryClient_impl(t *testing.T) {
@ -15,18 +17,18 @@ func TestArtifactoryFactory(t *testing.T) {
// This test just instantiates the client. Shouldn't make any actual
// requests nor incur any costs.
config := make(map[string]interface{})
config["url"] = "http://artifactory.local:8081/artifactory"
config["repo"] = "terraform-repo"
config["subpath"] = "myproject"
config := make(map[string]cty.Value)
config["url"] = cty.StringVal("http://artifactory.local:8081/artifactory")
config["repo"] = cty.StringVal("terraform-repo")
config["subpath"] = cty.StringVal("myproject")
// For this test we'll provide the credentials as config. The
// acceptance tests implicitly test passing credentials as
// environment variables.
config["username"] = "test"
config["password"] = "testpass"
config["username"] = cty.StringVal("test")
config["password"] = cty.StringVal("testpass")
b := backend.TestBackendConfig(t, New(), config)
b := backend.TestBackendConfig(t, New(), configs.SynthBody("synth", config))
state, err := b.State(backend.DefaultStateName)
if err != nil {

View File

@ -40,7 +40,7 @@ func TestBackendConfig(t *testing.T) {
"access_key": "QUNDRVNTX0tFWQ0K",
}
b := backend.TestBackendConfig(t, New(), config).(*Backend)
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend)
if b.containerName != "tfcontainer" {
t.Fatalf("Incorrect bucketName was populated")
@ -57,12 +57,12 @@ func TestBackend(t *testing.T) {
res := setupResources(t, keyName)
defer destroyResources(t, res.resourceGroupName)
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
"container_name": res.containerName,
"key": keyName,
"access_key": res.accessKey,
}).(*Backend)
})).(*Backend)
backend.TestBackendStates(t, b)
}
@ -74,19 +74,19 @@ func TestBackendLocked(t *testing.T) {
res := setupResources(t, keyName)
defer destroyResources(t, res.resourceGroupName)
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
"container_name": res.containerName,
"key": keyName,
"access_key": res.accessKey,
}).(*Backend)
})).(*Backend)
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
"container_name": res.containerName,
"key": keyName,
"access_key": res.accessKey,
}).(*Backend)
})).(*Backend)
backend.TestBackendStateLocks(t, b1, b2)
backend.TestBackendStateForceUnlock(t, b1, b2)

View File

@ -21,12 +21,12 @@ func TestRemoteClient(t *testing.T) {
res := setupResources(t, keyName)
defer destroyResources(t, res.resourceGroupName)
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
"container_name": res.containerName,
"key": keyName,
"access_key": res.accessKey,
}).(*Backend)
})).(*Backend)
state, err := b.State(backend.DefaultStateName)
if err != nil {
@ -43,19 +43,19 @@ func TestRemoteClientLocks(t *testing.T) {
res := setupResources(t, keyName)
defer destroyResources(t, res.resourceGroupName)
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
"container_name": res.containerName,
"key": keyName,
"access_key": res.accessKey,
}).(*Backend)
})).(*Backend)
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
"container_name": res.containerName,
"key": keyName,
"access_key": res.accessKey,
}).(*Backend)
})).(*Backend)
s1, err := b1.State(backend.DefaultStateName)
if err != nil {

View File

@ -10,7 +10,8 @@ import (
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// Backend implements backend.Backend for remote state backends.
@ -30,7 +31,8 @@ type Backend struct {
client remote.Client
}
func (b *Backend) Configure(rc *terraform.ResourceConfig) error {
func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics {
// Set our configureFunc manually
b.Backend.ConfigureFunc = func(ctx context.Context) error {
c, err := b.ConfigureFunc(ctx)
@ -43,8 +45,7 @@ func (b *Backend) Configure(rc *terraform.ResourceConfig) error {
return nil
}
// Run the normal configuration
return b.Backend.Configure(rc)
return b.Backend.Configure(obj)
}
func (b *Backend) States() ([]string, error) {

View File

@ -52,15 +52,15 @@ func TestBackend(t *testing.T) {
path := fmt.Sprintf("tf-unit/%s", time.Now().String())
// Get the backend. We need two to test locking.
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"address": srv.HTTPAddr,
"path": path,
})
}))
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"address": srv.HTTPAddr,
"path": path,
})
}))
// Test
backend.TestBackendStates(t, b1)
@ -71,17 +71,17 @@ func TestBackend_lockDisabled(t *testing.T) {
path := fmt.Sprintf("tf-unit/%s", time.Now().String())
// Get the backend. We need two to test locking.
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"address": srv.HTTPAddr,
"path": path,
"lock": false,
})
}))
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"address": srv.HTTPAddr,
"path": path + "different", // Diff so locking test would fail if it was locking
"lock": false,
})
}))
// Test
backend.TestBackendStates(t, b1)
@ -90,11 +90,11 @@ func TestBackend_lockDisabled(t *testing.T) {
func TestBackend_gzip(t *testing.T) {
// Get the backend
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"address": srv.HTTPAddr,
"path": fmt.Sprintf("tf-unit/%s", time.Now().String()),
"gzip": true,
})
}))
// Test
backend.TestBackendStates(t, b)

View File

@ -20,10 +20,10 @@ func TestRemoteClient_impl(t *testing.T) {
func TestRemoteClient(t *testing.T) {
// Get the backend
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"address": srv.HTTPAddr,
"path": fmt.Sprintf("tf-unit/%s", time.Now().String()),
})
}))
// Grab the client
state, err := b.State(backend.DefaultStateName)
@ -40,10 +40,10 @@ func TestRemoteClient_gzipUpgrade(t *testing.T) {
statePath := fmt.Sprintf("tf-unit/%s", time.Now().String())
// Get the backend
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"address": srv.HTTPAddr,
"path": statePath,
})
}))
// Grab the client
state, err := b.State(backend.DefaultStateName)
@ -55,11 +55,11 @@ func TestRemoteClient_gzipUpgrade(t *testing.T) {
remote.TestClient(t, state.(*remote.State).Client)
// create a new backend with gzip
b = backend.TestBackendConfig(t, New(), map[string]interface{}{
b = backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"address": srv.HTTPAddr,
"path": statePath,
"gzip": true,
})
}))
// Grab the client
state, err = b.State(backend.DefaultStateName)
@ -75,18 +75,18 @@ func TestConsul_stateLock(t *testing.T) {
path := fmt.Sprintf("tf-unit/%s", time.Now().String())
// create 2 instances to get 2 remote.Clients
sA, err := backend.TestBackendConfig(t, New(), map[string]interface{}{
sA, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"address": srv.HTTPAddr,
"path": path,
}).State(backend.DefaultStateName)
})).State(backend.DefaultStateName)
if err != nil {
t.Fatal(err)
}
sB, err := backend.TestBackendConfig(t, New(), map[string]interface{}{
sB, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"address": srv.HTTPAddr,
"path": path,
}).State(backend.DefaultStateName)
})).State(backend.DefaultStateName)
if err != nil {
t.Fatal(err)
}
@ -96,10 +96,10 @@ func TestConsul_stateLock(t *testing.T) {
func TestConsul_destroyLock(t *testing.T) {
// Get the backend
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"address": srv.HTTPAddr,
"path": fmt.Sprintf("tf-unit/%s", time.Now().String()),
})
}))
// Grab the client
s, err := b.State(backend.DefaultStateName)
@ -135,18 +135,18 @@ func TestConsul_lostLock(t *testing.T) {
path := fmt.Sprintf("tf-unit/%s", time.Now().String())
// create 2 instances to get 2 remote.Clients
sA, err := backend.TestBackendConfig(t, New(), map[string]interface{}{
sA, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"address": srv.HTTPAddr,
"path": path,
}).State(backend.DefaultStateName)
})).State(backend.DefaultStateName)
if err != nil {
t.Fatal(err)
}
sB, err := backend.TestBackendConfig(t, New(), map[string]interface{}{
sB, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"address": srv.HTTPAddr,
"path": path + "-not-used",
}).State(backend.DefaultStateName)
})).State(backend.DefaultStateName)
if err != nil {
t.Fatal(err)
}
@ -190,10 +190,10 @@ func TestConsul_lostLockConnection(t *testing.T) {
path := fmt.Sprintf("tf-unit/%s", time.Now().String())
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"address": srv.HTTPAddr,
"path": path,
})
}))
s, err := b.State(backend.DefaultStateName)
if err != nil {

View File

@ -7,7 +7,9 @@ import (
"time"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/state/remote"
"github.com/zclconf/go-cty/cty"
)
func TestEtcdClient_impl(t *testing.T) {
@ -21,19 +23,19 @@ func TestEtcdClient(t *testing.T) {
}
// Get the backend
config := map[string]interface{}{
"endpoints": endpoint,
"path": fmt.Sprintf("tf-unit/%s", time.Now().String()),
config := map[string]cty.Value{
"endpoints": cty.StringVal(endpoint),
"path": cty.StringVal(fmt.Sprintf("tf-unit/%s", time.Now().String())),
}
if username := os.Getenv("ETCD_USERNAME"); username != "" {
config["username"] = username
config["username"] = cty.StringVal(username)
}
if password := os.Getenv("ETCD_PASSWORD"); password != "" {
config["password"] = password
config["password"] = cty.StringVal(password)
}
b := backend.TestBackendConfig(t, New(), config)
b := backend.TestBackendConfig(t, New(), configs.SynthBody("synth", config))
state, err := b.State(backend.DefaultStateName)
if err != nil {
t.Fatalf("Error for valid config: %s", err)

View File

@ -55,15 +55,15 @@ func TestBackend(t *testing.T) {
prefix := fmt.Sprintf("%s/%s/", keyPrefix, time.Now().Format(time.RFC3339))
// Get the backend. We need two to test locking.
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"endpoints": etcdv3Endpoints,
"prefix": prefix,
})
}))
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"endpoints": etcdv3Endpoints,
"prefix": prefix,
})
}))
// Test
backend.TestBackendStates(t, b1)
@ -78,17 +78,17 @@ func TestBackend_lockDisabled(t *testing.T) {
prefix := fmt.Sprintf("%s/%s/", keyPrefix, time.Now().Format(time.RFC3339))
// Get the backend. We need two to test locking.
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"endpoints": etcdv3Endpoints,
"prefix": prefix,
"lock": false,
})
}))
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"endpoints": etcdv3Endpoints,
"prefix": prefix + "/" + "different", // Diff so locking test would fail if it was locking
"lock": false,
})
}))
// Test
backend.TestBackendStateLocks(t, b1, b2)

View File

@ -22,10 +22,10 @@ func TestRemoteClient(t *testing.T) {
prefix := fmt.Sprintf("%s/%s/", keyPrefix, time.Now().Format(time.RFC3339))
// Get the backend
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"endpoints": etcdv3Endpoints,
"prefix": prefix,
})
}))
// Grab the client
state, err := b.State(backend.DefaultStateName)
@ -44,18 +44,18 @@ func TestEtcdv3_stateLock(t *testing.T) {
prefix := fmt.Sprintf("%s/%s/", keyPrefix, time.Now().Format(time.RFC3339))
// Get the backend
s1, err := backend.TestBackendConfig(t, New(), map[string]interface{}{
s1, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"endpoints": etcdv3Endpoints,
"prefix": prefix,
}).State(backend.DefaultStateName)
})).State(backend.DefaultStateName)
if err != nil {
t.Fatal(err)
}
s2, err := backend.TestBackendConfig(t, New(), map[string]interface{}{
s2, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"endpoints": etcdv3Endpoints,
"prefix": prefix,
}).State(backend.DefaultStateName)
})).State(backend.DefaultStateName)
if err != nil {
t.Fatal(err)
}
@ -70,10 +70,10 @@ func TestEtcdv3_destroyLock(t *testing.T) {
prefix := fmt.Sprintf("%s/%s/", keyPrefix, time.Now().Format(time.RFC3339))
// Get the backend
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"endpoints": etcdv3Endpoints,
"prefix": prefix,
})
}))
// Grab the client
s, err := b.State(backend.DefaultStateName)

View File

@ -187,7 +187,7 @@ func setupBackend(t *testing.T, bucket, prefix, key string) backend.Backend {
"encryption_key": key,
}
b := backend.TestBackendConfig(t, New(), config)
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config))
be := b.(*Backend)
// create the bucket if it doesn't exist

View File

@ -3,6 +3,9 @@ package http
import (
"testing"
"github.com/hashicorp/terraform/configs"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/backend"
)
@ -13,16 +16,16 @@ func TestBackend_impl(t *testing.T) {
func TestHTTPClientFactory(t *testing.T) {
// defaults
conf := map[string]interface{}{
"address": "http://127.0.0.1:8888/foo",
conf := map[string]cty.Value{
"address": cty.StringVal("http://127.0.0.1:8888/foo"),
}
b := backend.TestBackendConfig(t, New(), conf).(*Backend)
b := backend.TestBackendConfig(t, New(), configs.SynthBody("synth", conf)).(*Backend)
client := b.client
if client == nil {
t.Fatal("Unexpected failure, address")
}
if client.URL.String() != conf["address"] {
if client.URL.String() != "http://127.0.0.1:8888/foo" {
t.Fatalf("Expected address \"%s\", got \"%s\"", conf["address"], client.URL.String())
}
if client.UpdateMethod != "POST" {
@ -39,18 +42,18 @@ func TestHTTPClientFactory(t *testing.T) {
}
// custom
conf = map[string]interface{}{
"address": "http://127.0.0.1:8888/foo",
"update_method": "BLAH",
"lock_address": "http://127.0.0.1:8888/bar",
"lock_method": "BLIP",
"unlock_address": "http://127.0.0.1:8888/baz",
"unlock_method": "BLOOP",
"username": "user",
"password": "pass",
conf = map[string]cty.Value{
"address": cty.StringVal("http://127.0.0.1:8888/foo"),
"update_method": cty.StringVal("BLAH"),
"lock_address": cty.StringVal("http://127.0.0.1:8888/bar"),
"lock_method": cty.StringVal("BLIP"),
"unlock_address": cty.StringVal("http://127.0.0.1:8888/baz"),
"unlock_method": cty.StringVal("BLOOP"),
"username": cty.StringVal("user"),
"password": cty.StringVal("pass"),
}
b = backend.TestBackendConfig(t, New(), conf).(*Backend)
b = backend.TestBackendConfig(t, New(), configs.SynthBody("synth", conf)).(*Backend)
client = b.client
if client == nil {
@ -59,13 +62,13 @@ func TestHTTPClientFactory(t *testing.T) {
if client.UpdateMethod != "BLAH" {
t.Fatalf("Expected update_method \"%s\", got \"%s\"", "BLAH", client.UpdateMethod)
}
if client.LockURL.String() != conf["lock_address"] || client.LockMethod != "BLIP" {
if client.LockURL.String() != conf["lock_address"].AsString() || client.LockMethod != "BLIP" {
t.Fatalf("Unexpected lock_address \"%s\" vs \"%s\" or lock_method \"%s\" vs \"%s\"", client.LockURL.String(),
conf["lock_address"], client.LockMethod, conf["lock_method"])
conf["lock_address"].AsString(), client.LockMethod, conf["lock_method"])
}
if client.UnlockURL.String() != conf["unlock_address"] || client.UnlockMethod != "BLOOP" {
if client.UnlockURL.String() != conf["unlock_address"].AsString() || client.UnlockMethod != "BLOOP" {
t.Fatalf("Unexpected unlock_address \"%s\" vs \"%s\" or unlock_method \"%s\" vs \"%s\"", client.UnlockURL.String(),
conf["unlock_address"], client.UnlockMethod, conf["unlock_method"])
conf["unlock_address"].AsString(), client.UnlockMethod, conf["unlock_method"])
}
if client.Username != "user" || client.Password != "pass" {
t.Fatalf("Unexpected username \"%s\" vs \"%s\" or password \"%s\" vs \"%s\"", client.Username, conf["username"],

View File

@ -3,6 +3,7 @@ package inmem
import (
"testing"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
@ -20,7 +21,7 @@ func TestBackendConfig(t *testing.T) {
"lock_id": testID,
}
b := backend.TestBackendConfig(t, New(), config).(*Backend)
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend)
s, err := b.State(backend.DefaultStateName)
if err != nil {
@ -39,14 +40,14 @@ func TestBackendConfig(t *testing.T) {
func TestBackend(t *testing.T) {
defer Reset()
b := backend.TestBackendConfig(t, New(), nil).(*Backend)
b := backend.TestBackendConfig(t, New(), hcl.EmptyBody()).(*Backend)
backend.TestBackendStates(t, b)
}
func TestBackendLocked(t *testing.T) {
defer Reset()
b1 := backend.TestBackendConfig(t, New(), nil).(*Backend)
b2 := backend.TestBackendConfig(t, New(), nil).(*Backend)
b1 := backend.TestBackendConfig(t, New(), hcl.EmptyBody()).(*Backend)
b2 := backend.TestBackendConfig(t, New(), hcl.EmptyBody()).(*Backend)
backend.TestBackendStateLocks(t, b1, b2)
}
@ -54,7 +55,7 @@ func TestBackendLocked(t *testing.T) {
// use the this backen to test the remote.State implementation
func TestRemoteState(t *testing.T) {
defer Reset()
b := backend.TestBackendConfig(t, New(), nil)
b := backend.TestBackendConfig(t, New(), hcl.EmptyBody())
workspace := "workspace"

View File

@ -3,6 +3,7 @@ package inmem
import (
"testing"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/state/remote"
)
@ -14,7 +15,7 @@ func TestRemoteClient_impl(t *testing.T) {
func TestRemoteClient(t *testing.T) {
defer Reset()
b := backend.TestBackendConfig(t, New(), nil)
b := backend.TestBackendConfig(t, New(), hcl.EmptyBody())
s, err := b.State(backend.DefaultStateName)
if err != nil {
@ -26,7 +27,7 @@ func TestRemoteClient(t *testing.T) {
func TestInmemLocks(t *testing.T) {
defer Reset()
s, err := backend.TestBackendConfig(t, New(), nil).State(backend.DefaultStateName)
s, err := backend.TestBackendConfig(t, New(), hcl.EmptyBody()).State(backend.DefaultStateName)
if err != nil {
t.Fatal(err)
}

View File

@ -30,10 +30,10 @@ func TestBackend(t *testing.T) {
directory := fmt.Sprintf("terraform-remote-manta-test-%x", time.Now().Unix())
keyName := "testState"
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
"path": directory,
"object_name": keyName,
}).(*Backend)
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"path": directory,
"objectName": keyName,
})).(*Backend)
createMantaFolder(t, b.storageClient, directory)
defer deleteMantaFolder(t, b.storageClient, directory)
@ -47,15 +47,15 @@ func TestBackendLocked(t *testing.T) {
directory := fmt.Sprintf("terraform-remote-manta-test-%x", time.Now().Unix())
keyName := "testState"
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"path": directory,
"object_name": keyName,
}).(*Backend)
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"path": directory,
"objectName": keyName,
})).(*Backend)
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"path": directory,
"object_name": keyName,
}).(*Backend)
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"path": directory,
"objectName": keyName,
})).(*Backend)
createMantaFolder(t, b1.storageClient, directory)
defer deleteMantaFolder(t, b1.storageClient, directory)

View File

@ -20,10 +20,10 @@ func TestRemoteClient(t *testing.T) {
directory := fmt.Sprintf("terraform-remote-manta-test-%x", time.Now().Unix())
keyName := "testState"
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
"path": directory,
"object_name": keyName,
}).(*Backend)
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"path": directory,
"objectName": keyName,
})).(*Backend)
createMantaFolder(t, b.storageClient, directory)
defer deleteMantaFolder(t, b.storageClient, directory)
@ -41,15 +41,15 @@ func TestRemoteClientLocks(t *testing.T) {
directory := fmt.Sprintf("terraform-remote-manta-test-%x", time.Now().Unix())
keyName := "testState"
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"path": directory,
"object_name": keyName,
}).(*Backend)
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"path": directory,
"objectName": keyName,
})).(*Backend)
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"path": directory,
"object_name": keyName,
}).(*Backend)
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"path": directory,
"objectName": keyName,
})).(*Backend)
createMantaFolder(t, b1.storageClient, directory)
defer deleteMantaFolder(t, b1.storageClient, directory)

View File

@ -11,7 +11,7 @@ import (
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/hcl2shim"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
)
@ -42,7 +42,7 @@ func TestBackendConfig(t *testing.T) {
"dynamodb_table": "dynamoTable",
}
b := backend.TestBackendConfig(t, New(), config).(*Backend)
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend)
if *b.s3Client.Config.Region != "us-west-1" {
t.Fatalf("Incorrect region was populated")
@ -68,22 +68,16 @@ func TestBackendConfig(t *testing.T) {
func TestBackendConfig_invalidKey(t *testing.T) {
testACC(t)
cfg := map[string]interface{}{
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{
"region": "us-west-1",
"bucket": "tf-test",
"key": "/leading-slash",
"encrypt": true,
"dynamodb_table": "dynamoTable",
}
})
rawCfg, err := config.NewRawConfig(cfg)
if err != nil {
t.Fatal(err)
}
resCfg := terraform.NewResourceConfig(rawCfg)
_, errs := New().Validate(resCfg)
if len(errs) != 1 {
diags := New().ValidateConfig(cfg)
if !diags.HasErrors() {
t.Fatal("expected config validation error")
}
}
@ -94,11 +88,11 @@ func TestBackend(t *testing.T) {
bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
keyName := "testState"
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"encrypt": true,
}).(*Backend)
})).(*Backend)
createS3Bucket(t, b.s3Client, bucketName)
defer deleteS3Bucket(t, b.s3Client, bucketName)
@ -112,19 +106,19 @@ func TestBackendLocked(t *testing.T) {
bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
keyName := "test/state"
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"encrypt": true,
"dynamodb_table": bucketName,
}).(*Backend)
})).(*Backend)
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"encrypt": true,
"dynamodb_table": bucketName,
}).(*Backend)
})).(*Backend)
createS3Bucket(t, b1.s3Client, bucketName)
defer deleteS3Bucket(t, b1.s3Client, bucketName)
@ -141,11 +135,11 @@ func TestBackendExtraPaths(t *testing.T) {
bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
keyName := "test/state/tfstate"
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"encrypt": true,
}).(*Backend)
})).(*Backend)
createS3Bucket(t, b.s3Client, bucketName)
defer deleteS3Bucket(t, b.s3Client, bucketName)
@ -256,11 +250,11 @@ func TestBackendPrefixInWorkspace(t *testing.T) {
testACC(t)
bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
"bucket": bucketName,
"key": "test-env.tfstate",
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": "test-env.tfstate",
"workspace_key_prefix": "env",
}).(*Backend)
})).(*Backend)
createS3Bucket(t, b.s3Client, bucketName)
defer deleteS3Bucket(t, b.s3Client, bucketName)
@ -284,33 +278,33 @@ func TestKeyEnv(t *testing.T) {
keyName := "some/paths/tfstate"
bucket0Name := fmt.Sprintf("terraform-remote-s3-test-%x-0", time.Now().Unix())
b0 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b0 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucket0Name,
"key": keyName,
"encrypt": true,
"workspace_key_prefix": "",
}).(*Backend)
})).(*Backend)
createS3Bucket(t, b0.s3Client, bucket0Name)
defer deleteS3Bucket(t, b0.s3Client, bucket0Name)
bucket1Name := fmt.Sprintf("terraform-remote-s3-test-%x-1", time.Now().Unix())
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucket1Name,
"key": keyName,
"encrypt": true,
"workspace_key_prefix": "project/env:",
}).(*Backend)
})).(*Backend)
createS3Bucket(t, b1.s3Client, bucket1Name)
defer deleteS3Bucket(t, b1.s3Client, bucket1Name)
bucket2Name := fmt.Sprintf("terraform-remote-s3-test-%x-2", time.Now().Unix())
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucket2Name,
"key": keyName,
"encrypt": true,
}).(*Backend)
})).(*Backend)
createS3Bucket(t, b2.s3Client, bucket2Name)
defer deleteS3Bucket(t, b2.s3Client, bucket2Name)

View File

@ -24,11 +24,11 @@ func TestRemoteClient(t *testing.T) {
bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
keyName := "testState"
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"encrypt": true,
}).(*Backend)
})).(*Backend)
createS3Bucket(t, b.s3Client, bucketName)
defer deleteS3Bucket(t, b.s3Client, bucketName)
@ -46,19 +46,19 @@ func TestRemoteClientLocks(t *testing.T) {
bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
keyName := "testState"
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"encrypt": true,
"dynamodb_table": bucketName,
}).(*Backend)
})).(*Backend)
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"encrypt": true,
"dynamodb_table": bucketName,
}).(*Backend)
})).(*Backend)
createS3Bucket(t, b1.s3Client, bucketName)
defer deleteS3Bucket(t, b1.s3Client, bucketName)
@ -84,19 +84,19 @@ func TestForceUnlock(t *testing.T) {
bucketName := fmt.Sprintf("terraform-remote-s3-test-force-%x", time.Now().Unix())
keyName := "testState"
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"encrypt": true,
"dynamodb_table": bucketName,
}).(*Backend)
})).(*Backend)
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"encrypt": true,
"dynamodb_table": bucketName,
}).(*Backend)
})).(*Backend)
createS3Bucket(t, b1.s3Client, bucketName)
defer deleteS3Bucket(t, b1.s3Client, bucketName)
@ -161,11 +161,11 @@ func TestRemoteClient_clientMD5(t *testing.T) {
bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
keyName := "testState"
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"dynamodb_table": bucketName,
}).(*Backend)
})).(*Backend)
createS3Bucket(t, b.s3Client, bucketName)
defer deleteS3Bucket(t, b.s3Client, bucketName)
@ -209,11 +209,11 @@ func TestRemoteClient_stateChecksum(t *testing.T) {
bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
keyName := "testState"
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"dynamodb_table": bucketName,
}).(*Backend)
})).(*Backend)
createS3Bucket(t, b1.s3Client, bucketName)
defer deleteS3Bucket(t, b1.s3Client, bucketName)
@ -240,10 +240,10 @@ func TestRemoteClient_stateChecksum(t *testing.T) {
// Use b2 without a dynamodb_table to bypass the lock table to write the state directly.
// client2 will write the "incorrect" state, simulating s3 eventually consistency delays
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
}).(*Backend)
})).(*Backend)
s2, err := b2.State(backend.DefaultStateName)
if err != nil {
t.Fatal(err)

View File

@ -46,7 +46,7 @@ func TestBackendConfig(t *testing.T) {
"container": "test-tfstate",
}
b := backend.TestBackendConfig(t, New(), config).(*Backend)
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend)
if b.container != "test-tfstate" {
t.Fatal("Incorrect path was provided.")
@ -61,9 +61,9 @@ func TestBackend(t *testing.T) {
container := fmt.Sprintf("terraform-state-swift-test-%x", time.Now().Unix())
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"container": container,
}).(*Backend)
})).(*Backend)
defer deleteSwiftContainer(t, b.client, container)
@ -75,9 +75,9 @@ func TestBackendPath(t *testing.T) {
path := fmt.Sprintf("terraform-state-swift-test-%x", time.Now().Unix())
t.Logf("[DEBUG] Generating backend config")
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"path": path,
}).(*Backend)
})).(*Backend)
t.Logf("[DEBUG] Backend configured")
defer deleteSwiftContainer(t, b.client, path)
@ -131,10 +131,10 @@ func TestBackendArchive(t *testing.T) {
container := fmt.Sprintf("terraform-state-swift-test-%x", time.Now().Unix())
archiveContainer := fmt.Sprintf("%s_archive", container)
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"archive_container": archiveContainer,
"container": container,
}).(*Backend)
})).(*Backend)
defer deleteSwiftContainer(t, b.client, container)
defer deleteSwiftContainer(t, b.client, archiveContainer)

View File

@ -18,9 +18,9 @@ func TestRemoteClient(t *testing.T) {
container := fmt.Sprintf("terraform-state-swift-test-%x", time.Now().Unix())
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"container": container,
}).(*Backend)
})).(*Backend)
state, err := b.State(backend.DefaultStateName)
if err != nil {

View File

@ -5,41 +5,61 @@ import (
"sort"
"testing"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/config/hcl2shim"
"github.com/hashicorp/terraform/tfdiags"
"github.com/hashicorp/hcl2/hcldec"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
)
// TestBackendConfig validates and configures the backend with the
// given configuration.
func TestBackendConfig(t *testing.T, b Backend, c map[string]interface{}) Backend {
func TestBackendConfig(t *testing.T, b Backend, c hcl.Body) Backend {
t.Helper()
// Get the proper config structure
rc, err := config.NewRawConfig(c)
if err != nil {
t.Fatalf("bad: %s", err)
}
conf := terraform.NewResourceConfig(rc)
t.Logf("TestBackendConfig on %T with %#v", b, c)
// Validate
warns, errs := b.Validate(conf)
if len(warns) > 0 {
t.Fatalf("warnings: %s", warns)
}
if len(errs) > 0 {
t.Fatalf("errors: %s", errs)
var diags tfdiags.Diagnostics
schema := b.ConfigSchema()
spec := schema.DecoderSpec()
obj, decDiags := hcldec.Decode(c, spec, nil)
diags = diags.Append(decDiags)
valDiags := b.ValidateConfig(obj)
diags = diags.Append(valDiags.InConfigBody(c))
if len(diags) != 0 {
t.Fatal(diags)
}
// Configure
if err := b.Configure(conf); err != nil {
t.Fatalf("err: %s", err)
confDiags := b.Configure(obj)
if len(confDiags) != 0 {
confDiags = confDiags.InConfigBody(c)
t.Fatal(confDiags)
}
return b
}
// TestWrapConfig takes a raw data structure and converts it into a
// synthetic hcl.Body to use for testing.
//
// The given structure should only include values that can be accepted by
// hcl2shim.HCL2ValueFromConfigValue. If incompatible values are given,
// this function will panic.
func TestWrapConfig(raw map[string]interface{}) hcl.Body {
obj := hcl2shim.HCL2ValueFromConfigValue(raw)
return configs.SynthBody("<TestWrapConfig>", obj.AsValueMap())
}
// TestBackend will test the functionality of a Backend. The backend is
// assumed to already be configured. This will test state functionality.
// If the backend reports it doesn't support multi-state by returning the

View File

@ -1,497 +1,116 @@
package terraform
import (
"fmt"
"log"
"time"
"fmt"
"log"
"time"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/backend"
backendInit "github.com/hashicorp/terraform/backend/init"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/backend"
backendinit "github.com/hashicorp/terraform/backend/init"
"github.com/hashicorp/terraform/config/hcl2shim"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/tfdiags"
)
func dataSourceRemoteState() *schema.Resource {
return &schema.Resource{
Read: dataSourceRemoteStateRead,
return &schema.Resource{
Read: dataSourceRemoteStateRead,
Schema: map[string]*schema.Schema{
"backend": {
Type: schema.TypeString,
Required: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
if vStr, ok := v.(string); ok && vStr == "_local" {
ws = append(ws, "Use of the %q backend is now officially "+
"supported as %q. Please update your configuration to ensure "+
"compatibility with future versions of Terraform.",
"_local", "local")
}
Schema: map[string]*schema.Schema{
"backend": {
Type: schema.TypeString,
Required: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
if vStr, ok := v.(string); ok && vStr == "_local" {
ws = append(ws, "Use of the %q backend is now officially "+
"supported as %q. Please update your configuration to ensure "+
"compatibility with future versions of Terraform.",
"_local", "local")
}
return
},
},
return
},
},
// This field now contains all possible attributes that are supported
// by any of the existing backends. When merging this into 0.12 this
// should be reverted and instead the new 'cty.DynamicPseudoType' type
// should be used to make this work with any future backends as well.
"config": {
Type: schema.TypeSet,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"path": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"hostname": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"organization": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"workspaces": &schema.Schema{
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Schema{Type: schema.TypeMap},
},
"username": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"password": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"url": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"repo": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"subpath": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"storage_account_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"container_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"key": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"access_key": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"environment": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"resource_group_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"arm_subscription_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"arm_client_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"arm_client_secret": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"arm_tenant_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"access_token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"scheme": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"datacenter": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"http_auth": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"gzip": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"lock": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"ca_file": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"cert_file": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"key_file": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"endpoints": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"prefix": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"cacert_path": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"cert_path": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"key_path": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"bucket": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"credentials": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"project": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"region": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"encryption_key": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"update_method": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"lock_address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"lock_method": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"unlock_address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"unlock_method": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"skip_cert_verification": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"account": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"user": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"key_material": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"key_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"insecure_skip_tls_verify": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"object_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"endpoint": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"encrypt": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"acl": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"secret_key": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"kms_key_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"lock_table": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"dynamodb_table": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"profile": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"shared_credentials_file": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"role_arn": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"assume_role_policy": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"external_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"session_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"workspace_key_prefix": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"skip_credentials_validation": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"skip_get_ec2_platforms": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"skip_region_validation": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"skip_requesting_account_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"skip_metadata_api_check": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"auth_url": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"container": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"user_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"user_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"region_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"tenant_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"tenant_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"domain_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"domain_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"insecure": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"cacert_file": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"cert": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"archive_container": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"archive_path": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"expire_after": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
},
},
"config": {
Type: schema.TypeMap,
Optional: true,
},
"defaults": {
Type: schema.TypeMap,
Optional: true,
},
"defaults": {
Type: schema.TypeMap,
Optional: true,
},
"environment": {
Type: schema.TypeString,
Optional: true,
Default: backend.DefaultStateName,
Deprecated: "Terraform environments are now called workspaces. Please use the workspace key instead.",
},
"environment": {
Type: schema.TypeString,
Optional: true,
Default: backend.DefaultStateName,
Deprecated: "Terraform environments are now called workspaces. Please use the workspace key instead.",
},
"workspace": {
Type: schema.TypeString,
Optional: true,
Default: backend.DefaultStateName,
},
"workspace": {
Type: schema.TypeString,
Optional: true,
Default: backend.DefaultStateName,
},
"__has_dynamic_attributes": {
Type: schema.TypeString,
Optional: true,
},
},
}
"__has_dynamic_attributes": {
Type: schema.TypeString,
Optional: true,
},
},
}
}
func dataSourceRemoteStateRead(d *schema.ResourceData, meta interface{}) error {
backendType := d.Get("backend").(string)
backendType := d.Get("backend").(string)
// Get the configuration in a type we want. This is a bit of a hack but makes
// things work for the 'remote' backend as well. This can simply be deleted or
// reverted when merging this 0.12.
raw := make(map[string]interface{})
if cfg, ok := d.GetOk("config"); ok {
if raw, ok = cfg.(*schema.Set).List()[0].(map[string]interface{}); ok {
for k, v := range raw {
switch v := v.(type) {
case string:
if v == "" {
delete(raw, k)
}
case []interface{}:
if len(v) == 0 {
delete(raw, k)
}
}
}
}
}
rawConfig, err := config.NewRawConfig(raw)
if err != nil {
return fmt.Errorf("error initializing backend: %s", err)
}
// Don't break people using the old _local syntax - but note warning above
if backendType == "_local" {
log.Println(`[INFO] Switching old (unsupported) backend "_local" to "local"`)
backendType = "local"
// Don't break people using the old _local syntax - but note warning above
if backendType == "_local" {
log.Println(`[INFO] Switching old (unsupported) backend "_local" to "local"`)
backendType = "local"
}
// Create the client to access our remote state
log.Printf("[DEBUG] Initializing remote state backend: %s", backendType)
f := backendInit.Backend(backendType)
f := backendinit.Backend(backendType)
if f == nil {
return fmt.Errorf("Unknown backend type: %s", backendType)
return fmt.Errorf("Unknown backend type: %s", backendType)
}
b := f()
warns, errs := b.Validate(terraform.NewResourceConfig(rawConfig))
for _, warning := range warns {
log.Printf("[DEBUG] Warning validating backend config: %s", warning)
}
if len(errs) > 0 {
return fmt.Errorf("error validating backend config: %s", multierror.Append(nil, errs...))
}
schema := b.ConfigSchema()
rawConfig := d.Get("config")
configVal := hcl2shim.HCL2ValueFromConfigValue(rawConfig)
// Configure the backend
if err := b.Configure(terraform.NewResourceConfig(rawConfig)); err != nil {
return fmt.Errorf("error initializing backend: %s", err)
// Try to coerce the provided value into the desired configuration type.
configVal, err := schema.CoerceValue(configVal)
if err != nil {
return fmt.Errorf("invalid %s backend configuration: %s", backendType, tfdiags.FormatError(err))
}
validateDiags := b.ValidateConfig(configVal)
if validateDiags.HasErrors() {
return validateDiags.Err()
}
configureDiags := b.Configure(configVal)
if configureDiags.HasErrors() {
return configureDiags.Err()
}
// environment is deprecated in favour of workspace.
// If both keys are set workspace should win.
name := d.Get("environment").(string)
if ws, ok := d.GetOk("workspace"); ok && ws != backend.DefaultStateName {
name = ws.(string)
name = ws.(string)
}
state, err := b.State(name)
if err != nil {
return fmt.Errorf("error loading the remote state: %s", err)
return fmt.Errorf("error loading the remote state: %s", err)
}
if err := state.RefreshState(); err != nil {
return err
return err
}
d.SetId(time.Now().UTC().String())
@ -499,24 +118,24 @@ func dataSourceRemoteStateRead(d *schema.ResourceData, meta interface{}) error {
defaults := d.Get("defaults").(map[string]interface{})
for key, val := range defaults {
outputMap[key] = val
outputMap[key] = val
}
remoteState := state.State()
if remoteState.Empty() {
log.Println("[DEBUG] empty remote state")
log.Println("[DEBUG] empty remote state")
} else {
for key, val := range remoteState.RootModule().Outputs {
if val.Value != nil {
outputMap[key] = val.Value
for key, val := range remoteState.RootModule().Outputs {
if val.Value != nil {
outputMap[key] = val.Value
}
}
}
}
mappedOutputs := remoteStateFlatten(outputMap)
for key, val := range mappedOutputs {
d.UnsafeSetFieldRaw(key, val)
d.UnsafeSetFieldRaw(key, val)
}
return nil

View File

@ -17,6 +17,7 @@ import (
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/command/clistate"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/mapstructure"
@ -27,9 +28,9 @@ import (
// BackendOpts are the options used to initialize a backend.Backend.
type BackendOpts struct {
// Module is the root module from which we will extract the terraform and
// backend configuration.
Config *config.Config
// Config is a representation of the backend configuration block given in
// the root module, or nil if no such block is present.
Config *configs.Backend
// ConfigFile is a path to a file that contains configuration that
// is merged directly into the backend configuration when loaded

View File

@ -80,6 +80,25 @@ func (m *Meta) loadSingleModule(dir string) (*configs.Module, tfdiags.Diagnostic
return module, diags
}
// loadBackendConfig reads configuration from the given directory and returns
// the backend configuration defined by that module, if any. Nil is returned
// if the specified module does not have an explicit backend configuration.
//
// This is a convenience method for command code that will delegate to the
// configured backend to do most of its work, since in that case it is the
// backend that will do the full configuration load.
//
// Although this method returns only the backend configuration, at present it
// actually loads and validates the entire configuration first. Therefore errors
// returned may be about other aspects of the configuration. This behavior may
// change in future, so callers must not rely on it. (That is, they must expect
// that a call to loadSingleModule or loadConfig could fail on the same
// directory even if loadBackendConfig succeeded.)
func (m *Meta) loadBackendConfig(rootDir string) (*configs.Backend, tfdiags.Diagnostics) {
mod, diags := m.loadSingleModule(rootDir)
return mod.Backend, diags
}
// installModules reads a root module from the given directory and attempts
// recursively install all of its descendent modules.
//

View File

@ -3,6 +3,11 @@ package schema
import (
"context"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/config/configschema"
"github.com/hashicorp/terraform/config/hcl2shim"
"github.com/hashicorp/terraform/terraform"
)
@ -38,41 +43,50 @@ func FromContextBackendConfig(ctx context.Context) *ResourceData {
return ctx.Value(backendConfigKey).(*ResourceData)
}
func (b *Backend) Input(
input terraform.UIInput,
c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
if b == nil {
return c, nil
}
return schemaMap(b.Schema).Input(input, c)
func (b *Backend) ConfigSchema() *configschema.Block {
// This is an alias of CoreConfigSchema just to implement the
// backend.Backend interface.
return b.CoreConfigSchema()
}
func (b *Backend) Validate(c *terraform.ResourceConfig) ([]string, []error) {
if b == nil {
return nil, nil
}
return schemaMap(b.Schema).Validate(c)
}
func (b *Backend) Configure(c *terraform.ResourceConfig) error {
func (b *Backend) ValidateConfig(obj cty.Value) tfdiags.Diagnostics {
if b == nil {
return nil
}
var diags tfdiags.Diagnostics
shimRC := b.shimConfig(obj)
warns, errs := schemaMap(b.Schema).Validate(shimRC)
for _, warn := range warns {
diags = diags.Append(tfdiags.SimpleWarning(warn))
}
for _, err := range errs {
diags = diags.Append(err)
}
return diags
}
func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics {
if b == nil {
return nil
}
var diags tfdiags.Diagnostics
sm := schemaMap(b.Schema)
shimRC := b.shimConfig(obj)
// Get a ResourceData for this configuration. To do this, we actually
// generate an intermediary "diff" although that is never exposed.
diff, err := sm.Diff(nil, c, nil, nil)
diff, err := sm.Diff(nil, shimRC, nil, nil)
if err != nil {
return err
diags = diags.Append(err)
return diags
}
data, err := sm.Data(nil, diff)
if err != nil {
return err
diags = diags.Append(err)
return diags
}
b.config = data
@ -80,11 +94,24 @@ func (b *Backend) Configure(c *terraform.ResourceConfig) error {
err = b.ConfigureFunc(context.WithValue(
context.Background(), backendConfigKey, data))
if err != nil {
return err
diags = diags.Append(err)
return diags
}
}
return nil
return diags
}
// shimConfig turns a new-style cty.Value configuration (which must be of
// an object type) into a minimal old-style *terraform.ResourceConfig object
// that should be populated enough to appease the not-yet-updated functionality
// in this package. This should be removed once everything is updated.
func (b *Backend) shimConfig(obj cty.Value) *terraform.ResourceConfig {
shimMap := hcl2shim.ConfigValueFromHCL2(obj).(map[string]interface{})
return &terraform.ResourceConfig{
Config: shimMap,
Raw: shimMap,
}
}
// Config returns the configuration. This is available after Configure is

View File

@ -5,15 +5,14 @@ import (
"fmt"
"testing"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
"github.com/zclconf/go-cty/cty"
)
func TestBackendValidate(t *testing.T) {
cases := []struct {
Name string
B *Backend
Config map[string]interface{}
Config map[string]cty.Value
Err bool
}{
{
@ -26,7 +25,7 @@ func TestBackendValidate(t *testing.T) {
},
},
},
nil,
map[string]cty.Value{},
true,
},
@ -40,8 +39,8 @@ func TestBackendValidate(t *testing.T) {
},
},
},
map[string]interface{}{
"foo": "bar",
map[string]cty.Value{
"foo": cty.StringVal("bar"),
},
false,
},
@ -49,14 +48,9 @@ func TestBackendValidate(t *testing.T) {
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
c, err := config.NewRawConfig(tc.Config)
if err != nil {
t.Fatalf("err: %s", err)
}
_, es := tc.B.Validate(terraform.NewResourceConfig(c))
if len(es) > 0 != tc.Err {
t.Fatalf("%d: %#v", i, es)
diags := tc.B.ValidateConfig(cty.ObjectVal(tc.Config))
if diags.HasErrors() != tc.Err {
t.Errorf("wrong number of diagnostics")
}
})
}
@ -66,7 +60,7 @@ func TestBackendConfigure(t *testing.T) {
cases := []struct {
Name string
B *Backend
Config map[string]interface{}
Config map[string]cty.Value
Err bool
}{
{
@ -88,8 +82,8 @@ func TestBackendConfigure(t *testing.T) {
return nil
},
},
map[string]interface{}{
"foo": 42,
map[string]cty.Value{
"foo": cty.NumberIntVal(42),
},
false,
},
@ -97,14 +91,9 @@ func TestBackendConfigure(t *testing.T) {
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
c, err := config.NewRawConfig(tc.Config)
if err != nil {
t.Fatalf("err: %s", err)
}
err = tc.B.Configure(terraform.NewResourceConfig(c))
if err != nil != tc.Err {
t.Fatalf("%d: %s", i, err)
diags := tc.B.Configure(cty.ObjectVal(tc.Config))
if diags.HasErrors() != tc.Err {
t.Errorf("wrong number of diagnostics")
}
})
}