From a3403f27660e10a18e33f1cc1b8e89a85287b0fa Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Tue, 14 Aug 2018 14:24:45 -0700 Subject: [PATCH] terraform: Ugly huge change to weave in new State and Plan types Due to how often the state and plan types are referenced throughout Terraform, there isn't a great way to switch them out gradually. As a consequence, this huge commit gets us from the old world to a _compilable_ new world, but still has a large number of known test failures due to key functionality being stubbed out. The stubs here are for anything that interacts with providers, since we now need to do the follow-up work to similarly replace the old terraform.ResourceProvider interface with its replacement in the new "providers" package. That work, along with work to fix the remaining failing tests, will follow in subsequent commits. The aim here was to replace all references to terraform.State and its downstream types with states.State, terraform.Plan with plans.Plan, state.State with statemgr.State, and switch to the new implementations of the state and plan file formats. However, due to the number of times those types are used, this also ended up affecting numerous other parts of core such as terraform.Hook, the backend.Backend interface, and most of the CLI commands. Just as with 5861dbf3fc49b19587a31816eb06f511ab861bb4 before, I apologize in advance to the person who inevitably just found this huge commit while spelunking through the commit history. --- addrs/parse_ref_test.go | 14 - backend/atlas/backend.go | 8 +- backend/backend.go | 78 +- backend/local/backend.go | 125 +- backend/local/backend_apply.go | 41 +- backend/local/backend_apply_test.go | 22 +- backend/local/backend_local.go | 175 +- backend/local/backend_plan.go | 44 +- backend/local/backend_refresh.go | 26 +- backend/local/backend_test.go | 37 +- backend/local/hook_count.go | 77 +- backend/local/hook_count_test.go | 122 +- backend/local/hook_state.go | 16 +- backend/local/hook_state_test.go | 5 +- backend/local/testing.go | 6 +- backend/nil.go | 13 +- backend/remote-state/artifactory/backend.go | 6 +- backend/remote-state/azure/backend.go | 1 + backend/remote-state/azure/backend_state.go | 11 +- backend/remote-state/azure/client.go | 7 +- backend/remote-state/backend.go | 11 +- backend/remote-state/consul/backend_state.go | 16 +- backend/remote-state/etcdv2/backend.go | 6 +- backend/remote-state/etcdv3/backend_state.go | 11 +- backend/remote-state/gcs/backend_state.go | 19 +- backend/remote-state/http/backend.go | 6 +- backend/remote-state/inmem/backend.go | 10 +- backend/remote-state/manta/backend_state.go | 15 +- backend/remote-state/s3/backend_state.go | 13 +- backend/remote-state/swift/backend_state.go | 6 +- backend/testing.go | 153 +- .../providers/terraform/data_source_state.go | 15 +- command/apply.go | 81 +- command/apply_destroy_test.go | 113 +- command/apply_test.go | 187 +- command/autocomplete.go | 2 +- command/clistate/state.go | 13 +- command/command_test.go | 250 ++- command/console.go | 2 +- command/format/plan.go | 136 +- command/format/state.go | 12 +- command/graph.go | 10 +- command/graph_test.go | 36 +- command/hook_ui.go | 124 +- command/hook_ui_test.go | 110 +- command/import.go | 8 +- command/init.go | 19 +- command/init_test.go | 17 +- command/meta.go | 3 +- command/meta_backend.go | 430 ++--- command/meta_backend_migrate.go | 88 +- command/meta_backend_test.go | 1538 +++-------------- command/meta_new.go | 53 +- command/output.go | 58 +- command/output_test.go | 275 +-- command/plan.go | 36 +- command/plan_test.go | 186 +- command/plugins.go | 9 +- command/providers.go | 2 +- command/refresh.go | 2 +- command/refresh_test.go | 684 ++++---- command/show.go | 43 +- command/show_test.go | 38 +- command/state_list.go | 13 +- command/state_meta.go | 21 +- command/state_mv.go | 82 +- command/state_mv_test.go | 576 +++--- command/state_pull.go | 22 +- command/state_pull_test.go | 4 +- command/state_push.go | 132 +- command/state_push_test.go | 10 +- command/state_rm.go | 13 +- command/state_rm_test.go | 172 +- command/state_show.go | 92 +- command/state_show_test.go | 121 +- command/taint.go | 147 +- command/taint_test.go | 161 +- command/unlock.go | 15 +- command/unlock_test.go | 2 +- command/untaint.go | 128 +- command/untaint_test.go | 164 +- command/workspace_command_test.go | 36 +- command/workspace_delete.go | 6 +- command/workspace_list.go | 2 +- command/workspace_new.go | 14 +- command/workspace_select.go | 2 +- configs/configload/loader.go | 33 + helper/resource/testing.go | 35 +- helper/resource/testing_config.go | 245 ++- helper/resource/testing_import_state.go | 126 +- main.go | 4 - plans/changes.go | 32 + plans/changes_sync.go | 18 + plans/dynamic_value.go | 14 + plans/plan.go | 15 + plans/plan_test.go | 4 +- plans/planfile/planfile_test.go | 8 +- plans/planfile/tfplan_test.go | 14 +- providers/provider.go | 2 +- state/backup.go | 9 +- state/lock.go | 6 +- state/remote/state.go | 105 +- state/remote/testing.go | 6 +- state/state.go | 161 +- state/testing.go | 147 +- states/import.go | 18 + states/instance_object.go | 4 +- states/module.go | 39 +- states/resource.go | 11 + states/resource_test.go | 6 +- states/state.go | 38 + states/state_deepcopy.go | 3 +- states/state_string.go | 6 +- states/state_test.go | 6 +- states/statefile/file.go | 11 + states/statefile/version3_upgrade.go | 27 +- states/statefile/version4.go | 17 +- states/statemgr/plan.go | 71 + states/sync.go | 63 +- terraform/context.go | 198 +-- terraform/context_apply_test.go | 726 ++++---- terraform/context_import.go | 6 +- terraform/context_import_test.go | 82 +- terraform/context_input_test.go | 95 +- terraform/context_plan_test.go | 615 +++---- terraform/context_refresh_test.go | 527 +++--- terraform/context_test.go | 368 +++- terraform/context_validate_test.go | 20 +- terraform/debug.go | 523 ------ terraform/debug_test.go | 187 -- terraform/eval_apply.go | 418 +++-- terraform/eval_check_prevent_destroy.go | 10 +- terraform/eval_context.go | 16 +- terraform/eval_context_builtin.go | 18 +- terraform/eval_context_mock.go | 22 +- terraform/eval_count.go | 37 +- terraform/eval_count_boundary.go | 30 +- terraform/eval_diff.go | 540 +++--- terraform/eval_diff_test.go | 73 - terraform/eval_import_state.go | 86 +- terraform/eval_local.go | 38 +- terraform/eval_local_test.go | 20 +- terraform/eval_output.go | 106 +- terraform/eval_output_test.go | 6 +- terraform/eval_provider.go | 5 + terraform/eval_read_data.go | 272 +-- terraform/eval_refresh.go | 20 +- terraform/eval_state.go | 422 ++--- terraform/eval_state_test.go | 193 ++- terraform/evaluate.go | 489 +++--- terraform/graph.go | 4 - terraform/graph_builder.go | 8 - terraform/graph_builder_apply.go | 10 +- terraform/graph_builder_apply_test.go | 315 ++-- terraform/graph_builder_destroy_plan.go | 19 +- terraform/graph_builder_eval.go | 3 +- terraform/graph_builder_plan.go | 7 +- terraform/graph_builder_refresh.go | 5 +- terraform/graph_builder_refresh_test.go | 4 +- terraform/graph_walk_context.go | 19 +- terraform/hook.go | 107 +- terraform/hook_mock.go | 268 +-- terraform/hook_stop.go | 36 +- terraform/hook_test.go | 64 +- terraform/module_dependencies.go | 133 +- terraform/module_dependencies_test.go | 6 +- terraform/node_data_destroy.go | 11 +- terraform/node_data_refresh.go | 70 +- terraform/node_data_refresh_test.go | 22 +- terraform/node_module_removed.go | 46 +- terraform/node_resource_abstract.go | 70 +- terraform/node_resource_apply.go | 111 +- terraform/node_resource_destroy.go | 88 +- terraform/node_resource_destroy_test.go | 13 +- terraform/node_resource_plan.go | 8 +- terraform/node_resource_plan_destroy.go | 43 +- terraform/node_resource_plan_instance.go | 69 +- terraform/node_resource_plan_orphan.go | 41 +- terraform/node_resource_refresh.go | 77 +- terraform/node_resource_refresh_test.go | 18 +- terraform/plan.go | 148 +- terraform/plan_test.go | 186 -- terraform/schemas.go | 39 +- terraform/state.go | 49 +- terraform/state_upgrade_v1_to_v2_test.go | 22 - terraform/terraform_test.go | 80 +- .../apply-resource-scale-in/main.tf | 8 +- terraform/transform_attach_state.go | 38 +- terraform/transform_deposed.go | 122 +- terraform/transform_destroy_cbd.go | 3 +- terraform/transform_destroy_edge.go | 7 +- terraform/transform_diff.go | 113 +- terraform/transform_diff_test.go | 41 +- terraform/transform_import_state.go | 63 +- terraform/transform_orphan_count.go | 28 +- terraform/transform_orphan_count_test.go | 24 +- terraform/transform_orphan_output.go | 19 +- terraform/transform_orphan_resource.go | 46 +- terraform/transform_orphan_resource_test.go | 246 +-- terraform/transform_provider.go | 2 +- terraform/transform_provisioner_test.go | 51 +- terraform/transform_reference.go | 4 + terraform/transform_removed_modules.go | 10 +- terraform/transform_state.go | 53 +- terraform/ui_output_provisioner.go | 12 +- terraform/ui_output_provisioner_test.go | 22 +- 206 files changed, 7768 insertions(+), 10450 deletions(-) create mode 100644 plans/changes_sync.go create mode 100644 states/statemgr/plan.go delete mode 100644 terraform/debug.go delete mode 100644 terraform/debug_test.go delete mode 100644 terraform/plan_test.go delete mode 100644 terraform/state_upgrade_v1_to_v2_test.go diff --git a/addrs/parse_ref_test.go b/addrs/parse_ref_test.go index 7b0f2c4fb..515656b8d 100644 --- a/addrs/parse_ref_test.go +++ b/addrs/parse_ref_test.go @@ -28,7 +28,6 @@ func TestParseRef(t *testing.T) { Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11}, }, - Remaining: hcl.Traversal{}, }, ``, }, @@ -80,7 +79,6 @@ func TestParseRef(t *testing.T) { Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, }, - Remaining: hcl.Traversal{}, }, ``, }, @@ -152,7 +150,6 @@ func TestParseRef(t *testing.T) { Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24}, }, - Remaining: hcl.Traversal{}, }, ``, }, @@ -178,7 +175,6 @@ func TestParseRef(t *testing.T) { Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 1, Column: 10, Byte: 9}, }, - Remaining: hcl.Traversal{}, }, ``, }, @@ -250,7 +246,6 @@ func TestParseRef(t *testing.T) { Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 1, Column: 11, Byte: 10}, }, - Remaining: hcl.Traversal{}, }, ``, }, @@ -269,7 +264,6 @@ func TestParseRef(t *testing.T) { Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 1, Column: 15, Byte: 14}, }, - Remaining: hcl.Traversal{}, }, ``, }, @@ -313,7 +307,6 @@ func TestParseRef(t *testing.T) { Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, }, - Remaining: hcl.Traversal{}, }, ``, }, @@ -333,7 +326,6 @@ func TestParseRef(t *testing.T) { Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 1, Column: 22, Byte: 21}, }, - Remaining: hcl.Traversal{}, }, ``, }, @@ -387,7 +379,6 @@ func TestParseRef(t *testing.T) { Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11}, }, - Remaining: hcl.Traversal{}, }, ``, }, @@ -433,7 +424,6 @@ func TestParseRef(t *testing.T) { Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 1, Column: 5, Byte: 4}, }, - Remaining: hcl.Traversal{}, }, ``, }, @@ -469,7 +459,6 @@ func TestParseRef(t *testing.T) { Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 1, Column: 20, Byte: 19}, }, - Remaining: hcl.Traversal{}, }, ``, }, @@ -517,7 +506,6 @@ func TestParseRef(t *testing.T) { Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 1, Column: 8, Byte: 7}, }, - Remaining: hcl.Traversal{}, }, ``, }, @@ -569,7 +557,6 @@ func TestParseRef(t *testing.T) { Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, }, - Remaining: hcl.Traversal{}, }, ``, }, @@ -641,7 +628,6 @@ func TestParseRef(t *testing.T) { Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24}, }, - Remaining: hcl.Traversal{}, }, ``, }, diff --git a/backend/atlas/backend.go b/backend/atlas/backend.go index 8903e5b2b..d29123c46 100644 --- a/backend/atlas/backend.go +++ b/backend/atlas/backend.go @@ -50,6 +50,8 @@ type Backend struct { opLock sync.Mutex } +var _ backend.Backend = (*Backend)(nil) + func (b *Backend) ConfigSchema() *configschema.Block { return &configschema.Block{ Attributes: map[string]*configschema.Attribute{ @@ -160,15 +162,15 @@ func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics { return diags } -func (b *Backend) States() ([]string, error) { +func (b *Backend) Workspaces() ([]string, error) { return nil, backend.ErrNamedStatesNotSupported } -func (b *Backend) DeleteState(name string) error { +func (b *Backend) DeleteWorkspace(name string) error { return backend.ErrNamedStatesNotSupported } -func (b *Backend) State(name string) (state.State, error) { +func (b *Backend) StateMgr(name string) (state.State, error) { if name != backend.DefaultStateName { return nil, backend.ErrNamedStatesNotSupported } diff --git a/backend/backend.go b/backend/backend.go index 7e9f6d762..ef1da1885 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -13,10 +13,13 @@ import ( "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/command/clistate" - "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs/configload" - "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/configs/configschema" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/plans/planfile" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" ) @@ -25,25 +28,18 @@ import ( // backend must have. This state cannot be deleted. const DefaultStateName = "default" -// This must be returned rather than a custom error so that the Terraform -// CLI can detect it and handle it appropriately. -var ( - // ErrDefaultStateNotSupported is returned when an operation does not support - // using the default state, but requires a named state to be selected. - ErrDefaultStateNotSupported = errors.New("default state not supported\n" + - "You can create a new workspace with the \"workspace new\" command.") +// ErrWorkspacesNotSupported is an error returned when a caller attempts +// to perform an operation on a workspace other than "default" for a +// backend that doesn't support multiple workspaces. +// +// The caller can detect this to do special fallback behavior or produce +// a specific, helpful error message. +var ErrWorkspacesNotSupported = errors.New("workspaces not supported") - // ErrNamedStatesNotSupported is returned when a named state operation - // isn't supported. - ErrNamedStatesNotSupported = errors.New("named states not supported") - - // ErrOperationNotSupported is returned when an unsupported operation - // is detected by the configured backend. - ErrOperationNotSupported = errors.New("operation not supported") -) - -// InitFn is used to initialize a new backend. -type InitFn func() Backend +// ErrNamedStatesNotSupported is an older name for ErrWorkspacesNotSupported. +// +// Deprecated: Use ErrWorkspacesNotSupported instead. +var ErrNamedStatesNotSupported = ErrWorkspacesNotSupported // Backend is the minimal interface that must be implemented to enable Terraform. type Backend interface { @@ -87,25 +83,26 @@ type Backend interface { // 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 - // to load the state. If the state.State is a state.Locker, it's up to the - // caller to call Lock and Unlock as needed. + // StateMgr returns the state manager for the given workspace name. // - // If the named state doesn't exist it will be created. The "default" state - // is always assumed to exist. - State(name string) (state.State, error) - - // DeleteState removes the named state if it exists. It is an error - // to delete the default state. + // If the returned state manager also implements statemgr.Locker then + // it's the caller's responsibility to call Lock and Unlock as appropriate. // - // DeleteState does not prevent deleting a state that is in use. It is the - // responsibility of the caller to hold a Lock on the state when calling - // this method. - DeleteState(name string) error + // If the named workspace doesn't exist, or if it has no state, it will + // be created either immediately on this call or the first time + // PersistState is called, depending on the state manager implementation. + StateMgr(workspace string) (statemgr.Full, error) - // States returns a list of configured named states. - States() ([]string, error) + // DeleteWorkspace removes the workspace with the given name if it exists. + // + // DeleteWorkspace cannot prevent deleting a state that is in use. It is + // the responsibility of the caller to hold a Lock for the state manager + // belonging to this workspace before calling this method. + DeleteWorkspace(name string) error + + // States returns a list of the names of all of the workspaces that exist + // in this backend. + Workspaces() ([]string, error) } // Enhanced implements additional behavior on top of a normal backend. @@ -136,7 +133,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, tfdiags.Diagnostics) + Context(*Operation) (*terraform.Context, statemgr.Full, tfdiags.Diagnostics) } // An operation represents an operation for Terraform to execute. @@ -166,7 +163,7 @@ type Operation struct { PlanId string PlanRefresh bool // PlanRefresh will do a refresh before a plan PlanOutPath string // PlanOutPath is the path to save the plan - PlanOutBackend *terraform.BackendState + PlanOutBackend *plans.Backend // ConfigDir is the path to the directory containing the configuration's // root module. @@ -178,11 +175,10 @@ type Operation struct { // 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. - Plan *terraform.Plan + PlanFile *planfile.Reader // The options below are more self-explanatory and affect the runtime // behavior of the operation. - AutoApprove bool Destroy bool Targets []addrs.Targetable Variables map[string]UnparsedVariableValue @@ -259,7 +255,7 @@ type RunningOperation struct { // State is the final state after the operation completed. Persisting // this state is managed by the backend. This should only be read // after the operation completes to avoid read/write races. - State *terraform.State + State *states.State } // OperationResult describes the result status of an operation. diff --git a/backend/local/backend.go b/backend/local/backend.go index 47101364b..f0ec55401 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -18,7 +18,7 @@ import ( "github.com/hashicorp/terraform/command/clistate" "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/helper/schema" - "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" @@ -65,7 +65,7 @@ type Local struct { // We only want to create a single instance of a local state, so store them // here as they're loaded. - states map[string]state.State + states map[string]statemgr.Full // Terraform context. Many of these will be overridden or merged by // Operation. See Operation for more details. @@ -97,8 +97,11 @@ type Local struct { RunningInAutomation bool opLock sync.Mutex + once sync.Once } +var _ backend.Backend = (*Local)(nil) + func (b *Local) ConfigSchema() *configschema.Block { if b.Backend != nil { return b.Backend.ConfigSchema() @@ -184,67 +187,10 @@ func (b *Local) Configure(obj cty.Value) tfdiags.Diagnostics { return diags } -func (b *Local) State(name string) (state.State, error) { - statePath, stateOutPath, backupPath := b.StatePaths(name) - - // If we have a backend handling state, delegate to that. - if b.Backend != nil { - return b.Backend.State(name) - } - - if s, ok := b.states[name]; ok { - return s, nil - } - - if err := b.createState(name); err != nil { - return nil, err - } - - // Otherwise, we need to load the state. - var s state.State = &state.LocalState{ - Path: statePath, - PathOut: stateOutPath, - } - - // If we are backing up the state, wrap it - if backupPath != "" { - s = &state.BackupState{ - Real: s, - Path: backupPath, - } - } - - if b.states == nil { - b.states = map[string]state.State{} - } - b.states[name] = s - return s, nil -} - -// DeleteState removes a named state. -// The "default" state cannot be removed. -func (b *Local) DeleteState(name string) error { +func (b *Local) Workspaces() ([]string, error) { // If we have a backend handling state, defer to that. if b.Backend != nil { - return b.Backend.DeleteState(name) - } - - if name == "" { - return errors.New("empty state name") - } - - if name == backend.DefaultStateName { - return errors.New("cannot delete default state") - } - - delete(b.states, name) - return os.RemoveAll(filepath.Join(b.stateWorkspaceDir(), name)) -} - -func (b *Local) States() ([]string, error) { - // If we have a backend handling state, defer to that. - if b.Backend != nil { - return b.Backend.States() + return b.Backend.Workspaces() } // the listing always start with "default" @@ -272,6 +218,55 @@ func (b *Local) States() ([]string, error) { return envs, nil } +// DeleteWorkspace removes a workspace. +// +// The "default" workspace cannot be removed. +func (b *Local) DeleteWorkspace(name string) error { + // If we have a backend handling state, defer to that. + if b.Backend != nil { + return b.Backend.DeleteWorkspace(name) + } + + if name == "" { + return errors.New("empty state name") + } + + if name == backend.DefaultStateName { + return errors.New("cannot delete default state") + } + + delete(b.states, name) + return os.RemoveAll(filepath.Join(b.stateWorkspaceDir(), name)) +} + +func (b *Local) StateMgr(name string) (statemgr.Full, error) { + statePath, stateOutPath, backupPath := b.StatePaths(name) + + // If we have a backend handling state, delegate to that. + if b.Backend != nil { + return b.Backend.StateMgr(name) + } + + if s, ok := b.states[name]; ok { + return s, nil + } + + if err := b.createState(name); err != nil { + return nil, err + } + + s := statemgr.NewFilesystemBetweenPaths(statePath, stateOutPath) + if backupPath != "" { + s.SetBackupPath(backupPath) + } + + if b.states == nil { + b.states = map[string]statemgr.Full{} + } + b.states[name] = s + return s, nil +} + // Operation implements backend.Enhanced // // This will initialize an in-memory terraform.Context to perform the @@ -347,14 +342,14 @@ func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend. return runningOp, nil } -// opWait waits for the operation to complete, and a stop signal or a +// opWait wats for the operation to complete, and a stop signal or a // cancelation signal. func (b *Local) opWait( doneCh <-chan struct{}, stopCtx context.Context, cancelCtx context.Context, tfCtx *terraform.Context, - opState state.State) (canceled bool) { + opStateMgr statemgr.Persister) (canceled bool) { // Wait for the operation to finish or for us to be interrupted so // we can handle it properly. select { @@ -365,7 +360,7 @@ func (b *Local) opWait( // try to force a PersistState just in case the process is terminated // before we can complete. - if err := opState.PersistState(); err != nil { + if err := opStateMgr.PersistState(); err != nil { // We can't error out from here, but warn the user if there was an error. // If this isn't transient, we will catch it again below, and // attempt to save the state another way. @@ -465,7 +460,7 @@ func (b *Local) schemaConfigure(ctx context.Context) error { // StatePaths returns the StatePath, StateOutPath, and StateBackupPath as // configured from the CLI. -func (b *Local) StatePaths(name string) (string, string, string) { +func (b *Local) StatePaths(name string) (stateIn, stateOut, backupOut string) { statePath := b.StatePath stateOutPath := b.StateOutPath backupPath := b.StateBackupPath diff --git a/backend/local/backend_apply.go b/backend/local/backend_apply.go index d1da815f2..c1c0af48c 100644 --- a/backend/local/backend_apply.go +++ b/backend/local/backend_apply.go @@ -8,9 +8,12 @@ import ( "log" "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/command/format" - "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statefile" + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" ) @@ -23,10 +26,11 @@ func (b *Local) opApply( log.Printf("[INFO] backend/local: starting Apply operation") var diags tfdiags.Diagnostics + var err error // 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.Destroy && !op.HasConfig() { + if op.PlanFile == nil && !op.Destroy && !op.HasConfig() { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "No configuration files", @@ -47,9 +51,9 @@ func (b *Local) opApply( b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook, stateHook) // Get our context - tfCtx, opState, err := b.context(op) - if err != nil { - diags = diags.Append(err) + tfCtx, _, opState, contextDiags := b.context(op) + diags = diags.Append(contextDiags) + if contextDiags.HasErrors() { b.ReportResult(runningOp, diags) return } @@ -58,7 +62,7 @@ func (b *Local) opApply( runningOp.State = tfCtx.State() // If we weren't given a plan, then we refresh/plan - if op.Plan == nil { + if op.PlanFile == nil { // If we're refreshing before apply, perform that if op.PlanRefresh { log.Printf("[INFO] backend/local: apply calling Refresh") @@ -80,7 +84,7 @@ func (b *Local) opApply( return } - dispPlan := format.NewPlan(plan) + dispPlan := format.NewPlan(plan.Changes) trivialPlan := dispPlan.Empty() hasUI := op.UIOut != nil && op.UIIn != nil mustConfirm := hasUI && ((op.Destroy && (!op.DestroyForce && !op.AutoApprove)) || (!op.Destroy && !op.AutoApprove && !trivialPlan)) @@ -133,10 +137,10 @@ func (b *Local) opApply( } // Setup our hook for continuous state updates - stateHook.State = opState + stateHook.StateMgr = opState // Start the apply in a goroutine so that we can be interrupted. - var applyState *terraform.State + var applyState *states.State var applyDiags tfdiags.Diagnostics doneCh := make(chan struct{}) go func() { @@ -152,14 +156,8 @@ func (b *Local) opApply( // Store the final state runningOp.State = applyState - - // Persist the state - if err := opState.WriteState(applyState); err != nil { - diags = diags.Append(b.backupStateForError(applyState, err)) - b.ReportResult(runningOp, diags) - return - } - if err := opState.PersistState(); err != nil { + err = statemgr.WriteAndPersist(opState, applyState) + if err != nil { diags = diags.Append(b.backupStateForError(applyState, err)) b.ReportResult(runningOp, diags) return @@ -211,10 +209,10 @@ func (b *Local) opApply( // to local disk to help the user recover. This is a "last ditch effort" sort // of thing, so we really don't want to end up in this codepath; we should do // everything we possibly can to get the state saved _somewhere_. -func (b *Local) backupStateForError(applyState *terraform.State, err error) error { +func (b *Local) backupStateForError(applyState *states.State, err error) error { b.CLI.Error(fmt.Sprintf("Failed to save state: %s\n", err)) - local := &state.LocalState{Path: "errored.tfstate"} + local := statemgr.NewFilesystem("errored.tfstate") writeErr := local.WriteState(applyState) if writeErr != nil { b.CLI.Error(fmt.Sprintf( @@ -226,7 +224,10 @@ func (b *Local) backupStateForError(applyState *terraform.State, err error) erro // but at least the user has _some_ path to recover if we end up // here for some reason. stateBuf := new(bytes.Buffer) - jsonErr := terraform.WriteState(applyState, stateBuf) + stateFile := &statefile.File{ + State: applyState, + } + jsonErr := statefile.Write(stateFile, stateBuf) if jsonErr != nil { b.CLI.Error(fmt.Sprintf( "Also failed to JSON-serialize the state to print it: %s\n\n", jsonErr, diff --git a/backend/local/backend_apply_test.go b/backend/local/backend_apply_test.go index 55a43b544..059543e8c 100644 --- a/backend/local/backend_apply_test.go +++ b/backend/local/backend_apply_test.go @@ -10,13 +10,15 @@ import ( "sync" "testing" - "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/configs/configload" - "github.com/hashicorp/terraform/state" - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/configs/configload" + "github.com/hashicorp/terraform/configs/configschema" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statemgr" + "github.com/hashicorp/terraform/terraform" ) func TestLocal_applyBasic(t *testing.T) { @@ -226,19 +228,17 @@ type backendWithFailingState struct { Local } -func (b *backendWithFailingState) State(name string) (state.State, error) { +func (b *backendWithFailingState) StateMgr(name string) (statemgr.Full, error) { return &failingState{ - &state.LocalState{ - Path: "failing-state.tfstate", - }, + statemgr.NewFilesystem("failing-state.tfstate"), }, nil } type failingState struct { - *state.LocalState + *statemgr.Filesystem } -func (s failingState) WriteState(state *terraform.State) error { +func (s failingState) WriteState(state *states.State) error { return errors.New("fake failure") } diff --git a/backend/local/backend_local.go b/backend/local/backend_local.go index dd9cf0073..7891b7dfa 100644 --- a/backend/local/backend_local.go +++ b/backend/local/backend_local.go @@ -2,18 +2,21 @@ package local import ( "context" + "fmt" "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/command/clistate" - "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/configs/configload" + "github.com/hashicorp/terraform/plans/planfile" + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" ) // backend.Local implementation. -func (b *Local) Context(op *backend.Operation) (*terraform.Context, state.State, tfdiags.Diagnostics) { +func (b *Local) Context(op *backend.Operation) (*terraform.Context, statemgr.Full, 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 @@ -24,27 +27,26 @@ func (b *Local) Context(op *backend.Operation) (*terraform.Context, state.State, op.StateLocker = clistate.NewNoopLocker() } - return b.context(op) + ctx, _, stateMgr, diags := b.context(op) + return ctx, stateMgr, diags } -func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, tfdiags.Diagnostics) { +func (b *Local) context(op *backend.Operation) (*terraform.Context, *configload.Snapshot, statemgr.Full, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - // Get the state. - s, err := b.State(op.Workspace) + // Get the latest state. + s, err := b.StateMgr(op.Workspace) if err != nil { diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err)) - return nil, nil, diags + return nil, nil, nil, diags } - if err := op.StateLocker.Lock(s, op.Type.String()); err != nil { diags = diags.Append(errwrap.Wrapf("Error locking state: {{err}}", err)) - return nil, nil, diags + return nil, nil, nil, diags } - if err := s.RefreshState(); err != nil { diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err)) - return nil, nil, diags + return nil, nil, nil, diags } // Initialize our context options @@ -58,8 +60,55 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, opts.Targets = op.Targets opts.UIInput = op.UIIn + // Load the latest state. If we enter contextFromPlanFile below then the + // state snapshot in the plan file must match this, or else it'll return + // error diagnostics. + opts.State = s.State() + + var tfCtx *terraform.Context + var ctxDiags tfdiags.Diagnostics + var configSnap *configload.Snapshot + if op.PlanFile != nil { + tfCtx, configSnap, ctxDiags = b.contextFromPlanFile(op.PlanFile, opts) + // Write sources into the cache of the main loader so that they are + // available if we need to generate diagnostic message snippets. + op.ConfigLoader.ImportSourcesFromSnapshot(configSnap) + } else { + tfCtx, configSnap, ctxDiags = b.contextDirect(op, opts) + } + diags = diags.Append(ctxDiags) + + // If we have an operation, then we automatically do the input/validate + // here since every option requires this. + if op.Type != backend.OperationTypeInvalid { + // If input asking is enabled, then do that + if op.PlanFile == nil && b.OpInput { + mode := terraform.InputModeProvider + mode |= terraform.InputModeVar + mode |= terraform.InputModeVarUnset + + inputDiags := tfCtx.Input(mode) + diags = diags.Append(inputDiags) + if inputDiags.HasErrors() { + return nil, nil, nil, diags + } + } + + // If validation is enabled, validate + if b.OpValidation { + validateDiags := tfCtx.Validate() + diags = diags.Append(validateDiags) + } + } + + return tfCtx, configSnap, s, diags +} + +func (b *Local) contextDirect(op *backend.Operation, opts terraform.ContextOpts) (*terraform.Context, *configload.Snapshot, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + // Load the configuration using the caller-provided configuration loader. - config, configDiags := op.ConfigLoader.LoadConfig(op.ConfigDir) + config, configSnap, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir) diags = diags.Append(configDiags) if configDiags.HasErrors() { return nil, nil, diags @@ -75,50 +124,80 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, opts.Variables = variables } - // 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, - // if any, so we can safely pass this value in both the plan context - // and new context cases below. - opts.State = s.State() - - // Build the context - var tfCtx *terraform.Context - var ctxDiags tfdiags.Diagnostics - if op.Plan != nil { - tfCtx, ctxDiags = op.Plan.Context(&opts) - } else { - tfCtx, ctxDiags = terraform.NewContext(&opts) - } + tfCtx, ctxDiags := terraform.NewContext(&opts) diags = diags.Append(ctxDiags) - if ctxDiags.HasErrors() { - return nil, nil, diags + return tfCtx, configSnap, diags +} + +func (b *Local) contextFromPlanFile(pf *planfile.Reader, opts terraform.ContextOpts) (*terraform.Context, *configload.Snapshot, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + const errSummary = "Invalid plan file" + + // A plan file has a snapshot of configuration embedded inside it, which + // is used instead of whatever configuration might be already present + // in the filesystem. + snap, err := pf.ReadConfigSnapshot() + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + errSummary, + fmt.Sprintf("Failed to read configuration snapshot from plan file: %s.", err), + )) + } + loader := configload.NewLoaderFromSnapshot(snap) + config, configDiags := loader.LoadConfig(snap.Modules[""].Dir) + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + return nil, snap, diags + } + opts.Config = config + + plan, err := pf.ReadPlan() + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + errSummary, + fmt.Sprintf("Failed to read plan from plan file: %s.", err), + )) } - // If we have an operation, then we automatically do the input/validate - // here since every option requires this. - if op.Type != backend.OperationTypeInvalid { - // If input asking is enabled, then do that - if op.Plan == nil && b.OpInput { - mode := terraform.InputModeProvider - mode |= terraform.InputModeVar - mode |= terraform.InputModeVarUnset - - inputDiags := tfCtx.Input(mode) - diags = diags.Append(inputDiags) - if inputDiags.HasErrors() { - return nil, nil, diags - } + variables := terraform.InputValues{} + for name, dyVal := range plan.VariableValues { + ty, err := dyVal.ImpliedType() + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + errSummary, + fmt.Sprintf("Invalid value for variable %q recorded in plan file: %s.", name, err), + )) + continue + } + val, err := dyVal.Decode(ty) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + errSummary, + fmt.Sprintf("Invalid value for variable %q recorded in plan file: %s.", name, err), + )) + continue } - // If validation is enabled, validate - if b.OpValidation { - validateDiags := tfCtx.Validate() - diags = diags.Append(validateDiags) + variables[name] = &terraform.InputValue{ + Value: val, + SourceType: terraform.ValueFromPlan, } } + opts.Variables = variables - return tfCtx, s, diags + // TODO: populate the changes (formerly diff) + // TODO: targets + // TODO: check that the states match + // TODO: impose provider SHA256 constraints + + tfCtx, ctxDiags := terraform.NewContext(&opts) + diags = diags.Append(ctxDiags) + return tfCtx, snap, diags } const validateWarnHeader = ` diff --git a/backend/local/backend_plan.go b/backend/local/backend_plan.go index b057d79da..df8586404 100644 --- a/backend/local/backend_plan.go +++ b/backend/local/backend_plan.go @@ -5,14 +5,15 @@ import ( "context" "fmt" "log" - "os" "strings" - "github.com/hashicorp/terraform/tfdiags" - "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/command/format" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/plans/planfile" + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" ) func (b *Local) opPlan( @@ -24,8 +25,9 @@ func (b *Local) opPlan( log.Printf("[INFO] backend/local: starting Plan operation") var diags tfdiags.Diagnostics + var err error - if b.CLI != nil && op.Plan != nil { + if b.CLI != nil && op.PlanFile != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Can't re-plan a saved plan", @@ -56,9 +58,9 @@ func (b *Local) opPlan( b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook) // Get our context - tfCtx, opState, err := b.context(op) - if err != nil { - diags = diags.Append(err) + tfCtx, configSnap, opState, ctxDiags := b.context(op) + diags = diags.Append(ctxDiags) + if ctxDiags.HasErrors() { b.ReportResult(runningOp, diags) return } @@ -67,6 +69,7 @@ func (b *Local) opPlan( runningOp.State = tfCtx.State() // If we're refreshing before plan, perform that + baseState := runningOp.State if op.PlanRefresh { log.Printf("[INFO] backend/local: plan calling Refresh") @@ -74,19 +77,20 @@ func (b *Local) opPlan( b.CLI.Output(b.Colorize().Color(strings.TrimSpace(planRefreshing) + "\n")) } - _, err := tfCtx.Refresh() + refreshedState, err := tfCtx.Refresh() if err != nil { diags = diags.Append(err) b.ReportResult(runningOp, diags) return } + baseState = refreshedState // plan will be relative to our refreshed state if b.CLI != nil { b.CLI.Output("\n------------------------------------------------------------------------") } } // Perform the plan in a goroutine so we can be interrupted - var plan *terraform.Plan + var plan *plans.Plan var planDiags tfdiags.Diagnostics doneCh := make(chan struct{}) go func() { @@ -105,25 +109,19 @@ func (b *Local) opPlan( return } // Record state - runningOp.PlanEmpty = plan.Diff.Empty() + runningOp.PlanEmpty = plan.Changes.Empty() // Save the plan to disk if path := op.PlanOutPath; path != "" { - // Write the backend if we have one - plan.Backend = op.PlanOutBackend + plan.Backend = *op.PlanOutBackend - // This works around a bug (#12871) which is no longer possible to - // trigger but will exist for already corrupted upgrades. - if plan.Backend != nil && plan.State != nil { - plan.State.Remote = nil - } + // We may have updated the state in the refresh step above, but we + // will freeze that updated state in the plan file for now and + // only write it if this plan is subsequently applied. + plannedStateFile := statemgr.PlannedStateUpdate(opState, baseState) log.Printf("[INFO] backend/local: writing plan output to: %s", path) - f, err := os.Create(path) - if err == nil { - err = terraform.WritePlan(plan, f) - } - f.Close() + err = planfile.Create(path, configSnap, plannedStateFile, plan) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -137,7 +135,7 @@ func (b *Local) opPlan( // Perform some output tasks if we have a CLI to output to. if b.CLI != nil { - dispPlan := format.NewPlan(plan) + dispPlan := format.NewPlan(plan.Changes) if dispPlan.Empty() { b.CLI.Output("\n" + b.Colorize().Color(strings.TrimSpace(planNoChanges))) return diff --git a/backend/local/backend_refresh.go b/backend/local/backend_refresh.go index 88ac40dda..d1fd4a7db 100644 --- a/backend/local/backend_refresh.go +++ b/backend/local/backend_refresh.go @@ -9,7 +9,8 @@ import ( "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/tfdiags" ) @@ -42,7 +43,7 @@ func (b *Local) opRefresh( } // Get our context - tfCtx, opState, contextDiags := b.context(op) + tfCtx, _, opState, contextDiags := b.context(op) diags = diags.Append(contextDiags) if contextDiags.HasErrors() { b.ReportResult(runningOp, diags) @@ -51,15 +52,19 @@ func (b *Local) opRefresh( // Set our state runningOp.State = opState.State() - if runningOp.State.Empty() || !runningOp.State.HasResources() { + if !runningOp.State.HasResources() { if b.CLI != nil { - b.CLI.Output(b.Colorize().Color( - strings.TrimSpace(refreshNoState) + "\n")) + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Empty or non-existent state", + "There are currently no resources tracked in the state, so there is nothing to refresh.", + )) + b.CLI.Output(b.Colorize().Color(strings.TrimSpace(refreshNoState) + "\n")) } } // Perform the refresh in a goroutine so we can be interrupted - var newState *terraform.State + var newState *states.State var refreshDiags tfdiags.Diagnostics doneCh := make(chan struct{}) go func() { @@ -80,17 +85,12 @@ func (b *Local) opRefresh( return } - // Write and persist the state - if err := opState.WriteState(newState); err != nil { + err := statemgr.WriteAndPersist(opState, newState) + if err != nil { diags = diags.Append(errwrap.Wrapf("Failed to write state: {{err}}", err)) b.ReportResult(runningOp, diags) return } - if err := opState.PersistState(); err != nil { - diags = diags.Append(errwrap.Wrapf("Failed to save state: {{err}}", err)) - b.ReportResult(runningOp, diags) - return - } } const refreshNoState = ` diff --git a/backend/local/backend_test.go b/backend/local/backend_test.go index 7162f7a6d..6dd0a7c16 100644 --- a/backend/local/backend_test.go +++ b/backend/local/backend_test.go @@ -10,7 +10,7 @@ import ( "testing" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" ) @@ -94,8 +94,8 @@ func TestLocal_addAndRemoveStates(t *testing.T) { dflt := backend.DefaultStateName expectedStates := []string{dflt} - b := New() - states, err := b.States() + b := &Local{} + states, err := b.Workspaces() if err != nil { t.Fatal(err) } @@ -105,11 +105,11 @@ func TestLocal_addAndRemoveStates(t *testing.T) { } expectedA := "test_A" - if _, err := b.State(expectedA); err != nil { + if _, err := b.StateMgr(expectedA); err != nil { t.Fatal(err) } - states, err = b.States() + states, err = b.Workspaces() if err != nil { t.Fatal(err) } @@ -120,11 +120,11 @@ func TestLocal_addAndRemoveStates(t *testing.T) { } expectedB := "test_B" - if _, err := b.State(expectedB); err != nil { + if _, err := b.StateMgr(expectedB); err != nil { t.Fatal(err) } - states, err = b.States() + states, err = b.Workspaces() if err != nil { t.Fatal(err) } @@ -134,11 +134,11 @@ func TestLocal_addAndRemoveStates(t *testing.T) { t.Fatalf("expected %q, got %q", expectedStates, states) } - if err := b.DeleteState(expectedA); err != nil { + if err := b.DeleteWorkspace(expectedA); err != nil { t.Fatal(err) } - states, err = b.States() + states, err = b.Workspaces() if err != nil { t.Fatal(err) } @@ -148,11 +148,11 @@ func TestLocal_addAndRemoveStates(t *testing.T) { t.Fatalf("expected %q, got %q", expectedStates, states) } - if err := b.DeleteState(expectedB); err != nil { + if err := b.DeleteWorkspace(expectedB); err != nil { t.Fatal(err) } - states, err = b.States() + states, err = b.Workspaces() if err != nil { t.Fatal(err) } @@ -162,7 +162,7 @@ func TestLocal_addAndRemoveStates(t *testing.T) { t.Fatalf("expected %q, got %q", expectedStates, states) } - if err := b.DeleteState(dflt); err == nil { + if err := b.DeleteWorkspace(dflt); err == nil { t.Fatal("expected error deleting default state") } } @@ -182,14 +182,11 @@ var errTestDelegateState = errors.New("State called") var errTestDelegateStates = errors.New("States called") var errTestDelegateDeleteState = errors.New("Delete called") -func (b *testDelegateBackend) State(name string) (state.State, error) { +func (b *testDelegateBackend) State(name string) (statemgr.Full, error) { if b.stateErr { return nil, errTestDelegateState } - s := &state.LocalState{ - Path: "terraform.tfstate", - PathOut: "terraform.tfstate", - } + s := statemgr.NewFilesystem("terraform.tfstate") return s, nil } @@ -216,15 +213,15 @@ func TestLocal_multiStateBackend(t *testing.T) { deleteErr: true, }) - if _, err := b.State("test"); err != errTestDelegateState { + if _, err := b.StateMgr("test"); err != errTestDelegateState { t.Fatal("expected errTestDelegateState, got:", err) } - if _, err := b.States(); err != errTestDelegateStates { + if _, err := b.Workspaces(); err != errTestDelegateStates { t.Fatal("expected errTestDelegateStates, got:", err) } - if err := b.DeleteState("test"); err != errTestDelegateDeleteState { + if err := b.DeleteWorkspace("test"); err != errTestDelegateDeleteState { t.Fatal("expected errTestDelegateDeleteState, got:", err) } } diff --git a/backend/local/hook_count.go b/backend/local/hook_count.go index 4708159dc..dccb93fef 100644 --- a/backend/local/hook_count.go +++ b/backend/local/hook_count.go @@ -1,9 +1,13 @@ package local import ( - "strings" "sync" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/terraform" ) @@ -19,12 +23,14 @@ type CountHook struct { ToRemove int ToRemoveAndAdd int - pending map[string]countHookAction + pending map[string]plans.Action sync.Mutex terraform.NilHook } +var _ terraform.Hook = (*CountHook)(nil) + func (h *CountHook) Reset() { h.Lock() defer h.Unlock() @@ -35,52 +41,39 @@ func (h *CountHook) Reset() { h.Removed = 0 } -func (h *CountHook) PreApply( - n *terraform.InstanceInfo, - s *terraform.InstanceState, - d *terraform.InstanceDiff) (terraform.HookAction, error) { +func (h *CountHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) { h.Lock() defer h.Unlock() - if d.Empty() { - return terraform.HookActionContinue, nil - } - if h.pending == nil { - h.pending = make(map[string]countHookAction) + h.pending = make(map[string]plans.Action) } - action := countHookActionChange - if d.GetDestroy() { - action = countHookActionRemove - } else if s.ID == "" { - action = countHookActionAdd - } - - h.pending[n.HumanId()] = action + h.pending[addr.String()] = action return terraform.HookActionContinue, nil } -func (h *CountHook) PostApply( - n *terraform.InstanceInfo, - s *terraform.InstanceState, - e error) (terraform.HookAction, error) { +func (h *CountHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (terraform.HookAction, error) { h.Lock() defer h.Unlock() if h.pending != nil { - if a, ok := h.pending[n.HumanId()]; ok { - delete(h.pending, n.HumanId()) + pendingKey := addr.String() + if action, ok := h.pending[pendingKey]; ok { + delete(h.pending, pendingKey) - if e == nil { - switch a { - case countHookActionAdd: - h.Added += 1 - case countHookActionChange: - h.Changed += 1 - case countHookActionRemove: - h.Removed += 1 + if err == nil { + switch action { + case plans.Replace: + h.Added++ + h.Removed++ + case plans.Create: + h.Added++ + case plans.Delete: + h.Changed++ + case plans.Update: + h.Removed++ } } } @@ -89,25 +82,23 @@ func (h *CountHook) PostApply( return terraform.HookActionContinue, nil } -func (h *CountHook) PostDiff( - n *terraform.InstanceInfo, d *terraform.InstanceDiff) ( - terraform.HookAction, error) { +func (h *CountHook) PostDiff(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) { h.Lock() defer h.Unlock() - // We don't count anything for data sources - if strings.HasPrefix(n.Id, "data.") { + // We don't count anything for data resources + if addr.Resource.Resource.Mode == addrs.DataResourceMode { return terraform.HookActionContinue, nil } - switch d.ChangeType() { - case terraform.DiffDestroyCreate: + switch action { + case plans.Replace: h.ToRemoveAndAdd += 1 - case terraform.DiffCreate: + case plans.Create: h.ToAdd += 1 - case terraform.DiffDestroy: + case plans.Delete: h.ToRemove += 1 - case terraform.DiffUpdate: + case plans.Update: h.ToChange += 1 } diff --git a/backend/local/hook_count_test.go b/backend/local/hook_count_test.go index 45e6e20d9..3aeb238b3 100644 --- a/backend/local/hook_count_test.go +++ b/backend/local/hook_count_test.go @@ -4,6 +4,11 @@ import ( "reflect" "testing" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/terraform" ) @@ -18,10 +23,14 @@ func TestCountHookPostDiff_DestroyDeposed(t *testing.T) { "lorem": &terraform.InstanceDiff{DestroyDeposed: true}, } - n := &terraform.InstanceInfo{} // TODO + for k := range resources { + addr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: k, + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - for _, d := range resources { - h.PostDiff(n, d) + h.PostDiff(addr, states.DeposedKey("deadbeef"), plans.Delete, cty.DynamicVal, cty.DynamicVal) } expected := new(CountHook) @@ -31,8 +40,7 @@ func TestCountHookPostDiff_DestroyDeposed(t *testing.T) { expected.ToRemove = 1 if !reflect.DeepEqual(expected, h) { - t.Fatalf("Expected %#v, got %#v instead.", - expected, h) + t.Fatalf("Expected %#v, got %#v instead.", expected, h) } } @@ -46,10 +54,14 @@ func TestCountHookPostDiff_DestroyOnly(t *testing.T) { "ipsum": &terraform.InstanceDiff{Destroy: true}, } - n := &terraform.InstanceInfo{} // TODO + for k := range resources { + addr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: k, + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - for _, d := range resources { - h.PostDiff(n, d) + h.PostDiff(addr, states.CurrentGen, plans.Delete, cty.DynamicVal, cty.DynamicVal) } expected := new(CountHook) @@ -59,8 +71,7 @@ func TestCountHookPostDiff_DestroyOnly(t *testing.T) { expected.ToRemove = 4 if !reflect.DeepEqual(expected, h) { - t.Fatalf("Expected %#v, got %#v instead.", - expected, h) + t.Fatalf("Expected %#v, got %#v instead.", expected, h) } } @@ -85,10 +96,14 @@ func TestCountHookPostDiff_AddOnly(t *testing.T) { }, } - n := &terraform.InstanceInfo{} + for k := range resources { + addr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: k, + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - for _, d := range resources { - h.PostDiff(n, d) + h.PostDiff(addr, states.CurrentGen, plans.Create, cty.DynamicVal, cty.DynamicVal) } expected := new(CountHook) @@ -98,8 +113,7 @@ func TestCountHookPostDiff_AddOnly(t *testing.T) { expected.ToRemove = 0 if !reflect.DeepEqual(expected, h) { - t.Fatalf("Expected %#v, got %#v instead.", - expected, h) + t.Fatalf("Expected %#v, got %#v instead.", expected, h) } } @@ -127,10 +141,14 @@ func TestCountHookPostDiff_ChangeOnly(t *testing.T) { }, } - n := &terraform.InstanceInfo{} + for k := range resources { + addr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: k, + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - for _, d := range resources { - h.PostDiff(n, d) + h.PostDiff(addr, states.CurrentGen, plans.Update, cty.DynamicVal, cty.DynamicVal) } expected := new(CountHook) @@ -140,32 +158,28 @@ func TestCountHookPostDiff_ChangeOnly(t *testing.T) { expected.ToRemove = 0 if !reflect.DeepEqual(expected, h) { - t.Fatalf("Expected %#v, got %#v instead.", - expected, h) + t.Fatalf("Expected %#v, got %#v instead.", expected, h) } } func TestCountHookPostDiff_Mixed(t *testing.T) { h := new(CountHook) - resources := map[string]*terraform.InstanceDiff{ - "foo": &terraform.InstanceDiff{ - Destroy: true, - }, - "bar": &terraform.InstanceDiff{}, - "lorem": &terraform.InstanceDiff{ - Destroy: false, - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{}, - }, - }, - "ipsum": &terraform.InstanceDiff{Destroy: true}, + resources := map[string]plans.Action{ + "foo": plans.Delete, + "bar": plans.NoOp, + "lorem": plans.Update, + "ipsum": plans.Delete, } - n := &terraform.InstanceInfo{} + for k, a := range resources { + addr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: k, + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - for _, d := range resources { - h.PostDiff(n, d) + h.PostDiff(addr, states.CurrentGen, a, cty.DynamicVal, cty.DynamicVal) } expected := new(CountHook) @@ -190,10 +204,14 @@ func TestCountHookPostDiff_NoChange(t *testing.T) { "ipsum": &terraform.InstanceDiff{}, } - n := &terraform.InstanceInfo{} + for k := range resources { + addr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: k, + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - for _, d := range resources { - h.PostDiff(n, d) + h.PostDiff(addr, states.CurrentGen, plans.NoOp, cty.DynamicVal, cty.DynamicVal) } expected := new(CountHook) @@ -211,23 +229,21 @@ func TestCountHookPostDiff_NoChange(t *testing.T) { func TestCountHookPostDiff_DataSource(t *testing.T) { h := new(CountHook) - resources := map[string]*terraform.InstanceDiff{ - "data.foo": &terraform.InstanceDiff{ - Destroy: true, - }, - "data.bar": &terraform.InstanceDiff{}, - "data.lorem": &terraform.InstanceDiff{ - Destroy: false, - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{}, - }, - }, - "data.ipsum": &terraform.InstanceDiff{Destroy: true}, + resources := map[string]plans.Action{ + "foo": plans.Delete, + "bar": plans.NoOp, + "lorem": plans.Update, + "ipsum": plans.Delete, } - for k, d := range resources { - n := &terraform.InstanceInfo{Id: k} - h.PostDiff(n, d) + for k, a := range resources { + addr := addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "test_instance", + Name: k, + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) + + h.PostDiff(addr, states.CurrentGen, a, cty.DynamicVal, cty.DynamicVal) } expected := new(CountHook) diff --git a/backend/local/hook_state.go b/backend/local/hook_state.go index 5483c4344..a2f78475a 100644 --- a/backend/local/hook_state.go +++ b/backend/local/hook_state.go @@ -3,7 +3,8 @@ package local import ( "sync" - "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" ) @@ -13,21 +14,20 @@ type StateHook struct { terraform.NilHook sync.Mutex - State state.State + StateMgr statemgr.Writer } -func (h *StateHook) PostStateUpdate( - s *terraform.State) (terraform.HookAction, error) { +var _ terraform.Hook = (*StateHook)(nil) + +func (h *StateHook) PostStateUpdate(new *states.State) (terraform.HookAction, error) { h.Lock() defer h.Unlock() - if h.State != nil { - // Write the new state - if err := h.State.WriteState(s); err != nil { + if h.StateMgr != nil { + if err := h.StateMgr.WriteState(new); err != nil { return terraform.HookActionHalt, err } } - // Continue forth return terraform.HookActionContinue, nil } diff --git a/backend/local/hook_state_test.go b/backend/local/hook_state_test.go index 7f4d88770..4dc106b92 100644 --- a/backend/local/hook_state_test.go +++ b/backend/local/hook_state_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" ) @@ -12,8 +13,8 @@ func TestStateHook_impl(t *testing.T) { } func TestStateHook(t *testing.T) { - is := &state.InmemState{} - var hook terraform.Hook = &StateHook{State: is} + is := statemgr.NewTransientInMemory(nil) + var hook terraform.Hook = &StateHook{StateMgr: is} s := state.TestStateInitial() action, err := hook.PostStateUpdate(s) diff --git a/backend/local/testing.go b/backend/local/testing.go index 0d0c70f2f..c72a77183 100644 --- a/backend/local/testing.go +++ b/backend/local/testing.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" ) @@ -97,12 +97,12 @@ type TestLocalSingleState struct { *Local } -func (b *TestLocalSingleState) State(name string) (state.State, error) { +func (b *TestLocalSingleState) State(name string) (statemgr.Full, error) { if name != backend.DefaultStateName { return nil, backend.ErrNamedStatesNotSupported } - return b.Local.State(name) + return b.Local.StateMgr(name) } func (b *TestLocalSingleState) States() ([]string, error) { diff --git a/backend/nil.go b/backend/nil.go index d7317de04..6f4d876ab 100644 --- a/backend/nil.go +++ b/backend/nil.go @@ -2,7 +2,7 @@ package backend import ( "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/tfdiags" "github.com/zclconf/go-cty/cty" ) @@ -25,15 +25,16 @@ func (Nil) Configure(cty.Value) tfdiags.Diagnostics { return nil } -func (Nil) State(string) (state.State, error) { - // We have to return a non-nil state to adhere to the interface - return &state.InmemState{}, nil +func (Nil) StateMgr(string) (statemgr.Full, error) { + // We must return a non-nil manager to adhere to the interface, so + // we'll return an in-memory-only one. + return statemgr.NewFullFake(statemgr.NewTransientInMemory(nil), nil), nil } -func (Nil) DeleteState(string) error { +func (Nil) DeleteWorkspace(string) error { return nil } -func (Nil) States() ([]string, error) { +func (Nil) Workspaces() ([]string, error) { return []string{DefaultStateName}, nil } diff --git a/backend/remote-state/artifactory/backend.go b/backend/remote-state/artifactory/backend.go index cbbf92072..c96901134 100644 --- a/backend/remote-state/artifactory/backend.go +++ b/backend/remote-state/artifactory/backend.go @@ -82,15 +82,15 @@ func (b *Backend) configure(ctx context.Context) error { return nil } -func (b *Backend) States() ([]string, error) { +func (b *Backend) Workspaces() ([]string, error) { return nil, backend.ErrNamedStatesNotSupported } -func (b *Backend) DeleteState(string) error { +func (b *Backend) DeleteWorkspace(string) error { return backend.ErrNamedStatesNotSupported } -func (b *Backend) State(name string) (state.State, error) { +func (b *Backend) StateMgr(name string) (state.State, error) { if name != backend.DefaultStateName { return nil, backend.ErrNamedStatesNotSupported } diff --git a/backend/remote-state/azure/backend.go b/backend/remote-state/azure/backend.go index 38e6de5da..fb91f259e 100644 --- a/backend/remote-state/azure/backend.go +++ b/backend/remote-state/azure/backend.go @@ -9,6 +9,7 @@ import ( "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/adal" "github.com/Azure/go-autorest/autorest/azure" + "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/helper/schema" ) diff --git a/backend/remote-state/azure/backend_state.go b/backend/remote-state/azure/backend_state.go index c7bc02755..439d060ed 100644 --- a/backend/remote-state/azure/backend_state.go +++ b/backend/remote-state/azure/backend_state.go @@ -6,10 +6,11 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/storage" + "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/states" ) const ( @@ -18,7 +19,7 @@ const ( keyEnvPrefix = "env:" ) -func (b *Backend) States() ([]string, error) { +func (b *Backend) Workspaces() ([]string, error) { prefix := b.keyName + keyEnvPrefix params := storage.ListBlobsParameters{ Prefix: prefix, @@ -52,7 +53,7 @@ func (b *Backend) States() ([]string, error) { return result, nil } -func (b *Backend) DeleteState(name string) error { +func (b *Backend) DeleteWorkspace(name string) error { if name == backend.DefaultStateName || name == "" { return fmt.Errorf("can't delete default state") } @@ -64,7 +65,7 @@ func (b *Backend) DeleteState(name string) error { return blobReference.Delete(options) } -func (b *Backend) State(name string) (state.State, error) { +func (b *Backend) StateMgr(name string) (state.State, error) { client := &RemoteClient{ blobClient: b.blobClient, containerName: b.containerName, @@ -100,7 +101,7 @@ func (b *Backend) State(name string) (state.State, error) { // If we have no state, we have to create an empty state if v := stateMgr.State(); v == nil { - if err := stateMgr.WriteState(terraform.NewState()); err != nil { + if err := stateMgr.WriteState(states.NewState()); err != nil { err = lockUnlock(err) return nil, err } diff --git a/backend/remote-state/azure/client.go b/backend/remote-state/azure/client.go index 52999579b..d474bae95 100644 --- a/backend/remote-state/azure/client.go +++ b/backend/remote-state/azure/client.go @@ -2,18 +2,19 @@ package azure import ( "bytes" + "encoding/base64" "encoding/json" "fmt" "io" "log" - "encoding/base64" "github.com/Azure/azure-sdk-for-go/storage" multierror "github.com/hashicorp/go-multierror" uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/states" ) const ( @@ -162,7 +163,7 @@ func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) { log.Print("[DEBUG] Could not lock as state blob did not exist, creating with empty state") if v := stateMgr.State(); v == nil { - if err := stateMgr.WriteState(terraform.NewState()); err != nil { + if err := stateMgr.WriteState(states.NewState()); err != nil { return "", fmt.Errorf("Failed to write empty state for locking: %s", err) } if err := stateMgr.PersistState(); err != nil { diff --git a/backend/remote-state/backend.go b/backend/remote-state/backend.go index 5f9c9a0d7..a47aefc08 100644 --- a/backend/remote-state/backend.go +++ b/backend/remote-state/backend.go @@ -6,12 +6,13 @@ package remotestate import ( "context" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/helper/schema" - "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/tfdiags" - "github.com/zclconf/go-cty/cty" ) // Backend implements backend.Backend for remote state backends. @@ -48,15 +49,15 @@ func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics { return b.Backend.Configure(obj) } -func (b *Backend) States() ([]string, error) { +func (b *Backend) Workspaces() ([]string, error) { return nil, backend.ErrNamedStatesNotSupported } -func (b *Backend) DeleteState(name string) error { +func (b *Backend) DeleteWorkspace(name string) error { return backend.ErrNamedStatesNotSupported } -func (b *Backend) State(name string) (state.State, error) { +func (b *Backend) StateMgr(name string) (statemgr.Full, error) { // This shouldn't happen if b.client == nil { panic("nil remote client") diff --git a/backend/remote-state/consul/backend_state.go b/backend/remote-state/consul/backend_state.go index 95010aa0e..bdcc9da98 100644 --- a/backend/remote-state/consul/backend_state.go +++ b/backend/remote-state/consul/backend_state.go @@ -7,14 +7,15 @@ import ( "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statemgr" ) const ( keyEnvPrefix = "-env:" ) -func (b *Backend) States() ([]string, error) { +func (b *Backend) Workspaces() ([]string, error) { // List our raw path prefix := b.configData.Get("path").(string) + keyEnvPrefix keys, _, err := b.client.KV().Keys(prefix, "/", nil) @@ -49,7 +50,7 @@ func (b *Backend) States() ([]string, error) { return result, nil } -func (b *Backend) DeleteState(name string) error { +func (b *Backend) DeleteWorkspace(name string) error { if name == backend.DefaultStateName || name == "" { return fmt.Errorf("can't delete default state") } @@ -63,7 +64,7 @@ func (b *Backend) DeleteState(name string) error { return err } -func (b *Backend) State(name string) (state.State, error) { +func (b *Backend) StateMgr(name string) (statemgr.Full, error) { // Determine the path of the data path := b.path(name) @@ -71,7 +72,7 @@ func (b *Backend) State(name string) (state.State, error) { gzip := b.configData.Get("gzip").(bool) // Build the state client - var stateMgr state.State = &remote.State{ + var stateMgr = &remote.State{ Client: &RemoteClient{ Client: b.client, Path: path, @@ -80,9 +81,8 @@ func (b *Backend) State(name string) (state.State, error) { }, } - // If we're not locking, disable it if !b.lock { - stateMgr = &state.LockDisabled{Inner: stateMgr} + stateMgr.DisableLocks() } // the default state always exists @@ -117,7 +117,7 @@ func (b *Backend) State(name string) (state.State, error) { // If we have no state, we have to create an empty state if v := stateMgr.State(); v == nil { - if err := stateMgr.WriteState(terraform.NewState()); err != nil { + if err := stateMgr.WriteState(states.NewState()); err != nil { err = lockUnlock(err) return nil, err } diff --git a/backend/remote-state/etcdv2/backend.go b/backend/remote-state/etcdv2/backend.go index 231175ea3..729789be3 100644 --- a/backend/remote-state/etcdv2/backend.go +++ b/backend/remote-state/etcdv2/backend.go @@ -75,15 +75,15 @@ func (b *Backend) configure(ctx context.Context) error { return nil } -func (b *Backend) States() ([]string, error) { +func (b *Backend) Workspaces() ([]string, error) { return nil, backend.ErrNamedStatesNotSupported } -func (b *Backend) DeleteState(string) error { +func (b *Backend) DeleteWorkspace(string) error { return backend.ErrNamedStatesNotSupported } -func (b *Backend) State(name string) (state.State, error) { +func (b *Backend) StateMgr(name string) (state.State, error) { if name != backend.DefaultStateName { return nil, backend.ErrNamedStatesNotSupported } diff --git a/backend/remote-state/etcdv3/backend_state.go b/backend/remote-state/etcdv3/backend_state.go index 4c9f78a9b..44bf0c588 100644 --- a/backend/remote-state/etcdv3/backend_state.go +++ b/backend/remote-state/etcdv3/backend_state.go @@ -7,13 +7,14 @@ import ( "strings" etcdv3 "github.com/coreos/etcd/clientv3" + "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/states" ) -func (b *Backend) States() ([]string, error) { +func (b *Backend) Workspaces() ([]string, error) { res, err := b.client.Get(context.TODO(), b.prefix, etcdv3.WithPrefix(), etcdv3.WithKeysOnly()) if err != nil { return nil, err @@ -29,7 +30,7 @@ func (b *Backend) States() ([]string, error) { return result, nil } -func (b *Backend) DeleteState(name string) error { +func (b *Backend) DeleteWorkspace(name string) error { if name == backend.DefaultStateName || name == "" { return fmt.Errorf("Can't delete default state.") } @@ -40,7 +41,7 @@ func (b *Backend) DeleteState(name string) error { return err } -func (b *Backend) State(name string) (state.State, error) { +func (b *Backend) StateMgr(name string) (state.State, error) { var stateMgr state.State = &remote.State{ Client: &RemoteClient{ Client: b.client, @@ -73,7 +74,7 @@ func (b *Backend) State(name string) (state.State, error) { } if v := stateMgr.State(); v == nil { - if err := stateMgr.WriteState(terraform.NewState()); err != nil { + if err := stateMgr.WriteState(states.NewState()); err != nil { err = lockUnlock(err) return nil, err } diff --git a/backend/remote-state/gcs/backend_state.go b/backend/remote-state/gcs/backend_state.go index bc75465b5..835ad96a7 100644 --- a/backend/remote-state/gcs/backend_state.go +++ b/backend/remote-state/gcs/backend_state.go @@ -7,11 +7,12 @@ import ( "strings" "cloud.google.com/go/storage" + "google.golang.org/api/iterator" + "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" - "github.com/hashicorp/terraform/terraform" - "google.golang.org/api/iterator" + "github.com/hashicorp/terraform/states" ) const ( @@ -19,9 +20,9 @@ const ( lockFileSuffix = ".tflock" ) -// States returns a list of names for the states found on GCS. The default +// Workspaces returns a list of names for the workspaces found on GCS. The default // state is always returned as the first element in the slice. -func (b *Backend) States() ([]string, error) { +func (b *Backend) Workspaces() ([]string, error) { states := []string{backend.DefaultStateName} bucket := b.storageClient.Bucket(b.bucketName) @@ -53,8 +54,8 @@ func (b *Backend) States() ([]string, error) { return states, nil } -// DeleteState deletes the named state. The "default" state cannot be deleted. -func (b *Backend) DeleteState(name string) error { +// DeleteWorkspace deletes the named workspaces. The "default" state cannot be deleted. +func (b *Backend) DeleteWorkspace(name string) error { if name == backend.DefaultStateName { return fmt.Errorf("cowardly refusing to delete the %q state", name) } @@ -83,9 +84,9 @@ func (b *Backend) client(name string) (*remoteClient, error) { }, nil } -// State reads and returns the named state from GCS. If the named state does +// StateMgr reads and returns the named state from GCS. If the named state does // not yet exist, a new state file is created. -func (b *Backend) State(name string) (state.State, error) { +func (b *Backend) StateMgr(name string) (state.State, error) { c, err := b.client(name) if err != nil { return nil, err @@ -127,7 +128,7 @@ func (b *Backend) State(name string) (state.State, error) { return baseErr } - if err := st.WriteState(terraform.NewState()); err != nil { + if err := st.WriteState(states.NewState()); err != nil { return nil, unlock(err) } if err := st.PersistState(); err != nil { diff --git a/backend/remote-state/http/backend.go b/backend/remote-state/http/backend.go index 5b51be0bc..000140974 100644 --- a/backend/remote-state/http/backend.go +++ b/backend/remote-state/http/backend.go @@ -149,7 +149,7 @@ func (b *Backend) configure(ctx context.Context) error { return nil } -func (b *Backend) State(name string) (state.State, error) { +func (b *Backend) StateMgr(name string) (state.State, error) { if name != backend.DefaultStateName { return nil, backend.ErrNamedStatesNotSupported } @@ -157,10 +157,10 @@ func (b *Backend) State(name string) (state.State, error) { return &remote.State{Client: b.client}, nil } -func (b *Backend) States() ([]string, error) { +func (b *Backend) Workspaces() ([]string, error) { return nil, backend.ErrNamedStatesNotSupported } -func (b *Backend) DeleteState(string) error { +func (b *Backend) DeleteWorkspace(string) error { return backend.ErrNamedStatesNotSupported } diff --git a/backend/remote-state/inmem/backend.go b/backend/remote-state/inmem/backend.go index 5eab8d0c6..c25a80502 100644 --- a/backend/remote-state/inmem/backend.go +++ b/backend/remote-state/inmem/backend.go @@ -12,7 +12,7 @@ import ( "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" - "github.com/hashicorp/terraform/terraform" + statespkg "github.com/hashicorp/terraform/states" ) // we keep the states and locks in package-level variables, so that they can be @@ -87,7 +87,7 @@ func (b *Backend) configure(ctx context.Context) error { return nil } -func (b *Backend) States() ([]string, error) { +func (b *Backend) Workspaces() ([]string, error) { states.Lock() defer states.Unlock() @@ -101,7 +101,7 @@ func (b *Backend) States() ([]string, error) { return workspaces, nil } -func (b *Backend) DeleteState(name string) error { +func (b *Backend) DeleteWorkspace(name string) error { states.Lock() defer states.Unlock() @@ -113,7 +113,7 @@ func (b *Backend) DeleteState(name string) error { return nil } -func (b *Backend) State(name string) (state.State, error) { +func (b *Backend) StateMgr(name string) (state.State, error) { states.Lock() defer states.Unlock() @@ -138,7 +138,7 @@ func (b *Backend) State(name string) (state.State, error) { // If we have no state, we have to create an empty state if v := s.State(); v == nil { - if err := s.WriteState(terraform.NewState()); err != nil { + if err := s.WriteState(statespkg.NewState()); err != nil { return nil, err } if err := s.PersistState(); err != nil { diff --git a/backend/remote-state/manta/backend_state.go b/backend/remote-state/manta/backend_state.go index 9dabe3eb1..1eec6f070 100644 --- a/backend/remote-state/manta/backend_state.go +++ b/backend/remote-state/manta/backend_state.go @@ -8,15 +8,16 @@ import ( "sort" "strings" + tritonErrors "github.com/joyent/triton-go/errors" + "github.com/joyent/triton-go/storage" + "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" - "github.com/hashicorp/terraform/terraform" - tritonErrors "github.com/joyent/triton-go/errors" - "github.com/joyent/triton-go/storage" + "github.com/hashicorp/terraform/states" ) -func (b *Backend) States() ([]string, error) { +func (b *Backend) Workspaces() ([]string, error) { result := []string{backend.DefaultStateName} objs, err := b.storageClient.Dir().List(context.Background(), &storage.ListDirectoryInput{ @@ -39,7 +40,7 @@ func (b *Backend) States() ([]string, error) { return result, nil } -func (b *Backend) DeleteState(name string) error { +func (b *Backend) DeleteWorkspace(name string) error { if name == backend.DefaultStateName || name == "" { return fmt.Errorf("can't delete default state") } @@ -63,7 +64,7 @@ func (b *Backend) DeleteState(name string) error { return nil } -func (b *Backend) State(name string) (state.State, error) { +func (b *Backend) StateMgr(name string) (state.State, error) { if name == "" { return nil, errors.New("missing state name") } @@ -103,7 +104,7 @@ func (b *Backend) State(name string) (state.State, error) { // If we have no state, we have to create an empty state if v := stateMgr.State(); v == nil { - if err := stateMgr.WriteState(terraform.NewState()); err != nil { + if err := stateMgr.WriteState(states.NewState()); err != nil { err = lockUnlock(err) return nil, err } diff --git a/backend/remote-state/s3/backend_state.go b/backend/remote-state/s3/backend_state.go index 842aa45a5..bd4fb90a5 100644 --- a/backend/remote-state/s3/backend_state.go +++ b/backend/remote-state/s3/backend_state.go @@ -8,13 +8,14 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/s3" + "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/states" ) -func (b *Backend) States() ([]string, error) { +func (b *Backend) Workspaces() ([]string, error) { prefix := b.workspaceKeyPrefix + "/" // List bucket root if there is no workspaceKeyPrefix @@ -78,7 +79,7 @@ func (b *Backend) keyEnv(key string) string { return parts[1] } -func (b *Backend) DeleteState(name string) error { +func (b *Backend) DeleteWorkspace(name string) error { if name == backend.DefaultStateName || name == "" { return fmt.Errorf("can't delete default state") } @@ -111,7 +112,7 @@ func (b *Backend) remoteClient(name string) (*RemoteClient, error) { return client, nil } -func (b *Backend) State(name string) (state.State, error) { +func (b *Backend) StateMgr(name string) (state.State, error) { client, err := b.remoteClient(name) if err != nil { return nil, err @@ -126,7 +127,7 @@ func (b *Backend) State(name string) (state.State, error) { // If we need to force-unlock, but for some reason the state no longer // exists, the user will have to use aws tools to manually fix the // situation. - existing, err := b.States() + existing, err := b.Workspaces() if err != nil { return nil, err } @@ -167,7 +168,7 @@ func (b *Backend) State(name string) (state.State, error) { // If we have no state, we have to create an empty state if v := stateMgr.State(); v == nil { - if err := stateMgr.WriteState(terraform.NewState()); err != nil { + if err := stateMgr.WriteState(states.NewState()); err != nil { err = lockUnlock(err) return nil, err } diff --git a/backend/remote-state/swift/backend_state.go b/backend/remote-state/swift/backend_state.go index b8ab98107..42a15d614 100644 --- a/backend/remote-state/swift/backend_state.go +++ b/backend/remote-state/swift/backend_state.go @@ -6,15 +6,15 @@ import ( "github.com/hashicorp/terraform/state/remote" ) -func (b *Backend) States() ([]string, error) { +func (b *Backend) Workspaces() ([]string, error) { return nil, backend.ErrNamedStatesNotSupported } -func (b *Backend) DeleteState(name string) error { +func (b *Backend) DeleteWorkspace(name string) error { return backend.ErrNamedStatesNotSupported } -func (b *Backend) State(name string) (state.State, error) { +func (b *Backend) StateMgr(name string) (state.State, error) { if name != backend.DefaultStateName { return nil, backend.ErrNamedStatesNotSupported } diff --git a/backend/testing.go b/backend/testing.go index d0f32b995..c3da496c3 100644 --- a/backend/testing.go +++ b/backend/testing.go @@ -5,18 +5,17 @@ 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/hcl2/hcl" + "github.com/hashicorp/hcl2/hcldec" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/config/hcl2shim" + "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/state" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statemgr" + "github.com/hashicorp/terraform/tfdiags" ) // TestBackendConfig validates and configures the backend with the @@ -67,31 +66,19 @@ func TestWrapConfig(raw map[string]interface{}) hcl.Body { func TestBackendStates(t *testing.T, b Backend) { t.Helper() - noDefault := false - if _, err := b.State(DefaultStateName); err != nil { - if err == ErrDefaultStateNotSupported { - noDefault = true - } else { - t.Fatalf("error: %v", err) - } - } - - states, err := b.States() - if err != nil { - if err == ErrNamedStatesNotSupported { - t.Logf("TestBackend: named states not supported in %T, skipping", b) - return - } - t.Fatalf("error: %v", err) + workspaces, err := b.Workspaces() + if err == ErrNamedStatesNotSupported { + t.Logf("TestBackend: workspaces not supported in %T, skipping", b) + return } // Test it starts with only the default - if !noDefault && (len(states) != 1 || states[0] != DefaultStateName) { - t.Fatalf("should have default to start: %#v", states) + if len(workspaces) != 1 || workspaces[0] != DefaultStateName { + t.Fatalf("should only have default to start: %#v", workspaces) } // Create a couple states - foo, err := b.State("foo") + foo, err := b.StateMgr("foo") if err != nil { t.Fatalf("error: %s", err) } @@ -102,7 +89,7 @@ func TestBackendStates(t *testing.T, b Backend) { t.Fatalf("should be empty: %s", v) } - bar, err := b.State("bar") + bar, err := b.StateMgr("bar") if err != nil { t.Fatalf("error: %s", err) } @@ -115,24 +102,10 @@ func TestBackendStates(t *testing.T, b Backend) { // Verify they are distinct states that can be read back from storage { - // start with a fresh state, and record the lineage being - // written to "bar" - barState := terraform.NewState() - - // creating the named state may have created a lineage, so use that if it exists. - if s := bar.State(); s != nil && s.Lineage != "" { - barState.Lineage = s.Lineage - } - barLineage := barState.Lineage - - // the foo lineage should be distinct from bar, and unchanged after - // modifying bar - fooState := terraform.NewState() - // creating the named state may have created a lineage, so use that if it exists. - if s := foo.State(); s != nil && s.Lineage != "" { - fooState.Lineage = s.Lineage - } - fooLineage := fooState.Lineage + // We'll use two distinct states here and verify that changing one + // does not also change the other. + barState := states.NewState() + fooState := states.NewState() // write a known state to foo if err := foo.WriteState(fooState); err != nil { @@ -142,6 +115,25 @@ func TestBackendStates(t *testing.T, b Backend) { t.Fatal("error persisting foo state:", err) } + // We'll make "bar" different by adding a fake resource state to it. + barState.SyncWrapper().SetResourceInstanceCurrent( + addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_thing", + Name: "foo", + }, + }.Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte("{}"), + Status: states.ObjectReady, + SchemaVersion: 0, + }, + addrs.ProviderConfig{ + Type: "test", + }.Absolute(addrs.RootModuleInstance), + ) + // write a distinct known state to bar if err := bar.WriteState(barState); err != nil { t.Fatalf("bad: %s", err) @@ -155,17 +147,12 @@ func TestBackendStates(t *testing.T, b Backend) { t.Fatal("error refreshing foo:", err) } fooState = foo.State() - switch { - case fooState == nil: - t.Fatal("nil state read from foo") - case fooState.Lineage == barLineage: - t.Fatalf("bar lineage read from foo: %#v", fooState) - case fooState.Lineage != fooLineage: - t.Fatal("foo lineage alterred") + if fooState.HasResources() { + t.Fatal("after writing a resource to bar, foo now has resources too") } // fetch foo again from the backend - foo, err = b.State("foo") + foo, err = b.StateMgr("foo") if err != nil { t.Fatal("error re-fetching state:", err) } @@ -173,15 +160,12 @@ func TestBackendStates(t *testing.T, b Backend) { t.Fatal("error refreshing foo:", err) } fooState = foo.State() - switch { - case fooState == nil: - t.Fatal("nil state read from foo") - case fooState.Lineage != fooLineage: - t.Fatal("incorrect state returned from backend") + if fooState.HasResources() { + t.Fatal("after writing a resource to bar and re-reading foo, foo now has resources too") } // fetch the bar again from the backend - bar, err = b.State("bar") + bar, err = b.StateMgr("bar") if err != nil { t.Fatal("error re-fetching state:", err) } @@ -189,46 +173,40 @@ func TestBackendStates(t *testing.T, b Backend) { t.Fatal("error refreshing bar:", err) } barState = bar.State() - switch { - case barState == nil: - t.Fatal("nil state read from bar") - case barState.Lineage != barLineage: - t.Fatal("incorrect state returned from backend") + if !barState.HasResources() { + t.Fatal("after writing a resource instance object to bar and re-reading it, the object has vanished") } } // Verify we can now list them { // we determined that named stated are supported earlier - states, err := b.States() + workspaces, err := b.Workspaces() if err != nil { t.Fatal(err) } - sort.Strings(states) + sort.Strings(workspaces) expected := []string{"bar", "default", "foo"} - if noDefault { - expected = []string{"bar", "foo"} - } - if !reflect.DeepEqual(states, expected) { - t.Fatalf("bad: %#v", states) + if !reflect.DeepEqual(workspaces, expected) { + t.Fatalf("wrong workspaces list\ngot: %#v\nwant: %#v", workspaces, expected) } } - // Delete some states - if err := b.DeleteState("foo"); err != nil { + // Delete some workspaces + if err := b.DeleteWorkspace("foo"); err != nil { t.Fatalf("err: %s", err) } // Verify the default state can't be deleted - if err := b.DeleteState(DefaultStateName); err == nil { + if err := b.DeleteWorkspace(DefaultStateName); err == nil { t.Fatal("expected error") } - // Create and delete the foo state again. + // Create and delete the foo workspace again. // Make sure that there are no leftover artifacts from a deleted state // preventing re-creation. - foo, err = b.State("foo") + foo, err = b.StateMgr("foo") if err != nil { t.Fatalf("error: %s", err) } @@ -239,23 +217,20 @@ func TestBackendStates(t *testing.T, b Backend) { t.Fatalf("should be empty: %s", v) } // and delete it again - if err := b.DeleteState("foo"); err != nil { + if err := b.DeleteWorkspace("foo"); err != nil { t.Fatalf("err: %s", err) } // Verify deletion { - states, err := b.States() - if err == ErrNamedStatesNotSupported { + states, err := b.Workspaces() + if err == ErrWorkspacesNotSupported { t.Logf("TestBackend: named states not supported in %T, skipping", b) return } sort.Strings(states) expected := []string{"bar", "default"} - if noDefault { - expected = []string{"bar"} - } if !reflect.DeepEqual(states, expected) { t.Fatalf("bad: %#v", states) } @@ -282,7 +257,7 @@ func testLocks(t *testing.T, b1, b2 Backend, testForceUnlock bool) { t.Helper() // Get the default state for each - b1StateMgr, err := b1.State(DefaultStateName) + b1StateMgr, err := b1.StateMgr(DefaultStateName) if err != nil { t.Fatalf("error: %s", err) } @@ -298,7 +273,7 @@ func testLocks(t *testing.T, b1, b2 Backend, testForceUnlock bool) { t.Logf("TestBackend: testing state locking for %T", b1) - b2StateMgr, err := b2.State(DefaultStateName) + b2StateMgr, err := b2.StateMgr(DefaultStateName) if err != nil { t.Fatalf("error: %s", err) } @@ -326,7 +301,7 @@ func testLocks(t *testing.T, b1, b2 Backend, testForceUnlock bool) { // Make sure we can still get the state.State from another instance even // when locked. This should only happen when a state is loaded via the // backend, and as a remote state. - _, err = b2.State(DefaultStateName) + _, err = b2.StateMgr(DefaultStateName) if err != nil { t.Errorf("failed to read locked state from another backend instance: %s", err) } @@ -389,10 +364,10 @@ func testLocks(t *testing.T, b1, b2 Backend, testForceUnlock bool) { t.Fatal("client B obtained lock while held by client A") } - infoErr, ok := err.(*state.LockError) + infoErr, ok := err.(*statemgr.LockError) if !ok { unlock() - t.Fatalf("expected type *state.LockError, got : %#v", err) + t.Fatalf("expected type *statemgr.LockError, got : %#v", err) } // try to unlock with the second unlocker, using the ID from the error diff --git a/builtin/providers/terraform/data_source_state.go b/builtin/providers/terraform/data_source_state.go index 5fe0cf8cd..22c15a67e 100644 --- a/builtin/providers/terraform/data_source_state.go +++ b/builtin/providers/terraform/data_source_state.go @@ -4,13 +4,13 @@ import ( "fmt" "log" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/backend" backendinit "github.com/hashicorp/terraform/backend/init" - "github.com/hashicorp/terraform/config/hcl2shim" "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/tfdiags" - "github.com/zclconf/go-cty/cty" ) func dataSourceRemoteStateGetSchema() providers.Schema { @@ -109,7 +109,7 @@ func dataSourceRemoteStateRead(d *cty.Value) (cty.Value, tfdiags.Diagnostics) { } } - state, err := b.State(name) + state, err := b.StateMgr(name) if err != nil { diags = diags.Append(tfdiags.AttributeValue( tfdiags.Error, @@ -137,11 +137,10 @@ func dataSourceRemoteStateRead(d *cty.Value) (cty.Value, tfdiags.Diagnostics) { } remoteState := state.State() - if remoteState.Empty() { - log.Println("[DEBUG] empty remote state") - } else { - for k, os := range remoteState.RootModule().Outputs { - outputs[k] = hcl2shim.HCL2ValueFromConfigValue(os.Value) + mod := remoteState.RootModule() + if mod != nil { // should always have a root module in any valid state + for k, os := range mod.OutputValues { + outputs[k] = os.Value } } diff --git a/command/apply.go b/command/apply.go index 76640e682..de7ca3207 100644 --- a/command/apply.go +++ b/command/apply.go @@ -11,9 +11,8 @@ import ( "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/configs" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" ) @@ -45,8 +44,7 @@ func (c *ApplyCommand) Run(args []string) int { cmdFlags.BoolVar(&destroyForce, "force", false, "deprecated: same as auto-approve") } cmdFlags.BoolVar(&refresh, "refresh", true, "refresh") - cmdFlags.IntVar( - &c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism") + cmdFlags.IntVar(&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism") cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") @@ -84,8 +82,7 @@ func (c *ApplyCommand) Run(args []string) int { // Do a detect to determine if we need to do an init + apply. if detected, err := getter.Detect(configPath, pwd, getter.Detectors); err != nil { - c.Ui.Error(fmt.Sprintf( - "Invalid path: %s", err)) + c.Ui.Error(fmt.Sprintf("Invalid path: %s", err)) return 1 } else if !strings.HasPrefix(detected, "file") { // If this isn't a file URL then we're doing an init + @@ -102,39 +99,47 @@ func (c *ApplyCommand) Run(args []string) int { } // Check if the path is a plan - plan, err := c.Plan(configPath) + planFile, err := c.PlanFile(configPath) if err != nil { c.Ui.Error(err.Error()) return 1 } - if c.Destroy && plan != nil { - c.Ui.Error(fmt.Sprintf( - "Destroy can't be called with a plan file.")) + if c.Destroy && planFile != nil { + c.Ui.Error(fmt.Sprintf("Destroy can't be called with a plan file.")) return 1 } - if plan != nil { + if planFile != nil { // Reset the config path for backend loading configPath = "" } var diags tfdiags.Diagnostics - var backendConfig *configs.Backend - if plan == nil { - var configDiags tfdiags.Diagnostics - backendConfig, configDiags = c.loadBackendConfig(configPath) + // Load the backend + var be backend.Enhanced + var beDiags tfdiags.Diagnostics + if planFile == nil { + backendConfig, configDiags := c.loadBackendConfig(configPath) diags = diags.Append(configDiags) if configDiags.HasErrors() { c.showDiagnostics(diags) return 1 } - } - // Load the backend - b, beDiags := c.Backend(&BackendOpts{ - Config: backendConfig, - Plan: plan, - }) + be, beDiags = c.Backend(&BackendOpts{ + Config: backendConfig, + }) + } else { + plan, err := planFile.ReadPlan() + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to read plan from plan file", + fmt.Sprintf("Cannot read the plan from the given plan file: %s.", err), + )) + } + be, beDiags = c.BackendForPlan(plan.Backend) + } diags = diags.Append(beDiags) if beDiags.HasErrors() { c.showDiagnostics(diags) @@ -148,11 +153,11 @@ func (c *ApplyCommand) Run(args []string) int { diags = nil // Build the operation - opReq := c.Operation() + opReq := c.Operation(be) opReq.AutoApprove = autoApprove opReq.Destroy = c.Destroy opReq.ConfigDir = configPath - opReq.Plan = plan + opReq.PlanFile = planFile opReq.PlanRefresh = refresh opReq.Type = backend.OperationTypeApply opReq.AutoApprove = autoApprove @@ -163,7 +168,7 @@ func (c *ApplyCommand) Run(args []string) int { return 1 } - op, err := c.RunOperation(b, opReq) + op, err := c.RunOperation(be, opReq) if err != nil { c.showDiagnostics(err) return 1 @@ -314,26 +319,19 @@ Options: return strings.TrimSpace(helpText) } -func outputsAsString(state *terraform.State, modPath addrs.ModuleInstance, schema []*config.Output, includeHeader bool) string { +func outputsAsString(state *states.State, modPath addrs.ModuleInstance, schema map[string]*configs.Output, includeHeader bool) string { if state == nil { return "" } - ms := state.ModuleByPath(modPath) + ms := state.Module(modPath) if ms == nil { return "" } - outputs := ms.Outputs + outputs := ms.OutputValues outputBuf := new(bytes.Buffer) if len(outputs) > 0 { - schemaMap := make(map[string]*config.Output) - if schema != nil { - for _, s := range schema { - schemaMap[s.Name] = s - } - } - if includeHeader { outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n") } @@ -350,23 +348,14 @@ func outputsAsString(state *terraform.State, modPath addrs.ModuleInstance, schem sort.Strings(ks) for _, k := range ks { - schema, ok := schemaMap[k] + schema, ok := schema[k] if ok && schema.Sensitive { outputBuf.WriteString(fmt.Sprintf("%s = \n", k)) continue } - v := outputs[k] - switch typedV := v.Value.(type) { - case string: - outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, typedV)) - case []interface{}: - outputBuf.WriteString(formatListOutput("", k, typedV)) - outputBuf.WriteString("\n") - case map[string]interface{}: - outputBuf.WriteString(formatMapOutput("", k, typedV)) - outputBuf.WriteString("\n") - } + //v := outputs[k] + outputBuf.WriteString("output printer not yet updated to use the same value formatter as 'terraform console'") } } diff --git a/command/apply_destroy_test.go b/command/apply_destroy_test.go index 894db1b7f..dad38a63b 100644 --- a/command/apply_destroy_test.go +++ b/command/apply_destroy_test.go @@ -5,29 +5,30 @@ import ( "strings" "testing" - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/configs/configschema" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/terraform" ) func TestApply_destroy(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - }, - }, - }, + originalState := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, originalState) p := testProvider() @@ -98,22 +99,20 @@ func TestApply_destroy(t *testing.T) { } func TestApply_destroyLockedState(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - }, - }, - }, + originalState := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, originalState) unlock, err := testLockState("./testdata", statePath) @@ -150,9 +149,7 @@ func TestApply_destroyLockedState(t *testing.T) { } func TestApply_destroyPlan(t *testing.T) { - planPath := testPlanFile(t, &terraform.Plan{ - Config: testModule(t, "apply"), - }) + planPath := testPlanFileNoop(t) p := testProvider() ui := new(cli.MockUi) @@ -174,28 +171,32 @@ func TestApply_destroyPlan(t *testing.T) { } func TestApply_destroyTargeted(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "i-ab123", - }, - }, - "test_load_balancer.foo": &terraform.ResourceState{ - Type: "test_load_balancer", - Primary: &terraform.InstanceState{ - ID: "lb-abc123", - }, - }, - }, + originalState := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"i-ab123"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_load_balancer", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"i-abc123"}`), + Status: states.ObjectReady, + }, + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, originalState) p := testProvider() diff --git a/command/apply_test.go b/command/apply_test.go index 6cbdc0334..b1cbef43e 100644 --- a/command/apply_test.go +++ b/command/apply_test.go @@ -19,8 +19,12 @@ import ( "github.com/mitchellh/cli" "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs/configschema" + "github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" ) @@ -285,8 +289,6 @@ func TestApply_defaultState(t *testing.T) { t.Fatal(err) } - serial := localState.State().Serial - args := []string{ "-auto-approve", testFixturePath("apply"), @@ -303,10 +305,6 @@ func TestApply_defaultState(t *testing.T) { if state == nil { t.Fatal("state should not be nil") } - - if state.Serial <= serial { - t.Fatalf("serial was not incremented. previous:%d, current%d", serial, state.Serial) - } } func TestApply_error(t *testing.T) { @@ -499,9 +497,7 @@ func TestApply_plan(t *testing.T) { defaultInputReader = new(bytes.Buffer) defaultInputWriter = new(bytes.Buffer) - planPath := testPlanFile(t, &terraform.Plan{ - Config: testModule(t, "apply"), - }) + planPath := testPlanFileNoop(t) statePath := testTempFile(t) p := testProvider() @@ -536,8 +532,7 @@ func TestApply_plan(t *testing.T) { } func TestApply_plan_backup(t *testing.T) { - plan := testPlan(t) - planPath := testPlanFile(t, plan) + planPath := testPlanFileNoop(t) statePath := testTempFile(t) backupPath := testTempFile(t) @@ -551,7 +546,7 @@ func TestApply_plan_backup(t *testing.T) { } // create a state file that needs to be backed up - err := (&state.LocalState{Path: statePath}).WriteState(plan.State) + err := statemgr.NewFilesystem(statePath).WriteState(states.NewState()) if err != nil { t.Fatal(err) } @@ -569,7 +564,7 @@ func TestApply_plan_backup(t *testing.T) { } func TestApply_plan_noBackup(t *testing.T) { - planPath := testPlanFile(t, testPlan(t)) + planPath := testPlanFileNoop(t) statePath := testTempFile(t) p := testProvider() @@ -620,14 +615,12 @@ func TestApply_plan_remoteState(t *testing.T) { // Create a remote state state := testState() - conf, srv := testRemoteState(t, state, 200) + backendState, srv := testRemoteState(t, state, 200) defer srv.Close() - state.Remote = conf + testStateFileRemote(t, backendState) - planPath := testPlanFile(t, &terraform.Plan{ - Config: testModule(t, "apply"), - State: state, - }) + _, snap := testModuleWithSnapshot(t, "apply") + planPath := testPlanFile(t, snap, state, &plans.Plan{}) p := testProvider() ui := new(cli.MockUi) @@ -668,9 +661,7 @@ func TestApply_planWithVarFile(t *testing.T) { t.Fatalf("err: %s", err) } - planPath := testPlanFile(t, &terraform.Plan{ - Config: testModule(t, "apply"), - }) + planPath := testPlanFileNoop(t) statePath := testTempFile(t) cwd, err := os.Getwd() @@ -710,9 +701,7 @@ func TestApply_planWithVarFile(t *testing.T) { } func TestApply_planVars(t *testing.T) { - planPath := testPlanFile(t, &terraform.Plan{ - Config: testModule(t, "apply"), - }) + planPath := testPlanFileNoop(t) statePath := testTempFile(t) p := testProvider() @@ -743,9 +732,7 @@ func TestApply_planNoModuleFiles(t *testing.T) { defer testChdir(t, td)() p := testProvider() - planFile := testPlanFile(t, &terraform.Plan{ - Config: testModule(t, "apply-plan-no-module"), - }) + planFile := testPlanFileNoop(t) apply := &ApplyCommand{ Meta: Meta{ @@ -763,22 +750,20 @@ func TestApply_planNoModuleFiles(t *testing.T) { } func TestApply_refresh(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - }, - }, - }, + originalState := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, originalState) p := testProvider() @@ -909,22 +894,20 @@ func TestApply_shutdown(t *testing.T) { } func TestApply_state(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - }, - }, - }, + originalState := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, originalState) p := testProvider() @@ -979,9 +962,6 @@ func TestApply_state(t *testing.T) { backupState := testStateRead(t, statePath+DefaultBackupExtension) - // nil out the ConnInfo since that should not be restored - originalState.RootModule().Resources["test_instance.foo"].Primary.Ephemeral.ConnInfo = nil - actualStr := strings.TrimSpace(backupState.String()) expectedStr := strings.TrimSpace(originalState.String()) if actualStr != expectedStr { @@ -1039,62 +1019,6 @@ func TestApply_sensitiveOutput(t *testing.T) { } } -func TestApply_stateFuture(t *testing.T) { - originalState := testState() - originalState.TFVersion = "99.99.99" - statePath := testStateFile(t, originalState) - - p := testProvider() - ui := new(cli.MockUi) - c := &ApplyCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(p), - Ui: ui, - }, - } - - args := []string{ - "-state", statePath, - "-auto-approve", - testFixturePath("apply"), - } - if code := c.Run(args); code == 0 { - t.Fatal("should fail") - } - - newState := testStateRead(t, statePath) - if !newState.Equal(originalState) { - t.Fatalf("bad: %#v", newState) - } - if newState.TFVersion != originalState.TFVersion { - t.Fatalf("bad: %#v", newState) - } -} - -func TestApply_statePast(t *testing.T) { - originalState := testState() - originalState.TFVersion = "0.1.0" - statePath := testStateFile(t, originalState) - - p := testProvider() - ui := new(cli.MockUi) - c := &ApplyCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(p), - Ui: ui, - }, - } - - args := []string{ - "-state", statePath, - "-auto-approve", - testFixturePath("apply"), - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } -} - func TestApply_vars(t *testing.T) { statePath := testTempFile(t) @@ -1285,23 +1209,20 @@ func TestApply_varFileDefaultJSON(t *testing.T) { } func TestApply_backup(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - }, - }, - }, + originalState := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectReady, }, - }, - } - originalState.Init() - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, originalState) backupPath := testTempFile(t) diff --git a/command/autocomplete.go b/command/autocomplete.go index 82d6a4be2..4b19c1c95 100644 --- a/command/autocomplete.go +++ b/command/autocomplete.go @@ -61,7 +61,7 @@ func (m *Meta) completePredictWorkspaceName() complete.Predictor { return nil } - names, _ := b.States() + names, _ := b.Workspaces() return names }) } diff --git a/command/clistate/state.go b/command/clistate/state.go index a17f96c34..881377d4d 100644 --- a/command/clistate/state.go +++ b/command/clistate/state.go @@ -15,6 +15,7 @@ import ( multierror "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform/helper/slowmessage" "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/states/statemgr" "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" ) @@ -60,8 +61,8 @@ that no one else is holding a lock. // Unlock, which is at a minimum the LockID string returned by the // state.Locker. type Locker interface { - // Lock the provided state, storing the reason string in the LockInfo. - Lock(s state.State, reason string) error + // Lock the provided state manager, storing the reason string in the LockInfo. + Lock(s statemgr.Locker, reason string) error // Unlock the previously locked state. // An optional error can be passed in, and will be combined with any error // from the Unlock operation. @@ -72,7 +73,7 @@ type locker struct { mu sync.Mutex ctx context.Context timeout time.Duration - state state.State + state statemgr.Locker ui cli.Ui color *colorstring.Colorize lockID string @@ -100,7 +101,7 @@ func NewLocker( // Locker locks the given state and outputs to the user if locking is taking // longer than the threshold. The lock is retried until the context is // cancelled. -func (l *locker) Lock(s state.State, reason string) error { +func (l *locker) Lock(s statemgr.Locker, reason string) error { l.mu.Lock() defer l.mu.Unlock() @@ -113,7 +114,7 @@ func (l *locker) Lock(s state.State, reason string) error { lockInfo.Operation = reason err := slowmessage.Do(LockThreshold, func() error { - id, err := state.LockWithContext(ctx, s, lockInfo) + id, err := statemgr.LockWithContext(ctx, s, lockInfo) l.lockID = id return err }, func() { @@ -165,7 +166,7 @@ func NewNoopLocker() Locker { return noopLocker{} } -func (l noopLocker) Lock(state.State, string) error { +func (l noopLocker) Lock(statemgr.Locker, string) error { return nil } diff --git a/command/command_test.go b/command/command_test.go index 0e00c7ed5..5062df4f1 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -19,10 +19,20 @@ import ( "syscall" "testing" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs/configload" + "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/helper/logging" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/plans/planfile" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statefile" + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/version" ) // This is the directory where our test fixtures are. @@ -115,6 +125,12 @@ func metaOverridesForProviderAndProvisioner(p terraform.ResourceProvider, pr ter func testModule(t *testing.T, name string) *configs.Config { t.Helper() + c, _ := testModuleWithSnapshot(t, name) + return c +} + +func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *configload.Snapshot) { + t.Helper() dir := filepath.Join(fixtureDir, name) @@ -131,48 +147,59 @@ func testModule(t *testing.T, name string) *configs.Config { t.Fatal(diags.Error()) } - config, diags := loader.LoadConfig(dir) + config, snap, diags := loader.LoadConfigWithSnapshot(dir) if diags.HasErrors() { t.Fatal(diags.Error()) } - return config + return config, snap } // testPlan returns a non-nil noop plan. -func testPlan(t *testing.T) *terraform.Plan { +func testPlan(t *testing.T) *plans.Plan { t.Helper() - - state := terraform.NewState() - state.RootModule().Outputs["foo"] = &terraform.OutputState{ - Type: "string", - Value: "foo", - } - - return &terraform.Plan{ - Config: testModule(t, "apply"), - State: state, + return &plans.Plan{ + Changes: plans.NewChanges(), } } -func testPlanFile(t *testing.T, plan *terraform.Plan) string { +func testPlanFile(t *testing.T, configSnap *configload.Snapshot, state *states.State, plan *plans.Plan) string { t.Helper() - path := testTempFile(t) - - f, err := os.Create(path) - if err != nil { - t.Fatalf("err: %s", err) + stateFile := &statefile.File{ + Lineage: "command.testPlanFile", + State: state, + TerraformVersion: version.SemVer, } - defer f.Close() - if err := terraform.WritePlan(plan, f); err != nil { - t.Fatalf("err: %s", err) + path := testTempFile(t) + err := planfile.Create(path, configSnap, stateFile, plan) + if err != nil { + t.Fatalf("failed to create temporary plan file: %s", err) } return path } +// testPlanFileNoop is a shortcut function that creates a plan file that +// represents no changes and returns its path. This is useful when a test +// just needs any plan file, and it doesn't matter what is inside it. +func testPlanFileNoop(t *testing.T) string { + snap := &configload.Snapshot{ + Modules: map[string]*configload.SnapshotModule{ + "": { + Dir: ".", + Files: map[string][]byte{ + "main.tf": nil, + }, + }, + }, + } + state := states.NewState() + plan := testPlan(t) + return testPlanFile(t, snap, state, plan) +} + func testReadPlan(t *testing.T, path string) *terraform.Plan { t.Helper() @@ -191,40 +218,110 @@ func testReadPlan(t *testing.T, path string) *terraform.Plan { } // testState returns a test State structure that we use for a lot of tests. -func testState() *terraform.State { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - }, - }, - }, - Outputs: map[string]*terraform.OutputState{}, +func testState() *states.State { + return states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectReady, }, - }, - } - state.Init() - return state + addrs.ProviderConfig{ + Type: "test", + }.Absolute(addrs.RootModuleInstance), + ) + }) } -func testStateFile(t *testing.T, s *terraform.State) string { +// writeStateForTesting is a helper that writes the given naked state to the +// given writer, generating a stub *statefile.File wrapper which is then +// immediately discarded. +func writeStateForTesting(state *states.State, w io.Writer) error { + sf := &statefile.File{ + Serial: 0, + Lineage: "fake-for-testing", + State: state, + } + return statefile.Write(sf, w) +} + +// testStateMgrCurrentLineage returns the current lineage for the given state +// manager, or the empty string if it does not use lineage. This is primarily +// for testing against the local backend, which always supports lineage. +func testStateMgrCurrentLineage(mgr statemgr.Persistent) string { + if pm, ok := mgr.(statemgr.PersistentMeta); ok { + m := pm.StateSnapshotMeta() + return m.Lineage + } + return "" +} + +// markStateForMatching is a helper that writes a specific marker value to +// a state so that it can be recognized later with getStateMatchingMarker. +// +// Internally this just sets a root module output value called "testing_mark" +// to the given string value. If the state is being checked in other ways, +// the test code may need to compensate for the addition or overwriting of this +// special output value name. +// +// The given mark string is returned verbatim, to allow the following pattern +// in tests: +// +// mark := markStateForMatching(state, "foo") +// // (do stuff to the state) +// assertStateHasMarker(state, mark) +func markStateForMatching(state *states.State, mark string) string { + state.RootModule().SetOutputValue("testing_mark", cty.StringVal(mark), false) + return mark +} + +// getStateMatchingMarker is used with markStateForMatching to retrieve the +// mark string previously added to the given state. If no such mark is present, +// the result is an empty string. +func getStateMatchingMarker(state *states.State) string { + os := state.RootModule().OutputValues["testing_mark"] + if os == nil { + return "" + } + v := os.Value + if v.Type() == cty.String && v.IsKnown() && !v.IsNull() { + return v.AsString() + } + return "" +} + +// stateHasMarker is a helper around getStateMatchingMarker that also includes +// the equality test, for more convenient use in test assertion branches. +func stateHasMarker(state *states.State, want string) bool { + return getStateMatchingMarker(state) == want +} + +// assertStateHasMarker wraps stateHasMarker to automatically generate a +// fatal test result (i.e. t.Fatal) if the marker doesn't match. +func assertStateHasMarker(t *testing.T, state *states.State, want string) { + if !stateHasMarker(state, want) { + t.Fatalf("wrong state marker\ngot: %q\nwant: %q", getStateMatchingMarker(state), want) + } +} + +func testStateFile(t *testing.T, s *states.State) string { t.Helper() path := testTempFile(t) f, err := os.Create(path) if err != nil { - t.Fatalf("err: %s", err) + t.Fatalf("failed to create temporary state file %s: %s", path, err) } defer f.Close() - if err := terraform.WriteState(s, f); err != nil { - t.Fatalf("err: %s", err) + err = writeStateForTesting(s, f) + if err != nil { + t.Fatalf("failed to write state to temporary file %s: %s", path, err) } return path @@ -272,7 +369,7 @@ func testStateFileRemote(t *testing.T, s *terraform.State) string { } // testStateRead reads the state from a file -func testStateRead(t *testing.T, path string) *terraform.State { +func testStateRead(t *testing.T, path string) *states.State { t.Helper() f, err := os.Open(path) @@ -281,12 +378,34 @@ func testStateRead(t *testing.T, path string) *terraform.State { } defer f.Close() - newState, err := terraform.ReadState(f) + sf, err := statefile.Read(f) if err != nil { t.Fatalf("err: %s", err) } - return newState + return sf.State +} + +// testDataStateRead reads a "data state", which is a file format resembling +// our state format v3 that is used only to track current backend settings. +// +// This old format still uses *terraform.State, but should be replaced with +// a more specialized type in a later release. +func testDataStateRead(t *testing.T, path string) *terraform.State { + t.Helper() + + f, err := os.Open(path) + if err != nil { + t.Fatalf("err: %s", err) + } + defer f.Close() + + s, err := terraform.ReadState(f) + if err != nil { + t.Fatalf("err: %s", err) + } + + return s } // testStateOutput tests that the state at the given path contains @@ -571,7 +690,11 @@ func testBackendState(t *testing.T, s *terraform.State, c int) (*terraform.State // testRemoteState is used to make a test HTTP server to return a given // state file that can be used for testing legacy remote state. -func testRemoteState(t *testing.T, s *terraform.State, c int) (*terraform.RemoteState, *httptest.Server) { +// +// The return values are a *terraform.State instance that should be written +// as the "data state" (really: backend state) and the server that the +// returned data state refers to. +func testRemoteState(t *testing.T, s *states.State, c int) (*terraform.State, *httptest.Server) { t.Helper() var b64md5 string @@ -591,25 +714,32 @@ func testRemoteState(t *testing.T, s *terraform.State, c int) (*terraform.Remote resp.Write(buf.Bytes()) } + retState := terraform.NewState() + srv := httptest.NewServer(http.HandlerFunc(cb)) - remote := &terraform.RemoteState{ - Type: "http", - Config: map[string]string{"address": srv.URL}, + b := &terraform.BackendState{ + Type: "http", } + b.SetConfig(cty.ObjectVal(map[string]cty.Value{ + "address": cty.StringVal(srv.URL), + }), &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "address": { + Type: cty.String, + Required: true, + }, + }, + }) + retState.Backend = b if s != nil { - // Set the remote data - s.Remote = remote - - enc := json.NewEncoder(buf) - if err := enc.Encode(s); err != nil { - t.Fatalf("err: %v", err) + err := statefile.Write(&statefile.File{State: s}, buf) + if err != nil { + t.Fatalf("failed to write initial state: %v", err) } - md5 := md5.Sum(buf.Bytes()) - b64md5 = base64.StdEncoding.EncodeToString(md5[:16]) } - return remote, srv + return retState, srv } // testlockState calls a separate process to the lock the state file at path. diff --git a/command/console.go b/command/console.go index 3ddc26d57..749f4a2f1 100644 --- a/command/console.go +++ b/command/console.go @@ -66,7 +66,7 @@ func (c *ConsoleCommand) Run(args []string) int { } // Build the operation - opReq := c.Operation() + opReq := c.Operation(b) opReq.ConfigDir = configPath opReq.ConfigLoader, err = c.initConfigLoader() if err != nil { diff --git a/command/format/plan.go b/command/format/plan.go index 65d2c9d27..7b333017c 100644 --- a/command/format/plan.go +++ b/command/format/plan.go @@ -6,9 +6,12 @@ import ( "sort" "strings" - "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/colorstring" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/terraform" ) // Plan is a representation of a plan optimized for display to @@ -59,100 +62,63 @@ type PlanStats struct { } // NewPlan produces a display-oriented Plan from a terraform.Plan. -func NewPlan(plan *terraform.Plan) *Plan { +func NewPlan(changes *plans.Changes) *Plan { ret := &Plan{} - if plan == nil || plan.Diff == nil || plan.Diff.Empty() { + if changes == nil { // Nothing to do! return ret } - for _, m := range plan.Diff.Modules { - var modulePath []string - if !m.IsRoot() { - // trim off the leading "root" path segment, since it's implied - // when we use a path in a resource address. - modulePath = m.Path[1:] + for _, rc := range changes.Resources { + addr := rc.Addr + dataSource := addr.Resource.Resource.Mode == addrs.DataResourceMode + + // We create "delete" actions for data resources so we can clean + // up their entries in state, but this is an implementation detail + // that users shouldn't see. + if dataSource && rc.Action == plans.Delete { + continue } - for k, r := range m.Resources { - if r.Empty() { - continue - } + // For now we'll shim this to work with our old types. + // TODO: Update for the new plan types, ideally also switching over to + // a structural diff renderer instead of a flat renderer. + did := &InstanceDiff{ + Addr: terraform.NewLegacyResourceInstanceAddress(addr), + } - addr, err := terraform.ParseResourceAddressForInstanceDiff(modulePath, k) - if err != nil { - // should never happen; indicates invalid diff - panic("invalid resource address in diff") - } - - dataSource := addr.Mode == config.DataResourceMode - - // We create "destroy" actions for data resources so we can clean - // up their entries in state, but this is an implementation detail - // that users shouldn't see. - if dataSource && r.ChangeType() == terraform.DiffDestroy { - continue - } - - did := &InstanceDiff{ - Addr: addr, - Action: r.ChangeType(), - Tainted: r.DestroyTainted, - Deposed: r.DestroyDeposed, - } - - if dataSource && did.Action == terraform.DiffCreate { - // Use "refresh" as the action for display, since core - // currently uses Create for this. + switch rc.Action { + case plans.Create: + if dataSource { + // Use "refresh" as the action for display, but core + // currently uses Create for this internally. + // FIXME: Update core to generate plans.Read for this case + // instead. did.Action = terraform.DiffRefresh + } else { + did.Action = terraform.DiffCreate } - - ret.Resources = append(ret.Resources, did) - - if did.Action == terraform.DiffDestroy { - // Don't show any outputs for destroy actions - continue - } - - for k, a := range r.Attributes { - var action terraform.DiffChangeType - switch { - case a.NewRemoved: - action = terraform.DiffDestroy - case did.Action == terraform.DiffCreate: - action = terraform.DiffCreate - default: - action = terraform.DiffUpdate - } - - did.Attributes = append(did.Attributes, &AttributeDiff{ - Path: k, - Action: action, - - OldValue: a.Old, - NewValue: a.New, - - Sensitive: a.Sensitive, - ForcesNew: a.RequiresNew, - NewComputed: a.NewComputed, - }) - } - - // Sort the attributes by their paths for display - sort.Slice(did.Attributes, func(i, j int) bool { - iPath := did.Attributes[i].Path - jPath := did.Attributes[j].Path - - // as a special case, "id" is always first - switch { - case iPath != jPath && (iPath == "id" || jPath == "id"): - return iPath == "id" - default: - return iPath < jPath - } - }) - + case plans.Read: + did.Action = terraform.DiffRefresh + case plans.Delete: + did.Action = terraform.DiffDestroy + case plans.Replace: + did.Action = terraform.DiffDestroyCreate + case plans.Update: + did.Action = terraform.DiffUpdate + default: + panic(fmt.Sprintf("unexpected change action %s", rc.Action)) } + + if rc.DeposedKey != states.NotDeposed { + did.Deposed = true + } + + // Since this is just a temporary stub implementation on the way + // to us replacing this with the structural diff renderer, we currently + // don't include any attributes here. + // FIXME: Implement the structural diff renderer to replace this + // codepath altogether. } // Sort the instance diffs by their addresses for display. diff --git a/command/format/state.go b/command/format/state.go index f8a658bce..631c3d055 100644 --- a/command/format/state.go +++ b/command/format/state.go @@ -6,14 +6,16 @@ import ( "sort" "strings" - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/colorstring" + + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/terraform" ) // StateOpts are the options for formatting a state. type StateOpts struct { // State is the state to format. This is required. - State *terraform.State + State *states.State // Color is the colorizer. This is optional. Color *colorstring.Colorize @@ -34,7 +36,10 @@ func State(opts *StateOpts) string { return "The state file is empty. No resources are represented." } - var buf bytes.Buffer + // FIXME: State formatter not yet updated for new state types + return "FIXME: State formatter not yet updated for new state types" + + /*var buf bytes.Buffer buf.WriteString("[reset]") // Format all the modules @@ -76,6 +81,7 @@ func State(opts *StateOpts) string { } return opts.Color.Color(strings.TrimSpace(buf.String())) + */ } func formatStateModuleExpand( diff --git a/command/graph.go b/command/graph.go index 5bf97f622..03612f6a1 100644 --- a/command/graph.go +++ b/command/graph.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/backend" @@ -46,12 +47,13 @@ func (c *GraphCommand) Run(args []string) int { } // Check if the path is a plan - plan, err := c.Plan(configPath) + var plan *plans.Plan + planFile, err := c.PlanFile(configPath) if err != nil { c.Ui.Error(err.Error()) return 1 } - if plan != nil { + if planFile != nil { // Reset for backend loading configPath = "" } @@ -84,10 +86,10 @@ func (c *GraphCommand) Run(args []string) int { } // Build the operation - opReq := c.Operation() + opReq := c.Operation(b) opReq.ConfigDir = configPath opReq.ConfigLoader, err = c.initConfigLoader() - opReq.Plan = plan + opReq.PlanFile = planFile if err != nil { diags = diags.Append(err) c.showDiagnostics(diags) diff --git a/command/graph_test.go b/command/graph_test.go index 131312b1f..6cfcd1ee0 100644 --- a/command/graph_test.go +++ b/command/graph_test.go @@ -5,8 +5,11 @@ import ( "strings" "testing" - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" ) func TestGraph(t *testing.T) { @@ -107,22 +110,25 @@ func TestGraph_plan(t *testing.T) { tmp, cwd := testCwd(t) defer testFixCwd(t, tmp, cwd) - planPath := testPlanFile(t, &terraform.Plan{ - Diff: &terraform.Diff{ - Modules: []*terraform.ModuleDiff{ - &terraform.ModuleDiff{ - Path: []string{"root"}, - Resources: map[string]*terraform.InstanceDiff{ - "test_instance.bar": &terraform.InstanceDiff{ - Destroy: true, - }, - }, - }, - }, + plan := &plans.Plan{ + Changes: plans.NewChanges(), + } + plan.Changes.Resources = append(plan.Changes.Resources, &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: plans.DynamicValue(`{}`), + After: plans.DynamicValue(`null`), }, - - Config: testModule(t, "graph"), + ProviderAddr: addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), }) + _, configSnap := testModuleWithSnapshot(t, "graph") + + planPath := testPlanFile(t, configSnap, states.NewState(), plan) ui := new(cli.MockUi) c := &GraphCommand{ diff --git a/command/hook_ui.go b/command/hook_ui.go index 76b1ca59c..3600af606 100644 --- a/command/hook_ui.go +++ b/command/hook_ui.go @@ -10,9 +10,15 @@ import ( "time" "unicode" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/plans" + "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/terraform" ) const defaultPeriodicUiTimer = 10 * time.Second @@ -31,6 +37,8 @@ type UiHook struct { ui cli.Ui } +var _ terraform.Hook = (*UiHook)(nil) + // uiResourceState tracks the state of a single resource type uiResourceState struct { Name string @@ -53,37 +61,21 @@ const ( uiResourceDestroy ) -func (h *UiHook) PreApply( - n *terraform.InstanceInfo, - s *terraform.InstanceState, - d *terraform.InstanceDiff) (terraform.HookAction, error) { +func (h *UiHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) { h.once.Do(h.init) - // if there's no diff, there's nothing to output - if d.Empty() { - return terraform.HookActionContinue, nil - } - - id := n.HumanId() - addr := n.ResourceAddress() - - op := uiResourceModify - if d.Destroy { - op = uiResourceDestroy - } else if s.ID == "" { - op = uiResourceCreate - } - var operation string - switch op { - case uiResourceModify: - operation = "Modifying..." - case uiResourceDestroy: + var op uiResourceOp + switch action { + case plans.Delete: operation = "Destroying..." - case uiResourceCreate: + op = uiResourceDestroy + case plans.Create: operation = "Creating..." - case uiResourceUnknown: - return terraform.HookActionContinue, nil + op = uiResourceCreate + default: + operation = "Modifying..." + op = uiResourceModify } attrBuf := new(bytes.Buffer) @@ -92,7 +84,11 @@ func (h *UiHook) PreApply( // determine the longest key so that we can align them all. keyLen := 0 - dAttrs := d.CopyAttributes() + // FIXME: This is stubbed out in preparation for rewriting it to use + // a structural presentation rather than the old-style flatmap one. + // We just assume no attributes at all for now, pending new code to + // work with the two cty.Values we are given. + dAttrs := map[string]terraform.ResourceAttrDiff{} keys := make([]string, 0, len(dAttrs)) for key, _ := range dAttrs { // Skip the ID since we do that specially @@ -109,7 +105,7 @@ func (h *UiHook) PreApply( // Go through and output each attribute for _, attrK := range keys { - attrDiff, _ := d.GetAttribute(attrK) + attrDiff := dAttrs[attrK] v := attrDiff.New u := attrDiff.Old @@ -136,18 +132,16 @@ func (h *UiHook) PreApply( } var stateId, stateIdSuffix string - if s != nil && s.ID != "" { - stateId = s.ID - stateIdSuffix = fmt.Sprintf(" (ID: %s)", truncateId(s.ID, maxIdLen)) - } h.ui.Output(h.Colorize.Color(fmt.Sprintf( "[reset][bold]%s: %s%s[reset]%s", addr, operation, stateIdSuffix, - attrString))) + attrString, + ))) + id := addr.String() uiState := uiResourceState{ Name: id, ResourceId: stateId, @@ -205,13 +199,9 @@ func (h *UiHook) stillApplying(state uiResourceState) { } } -func (h *UiHook) PostApply( - n *terraform.InstanceInfo, - s *terraform.InstanceState, - applyerr error) (terraform.HookAction, error) { +func (h *UiHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, applyerr error) (terraform.HookAction, error) { - id := n.HumanId() - addr := n.ResourceAddress() + id := addr.String() h.l.Lock() state := h.resources[id] @@ -223,9 +213,6 @@ func (h *UiHook) PostApply( h.l.Unlock() var stateIdSuffix string - if s != nil && s.ID != "" { - stateIdSuffix = fmt.Sprintf(" (ID: %s)", truncateId(s.ID, maxIdLen)) - } var msg string switch state.Op { @@ -253,31 +240,23 @@ func (h *UiHook) PostApply( return terraform.HookActionContinue, nil } -func (h *UiHook) PreDiff( - n *terraform.InstanceInfo, - s *terraform.InstanceState) (terraform.HookAction, error) { +func (h *UiHook) PreDiff(addr addrs.AbsResourceInstance, gen states.Generation, priorState, proposedNewState cty.Value) (terraform.HookAction, error) { return terraform.HookActionContinue, nil } -func (h *UiHook) PreProvision( - n *terraform.InstanceInfo, - provId string) (terraform.HookAction, error) { - addr := n.ResourceAddress() +func (h *UiHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (terraform.HookAction, error) { h.ui.Output(h.Colorize.Color(fmt.Sprintf( "[reset][bold]%s: Provisioning with '%s'...[reset]", - addr, provId))) + addr, typeName, + ))) return terraform.HookActionContinue, nil } -func (h *UiHook) ProvisionOutput( - n *terraform.InstanceInfo, - provId string, - msg string) { - addr := n.ResourceAddress() +func (h *UiHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, msg string) { var buf bytes.Buffer buf.WriteString(h.Colorize.Color("[reset]")) - prefix := fmt.Sprintf("%s (%s): ", addr, provId) + prefix := fmt.Sprintf("%s (%s): ", addr, typeName) s := bufio.NewScanner(strings.NewReader(msg)) s.Split(scanLines) for s.Scan() { @@ -290,19 +269,10 @@ func (h *UiHook) ProvisionOutput( h.ui.Output(strings.TrimSpace(buf.String())) } -func (h *UiHook) PreRefresh( - n *terraform.InstanceInfo, - s *terraform.InstanceState) (terraform.HookAction, error) { +func (h *UiHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (terraform.HookAction, error) { h.once.Do(h.init) - addr := n.ResourceAddress() - var stateIdSuffix string - // Data resources refresh before they have ids, whereas managed - // resources are only refreshed when they have ids. - if s.ID != "" { - stateIdSuffix = fmt.Sprintf(" (ID: %s)", truncateId(s.ID, maxIdLen)) - } h.ui.Output(h.Colorize.Color(fmt.Sprintf( "[reset][bold]%s: Refreshing state...%s", @@ -310,30 +280,26 @@ func (h *UiHook) PreRefresh( return terraform.HookActionContinue, nil } -func (h *UiHook) PreImportState( - n *terraform.InstanceInfo, - id string) (terraform.HookAction, error) { +func (h *UiHook) PreImportState(addr addrs.AbsResourceInstance, importID string) (terraform.HookAction, error) { h.once.Do(h.init) - addr := n.ResourceAddress() h.ui.Output(h.Colorize.Color(fmt.Sprintf( "[reset][bold]%s: Importing from ID %q...", - addr, id))) + addr, importID, + ))) return terraform.HookActionContinue, nil } -func (h *UiHook) PostImportState( - n *terraform.InstanceInfo, - s []*terraform.InstanceState) (terraform.HookAction, error) { +func (h *UiHook) PostImportState(addr addrs.AbsResourceInstance, imported []*states.ImportedObject) (terraform.HookAction, error) { h.once.Do(h.init) - addr := n.ResourceAddress() h.ui.Output(h.Colorize.Color(fmt.Sprintf( "[reset][bold][green]%s: Import complete!", addr))) - for _, s := range s { + for _, s := range imported { h.ui.Output(h.Colorize.Color(fmt.Sprintf( - "[reset][green] Imported %s (ID: %s)", - s.Ephemeral.Type, s.ID))) + "[reset][green] Imported %s", + s.ResourceType, + ))) } return terraform.HookActionContinue, nil diff --git a/command/hook_ui_test.go b/command/hook_ui_test.go index 643211b58..cce88d03c 100644 --- a/command/hook_ui_test.go +++ b/command/hook_ui_test.go @@ -6,9 +6,14 @@ import ( "testing" "time" - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/terraform" ) func TestUiHookPreApply_periodicTimer(t *testing.T) { @@ -30,28 +35,27 @@ func TestUiHookPreApply_periodicTimer(t *testing.T) { }, } - n := &terraform.InstanceInfo{ - Id: "data.aws_availability_zones.available", - ModulePath: []string{"root"}, - Type: "aws_availability_zones", - } + addr := addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "aws_availability_zones", + Name: "available", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - s := &terraform.InstanceState{ - ID: "2017-03-05 10:56:59.298784526 +0000 UTC", - Attributes: map[string]string{ - "id": "2017-03-05 10:56:59.298784526 +0000 UTC", - "names.#": "4", - "names.0": "us-east-1a", - "names.1": "us-east-1b", - "names.2": "us-east-1c", - "names.3": "us-east-1d", - }, - } - d := &terraform.InstanceDiff{ - Destroy: true, - } + priorState := cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "names": cty.List(cty.String), + })) + plannedNewState := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("2017-03-05 10:56:59.298784526 +0000 UTC"), + "names": cty.ListVal([]cty.Value{ + cty.StringVal("us-east-1a"), + cty.StringVal("us-east-1b"), + cty.StringVal("us-east-1c"), + cty.StringVal("us-east-1d"), + }), + }) - action, err := h.PreApply(n, s, d) + action, err := h.PreApply(addr, states.CurrentGen, plans.Delete, priorState, plannedNewState) if err != nil { t.Fatal(err) } @@ -62,7 +66,7 @@ func TestUiHookPreApply_periodicTimer(t *testing.T) { time.Sleep(3100 * time.Millisecond) // stop the background writer - uiState := h.resources[n.HumanId()] + uiState := h.resources[addr.String()] close(uiState.DoneCh) <-uiState.done @@ -101,28 +105,27 @@ func TestUiHookPreApply_destroy(t *testing.T) { }, } - n := &terraform.InstanceInfo{ - Id: "data.aws_availability_zones.available", - ModulePath: []string{"root"}, - Type: "aws_availability_zones", - } + addr := addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "aws_availability_zones", + Name: "available", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - s := &terraform.InstanceState{ - ID: "2017-03-05 10:56:59.298784526 +0000 UTC", - Attributes: map[string]string{ - "id": "2017-03-05 10:56:59.298784526 +0000 UTC", - "names.#": "4", - "names.0": "us-east-1a", - "names.1": "us-east-1b", - "names.2": "us-east-1c", - "names.3": "us-east-1d", - }, - } - d := &terraform.InstanceDiff{ - Destroy: true, - } + priorState := cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "names": cty.List(cty.String), + })) + plannedNewState := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("2017-03-05 10:56:59.298784526 +0000 UTC"), + "names": cty.ListVal([]cty.Value{ + cty.StringVal("us-east-1a"), + cty.StringVal("us-east-1b"), + cty.StringVal("us-east-1c"), + cty.StringVal("us-east-1d"), + }), + }) - action, err := h.PreApply(n, s, d) + action, err := h.PreApply(addr, states.CurrentGen, plans.Delete, priorState, plannedNewState) if err != nil { t.Fatal(err) } @@ -130,6 +133,11 @@ func TestUiHookPreApply_destroy(t *testing.T) { t.Fatalf("Expected hook to continue, given: %#v", action) } + // stop the background writer + uiState := h.resources[addr.String()] + close(uiState.DoneCh) + <-uiState.done + expectedOutput := "data.aws_availability_zones.available: Destroying... (ID: 2017-03-05 10:56:59.298784526 +0000 UTC)\n" output := ui.OutputWriter.String() if output != expectedOutput { @@ -161,12 +169,18 @@ func TestUiHookPostApply_emptyState(t *testing.T) { }, } - n := &terraform.InstanceInfo{ - Id: "data.google_compute_zones.available", - ModulePath: []string{"root"}, - Type: "google_compute_zones", - } - action, err := h.PostApply(n, nil, nil) + addr := addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "google_compute_zones", + Name: "available", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) + + newState := cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "names": cty.List(cty.String), + })) + + action, err := h.PostApply(addr, states.CurrentGen, newState, nil) if err != nil { t.Fatal(err) } diff --git a/command/import.go b/command/import.go index 8480c91ee..93ab80495 100644 --- a/command/import.go +++ b/command/import.go @@ -7,12 +7,10 @@ import ( "os" "strings" - "github.com/hashicorp/terraform/addrs" - + "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl/hclsyntax" - "github.com/hashicorp/hcl2/hcl" - + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/terraform" @@ -208,7 +206,7 @@ func (c *ImportCommand) Run(args []string) int { } // Build the operation - opReq := c.Operation() + opReq := c.Operation(b) opReq.ConfigDir = configPath opReq.ConfigLoader, err = c.initConfigLoader() if err != nil { diff --git a/command/init.go b/command/init.go index a700fc659..32a814631 100644 --- a/command/init.go +++ b/command/init.go @@ -8,17 +8,19 @@ import ( "strings" "github.com/hashicorp/hcl2/hcl" + "github.com/posener/complete" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/backend" backendinit "github.com/hashicorp/terraform/backend/init" "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/plugin" "github.com/hashicorp/terraform/plugin/discovery" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" - "github.com/posener/complete" - "github.com/zclconf/go-cty/cty" ) // InitCommand is a Command implementation that takes a Terraform @@ -266,13 +268,13 @@ func (c *InitCommand) Run(args []string) int { } } - var state *terraform.State + var state *states.State // If we have a functional backend (either just initialized or initialized // on a previous run) we'll use the current state as a potential source // of provider dependencies. if back != nil { - sMgr, err := back.State(c.Workspace()) + sMgr, err := back.StateMgr(c.Workspace()) if err != nil { c.Ui.Error(fmt.Sprintf("Error loading state: %s", err)) return 1 @@ -391,17 +393,12 @@ func (c *InitCommand) backendConfigOverrideBody(flags rawFlags, schema *configsc // Load the complete module tree, and fetch any missing providers. // This method outputs its own Ui. -func (c *InitCommand) getProviders(path string, state *terraform.State, upgrade bool) tfdiags.Diagnostics { +func (c *InitCommand) getProviders(path string, state *states.State, upgrade bool) tfdiags.Diagnostics { config, diags := c.loadConfig(path) if diags.HasErrors() { return diags } - if err := terraform.CheckStateVersion(state, false); err != nil { - diags = diags.Append(err) - return diags - } - var available discovery.PluginMetaSet if upgrade { // If we're in upgrade mode, we ignore any auto-installed plugins diff --git a/command/init_test.go b/command/init_test.go index add530bbb..dd6e77d38 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -285,8 +285,7 @@ func TestInit_backendUnset(t *testing.T) { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) } - s := testStateRead(t, filepath.Join( - DefaultDataDir, DefaultStateFilename)) + s := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if !s.Backend.Empty() { t.Fatal("should not have backend config") } @@ -314,7 +313,7 @@ func TestInit_backendConfigFile(t *testing.T) { } // Read our saved backend config and verify we have our settings - state := testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) + state := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if got, want := string(state.Backend.ConfigRaw), `{"path":"hello"}`; got != want { t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) } @@ -346,7 +345,7 @@ func TestInit_backendConfigFileChange(t *testing.T) { } // Read our saved backend config and verify we have our settings - state := testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) + state := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if got, want := string(state.Backend.ConfigRaw), `{"path":"hello"}`; got != want { t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) } @@ -373,7 +372,7 @@ func TestInit_backendConfigKV(t *testing.T) { } // Read our saved backend config and verify we have our settings - state := testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) + state := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if got, want := string(state.Backend.ConfigRaw), `{"path":"hello"}`; got != want { t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) } @@ -447,7 +446,7 @@ func TestInit_backendReinitWithExtra(t *testing.T) { } // Read our saved backend config and verify we have our settings - state := testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) + state := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if got, want := string(state.Backend.ConfigRaw), `{"path":"hello"}`; got != want { t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) } @@ -460,7 +459,7 @@ func TestInit_backendReinitWithExtra(t *testing.T) { if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) } - state = testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) + state = testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if got, want := string(state.Backend.ConfigRaw), `{"path":"hello"}`; got != want { t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) } @@ -489,7 +488,7 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) { } // Read our saved backend config and verify we have our settings - state := testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) + state := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if got, want := string(state.Backend.ConfigRaw), `{"path":"foo"}`; got != want { t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) } @@ -506,7 +505,7 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) { if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) } - state = testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) + state = testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if state.Backend.Hash == backendHash { t.Fatal("state.Backend.Hash was not updated") diff --git a/command/meta.go b/command/meta.go index aa7e8972f..a437d09ab 100644 --- a/command/meta.go +++ b/command/meta.go @@ -318,13 +318,12 @@ const ( // context with the settings from this Meta. func (m *Meta) contextOpts() *terraform.ContextOpts { var opts terraform.ContextOpts - opts.Hooks = []terraform.Hook{m.uiHook(), &terraform.DebugHook{}} + opts.Hooks = []terraform.Hook{m.uiHook()} opts.Hooks = append(opts.Hooks, m.ExtraHooks...) opts.Targets = m.targets opts.UIInput = m.UIInput() opts.Parallelism = m.parallelism - opts.Shadow = m.shadow // If testingOverrides are set, we'll skip the plugin discovery process // and just work with what we've been given, thus allowing the tests diff --git a/command/meta_backend.go b/command/meta_backend.go index 7f8ab6a78..0691d30bd 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -15,16 +15,18 @@ import ( "github.com/hashicorp/errwrap" "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcldec" + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" + "github.com/hashicorp/terraform/backend" backendinit "github.com/hashicorp/terraform/backend/init" backendlocal "github.com/hashicorp/terraform/backend/local" "github.com/hashicorp/terraform/command/clistate" "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" - "github.com/zclconf/go-cty/cty" - ctyjson "github.com/zclconf/go-cty/cty/json" ) // BackendOpts are the options used to initialize a backend.Backend. @@ -38,10 +40,6 @@ type BackendOpts struct { // arguments in Config. ConfigOverride hcl.Body - // Plan is a plan that is being used. If this is set, the backend - // configuration and output configuration will come from this plan. - Plan *terraform.Plan - // Init should be set to true if initialization is allowed. If this is // false, then any configuration that requires configuration will show // an error asking the user to reinitialize. @@ -78,17 +76,9 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, tfdiags.Diagnostics // local operation. var b backend.Backend if !opts.ForceLocal { - // If we have a plan then, we get the the backend from there. Otherwise, - // the backend comes from the configuration. - if opts.Plan != nil { - var backendDiags tfdiags.Diagnostics - b, backendDiags = m.backendFromPlan(opts) - diags = diags.Append(backendDiags) - } else { - var backendDiags tfdiags.Diagnostics - b, backendDiags = m.backendFromConfig(opts) - diags = diags.Append(backendDiags) - } + var backendDiags tfdiags.Diagnostics + b, backendDiags = m.backendFromConfig(opts) + diags = diags.Append(backendDiags) if diags.HasErrors() { return nil, diags @@ -97,23 +87,9 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, tfdiags.Diagnostics log.Printf("[INFO] command: backend initialized: %T", b) } - // Setup the CLI opts we pass into backends that support it. - cliOpts := &backend.CLIOpts{ - CLI: m.Ui, - CLIColor: m.Colorize(), - ShowDiagnostics: m.showDiagnostics, - StatePath: m.statePath, - StateOutPath: m.stateOutPath, - StateBackupPath: m.backupPath, - ContextOpts: m.contextOpts(), - Input: m.Input(), - RunningInAutomation: m.RunningInAutomation, - } - - // Don't validate if we have a plan. Validation is normally harmless here, - // but validation requires interpolation, and `file()` function calls may - // not have the original files in the current execution context. - cliOpts.Validation = opts.Plan == nil + // Setup the CLI opts we pass into backends that support it + cliOpts := m.backendCLIOpts() + cliOpts.Validation = true // If the backend supports CLI initialization, do it. if cli, ok := b.(backend.CLI); ok { @@ -151,6 +127,73 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, tfdiags.Diagnostics return local, nil } +// BackendForPlan is similar to Backend, but uses backend settings that were +// stored in a plan. +// +// The current workspace name is also stored as part of the plan, and so this +// method will check that it matches the currently-selected workspace name +// and produce error diagnostics if not. +func (m *Meta) BackendForPlan(settings plans.Backend) (backend.Enhanced, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + f := backendinit.Backend(settings.Type) + if f == nil { + diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), settings.Type)) + return nil, diags + } + b := f() + + schema := b.ConfigSchema() + configVal, err := settings.Config.Decode(schema.ImpliedType()) + if err != nil { + diags = diags.Append(errwrap.Wrapf("saved backend configuration is invalid: {{err}}", err)) + return nil, diags + } + + validateDiags := b.ValidateConfig(configVal) + diags = diags.Append(validateDiags) + if validateDiags.HasErrors() { + return nil, diags + } + + configureDiags := b.Configure(configVal) + diags = diags.Append(configureDiags) + + // If the result of loading the backend is an enhanced backend, + // then return that as-is. This works even if b == nil (it will be !ok). + if enhanced, ok := b.(backend.Enhanced); ok { + return enhanced, nil + } + + // Otherwise, we'll wrap our state-only remote backend in the local backend + // to cause any operations to be run locally. + cliOpts := m.backendCLIOpts() + cliOpts.Validation = false // don't validate here in case config contains file(...) calls where the file doesn't exist + local := &backendlocal.Local{Backend: b} + if err := local.CLIInit(cliOpts); err != nil { + // Local backend should never fail, so this is always a bug. + panic(err) + } + + return local, diags +} + +// backendCLIOpts returns a backend.CLIOpts object that should be passed to +// a backend that supports local CLI operations. +func (m *Meta) backendCLIOpts() *backend.CLIOpts { + return &backend.CLIOpts{ + CLI: m.Ui, + CLIColor: m.Colorize(), + ShowDiagnostics: m.showDiagnostics, + StatePath: m.statePath, + StateOutPath: m.stateOutPath, + StateBackupPath: m.backupPath, + ContextOpts: m.contextOpts(), + Input: m.Input(), + RunningInAutomation: m.RunningInAutomation, + } +} + // IsLocalBackend returns true if the backend is a local backend. We use this // for some checks that require a remote backend. func (m *Meta) IsLocalBackend(b backend.Backend) bool { @@ -170,15 +213,24 @@ func (m *Meta) IsLocalBackend(b backend.Backend) bool { // This prepares the operation. After calling this, the caller is expected // to modify fields of the operation such as Sequence to specify what will // be called. -func (m *Meta) Operation() *backend.Operation { +func (m *Meta) Operation(b backend.Backend) *backend.Operation { + schema := b.ConfigSchema() + workspace := m.Workspace() + planOutBackend, err := m.backendState.ForPlan(schema, workspace) + if err != nil { + // Always indicates an implementation error in practice, because + // errors here indicate invalid encoding of the backend configuration + // in memory, and we should always have validated that by the time + // we get here. + panic(fmt.Sprintf("failed to encode backend configuration for plan: %s", err)) + } + return &backend.Operation{ - PlanOutBackend: m.backendState, - Parallelism: m.parallelism, + PlanOutBackend: planOutBackend, Targets: m.targets, UIIn: m.UIInput(), UIOut: m.Ui, - Variables: m.variables, - Workspace: m.Workspace(), + Workspace: workspace, LockState: m.stateLock, StateLockTimeout: m.stateLockTimeout, } @@ -242,9 +294,12 @@ func (m *Meta) backendConfig(opts *BackendOpts) (*configs.Backend, int, tfdiags. // backendFromConfig returns the initialized (not configured) backend // directly from the config/state.. // -// This function handles any edge cases around backend config loading. For -// example: legacy remote state, new config changes, backend type changes, -// etc. +// This function handles various edge cases around backend config loading. For +// example: new config changes, backend type changes, etc. +// +// As of the 0.12 release it can no longer migrate from legacy remote state +// to backends, and will instead instruct users to use 0.11 or earlier as +// a stepping-stone to do that migration. // // This function may query the user for input unless input is disabled, in // which case this function will error. @@ -288,16 +343,26 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di } }() - // This giant switch statement covers all eight possible combinations - // of state settings between: configuring new backends, saved (previously- - // configured) backends, and legacy remote state. + if !s.Remote.Empty() { + // Legacy remote state is no longer supported. User must first + // migrate with Terraform 0.11 or earlier. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Legacy remote state not supported", + "This working directory is configured for legacy remote state, which is no longer supported from Terraform v0.12 onwards. To migrate this environment, first run \"terraform init\" under a Terraform 0.11 release, and then upgrade Terraform again.", + )) + return nil, diags + } + + // This switch statement covers all the different combinations of + // configuring new backends, updating previously-configured backends, etc. switch { // No configuration set at all. Pure local state. - case c == nil && s.Remote.Empty() && s.Backend.Empty(): + case c == nil && s.Backend.Empty(): return nil, nil // We're unsetting a backend (moving from backend => local) - case c == nil && s.Remote.Empty() && !s.Backend.Empty(): + case c == nil && !s.Backend.Empty(): if !opts.Init { initReason := fmt.Sprintf( "Unsetting the previously set backend %q", @@ -309,30 +374,8 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di return m.backend_c_r_S(c, cHash, sMgr, true) - // We have a legacy remote state configuration but no new backend config - case c == nil && !s.Remote.Empty() && s.Backend.Empty(): - return m.backend_c_R_s(c, sMgr) - - // We have a legacy remote state configuration simultaneously with a - // saved backend configuration while at the same time disabling backend - // configuration. - // - // This is a naturally impossible case: Terraform will never put you - // in this state, though it is theoretically possible through manual edits - case c == nil && !s.Remote.Empty() && !s.Backend.Empty(): - if !opts.Init { - initReason := fmt.Sprintf( - "Unsetting the previously set backend %q", - s.Backend.Type) - m.backendInitRequired(initReason) - diags = diags.Append(errBackendInitRequired) - return nil, diags - } - - return m.backend_c_R_S(c, cHash, sMgr) - // Configuring a backend for the first time. - case c != nil && s.Remote.Empty() && s.Backend.Empty(): + case c != nil && s.Backend.Empty(): if !opts.Init { initReason := fmt.Sprintf( "Initial configuration of the requested backend %q", @@ -345,7 +388,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di return m.backend_C_r_s(c, cHash, sMgr) // Potentially changing a backend configuration - case c != nil && s.Remote.Empty() && !s.Backend.Empty(): + case c != nil && !s.Backend.Empty(): // If our configuration is the same, then we're just initializing // a previously configured remote backend. if !s.Backend.Empty() { @@ -369,237 +412,18 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di s.Backend.Hash, cHash) return m.backend_C_r_S_changed(c, cHash, sMgr, true) - // Configuring a backend for the first time while having legacy - // remote state. This is very possible if a Terraform user configures - // a backend prior to ever running Terraform on an old state. - case c != nil && !s.Remote.Empty() && s.Backend.Empty(): - if !opts.Init { - initReason := fmt.Sprintf( - "Initial configuration for backend %q", - c.Type) - m.backendInitRequired(initReason) - diags = diags.Append(errBackendInitRequired) - return nil, diags - } - - return m.backend_C_R_s(c, sMgr) - - // Configuring a backend with both a legacy remote state set - // and a pre-existing backend saved. - case c != nil && !s.Remote.Empty() && !s.Backend.Empty(): - // If the hashes are the same, we have a legacy remote state with - // an unchanged stored backend state. - storedHash := s.Backend.Hash - if storedHash == cHash { - if !opts.Init { - initReason := fmt.Sprintf( - "Legacy remote state found with configured backend %q", - c.Type) - m.backendInitRequired(initReason) - diags = diags.Append(errBackendInitRequired) - return nil, diags - } - - return m.backend_C_R_S_unchanged(c, sMgr, true) - } - - if !opts.Init { - initReason := fmt.Sprintf( - "Reconfiguring the backend %q", - c.Type) - m.backendInitRequired(initReason) - diags = diags.Append(errBackendInitRequired) - return nil, diags - } - - // We have change in all three - return m.backend_C_R_S_changed(c, sMgr) default: - // This should be impossible since all state possibilties are - // tested above, but we need a default case anyways and we should - // protect against the scenario where a case is somehow removed. diags = diags.Append(fmt.Errorf( "Unhandled backend configuration state. This is a bug. Please\n"+ "report this error with the following information.\n\n"+ "Config Nil: %v\n"+ - "Saved Backend Empty: %v\n"+ - "Legacy Remote Empty: %v\n", - c == nil, s.Backend.Empty(), s.Remote.Empty(), + "Saved Backend Empty: %v\n", + c == nil, s.Backend.Empty(), )) return nil, diags } } -// backendFromPlan loads the backend from a given plan file. -func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, tfdiags.Diagnostics) { - if opts.Plan == nil { - panic("plan should not be nil") - } - - var diags tfdiags.Diagnostics - - // We currently don't allow "-state" to be specified. - if m.statePath != "" { - diags = diags.Append(fmt.Errorf( - "State path cannot be specified with a plan file. The plan itself contains\n" + - "the state to use. If you wish to change that, please create a new plan\n" + - "and specify the state path when creating the plan.", - )) - } - - planBackend := opts.Plan.Backend - planState := opts.Plan.State - if planState == nil { - // The state can be nil, we just have to make it empty for the logic - // in this function. - planState = terraform.NewState() - } - - // Validation only for non-local plans - local := planState.Remote.Empty() && planBackend.Empty() - if !local { - // We currently don't allow "-state-out" to be specified. - if m.stateOutPath != "" { - diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendPlanStateFlag))) - return nil, diags - } - } - - // If we have a stateOutPath, we must also specify it as the - // input path so we can check it properly. We restore it after this - // function exits. - original := m.statePath - m.statePath = m.stateOutPath - defer func() { m.statePath = original }() - - var b backend.Backend - switch { - // No remote state at all, all local - case planState.Remote.Empty() && planBackend.Empty(): - log.Printf("[INFO] command: initializing local backend from plan (not set)") - - // Get the local backend - var backendDiags tfdiags.Diagnostics - b, backendDiags = m.Backend(&BackendOpts{ForceLocal: true}) - diags = diags.Append(backendDiags) - - // New backend configuration set - case planState.Remote.Empty() && !planBackend.Empty(): - log.Printf( - "[INFO] command: initializing backend from plan: %s", - planBackend.Type) - - var backendDiags tfdiags.Diagnostics - b, backendDiags = m.backendInitFromSaved(planBackend) - diags = diags.Append(backendDiags) - - // Legacy remote state set - case !planState.Remote.Empty() && planBackend.Empty(): - log.Printf( - "[INFO] command: initializing legacy remote backend from plan: %s", - planState.Remote.Type) - - // Write our current state to an inmemory state just so that we - // have it in the format of state.State - inmem := &state.InmemState{} - inmem.WriteState(planState) - - // Get the backend through the normal means of legacy state - var moreDiags tfdiags.Diagnostics - b, moreDiags = m.backend_c_R_s(nil, inmem) - diags = diags.Append(moreDiags) - - // Both set, this can't happen in a plan. - case !planState.Remote.Empty() && !planBackend.Empty(): - diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendPlanBoth))) - return nil, diags - } - - // If we had an error, return that - if diags.HasErrors() { - return nil, diags - } - - env := m.Workspace() - - // Get the state so we can determine the effect of using this plan - realMgr, err := b.State(env) - if err != nil { - diags = diags.Append(fmt.Errorf("Error reading state: %s", err)) - return nil, diags - } - - if m.stateLock { - stateLocker := clistate.NewLocker(context.Background(), m.stateLockTimeout, m.Ui, m.Colorize()) - if err := stateLocker.Lock(realMgr, "backend from plan"); err != nil { - diags = diags.Append(fmt.Errorf("Error locking state: %s", err)) - return nil, diags - } - defer stateLocker.Unlock(nil) - } - - if err := realMgr.RefreshState(); err != nil { - diags = diags.Append(fmt.Errorf("Error reading state: %s", err)) - return nil, diags - } - real := realMgr.State() - if real != nil { - // If they're not the same lineage, don't allow this - if !real.SameLineage(planState) { - diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendPlanLineageDiff))) - return nil, diags - } - - // Compare ages - comp, err := real.CompareAges(planState) - if err != nil { - diags = diags.Append(fmt.Errorf("Error comparing state ages for safety: %s", err)) - return nil, diags - } - switch comp { - case terraform.StateAgeEqual: - // State ages are equal, this is perfect - - case terraform.StateAgeReceiverOlder: - // Real state is somehow older, this is okay. - - case terraform.StateAgeReceiverNewer: - // If we have an older serial it is a problem but if we have a - // differing serial but are still identical, just let it through. - if real.Equal(planState) { - log.Printf("[WARN] command: state in plan has older serial, but Equal is true") - break - } - - // The real state is newer, this is not allowed. - diags = diags.Append(fmt.Errorf( - strings.TrimSpace(errBackendPlanOlder), - planState.Serial, real.Serial, - )) - return nil, diags - } - } - - // Write the state - newState := opts.Plan.State.DeepCopy() - if newState != nil { - newState.Remote = nil - newState.Backend = nil - } - - // realMgr locked above - if err := realMgr.WriteState(newState); err != nil { - diags = diags.Append(fmt.Errorf("Error writing state: %s", err)) - return nil, diags - } - if err := realMgr.PersistState(); err != nil { - diags = diags.Append(fmt.Errorf("Error writing state: %s", err)) - return nil, diags - } - - return b, diags -} - //------------------------------------------------------------------- // Backend Config Scenarios // @@ -618,7 +442,7 @@ func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, tfdiags.Diag //------------------------------------------------------------------- // Unconfiguring a backend (moving from backend => local). -func (m *Meta) backend_c_r_S(c *configs.Backend, cHash int, sMgr state.State, output bool) (backend.Backend, tfdiags.Diagnostics) { +func (m *Meta) backend_c_r_S(c *configs.Backend, cHash int, sMgr *state.LocalState, output bool) (backend.Backend, tfdiags.Diagnostics) { s := sMgr.State() // Get the backend type for output @@ -673,7 +497,7 @@ func (m *Meta) backend_c_r_S(c *configs.Backend, cHash int, sMgr state.State, ou } // Legacy remote state -func (m *Meta) backend_c_R_s(c *configs.Backend, sMgr state.State) (backend.Backend, tfdiags.Diagnostics) { +func (m *Meta) backend_c_R_s(c *configs.Backend, sMgr *state.LocalState) (backend.Backend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics m.Ui.Error(strings.TrimSpace(errBackendLegacy) + "\n") @@ -683,7 +507,7 @@ func (m *Meta) backend_c_R_s(c *configs.Backend, sMgr state.State) (backend.Back } // Unsetting backend, saved backend, legacy remote state -func (m *Meta) backend_c_R_S(c *configs.Backend, cHash int, sMgr state.State) (backend.Backend, tfdiags.Diagnostics) { +func (m *Meta) backend_c_R_S(c *configs.Backend, cHash int, sMgr *state.LocalState) (backend.Backend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics m.Ui.Error(strings.TrimSpace(errBackendLegacy) + "\n") @@ -693,7 +517,7 @@ func (m *Meta) backend_c_R_S(c *configs.Backend, cHash int, sMgr state.State) (b } // Configuring a backend for the first time with legacy remote state. -func (m *Meta) backend_C_R_s(c *configs.Backend, sMgr state.State) (backend.Backend, tfdiags.Diagnostics) { +func (m *Meta) backend_C_R_s(c *configs.Backend, sMgr *state.LocalState) (backend.Backend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics m.Ui.Error(strings.TrimSpace(errBackendLegacy) + "\n") @@ -703,7 +527,7 @@ func (m *Meta) backend_C_R_s(c *configs.Backend, sMgr state.State) (backend.Back } // Configuring a backend for the first time. -func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr state.State) (backend.Backend, tfdiags.Diagnostics) { +func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *state.LocalState) (backend.Backend, tfdiags.Diagnostics) { // Get the backend b, configVal, diags := m.backendInitFromConfig(c) if diags.HasErrors() { @@ -717,7 +541,9 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr state.State) (b return nil, diags } - workspaces, err := localB.States() + workspace := m.Workspace() + + localState, err := localB.StateMgr(workspace) if err != nil { diags = diags.Append(fmt.Errorf(errBackendLocalRead, err)) return nil, diags @@ -809,7 +635,7 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr state.State) (b } // Changing a previously saved backend. -func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr state.State, output bool) (backend.Backend, tfdiags.Diagnostics) { +func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *state.LocalState, output bool) (backend.Backend, tfdiags.Diagnostics) { if output { // Notify the user m.Ui.Output(m.Colorize().Color(fmt.Sprintf( @@ -894,7 +720,7 @@ func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr state.S } // Initiailizing an unchanged saved backend -func (m *Meta) backend_C_r_S_unchanged(c *configs.Backend, cHash int, sMgr state.State) (backend.Backend, tfdiags.Diagnostics) { +func (m *Meta) backend_C_r_S_unchanged(c *configs.Backend, cHash int, sMgr *state.LocalState) (backend.Backend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics s := sMgr.State() @@ -948,7 +774,7 @@ func (m *Meta) backend_C_r_S_unchanged(c *configs.Backend, cHash int, sMgr state } // Initiailizing a changed saved backend with legacy remote state. -func (m *Meta) backend_C_R_S_changed(c *configs.Backend, sMgr state.State) (backend.Backend, tfdiags.Diagnostics) { +func (m *Meta) backend_C_R_S_changed(c *configs.Backend, sMgr *state.LocalState) (backend.Backend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics m.Ui.Error(strings.TrimSpace(errBackendLegacy) + "\n") @@ -958,7 +784,7 @@ func (m *Meta) backend_C_R_S_changed(c *configs.Backend, sMgr state.State) (back } // Initiailizing an unchanged saved backend with legacy remote state. -func (m *Meta) backend_C_R_S_unchanged(c *configs.Backend, sMgr state.State, output bool) (backend.Backend, tfdiags.Diagnostics) { +func (m *Meta) backend_C_R_S_unchanged(c *configs.Backend, sMgr *state.LocalState, output bool) (backend.Backend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics m.Ui.Error(strings.TrimSpace(errBackendLegacy) + "\n") diff --git a/command/meta_backend_migrate.go b/command/meta_backend_migrate.go index 4c5aedd9a..a8116adf2 100644 --- a/command/meta_backend_migrate.go +++ b/command/meta_backend_migrate.go @@ -11,6 +11,9 @@ import ( "strconv" "strings" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statemgr" + "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/command/clistate" "github.com/hashicorp/terraform/state" @@ -43,8 +46,8 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { // We need to check what the named state status is. If we're converting // from multi-state to single-state for example, we need to handle that. var oneSingle, twoSingle bool - oneStates, err := opts.One.States() - if err == backend.ErrNamedStatesNotSupported { + oneStates, err := opts.One.Workspaces() + if err == backend.ErrWorkspacesNotSupported { oneSingle = true err = nil } @@ -53,8 +56,8 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { errMigrateLoadStates), opts.OneType, err) } - _, err = opts.Two.States() - if err == backend.ErrNamedStatesNotSupported { + _, err = opts.Two.Workspaces() + if err == backend.ErrWorkspacesNotSupported { twoSingle = true err = nil } @@ -144,7 +147,7 @@ func (m *Meta) backendMigrateState_S_S(opts *backendMigrateOpts) error { } // Read all the states - oneStates, err := opts.One.States() + oneStates, err := opts.One.Workspaces() if err != nil { return fmt.Errorf(strings.TrimSpace( errMigrateLoadStates), opts.OneType, err) @@ -260,7 +263,7 @@ func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error { // Single state to single state, assumed default state name. func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error { - stateOne, err := opts.One.State(opts.oneEnv) + stateOne, err := opts.One.StateMgr(opts.oneEnv) if err != nil { return fmt.Errorf(strings.TrimSpace( errMigrateSingleLoadDefault), opts.OneType, err) @@ -270,47 +273,7 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error { errMigrateSingleLoadDefault), opts.OneType, err) } - // Do not migrate workspaces without state. - if stateOne.State() == nil { - return nil - } - - stateTwo, err := opts.Two.State(opts.twoEnv) - if err == backend.ErrDefaultStateNotSupported { - // If the backend doesn't support using the default state, we ask the user - // for a new name and migrate the default state to the given named state. - stateTwo, err = func() (state.State, error) { - name, err := m.UIInput().Input(&terraform.InputOpts{ - Id: "new-state-name", - Query: fmt.Sprintf( - "[reset][bold][yellow]The %q backend configuration only allows "+ - "named workspaces![reset]", - opts.TwoType), - Description: strings.TrimSpace(inputBackendNewWorkspaceName), - }) - if err != nil { - return nil, fmt.Errorf("Error asking for new state name: %s", err) - } - - // Update the name of the target state. - opts.twoEnv = name - - stateTwo, err := opts.Two.State(opts.twoEnv) - if err != nil { - return nil, err - } - - // If the currently selected workspace is the default workspace, then set - // the named workspace as the new selected workspace. - if m.Workspace() == backend.DefaultStateName { - if err := m.SetWorkspace(opts.twoEnv); err != nil { - return nil, fmt.Errorf("Failed to set new workspace: %s", err) - } - } - - return stateTwo, nil - }() - } + stateTwo, err := opts.Two.StateMgr(opts.twoEnv) if err != nil { return fmt.Errorf(strings.TrimSpace( errMigrateSingleLoadDefault), opts.TwoType, err) @@ -328,8 +291,15 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error { // no reason to migrate if the state is already there if one.Equal(two) { // Equal isn't identical; it doesn't check lineage. - if one != nil && two != nil && one.Lineage == two.Lineage { - return nil + sm1, _ := stateOne.(statemgr.PersistentMeta) + sm2, _ := stateTwo.(statemgr.PersistentMeta) + if one != nil && two != nil { + if sm1 == nil || sm2 == nil { + return nil + } + if sm1.StateSnapshotMeta().Lineage == sm2.StateSnapshotMeta().Lineage { + return nil + } } } @@ -363,15 +333,6 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error { two = stateTwo.State() } - // Clear the legacy remote state in both cases. If we're at the migration - // step then this won't be used anymore. - if one != nil { - one.Remote = nil - } - if two != nil { - two.Remote = nil - } - var confirmFunc func(state.State, state.State, *backendMigrateOpts) (bool, error) switch { // No migration necessary @@ -453,14 +414,9 @@ func (m *Meta) backendMigrateNonEmptyConfirm( defer os.RemoveAll(td) // Helper to write the state - saveHelper := func(n, path string, s *terraform.State) error { - f, err := os.Create(path) - if err != nil { - return err - } - defer f.Close() - - return terraform.WriteState(s, f) + saveHelper := func(n, path string, s *states.State) error { + mgr := statemgr.NewFilesystem(path) + return mgr.WriteState(s) } // Write the states diff --git a/command/meta_backend_test.go b/command/meta_backend_test.go index ba39e2fb5..3e6d6e7bd 100644 --- a/command/meta_backend_test.go +++ b/command/meta_backend_test.go @@ -6,19 +6,22 @@ import ( "path/filepath" "reflect" "sort" - "strings" "testing" - "github.com/hashicorp/terraform/configs" + "github.com/mitchellh/cli" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/backend" backendInit "github.com/hashicorp/terraform/backend/init" backendLocal "github.com/hashicorp/terraform/backend/local" + "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/helper/copy" + "github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statefile" + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/cli" ) // Test empty directory with no config/state creates a local state. @@ -37,13 +40,13 @@ func TestMetaBackend_emptyDir(t *testing.T) { } // Write some state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } s.WriteState(testState()) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify it exists where we expect it to @@ -92,7 +95,7 @@ func TestMetaBackend_emptyWithDefaultState(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - err = terraform.WriteState(testState(), f) + err = writeStateForTesting(testState(), f) f.Close() if err != nil { t.Fatalf("err: %s", err) @@ -107,9 +110,9 @@ func TestMetaBackend_emptyWithDefaultState(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { t.Fatalf("err: %s", err) @@ -130,10 +133,10 @@ func TestMetaBackend_emptyWithDefaultState(t *testing.T) { // Write some state next := testState() - next.Modules[0].Outputs["foo"] = &terraform.OutputState{Value: "bar"} - s.WriteState(testState()) + next.RootModule().SetOutputValue("foo", cty.StringVal("bar"), false) + s.WriteState(next) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify a backup was made since we're modifying a pre-existing state @@ -162,7 +165,7 @@ func TestMetaBackend_emptyWithExplicitState(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - err = terraform.WriteState(testState(), f) + err = writeStateForTesting(testState(), f) f.Close() if err != nil { t.Fatalf("err: %s", err) @@ -180,9 +183,9 @@ func TestMetaBackend_emptyWithExplicitState(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { t.Fatalf("err: %s", err) @@ -203,10 +206,10 @@ func TestMetaBackend_emptyWithExplicitState(t *testing.T) { // Write some state next := testState() - next.Modules[0].Outputs["foo"] = &terraform.OutputState{Value: "bar"} + markStateForMatching(next, "bar") // just any change so it shows as different than before s.WriteState(testState()) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify a backup was made since we're modifying a pre-existing state @@ -215,61 +218,6 @@ func TestMetaBackend_emptyWithExplicitState(t *testing.T) { } } -// Empty directory with legacy remote state -func TestMetaBackend_emptyLegacyRemote(t *testing.T) { - // Create a temporary working directory that is empty - td := tempDir(t) - os.MkdirAll(td, 0755) - defer os.RemoveAll(td) - defer testChdir(t, td)() - - // Create some legacy remote state - legacyState := testState() - _, srv := testRemoteState(t, legacyState, 200) - defer srv.Close() - statePath := testStateFileRemote(t, legacyState) - - // Setup the meta - m := testMetaBackend(t, nil) - - // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) - if diags.HasErrors() { - t.Fatal(diags.Err()) - } - - // Check the state - s, err := b.State(backend.DefaultStateName) - if err != nil { - t.Fatalf("bad: %s", err) - } - if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) - } - state := s.State() - if actual := state.String(); actual != legacyState.String() { - t.Fatalf("bad: %s", actual) - } - - // Verify we didn't setup the backend state - if !state.Backend.Empty() { - t.Fatal("shouldn't configure backend") - } - - // Verify the default paths don't exist - if _, err := os.Stat(DefaultStateFilename); err == nil { - t.Fatal("file should not exist") - } - - // Verify a backup doesn't exist - if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { - t.Fatal("file should not exist") - } - if _, err := os.Stat(statePath + DefaultBackupExtension); err == nil { - t.Fatal("file should not exist") - } -} - // Verify that interpolations result in an error func TestMetaBackend_configureInterpolation(t *testing.T) { // Create a temporary working directory that is empty @@ -306,12 +254,12 @@ func TestMetaBackend_configureNew(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state != nil { @@ -319,11 +267,12 @@ func TestMetaBackend_configureNew(t *testing.T) { } // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark := markStateForMatching(state, "changing") + s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -332,15 +281,13 @@ func TestMetaBackend_configureNew(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } // Verify the default paths don't exist @@ -375,28 +322,29 @@ func TestMetaBackend_configureNewWithState(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state is nil") } - if state.Lineage != "backend-new-migrate" { + if testStateMgrCurrentLineage(s) != "backend-new-migrate" { t.Fatalf("bad: %#v", state) } // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark := markStateForMatching(state, "changing") + s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -405,15 +353,13 @@ func TestMetaBackend_configureNewWithState(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } // Verify the default paths don't exist @@ -457,7 +403,7 @@ func TestMetaBackend_configureNewWithoutCopy(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) @@ -502,12 +448,12 @@ func TestMetaBackend_configureNewWithStateNoMigrate(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if state := s.State(); state != nil { t.Fatal("state is not nil") @@ -546,27 +492,28 @@ func TestMetaBackend_configureNewWithStateExisting(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state is nil") } - if state.Lineage != "local" { + if testStateMgrCurrentLineage(s) != "local" { t.Fatalf("bad: %#v", state) } // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark := markStateForMatching(state, "changing") + s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -575,15 +522,13 @@ func TestMetaBackend_configureNewWithStateExisting(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } // Verify the default paths don't exist @@ -620,27 +565,27 @@ func TestMetaBackend_configureNewWithStateExistingNoMigrate(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state is nil") } - if state.Lineage != "remote" { + if testStateMgrCurrentLineage(s) != "remote" { t.Fatalf("bad: %#v", state) } // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark := markStateForMatching(state, "changing") s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -649,15 +594,13 @@ func TestMetaBackend_configureNewWithStateExistingNoMigrate(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } // Verify the default paths don't exist @@ -673,200 +616,6 @@ func TestMetaBackend_configureNewWithStateExistingNoMigrate(t *testing.T) { } } -// Newly configured backend with lgacy -func TestMetaBackend_configureNewLegacy(t *testing.T) { - // Create a temporary working directory that is empty - td := tempDir(t) - copy.CopyDir(testFixturePath("backend-new-legacy"), td) - defer os.RemoveAll(td) - defer testChdir(t, td)() - - // Ask input - defer testInteractiveInput(t, []string{"no"})() - - // Setup the meta - m := testMetaBackend(t, nil) - - // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) - if diags.HasErrors() { - t.Fatal(diags.Err()) - } - - // Check the state - s, err := b.State(backend.DefaultStateName) - if err != nil { - t.Fatalf("bad: %s", err) - } - if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) - } - state := s.State() - if state != nil { - t.Fatal("state should be nil") - } - - // Verify we have no configured legacy - { - path := filepath.Join(m.DataDir(), DefaultStateFilename) - f, err := os.Open(path) - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if !actual.Remote.Empty() { - t.Fatalf("bad: %#v", actual) - } - if actual.Backend.Empty() { - t.Fatalf("bad: %#v", actual) - } - } - - // Write some state - state = terraform.NewState() - state.Lineage = "changing" - s.WriteState(state) - if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) - } - - // Verify the state is where we expect - { - f, err := os.Open("local-state.tfstate") - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } - } - - // Verify the default paths don't exist - if !isEmptyState(DefaultStateFilename) { - data, _ := ioutil.ReadFile(DefaultStateFilename) - - t.Fatal("state should not exist, but contains:\n", string(data)) - } - - // Verify a backup doesn't exist - if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) { - data, _ := ioutil.ReadFile(DefaultStateFilename) - - t.Fatal("backup should be empty, but contains:\n", string(data)) - } -} - -// Newly configured backend with legacy -func TestMetaBackend_configureNewLegacyCopy(t *testing.T) { - // Create a temporary working directory that is empty - td := tempDir(t) - copy.CopyDir(testFixturePath("backend-new-legacy"), td) - defer os.RemoveAll(td) - defer testChdir(t, td)() - - // Setup the meta - m := testMetaBackend(t, nil) - - // suppress input - m.forceInitCopy = true - - // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) - if diags.HasErrors() { - t.Fatal(diags.Err()) - } - - // Check the state - s, err := b.State(backend.DefaultStateName) - if err != nil { - t.Fatalf("bad: %s", err) - } - if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) - } - state := s.State() - if state == nil { - t.Fatal("nil state") - } - if state.Lineage != "backend-new-legacy" { - t.Fatalf("bad: %#v", state) - } - - // Verify we have no configured legacy - { - path := filepath.Join(m.DataDir(), DefaultStateFilename) - f, err := os.Open(path) - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if !actual.Remote.Empty() { - t.Fatalf("bad: %#v", actual) - } - if actual.Backend.Empty() { - t.Fatalf("bad: %#v", actual) - } - } - - // Verify we have no configured legacy in the state itself - { - if !state.Remote.Empty() { - t.Fatalf("legacy has remote state: %#v", state.Remote) - } - } - - // Write some state - state = terraform.NewState() - state.Lineage = "changing" - s.WriteState(state) - if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) - } - - // Verify the state is where we expect - { - f, err := os.Open("local-state.tfstate") - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } - } - - // Verify the default paths don't exist - if _, err := os.Stat(DefaultStateFilename); err == nil { - t.Fatal("file should not exist") - } - - // Verify a backup doesn't exist - if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { - t.Fatal("file should not exist") - } -} - // Saved backend state matching config func TestMetaBackend_configuredUnchanged(t *testing.T) { defer testChdir(t, testFixturePath("backend-unchanged"))() @@ -881,18 +630,18 @@ func TestMetaBackend_configuredUnchanged(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("nil state") } - if state.Lineage != "configuredUnchanged" { + if testStateMgrCurrentLineage(s) != "configuredUnchanged" { t.Fatalf("bad: %#v", state) } @@ -928,12 +677,12 @@ func TestMetaBackend_configuredChange(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state != nil { @@ -951,11 +700,12 @@ func TestMetaBackend_configuredChange(t *testing.T) { } // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark := markStateForMatching(state, "changing") + s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -964,15 +714,13 @@ func TestMetaBackend_configuredChange(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } // Verify no local state @@ -1016,12 +764,12 @@ func TestMetaBackend_reconfigureChange(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } newState := s.State() if newState != nil || !newState.Empty() { @@ -1029,7 +777,7 @@ func TestMetaBackend_reconfigureChange(t *testing.T) { } // verify that the old state is still there - s = (&state.LocalState{Path: "local-state.tfstate"}) + s = statemgr.NewFilesystem("local-state.tfstate") if err := s.RefreshState(); err != nil { t.Fatal(err) } @@ -1060,18 +808,18 @@ func TestMetaBackend_configuredChangeCopy(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state should not be nil") } - if state.Lineage != "backend-change" { + if testStateMgrCurrentLineage(s) != "backend-change" { t.Fatalf("bad: %#v", state) } @@ -1114,18 +862,18 @@ func TestMetaBackend_configuredChangeCopy_singleState(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state should not be nil") } - if state.Lineage != "backend-change" { + if testStateMgrCurrentLineage(s) != "backend-change" { t.Fatalf("bad: %#v", state) } @@ -1169,18 +917,18 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleDefault(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state should not be nil") } - if state.Lineage != "backend-change" { + if testStateMgrCurrentLineage(s) != "backend-change" { t.Fatalf("bad: %#v", state) } @@ -1224,18 +972,18 @@ func TestMetaBackend_configuredChangeCopy_multiToSingle(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state should not be nil") } - if state.Lineage != "backend-change" { + if testStateMgrCurrentLineage(s) != "backend-change" { t.Fatalf("bad: %#v", state) } @@ -1285,7 +1033,7 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleCurrentEnv(t *testing.T) // Change env if err := m.SetWorkspace("env2"); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Get the backend @@ -1295,18 +1043,18 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleCurrentEnv(t *testing.T) } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state should not be nil") } - if state.Lineage != "backend-change-env2" { + if testStateMgrCurrentLineage(s) != "backend-change-env2" { t.Fatalf("bad: %#v", state) } @@ -1351,9 +1099,9 @@ func TestMetaBackend_configuredChangeCopy_multiToMulti(t *testing.T) { } // Check resulting states - states, err := b.States() + states, err := b.Workspaces() if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } sort.Strings(states) @@ -1364,36 +1112,36 @@ func TestMetaBackend_configuredChangeCopy_multiToMulti(t *testing.T) { { // Check the default state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state should not be nil") } - if state.Lineage != "backend-change" { + if testStateMgrCurrentLineage(s) != "backend-change" { t.Fatalf("bad: %#v", state) } } { // Check the other state - s, err := b.State("env2") + s, err := b.StateMgr("env2") if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state should not be nil") } - if state.Lineage != "backend-change-env2" { + if testStateMgrCurrentLineage(s) != "backend-change-env2" { t.Fatalf("bad: %#v", state) } } @@ -1593,12 +1341,12 @@ func TestMetaBackend_configuredUnset(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state != nil { @@ -1617,31 +1365,10 @@ func TestMetaBackend_configuredUnset(t *testing.T) { t.Fatal("backup should not exist, but contains:\n", string(data)) } - // Verify we have no configured backend/legacy - path := filepath.Join(m.DataDir(), DefaultStateFilename) - if _, err := os.Stat(path); err == nil { - f, err := os.Open(path) - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if !actual.Remote.Empty() { - t.Fatalf("bad: %#v", actual) - } - if !actual.Backend.Empty() { - t.Fatalf("bad: %#v", actual) - } - } - // Write some state s.WriteState(testState()) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify it exists where we expect it to @@ -1677,18 +1404,18 @@ func TestMetaBackend_configuredUnsetCopy(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state is nil") } - if state.Lineage != "configuredUnset" { + if testStateMgrCurrentLineage(s) != "configuredUnset" { t.Fatalf("bad: %#v", state) } @@ -1697,31 +1424,10 @@ func TestMetaBackend_configuredUnsetCopy(t *testing.T) { t.Fatalf("backup state should be empty") } - // Verify we have no configured backend/legacy - path := filepath.Join(m.DataDir(), DefaultStateFilename) - if _, err := os.Stat(path); err == nil { - f, err := os.Open(path) - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if !actual.Remote.Empty() { - t.Fatalf("bad: %#v", actual) - } - if !actual.Backend.Empty() { - t.Fatalf("bad: %#v", actual) - } - } - // Write some state s.WriteState(testState()) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify it exists where we expect it to @@ -1756,18 +1462,18 @@ func TestMetaBackend_configuredUnchangedLegacy(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state is nil") } - if state.Lineage != "configured" { + if testStateMgrCurrentLineage(s) != "configured" { t.Fatalf("bad: %#v", state) } @@ -1781,33 +1487,13 @@ func TestMetaBackend_configuredUnchangedLegacy(t *testing.T) { t.Fatal("file should not exist") } - // Verify we have no configured legacy - { - path := filepath.Join(m.DataDir(), DefaultStateFilename) - f, err := os.Open(path) - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if !actual.Remote.Empty() { - t.Fatalf("bad: %#v", actual) - } - if actual.Backend.Empty() { - t.Fatalf("bad: %#v", actual) - } - } - // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark := markStateForMatching(state, "changing") + s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -1816,15 +1502,13 @@ func TestMetaBackend_configuredUnchangedLegacy(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } // Verify no local state @@ -1857,18 +1541,18 @@ func TestMetaBackend_configuredUnchangedLegacyCopy(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state is nil") } - if state.Lineage != "backend-unchanged-with-legacy" { + if testStateMgrCurrentLineage(s) != "backend-unchanged-with-legacy" { t.Fatalf("bad: %#v", state) } @@ -1882,33 +1566,13 @@ func TestMetaBackend_configuredUnchangedLegacyCopy(t *testing.T) { t.Fatal("file should not exist") } - // Verify we have no configured legacy - { - path := filepath.Join(m.DataDir(), DefaultStateFilename) - f, err := os.Open(path) - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if !actual.Remote.Empty() { - t.Fatalf("bad: %#v", actual) - } - if actual.Backend.Empty() { - t.Fatalf("bad: %#v", actual) - } - } - // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark := markStateForMatching(state, "changing") + s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -1917,15 +1581,13 @@ func TestMetaBackend_configuredUnchangedLegacyCopy(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } // Verify no local state @@ -1960,12 +1622,12 @@ func TestMetaBackend_configuredChangedLegacy(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state != nil { @@ -1982,33 +1644,13 @@ func TestMetaBackend_configuredChangedLegacy(t *testing.T) { t.Fatal("file should not exist") } - // Verify we have no configured legacy - { - path := filepath.Join(m.DataDir(), DefaultStateFilename) - f, err := os.Open(path) - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if !actual.Remote.Empty() { - t.Fatalf("bad: %#v", actual) - } - if actual.Backend.Empty() { - t.Fatalf("bad: %#v", actual) - } - } - // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark := markStateForMatching(state, "changing") + s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -2017,15 +1659,13 @@ func TestMetaBackend_configuredChangedLegacy(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } // Verify no local state @@ -2060,18 +1700,18 @@ func TestMetaBackend_configuredChangedLegacyCopyBackend(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state is nil") } - if state.Lineage != "configured" { + if testStateMgrCurrentLineage(s) != "configured" { t.Fatalf("bad: %#v", state) } @@ -2085,33 +1725,13 @@ func TestMetaBackend_configuredChangedLegacyCopyBackend(t *testing.T) { t.Fatal("file should not exist") } - // Verify we have no configured legacy - { - path := filepath.Join(m.DataDir(), DefaultStateFilename) - f, err := os.Open(path) - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if !actual.Remote.Empty() { - t.Fatalf("bad: %#v", actual) - } - if actual.Backend.Empty() { - t.Fatalf("bad: %#v", actual) - } - } - // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark := markStateForMatching(state, "changing") + s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -2120,15 +1740,13 @@ func TestMetaBackend_configuredChangedLegacyCopyBackend(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } // Verify no local state @@ -2163,18 +1781,18 @@ func TestMetaBackend_configuredChangedLegacyCopyLegacy(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state is nil") } - if state.Lineage != "legacy" { + if testStateMgrCurrentLineage(s) != "legacy" { t.Fatalf("bad: %#v", state) } @@ -2188,33 +1806,13 @@ func TestMetaBackend_configuredChangedLegacyCopyLegacy(t *testing.T) { t.Fatal("file should not exist") } - // Verify we have no configured legacy - { - path := filepath.Join(m.DataDir(), DefaultStateFilename) - f, err := os.Open(path) - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if !actual.Remote.Empty() { - t.Fatalf("bad: %#v", actual) - } - if actual.Backend.Empty() { - t.Fatalf("bad: %#v", actual) - } - } - // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark := markStateForMatching(state, "changing") + s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -2223,15 +1821,13 @@ func TestMetaBackend_configuredChangedLegacyCopyLegacy(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } // Verify no local state @@ -2266,18 +1862,18 @@ func TestMetaBackend_configuredChangedLegacyCopyBoth(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state is nil") } - if state.Lineage != "legacy" { + if testStateMgrCurrentLineage(s) != "legacy" { t.Fatalf("bad: %#v", state) } @@ -2291,33 +1887,13 @@ func TestMetaBackend_configuredChangedLegacyCopyBoth(t *testing.T) { t.Fatal("file should not exist") } - // Verify we have no configured legacy - { - path := filepath.Join(m.DataDir(), DefaultStateFilename) - f, err := os.Open(path) - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if !actual.Remote.Empty() { - t.Fatalf("bad: %#v", actual) - } - if actual.Backend.Empty() { - t.Fatalf("bad: %#v", actual) - } - } - // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark := markStateForMatching(state, "changing") + s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -2326,15 +1902,13 @@ func TestMetaBackend_configuredChangedLegacyCopyBoth(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } // Verify no local state @@ -2369,12 +1943,12 @@ func TestMetaBackend_configuredUnsetWithLegacyNoCopy(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state != nil { @@ -2391,33 +1965,13 @@ func TestMetaBackend_configuredUnsetWithLegacyNoCopy(t *testing.T) { t.Fatal("backup should be empty") } - // Verify we have no configured backend/legacy - path := filepath.Join(m.DataDir(), DefaultStateFilename) - if _, err := os.Stat(path); err == nil { - f, err := os.Open(path) - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if !actual.Remote.Empty() { - t.Fatalf("bad: %#v", actual) - } - if !actual.Backend.Empty() { - t.Fatalf("bad: %#v", actual) - } - } - // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark := markStateForMatching(state, "changing") + s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -2426,15 +1980,13 @@ func TestMetaBackend_configuredUnsetWithLegacyNoCopy(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } } @@ -2459,18 +2011,18 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyBackend(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state is nil") } - if state.Lineage != "backend" { + if testStateMgrCurrentLineage(s) != "backend" { t.Fatalf("bad: %#v", state) } @@ -2484,33 +2036,13 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyBackend(t *testing.T) { t.Fatal("backupstate should be empty") } - // Verify we have no configured backend/legacy - path := filepath.Join(m.DataDir(), DefaultStateFilename) - if _, err := os.Stat(path); err == nil { - f, err := os.Open(path) - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if !actual.Remote.Empty() { - t.Fatalf("bad: %#v", actual) - } - if !actual.Backend.Empty() { - t.Fatalf("bad: %#v", actual) - } - } - // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark := markStateForMatching(state, "changing") + s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -2519,15 +2051,13 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyBackend(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } // Verify a local backup @@ -2557,18 +2087,18 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyLegacy(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state is nil") } - if state.Lineage != "legacy" { + if testStateMgrCurrentLineage(s) != "legacy" { t.Fatalf("bad: %#v", state) } @@ -2582,33 +2112,13 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyLegacy(t *testing.T) { t.Fatal("backupstate should be empty") } - // Verify we have no configured backend/legacy - path := filepath.Join(m.DataDir(), DefaultStateFilename) - if _, err := os.Stat(path); err == nil { - f, err := os.Open(path) - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if !actual.Remote.Empty() { - t.Fatalf("bad: %#v", actual) - } - if !actual.Backend.Empty() { - t.Fatalf("bad: %#v", actual) - } - } - // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark := markStateForMatching(state, "changing") + s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -2617,15 +2127,13 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyLegacy(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } // Verify a local backup @@ -2655,18 +2163,18 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyBoth(t *testing.T) { } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state is nil") } - if state.Lineage != "legacy" { + if testStateMgrCurrentLineage(s) != "legacy" { t.Fatalf("bad: %#v", state) } @@ -2680,33 +2188,13 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyBoth(t *testing.T) { t.Fatal("backup is empty") } - // Verify we have no configured backend/legacy - path := filepath.Join(m.DataDir(), DefaultStateFilename) - if _, err := os.Stat(path); err == nil { - f, err := os.Open(path) - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if !actual.Remote.Empty() { - t.Fatalf("bad: %#v", actual) - } - if !actual.Backend.Empty() { - t.Fatalf("bad: %#v", actual) - } - } - // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark := markStateForMatching(state, "changing") + s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -2715,15 +2203,13 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyBoth(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } // Verify a local backup @@ -2732,7 +2218,7 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyBoth(t *testing.T) { } } -// A plan that has no backend config +// A plan that has uses the local backend func TestMetaBackend_planLocal(t *testing.T) { // Create a temporary working directory that is empty td := tempDir(t) @@ -2740,56 +2226,57 @@ func TestMetaBackend_planLocal(t *testing.T) { defer os.RemoveAll(td) defer testChdir(t, td)() - // Create the plan - plan := &terraform.Plan{ - Config: testModule(t, "backend-plan-local"), - State: nil, + backendConfig := plans.Backend{ + Type: "local", + Config: plans.DynamicValue("{}"), + Workspace: "default", } // Setup the meta m := testMetaBackend(t, nil) // Get the backend - b, diags := m.Backend(&BackendOpts{Plan: plan}) + b, diags := m.BackendForPlan(backendConfig) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state != nil { t.Fatalf("state should be nil: %#v", state) } - // Verify the default path doens't exist + // The default state file should not exist yet if !isEmptyState(DefaultStateFilename) { t.Fatal("expected empty state") } - // Verify a backup doesn't exists + // A backup file shouldn't exist yet either. if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) { t.Fatal("expected empty backup") } - // Verify we have no configured backend/legacy + // Verify we have no configured backend path := filepath.Join(m.DataDir(), DefaultStateFilename) if _, err := os.Stat(path); err == nil { t.Fatalf("should not have backend configured") } // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark := markStateForMatching(state, "changing") + s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -2798,15 +2285,13 @@ func TestMetaBackend_planLocal(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } // Verify no local backup @@ -2823,21 +2308,20 @@ func TestMetaBackend_planLocalStatePath(t *testing.T) { defer os.RemoveAll(td) defer testChdir(t, td)() - // Create our state original := testState() - original.Lineage = "hello" + mark := markStateForMatching(original, "hello") - // Create the plan - plan := &terraform.Plan{ - Config: testModule(t, "backend-plan-local"), - State: original, + backendConfig := plans.Backend{ + Type: "local", + Config: plans.DynamicValue("{}"), + Workspace: "default", } // Create an alternate output path statePath := "foo.tfstate" // put a initial state there that needs to be backed up - err := (&state.LocalState{Path: statePath}).WriteState(original) + err := (statemgr.NewFilesystem(statePath)).WriteState(original) if err != nil { t.Fatal(err) } @@ -2847,26 +2331,24 @@ func TestMetaBackend_planLocalStatePath(t *testing.T) { m.stateOutPath = statePath // Get the backend - b, diags := m.Backend(&BackendOpts{Plan: plan}) + b, diags := m.BackendForPlan(backendConfig) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state is nil") } - if state.Lineage != "hello" { - t.Fatalf("bad: %#v", state) - } + assertStateHasMarker(t, state, mark) // Verify the default path doesn't exist if _, err := os.Stat(DefaultStateFilename); err == nil { @@ -2885,11 +2367,12 @@ func TestMetaBackend_planLocalStatePath(t *testing.T) { } // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark = markStateForMatching(state, "changing") + s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -2898,15 +2381,13 @@ func TestMetaBackend_planLocalStatePath(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } // Verify we have a backup @@ -2923,34 +2404,34 @@ func TestMetaBackend_planLocalMatch(t *testing.T) { defer os.RemoveAll(td) defer testChdir(t, td)() - // Create the plan - plan := &terraform.Plan{ - Config: testModule(t, "backend-plan-local-match"), - State: testStateRead(t, DefaultStateFilename), + backendConfig := plans.Backend{ + Type: "local", + Config: plans.DynamicValue("{}"), + Workspace: "default", } // Setup the meta m := testMetaBackend(t, nil) // Get the backend - b, diags := m.Backend(&BackendOpts{Plan: plan}) + b, diags := m.BackendForPlan(backendConfig) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state - s, err := b.State(backend.DefaultStateName) + s, err := b.StateMgr(backend.DefaultStateName) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("should is nil") } - if state.Lineage != "hello" { + if testStateMgrCurrentLineage(s) != "hello" { t.Fatalf("bad: %#v", state) } @@ -2971,11 +2452,12 @@ func TestMetaBackend_planLocalMatch(t *testing.T) { } // Write some state - state = terraform.NewState() - state.Lineage = "changing" + state = states.NewState() + mark := markStateForMatching(state, "changing") + s.WriteState(state) if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect @@ -2984,15 +2466,13 @@ func TestMetaBackend_planLocalMatch(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - actual, err := terraform.ReadState(f) + actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } + assertStateHasMarker(t, actual.State, mark) } // Verify local backup @@ -3001,476 +2481,6 @@ func TestMetaBackend_planLocalMatch(t *testing.T) { } } -// A plan that has no backend config, mismatched lineage -func TestMetaBackend_planLocalMismatchLineage(t *testing.T) { - // Create a temporary working directory that is empty - td := tempDir(t) - copy.CopyDir(testFixturePath("backend-plan-local-mismatch-lineage"), td) - defer os.RemoveAll(td) - defer testChdir(t, td)() - - // Save the original - original := testStateRead(t, DefaultStateFilename) - - // Change the lineage - planState := testStateRead(t, DefaultStateFilename) - planState.Lineage = "bad" - - // Create the plan - plan := &terraform.Plan{ - Config: testModule(t, "backend-plan-local-mismatch-lineage"), - State: planState, - } - - // Setup the meta - m := testMetaBackend(t, nil) - - // Get the backend - _, diags := m.Backend(&BackendOpts{Plan: plan}) - if !diags.HasErrors() { - t.Fatal("should have error") - } - if !strings.Contains(diags[0].Description().Summary, "lineage") { - t.Fatalf("wrong diagnostic message %q; want something containing \"lineage\"", diags[0].Description().Summary) - } - - // Verify our local state didn't change - actual := testStateRead(t, DefaultStateFilename) - if !actual.Equal(original) { - t.Fatalf("bad: %#v", actual) - } - - // Verify a backup doesn't exists - if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { - t.Fatal("file should not exist") - } - - // Verify we have no configured backend/legacy - path := filepath.Join(m.DataDir(), DefaultStateFilename) - if _, err := os.Stat(path); err == nil { - t.Fatalf("should not have backend configured") - } -} - -// A plan that has no backend config, newer local -func TestMetaBackend_planLocalNewer(t *testing.T) { - // Create a temporary working directory that is empty - td := tempDir(t) - copy.CopyDir(testFixturePath("backend-plan-local-newer"), td) - defer os.RemoveAll(td) - defer testChdir(t, td)() - - // Save the original - original := testStateRead(t, DefaultStateFilename) - - // Change the serial - planState := testStateRead(t, DefaultStateFilename) - planState.Serial = 7 - planState.RootModule().Dependencies = []string{"foo"} - - // Create the plan - plan := &terraform.Plan{ - Config: testModule(t, "backend-plan-local-newer"), - State: planState, - } - - // Setup the meta - m := testMetaBackend(t, nil) - - // Get the backend - _, diags := m.Backend(&BackendOpts{Plan: plan}) - if !diags.HasErrors() { - t.Fatal("should have error") - } - if !strings.Contains(diags[0].Description().Summary, "older") { - t.Fatalf("wrong diagnostic message %q; want something containing \"older\"", diags[0].Description().Summary) - } - - // Verify our local state didn't change - actual := testStateRead(t, DefaultStateFilename) - if !actual.Equal(original) { - t.Fatalf("bad: %#v", actual) - } - - // Verify a backup doesn't exists - if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { - t.Fatal("file should not exist") - } - - // Verify we have no configured backend/legacy - path := filepath.Join(m.DataDir(), DefaultStateFilename) - if _, err := os.Stat(path); err == nil { - t.Fatalf("should not have backend configured") - } -} - -// A plan that has a backend in an empty dir -func TestMetaBackend_planBackendEmptyDir(t *testing.T) { - // Create a temporary working directory that is empty - td := tempDir(t) - copy.CopyDir(testFixturePath("backend-plan-backend-empty"), td) - defer os.RemoveAll(td) - defer testChdir(t, td)() - - // Get the state for the plan by getting the real state and - // adding the backend config to it. - original := testStateRead(t, filepath.Join( - testFixturePath("backend-plan-backend-empty-config"), - "local-state.tfstate")) - backendState := testStateRead(t, filepath.Join( - testFixturePath("backend-plan-backend-empty-config"), - DefaultDataDir, DefaultStateFilename)) - planState := original.DeepCopy() - - // Create the plan - plan := &terraform.Plan{ - Config: testModule(t, "backend-plan-backend-empty-config"), - State: planState, - Backend: backendState.Backend, - } - - // Setup the meta - m := testMetaBackend(t, nil) - - // Get the backend - b, diags := m.Backend(&BackendOpts{Plan: plan}) - if diags.HasErrors() { - t.Fatal(diags.Err()) - } - - // Check the state - s, err := b.State(backend.DefaultStateName) - if err != nil { - t.Fatalf("bad: %s", err) - } - if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) - } - state := s.State() - if state == nil { - t.Fatal("should is nil") - } - if state.Lineage != "hello" { - t.Fatalf("bad: %#v", state) - } - - // Verify the default path doesn't exist - if !isEmptyState(DefaultStateFilename) { - t.Fatal("state is not empty") - } - - // Verify a backup doesn't exist - if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) { - t.Fatal("backup is not empty") - } - - // Verify we have no configured backend/legacy - path := filepath.Join(m.DataDir(), DefaultStateFilename) - if _, err := os.Stat(path); err == nil { - t.Fatalf("should not have backend configured") - } - - // Write some state - state = terraform.NewState() - state.Lineage = "changing" - s.WriteState(state) - if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) - } - - // Verify the state is where we expect - { - f, err := os.Open("local-state.tfstate") - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } - } - - // Verify no default path - if _, err := os.Stat(DefaultStateFilename); err == nil { - t.Fatal("file should not exist") - } - - // Verify no local backup - if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { - t.Fatal("file should not exist") - } -} - -// A plan that has a backend with matching state -func TestMetaBackend_planBackendMatch(t *testing.T) { - // Create a temporary working directory that is empty - td := tempDir(t) - copy.CopyDir(testFixturePath("backend-plan-backend-match"), td) - defer os.RemoveAll(td) - defer testChdir(t, td)() - - // Get the state for the plan by getting the real state and - // adding the backend config to it. - original := testStateRead(t, filepath.Join( - testFixturePath("backend-plan-backend-empty-config"), - "local-state.tfstate")) - backendState := testStateRead(t, filepath.Join( - testFixturePath("backend-plan-backend-empty-config"), - DefaultDataDir, DefaultStateFilename)) - planState := original.DeepCopy() - - // Create the plan - plan := &terraform.Plan{ - Config: testModule(t, "backend-plan-backend-empty-config"), - State: planState, - Backend: backendState.Backend, - } - - // Setup the meta - m := testMetaBackend(t, nil) - - // Get the backend - b, diags := m.Backend(&BackendOpts{Plan: plan}) - if diags.HasErrors() { - t.Fatal(diags.Err()) - } - - // Check the state - s, err := b.State(backend.DefaultStateName) - if err != nil { - t.Fatalf("bad: %s", err) - } - if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) - } - state := s.State() - if state == nil { - t.Fatal("should is nil") - } - if state.Lineage != "hello" { - t.Fatalf("bad: %#v", state) - } - - // Verify the default path exists - if _, err := os.Stat(DefaultStateFilename); err == nil { - t.Fatal("file should not exist") - } - - // Verify a backup doesn't exist - if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { - t.Fatal("file should not exist") - } - - // Verify we have no configured backend/legacy - path := filepath.Join(m.DataDir(), DefaultStateFilename) - if _, err := os.Stat(path); err == nil { - t.Fatalf("should not have backend configured") - } - - // Write some state - state = terraform.NewState() - state.Lineage = "changing" - s.WriteState(state) - if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) - } - - // Verify the state is where we expect - { - f, err := os.Open("local-state.tfstate") - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } - } - - // Verify no default path - if _, err := os.Stat(DefaultStateFilename); err == nil { - t.Fatal("file should not exist") - } - - // Verify no local backup - if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { - t.Fatal("file should not exist") - } -} - -// A plan that has a backend with mismatching lineage -func TestMetaBackend_planBackendMismatchLineage(t *testing.T) { - // Create a temporary working directory that is empty - td := tempDir(t) - copy.CopyDir(testFixturePath("backend-plan-backend-mismatch"), td) - defer os.RemoveAll(td) - defer testChdir(t, td)() - - // Get the state for the plan by getting the real state and - // adding the backend config to it. - original := testStateRead(t, filepath.Join( - testFixturePath("backend-plan-backend-empty-config"), - "local-state.tfstate")) - backendState := testStateRead(t, filepath.Join( - testFixturePath("backend-plan-backend-empty-config"), - DefaultDataDir, DefaultStateFilename)) - planState := original.DeepCopy() - - // Get the real original - original = testStateRead(t, "local-state.tfstate") - - // Create the plan - plan := &terraform.Plan{ - Config: testModule(t, "backend-plan-backend-empty-config"), - State: planState, - Backend: backendState.Backend, - } - - // Setup the meta - m := testMetaBackend(t, nil) - - // Get the backend - _, diags := m.Backend(&BackendOpts{Plan: plan}) - if !diags.HasErrors() { - t.Fatal("should have error") - } - if !strings.Contains(diags[0].Description().Summary, "lineage") { - t.Fatalf("wrong diagnostic message %q; want something containing \"lineage\"", diags[0].Description().Summary) - } - - // Verify our local state didn't change - actual := testStateRead(t, "local-state.tfstate") - if !actual.Equal(original) { - t.Fatalf("bad: %#v", actual) - } - - // Verify a backup doesn't exist - if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { - t.Fatal("file should not exist") - } - - // Verify we have no configured backend/legacy - path := filepath.Join(m.DataDir(), DefaultStateFilename) - if _, err := os.Stat(path); err == nil { - t.Fatalf("should not have backend configured") - } - - // Verify we have no default state - if _, err := os.Stat(DefaultStateFilename); err == nil { - t.Fatal("file should not exist") - } -} - -// A plan that has a legacy remote state -func TestMetaBackend_planLegacy(t *testing.T) { - // Create a temporary working directory that is empty - td := tempDir(t) - copy.CopyDir(testFixturePath("backend-plan-legacy"), td) - defer os.RemoveAll(td) - defer testChdir(t, td)() - - // Get the state for the plan by getting the real state and - // adding the backend config to it. - original := testStateRead(t, filepath.Join( - testFixturePath("backend-plan-legacy-data"), "local-state.tfstate")) - dataState := testStateRead(t, filepath.Join( - testFixturePath("backend-plan-legacy-data"), "state.tfstate")) - planState := original.DeepCopy() - planState.Remote = dataState.Remote - - // Create the plan - plan := &terraform.Plan{ - Config: testModule(t, "backend-plan-legacy-data"), - State: planState, - } - - // Setup the meta - m := testMetaBackend(t, nil) - - // Get the backend - b, diags := m.Backend(&BackendOpts{Plan: plan}) - if diags.HasErrors() { - t.Fatal(diags.Err()) - } - - // Check the state - s, err := b.State(backend.DefaultStateName) - if err != nil { - t.Fatalf("bad: %s", err) - } - if err := s.RefreshState(); err != nil { - t.Fatalf("bad: %s", err) - } - state := s.State() - if state == nil { - t.Fatal("should is nil") - } - if state.Lineage != "hello" { - t.Fatalf("bad: %#v", state) - } - - // Verify the default path - if _, err := os.Stat(DefaultStateFilename); err == nil { - t.Fatal("file should not exist") - } - - // Verify a backup doesn't exist - if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { - t.Fatal("file should not exist") - } - - // Verify we have no configured backend/legacy - path := filepath.Join(m.DataDir(), DefaultStateFilename) - if _, err := os.Stat(path); err == nil { - t.Fatalf("should not have backend configured") - } - - // Write some state - state = terraform.NewState() - state.Lineage = "changing" - s.WriteState(state) - if err := s.PersistState(); err != nil { - t.Fatalf("bad: %s", err) - } - - // Verify the state is where we expect - { - f, err := os.Open("local-state.tfstate") - if err != nil { - t.Fatalf("err: %s", err) - } - actual, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if actual.Lineage != state.Lineage { - t.Fatalf("bad: %#v", actual) - } - } - - // Verify no default path - if _, err := os.Stat(DefaultStateFilename); err == nil { - t.Fatal("file should not exist") - } - - // Verify no local backup - if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { - t.Fatal("file should not exist") - } -} - // init a backend using -backend-config options multiple times func TestMetaBackend_configureWithExtra(t *testing.T) { // Create a temporary working directory that is empty @@ -3501,7 +2511,7 @@ func TestMetaBackend_configureWithExtra(t *testing.T) { } // Check the state - s := testStateRead(t, filepath.Join(DefaultDataDir, backendlocal.DefaultStateFilename)) + s := testDataStateRead(t, filepath.Join(DefaultDataDir, backendlocal.DefaultStateFilename)) if s.Backend.Hash != cHash { t.Fatal("mismatched state and config backend hashes") } @@ -3513,11 +2523,11 @@ func TestMetaBackend_configureWithExtra(t *testing.T) { Init: true, }) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Check the state - s = testStateRead(t, filepath.Join(DefaultDataDir, backendlocal.DefaultStateFilename)) + s = testDataStateRead(t, filepath.Join(DefaultDataDir, backendlocal.DefaultStateFilename)) if s.Backend.Hash != cHash { t.Fatal("mismatched state and config backend hashes") } @@ -3580,11 +2590,11 @@ func TestMetaBackend_configToExtra(t *testing.T) { Init: true, }) if err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } // Check the state - s := testStateRead(t, filepath.Join(DefaultDataDir, backendLocal.DefaultStateFilename)) + s := testDataStateRead(t, filepath.Join(DefaultDataDir, backendlocal.DefaultStateFilename)) backendHash := s.Backend.Hash // init again but remove the path option from the config @@ -3605,7 +2615,7 @@ func TestMetaBackend_configToExtra(t *testing.T) { t.Fatal(diags.Err()) } - s = testStateRead(t, filepath.Join(DefaultDataDir, backendLocal.DefaultStateFilename)) + s = testDataStateRead(t, filepath.Join(DefaultDataDir, backendlocal.DefaultStateFilename)) if s.Backend.Hash == backendHash { t.Fatal("state.Backend.Hash was not updated") @@ -3618,7 +2628,7 @@ func testMetaBackend(t *testing.T, args []string) *Meta { m.process(args, true) f := m.flagSet("test") if err := f.Parse(args); err != nil { - t.Fatalf("bad: %s", err) + t.Fatalf("unexpected error: %s", err) } return &m diff --git a/command/meta_new.go b/command/meta_new.go index 269443151..9f87d792f 100644 --- a/command/meta_new.go +++ b/command/meta_new.go @@ -7,10 +7,11 @@ import ( "path/filepath" "strconv" + "github.com/hashicorp/terraform/plans/planfile" + "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config/module" - "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" ) @@ -129,51 +130,23 @@ func (m *Meta) Config(path string) (*config.Config, error) { return c, nil } -// Plan returns the plan for the given path. +// PlanFile returns a reader for the plan file at the given path. // -// This only has an effect if the path itself looks like a plan. -// If error is nil and the plan is nil, then the path didn't look like -// a plan. +// If the return value and error are both nil, the given path exists but seems +// to be a configuration directory instead. // -// Error will be non-nil if path looks like a plan and loading the plan -// failed. -func (m *Meta) Plan(path string) (*terraform.Plan, error) { - // Open the path no matter if its a directory or file - f, err := os.Open(path) - defer f.Close() - if err != nil { - return nil, fmt.Errorf( - "Failed to load Terraform configuration or plan: %s", err) - } - - // Stat it so we can check if its a directory - fi, err := f.Stat() - if err != nil { - return nil, fmt.Errorf( - "Failed to load Terraform configuration or plan: %s", err) - } - - // If this path is a directory, then it can't be a plan. Not an error. - if fi.IsDir() { - return nil, nil - } - - // Read the plan - p, err := terraform.ReadPlan(f) +// Error will be non-nil if path refers to something which looks like a plan +// file and loading the file fails. +func (m *Meta) PlanFile(path string) (*planfile.Reader, error) { + fi, err := os.Stat(path) if err != nil { return nil, err } - // We do a validation here that seems odd but if any plan is given, - // we must not have set any extra variables. The plan itself contains - // the variables and those aren't overwritten. - if len(m.variableArgs.AllItems()) > 0 { - return nil, fmt.Errorf( - "You can't set variables with the '-var' or '-var-file' flag\n" + - "when you're applying a plan file. The variables used when\n" + - "the plan was created will be used. If you wish to use different\n" + - "variable values, create a new plan file.") + if fi.IsDir() { + // Looks like a configuration directory. + return nil, nil } - return p, nil + return planfile.Open(path) } diff --git a/command/output.go b/command/output.go index bc57dc20b..e8e172923 100644 --- a/command/output.go +++ b/command/output.go @@ -2,14 +2,15 @@ package command import ( "bytes" - "encoding/json" "flag" "fmt" "sort" "strings" - "github.com/hashicorp/terraform/addrs" + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/tfdiags" ) @@ -63,7 +64,7 @@ func (c *OutputCommand) Run(args []string) int { env := c.Workspace() // Get the state - stateStore, err := b.State(env) + stateStore, err := b.StateMgr(env) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) return 1 @@ -74,13 +75,15 @@ func (c *OutputCommand) Run(args []string) int { return 1 } - // This command uses a legacy shorthand syntax for the module path that - // can't deal with keyed instances, so we'll just shim it for now and - // make the breaking change for this interface later. - modPath := addrs.Module(strings.Split(module, ".")).UnkeyedInstanceShim() + moduleAddr, addrDiags := addrs.ParseModuleInstanceStr(module) + diags = diags.Append(addrDiags) + if addrDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } state := stateStore.State() - mod := state.ModuleByPath(modPath) + mod := state.Module(moduleAddr) if mod == nil { c.Ui.Error(fmt.Sprintf( "The module %s could not be found. There is nothing to output.", @@ -88,7 +91,12 @@ func (c *OutputCommand) Run(args []string) int { return 1 } - if !jsonOutput && (state.Empty() || len(mod.Outputs) == 0) { + // TODO: We need to do an eval walk here to make sure all of the output + // values recorded in the state are up-to-date. + c.Ui.Error("output command not yet updated to do eval walk") + return 1 + + if !jsonOutput && (state.Empty() || len(mod.OutputValues) == 0) { c.Ui.Error( "The state file either has no outputs defined, or all the defined\n" + "outputs are empty. Please define an output in your configuration\n" + @@ -101,7 +109,12 @@ func (c *OutputCommand) Run(args []string) int { if name == "" { if jsonOutput { - jsonOutputs, err := json.MarshalIndent(mod.Outputs, "", " ") + vals := make(map[string]cty.Value, len(mod.OutputValues)) + for n, os := range mod.OutputValues { + vals[n] = os.Value + } + valsObj := cty.ObjectVal(vals) + jsonOutputs, err := ctyjson.Marshal(valsObj, valsObj.Type()) if err != nil { return 1 } @@ -109,12 +122,12 @@ func (c *OutputCommand) Run(args []string) int { c.Ui.Output(string(jsonOutputs)) return 0 } else { - c.Ui.Output(outputsAsString(state, modPath, nil, false)) + c.Ui.Output(outputsAsString(state, moduleAddr, nil, false)) return 0 } } - v, ok := mod.Outputs[name] + os, ok := mod.OutputValues[name] if !ok { c.Ui.Error(fmt.Sprintf( "The output variable requested could not be found in the state\n" + @@ -123,29 +136,18 @@ func (c *OutputCommand) Run(args []string) int { "with new output variables until that command is run.")) return 1 } + v := os.Value if jsonOutput { - jsonOutputs, err := json.MarshalIndent(v, "", " ") + jsonOutput, err := ctyjson.Marshal(v, v.Type()) if err != nil { return 1 } - c.Ui.Output(string(jsonOutputs)) + c.Ui.Output(string(jsonOutput)) } else { - switch output := v.Value.(type) { - case string: - c.Ui.Output(output) - return 0 - case []interface{}: - c.Ui.Output(formatListOutput("", "", output)) - return 0 - case map[string]interface{}: - c.Ui.Output(formatMapOutput("", "", output)) - return 0 - default: - c.Ui.Error(fmt.Sprintf("Unknown output type: %T", v.Type)) - return 1 - } + c.Ui.Error("TODO: update output command to use the same value renderer as the console") + return 1 } return 0 diff --git a/command/output_test.go b/command/output_test.go index 85ed3f882..c210dff8f 100644 --- a/command/output_test.go +++ b/command/output_test.go @@ -6,24 +6,21 @@ import ( "strings" "testing" - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/states" ) func TestOutput(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Outputs: map[string]*terraform.OutputState{ - "foo": { - Value: "bar", - Type: "string", - }, - }, - }, - }, - } + originalState := states.BuildState(func(s *states.SyncState) { + s.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), + false, + ) + }) statePath := testStateFile(t, originalState) @@ -50,28 +47,18 @@ func TestOutput(t *testing.T) { } func TestModuleOutput(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Outputs: map[string]*terraform.OutputState{ - "foo": { - Value: "bar", - Type: "string", - }, - }, - }, - { - Path: []string{"root", "my_module"}, - Outputs: map[string]*terraform.OutputState{ - "blah": { - Value: "tastatur", - Type: "string", - }, - }, - }, - }, - } + originalState := states.BuildState(func(s *states.SyncState) { + s.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), + false, + ) + s.SetOutputValue( + addrs.OutputValue{Name: "blah"}.Absolute(addrs.Module{"my_module"}.UnkeyedInstanceShim()), + cty.StringVal("tastatur"), + false, + ) + }) statePath := testStateFile(t, originalState) @@ -100,28 +87,18 @@ func TestModuleOutput(t *testing.T) { } func TestModuleOutputs(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Outputs: map[string]*terraform.OutputState{ - "foo": { - Value: "bar", - Type: "string", - }, - }, - }, - { - Path: []string{"root", "my_module"}, - Outputs: map[string]*terraform.OutputState{ - "blah": { - Value: "tastatur", - Type: "string", - }, - }, - }, - }, - } + originalState := states.BuildState(func(s *states.SyncState) { + s.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), + false, + ) + s.SetOutputValue( + addrs.OutputValue{Name: "blah"}.Absolute(addrs.Module{"my_module"}.UnkeyedInstanceShim()), + cty.StringVal("tastatur"), + false, + ) + }) statePath := testStateFile(t, originalState) @@ -149,28 +126,21 @@ func TestModuleOutputs(t *testing.T) { } func TestOutput_nestedListAndMap(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Outputs: map[string]*terraform.OutputState{ - "foo": { - Value: []interface{}{ - map[string]interface{}{ - "key": "value", - "key2": "value2", - }, - map[string]interface{}{ - "key": "value", - }, - }, - Type: "list", - }, - }, - }, - }, - } - + originalState := states.BuildState(func(s *states.SyncState) { + s.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.ListVal([]cty.Value{ + cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("value"), + "key2": cty.StringVal("value2"), + }), + cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("value"), + }), + }), + false, + ) + }) statePath := testStateFile(t, originalState) ui := new(cli.MockUi) @@ -196,19 +166,13 @@ func TestOutput_nestedListAndMap(t *testing.T) { } func TestOutput_json(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Outputs: map[string]*terraform.OutputState{ - "foo": { - Value: "bar", - Type: "string", - }, - }, - }, - }, - } + originalState := states.BuildState(func(s *states.SyncState) { + s.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), + false, + ) + }) statePath := testStateFile(t, originalState) @@ -236,15 +200,7 @@ func TestOutput_json(t *testing.T) { } func TestOutput_emptyOutputsErr(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Outputs: map[string]*terraform.OutputState{}, - }, - }, - } - + originalState := states.NewState() statePath := testStateFile(t, originalState) p := testProvider() @@ -265,15 +221,7 @@ func TestOutput_emptyOutputsErr(t *testing.T) { } func TestOutput_jsonEmptyOutputs(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Outputs: map[string]*terraform.OutputState{}, - }, - }, - } - + originalState := states.NewState() statePath := testStateFile(t, originalState) p := testProvider() @@ -301,20 +249,13 @@ func TestOutput_jsonEmptyOutputs(t *testing.T) { } func TestMissingModuleOutput(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Outputs: map[string]*terraform.OutputState{ - "foo": { - Value: "bar", - Type: "string", - }, - }, - }, - }, - } - + originalState := states.BuildState(func(s *states.SyncState) { + s.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), + false, + ) + }) statePath := testStateFile(t, originalState) ui := new(cli.MockUi) @@ -337,20 +278,13 @@ func TestMissingModuleOutput(t *testing.T) { } func TestOutput_badVar(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Outputs: map[string]*terraform.OutputState{ - "foo": { - Value: "bar", - Type: "string", - }, - }, - }, - }, - } - + originalState := states.BuildState(func(s *states.SyncState) { + s.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), + false, + ) + }) statePath := testStateFile(t, originalState) ui := new(cli.MockUi) @@ -371,24 +305,18 @@ func TestOutput_badVar(t *testing.T) { } func TestOutput_blank(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Outputs: map[string]*terraform.OutputState{ - "foo": { - Value: "bar", - Type: "string", - }, - "name": { - Value: "john-doe", - Type: "string", - }, - }, - }, - }, - } - + originalState := states.BuildState(func(s *states.SyncState) { + s.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), + false, + ) + s.SetOutputValue( + addrs.OutputValue{Name: "name"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("john-doe"), + false, + ) + }) statePath := testStateFile(t, originalState) ui := new(cli.MockUi) @@ -449,7 +377,7 @@ func TestOutput_noArgs(t *testing.T) { } func TestOutput_noState(t *testing.T) { - originalState := &terraform.State{} + originalState := states.NewState() statePath := testStateFile(t, originalState) ui := new(cli.MockUi) @@ -470,14 +398,7 @@ func TestOutput_noState(t *testing.T) { } func TestOutput_noVars(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Outputs: map[string]*terraform.OutputState{}, - }, - }, - } + originalState := states.NewState() statePath := testStateFile(t, originalState) @@ -499,19 +420,13 @@ func TestOutput_noVars(t *testing.T) { } func TestOutput_stateDefault(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Outputs: map[string]*terraform.OutputState{ - "foo": { - Value: "bar", - Type: "string", - }, - }, - }, - }, - } + originalState := states.BuildState(func(s *states.SyncState) { + s.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), + false, + ) + }) // Write the state file in a temporary directory with the // default filename. @@ -522,7 +437,7 @@ func TestOutput_stateDefault(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - err = terraform.WriteState(originalState, f) + err = writeStateForTesting(originalState, f) f.Close() if err != nil { t.Fatalf("err: %s", err) diff --git a/command/plan.go b/command/plan.go index fe262b0a9..790162123 100644 --- a/command/plan.go +++ b/command/plan.go @@ -53,37 +53,35 @@ func (c *PlanCommand) Run(args []string) int { return 1 } - // Check if the path is a plan - plan, err := c.Plan(configPath) + // Check if the path is a plan, which is not permitted + planFileReader, err := c.PlanFile(configPath) if err != nil { c.Ui.Error(err.Error()) return 1 } - if plan != nil { - // Disable refreshing no matter what since we only want to show the plan - refresh = false - - // Set the config path to empty for backend loading - configPath = "" + if planFileReader != nil { + c.showDiagnostics(tfdiags.Sourceless( + tfdiags.Error, + "Invalid configuration directory", + fmt.Sprintf("Cannot pass a saved plan file to the 'terraform plan' command. To apply a saved plan, use: terraform apply %s", configPath), + )) + return 1 } var diags tfdiags.Diagnostics var backendConfig *configs.Backend - if plan == nil { - var configDiags tfdiags.Diagnostics - backendConfig, configDiags = c.loadBackendConfig(configPath) - diags = diags.Append(configDiags) - if configDiags.HasErrors() { - c.showDiagnostics(diags) - return 1 - } + var configDiags tfdiags.Diagnostics + backendConfig, configDiags = c.loadBackendConfig(configPath) + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 } // Load the backend b, backendDiags := c.Backend(&BackendOpts{ Config: backendConfig, - Plan: plan, }) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { @@ -97,10 +95,10 @@ func (c *PlanCommand) Run(args []string) int { diags = nil // Build the operation - opReq := c.Operation() + opReq := c.Operation(b) opReq.Destroy = destroy opReq.ConfigDir = configPath - opReq.Plan = plan + opReq.PlanRefresh = refresh opReq.PlanOutPath = outPath opReq.PlanRefresh = refresh opReq.Type = backend.OperationTypePlan diff --git a/command/plan_test.go b/command/plan_test.go index baf8ff210..1809306f2 100644 --- a/command/plan_test.go +++ b/command/plan_test.go @@ -11,7 +11,9 @@ import ( "testing" "time" + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/helper/copy" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) @@ -83,9 +85,7 @@ func TestPlan_plan(t *testing.T) { tmp, cwd := testCwd(t) defer testFixCwd(t, tmp, cwd) - planPath := testPlanFile(t, &terraform.Plan{ - Config: testModule(t, "apply"), - }) + planPath := testPlanFileNoop(t) p := testProvider() ui := new(cli.MockUi) @@ -107,22 +107,20 @@ func TestPlan_plan(t *testing.T) { } func TestPlan_destroy(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - }, - }, - }, + originalState := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) outPath := testTempFile(t) statePath := testStateFile(t, originalState) @@ -232,21 +230,20 @@ func TestPlan_outPath(t *testing.T) { } func TestPlan_outPathNoChange(t *testing.T) { - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - }, - }, - }, + originalState := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectReady, }, - }, - } + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, originalState) td := testTempDir(t) @@ -337,67 +334,6 @@ func TestPlan_outBackend(t *testing.T) { } } -// When using "-out" with a legacy remote state, the plan should encode -// the backend config -func TestPlan_outBackendLegacy(t *testing.T) { - // Create a temporary working directory that is empty - td := tempDir(t) - copy.CopyDir(testFixturePath("plan-out-backend-legacy"), td) - defer os.RemoveAll(td) - defer testChdir(t, td)() - - // Our state - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - }, - }, - }, - }, - }, - } - originalState.Init() - - // Setup our legacy state - remoteState, srv := testRemoteState(t, originalState, 200) - defer srv.Close() - dataState := terraform.NewState() - dataState.Remote = remoteState - testStateFileRemote(t, dataState) - - outPath := "foo" - p := testProvider() - ui := new(cli.MockUi) - c := &PlanCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(p), - Ui: ui, - }, - } - - args := []string{ - "-out", outPath, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - plan := testReadPlan(t, outPath) - if !plan.Diff.Empty() { - t.Fatalf("Expected empty plan to be written to plan file, got: %s", plan) - } - - if plan.State.Remote.Empty() { - t.Fatal("should have remote info") - } -} - func TestPlan_refresh(t *testing.T) { tmp, cwd := testCwd(t) defer testFixCwd(t, tmp, cwd) @@ -492,70 +428,6 @@ func TestPlan_stateDefault(t *testing.T) { } } -func TestPlan_stateFuture(t *testing.T) { - originalState := testState() - originalState.TFVersion = "99.99.99" - statePath := testStateFile(t, originalState) - - p := testProvider() - ui := new(cli.MockUi) - c := &PlanCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(p), - Ui: ui, - }, - } - - args := []string{ - "-state", statePath, - testFixturePath("plan"), - } - if code := c.Run(args); code == 0 { - t.Fatal("should fail") - } - - f, err := os.Open(statePath) - if err != nil { - t.Fatalf("err: %s", err) - } - - newState, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - if !newState.Equal(originalState) { - t.Fatalf("bad: %#v", newState) - } - if newState.TFVersion != originalState.TFVersion { - t.Fatalf("bad: %#v", newState) - } -} - -func TestPlan_statePast(t *testing.T) { - originalState := testState() - originalState.TFVersion = "0.1.0" - statePath := testStateFile(t, originalState) - - p := testProvider() - ui := new(cli.MockUi) - c := &PlanCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(p), - Ui: ui, - }, - } - - args := []string{ - "-state", statePath, - testFixturePath("plan"), - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } -} - func TestPlan_validate(t *testing.T) { // This is triggered by not asking for input so we have to set this to false test = false diff --git a/command/plugins.go b/command/plugins.go index 467279ec8..9ad4b669b 100644 --- a/command/plugins.go +++ b/command/plugins.go @@ -13,7 +13,6 @@ import ( "strings" plugin "github.com/hashicorp/go-plugin" - terraformProvider "github.com/hashicorp/terraform/builtin/providers/terraform" tfplugin "github.com/hashicorp/terraform/plugin" "github.com/hashicorp/terraform/plugin/discovery" "github.com/hashicorp/terraform/terraform" @@ -280,9 +279,11 @@ func (m *Meta) providerResolver() terraform.ResourceProviderResolver { func (m *Meta) internalProviders() map[string]terraform.ResourceProviderFactory { return map[string]terraform.ResourceProviderFactory{ - "terraform": func() (terraform.ResourceProvider, error) { - return terraformProvider.Provider(), nil - }, + // FIXME: Re-enable this once the internal provider system is updated + // for the new provider interface. + //"terraform": func() (terraform.ResourceProvider, error) { + // return terraformProvider.Provider(), nil + //}, } } diff --git a/command/providers.go b/command/providers.go index 97d628715..7dc2ddf6a 100644 --- a/command/providers.go +++ b/command/providers.go @@ -60,7 +60,7 @@ func (c *ProvidersCommand) Run(args []string) int { // Get the state env := c.Workspace() - state, err := b.State(env) + state, err := b.StateMgr(env) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) return 1 diff --git a/command/refresh.go b/command/refresh.go index 42579130f..e3e5c3f90 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -70,7 +70,7 @@ func (c *RefreshCommand) Run(args []string) int { diags = nil // Build the operation - opReq := c.Operation() + opReq := c.Operation(b) opReq.Type = backend.OperationTypeRefresh opReq.ConfigDir = configPath opReq.ConfigLoader, err = c.initConfigLoader() diff --git a/command/refresh_test.go b/command/refresh_test.go index a083849f3..688ffabc3 100644 --- a/command/refresh_test.go +++ b/command/refresh_test.go @@ -2,18 +2,19 @@ package command import ( "bytes" + "encoding/json" + "fmt" "io/ioutil" "os" "path/filepath" - "reflect" "strings" "testing" - "github.com/hashicorp/terraform/helper/copy" - "github.com/hashicorp/terraform/state" - "github.com/hashicorp/terraform/terraform" - "github.com/hashicorp/terraform/version" "github.com/mitchellh/cli" + + "github.com/hashicorp/terraform/helper/copy" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/terraform" ) func TestRefresh(t *testing.T) { @@ -185,263 +186,162 @@ func TestRefresh_cwd(t *testing.T) { } func TestRefresh_defaultState(t *testing.T) { - originalState := testState() + t.Fatal("not yet updated for new provider types") + /* + originalState := testState() - // Write the state file in a temporary directory with the - // default filename. - statePath := testStateFile(t, originalState) + // Write the state file in a temporary directory with the + // default filename. + statePath := testStateFile(t, originalState) - localState := &state.LocalState{Path: statePath} - if err := localState.RefreshState(); err != nil { - t.Fatal(err) - } - s := localState.State() - if s == nil { - t.Fatal("empty test state") - } - serial := s.Serial + localState := &state.LocalState{Path: statePath} + if err := localState.RefreshState(); err != nil { + t.Fatal(err) + } + s := localState.State() + if s == nil { + t.Fatal("empty test state") + } + serial := s.Serial - // Change to that directory - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("err: %s", err) - } - if err := os.Chdir(filepath.Dir(statePath)); err != nil { - t.Fatalf("err: %s", err) - } - defer os.Chdir(cwd) + // Change to that directory + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(filepath.Dir(statePath)); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) - p := testProvider() - ui := new(cli.MockUi) - c := &RefreshCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(p), - Ui: ui, - }, - } + p := testProvider() + ui := new(cli.MockUi) + c := &RefreshCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + }, + } - p.RefreshFn = nil - p.RefreshReturn = newInstanceState("yes") + p.RefreshFn = nil + p.RefreshReturn = newInstanceState("yes") - args := []string{ - "-state", statePath, - testFixturePath("refresh"), - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + args := []string{ + "-state", statePath, + testFixturePath("refresh"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } - if !p.RefreshCalled { - t.Fatal("refresh should be called") - } + if !p.RefreshCalled { + t.Fatal("refresh should be called") + } - newState := testStateRead(t, statePath) + newState := testStateRead(t, statePath) - actual := newState.RootModule().Resources["test_instance.foo"].Primary - expected := p.RefreshReturn - if !reflect.DeepEqual(actual, expected) { - t.Logf("expected:\n%#v", expected) - t.Fatalf("bad:\n%#v", actual) - } + actual := newState.RootModule().Resources["test_instance.foo"].Instances[addrs.NoKey].Current + expected := p.RefreshReturn + if !reflect.DeepEqual(actual, expected) { + t.Logf("expected:\n%#v", expected) + t.Fatalf("bad:\n%#v", actual) + } - if newState.Serial <= serial { - t.Fatalf("serial not incremented during refresh. previous:%d, current:%d", serial, newState.Serial) - } + backupState := testStateRead(t, statePath+DefaultBackupExtension) - backupState := testStateRead(t, statePath+DefaultBackupExtension) - - actual = backupState.RootModule().Resources["test_instance.foo"].Primary - expected = originalState.RootModule().Resources["test_instance.foo"].Primary - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } -} - -func TestRefresh_futureState(t *testing.T) { - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("err: %s", err) - } - if err := os.Chdir(testFixturePath("refresh")); err != nil { - t.Fatalf("err: %s", err) - } - defer os.Chdir(cwd) - - state := testState() - state.TFVersion = "99.99.99" - statePath := testStateFile(t, state) - - p := testProvider() - ui := new(cli.MockUi) - c := &RefreshCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(p), - Ui: ui, - }, - } - - args := []string{ - "-state", statePath, - } - if code := c.Run(args); code == 0 { - t.Fatal("should fail") - } - - if p.RefreshCalled { - t.Fatal("refresh should not be called") - } - - f, err := os.Open(statePath) - if err != nil { - t.Fatalf("err: %s", err) - } - - newState, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(newState.String()) - expected := strings.TrimSpace(state.String()) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestRefresh_pastState(t *testing.T) { - state := testState() - state.TFVersion = "0.1.0" - statePath := testStateFile(t, state) - - p := testProvider() - ui := new(cli.MockUi) - c := &RefreshCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(p), - Ui: ui, - }, - } - - p.RefreshFn = nil - p.RefreshReturn = &terraform.InstanceState{ID: "yes"} - - args := []string{ - "-state", statePath, - testFixturePath("refresh"), - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - if !p.RefreshCalled { - t.Fatal("refresh should be called") - } - - f, err := os.Open(statePath) - if err != nil { - t.Fatalf("err: %s", err) - } - - newState, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(newState.String()) - expected := strings.TrimSpace(testRefreshStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } - - if newState.TFVersion != version.Version { - t.Fatalf("bad:\n\n%s", newState.TFVersion) - } + actual = backupState.RootModule().Resources["test_instance.foo"].Instances[addrs.NoKey].Current + expected = originalState.RootModule().Resources["test_instance.foo"].Instances[addrs.NoKey].Current + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } + */ } func TestRefresh_outPath(t *testing.T) { - state := testState() - statePath := testStateFile(t, state) + t.Fatal("not yet updated for new provider types") + /* + state := testState() + statePath := testStateFile(t, state) - // Output path - outf, err := ioutil.TempFile(testingDir, "tf") - if err != nil { - t.Fatalf("err: %s", err) - } - outPath := outf.Name() - outf.Close() - os.Remove(outPath) + // Output path + outf, err := ioutil.TempFile(testingDir, "tf") + if err != nil { + t.Fatalf("err: %s", err) + } + outPath := outf.Name() + outf.Close() + os.Remove(outPath) - p := testProvider() - ui := new(cli.MockUi) - c := &RefreshCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(p), - Ui: ui, - }, - } + p := testProvider() + ui := new(cli.MockUi) + c := &RefreshCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + }, + } - p.RefreshFn = nil - p.RefreshReturn = newInstanceState("yes") + p.RefreshFn = nil + p.RefreshReturn = newInstanceState("yes") - args := []string{ - "-state", statePath, - "-state-out", outPath, - testFixturePath("refresh"), - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + args := []string{ + "-state", statePath, + "-state-out", outPath, + testFixturePath("refresh"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } - f, err := os.Open(statePath) - if err != nil { - t.Fatalf("err: %s", err) - } + f, err := os.Open(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } - newState, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } + newState, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } - if !reflect.DeepEqual(newState, state) { - t.Fatalf("bad: %#v", newState) - } + if !reflect.DeepEqual(newState, state) { + t.Fatalf("bad: %#v", newState) + } - f, err = os.Open(outPath) - if err != nil { - t.Fatalf("err: %s", err) - } + f, err = os.Open(outPath) + if err != nil { + t.Fatalf("err: %s", err) + } - newState, err = terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } + newState, err = terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } - actual := newState.RootModule().Resources["test_instance.foo"].Primary - expected := p.RefreshReturn - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } + actual := newState.RootModule().Resources["test_instance.foo"].Primary + expected := p.RefreshReturn + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } - f, err = os.Open(outPath + DefaultBackupExtension) - if err != nil { - t.Fatalf("err: %s", err) - } + f, err = os.Open(outPath + DefaultBackupExtension) + if err != nil { + t.Fatalf("err: %s", err) + } - backupState, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } + backupState, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } - actualStr := strings.TrimSpace(backupState.String()) - expectedStr := strings.TrimSpace(state.String()) - if actualStr != expectedStr { - t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) - } + actualStr := strings.TrimSpace(backupState.String()) + expectedStr := strings.TrimSpace(state.String()) + if actualStr != expectedStr { + t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) + } + */ } func TestRefresh_var(t *testing.T) { @@ -582,175 +482,181 @@ func TestRefresh_varsUnset(t *testing.T) { } func TestRefresh_backup(t *testing.T) { - state := testState() - statePath := testStateFile(t, state) + t.Fatal("not yet updated for new provider types") + /* + state := testState() + statePath := testStateFile(t, state) - // Output path - outf, err := ioutil.TempFile(testingDir, "tf") - if err != nil { - t.Fatalf("err: %s", err) - } - outPath := outf.Name() - outf.Close() - os.Remove(outPath) + // Output path + outf, err := ioutil.TempFile(testingDir, "tf") + if err != nil { + t.Fatalf("err: %s", err) + } + outPath := outf.Name() + outf.Close() + os.Remove(outPath) - // Backup path - backupf, err := ioutil.TempFile(testingDir, "tf") - if err != nil { - t.Fatalf("err: %s", err) - } - backupPath := backupf.Name() - backupf.Close() - os.Remove(backupPath) + // Backup path + backupf, err := ioutil.TempFile(testingDir, "tf") + if err != nil { + t.Fatalf("err: %s", err) + } + backupPath := backupf.Name() + backupf.Close() + os.Remove(backupPath) - p := testProvider() - ui := new(cli.MockUi) - c := &RefreshCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(p), - Ui: ui, - }, - } + p := testProvider() + ui := new(cli.MockUi) + c := &RefreshCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + }, + } - p.RefreshFn = nil - p.RefreshReturn = newInstanceState("yes") + p.RefreshFn = nil + p.RefreshReturn = newInstanceState("yes") - args := []string{ - "-state", statePath, - "-state-out", outPath, - "-backup", backupPath, - testFixturePath("refresh"), - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + args := []string{ + "-state", statePath, + "-state-out", outPath, + "-backup", backupPath, + testFixturePath("refresh"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } - f, err := os.Open(statePath) - if err != nil { - t.Fatalf("err: %s", err) - } + f, err := os.Open(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } - newState, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } + newState, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } - if !reflect.DeepEqual(newState, state) { - t.Fatalf("bad: %#v", newState) - } + if !reflect.DeepEqual(newState, state) { + t.Fatalf("bad: %#v", newState) + } - f, err = os.Open(outPath) - if err != nil { - t.Fatalf("err: %s", err) - } + f, err = os.Open(outPath) + if err != nil { + t.Fatalf("err: %s", err) + } - newState, err = terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } + newState, err = terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } - actual := newState.RootModule().Resources["test_instance.foo"].Primary - expected := p.RefreshReturn - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } + actual := newState.RootModule().Resources["test_instance.foo"].Primary + expected := p.RefreshReturn + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } - f, err = os.Open(backupPath) - if err != nil { - t.Fatalf("err: %s", err) - } + f, err = os.Open(backupPath) + if err != nil { + t.Fatalf("err: %s", err) + } - backupState, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } + backupState, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } - actualStr := strings.TrimSpace(backupState.String()) - expectedStr := strings.TrimSpace(state.String()) - if actualStr != expectedStr { - t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) - } + actualStr := strings.TrimSpace(backupState.String()) + expectedStr := strings.TrimSpace(state.String()) + if actualStr != expectedStr { + t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) + } + */ } func TestRefresh_disableBackup(t *testing.T) { - state := testState() - statePath := testStateFile(t, state) + t.Fatal("not yet updated for new provider types") + /* + state := testState() + statePath := testStateFile(t, state) - // Output path - outf, err := ioutil.TempFile(testingDir, "tf") - if err != nil { - t.Fatalf("err: %s", err) - } - outPath := outf.Name() - outf.Close() - os.Remove(outPath) + // Output path + outf, err := ioutil.TempFile(testingDir, "tf") + if err != nil { + t.Fatalf("err: %s", err) + } + outPath := outf.Name() + outf.Close() + os.Remove(outPath) - p := testProvider() - ui := new(cli.MockUi) - c := &RefreshCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(p), - Ui: ui, - }, - } + p := testProvider() + ui := new(cli.MockUi) + c := &RefreshCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + }, + } - p.RefreshFn = nil - p.RefreshReturn = newInstanceState("yes") + p.RefreshFn = nil + p.RefreshReturn = newInstanceState("yes") - args := []string{ - "-state", statePath, - "-state-out", outPath, - "-backup", "-", - testFixturePath("refresh"), - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + args := []string{ + "-state", statePath, + "-state-out", outPath, + "-backup", "-", + testFixturePath("refresh"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } - f, err := os.Open(statePath) - if err != nil { - t.Fatalf("err: %s", err) - } + f, err := os.Open(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } - newState, err := terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } + newState, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } - if !reflect.DeepEqual(newState, state) { - t.Fatalf("bad: %#v", newState) - } + if !reflect.DeepEqual(newState, state) { + t.Fatalf("bad: %#v", newState) + } - f, err = os.Open(outPath) - if err != nil { - t.Fatalf("err: %s", err) - } + f, err = os.Open(outPath) + if err != nil { + t.Fatalf("err: %s", err) + } - newState, err = terraform.ReadState(f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } + newState, err = terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } - actual := newState.RootModule().Resources["test_instance.foo"].Primary - expected := p.RefreshReturn - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } + actual := newState.RootModule().Resources["test_instance.foo"].Primary + expected := p.RefreshReturn + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } - // Ensure there is no backup - _, err = os.Stat(outPath + DefaultBackupExtension) - if err == nil || !os.IsNotExist(err) { - t.Fatalf("backup should not exist") - } - _, err = os.Stat("-") - if err == nil || !os.IsNotExist(err) { - t.Fatalf("backup should not exist") - } + // Ensure there is no backup + _, err = os.Stat(outPath + DefaultBackupExtension) + if err == nil || !os.IsNotExist(err) { + t.Fatalf("backup should not exist") + } + _, err = os.Stat("-") + if err == nil || !os.IsNotExist(err) { + t.Fatalf("backup should not exist") + } + */ } func TestRefresh_displaysOutputs(t *testing.T) { @@ -782,17 +688,21 @@ func TestRefresh_displaysOutputs(t *testing.T) { } } -// When creating an InstaneState for direct comparison to one contained in -// terraform.State, all fields must be initialized (duplicating the -// InstanceState.init() method) -func newInstanceState(id string) *terraform.InstanceState { - return &terraform.InstanceState{ - ID: id, - Attributes: make(map[string]string), - Ephemeral: terraform.EphemeralState{ - ConnInfo: make(map[string]string), - }, - Meta: make(map[string]interface{}), +// newInstanceState creates a new states.ResourceInstanceObjectSrc with the +// given value for its single id attribute. It is named newInstanceState for +// historical reasons, because it was originally written for the poorly-named +// terraform.InstanceState type. +func newInstanceState(id string) *states.ResourceInstanceObjectSrc { + attrs := map[string]interface{}{ + "id": id, + } + attrsJSON, err := json.Marshal(attrs) + if err != nil { + panic(fmt.Sprintf("failed to marshal attributes: %s", err)) // should never happen + } + return &states.ResourceInstanceObjectSrc{ + AttrsJSON: attrsJSON, + Status: states.ObjectReady, } } diff --git a/command/show.go b/command/show.go index bc24cc993..5c65c87b2 100644 --- a/command/show.go +++ b/command/show.go @@ -6,8 +6,12 @@ import ( "os" "strings" + "github.com/hashicorp/terraform/plans/planfile" + "github.com/hashicorp/terraform/states/statefile" + "github.com/hashicorp/terraform/command/format" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" ) // ShowCommand is a Command implementation that reads and outputs the @@ -42,31 +46,30 @@ func (c *ShowCommand) Run(args []string) int { var planErr, stateErr error var path string - var plan *terraform.Plan - var state *terraform.State + var plan *plans.Plan + var state *states.State if len(args) > 0 { path = args[0] - f, err := os.Open(path) + pr, err := planfile.Open(path) if err != nil { - c.Ui.Error(fmt.Sprintf("Error loading file: %s", err)) - return 1 - } - defer f.Close() - - plan, err = terraform.ReadPlan(f) - if err != nil { - if _, err := f.Seek(0, 0); err != nil { - c.Ui.Error(fmt.Sprintf("Error reading file: %s", err)) + f, err := os.Open(path) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error loading file: %s", err)) return 1 } + defer f.Close() - plan = nil - planErr = err - } - if plan == nil { - state, err = terraform.ReadState(f) + var stateFile *statefile.File + stateFile, err = statefile.Read(f) if err != nil { stateErr = err + } else { + state = stateFile.State + } + } else { + plan, err = pr.ReadPlan() + if err != nil { + planErr = err } } } else { @@ -80,7 +83,7 @@ func (c *ShowCommand) Run(args []string) int { env := c.Workspace() // Get the state - stateStore, err := b.State(env) + stateStore, err := b.StateMgr(env) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) return 1 @@ -110,7 +113,7 @@ func (c *ShowCommand) Run(args []string) int { } if plan != nil { - dispPlan := format.NewPlan(plan) + dispPlan := format.NewPlan(plan.Changes) c.Ui.Output(dispPlan.Format(c.Colorize())) return 0 } diff --git a/command/show_test.go b/command/show_test.go index 61ae55b96..e0ff42cca 100644 --- a/command/show_test.go +++ b/command/show_test.go @@ -2,12 +2,8 @@ package command import ( "path/filepath" - "strings" "testing" - "github.com/hashicorp/terraform/configs" - - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) @@ -68,9 +64,7 @@ func TestShow_noArgsNoState(t *testing.T) { } func TestShow_plan(t *testing.T) { - planPath := testPlanFile(t, &terraform.Plan{ - Config: configs.NewEmptyConfig(), - }) + planPath := testPlanFileNoop(t) ui := new(cli.MockUi) c := &ShowCommand{ @@ -88,36 +82,6 @@ func TestShow_plan(t *testing.T) { } } -func TestShow_noArgsRemoteState(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - // Create some legacy remote state - legacyState := testState() - _, srv := testRemoteState(t, legacyState, 200) - defer srv.Close() - testStateFileRemote(t, legacyState) - - ui := new(cli.MockUi) - c := &ShowCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - }, - } - - args := []string{} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.OutputWriter.String()) - } - - expected := "test_instance.foo" - actual := ui.OutputWriter.String() - if !strings.Contains(actual, expected) { - t.Fatalf("expected:\n%s\n\nto include: %q", actual, expected) - } -} - func TestShow_state(t *testing.T) { originalState := testState() statePath := testStateFile(t, originalState) diff --git a/command/state_list.go b/command/state_list.go index 4913c67b4..9f24e523d 100644 --- a/command/state_list.go +++ b/command/state_list.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) @@ -23,7 +22,7 @@ func (c *StateListCommand) Run(args []string) int { cmdFlags := c.Meta.flagSet("state list") cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") - lookupId := cmdFlags.String("id", "", "Restrict output to paths with a resource having the specified ID.") + //lookupId := cmdFlags.String("id", "", "Restrict output to paths with a resource having the specified ID.") if err := cmdFlags.Parse(args); err != nil { return cli.RunResultHelp } @@ -38,7 +37,7 @@ func (c *StateListCommand) Run(args []string) int { env := c.Workspace() // Get the state - state, err := b.State(env) + state, err := b.StateMgr(env) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) return 1 @@ -55,7 +54,11 @@ func (c *StateListCommand) Run(args []string) int { return 1 } - filter := &terraform.StateFilter{State: stateReal} + // FIXME: update this for the new state types + c.Ui.Error("state list command not yet updated for new state types") + return 1 + + /*filter := &terraform.StateFilter{State: stateReal} results, err := filter.Filter(args...) if err != nil { c.Ui.Error(fmt.Sprintf(errStateFilter, err)) @@ -68,7 +71,7 @@ func (c *StateListCommand) Run(args []string) int { c.Ui.Output(result.Address) } } - } + }*/ return 0 } diff --git a/command/state_meta.go b/command/state_meta.go index 334e03a28..cc66d7586 100644 --- a/command/state_meta.go +++ b/command/state_meta.go @@ -7,6 +7,7 @@ import ( backendLocal "github.com/hashicorp/terraform/backend/local" "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" ) @@ -26,9 +27,7 @@ func (c *StateMeta) State() (state.State, error) { // use the specified state if c.statePath != "" { - realState = &state.LocalState{ - Path: c.statePath, - } + realState = statemgr.NewFilesystem(c.statePath) } else { // Load the backend b, backendDiags := c.Backend(nil) @@ -36,9 +35,9 @@ func (c *StateMeta) State() (state.State, error) { return nil, backendDiags.Err() } - env := c.Workspace() + workspace := c.Workspace() // Get the state - s, err := b.State(env) + s, err := b.StateMgr(workspace) if err != nil { return nil, err } @@ -49,8 +48,8 @@ func (c *StateMeta) State() (state.State, error) { // This should never fail panic(backendDiags.Err()) } - localB := localRaw.(*backendLocal.Local) - _, stateOutPath, _ = localB.StatePaths(env) + localB := localRaw.(*backendlocal.Local) + _, stateOutPath, _ = localB.StatePaths(workspace) if err != nil { return nil, err } @@ -70,10 +69,10 @@ func (c *StateMeta) State() (state.State, error) { DefaultBackupExtension) } - // Wrap it for backups - realState = &state.BackupState{ - Real: realState, - Path: backupPath, + // If the backend is local (which it should always be, given our asserting + // of it above) we can now enable backups for it. + if lb, ok := realState.(*statemgr.Filesystem); ok { + lb.SetBackupPath(backupPath) } return realState, nil diff --git a/command/state_mv.go b/command/state_mv.go index e2f89c963..2f0c6305c 100644 --- a/command/state_mv.go +++ b/command/state_mv.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) @@ -74,59 +75,62 @@ func (c *StateMvCommand) Run(args []string) int { stateToReal = stateTo.State() if stateToReal == nil { - stateToReal = terraform.NewState() + stateToReal = states.NewState() } } - // Filter what we're moving - filter := &terraform.StateFilter{State: stateFromReal} - results, err := filter.Filter(args[0]) - if err != nil { - c.Ui.Error(fmt.Sprintf(errStateMv, err)) - return cli.RunResultHelp - } - if len(results) == 0 { - c.Ui.Output(fmt.Sprintf("Item to move doesn't exist: %s", args[0])) - return 1 - } + c.Ui.Error("state mv command not yet updated for new state types") + /* + // Filter what we're moving + filter := &terraform.StateFilter{State: stateFromReal} + results, err := filter.Filter(args[0]) + if err != nil { + c.Ui.Error(fmt.Sprintf(errStateMv, err)) + return cli.RunResultHelp + } + if len(results) == 0 { + c.Ui.Output(fmt.Sprintf("Item to move doesn't exist: %s", args[0])) + return 1 + } - // Get the item to add to the state - add := c.addableResult(results) + // Get the item to add to the state + add := c.addableResult(results) - // Do the actual move - if err := stateFromReal.Remove(args[0]); err != nil { - c.Ui.Error(fmt.Sprintf(errStateMv, err)) - return 1 - } + // Do the actual move + if err := stateFromReal.Remove(args[0]); err != nil { + c.Ui.Error(fmt.Sprintf(errStateMv, err)) + return 1 + } - if err := stateToReal.Add(args[0], args[1], add); err != nil { - c.Ui.Error(fmt.Sprintf(errStateMv, err)) - return 1 - } + if err := stateToReal.Add(args[0], args[1], add); err != nil { + c.Ui.Error(fmt.Sprintf(errStateMv, err)) + return 1 + } - // Write the new state - if err := stateTo.WriteState(stateToReal); err != nil { - c.Ui.Error(fmt.Sprintf(errStateMvPersist, err)) - return 1 - } - - if err := stateTo.PersistState(); err != nil { - c.Ui.Error(fmt.Sprintf(errStateMvPersist, err)) - return 1 - } - - // Write the old state if it is different - if stateTo != stateFrom { - if err := stateFrom.WriteState(stateFromReal); err != nil { + // Write the new state + if err := stateTo.WriteState(stateToReal); err != nil { c.Ui.Error(fmt.Sprintf(errStateMvPersist, err)) return 1 } - if err := stateFrom.PersistState(); err != nil { + if err := stateTo.PersistState(); err != nil { c.Ui.Error(fmt.Sprintf(errStateMvPersist, err)) return 1 } - } + + // Write the old state if it is different + if stateTo != stateFrom { + if err := stateFrom.WriteState(stateFromReal); err != nil { + c.Ui.Error(fmt.Sprintf(errStateMvPersist, err)) + return 1 + } + + if err := stateFrom.PersistState(); err != nil { + c.Ui.Error(fmt.Sprintf(errStateMvPersist, err)) + return 1 + } + } + */ c.Ui.Output(fmt.Sprintf( "Moved %s to %s", args[0], args[1])) diff --git a/command/state_mv_test.go b/command/state_mv_test.go index 5a0d2ab44..80dcdf16c 100644 --- a/command/state_mv_test.go +++ b/command/state_mv_test.go @@ -1,47 +1,46 @@ package command import ( + "fmt" "os" "path/filepath" "testing" - "github.com/hashicorp/terraform/helper/copy" - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/helper/copy" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/terraform" ) func TestStateMv(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.baz": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "baz", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) p := testProvider() @@ -84,37 +83,32 @@ func TestStateMv_explicitWithBackend(t *testing.T) { backupPath := filepath.Join(td, "backup") - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.baz": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "baz", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) // init our backend @@ -162,37 +156,32 @@ func TestStateMv_backupExplicit(t *testing.T) { defer os.RemoveAll(td) backupPath := filepath.Join(td, "backup") - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.baz": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "baz", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) p := testProvider() @@ -224,26 +213,20 @@ func TestStateMv_backupExplicit(t *testing.T) { } func TestStateMv_stateOutNew(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) stateOutPath := statePath + ".out" @@ -281,44 +264,36 @@ func TestStateMv_stateOutNew(t *testing.T) { } func TestStateMv_stateOutExisting(t *testing.T) { - stateSrc := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - }, + stateSrc := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, stateSrc) - stateDst := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.qux": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - }, - }, - }, + stateDst := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "qux", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) stateOutPath := testStateFile(t, stateDst) p := testProvider() @@ -382,48 +357,44 @@ func TestStateMv_noState(t *testing.T) { } func TestStateMv_stateOutNew_count(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo.0": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.foo.1": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.bar": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) stateOutPath := statePath + ".out" @@ -463,147 +434,35 @@ func TestStateMv_stateOutNew_count(t *testing.T) { // Modules with more than 10 resources were sorted lexically, causing the // indexes in the new location to change. func TestStateMv_stateOutNew_largeCount(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo.0": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo0", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.foo.1": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo1", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.foo.2": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo2", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.foo.3": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo3", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.foo.4": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo4", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.foo.5": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo5", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.foo.6": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo6", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.foo.7": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo7", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.foo.8": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo8", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.foo.9": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo9", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.foo.10": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo10", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.bar": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + // test_instance.foo has 11 instances, all the same except for their ids + for i := 0; i < 11; i++ { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.IntKey(i)).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(fmt.Sprintf(`{"id":"foo%d","foo":"value","bar":"value"}`, i)), + Status: states.ObjectReady, }, + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + } + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) stateOutPath := statePath + ".out" @@ -641,51 +500,32 @@ func TestStateMv_stateOutNew_largeCount(t *testing.T) { } func TestStateMv_stateOutNew_nestedModule(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{}, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance.Child("foo", addrs.NoKey).Child("child1", addrs.NoKey)), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, }, - - &terraform.ModuleState{ - Path: []string{"root", "foo"}, - Resources: map[string]*terraform.ResourceState{}, + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance.Child("foo", addrs.NoKey).Child("child2", addrs.NoKey)), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, }, - - &terraform.ModuleState{ - Path: []string{"root", "foo", "child1"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - }, - }, - - &terraform.ModuleState{ - Path: []string{"root", "foo", "child2"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - }, - }, - }, - } + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) stateOutPath := statePath + ".out" diff --git a/command/state_pull.go b/command/state_pull.go index 6b211ca04..0f12aaf74 100644 --- a/command/state_pull.go +++ b/command/state_pull.go @@ -1,11 +1,9 @@ package command import ( - "bytes" "fmt" "strings" - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) @@ -36,7 +34,7 @@ func (c *StatePullCommand) Run(args []string) int { // Get the state env := c.Workspace() - state, err := b.State(env) + state, err := b.StateMgr(env) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) return 1 @@ -54,13 +52,19 @@ func (c *StatePullCommand) Run(args []string) int { return 0 } - var buf bytes.Buffer - if err := terraform.WriteState(s, &buf); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) - return 1 - } + c.Ui.Error("state pull not yet updated for new state types") + return 1 + + /* + var buf bytes.Buffer + if err := terraform.WriteState(s, &buf); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) + return 1 + } + + c.Ui.Output(buf.String()) + */ - c.Ui.Output(buf.String()) return 0 } diff --git a/command/state_pull_test.go b/command/state_pull_test.go index c93f4e43c..9e1935444 100644 --- a/command/state_pull_test.go +++ b/command/state_pull_test.go @@ -13,9 +13,9 @@ func TestStatePull(t *testing.T) { // Create some legacy remote state legacyState := testState() - _, srv := testRemoteState(t, legacyState, 200) + backendState, srv := testRemoteState(t, legacyState, 200) defer srv.Close() - testStateFileRemote(t, legacyState) + testStateFileRemote(t, backendState) p := testProvider() ui := new(cli.MockUi) diff --git a/command/state_push.go b/command/state_push.go index 5455fcc51..401d5ee1b 100644 --- a/command/state_push.go +++ b/command/state_push.go @@ -1,12 +1,8 @@ package command import ( - "fmt" - "io" - "os" "strings" - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) @@ -35,80 +31,86 @@ func (c *StatePushCommand) Run(args []string) int { return 1 } - // Determine our reader for the input state. This is the filepath - // or stdin if "-" is given. - var r io.Reader = os.Stdin - if args[0] != "-" { - f, err := os.Open(args[0]) + c.Ui.Error("state push not yet updated for new state types") + return 1 + + /* + // Determine our reader for the input state. This is the filepath + // or stdin if "-" is given. + var r io.Reader = os.Stdin + if args[0] != "-" { + f, err := os.Open(args[0]) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + // Note: we don't need to defer a Close here because we do a close + // automatically below directly after the read. + + r = f + } + + // Read the state + sourceState, err := terraform.ReadState(r) + if c, ok := r.(io.Closer); ok { + // Close the reader if possible right now since we're done with it. + c.Close() + } if err != nil { - c.Ui.Error(err.Error()) + c.Ui.Error(fmt.Sprintf("Error reading source state %q: %s", args[0], err)) return 1 } - // Note: we don't need to defer a Close here because we do a close - // automatically below directly after the read. - - r = f - } - - // Read the state - sourceState, err := terraform.ReadState(r) - if c, ok := r.(io.Closer); ok { - // Close the reader if possible right now since we're done with it. - c.Close() - } - if err != nil { - c.Ui.Error(fmt.Sprintf("Error reading source state %q: %s", args[0], err)) - return 1 - } - - // Load the backend - b, backendDiags := c.Backend(nil) - if backendDiags.HasErrors() { - c.showDiagnostics(backendDiags) - return 1 - } - - // Get the state - env := c.Workspace() - state, err := b.State(env) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err)) - return 1 - } - if err := state.RefreshState(); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err)) - return 1 - } - dstState := state.State() - - // If we're not forcing, then perform safety checks - if !flagForce && !dstState.Empty() { - if !dstState.SameLineage(sourceState) { - c.Ui.Error(strings.TrimSpace(errStatePushLineage)) + // Load the backend + b, backendDiags := c.Backend(nil) + if backendDiags.HasErrors() { + c.showDiagnostics(backendDiags) return 1 } - age, err := dstState.CompareAges(sourceState) + // Get the state + env := c.Workspace() + state, err := b.StateMgr(env) if err != nil { - c.Ui.Error(err.Error()) + c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err)) return 1 } - if age == terraform.StateAgeReceiverNewer { - c.Ui.Error(strings.TrimSpace(errStatePushSerialNewer)) + if err := state.RefreshState(); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err)) return 1 } - } - // Overwrite it - if err := state.WriteState(sourceState); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err)) - return 1 - } - if err := state.PersistState(); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err)) - return 1 - } + dstState := state.State() + + // If we're not forcing, then perform safety checks + if !flagForce && !dstState.Empty() { + if !dstState.SameLineage(sourceState) { + c.Ui.Error(strings.TrimSpace(errStatePushLineage)) + return 1 + } + + age, err := dstState.CompareAges(sourceState) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + if age == terraform.StateAgeReceiverNewer { + c.Ui.Error(strings.TrimSpace(errStatePushSerialNewer)) + return 1 + } + } + + // Overwrite it + if err := state.WriteState(sourceState); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err)) + return 1 + } + if err := state.PersistState(); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err)) + return 1 + } + */ return 0 } diff --git a/command/state_push_test.go b/command/state_push_test.go index bee9d4775..e7fd5f446 100644 --- a/command/state_push_test.go +++ b/command/state_push_test.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/backend/remote-state/inmem" "github.com/hashicorp/terraform/helper/copy" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/states" "github.com/mitchellh/cli" ) @@ -81,7 +81,7 @@ func TestStatePush_replaceMatchStdin(t *testing.T) { // Setup the replacement to come from stdin var buf bytes.Buffer - if err := terraform.WriteState(expected, &buf); err != nil { + if err := writeStateForTesting(expected, &buf); err != nil { t.Fatalf("err: %s", err) } defer testStdinPipe(t, &buf)() @@ -200,7 +200,7 @@ func TestStatePush_forceRemoteState(t *testing.T) { defer testChdir(t, td)() defer inmem.Reset() - s := terraform.NewState() + s := states.NewState() statePath := testStateFile(t, s) // init the backend @@ -223,11 +223,11 @@ func TestStatePush_forceRemoteState(t *testing.T) { // put a dummy state in place, so we have something to force b := backend.TestBackendConfig(t, inmem.New(), nil) - sMgr, err := b.State("test") + sMgr, err := b.StateMgr("test") if err != nil { t.Fatal(err) } - if err := sMgr.WriteState(terraform.NewState()); err != nil { + if err := sMgr.WriteState(states.NewState()); err != nil { t.Fatal(err) } if err := sMgr.PersistState(); err != nil { diff --git a/command/state_rm.go b/command/state_rm.go index 53bb50d01..342056864 100644 --- a/command/state_rm.go +++ b/command/state_rm.go @@ -47,10 +47,15 @@ func (c *StateRmCommand) Run(args []string) int { return 1 } - if err := stateReal.Remove(args...); err != nil { - c.Ui.Error(fmt.Sprintf(errStateRm, err)) - return 1 - } + c.Ui.Error("state rm not yet updated for new state types") + return 1 + + /* + if err := stateReal.Remove(args...); err != nil { + c.Ui.Error(fmt.Sprintf(errStateRm, err)) + return 1 + } + */ c.Ui.Output(fmt.Sprintf("%d items removed.", len(args))) diff --git a/command/state_rm_test.go b/command/state_rm_test.go index 3a4477793..1edacadbf 100644 --- a/command/state_rm_test.go +++ b/command/state_rm_test.go @@ -6,43 +6,41 @@ import ( "strings" "testing" - "github.com/hashicorp/terraform/helper/copy" - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/helper/copy" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/terraform" ) func TestStateRm(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.bar": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) p := testProvider() @@ -76,37 +74,32 @@ func TestStateRm(t *testing.T) { } func TestStateRmNoArgs(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.bar": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) p := testProvider() @@ -138,37 +131,32 @@ func TestStateRm_backupExplicit(t *testing.T) { defer os.RemoveAll(td) backupPath := filepath.Join(td, "backup") - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - - "test_instance.bar": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) p := testProvider() diff --git a/command/state_show.go b/command/state_show.go index 5d7443965..db59cbca1 100644 --- a/command/state_show.go +++ b/command/state_show.go @@ -2,12 +2,9 @@ package command import ( "fmt" - "sort" "strings" - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" - "github.com/ryanuber/columnize" ) // StateShowCommand is a Command implementation that shows a single resource. @@ -38,7 +35,7 @@ func (c *StateShowCommand) Run(args []string) int { // Get the state env := c.Workspace() - state, err := b.State(env) + state, err := b.StateMgr(env) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) return 1 @@ -54,50 +51,55 @@ func (c *StateShowCommand) Run(args []string) int { return 1 } - filter := &terraform.StateFilter{State: stateReal} - results, err := filter.Filter(args...) - if err != nil { - c.Ui.Error(fmt.Sprintf(errStateFilter, err)) - return 1 - } + c.Ui.Error("state show not yet updated for new state types") + return 1 - if len(results) == 0 { - return 0 - } - - instance, err := c.filterInstance(results) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - if instance == nil { - return 0 - } - - is := instance.Value.(*terraform.InstanceState) - - // Sort the keys - var keys []string - for k, _ := range is.Attributes { - keys = append(keys, k) - } - sort.Strings(keys) - - // Build the output - var output []string - output = append(output, fmt.Sprintf("id | %s", is.ID)) - for _, k := range keys { - if k != "id" { - output = append(output, fmt.Sprintf("%s | %s", k, is.Attributes[k])) + /* + filter := &terraform.StateFilter{State: stateReal} + results, err := filter.Filter(args...) + if err != nil { + c.Ui.Error(fmt.Sprintf(errStateFilter, err)) + return 1 } - } - // Output - config := columnize.DefaultConfig() - config.Glue = " = " - c.Ui.Output(columnize.Format(output, config)) - return 0 + if len(results) == 0 { + return 0 + } + + instance, err := c.filterInstance(results) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + if instance == nil { + return 0 + } + + is := instance.Value.(*terraform.InstanceState) + + // Sort the keys + var keys []string + for k, _ := range is.Attributes { + keys = append(keys, k) + } + sort.Strings(keys) + + // Build the output + var output []string + output = append(output, fmt.Sprintf("id | %s", is.ID)) + for _, k := range keys { + if k != "id" { + output = append(output, fmt.Sprintf("%s | %s", k, is.Attributes[k])) + } + } + + // Output + config := columnize.DefaultConfig() + config.Glue = " = " + c.Ui.Output(columnize.Format(output, config)) + return 0 + */ } func (c *StateShowCommand) Help() string { diff --git a/command/state_show_test.go b/command/state_show_test.go index e2cd96d38..c2f56c871 100644 --- a/command/state_show_test.go +++ b/command/state_show_test.go @@ -4,31 +4,27 @@ import ( "strings" "testing" - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/states" ) func TestStateShow(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) p := testProvider() @@ -57,36 +53,32 @@ func TestStateShow(t *testing.T) { } func TestStateShow_multi(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo.0": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - "test_instance.foo.1": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "value", - "bar": "value", - }, - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, }, - }, - } - + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) p := testProvider() @@ -127,8 +119,7 @@ func TestStateShow_noState(t *testing.T) { } func TestStateShow_emptyState(t *testing.T) { - state := terraform.NewState() - + state := states.NewState() statePath := testStateFile(t, state) p := testProvider() @@ -149,34 +140,6 @@ func TestStateShow_emptyState(t *testing.T) { } } -func TestStateShow_emptyStateWithModule(t *testing.T) { - // empty state with empty module - state := terraform.NewState() - - mod := &terraform.ModuleState{ - Path: []string{"root", "mod"}, - } - state.Modules = append(state.Modules, mod) - - statePath := testStateFile(t, state) - - p := testProvider() - ui := new(cli.MockUi) - c := &StateShowCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(p), - Ui: ui, - }, - } - - args := []string{ - "-state", statePath, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } -} - const testStateShowOutput = ` id = bar bar = value diff --git a/command/taint.go b/command/taint.go index a6a12f214..b0cff3dc0 100644 --- a/command/taint.go +++ b/command/taint.go @@ -3,12 +3,13 @@ package command import ( "context" "fmt" - "log" "strings" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/command/clistate" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" ) // TaintCommand is a cli.Command implementation that manually taints @@ -38,6 +39,8 @@ func (c *TaintCommand) Run(args []string) int { return 1 } + var diags tfdiags.Diagnostics + // Require the one argument for the resource to taint args = cmdFlags.Args() if len(args) != 1 { @@ -46,34 +49,34 @@ func (c *TaintCommand) Run(args []string) int { return 1 } - name := args[0] - if module == "" { - module = "root" - } else { - module = "root." + module - } - - rsk, err := terraform.ParseResourceStateKey(name) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to parse resource name: %s", err)) + if module != "" { + c.Ui.Error("The -module option is no longer used. Instead, include the module path in the main resource address, like \"module.foo.module.bar.null_resource.baz\".") return 1 } - if !rsk.Mode.Taintable() { - c.Ui.Error(fmt.Sprintf("Resource '%s' cannot be tainted", name)) + addr, addrDiags := addrs.ParseAbsResourceInstanceStr(args[0]) + diags = diags.Append(addrDiags) + if addrDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + + if addr.Resource.Resource.Mode != addrs.ManagedResourceMode { + c.Ui.Error(fmt.Sprintf("Resource instance %s cannot be tainted", addr)) return 1 } // Load the backend b, backendDiags := c.Backend(nil) + diags = diags.Append(backendDiags) if backendDiags.HasErrors() { - c.showDiagnostics(backendDiags) + c.showDiagnostics(diags) return 1 } // Get the state env := c.Workspace() - st, err := b.State(env) + st, err := b.StateMgr(env) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) return 1 @@ -97,63 +100,60 @@ func (c *TaintCommand) Run(args []string) int { s := st.State() if s.Empty() { if allowMissing { - return c.allowMissingExit(name, module) + return c.allowMissingExit(addr) } - c.Ui.Error(fmt.Sprintf( - "The state is empty. The most common reason for this is that\n" + - "an invalid state file path was given or Terraform has never\n " + - "been run for this infrastructure. Infrastructure must exist\n" + - "for it to be tainted.")) + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "No such resource instance", + "The state currently contains no resource instances whatsoever. This may occur if the configuration has never been applied or if it has recently been destroyed.", + )) + c.showDiagnostics(diags) return 1 } - // Get the ModuleState where we will taint. This is provided in a legacy - // string form that doesn't support module instance keys, so we'll shim - // it here. - modPath := addrs.Module(strings.Split(module, ".")).UnkeyedInstanceShim() - mod := s.ModuleByPath(modPath) - if mod == nil { + state := s.SyncWrapper() + + // Get the resource and instance we're going to taint + rs := state.Resource(addr.ContainingResource()) + is := state.ResourceInstance(addr) + if is == nil { if allowMissing { - return c.allowMissingExit(name, module) + return c.allowMissingExit(addr) } - c.Ui.Error(fmt.Sprintf( - "The module %s could not be found. There is nothing to taint.", - module)) + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "No such resource instance", + fmt.Sprintf("There is no resource instance in the state with the address %s. If the resource configuration has just been added, you must run \"terraform apply\" once to create the corresponding instance(s) before they can be tainted.", addr), + )) + c.showDiagnostics(diags) return 1 } - // If there are no resources in this module, it is an error - if len(mod.Resources) == 0 { - if allowMissing { - return c.allowMissingExit(name, module) + obj := is.Current + if obj == nil { + if len(is.Deposed) != 0 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "No such resource instance", + fmt.Sprintf("Resource instance %s is currently part-way through a create_before_destroy replacement action. Run \"terraform apply\" to complete its replacement before tainting it.", addr), + )) + } else { + // Don't know why we're here, but we'll produce a generic error message anyway. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "No such resource instance", + fmt.Sprintf("Resource instance %s does not currently have a remote object associated with it, so it cannot be tainted.", addr), + )) } - - c.Ui.Error(fmt.Sprintf( - "The module %s has no resources. There is nothing to taint.", - module)) + c.showDiagnostics(diags) return 1 } - // Get the resource we're looking for - rs, ok := mod.Resources[name] - if !ok { - if allowMissing { - return c.allowMissingExit(name, module) - } + obj.Status = states.ObjectTainted + state.SetResourceInstanceCurrent(addr, obj, rs.ProviderConfig) - c.Ui.Error(fmt.Sprintf( - "The resource %s couldn't be found in the module %s.", - name, - module)) - return 1 - } - - // Taint the resource - rs.Taint() - - log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath()) if err := st.WriteState(s); err != nil { c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) return 1 @@ -163,24 +163,28 @@ func (c *TaintCommand) Run(args []string) int { return 1 } - c.Ui.Output(fmt.Sprintf( - "The resource %s in the module %s has been marked as tainted!", - name, module)) + c.Ui.Output(fmt.Sprintf("Resource instance %s has been marked as tainted.", addr)) return 0 } func (c *TaintCommand) Help() string { helpText := ` -Usage: terraform taint [options] name +Usage: terraform taint [options]
Manually mark a resource as tainted, forcing a destroy and recreate on the next plan/apply. This will not modify your infrastructure. This command changes your state to mark a resource as tainted so that during the next plan or - apply, that resource will be destroyed and recreated. This command on - its own will not modify infrastructure. This command can be undone by - reverting the state backup file that is created. + apply that resource will be destroyed and recreated. This command on + its own will not modify infrastructure. This command can be undone + using the "terraform untaint" command with the same address. + + The address is in the usual resource address syntax, as shown in + the output from other commands, such as: + aws_instance.foo + aws_instance.bar[1] + module.foo.module.bar.aws_instance.baz Options: @@ -195,10 +199,6 @@ Options: -lock-timeout=0s Duration to retry a state lock. - -module=path The module path where the resource lives. By - default this will be root. Child modules can be specified - by names. Ex. "consul" or "consul.vpc" (nested modules). - -no-color If specified, output won't contain any color. -state=path Path to read and save state (unless state-out @@ -215,10 +215,11 @@ func (c *TaintCommand) Synopsis() string { return "Manually mark a resource for recreation" } -func (c *TaintCommand) allowMissingExit(name, module string) int { - c.Ui.Output(fmt.Sprintf( - "The resource %s in the module %s was not found, but\n"+ - "-allow-missing is set, so we're exiting successfully.", - name, module)) +func (c *TaintCommand) allowMissingExit(name addrs.AbsResourceInstance) int { + c.showDiagnostics(tfdiags.Sourceless( + tfdiags.Warning, + "No such resource instance", + "Resource instance %s was not found, but this is not an error because -allow-missing was set.", + )) return 0 } diff --git a/command/taint_test.go b/command/taint_test.go index c658ff0a3..8d2c3d74c 100644 --- a/command/taint_test.go +++ b/command/taint_test.go @@ -5,26 +5,28 @@ import ( "strings" "testing" - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/terraform" ) func TestTaint(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectReady, }, - }, - } + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) ui := new(cli.MockUi) @@ -46,21 +48,20 @@ func TestTaint(t *testing.T) { } func TestTaint_lockedState(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectReady, }, - }, - } + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) unlock, err := testLockState("./testdata", statePath) @@ -233,21 +234,20 @@ func TestTaint_defaultState(t *testing.T) { } func TestTaint_missing(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectReady, }, - }, - } + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) ui := new(cli.MockUi) @@ -267,21 +267,20 @@ func TestTaint_missing(t *testing.T) { } func TestTaint_missingAllow(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectReady, }, - }, - } + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) ui := new(cli.MockUi) @@ -344,32 +343,32 @@ func TestTaint_stateOut(t *testing.T) { } func TestTaint_module(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectReady, }, - &terraform.ModuleState{ - Path: []string{"root", "child"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.blah": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "blah", - }, - }, - }, + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "blah", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey)), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"blah"}`), + Status: states.ObjectReady, }, - }, - } + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) ui := new(cli.MockUi) diff --git a/command/unlock.go b/command/unlock.go index 21f8630ec..cf974030a 100644 --- a/command/unlock.go +++ b/command/unlock.go @@ -4,7 +4,8 @@ import ( "fmt" "strings" - "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/states/statemgr" + "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" "github.com/mitchellh/cli" @@ -67,21 +68,13 @@ func (c *UnlockCommand) Run(args []string) int { } env := c.Workspace() - st, err := b.State(env) + st, err := b.StateMgr(env) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) return 1 } - isLocal := false - switch s := st.(type) { - case *state.BackupState: - if _, ok := s.Real.(*state.LocalState); ok { - isLocal = true - } - case *state.LocalState: - isLocal = true - } + _, isLocal := st.(*statemgr.Filesystem) if !force { // Forcing this doesn't do anything, but doesn't break anything either, diff --git a/command/unlock_test.go b/command/unlock_test.go index b6dfceb47..27fce9c83 100644 --- a/command/unlock_test.go +++ b/command/unlock_test.go @@ -25,7 +25,7 @@ func TestUnlock(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - err = terraform.WriteState(testState(), f) + err = terraform.WriteState(terraform.NewState(), f) f.Close() if err != nil { t.Fatalf("err: %s", err) diff --git a/command/untaint.go b/command/untaint.go index 86fd95be8..082f9ce11 100644 --- a/command/untaint.go +++ b/command/untaint.go @@ -3,9 +3,12 @@ package command import ( "context" "fmt" - "log" "strings" + "github.com/hashicorp/terraform/states" + + "github.com/hashicorp/terraform/tfdiags" + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/command/clistate" ) @@ -37,6 +40,8 @@ func (c *UntaintCommand) Run(args []string) int { return 1 } + var diags tfdiags.Diagnostics + // Require the one argument for the resource to untaint args = cmdFlags.Args() if len(args) != 1 { @@ -45,23 +50,29 @@ func (c *UntaintCommand) Run(args []string) int { return 1 } - name := args[0] - if module == "" { - module = "root" - } else { - module = "root." + module + if module != "" { + c.Ui.Error("The -module option is no longer used. Instead, include the module path in the main resource address, like \"module.foo.module.bar.null_resource.baz\".") + return 1 + } + + addr, addrDiags := addrs.ParseAbsResourceInstanceStr(args[0]) + diags = diags.Append(addrDiags) + if addrDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 } // Load the backend b, backendDiags := c.Backend(nil) + diags = diags.Append(backendDiags) if backendDiags.HasErrors() { - c.showDiagnostics(backendDiags) + c.showDiagnostics(diags) return 1 } // Get the state - env := c.Workspace() - st, err := b.State(env) + workspace := c.Workspace() + st, err := b.StateMgr(workspace) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) return 1 @@ -85,63 +96,69 @@ func (c *UntaintCommand) Run(args []string) int { s := st.State() if s.Empty() { if allowMissing { - return c.allowMissingExit(name, module) + return c.allowMissingExit(addr) } - c.Ui.Error(fmt.Sprintf( - "The state is empty. The most common reason for this is that\n" + - "an invalid state file path was given or Terraform has never\n " + - "been run for this infrastructure. Infrastructure must exist\n" + - "for it to be untainted.")) + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "No such resource instance", + "The state currently contains no resource instances whatsoever. This may occur if the configuration has never been applied or if it has recently been destroyed.", + )) + c.showDiagnostics(diags) return 1 } - // Get the ModuleState where we will untaint. This is provided in a legacy - // string form that doesn't support module instance keys, so we'll shim - // it here. - modPath := addrs.Module(strings.Split(module, ".")).UnkeyedInstanceShim() - mod := s.ModuleByPath(modPath) - if mod == nil { + state := s.SyncWrapper() + + // Get the resource and instance we're going to taint + rs := state.Resource(addr.ContainingResource()) + is := state.ResourceInstance(addr) + if is == nil { if allowMissing { - return c.allowMissingExit(name, module) + return c.allowMissingExit(addr) } - c.Ui.Error(fmt.Sprintf( - "The module %s could not be found. There is nothing to untaint.", - module)) + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "No such resource instance", + fmt.Sprintf("There is no resource instance in the state with the address %s. If the resource configuration has just been added, you must run \"terraform apply\" once to create the corresponding instance(s) before they can be tainted.", addr), + )) + c.showDiagnostics(diags) return 1 } - // If there are no resources in this module, it is an error - if len(mod.Resources) == 0 { - if allowMissing { - return c.allowMissingExit(name, module) + obj := is.Current + if obj == nil { + if len(is.Deposed) != 0 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "No such resource instance", + fmt.Sprintf("Resource instance %s is currently part-way through a create_before_destroy replacement action. Run \"terraform apply\" to complete its replacement before tainting it.", addr), + )) + } else { + // Don't know why we're here, but we'll produce a generic error message anyway. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "No such resource instance", + fmt.Sprintf("Resource instance %s does not currently have a remote object associated with it, so it cannot be tainted.", addr), + )) } - - c.Ui.Error(fmt.Sprintf( - "The module %s has no resources. There is nothing to untaint.", - module)) + c.showDiagnostics(diags) return 1 } - // Get the resource we're looking for - rs, ok := mod.Resources[name] - if !ok { - if allowMissing { - return c.allowMissingExit(name, module) - } - - c.Ui.Error(fmt.Sprintf( - "The resource %s couldn't be found in the module %s.", - name, - module)) + if obj.Status != states.ObjectTainted { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Resource instance is not tainted", + fmt.Sprintf("Resource instance %s is not currently tainted, and so it cannot be untainted.", addr), + )) + c.showDiagnostics(diags) return 1 } + obj.Status = states.ObjectReady + state.SetResourceInstanceCurrent(addr, obj, rs.ProviderConfig) - // Untaint the resource - rs.Untaint() - - log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath()) if err := st.WriteState(s); err != nil { c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) return 1 @@ -151,9 +168,7 @@ func (c *UntaintCommand) Run(args []string) int { return 1 } - c.Ui.Output(fmt.Sprintf( - "The resource %s in the module %s has been successfully untainted!", - name, module)) + c.Ui.Output(fmt.Sprintf("Resource instance %s has been successfully untainted.", addr)) return 0 } @@ -203,10 +218,11 @@ func (c *UntaintCommand) Synopsis() string { return "Manually unmark a resource as tainted" } -func (c *UntaintCommand) allowMissingExit(name, module string) int { - c.Ui.Output(fmt.Sprintf( - "The resource %s in the module %s was not found, but\n"+ - "-allow-missing is set, so we're exiting successfully.", - name, module)) +func (c *UntaintCommand) allowMissingExit(name addrs.AbsResourceInstance) int { + c.showDiagnostics(tfdiags.Sourceless( + tfdiags.Warning, + "No such resource instance", + "Resource instance %s was not found, but this is not an error because -allow-missing was set.", + )) return 0 } diff --git a/command/untaint_test.go b/command/untaint_test.go index 0cc27ca0d..fc3d32eb6 100644 --- a/command/untaint_test.go +++ b/command/untaint_test.go @@ -5,27 +5,27 @@ import ( "strings" "testing" + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) func TestUntaint(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Tainted: true, - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectTainted, }, - }, - } + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) ui := new(cli.MockUi) @@ -51,22 +51,20 @@ test_instance.foo: } func TestUntaint_lockedState(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Tainted: true, - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectTainted, }, - }, - } + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) unlock, err := testLockState("./testdata", statePath) if err != nil { @@ -257,22 +255,20 @@ test_instance.foo: } func TestUntaint_missing(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Tainted: true, - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectTainted, }, - }, - } + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) ui := new(cli.MockUi) @@ -292,22 +288,20 @@ func TestUntaint_missing(t *testing.T) { } func TestUntaint_missingAllow(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Tainted: true, - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectTainted, }, - }, - } + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) ui := new(cli.MockUi) @@ -377,34 +371,32 @@ test_instance.foo: } func TestUntaint_module(t *testing.T) { - state := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Tainted: true, - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectTainted, }, - &terraform.ModuleState{ - Path: []string{"root", "child"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.blah": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - Tainted: true, - }, - }, - }, + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "blah", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey)), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectTainted, }, - }, - } + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) statePath := testStateFile(t, state) ui := new(cli.MockUi) diff --git a/command/workspace_command_test.go b/command/workspace_command_test.go index 7baabbed5..4503850fd 100644 --- a/command/workspace_command_test.go +++ b/command/workspace_command_test.go @@ -7,11 +7,14 @@ import ( "strings" "testing" + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/backend/local" "github.com/hashicorp/terraform/backend/remote-state/inmem" "github.com/hashicorp/terraform/helper/copy" "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) @@ -227,24 +230,22 @@ func TestWorkspace_createWithState(t *testing.T) { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) } - // create a non-empty state - originalState := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_instance.foo": &terraform.ResourceState{ - Type: "test_instance", - Primary: &terraform.InstanceState{ - ID: "bar", - }, - }, - }, + originalState := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectReady, }, - }, - } + addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), + ) + }) - err := (&state.LocalState{Path: "test.tfstate"}).WriteState(originalState) + err := statemgr.NewFilesystem("test.tfstate").WriteState(originalState) if err != nil { t.Fatal(err) } @@ -268,14 +269,13 @@ func TestWorkspace_createWithState(t *testing.T) { } b := backend.TestBackendConfig(t, inmem.New(), nil) - sMgr, err := b.State(workspace) + sMgr, err := b.StateMgr(workspace) if err != nil { t.Fatal(err) } newState := sMgr.State() - originalState.Version = newState.Version // the round-trip through the state manager implicitly populates version if !originalState.Equal(newState) { t.Fatalf("states not equal\norig: %s\nnew: %s", originalState, newState) } diff --git a/command/workspace_delete.go b/command/workspace_delete.go index afc78384b..0604e3f87 100644 --- a/command/workspace_delete.go +++ b/command/workspace_delete.go @@ -69,7 +69,7 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int { return 1 } - states, err := b.States() + states, err := b.Workspaces() if err != nil { c.Ui.Error(err.Error()) return 1 @@ -94,7 +94,7 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int { } // we need the actual state to see if it's empty - sMgr, err := b.State(delEnv) + sMgr, err := b.StateMgr(delEnv) if err != nil { c.Ui.Error(err.Error()) return 1 @@ -134,7 +134,7 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int { // be delegated from the Backend to the State itself. stateLocker.Unlock(nil) - err = b.DeleteState(delEnv) + err = b.DeleteWorkspace(delEnv) if err != nil { c.Ui.Error(err.Error()) return 1 diff --git a/command/workspace_list.go b/command/workspace_list.go index 5ddd02090..080b654a1 100644 --- a/command/workspace_list.go +++ b/command/workspace_list.go @@ -53,7 +53,7 @@ func (c *WorkspaceListCommand) Run(args []string) int { return 1 } - states, err := b.States() + states, err := b.Workspaces() if err != nil { c.Ui.Error(err.Error()) return 1 diff --git a/command/workspace_new.go b/command/workspace_new.go index d5232342c..2b8cac099 100644 --- a/command/workspace_new.go +++ b/command/workspace_new.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/hashicorp/terraform/command/clistate" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/states/statefile" "github.com/hashicorp/terraform/tfdiags" "github.com/mitchellh/cli" "github.com/posener/complete" @@ -79,7 +79,7 @@ func (c *WorkspaceNewCommand) Run(args []string) int { return 1 } - states, err := b.States() + states, err := b.Workspaces() if err != nil { c.Ui.Error(fmt.Sprintf("Failed to get configured named states: %s", err)) return 1 @@ -91,7 +91,7 @@ func (c *WorkspaceNewCommand) Run(args []string) int { } } - _, err = b.State(newEnv) + _, err = b.StateMgr(newEnv) if err != nil { c.Ui.Error(err.Error()) return 1 @@ -112,7 +112,7 @@ func (c *WorkspaceNewCommand) Run(args []string) int { } // load the new Backend state - sMgr, err := b.State(newEnv) + sMgr, err := b.StateMgr(newEnv) if err != nil { c.Ui.Error(err.Error()) return 1 @@ -128,20 +128,20 @@ func (c *WorkspaceNewCommand) Run(args []string) int { } // read the existing state file - stateFile, err := os.Open(statePath) + f, err := os.Open(statePath) if err != nil { c.Ui.Error(err.Error()) return 1 } - s, err := terraform.ReadState(stateFile) + stateFile, err := statefile.Read(f) if err != nil { c.Ui.Error(err.Error()) return 1 } // save the existing state in the new Backend. - err = sMgr.WriteState(s) + err = sMgr.WriteState(stateFile.State) if err != nil { c.Ui.Error(err.Error()) return 1 diff --git a/command/workspace_select.go b/command/workspace_select.go index 1335d328c..432eea30a 100644 --- a/command/workspace_select.go +++ b/command/workspace_select.go @@ -75,7 +75,7 @@ func (c *WorkspaceSelectCommand) Run(args []string) int { return 1 } - states, err := b.States() + states, err := b.Workspaces() if err != nil { c.Ui.Error(err.Error()) return 1 diff --git a/configs/configload/loader.go b/configs/configload/loader.go index 23bf39fc5..a95f5ca68 100644 --- a/configs/configload/loader.go +++ b/configs/configload/loader.go @@ -2,6 +2,7 @@ package configload import ( "fmt" + "path/filepath" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/registry" @@ -91,3 +92,35 @@ func (l *Loader) Sources() map[string][]byte { func (l *Loader) IsConfigDir(path string) bool { return l.parser.IsConfigDir(path) } + +// ImportSources writes into the receiver's source code the given source +// code buffers. +// +// This is useful in the situation where an ancillary loader is created for +// some reason (e.g. loading config from a plan file) but the cached source +// code from that loader must be imported into the "main" loader in order +// to return source code snapshots in diagnostic messages. +// +// loader.ImportSources(otherLoader.Sources()) +func (l *Loader) ImportSources(sources map[string][]byte) { + p := l.Parser() + for name, src := range sources { + p.ForceFileSource(name, src) + } +} + +// ImportSourcesFromSnapshot writes into the receiver's source code the +// source files from the given snapshot. +// +// This is similar to ImportSources but knows how to unpack and flatten a +// snapshot data structure to get the corresponding flat source file map. +func (l *Loader) ImportSourcesFromSnapshot(snap *Snapshot) { + p := l.Parser() + for _, m := range snap.Modules { + baseDir := m.Dir + for fn, src := range m.Files { + fullPath := filepath.Join(baseDir, fn) + p.ForceFileSource(fullPath, src) + } + } +} diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 0d208c099..517b45bbb 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -14,16 +14,16 @@ import ( "syscall" "testing" - "github.com/hashicorp/terraform/addrs" - - "github.com/hashicorp/terraform/configs/configload" - "github.com/davecgh/go-spew/spew" "github.com/hashicorp/errwrap" "github.com/hashicorp/go-multierror" "github.com/hashicorp/logutils" + + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/configs/configload" "github.com/hashicorp/terraform/helper/logging" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/terraform" ) @@ -674,17 +674,24 @@ func testIDOnlyRefresh(c TestCase, opts terraform.ContextOpts, step TestStep, r return nil } - name := fmt.Sprintf("%s.foo", r.Type) + addr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: r.Type, + Name: "foo", + }.Instance(addrs.NoKey) + absAddr := addr.Absolute(addrs.RootModuleInstance) // Build the state. The state is just the resource with an ID. There // are no attributes. We only set what is needed to perform a refresh. - state := terraform.NewState() - state.RootModule().Resources[name] = &terraform.ResourceState{ - Type: r.Type, - Primary: &terraform.InstanceState{ - ID: r.Primary.ID, + state := states.NewState() + state.RootModule().SetResourceInstanceCurrent( + addr, + &states.ResourceInstanceObjectSrc{ + AttrsFlat: r.Primary.Attributes, + Status: states.ObjectReady, }, - } + addrs.ProviderConfig{Type: "placeholder"}.Absolute(addrs.RootModuleInstance), + ) // Create the config module. We use the full config because Refresh // doesn't have access to it and we may need things like provider @@ -717,14 +724,14 @@ func testIDOnlyRefresh(c TestCase, opts terraform.ContextOpts, step TestStep, r } // Verify attribute equivalence. - actualR := state.RootModule().Resources[name] + actualR := state.ResourceInstance(absAddr) if actualR == nil { return fmt.Errorf("Resource gone!") } - if actualR.Primary == nil { + if actualR.Current == nil { return fmt.Errorf("Resource has no primary instance") } - actual := actualR.Primary.Attributes + actual := actualR.Current.AttrsFlat expected := r.Primary.Attributes // Remove fields we're ignoring for _, v := range c.IDRefreshIgnore { diff --git a/helper/resource/testing_config.go b/helper/resource/testing_config.go index a9f1a90cc..fcaf1944f 100644 --- a/helper/resource/testing_config.go +++ b/helper/resource/testing_config.go @@ -3,12 +3,7 @@ package resource import ( "errors" "fmt" - "log" - "strings" - "github.com/hashicorp/terraform/tfdiags" - - "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/terraform" ) @@ -20,149 +15,149 @@ func testStepConfig( return testStep(opts, state, step) } -func testStep( - opts terraform.ContextOpts, - state *terraform.State, - step TestStep) (*terraform.State, error) { - // Pre-taint any resources that have been defined in Taint, as long as this - // is not a destroy step. - if !step.Destroy { - if err := testStepTaint(state, step); err != nil { +func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep) (*terraform.State, error) { + return nil, fmt.Errorf("testStep not yet updated for new state type") + /* + // Pre-taint any resources that have been defined in Taint, as long as this + // is not a destroy step. + if !step.Destroy { + if err := testStepTaint(state, step); err != nil { + return state, err + } + } + + cfg, err := testConfig(opts, step) + if err != nil { return state, err } - } - cfg, err := testConfig(opts, step) - if err != nil { - return state, err - } + var stepDiags tfdiags.Diagnostics - var stepDiags tfdiags.Diagnostics - - // Build the context - opts.Config = cfg - opts.State = state - opts.Destroy = step.Destroy - ctx, stepDiags := terraform.NewContext(&opts) - if stepDiags.HasErrors() { - return state, fmt.Errorf("Error initializing context: %s", stepDiags.Err()) - } - if stepDiags := ctx.Validate(); len(stepDiags) > 0 { + // Build the context + opts.Config = cfg + opts.State = state + opts.Destroy = step.Destroy + ctx, stepDiags := terraform.NewContext(&opts) if stepDiags.HasErrors() { - return nil, errwrap.Wrapf("config is invalid: {{err}}", stepDiags.Err()) + return state, fmt.Errorf("Error initializing context: %s", stepDiags.Err()) } - - log.Printf("[WARN] Config warnings:\n%s", stepDiags) - } - - // Refresh! - state, stepDiags = ctx.Refresh() - if stepDiags.HasErrors() { - return state, fmt.Errorf("Error refreshing: %s", stepDiags.Err()) - } - - // If this step is a PlanOnly step, skip over this first Plan and subsequent - // Apply, and use the follow up Plan that checks for perpetual diffs - if !step.PlanOnly { - // Plan! - if p, stepDiags := ctx.Plan(); stepDiags.HasErrors() { - return state, fmt.Errorf("Error planning: %s", stepDiags.Err()) - } else { - log.Printf("[WARN] Test: Step plan: %s", p) - } - - // We need to keep a copy of the state prior to destroying - // such that destroy steps can verify their behaviour in the check - // function - stateBeforeApplication := state.DeepCopy() - - // Apply the diff, creating real resources. - state, stepDiags = ctx.Apply() - if stepDiags.HasErrors() { - return state, fmt.Errorf("Error applying: %s", stepDiags.Err()) - } - - // Run any configured checks - if step.Check != nil { - if step.Destroy { - if err := step.Check(stateBeforeApplication); err != nil { - return state, fmt.Errorf("Check failed: %s", err) - } - } else { - if err := step.Check(state); err != nil { - return state, fmt.Errorf("Check failed: %s", err) - } + if stepDiags := ctx.Validate(); len(stepDiags) > 0 { + if stepDiags.HasErrors() { + return nil, errwrap.Wrapf("config is invalid: {{err}}", stepDiags.Err()) } - } - } - // Now, verify that Plan is now empty and we don't have a perpetual diff issue - // We do this with TWO plans. One without a refresh. - var p *terraform.Plan - if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() { - return state, fmt.Errorf("Error on follow-up plan: %s", stepDiags.Err()) - } - if p.Diff != nil && !p.Diff.Empty() { - if step.ExpectNonEmptyPlan { - log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p) - } else { - return state, fmt.Errorf( - "After applying this step, the plan was not empty:\n\n%s", p) + log.Printf("[WARN] Config warnings:\n%s", stepDiags) } - } - // And another after a Refresh. - if !step.Destroy || (step.Destroy && !step.PreventPostDestroyRefresh) { + // Refresh! state, stepDiags = ctx.Refresh() if stepDiags.HasErrors() { - return state, fmt.Errorf("Error on follow-up refresh: %s", stepDiags.Err()) + return state, fmt.Errorf("Error refreshing: %s", stepDiags.Err()) } - } - if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() { - return state, fmt.Errorf("Error on second follow-up plan: %s", stepDiags.Err()) - } - empty := p.Diff == nil || p.Diff.Empty() - // Data resources are tricky because they legitimately get instantiated - // during refresh so that they will be already populated during the - // plan walk. Because of this, if we have any data resources in the - // config we'll end up wanting to destroy them again here. This is - // acceptable and expected, and we'll treat it as "empty" for the - // sake of this testing. - if step.Destroy { - empty = true + // If this step is a PlanOnly step, skip over this first Plan and subsequent + // Apply, and use the follow up Plan that checks for perpetual diffs + if !step.PlanOnly { + // Plan! + if p, stepDiags := ctx.Plan(); stepDiags.HasErrors() { + return state, fmt.Errorf("Error planning: %s", stepDiags.Err()) + } else { + log.Printf("[WARN] Test: Step plan: %s", p) + } - for _, moduleDiff := range p.Diff.Modules { - for k, instanceDiff := range moduleDiff.Resources { - if !strings.HasPrefix(k, "data.") { - empty = false - break - } + // We need to keep a copy of the state prior to destroying + // such that destroy steps can verify their behaviour in the check + // function + stateBeforeApplication := state.DeepCopy() - if !instanceDiff.Destroy { - empty = false + // Apply the diff, creating real resources. + state, stepDiags = ctx.Apply() + if stepDiags.HasErrors() { + return state, fmt.Errorf("Error applying: %s", stepDiags.Err()) + } + + // Run any configured checks + if step.Check != nil { + if step.Destroy { + if err := step.Check(stateBeforeApplication); err != nil { + return state, fmt.Errorf("Check failed: %s", err) + } + } else { + if err := step.Check(state); err != nil { + return state, fmt.Errorf("Check failed: %s", err) + } } } } - } - if !empty { - if step.ExpectNonEmptyPlan { - log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p) - } else { - return state, fmt.Errorf( - "After applying this step and refreshing, "+ - "the plan was not empty:\n\n%s", p) + // Now, verify that Plan is now empty and we don't have a perpetual diff issue + // We do this with TWO plans. One without a refresh. + var p *terraform.Plan + if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() { + return state, fmt.Errorf("Error on follow-up plan: %s", stepDiags.Err()) + } + if p.Diff != nil && !p.Diff.Empty() { + if step.ExpectNonEmptyPlan { + log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p) + } else { + return state, fmt.Errorf( + "After applying this step, the plan was not empty:\n\n%s", p) + } } - } - // Made it here, but expected a non-empty plan, fail! - if step.ExpectNonEmptyPlan && (p.Diff == nil || p.Diff.Empty()) { - return state, fmt.Errorf("Expected a non-empty plan, but got an empty plan!") - } + // And another after a Refresh. + if !step.Destroy || (step.Destroy && !step.PreventPostDestroyRefresh) { + state, stepDiags = ctx.Refresh() + if stepDiags.HasErrors() { + return state, fmt.Errorf("Error on follow-up refresh: %s", stepDiags.Err()) + } + } + if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() { + return state, fmt.Errorf("Error on second follow-up plan: %s", stepDiags.Err()) + } + empty := p.Diff == nil || p.Diff.Empty() - // Made it here? Good job test step! - return state, nil + // Data resources are tricky because they legitimately get instantiated + // during refresh so that they will be already populated during the + // plan walk. Because of this, if we have any data resources in the + // config we'll end up wanting to destroy them again here. This is + // acceptable and expected, and we'll treat it as "empty" for the + // sake of this testing. + if step.Destroy { + empty = true + + for _, moduleDiff := range p.Diff.Modules { + for k, instanceDiff := range moduleDiff.Resources { + if !strings.HasPrefix(k, "data.") { + empty = false + break + } + + if !instanceDiff.Destroy { + empty = false + } + } + } + } + + if !empty { + if step.ExpectNonEmptyPlan { + log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p) + } else { + return state, fmt.Errorf( + "After applying this step and refreshing, "+ + "the plan was not empty:\n\n%s", p) + } + } + + // Made it here, but expected a non-empty plan, fail! + if step.ExpectNonEmptyPlan && (p.Diff == nil || p.Diff.Empty()) { + return state, fmt.Errorf("Expected a non-empty plan, but got an empty plan!") + } + + // Made it here? Good job test step! + return state, nil + */ } func testStepTaint(state *terraform.State, step TestStep) error { diff --git a/helper/resource/testing_import_state.go b/helper/resource/testing_import_state.go index 0739af114..5536dd089 100644 --- a/helper/resource/testing_import_state.go +++ b/helper/resource/testing_import_state.go @@ -3,15 +3,12 @@ package resource import ( "fmt" "log" - "reflect" - "strings" - - "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl/hclsyntax" - "github.com/davecgh/go-spew/spew" + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/terraform" ) @@ -52,7 +49,7 @@ func testStepImportState( } opts.Config = cfg - opts.State = terraform.NewState() + opts.State = states.NewState() ctx, stepDiags := terraform.NewContext(&opts) if stepDiags.HasErrors() { return state, stepDiags.Err() @@ -89,77 +86,84 @@ func testStepImportState( // Go through the new state and verify if step.ImportStateCheck != nil { - var states []*terraform.InstanceState + var states []*states.ResourceInstanceObjectSrc for _, r := range newState.RootModule().Resources { - if r.Primary != nil { - states = append(states, r.Primary) + for _, i := range r.Instances { + if i.Current != nil { + states = append(states, i.Current) + } } } - if err := step.ImportStateCheck(states); err != nil { + // TODO: update for new state types + return nil, fmt.Errorf("ImportStateCheck call in testStepImportState not yet updated for new state types") + /*if err := step.ImportStateCheck(states); err != nil { return state, err - } + }*/ } // Verify that all the states match if step.ImportStateVerify { - new := newState.RootModule().Resources - old := state.RootModule().Resources - for _, r := range new { - // Find the existing resource - var oldR *terraform.ResourceState - for _, r2 := range old { - if r2.Primary != nil && r2.Primary.ID == r.Primary.ID && r2.Type == r.Type { - oldR = r2 - break - } - } - if oldR == nil { - return state, fmt.Errorf( - "Failed state verification, resource with ID %s not found", - r.Primary.ID) - } - - // Compare their attributes - actual := make(map[string]string) - for k, v := range r.Primary.Attributes { - actual[k] = v - } - expected := make(map[string]string) - for k, v := range oldR.Primary.Attributes { - expected[k] = v - } - - // Remove fields we're ignoring - for _, v := range step.ImportStateVerifyIgnore { - for k, _ := range actual { - if strings.HasPrefix(k, v) { - delete(actual, k) + return nil, fmt.Errorf("testStepImportStep ImportStateVerify not yet updated for new state types") + /* + new := newState.RootModule().Resources + old := state.RootModule().Resources + for _, r := range new { + // Find the existing resource + var oldR *terraform.ResourceState + for _, r2 := range old { + if r2.Primary != nil && r2.Primary.ID == r.Primary.ID && r2.Type == r.Type { + oldR = r2 + break } } - for k, _ := range expected { - if strings.HasPrefix(k, v) { - delete(expected, k) - } + if oldR == nil { + return state, fmt.Errorf( + "Failed state verification, resource with ID %s not found", + r.Primary.ID) } - } - if !reflect.DeepEqual(actual, expected) { - // Determine only the different attributes - for k, v := range expected { - if av, ok := actual[k]; ok && v == av { - delete(expected, k) - delete(actual, k) + // Compare their attributes + actual := make(map[string]string) + for k, v := range r.Primary.Attributes { + actual[k] = v + } + expected := make(map[string]string) + for k, v := range oldR.Primary.Attributes { + expected[k] = v + } + + // Remove fields we're ignoring + for _, v := range step.ImportStateVerifyIgnore { + for k, _ := range actual { + if strings.HasPrefix(k, v) { + delete(actual, k) + } + } + for k, _ := range expected { + if strings.HasPrefix(k, v) { + delete(expected, k) + } } } - spewConf := spew.NewDefaultConfig() - spewConf.SortKeys = true - return state, fmt.Errorf( - "ImportStateVerify attributes not equivalent. Difference is shown below. Top is actual, bottom is expected."+ - "\n\n%s\n\n%s", - spewConf.Sdump(actual), spewConf.Sdump(expected)) + if !reflect.DeepEqual(actual, expected) { + // Determine only the different attributes + for k, v := range expected { + if av, ok := actual[k]; ok && v == av { + delete(expected, k) + delete(actual, k) + } + } + + spewConf := spew.NewDefaultConfig() + spewConf.SortKeys = true + return state, fmt.Errorf( + "ImportStateVerify attributes not equivalent. Difference is shown below. Top is actual, bottom is expected."+ + "\n\n%s\n\n%s", + spewConf.Sdump(actual), spewConf.Sdump(expected)) + } } - } + */ } // Return the old state (non-imported) so we don't change anything. diff --git a/main.go b/main.go index 7b88b2925..2333bbc33 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,6 @@ import ( "github.com/hashicorp/terraform/command/format" "github.com/hashicorp/terraform/helper/logging" "github.com/hashicorp/terraform/svchost/disco" - "github.com/hashicorp/terraform/terraform" "github.com/mattn/go-colorable" "github.com/mattn/go-shellwords" "github.com/mitchellh/cli" @@ -113,9 +112,6 @@ func init() { func wrappedMain() int { var err error - // We always need to close the DebugInfo before we exit. - defer terraform.CloseDebugInfo() - log.SetOutput(os.Stderr) log.Printf( "[INFO] Terraform version: %s %s %s", diff --git a/plans/changes.go b/plans/changes.go index 10499b6e9..21d3f34ef 100644 --- a/plans/changes.go +++ b/plans/changes.go @@ -23,6 +23,38 @@ func NewChanges() *Changes { } } +func (c *Changes) Empty() bool { + return (len(c.Resources) + len(c.RootOutputs)) == 0 +} + +// ResourceInstance returns the planned change for the current object of the +// resource instance of the given address, if any. Returns nil if no change is +// planned. +func (c *Changes) ResourceInstance(addr addrs.AbsResourceInstance) *ResourceInstanceChangeSrc { + addrStr := addr.String() + for _, rc := range c.Resources { + if rc.Addr.String() == addrStr && rc.DeposedKey == states.NotDeposed { + return rc + } + } + + return nil +} + +// ResourceInstanceDeposed returns the plan change of a deposed object of +// the resource instance of the given address, if any. Returns nil if no change +// is planned. +func (c *Changes) ResourceInstanceDeposed(addr addrs.AbsResourceInstance, key states.DeposedKey) *ResourceInstanceChangeSrc { + addrStr := addr.String() + for _, rc := range c.Resources { + if rc.Addr.String() == addrStr && rc.DeposedKey == key { + return rc + } + } + + return nil +} + // ResourceInstanceChange describes a change to a particular resource instance // object. type ResourceInstanceChange struct { diff --git a/plans/changes_sync.go b/plans/changes_sync.go new file mode 100644 index 000000000..eec8756e7 --- /dev/null +++ b/plans/changes_sync.go @@ -0,0 +1,18 @@ +package plans + +import ( + "sync" +) + +// ChangesSync is a wrapper around a Changes that provides a concurrency-safe +// interface to insert new changes and retrieve copies of existing changes. +// +// Each ChangesSync is independent of all others, so all concurrent writers +// to a particular Changes must share a single ChangesSync. Behavior is +// undefined if any other caller makes changes to the underlying Changes +// object or its nested objects concurrently with any of the methods of a +// particular ChangesSync. +type ChangesSync struct { + lock sync.Mutex + changes *Changes +} diff --git a/plans/dynamic_value.go b/plans/dynamic_value.go index 0e1d82c01..fadf24312 100644 --- a/plans/dynamic_value.go +++ b/plans/dynamic_value.go @@ -69,3 +69,17 @@ func (v DynamicValue) Decode(ty cty.Type) (cty.Value, error) { return ctymsgpack.Unmarshal([]byte(v), ty) } + +// ImpliedType returns the type implied by the serialized structure of the +// receiving value. +// +// This will not necessarily be exactly the type that was given when the +// value was encoded, and in particular must not be used for values that +// were encoded with their static type given as cty.DynamicPseudoType. +// It is however safe to use this method for values that were encoded using +// their runtime type as the conforming type, with the result being +// semantically equivalent but with all lists and sets represented as tuples, +// and maps as objects, due to ambiguities of the serialization. +func (v DynamicValue) ImpliedType() (cty.Type, error) { + return ctymsgpack.ImpliedType([]byte(v)) +} diff --git a/plans/plan.go b/plans/plan.go index b28820e53..5a3e4548e 100644 --- a/plans/plan.go +++ b/plans/plan.go @@ -4,6 +4,8 @@ import ( "sort" "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/configs/configschema" + "github.com/zclconf/go-cty/cty" ) // Plan is the top-level type representing a planned set of changes. @@ -44,6 +46,19 @@ type Backend struct { Workspace string } +func NewBackend(typeName string, config cty.Value, configSchema *configschema.Block, workspaceName string) (*Backend, error) { + dv, err := NewDynamicValue(config, configSchema.ImpliedType()) + if err != nil { + return nil, err + } + + return &Backend{ + Type: typeName, + Config: dv, + Workspace: workspaceName, + }, nil +} + // ProviderAddrs returns a list of all of the provider configuration addresses // referenced throughout the receiving plan. // diff --git a/plans/plan_test.go b/plans/plan_test.go index c65a41524..d6c2f769a 100644 --- a/plans/plan_test.go +++ b/plans/plan_test.go @@ -13,8 +13,8 @@ func TestProviderAddrs(t *testing.T) { plan := &Plan{ VariableValues: map[string]DynamicValue{}, Changes: &Changes{ - RootOutputs: map[string]*OutputChange{}, - Resources: []*ResourceInstanceChange{ + RootOutputs: map[string]*OutputChangeSrc{}, + Resources: []*ResourceInstanceChangeSrc{ { Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, diff --git a/plans/planfile/planfile_test.go b/plans/planfile/planfile_test.go index c757d5913..7624b3b0e 100644 --- a/plans/planfile/planfile_test.go +++ b/plans/planfile/planfile_test.go @@ -7,12 +7,10 @@ import ( "testing" "github.com/davecgh/go-spew/spew" - - "github.com/hashicorp/terraform/plans" - version "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/configs/configload" + "github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states/statefile" ) @@ -45,8 +43,8 @@ func TestRoundtrip(t *testing.T) { // file is tested more fully in tfplan_test.go . planIn := &plans.Plan{ Changes: &plans.Changes{ - Resources: []*plans.ResourceInstanceChange{}, - RootOutputs: map[string]*plans.OutputChange{}, + Resources: []*plans.ResourceInstanceChangeSrc{}, + RootOutputs: map[string]*plans.OutputChangeSrc{}, }, ProviderSHA256s: map[string][]byte{}, VariableValues: map[string]plans.DynamicValue{ diff --git a/plans/planfile/tfplan_test.go b/plans/planfile/tfplan_test.go index 55d5dbb9f..7ab7377f0 100644 --- a/plans/planfile/tfplan_test.go +++ b/plans/planfile/tfplan_test.go @@ -21,16 +21,16 @@ func TestTFPlanRoundTrip(t *testing.T) { "foo": mustNewDynamicValueStr("foo value"), }, Changes: &plans.Changes{ - RootOutputs: map[string]*plans.OutputChange{ + RootOutputs: map[string]*plans.OutputChangeSrc{ "bar": { - Change: plans.Change{ + ChangeSrc: plans.ChangeSrc{ Action: plans.Create, After: mustNewDynamicValueStr("bar value"), }, Sensitive: false, }, "baz": { - Change: plans.Change{ + ChangeSrc: plans.ChangeSrc{ Action: plans.NoOp, Before: mustNewDynamicValueStr("baz value"), After: mustNewDynamicValueStr("baz value"), @@ -38,7 +38,7 @@ func TestTFPlanRoundTrip(t *testing.T) { Sensitive: false, }, "secret": { - Change: plans.Change{ + ChangeSrc: plans.ChangeSrc{ Action: plans.Update, Before: mustNewDynamicValueStr("old secret value"), After: mustNewDynamicValueStr("new secret value"), @@ -46,7 +46,7 @@ func TestTFPlanRoundTrip(t *testing.T) { Sensitive: true, }, }, - Resources: []*plans.ResourceInstanceChange{ + Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, @@ -56,7 +56,7 @@ func TestTFPlanRoundTrip(t *testing.T) { ProviderAddr: addrs.ProviderConfig{ Type: "test", }.Absolute(addrs.RootModuleInstance), - Change: plans.Change{ + ChangeSrc: plans.ChangeSrc{ Action: plans.Replace, Before: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("foo-bar-baz"), @@ -76,7 +76,7 @@ func TestTFPlanRoundTrip(t *testing.T) { ProviderAddr: addrs.ProviderConfig{ Type: "test", }.Absolute(addrs.RootModuleInstance), - Change: plans.Change{ + ChangeSrc: plans.ChangeSrc{ Action: plans.Delete, Before: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("bar-baz-foo"), diff --git a/providers/provider.go b/providers/provider.go index 3ee1d3119..c4044b4be 100644 --- a/providers/provider.go +++ b/providers/provider.go @@ -81,7 +81,7 @@ type GetSchemaResponse struct { // Schema pairs a provider or resource schema with that schema's version. // This is used to be able to upgrade the schema in UpgradeResourceState. type Schema struct { - Version int + Version uint64 Block *configschema.Block } diff --git a/state/backup.go b/state/backup.go index 047258f4d..0cac3b6cf 100644 --- a/state/backup.go +++ b/state/backup.go @@ -3,7 +3,8 @@ package state import ( "sync" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statemgr" ) // BackupState wraps a State that backs up the state on the first time that @@ -18,7 +19,7 @@ type BackupState struct { done bool } -func (s *BackupState) State() *terraform.State { +func (s *BackupState) State() *states.State { return s.Real.State() } @@ -26,7 +27,7 @@ func (s *BackupState) RefreshState() error { return s.Real.RefreshState() } -func (s *BackupState) WriteState(state *terraform.State) error { +func (s *BackupState) WriteState(state *states.State) error { s.mu.Lock() defer s.mu.Unlock() @@ -74,7 +75,7 @@ func (s *BackupState) backup() error { // purposes, but we don't need a backup or lock if the state is empty, so // skip this with a nil state. if state != nil { - ls := &LocalState{Path: s.Path} + ls := statemgr.NewFilesystem(s.Path) if err := ls.WriteState(state); err != nil { return err } diff --git a/state/lock.go b/state/lock.go index b3a03b3ef..4839df2a7 100644 --- a/state/lock.go +++ b/state/lock.go @@ -1,7 +1,7 @@ package state import ( - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/states" ) // LockDisabled implements State and Locker but disables state locking. @@ -13,11 +13,11 @@ type LockDisabled struct { Inner State } -func (s *LockDisabled) State() *terraform.State { +func (s *LockDisabled) State() *states.State { return s.Inner.State() } -func (s *LockDisabled) WriteState(v *terraform.State) error { +func (s *LockDisabled) WriteState(v *states.State) error { return s.Inner.WriteState(v) } diff --git a/state/remote/state.go b/state/remote/state.go index 575e4d187..12faeeb99 100644 --- a/state/remote/state.go +++ b/state/remote/state.go @@ -2,11 +2,15 @@ package remote import ( "bytes" - "log" + "fmt" "sync" + uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/terraform/state" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statefile" + "github.com/hashicorp/terraform/states/statemgr" ) // State implements the State interfaces in the state package to handle @@ -18,51 +22,36 @@ type State struct { Client Client - state, readState *terraform.State + lineage string + serial uint64 + state, readState *states.State + disableLocks bool } -// StateReader impl. -func (s *State) State() *terraform.State { +var _ statemgr.Full = (*State)(nil) + +// statemgr.Reader impl. +func (s *State) State() *states.State { s.mu.Lock() defer s.mu.Unlock() return s.state.DeepCopy() } -// StateWriter impl. -func (s *State) WriteState(state *terraform.State) error { +// statemgr.Writer impl. +func (s *State) WriteState(state *states.State) error { s.mu.Lock() defer s.mu.Unlock() - if s.readState != nil && !state.SameLineage(s.readState) { - // This can't error here, because we need to be able to overwrite the - // state in some cases, like `state push -force` or `workspace new - // -state=` - log.Printf("[WARN] incompatible state lineage; given %s but want %s", state.Lineage, s.readState.Lineage) - } - // We create a deep copy of the state here, because the caller also has // a reference to the given object and can potentially go on to mutate // it after we return, but we want the snapshot at this point in time. s.state = state.DeepCopy() - // Force our new state to have the same serial as our read state. We'll - // update this if PersistState is called later. (We don't require nor trust - // the caller to properly maintain serial for transient state objects since - // the rest of Terraform treats state as an openly mutable object.) - // - // If we have no read state then we assume we're either writing a new - // state for the first time or we're migrating a state from elsewhere, - // and in both cases we wish to retain the lineage and serial from - // the given state. - if s.readState != nil { - s.state.Serial = s.readState.Serial - } - return nil } -// StateRefresher impl. +// statemgr.Refresher impl. func (s *State) RefreshState() error { s.mu.Lock() defer s.mu.Unlock() @@ -74,38 +63,61 @@ func (s *State) RefreshState() error { // no remote state is OK if payload == nil { + s.readState = nil + s.state = nil + s.lineage = "" + s.serial = 0 return nil } - state, err := terraform.ReadState(bytes.NewReader(payload.Data)) + stateFile, err := statefile.Read(bytes.NewReader(payload.Data)) if err != nil { return err } - s.state = state + s.lineage = stateFile.Lineage + s.serial = stateFile.Serial + s.state = stateFile.State s.readState = s.state.DeepCopy() // our states must be separate instances so we can track changes return nil } -// StatePersister impl. +// statemgr.Persister impl. func (s *State) PersistState() error { s.mu.Lock() defer s.mu.Unlock() - if !s.state.MarshalEqual(s.readState) { - // Our new state does not marshal as byte-for-byte identical to - // the old, so we need to increment the serial. - // Note that in WriteState we force the serial to match that of - // s.readState, if we have a readState. - s.state.Serial++ + if s.readState != nil { + if !statefile.StatesMarshalEqual(s.state, s.readState) { + s.serial++ + } + } else { + // We might be writing a new state altogether, but before we do that + // we'll check to make sure there isn't already a snapshot present + // that we ought to be updating. + err := s.RefreshState() + if err != nil { + return fmt.Errorf("failed checking for existing remote state: %s", err) + } + if s.lineage == "" { // indicates that no state snapshot is present yet + lineage, err := uuid.GenerateUUID() + if err != nil { + return fmt.Errorf("failed to generate initial lineage: %v", err) + } + s.lineage = lineage + s.serial = 0 + } } + f := statefile.New(s.state, s.lineage, s.serial) + var buf bytes.Buffer - if err := terraform.WriteState(s.state, &buf); err != nil { + err := statefile.Write(f, &buf) + if err != nil { return err } - err := s.Client.Put(buf.Bytes()) + err = s.Client.Put(buf.Bytes()) if err != nil { return err } @@ -121,6 +133,10 @@ func (s *State) Lock(info *state.LockInfo) (string, error) { s.mu.Lock() defer s.mu.Unlock() + if s.disableLocks { + return "", nil + } + if c, ok := s.Client.(ClientLocker); ok { return c.Lock(info) } @@ -132,8 +148,19 @@ func (s *State) Unlock(id string) error { s.mu.Lock() defer s.mu.Unlock() + if s.disableLocks { + return nil + } + if c, ok := s.Client.(ClientLocker); ok { return c.Unlock(id) } return nil } + +// DisableLocks turns the Lock and Unlock methods into no-ops. This is intended +// to be called during initialization of a state manager and should not be +// called after any of the statemgr.Full interface methods have been called. +func (s *State) DisableLocks() { + s.disableLocks = true +} diff --git a/state/remote/testing.go b/state/remote/testing.go index bad22445e..002f50387 100644 --- a/state/remote/testing.go +++ b/state/remote/testing.go @@ -5,14 +5,16 @@ import ( "testing" "github.com/hashicorp/terraform/state" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/states/statefile" ) // TestClient is a generic function to test any client. func TestClient(t *testing.T, c Client) { var buf bytes.Buffer s := state.TestStateInitial() - if err := terraform.WriteState(s, &buf); err != nil { + sf := statefile.New(s, "stub-lineage", 2) + err := statefile.Write(sf, &buf) + if err != nil { t.Fatalf("err: %s", err) } data := buf.Bytes() diff --git a/state/state.go b/state/state.go index d9708d1b4..04707bccb 100644 --- a/state/state.go +++ b/state/state.go @@ -1,20 +1,16 @@ package state import ( - "bytes" "context" - "encoding/json" - "errors" "fmt" "math/rand" "os" "os/user" - "strings" - "text/template" "time" uuid "github.com/hashicorp/go-uuid" - "github.com/hashicorp/terraform/terraform" + + "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/version" ) @@ -24,74 +20,23 @@ func init() { rngSource = rand.New(rand.NewSource(time.Now().UnixNano())) } -// State is the collection of all state interfaces. -type State interface { - StateReader - StateWriter - StateRefresher - StatePersister - Locker -} +// State is a deprecated alias for statemgr.Full +type State = statemgr.Full -// StateReader is the interface for things that can return a state. Retrieving -// the state here must not error. Loading the state fresh (an operation that -// can likely error) should be implemented by RefreshState. If a state hasn't -// been loaded yet, it is okay for State to return nil. -// -// Each caller of this function must get a distinct copy of the state, and -// it must also be distinct from any instance cached inside the reader, to -// ensure that mutations of the returned state will not affect the values -// returned to other callers. -type StateReader interface { - State() *terraform.State -} +// StateReader is a deprecated alias for statemgr.Reader +type StateReader = statemgr.Reader -// StateWriter is the interface that must be implemented by something that -// can write a state. Writing the state can be cached or in-memory, as -// full persistence should be implemented by StatePersister. -// -// Implementors that cache the state in memory _must_ take a copy of it -// before returning, since the caller may continue to modify it once -// control returns. The caller must ensure that the state instance is not -// concurrently modified _during_ the call, or behavior is undefined. -// -// If an object implements StatePersister in conjunction with StateReader -// then these methods must coordinate such that a subsequent read returns -// a copy of the most recent write, even if it has not yet been persisted. -type StateWriter interface { - WriteState(*terraform.State) error -} +// StateWriter is a deprecated alias for statemgr.Writer +type StateWriter = statemgr.Writer -// StateRefresher is the interface that is implemented by something that -// can load a state. This might be refreshing it from a remote location or -// it might simply be reloading it from disk. -type StateRefresher interface { - RefreshState() error -} +// StateRefresher is a deprecated alias for statemgr.Refresher +type StateRefresher = statemgr.Refresher -// StatePersister is implemented to truly persist a state. Whereas StateWriter -// is allowed to perhaps be caching in memory, PersistState must write the -// state to some durable storage. -// -// If an object implements StatePersister in conjunction with StateReader -// and/or StateRefresher then these methods must coordinate such that -// subsequent reads after a persist return an updated value. -type StatePersister interface { - PersistState() error -} +// StatePersister is a deprecated alias for statemgr.Persister +type StatePersister = statemgr.Persister -// Locker is implemented to lock state during command execution. -// The info parameter can be recorded with the lock, but the -// implementation should not depend in its value. The string returned by Lock -// is an ID corresponding to the lock acquired, and must be passed to Unlock to -// ensure that the correct lock is being released. -// -// Lock and Unlock may return an error value of type LockError which in turn -// can contain the LockInfo of a conflicting lock. -type Locker interface { - Lock(info *LockInfo) (string, error) - Unlock(id string) error -} +// Locker is a deprecated alias for statemgr.Locker +type Locker = statemgr.Locker // test hook to verify that LockWithContext has attempted a lock var postLockHook func() @@ -165,78 +110,8 @@ func NewLockInfo() *LockInfo { return info } -// LockInfo stores lock metadata. -// -// Only Operation and Info are required to be set by the caller of Lock. -type LockInfo struct { - // Unique ID for the lock. NewLockInfo provides a random ID, but this may - // be overridden by the lock implementation. The final value if ID will be - // returned by the call to Lock. - ID string +// LockInfo is a deprecated lias for statemgr.LockInfo +type LockInfo = statemgr.LockInfo - // Terraform operation, provided by the caller. - Operation string - // Extra information to store with the lock, provided by the caller. - Info string - - // user@hostname when available - Who string - // Terraform version - Version string - // Time that the lock was taken. - Created time.Time - - // Path to the state file when applicable. Set by the Lock implementation. - Path string -} - -// Err returns the lock info formatted in an error -func (l *LockInfo) Err() error { - return errors.New(l.String()) -} - -// Marshal returns a string json representation of the LockInfo -func (l *LockInfo) Marshal() []byte { - js, err := json.Marshal(l) - if err != nil { - panic(err) - } - return js -} - -// String return a multi-line string representation of LockInfo -func (l *LockInfo) String() string { - tmpl := `Lock Info: - ID: {{.ID}} - Path: {{.Path}} - Operation: {{.Operation}} - Who: {{.Who}} - Version: {{.Version}} - Created: {{.Created}} - Info: {{.Info}} -` - - t := template.Must(template.New("LockInfo").Parse(tmpl)) - var out bytes.Buffer - if err := t.Execute(&out, l); err != nil { - panic(err) - } - return out.String() -} - -type LockError struct { - Info *LockInfo - Err error -} - -func (e *LockError) Error() string { - var out []string - if e.Err != nil { - out = append(out, e.Err.Error()) - } - - if e.Info != nil { - out = append(out, e.Info.String()) - } - return strings.Join(out, "\n") -} +// LockError is a deprecated alias for statemgr.LockError +type LockError = statemgr.LockError diff --git a/state/testing.go b/state/testing.go index 3b92b33c9..c0c1fbbc4 100644 --- a/state/testing.go +++ b/state/testing.go @@ -1,10 +1,10 @@ package state import ( - "reflect" "testing" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statemgr" ) // TestState is a helper for testing state implementations. It is expected @@ -12,148 +12,11 @@ import ( // state. func TestState(t *testing.T, s State) { t.Helper() - - if err := s.RefreshState(); err != nil { - t.Fatalf("err: %s", err) - } - - // Check that the initial state is correct. - // These do have different Lineages, but we will replace current below. - initial := TestStateInitial() - if state := s.State(); !state.Equal(initial) { - t.Fatalf("state does not match expected initial state:\n%#v\n\n%#v", state, initial) - } - - // Now we've proven that the state we're starting with is an initial - // state, we'll complete our work here with that state, since otherwise - // further writes would violate the invariant that we only try to write - // states that share the same lineage as what was initially written. - current := s.State() - - // Write a new state and verify that we have it - current.AddModuleState(&terraform.ModuleState{ - Path: []string{"root"}, - Outputs: map[string]*terraform.OutputState{ - "bar": &terraform.OutputState{ - Type: "string", - Sensitive: false, - Value: "baz", - }, - }, - }) - - if err := s.WriteState(current); err != nil { - t.Fatalf("err: %s", err) - } - - if actual := s.State(); !actual.Equal(current) { - t.Fatalf("bad:\n%#v\n\n%#v", actual, current) - } - - // Test persistence - if err := s.PersistState(); err != nil { - t.Fatalf("err: %s", err) - } - - // Refresh if we got it - if err := s.RefreshState(); err != nil { - t.Fatalf("err: %s", err) - } - - if s.State().Lineage != current.Lineage { - t.Fatalf("Lineage changed from %s to %s", s.State().Lineage, current.Lineage) - } - - // Just set the serials the same... Then compare. - actual := s.State() - if !actual.Equal(current) { - t.Fatalf("bad: %#v\n\n%#v", actual, current) - } - - // Same serial - serial := s.State().Serial - if err := s.WriteState(current); err != nil { - t.Fatalf("err: %s", err) - } - if err := s.PersistState(); err != nil { - t.Fatalf("err: %s", err) - } - - if s.State().Serial != serial { - t.Fatalf("serial changed after persisting with no changes: got %d, want %d", s.State().Serial, serial) - } - - // Change the serial - current = current.DeepCopy() - current.Modules = []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root", "somewhere"}, - Outputs: map[string]*terraform.OutputState{ - "serialCheck": &terraform.OutputState{ - Type: "string", - Sensitive: false, - Value: "true", - }, - }, - }, - } - if err := s.WriteState(current); err != nil { - t.Fatalf("err: %s", err) - } - if err := s.PersistState(); err != nil { - t.Fatalf("err: %s", err) - } - - if s.State().Serial <= serial { - t.Fatalf("serial incorrect after persisting with changes: got %d, want > %d", s.State().Serial, serial) - } - - if s.State().Version != current.Version { - t.Fatalf("Version changed from %d to %d", s.State().Version, current.Version) - } - - if s.State().TFVersion != current.TFVersion { - t.Fatalf("TFVersion changed from %s to %s", s.State().TFVersion, current.TFVersion) - } - - // verify that Lineage doesn't change along with Serial, or during copying. - if s.State().Lineage != current.Lineage { - t.Fatalf("Lineage changed from %s to %s", s.State().Lineage, current.Lineage) - } - - // Check that State() returns a copy by modifying the copy and comparing - // to the current state. - stateCopy := s.State() - stateCopy.Serial++ - if reflect.DeepEqual(stateCopy, s.State()) { - t.Fatal("State() should return a copy") - } - - // our current expected state should also marhsal identically to the persisted state - if current.MarshalEqual(s.State()) { - t.Fatalf("Persisted state altered unexpectedly. Expected: %#v\b Got: %#v", current, s.State()) - } + statemgr.TestFull(t, s) } // TestStateInitial is the initial state that a State should have // for TestState. -func TestStateInitial() *terraform.State { - initial := &terraform.State{ - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root", "child"}, - Outputs: map[string]*terraform.OutputState{ - "foo": &terraform.OutputState{ - Type: "string", - Sensitive: false, - Value: "bar", - }, - }, - }, - }, - } - - initial.Init() - - return initial +func TestStateInitial() *states.State { + return statemgr.TestFullInitialState() } diff --git a/states/import.go b/states/import.go index 6a79fe599..29578d49c 100644 --- a/states/import.go +++ b/states/import.go @@ -20,3 +20,21 @@ type ImportedObject struct { // will be available for future operations. Private cty.Value } + +// AsInstanceObject converts the receiving ImportedObject into a +// ResourceInstanceObject that has status ObjectReady. +// +// The returned object does not know its own resource type, so the caller must +// retain the ResourceType value from the source object if this information is +// needed. +// +// The returned object also has no dependency addresses, but the caller may +// freely modify the direct fields of the returned object without affecting +// the receiver. +func (io *ImportedObject) AsInstanceObject() *ResourceInstanceObject { + return &ResourceInstanceObject{ + Status: ObjectReady, + Value: io.Value, + Private: io.Private, + } +} diff --git a/states/instance_object.go b/states/instance_object.go index 9eb567a51..4db804c2f 100644 --- a/states/instance_object.go +++ b/states/instance_object.go @@ -67,8 +67,8 @@ const ( // The returned object may share internal references with the receiver and // so the caller must not mutate the receiver any further once once this // method is called. -func (o *ResourceInstanceObject) Encode(val cty.Value, ty cty.Type, schemaVersion uint64) (*ResourceInstanceObjectSrc, error) { - src, err := ctyjson.Marshal(val, ty) +func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*ResourceInstanceObjectSrc, error) { + src, err := ctyjson.Marshal(o.Value, ty) if err != nil { return nil, err } diff --git a/states/module.go b/states/module.go index 3732542f0..b6727b75d 100644 --- a/states/module.go +++ b/states/module.go @@ -1,8 +1,6 @@ package states import ( - "fmt" - "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/addrs" @@ -125,14 +123,41 @@ func (ms *Module) SetResourceInstanceCurrent(addr addrs.ResourceInstance, obj *R // is overwritten. Set obj to nil to remove the deposed object altogether. If // the instance is left with no objects after this operation then it will // be removed from its containing resource altogether. -func (ms *Module) SetResourceInstanceDeposed(addr addrs.ResourceInstance, key DeposedKey, obj *ResourceInstanceObjectSrc) { +func (ms *Module) SetResourceInstanceDeposed(addr addrs.ResourceInstance, key DeposedKey, obj *ResourceInstanceObjectSrc, provider addrs.AbsProviderConfig) { + ms.SetResourceMeta(addr.Resource, eachModeForInstanceKey(addr.Key), provider) + + rs := ms.Resource(addr.Resource) + is := rs.EnsureInstance(addr.Key) + if obj != nil { + is.Deposed[key] = obj + } else { + delete(is.Deposed, key) + } + + if !is.HasObjects() { + // If we have no objects at all then we'll clean up. + delete(rs.Instances, addr.Key) + } + if rs.EachMode == NoEach && len(rs.Instances) == 0 { + // Also clean up if we only expect to have one instance anyway + // and there are none. We leave the resource behind if an each mode + // is active because an empty list or map of instances is a valid state. + delete(ms.Resources, addr.Resource.String()) + } +} + +// ForgetResourceInstanceDeposed removes the record of the deposed object with +// the given address and key, if present. If not present, this is a no-op. +func (ms *Module) ForgetResourceInstanceDeposed(addr addrs.ResourceInstance, key DeposedKey) { rs := ms.Resource(addr.Resource) if rs == nil { - panic(fmt.Sprintf("attempt to register deposed instance object for non-existent resource %s", addr.Resource.Absolute(ms.Addr))) + return } - is := rs.EnsureInstance(addr.Key) - - is.Current = obj + is := rs.Instance(addr.Key) + if is == nil { + return + } + delete(is.Deposed, key) if !is.HasObjects() { // If we have no objects at all then we'll clean up. diff --git a/states/resource.go b/states/resource.go index bacea10fd..2f0f48ac1 100644 --- a/states/resource.go +++ b/states/resource.go @@ -206,5 +206,16 @@ func (k DeposedKey) GoString() string { } } +// Generation is a helper method to convert a DeposedKey into a Generation. +// If the reciever is anything other than NotDeposed then the result is +// just the same value as a Generation. If the receiver is NotDeposed then +// the result is CurrentGen. +func (k DeposedKey) Generation() Generation { + if k == NotDeposed { + return CurrentGen + } + return k +} + // generation is an implementation of Generation. func (k DeposedKey) generation() {} diff --git a/states/resource_test.go b/states/resource_test.go index b512f8c49..0ce422002 100644 --- a/states/resource_test.go +++ b/states/resource_test.go @@ -5,9 +5,9 @@ import ( ) func TestResourceInstanceDeposeCurrentObject(t *testing.T) { - obj := &ResourceInstanceObject{ - // Empty for the sake of this test, because we're just going to - // compare by pointer below anyway. + obj := &ResourceInstanceObjectSrc{ + // Empty for the sake of this test, because we're just going to + // compare by pointer below anyway. } is := NewResourceInstance() diff --git a/states/state.go b/states/state.go index 22f4bcdc0..7a8d72987 100644 --- a/states/state.go +++ b/states/state.go @@ -34,6 +34,30 @@ func NewState() *State { } } +// BuildState is a helper -- primarily intended for tests -- to build a state +// using imperative code against the StateSync type while still acting as +// an expression of type *State to assign into a containing struct. +func BuildState(cb func(*SyncState)) *State { + s := NewState() + cb(s.SyncWrapper()) + return s +} + +// Empty returns true if there are no resources or populated output values +// in the receiver. In other words, if this state could be safely replaced +// with the return value of NewState and be functionally equivalent. +func (s *State) Empty() bool { + for _, ms := range s.Modules { + if len(ms.Resources) != 0 { + return false + } + if len(ms.OutputValues) != 0 { + return false + } + } + return true +} + // Module returns the state for the module with the given address, or nil if // the requested module is not tracked in the state. func (s *State) Module(addr addrs.ModuleInstance) *Module { @@ -76,6 +100,20 @@ func (s *State) EnsureModule(addr addrs.ModuleInstance) *Module { return ms } +// HasResources returns true if there is at least one resource (of any mode) +// present in the receiving state. +func (s *State) HasResources() bool { + if s == nil { + return false + } + for _, ms := range s.Modules { + if len(ms.Resources) > 0 { + return true + } + } + return false +} + // Resource returns the state for the resource with the given address, or nil // if no such resource is tracked in the state. func (s *State) Resource(addr addrs.AbsResource) *Resource { diff --git a/states/state_deepcopy.go b/states/state_deepcopy.go index dbbc03df6..d07bc16c2 100644 --- a/states/state_deepcopy.go +++ b/states/state_deepcopy.go @@ -141,13 +141,14 @@ func (obj *ResourceInstanceObjectSrc) DeepCopy() *ResourceInstanceObjectSrc { var attrsJSON []byte if obj.AttrsJSON != nil { - attrsJSON := make([]byte, len(obj.AttrsJSON)) + attrsJSON = make([]byte, len(obj.AttrsJSON)) copy(attrsJSON, obj.AttrsJSON) } // Some addrs.Referencable implementations are technically mutable, but // we treat them as immutable by convention and so we don't deep-copy here. dependencies := make([]addrs.Referenceable, len(obj.Dependencies)) + copy(dependencies, obj.Dependencies) return &ResourceInstanceObjectSrc{ Status: obj.Status, diff --git a/states/state_string.go b/states/state_string.go index 19ff3a161..e052c6852 100644 --- a/states/state_string.go +++ b/states/state_string.go @@ -174,15 +174,13 @@ func (m *Module) testString() string { // CAUTION: Since deposed keys are now random strings instead of // incrementing integers, this result will not be deterministic // if there is more than one deposed object. - var idx int - for _, t := range is.Deposed { + for k, t := range is.Deposed { id := legacyInstanceObjectID(t) taintStr := "" if t.Status == ObjectTainted { taintStr = " (tainted)" } - buf.WriteString(fmt.Sprintf(" Deposed ID %d = %s%s\n", idx+1, id, taintStr)) - idx++ + buf.WriteString(fmt.Sprintf(" Deposed ID %s = %s%s\n", k, id, taintStr)) } if obj := is.Current; obj != nil && len(obj.Dependencies) > 0 { diff --git a/states/state_test.go b/states/state_test.go index 35e714a7e..8f941d179 100644 --- a/states/state_test.go +++ b/states/state_test.go @@ -30,7 +30,7 @@ func TestState(t *testing.T) { Type: "test_thing", Name: "baz", }.Instance(addrs.IntKey(0)), - &ResourceInstanceObject{ + &ResourceInstanceObjectSrc{ Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), @@ -70,12 +70,12 @@ func TestState(t *testing.T) { EachMode: EachList, Instances: map[addrs.InstanceKey]*ResourceInstance{ addrs.IntKey(0): { - Current: &ResourceInstanceObject{ + Current: &ResourceInstanceObjectSrc{ SchemaVersion: 1, Status: ObjectReady, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, - Deposed: map[DeposedKey]*ResourceInstanceObject{}, + Deposed: map[DeposedKey]*ResourceInstanceObjectSrc{}, }, }, ProviderConfig: addrs.ProviderConfig{ diff --git a/states/statefile/file.go b/states/statefile/file.go index 7d797fd9d..0694c5ccc 100644 --- a/states/statefile/file.go +++ b/states/statefile/file.go @@ -2,7 +2,9 @@ package statefile import ( version "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform/states" + tfversion "github.com/hashicorp/terraform/version" ) // File is the in-memory representation of a state file. It includes the state @@ -29,6 +31,15 @@ type File struct { State *states.State } +func New(state *states.State, lineage string, serial uint64) *File { + return &File{ + TerraformVersion: tfversion.SemVer, + State: state, + Lineage: lineage, + Serial: serial, + } +} + // DeepCopy is a convenience method to create a new File object whose state // is a deep copy of the receiver's, as implemented by states.State.DeepCopy. func (f *File) DeepCopy() *File { diff --git a/states/statefile/version3_upgrade.go b/states/statefile/version3_upgrade.go index 6f2d6600c..9569f63ec 100644 --- a/states/statefile/version3_upgrade.go +++ b/states/statefile/version3_upgrade.go @@ -219,7 +219,7 @@ func upgradeInstanceObjectV3ToV4(rsOld *resourceStateV2, isOld *instanceStateV2, // SDK, and not a first-class concept in the state format. Here we're // sniffing for the pre-0.12 SDK's way of representing schema versions // and promoting it to our first-class field if we find it. We'll ignore - // if if it doesn't look like what the SDK would've written. If this + // it if it doesn't look like what the SDK would've written. If this // sniffing fails then we'll assume schema version 0. var schemaVersion uint64 migratedSchemaVersion := false @@ -269,11 +269,34 @@ func upgradeInstanceObjectV3ToV4(rsOld *resourceStateV2, isOld *instanceStateV2, } } + var attributes map[string]string + if isOld.Attributes != nil { + attributes = make(map[string]string, len(isOld.Attributes)) + for k, v := range isOld.Attributes { + attributes[k] = v + } + } + if isOld.ID != "" { + // As a special case, if we don't already have an "id" attribute and + // yet there's a non-empty first-class ID on the old object then we'll + // create a synthetic id attribute to avoid losing that first-class id. + // In practice this generally arises only in tests where state literals + // are hand-written in a non-standard way; real code prior to 0.12 + // would always force the first-class ID to be copied into the + // id attribute before storing. + if attributes == nil { + attributes = make(map[string]string, len(isOld.Attributes)) + } + if idVal := attributes["id"]; idVal == "" { + attributes["id"] = isOld.ID + } + } + return &instanceObjectStateV4{ IndexKey: instKeyRaw, Status: status, Deposed: string(deposedKey), - AttributesFlat: isOld.Attributes, + AttributesFlat: attributes, Dependencies: rsOld.Dependencies, SchemaVersion: schemaVersion, PrivateRaw: privateJSON, diff --git a/states/statefile/version4.go b/states/statefile/version4.go index 8105698eb..10279aefb 100644 --- a/states/statefile/version4.go +++ b/states/statefile/version4.go @@ -151,12 +151,11 @@ func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) { case isV4.AttributesFlat != nil: obj.AttrsFlat = isV4.AttributesFlat default: - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Invalid resource instance attributes in state", - fmt.Sprintf("Instance %s does not have any stored attributes.", instAddr.Absolute(moduleAddr)), - )) - continue + // This is odd, but we'll accept it and just treat the + // object has being empty. In practice this should arise + // only from the contrived sort of state objects we tend + // to hand-write inline in tests. + obj.AttrsJSON = []byte{'{', '}'} } } @@ -219,6 +218,10 @@ func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) { fmt.Sprintf("Instance %s declares dependency on %q, which is not a reference to a dependable object.", instAddr.Absolute(moduleAddr), depRaw), )) } + if ref.Subject == nil { + // Should never happen + panic(fmt.Sprintf("parsing dependency %q for instance %s returned a nil address", depRaw, instAddr.Absolute(moduleAddr))) + } deps = append(deps, ref.Subject) } obj.Dependencies = deps @@ -245,7 +248,7 @@ func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) { continue } - ms.SetResourceInstanceDeposed(instAddr, dk, obj) + ms.SetResourceInstanceDeposed(instAddr, dk, obj, providerAddr) default: is := ms.ResourceInstance(instAddr) if is.HasCurrent() { diff --git a/states/statemgr/plan.go b/states/statemgr/plan.go new file mode 100644 index 000000000..b5036030a --- /dev/null +++ b/states/statemgr/plan.go @@ -0,0 +1,71 @@ +package statemgr + +import ( + "fmt" + + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statefile" +) + +// PlannedStateUpdate is a special helper to obtain a statefile representation +// of a not-yet-written state snapshot that can be written later by a call +// to the companion function WritePlannedStateUpdate. +// +// The statefile object returned here has an unusual interpretation of its +// metadata that is understood only by WritePlannedStateUpdate, and so the +// returned object should not be used for any other purpose. +// +// If the state manager implements Locker then it is the caller's +// responsibility to hold the lock at least for the duration of this call. +// It is not safe to modify the given state concurrently while +// PlannedStateUpdate is running. +func PlannedStateUpdate(mgr Transient, planned *states.State) *statefile.File { + ret := &statefile.File{ + State: planned.DeepCopy(), + } + + // If the given manager uses snapshot metadata then we'll save that + // in our file so we can check it again during WritePlannedStateUpdate. + if mr, ok := mgr.(PersistentMeta); ok { + m := mr.StateSnapshotMeta() + ret.Lineage = m.Lineage + ret.Serial = m.Serial + } + + return ret +} + +// WritePlannedStateUpdate is a companion to PlannedStateUpdate that attempts +// to apply a state update that was planned earlier to the given state +// manager. +// +// An error is returned if this function detects that a new state snapshot +// has been written to the backend since the update was planned, since that +// invalidates the plan. An error is returned also if the manager itself +// rejects the given state when asked to store it. +// +// If the returned error is nil, the given manager's transient state snapshot +// is updated to match what was planned. It is the caller's responsibility +// to then persist that state if the manager also implements Persistent and +// the snapshot should be written to the persistent store. +// +// If the state manager implements Locker then it is the caller's +// responsibility to hold the lock at least for the duration of this call. +func WritePlannedStateUpdate(mgr Transient, planned *statefile.File) error { + // If the given manager uses snapshot metadata then we'll check to make + // sure no new snapshots have been created since we planned to write + // the given state file. + if mr, ok := mgr.(PersistentMeta); ok { + m := mr.StateSnapshotMeta() + if planned.Lineage != "" { + if planned.Lineage != m.Lineage { + return fmt.Errorf("planned state update is from an unrelated state lineage than the current state") + } + if planned.Serial != m.Serial { + return fmt.Errorf("stored state has been changed by another operation since the given update was planned") + } + } + } + + return mgr.WriteState(planned.State) +} diff --git a/states/sync.go b/states/sync.go index 452aa5d1b..669d46162 100644 --- a/states/sync.go +++ b/states/sync.go @@ -195,6 +195,52 @@ func (s *SyncState) RemoveResource(addr addrs.AbsResource) { s.maybePruneModule(addr.Module) } +// MaybeFixUpResourceInstanceAddressForCount deals with the situation where a +// resource has changed from having "count" set to not set, or vice-versa, and +// so we need to rename the zeroth instance key to no key at all, or vice-versa. +// +// Set countEnabled to true if the resource has count set in its new +// configuration, or false if it does not. +// +// The state is modified in-place if necessary, moving a resource instance +// between the two addresses. The return value is true if a change was made, +// and false otherwise. +func (s *SyncState) MaybeFixUpResourceInstanceAddressForCount(addr addrs.AbsResource, countEnabled bool) bool { + s.lock.Lock() + defer s.lock.Unlock() + + ms := s.state.Module(addr.Module) + if ms == nil { + return false + } + + relAddr := addr.Resource + rs := ms.Resource(relAddr) + if rs == nil { + return false + } + huntKey := addrs.NoKey + replaceKey := addrs.InstanceKey(addrs.IntKey(0)) + if !countEnabled { + huntKey, replaceKey = replaceKey, huntKey + } + + is, exists := rs.Instances[huntKey] + if !exists { + return false + } + + if _, exists := rs.Instances[replaceKey]; exists { + // If the replacement key also exists then we'll do nothing and keep both. + return false + } + + // If we get here then we need to "rename" from hunt to replace + rs.Instances[replaceKey] = is + delete(rs.Instances, huntKey) + return true +} + // SetResourceInstanceCurrent saves the given instance object as the current // generation of the resource instance with the given address, simulataneously // updating the recorded provider configuration address, dependencies, and @@ -246,12 +292,12 @@ func (s *SyncState) SetResourceInstanceCurrent(addr addrs.AbsResourceInstance, o // // If the containing module for this resource or the resource itself are not // already tracked in state then they will be added as a side-effect. -func (s *SyncState) SetResourceInstanceDeposed(addr addrs.AbsResourceInstance, key DeposedKey, obj *ResourceInstanceObjectSrc) { +func (s *SyncState) SetResourceInstanceDeposed(addr addrs.AbsResourceInstance, key DeposedKey, obj *ResourceInstanceObjectSrc, provider addrs.AbsProviderConfig) { s.lock.Lock() defer s.lock.Unlock() ms := s.state.EnsureModule(addr.Module) - ms.SetResourceInstanceDeposed(addr.Resource, key, obj.DeepCopy()) + ms.SetResourceInstanceDeposed(addr.Resource, key, obj.DeepCopy(), provider) } // DeposeResourceInstanceObject moves the current instance object for the @@ -277,6 +323,19 @@ func (s *SyncState) DeposeResourceInstanceObject(addr addrs.AbsResourceInstance) return ms.deposeResourceInstanceObject(addr.Resource) } +// ForgetResourceInstanceDeposed removes the record of the deposed object with +// the given address and key, if present. If not present, this is a no-op. +func (s *SyncState) ForgetResourceInstanceDeposed(addr addrs.AbsResourceInstance, key DeposedKey) { + s.lock.Lock() + defer s.lock.Unlock() + + ms := s.state.Module(addr.Module) + if ms == nil { + return + } + ms.ForgetResourceInstanceDeposed(addr.Resource, key) +} + // Lock acquires an explicit lock on the state, allowing direct read and write // access to the returned state object. The caller must call Unlock once // access is no longer needed, and then immediately discard the state pointer diff --git a/terraform/context.go b/terraform/context.go index 2dbbc4663..9793f2425 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -7,17 +7,16 @@ import ( "strings" "sync" - "github.com/hashicorp/terraform/lang" - - "github.com/hashicorp/terraform/addrs" - - "github.com/hashicorp/terraform/configs" - "github.com/hashicorp/terraform/tfdiags" + "github.com/hashicorp/hcl" "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/hcl" + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/version" + "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/lang" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/tfdiags" ) // InputMode defines what sort of input will be asked for when Input @@ -54,19 +53,18 @@ var ( // ContextOpts are the user-configurable options to create a context with // NewContext. type ContextOpts struct { - Meta *ContextMeta - Destroy bool - Diff *Diff - Hooks []Hook - Config *configs.Config - Parallelism int - State *State - StateFutureAllowed bool - ProviderResolver ResourceProviderResolver - Provisioners map[string]ResourceProvisionerFactory - Shadow bool - Targets []addrs.Targetable - Variables InputValues + Config *configs.Config + Changes *plans.Changes + State *states.State + Targets []addrs.Targetable + Variables InputValues + Meta *ContextMeta + Destroy bool + + Hooks []Hook + Parallelism int + ProviderResolver ResourceProviderResolver + Provisioners map[string]ResourceProvisionerFactory // If non-nil, will apply as additional constraints on the provider // plugins that will be requested from the provider resolver. @@ -86,29 +84,21 @@ type ContextMeta struct { // Context represents all the context that Terraform needs in order to // perform operations on infrastructure. This structure is built using -// NewContext. See the documentation for that. -// -// Extra functions on Context can be found in context_*.go files. +// NewContext. type Context struct { - // Maintainer note: Anytime this struct is changed, please verify - // that newShadowContext still does the right thing. Tests should - // fail regardless but putting this note here as well. + config *configs.Config + changes *plans.Changes + state *states.State + targets []addrs.Targetable + variables InputValues + meta *ContextMeta + destroy bool + hooks []Hook components contextComponentFactory schemas *Schemas - destroy bool - diff *Diff - diffLock sync.RWMutex - hooks []Hook - meta *ContextMeta - config *configs.Config sh *stopHook - shadow bool - state *State - stateLock sync.RWMutex - targets []addrs.Targetable uiInput UIInput - variables InputValues l sync.Mutex // Lock acquired during any task parallelSem Semaphore @@ -121,9 +111,11 @@ type Context struct { shadowErr error } +// (additional methods on Context can be found in context_*.go files.) + // NewContext creates a new Context structure. // -// Once a Context is created, the caller should not access or mutate any of +// Once a Context is created, the caller must not access or mutate any of // the objects referenced (directly or indirectly) by the ContextOpts fields. // // If the returned diagnostics contains errors then the resulting context is @@ -146,22 +138,9 @@ func NewContext(opts *ContextOpts) (*Context, tfdiags.Diagnostics) { state := opts.State if state == nil { - state = new(State) - state.init() + state = states.NewState() } - // If our state is from the future, then error. Callers can avoid - // this error by explicitly setting `StateFutureAllowed`. - if stateDiags := CheckStateVersion(state, opts.StateFutureAllowed); stateDiags.HasErrors() { - diags = diags.Append(stateDiags) - return nil, diags - } - - // Explicitly reset our state version to our current version so that - // any operations we do will write out that our latest version - // has run. - state.TFVersion = version.Version - // Determine parallelism, default to 10. We do this both to limit // CPU pressure but also to have an extra guard against rate throttling // from providers. @@ -214,20 +193,24 @@ func NewContext(opts *ContextOpts) (*Context, tfdiags.Diagnostics) { return nil, diags } - diff := opts.Diff - if diff == nil { - diff = &Diff{} + changes := opts.Changes + if changes == nil { + changes = plans.NewChanges() + } + + config := opts.Config + if config == nil { + config = configs.NewEmptyConfig() } return &Context{ components: components, schemas: schemas, destroy: opts.Destroy, - diff: diff, + changes: changes, hooks: hooks, meta: opts.Meta, - config: opts.Config, - shadow: opts.Shadow, + config: config, state: state, targets: opts.Targets, uiInput: opts.UIInput, @@ -261,7 +244,7 @@ func (c *Context) Graph(typ GraphType, opts *ContextGraphOpts) (*Graph, tfdiags. case GraphTypeApply: return (&ApplyGraphBuilder{ Config: c.config, - Diff: c.diff, + Changes: c.changes, State: c.state, Components: c.components, Schemas: c.schemas, @@ -295,11 +278,12 @@ func (c *Context) Graph(typ GraphType, opts *ContextGraphOpts) (*Graph, tfdiags. case GraphTypePlanDestroy: return (&DestroyPlanGraphBuilder{ - Config: c.config, - State: c.state, - Schemas: c.schemas, - Targets: c.targets, - Validate: opts.Validate, + Config: c.config, + State: c.state, + Components: c.components, + Schemas: c.schemas, + Targets: c.targets, + Validate: opts.Validate, }).Build(addrs.RootModuleInstance) case GraphTypeRefresh: @@ -356,7 +340,7 @@ func (c *Context) ShadowError() error { // State returns a copy of the current state associated with this context. // // This cannot safely be called in parallel with any other Context function. -func (c *Context) State() *State { +func (c *Context) State() *states.State { return c.state.DeepCopy() } @@ -398,9 +382,6 @@ func (c *Context) Eval(path addrs.ModuleInstance) (*lang.Scope, tfdiags.Diagnost walker, walkDiags = c.walk(graph, walkEval) diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walkDiags) - - // Clean out any unused things - c.state.prune() } if walker == nil { @@ -441,7 +422,7 @@ func (c *Context) Interpolater() *Interpolater { // State() method. Currently the helper/resource testing framework relies // on the absence of a returned state to determine if Destroy can be // called, so that will need to be refactored before this can be changed. -func (c *Context) Apply() (*State, tfdiags.Diagnostics) { +func (c *Context) Apply() (*states.State, tfdiags.Diagnostics) { defer c.acquireRun("apply")() // Copy our own state @@ -464,9 +445,6 @@ func (c *Context) Apply() (*State, tfdiags.Diagnostics) { diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walkDiags) - // Clean out any unused things - c.state.prune() - return c.state, diags } @@ -477,39 +455,44 @@ func (c *Context) Apply() (*State, tfdiags.Diagnostics) { // // Plan also updates the diff of this context to be the diff generated // by the plan, so Apply can be called after. -func (c *Context) Plan() (*Plan, tfdiags.Diagnostics) { +func (c *Context) Plan() (*plans.Plan, tfdiags.Diagnostics) { defer c.acquireRun("plan")() - // The Plan struct wants the legacy-style of targets as a simple []string, - // so we must shim that here. - legacyTargets := make([]string, len(c.targets)) - for i, addr := range c.targets { - legacyTargets[i] = addr.String() + var diags tfdiags.Diagnostics + + varVals := make(map[string]plans.DynamicValue, len(c.variables)) + for k, iv := range c.variables { + // We use cty.DynamicPseudoType here so that we'll save both the + // value _and_ its dynamic type in the plan, so we can recover + // exactly the same value later. + dv, err := plans.NewDynamicValue(iv.Value, cty.DynamicPseudoType) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to prepare variable value for plan", + fmt.Sprintf("The value for variable %q could not be serialized to store in the plan: %s.", k, err), + )) + continue + } + varVals[k] = dv } - var diags tfdiags.Diagnostics - p := &Plan{ - Config: c.config, - Vars: c.variables.JustValues(), - State: c.state, - Targets: legacyTargets, - - TerraformVersion: version.String(), - ProviderSHA256s: c.providerSHA256s, + p := &plans.Plan{ + VariableValues: varVals, + TargetAddrs: c.targets, + ProviderSHA256s: c.providerSHA256s, } var operation walkOperation if c.destroy { operation = walkPlanDestroy - p.Destroy = true } else { // Set our state to be something temporary. We do this so that // the plan can update a fake state so that variables work, then // we replace it back with our old state. old := c.state if old == nil { - c.state = &State{} - c.state.init() + c.state = states.NewState() } else { c.state = old.DeepCopy() } @@ -520,12 +503,6 @@ func (c *Context) Plan() (*Plan, tfdiags.Diagnostics) { operation = walkPlan } - // Setup our diff - c.diffLock.Lock() - c.diff = new(Diff) - c.diff.init() - c.diffLock.Unlock() - // Build the graph. graphType := GraphTypePlan if c.destroy { @@ -544,18 +521,7 @@ func (c *Context) Plan() (*Plan, tfdiags.Diagnostics) { if walkDiags.HasErrors() { return nil, diags } - p.Diff = c.diff - - // If this is true, it means we're running unit tests. In this case, - // we perform a deep copy just to ensure that all context tests also - // test that a diff is copy-able. This will panic if it fails. This - // is enabled during unit tests. - // - // This should never be true during production usage, but even if it is, - // it can't do any real harm. - if contextTestDeepCopyOnPlan { - p.Diff.DeepCopy() - } + p.Changes = c.changes return p, diags } @@ -566,7 +532,7 @@ func (c *Context) Plan() (*Plan, tfdiags.Diagnostics) { // // Even in the case an error is returned, the state may be returned and // will potentially be partially updated. -func (c *Context) Refresh() (*State, tfdiags.Diagnostics) { +func (c *Context) Refresh() (*states.State, tfdiags.Diagnostics) { defer c.acquireRun("refresh")() // Copy our own state @@ -585,9 +551,6 @@ func (c *Context) Refresh() (*State, tfdiags.Diagnostics) { return nil, diags } - // Clean out any unused things - c.state.prune() - return c.state, diags } @@ -699,9 +662,6 @@ func (c *Context) acquireRun(phase string) func() { // Build our lock c.runCond = sync.NewCond(&c.l) - // Setup debugging - dbug.SetPhase(phase) - // Create a new run context c.runContext, c.runContextCancel = context.WithCancel(context.Background()) @@ -719,11 +679,6 @@ func (c *Context) releaseRun() { c.l.Lock() defer c.l.Unlock() - // setting the phase to "INVALID" lets us easily detect if we have - // operations happening outside of a run, or we missed setting the proper - // phase - dbug.SetPhase("INVALID") - // End our run. We check if runContext is non-nil because it can be // set to nil if it was cancelled via Stop() if c.runContextCancel != nil { @@ -760,6 +715,7 @@ func (c *Context) walk(graph *Graph, operation walkOperation) (*ContextGraphWalk func (c *Context) graphWalker(operation walkOperation) *ContextGraphWalker { return &ContextGraphWalker{ Context: c, + State: c.state.SyncWrapper(), Operation: operation, StopContext: c.runContext, RootVariableValues: c.variables, diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index 908c3fe25..8155e3b61 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -1,7 +1,7 @@ package terraform import ( - "bytes" + "encoding/json" "errors" "fmt" "log" @@ -19,8 +19,11 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/addrs" - "github.com/hashicorp/terraform/configs/configschema" + "github.com/hashicorp/terraform/config/hcl2shim" "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/configs/configschema" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" ) @@ -86,10 +89,18 @@ func TestContext2Apply_unstable(t *testing.T) { t.Fatalf("unexpected error during Plan: %s", diags.Err()) } - md := plan.Diff.RootModule() - rd := md.Resources["test_resource.foo"] - - randomVal := rd.Attributes["random"].New + addr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) + schema := p.GetSchemaReturn.ResourceTypes["test_resource"] // automatically available in mock + rds := plan.Changes.ResourceInstance(addr) + rd, err := rds.Decode(schema.ImpliedType()) + if err != nil { + t.Fatal(err) + } + randomVal := rd.After.GetAttr("random").AsString() t.Logf("plan-time value is %q", randomVal) state, diags := ctx.Apply() @@ -97,13 +108,15 @@ func TestContext2Apply_unstable(t *testing.T) { t.Fatalf("unexpected error during Apply: %s", diags.Err()) } - mod := state.RootModule() + mod := state.Module(addr.Module) + rss := state.ResourceInstance(addr) + if len(mod.Resources) != 1 { t.Fatalf("wrong number of resources %d; want 1", len(mod.Resources)) } - rs := mod.Resources["test_resource.foo"].Primary - if got, want := rs.Attributes["random"], randomVal; got != want { + rs, err := rss.Current.Decode(schema.ImpliedType()) + if got, want := rs.Value.GetAttr("random").AsString(), randomVal; got != want { // FIXME: We actually currently have a bug where we re-interpolate // the config during apply and end up with a random result, so this // check fails. This check _should not_ fail, so we should fix this @@ -274,7 +287,7 @@ func TestContext2Apply_resourceDependsOnModuleStateOnly(t *testing.T) { p := testProvider("aws") p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -302,7 +315,7 @@ func TestContext2Apply_resourceDependsOnModuleStateOnly(t *testing.T) { }, }, }, - } + }) { // verify the apply happens in the correct order @@ -362,7 +375,7 @@ func TestContext2Apply_resourceDependsOnModuleDestroy(t *testing.T) { p := testProvider("aws") p.DiffFn = testDiffFn - var globalState *State + var globalState *states.State { p.ApplyFn = testApplyFn ctx := testContext2(t, &ContextOpts{ @@ -687,7 +700,7 @@ func TestContext2Apply_providerAliasConfigure(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(p.String()) + t.Logf(legacyDiffComparisonString(p.Changes)) } // Configure to record calls AFTER Plan above @@ -802,7 +815,7 @@ func TestContext2Apply_createBeforeDestroy(t *testing.T) { p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -819,7 +832,7 @@ func TestContext2Apply_createBeforeDestroy(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -833,7 +846,7 @@ func TestContext2Apply_createBeforeDestroy(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(p.String()) + t.Logf(legacyDiffComparisonString(p.Changes)) } state, diags := ctx.Apply() @@ -858,7 +871,7 @@ func TestContext2Apply_createBeforeDestroyUpdate(t *testing.T) { p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -875,7 +888,7 @@ func TestContext2Apply_createBeforeDestroyUpdate(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -889,7 +902,7 @@ func TestContext2Apply_createBeforeDestroyUpdate(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(p.String()) + t.Logf(legacyDiffComparisonString(p.Changes)) } state, diags := ctx.Apply() @@ -916,7 +929,7 @@ func TestContext2Apply_createBeforeDestroy_dependsNonCBD(t *testing.T) { p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -943,7 +956,7 @@ func TestContext2Apply_createBeforeDestroy_dependsNonCBD(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -957,7 +970,7 @@ func TestContext2Apply_createBeforeDestroy_dependsNonCBD(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(p.String()) + t.Logf(legacyDiffComparisonString(p.Changes)) } state, diags := ctx.Apply() @@ -986,7 +999,7 @@ func TestContext2Apply_createBeforeDestroy_hook(t *testing.T) { p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1004,15 +1017,15 @@ func TestContext2Apply_createBeforeDestroy_hook(t *testing.T) { }, }, }, - } + }) var actual []string var actualLock sync.Mutex - h.PostApplyFn = func(n *InstanceInfo, s *InstanceState, e error) (HookAction, error) { + h.PostApplyFn = func(addr addrs.AbsResourceInstance, gen states.Generation, sv cty.Value, e error) (HookAction, error) { actualLock.Lock() defer actualLock.Unlock() - actual = append(actual, s.String()) + actual = append(actual, state.String()) return HookActionContinue, nil } @@ -1030,7 +1043,7 @@ func TestContext2Apply_createBeforeDestroy_hook(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(p.String()) + t.Logf(legacyDiffComparisonString(p.Changes)) } if _, diags := ctx.Apply(); diags.HasErrors() { @@ -1054,7 +1067,7 @@ func TestContext2Apply_createBeforeDestroy_deposedCount(t *testing.T) { p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1088,7 +1101,7 @@ func TestContext2Apply_createBeforeDestroy_deposedCount(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -1103,7 +1116,7 @@ func TestContext2Apply_createBeforeDestroy_deposedCount(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(p.String()) + t.Logf(legacyDiffComparisonString(p.Changes)) } state, diags := ctx.Apply() @@ -1133,7 +1146,7 @@ func TestContext2Apply_createBeforeDestroy_deposedOnly(t *testing.T) { p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1154,7 +1167,7 @@ func TestContext2Apply_createBeforeDestroy_deposedOnly(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -1169,7 +1182,7 @@ func TestContext2Apply_createBeforeDestroy_deposedOnly(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(p.String()) + t.Logf(legacyDiffComparisonString(p.Changes)) } state, diags := ctx.Apply() @@ -1189,7 +1202,7 @@ func TestContext2Apply_destroyComputed(t *testing.T) { p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1207,7 +1220,7 @@ func TestContext2Apply_destroyComputed(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -1222,7 +1235,7 @@ func TestContext2Apply_destroyComputed(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } else { - t.Logf("plan:\n\n%s", p.String()) + t.Logf("plan:\n\n%s", legacyDiffComparisonString(p.Changes)) } if _, diags := ctx.Apply(); diags.HasErrors() { @@ -1244,7 +1257,7 @@ func testContext2Apply_destroyDependsOn(t *testing.T) { p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1267,7 +1280,7 @@ func testContext2Apply_destroyDependsOn(t *testing.T) { }, }, }, - } + }) // Record the order we see Apply var actual []string @@ -1321,7 +1334,7 @@ func testContext2Apply_destroyDependsOnStateOnly(t *testing.T) { p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1347,7 +1360,7 @@ func testContext2Apply_destroyDependsOnStateOnly(t *testing.T) { }, }, }, - } + }) // Record the order we see Apply var actual []string @@ -1401,7 +1414,7 @@ func testContext2Apply_destroyDependsOnStateOnlyModule(t *testing.T) { p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "child"}, @@ -1427,7 +1440,7 @@ func testContext2Apply_destroyDependsOnStateOnlyModule(t *testing.T) { }, }, }, - } + }) // Record the order we see Apply var actual []string @@ -1485,7 +1498,7 @@ func TestContext2Apply_dataBasic(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(p.String()) + t.Logf(legacyDiffComparisonString(p.Changes)) } state, diags := ctx.Apply() @@ -1505,7 +1518,7 @@ func TestContext2Apply_destroyData(t *testing.T) { p := testProvider("null") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1523,7 +1536,7 @@ func TestContext2Apply_destroyData(t *testing.T) { }, }, }, - } + }) hook := &testHook{} ctx := testContext2(t, &ContextOpts{ Config: m, @@ -1540,7 +1553,7 @@ func TestContext2Apply_destroyData(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(p.String()) + t.Logf(legacyDiffComparisonString(p.Changes)) } newState, diags := ctx.Apply() @@ -1552,7 +1565,7 @@ func TestContext2Apply_destroyData(t *testing.T) { t.Fatalf("state has %d modules after destroy; want 1", got) } - if got := len(newState.Modules[0].Resources); got != 0 { + if got := len(newState.RootModule().Resources); got != 0 { t.Fatalf("state has %d resources after destroy; want 0", got) } @@ -1575,7 +1588,7 @@ func TestContext2Apply_destroySkipsCBD(t *testing.T) { p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1595,7 +1608,7 @@ func TestContext2Apply_destroySkipsCBD(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -1610,7 +1623,7 @@ func TestContext2Apply_destroySkipsCBD(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(p.String()) + t.Logf(legacyDiffComparisonString(p.Changes)) } if _, diags := ctx.Apply(); diags.HasErrors() { @@ -1623,7 +1636,7 @@ func TestContext2Apply_destroyModuleVarProviderConfig(t *testing.T) { p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "child"}, @@ -1637,7 +1650,7 @@ func TestContext2Apply_destroyModuleVarProviderConfig(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -1690,7 +1703,7 @@ func TestContext2Apply_destroyCrossProviders(t *testing.T) { } func getContextForApply_destroyCrossProviders(t *testing.T, m *configs.Config, providers map[string]ResourceProviderFactory) *Context { - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1723,7 +1736,7 @@ func getContextForApply_destroyCrossProviders(t *testing.T, m *configs.Config, p }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed(providers), @@ -1849,7 +1862,7 @@ func TestContext2Apply_cancel(t *testing.T) { // Start the Apply in a goroutine var applyDiags tfdiags.Diagnostics - stateCh := make(chan *State) + stateCh := make(chan *states.State) go func() { state, diags := ctx.Apply() applyDiags = diags @@ -1915,7 +1928,7 @@ func TestContext2Apply_cancelBlock(t *testing.T) { // Start the Apply in a goroutine var applyDiags tfdiags.Diagnostics - stateCh := make(chan *State) + stateCh := make(chan *states.State) go func() { state, diags := ctx.Apply() applyDiags = diags @@ -1994,7 +2007,7 @@ func TestContext2Apply_cancelProvisioner(t *testing.T) { // Start the Apply in a goroutine var applyDiags tfdiags.Diagnostics - stateCh := make(chan *State) + stateCh := make(chan *states.State) go func() { state, diags := ctx.Apply() applyDiags = diags @@ -2063,7 +2076,7 @@ func TestContext2Apply_countDecrease(t *testing.T) { m := testModule(t, "apply-count-dec") p := testProvider("aws") p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2101,7 +2114,7 @@ func TestContext2Apply_countDecrease(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2133,7 +2146,7 @@ func TestContext2Apply_countDecreaseToOneX(t *testing.T) { p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2163,7 +2176,7 @@ func TestContext2Apply_countDecreaseToOneX(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2200,7 +2213,7 @@ func TestContext2Apply_countDecreaseToOneCorrupted(t *testing.T) { p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2227,7 +2240,7 @@ func TestContext2Apply_countDecreaseToOneCorrupted(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2241,7 +2254,10 @@ func TestContext2Apply_countDecreaseToOneCorrupted(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - testStringMatch(t, p, testTerraformApplyCountDecToOneCorruptedPlanStr) + planStr := legacyPlanComparisonString(ctx.State(), p.Changes) + if got, want := planStr, testTerraformApplyCountDecToOneCorruptedPlanStr; got != want { + t.Fatalf("wrong plan result\ngot:\n%s\nwant:\n%s", got, want) + } } state, diags := ctx.Apply() @@ -2260,7 +2276,7 @@ func TestContext2Apply_countTainted(t *testing.T) { m := testModule(t, "apply-count-tainted") p := testProvider("aws") p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2279,7 +2295,7 @@ func TestContext2Apply_countTainted(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2371,7 +2387,7 @@ func TestContext2Apply_provisionerInterpCount(t *testing.T) { // even though the provisioner expression is evaluated during the plan // walk. https://github.com/hashicorp/terraform/issues/16840 - m := testModule(t, "apply-provisioner-interp-count") + m, snap := testModuleWithSnapshot(t, "apply-provisioner-interp-count") p := testProvider("aws") p.ApplyFn = testApplyFn @@ -2398,25 +2414,18 @@ func TestContext2Apply_provisionerInterpCount(t *testing.T) { t.Fatalf("plan failed unexpectedly: %s", diags.Err()) } + state := ctx.State() + // We'll marshal and unmarshal the plan here, to ensure that we have // a clean new context as would be created if we separately ran // terraform plan -out=tfplan && terraform apply tfplan - var planBuf bytes.Buffer - err := WritePlan(plan, &planBuf) + ctxOpts, err := contextOptsForPlanViaFile(snap, state, plan) if err != nil { - t.Fatalf("failed to write plan: %s", err) + t.Fatal(err) } - plan, err = ReadPlan(&planBuf) - if err != nil { - t.Fatalf("failed to read plan: %s", err) - } - - ctx, diags = plan.Context(&ContextOpts{ - // Most options are taken from the plan in this case, but we still - // need to provide the plugins. - ProviderResolver: providerResolver, - Provisioners: provisioners, - }) + ctxOpts.ProviderResolver = providerResolver + ctxOpts.Provisioners = provisioners + ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("failed to create context for plan: %s", diags.Err()) } @@ -2482,7 +2491,7 @@ func TestContext2Apply_moduleDestroyOrder(t *testing.T) { return nil, nil } - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2505,7 +2514,7 @@ func TestContext2Apply_moduleDestroyOrder(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -2608,7 +2617,7 @@ func TestContext2Apply_moduleOrphanInheritAlias(t *testing.T) { } // Create a state with an orphan module - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "child"}, @@ -2623,7 +2632,7 @@ func TestContext2Apply_moduleOrphanInheritAlias(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -2666,7 +2675,7 @@ func TestContext2Apply_moduleOrphanProvider(t *testing.T) { } // Create a state with an orphan module - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "child"}, @@ -2680,7 +2689,7 @@ func TestContext2Apply_moduleOrphanProvider(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -2716,7 +2725,7 @@ func TestContext2Apply_moduleOrphanGrandchildProvider(t *testing.T) { } // Create a state with an orphan module that is nested (grandchild) - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "parent", "child"}, @@ -2730,7 +2739,7 @@ func TestContext2Apply_moduleOrphanGrandchildProvider(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -2917,7 +2926,7 @@ func TestContext2Apply_moduleProviderCloseNested(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "child", "subchild"}, @@ -2931,7 +2940,7 @@ func TestContext2Apply_moduleProviderCloseNested(t *testing.T) { }, }, }, - }, + }), Destroy: true, }) @@ -2955,7 +2964,7 @@ func TestContext2Apply_moduleVarRefExisting(t *testing.T) { p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2973,7 +2982,7 @@ func TestContext2Apply_moduleVarRefExisting(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -3208,7 +3217,7 @@ func TestContext2Apply_multiProviderDestroy(t *testing.T) { }, } - var state *State + var state *states.State // First, create the instances { @@ -3335,7 +3344,7 @@ func TestContext2Apply_multiProviderDestroyChild(t *testing.T) { }, } - var state *State + var state *states.State // First, create the instances { @@ -3460,10 +3469,10 @@ func TestContext2Apply_multiVar(t *testing.T) { t.Fatalf("diags: %s", diags.Err()) } - actual := state.RootModule().Outputs["output"] - expected := "bar0,bar1,bar2" + actual := state.RootModule().OutputValues["output"] + expected := cty.StringVal("bar0,bar1,bar2") if actual == nil || actual.Value != expected { - t.Fatalf("bad: \n%s", actual) + t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected) } t.Logf("Initial state: %s", state.String()) @@ -3497,14 +3506,14 @@ func TestContext2Apply_multiVar(t *testing.T) { t.Logf("End state: %s", state.String()) - actual := state.RootModule().Outputs["output"] + actual := state.RootModule().OutputValues["output"] if actual == nil { t.Fatal("missing output") } - expected := "bar0" + expected := cty.StringVal("bar0") if actual.Value != expected { - t.Fatalf("bad: \n%s", actual) + t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected) } } } @@ -3698,8 +3707,8 @@ func TestContext2Apply_multiVarComprehensive(t *testing.T) { }, } got := map[string]interface{}{} - for k, s := range state.RootModule().Outputs { - got[k] = s.Value + for k, s := range state.RootModule().OutputValues { + got[k] = hcl2shim.ConfigValueFromHCL2(s.Value) } if !reflect.DeepEqual(got, want) { t.Errorf( @@ -3739,10 +3748,10 @@ func TestContext2Apply_multiVarOrder(t *testing.T) { t.Logf("State: %s", state.String()) - actual := state.RootModule().Outputs["should-be-11"] - expected := "index-11" + actual := state.RootModule().OutputValues["should-be-11"] + expected := cty.StringVal("index-11") if actual == nil || actual.Value != expected { - t.Fatalf("bad: \n%s", actual) + t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected) } } @@ -3775,17 +3784,17 @@ func TestContext2Apply_multiVarOrderInterp(t *testing.T) { t.Logf("State: %s", state.String()) - actual := state.RootModule().Outputs["should-be-11"] - expected := "baz-index-11" + actual := state.RootModule().OutputValues["should-be-11"] + expected := cty.StringVal("baz-index-11") if actual == nil || actual.Value != expected { - t.Fatalf("bad: \n%s", actual) + t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected) } } // Based on GH-10440 where a graph edge wasn't properly being created // between a modified resource and a count instance being destroyed. func TestContext2Apply_multiVarCountDec(t *testing.T) { - var s *State + var s *states.State // First create resources. Nothing sneaky here. { @@ -3882,7 +3891,7 @@ func TestContext2Apply_multiVarCountDec(t *testing.T) { t.Fatalf("plan errors: %s", diags.Err()) } - t.Logf("Step 2 plan:\n%s", plan) + t.Logf("Step 2 plan:\n%s", legacyDiffComparisonString(plan.Changes)) log.Print("\n========\nStep 2 Apply\n========") state, diags := ctx.Apply() @@ -4050,7 +4059,7 @@ func TestContext2Apply_outputOrphan(t *testing.T) { p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -4068,7 +4077,7 @@ func TestContext2Apply_outputOrphan(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -4102,7 +4111,7 @@ func TestContext2Apply_outputOrphanModule(t *testing.T) { p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "child"}, @@ -4118,7 +4127,7 @@ func TestContext2Apply_outputOrphanModule(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -4494,7 +4503,7 @@ func TestContext2Apply_provisionerFail_createBeforeDestroy(t *testing.T) { return fmt.Errorf("EXPLOSION") } - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -4511,7 +4520,7 @@ func TestContext2Apply_provisionerFail_createBeforeDestroy(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -4544,7 +4553,7 @@ func TestContext2Apply_provisionerFail_createBeforeDestroy(t *testing.T) { func TestContext2Apply_error_createBeforeDestroy(t *testing.T) { m := testModule(t, "apply-error-create-before") p := testProvider("aws") - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -4561,7 +4570,7 @@ func TestContext2Apply_error_createBeforeDestroy(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -4595,7 +4604,7 @@ func TestContext2Apply_error_createBeforeDestroy(t *testing.T) { func TestContext2Apply_errorDestroy_createBeforeDestroy(t *testing.T) { m := testModule(t, "apply-error-create-before") p := testProvider("aws") - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -4612,7 +4621,7 @@ func TestContext2Apply_errorDestroy_createBeforeDestroy(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -4657,7 +4666,7 @@ func TestContext2Apply_multiDepose_createBeforeDestroy(t *testing.T) { p := testProvider("aws") p.DiffFn = testDiffFn ps := map[string]ResourceProviderFactory{"aws": testProviderFuncFixed(p)} - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -4669,7 +4678,7 @@ func TestContext2Apply_multiDepose_createBeforeDestroy(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -4868,10 +4877,10 @@ func TestContext2Apply_provisionerFailContinueHook(t *testing.T) { t.Fatalf("apply errors: %s", diags.Err()) } - if !h.PostProvisionCalled { - t.Fatal("PostProvision not called") + if !h.PostProvisionInstanceStepCalled { + t.Fatal("PostProvisionInstanceStep not called") } - if h.PostProvisionErrorArg == nil { + if h.PostProvisionInstanceStepErrorArg == nil { t.Fatal("should have error") } } @@ -4891,7 +4900,7 @@ func TestContext2Apply_provisionerDestroy(t *testing.T) { return nil } - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -4905,7 +4914,7 @@ func TestContext2Apply_provisionerDestroy(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -4949,7 +4958,7 @@ func TestContext2Apply_provisionerDestroyFail(t *testing.T) { return fmt.Errorf("provisioner error") } - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -4963,7 +4972,7 @@ func TestContext2Apply_provisionerDestroyFail(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -5022,7 +5031,7 @@ func TestContext2Apply_provisionerDestroyFailContinue(t *testing.T) { return fmt.Errorf("provisioner error") } - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -5036,7 +5045,7 @@ func TestContext2Apply_provisionerDestroyFailContinue(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -5098,7 +5107,7 @@ func TestContext2Apply_provisionerDestroyFailContinueFail(t *testing.T) { return fmt.Errorf("provisioner error") } - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -5112,7 +5121,7 @@ func TestContext2Apply_provisionerDestroyFailContinueFail(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -5177,7 +5186,7 @@ func TestContext2Apply_provisionerDestroyTainted(t *testing.T) { return nil } - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -5192,7 +5201,7 @@ func TestContext2Apply_provisionerDestroyTainted(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -5249,7 +5258,7 @@ func TestContext2Apply_provisionerDestroyModule(t *testing.T) { return nil } - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "child"}, @@ -5263,7 +5272,7 @@ func TestContext2Apply_provisionerDestroyModule(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -5313,7 +5322,7 @@ func TestContext2Apply_provisionerDestroyRef(t *testing.T) { return nil } - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -5339,7 +5348,7 @@ func TestContext2Apply_provisionerDestroyRef(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -5383,7 +5392,7 @@ func TestContext2Apply_provisionerDestroyRefInvalid(t *testing.T) { return nil } - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -5404,7 +5413,7 @@ func TestContext2Apply_provisionerDestroyRefInvalid(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -5665,7 +5674,7 @@ func TestContext2Apply_provisionerExplicitSelfRef(t *testing.T) { return nil } - var state *State + var state *states.State { ctx := testContext2(t, &ContextOpts{ Config: m, @@ -5769,7 +5778,17 @@ func TestContext2Apply_Provisioner_Diff(t *testing.T) { // Change the state to force a diff mod := state.RootModule() - mod.Resources["aws_instance.bar"].Primary.Attributes["foo"] = "baz" + obj := mod.Resources["aws_instance.bar"].Instances[addrs.NoKey].Current + var attrs map[string]interface{} + err := json.Unmarshal(obj.AttrsJSON, &attrs) + if err != nil { + t.Fatal(err) + } + attrs["foo"] = "baz" + obj.AttrsJSON, err = json.Marshal(attrs) + if err != nil { + t.Fatal(err) + } // Re-create context with state ctx = testContext2(t, &ContextOpts{ @@ -5808,7 +5827,7 @@ func TestContext2Apply_Provisioner_Diff(t *testing.T) { func TestContext2Apply_outputDiffVars(t *testing.T) { m := testModule(t, "apply-good") p := testProvider("aws") - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -5822,7 +5841,7 @@ func TestContext2Apply_outputDiffVars(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -6115,7 +6134,7 @@ func TestContext2Apply_destroyModulePrefix(t *testing.T) { } // Verify that we got the apply info correct - if v := h.PreApplyInfo.HumanId(); v != "module.child.aws_instance.foo" { + if v := h.PreApplyAddr.String(); v != "module.child.aws_instance.foo" { t.Fatalf("bad: %s", v) } @@ -6143,7 +6162,7 @@ func TestContext2Apply_destroyModulePrefix(t *testing.T) { } // Test that things were destroyed - if v := h.PreApplyInfo.HumanId(); v != "module.child.aws_instance.foo" { + if v := h.PreApplyAddr.String(); v != "module.child.aws_instance.foo" { t.Fatalf("bad: %s", v) } } @@ -6154,7 +6173,7 @@ func TestContext2Apply_destroyNestedModule(t *testing.T) { p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "child", "subchild"}, @@ -6169,7 +6188,7 @@ func TestContext2Apply_destroyNestedModule(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -6204,7 +6223,7 @@ func TestContext2Apply_destroyDeeplyNestedModule(t *testing.T) { p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "child", "subchild", "subsubchild"}, @@ -6219,7 +6238,7 @@ func TestContext2Apply_destroyDeeplyNestedModule(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -6250,12 +6269,12 @@ func TestContext2Apply_destroyDeeplyNestedModule(t *testing.T) { // https://github.com/hashicorp/terraform/issues/5440 func TestContext2Apply_destroyModuleWithAttrsReferencingResource(t *testing.T) { - m := testModule(t, "apply-destroy-module-with-attrs") + m, snap := testModuleWithSnapshot(t, "apply-destroy-module-with-attrs") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - var state *State + var state *states.State { ctx := testContext2(t, &ContextOpts{ Config: m, @@ -6270,7 +6289,7 @@ func TestContext2Apply_destroyModuleWithAttrsReferencingResource(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan diags: %s", diags.Err()) } else { - t.Logf("Step 1 plan: %s", p) + t.Logf("Step 1 plan: %s", legacyDiffComparisonString(p.Changes)) } var diags tfdiags.Diagnostics @@ -6310,25 +6329,19 @@ func TestContext2Apply_destroyModuleWithAttrsReferencingResource(t *testing.T) { t.Fatalf("destroy plan err: %s", diags.Err()) } - t.Logf("Step 2 plan: %s", plan) + t.Logf("Step 2 plan: %s", legacyDiffComparisonString(plan.Changes)) - var buf bytes.Buffer - if err := WritePlan(plan, &buf); err != nil { - t.Fatalf("plan write err: %s", err) - } - - planFromFile, err := ReadPlan(&buf) + ctxOpts, err := contextOptsForPlanViaFile(snap, state, plan) if err != nil { - t.Fatalf("plan read err: %s", err) + t.Fatalf("failed to round-trip through planfile: %s", err) } - ctx, diags = planFromFile.Context(&ContextOpts{ - ProviderResolver: ResourceProviderResolverFixed( - map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - ), - }) + ctxOpts.ProviderResolver = ResourceProviderResolverFixed( + map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + ) + ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } @@ -6354,12 +6367,12 @@ module.child: } func TestContext2Apply_destroyWithModuleVariableAndCount(t *testing.T) { - m := testModule(t, "apply-destroy-mod-var-and-count") + m, snap := testModuleWithSnapshot(t, "apply-destroy-mod-var-and-count") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - var state *State + var state *states.State var diags tfdiags.Diagnostics { ctx := testContext2(t, &ContextOpts{ @@ -6404,23 +6417,17 @@ func TestContext2Apply_destroyWithModuleVariableAndCount(t *testing.T) { t.Fatalf("destroy plan err: %s", diags.Err()) } - var buf bytes.Buffer - if err := WritePlan(plan, &buf); err != nil { - t.Fatalf("plan write err: %s", err) - } - - planFromFile, err := ReadPlan(&buf) + ctxOpts, err := contextOptsForPlanViaFile(snap, state, plan) if err != nil { - t.Fatalf("plan read err: %s", err) + t.Fatalf("failed to round-trip through planfile: %s", err) } - ctx, diags = planFromFile.Context(&ContextOpts{ - ProviderResolver: ResourceProviderResolverFixed( - map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - ), - }) + ctxOpts.ProviderResolver = ResourceProviderResolverFixed( + map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + ) + ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } @@ -6449,7 +6456,7 @@ func TestContext2Apply_destroyTargetWithModuleVariableAndCount(t *testing.T) { p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - var state *State + var state *states.State var diags tfdiags.Diagnostics { ctx := testContext2(t, &ContextOpts{ @@ -6512,12 +6519,12 @@ module.child: } func TestContext2Apply_destroyWithModuleVariableAndCountNested(t *testing.T) { - m := testModule(t, "apply-destroy-mod-var-and-count-nested") + m, snap := testModuleWithSnapshot(t, "apply-destroy-mod-var-and-count-nested") p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - var state *State + var state *states.State var diags tfdiags.Diagnostics { ctx := testContext2(t, &ContextOpts{ @@ -6562,23 +6569,17 @@ func TestContext2Apply_destroyWithModuleVariableAndCountNested(t *testing.T) { t.Fatalf("destroy plan err: %s", diags.Err()) } - var buf bytes.Buffer - if err := WritePlan(plan, &buf); err != nil { - t.Fatalf("plan write err: %s", err) - } - - planFromFile, err := ReadPlan(&buf) + ctxOpts, err := contextOptsForPlanViaFile(snap, state, plan) if err != nil { - t.Fatalf("plan read err: %s", err) + t.Fatalf("failed to round-trip through planfile: %s", err) } - ctx, diags = planFromFile.Context(&ContextOpts{ - ProviderResolver: ResourceProviderResolverFixed( - map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - ), - }) + ctxOpts.ProviderResolver = ResourceProviderResolverFixed( + map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + ) + ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } @@ -6677,7 +6678,7 @@ func TestContext2Apply_destroyOutputs(t *testing.T) { func TestContext2Apply_destroyOrphan(t *testing.T) { m := testModule(t, "apply-error") p := testProvider("aws") - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -6691,7 +6692,7 @@ func TestContext2Apply_destroyOrphan(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -6749,7 +6750,7 @@ func TestContext2Apply_destroyTaintedProvisioner(t *testing.T) { return nil } - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -6767,7 +6768,7 @@ func TestContext2Apply_destroyTaintedProvisioner(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -6864,7 +6865,7 @@ func TestContext2Apply_errorPartial(t *testing.T) { m := testModule(t, "apply-error") p := testProvider("aws") - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -6878,7 +6879,7 @@ func TestContext2Apply_errorPartial(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -6975,7 +6976,7 @@ func TestContext2Apply_hookOrphan(t *testing.T) { p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -6990,7 +6991,7 @@ func TestContext2Apply_hookOrphan(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -7067,11 +7068,13 @@ func TestContext2Apply_idAttr(t *testing.T) { if !ok { t.Fatal("not in state") } - if rs.Primary.ID != "foo" { - t.Fatalf("bad: %#v", rs.Primary.ID) + var attrs map[string]interface{} + err := json.Unmarshal(rs.Instances[addrs.NoKey].Current.AttrsJSON, &attrs) + if err != nil { + t.Fatal(err) } - if rs.Primary.Attributes["id"] != "foo" { - t.Fatalf("bad: %#v", rs.Primary.Attributes) + if got, want := attrs["id"], "foo"; got != want { + t.Fatalf("wrong id\ngot: %#v\nwant: %#v", got, want) } } @@ -7267,7 +7270,7 @@ func TestContext2Apply_taintX(t *testing.T) { return testApplyFn(info, s, d) } p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -7286,7 +7289,7 @@ func TestContext2Apply_taintX(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -7300,7 +7303,7 @@ func TestContext2Apply_taintX(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf("plan: %s", p) + t.Logf("plan: %s", legacyDiffComparisonString(p.Changes)) } state, diags := ctx.Apply() @@ -7324,7 +7327,7 @@ func TestContext2Apply_taintDep(t *testing.T) { p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -7354,7 +7357,7 @@ func TestContext2Apply_taintDep(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -7368,7 +7371,7 @@ func TestContext2Apply_taintDep(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf("plan: %s", p) + t.Logf("plan: %s", legacyDiffComparisonString(p.Changes)) } state, diags := ctx.Apply() @@ -7388,7 +7391,7 @@ func TestContext2Apply_taintDepRequiresNew(t *testing.T) { p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -7418,7 +7421,7 @@ func TestContext2Apply_taintDepRequiresNew(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -7432,7 +7435,7 @@ func TestContext2Apply_taintDepRequiresNew(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf("plan: %s", p) + t.Logf("plan: %s", legacyDiffComparisonString(p.Changes)) } state, diags := ctx.Apply() @@ -7577,7 +7580,7 @@ func TestContext2Apply_targetedDestroy(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -7587,7 +7590,7 @@ func TestContext2Apply_targetedDestroy(t *testing.T) { }, }, }, - }, + }), Targets: []addrs.Targetable{ addrs.RootModuleInstance.Resource( addrs.ManagedResourceMode, "aws_instance", "foo", @@ -7654,7 +7657,7 @@ func TestContext2Apply_destroyProvisionerWithLocals(t *testing.T) { Provisioners: map[string]ResourceProvisionerFactory{ "shell": testProvisionerFuncFixed(pr), }, - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root"}, @@ -7663,7 +7666,7 @@ func TestContext2Apply_destroyProvisionerWithLocals(t *testing.T) { }, }, }, - }, + }), Destroy: true, // the test works without targeting, but this also tests that the local // node isn't inadvertently pruned because of the wrong evaluation @@ -7741,7 +7744,7 @@ func TestContext2Apply_destroyProvisionerWithMultipleLocals(t *testing.T) { Provisioners: map[string]ResourceProvisionerFactory{ "shell": testProvisionerFuncFixed(pr), }, - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root"}, @@ -7751,7 +7754,7 @@ func TestContext2Apply_destroyProvisionerWithMultipleLocals(t *testing.T) { }, }, }, - }, + }), Destroy: true, }) @@ -7792,7 +7795,7 @@ func TestContext2Apply_destroyProvisionerWithOutput(t *testing.T) { Provisioners: map[string]ResourceProvisionerFactory{ "shell": testProvisionerFuncFixed(pr), }, - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root"}, @@ -7821,7 +7824,7 @@ func TestContext2Apply_destroyProvisionerWithOutput(t *testing.T) { }, }, }, - }, + }), Destroy: true, // targeting the source of the value used by all resources should still @@ -7847,7 +7850,7 @@ func TestContext2Apply_destroyProvisionerWithOutput(t *testing.T) { // confirm all outputs were removed too for _, mod := range state.Modules { - if len(mod.Outputs) > 0 { + if len(mod.OutputValues) > 0 { t.Fatalf("output left in module state: %#v\n", mod) } } @@ -7865,7 +7868,7 @@ func TestContext2Apply_targetedDestroyCountDeps(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -7875,7 +7878,7 @@ func TestContext2Apply_targetedDestroyCountDeps(t *testing.T) { }, }, }, - }, + }), Targets: []addrs.Targetable{ addrs.RootModuleInstance.Resource( addrs.ManagedResourceMode, "aws_instance", "foo", @@ -7909,7 +7912,7 @@ func TestContext2Apply_targetedDestroyModule(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -7926,7 +7929,7 @@ func TestContext2Apply_targetedDestroyModule(t *testing.T) { }, }, }, - }, + }), Targets: []addrs.Targetable{ addrs.RootModuleInstance.Child("child", addrs.NoKey).Resource( addrs.ManagedResourceMode, "aws_instance", "foo", @@ -7971,7 +7974,7 @@ func TestContext2Apply_targetedDestroyCountIndex(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -7985,7 +7988,7 @@ func TestContext2Apply_targetedDestroyCountIndex(t *testing.T) { }, }, }, - }, + }), Targets: []addrs.Targetable{ addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.IntKey(2), @@ -8048,7 +8051,7 @@ func TestContext2Apply_targetedModule(t *testing.T) { t.Fatalf("diags: %s", diags.Err()) } - mod := state.ModuleByPath(addrs.RootModuleInstance.Child("child", addrs.NoKey)) + mod := state.Module(addrs.RootModuleInstance.Child("child", addrs.NoKey)) if mod == nil { t.Fatalf("no child module found in the state!\n\n%#v", state) } @@ -8095,7 +8098,7 @@ func TestContext2Apply_targetedModuleDep(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf("Diff: %s", p) + t.Logf("Diff: %s", legacyDiffComparisonString(p.Changes)) } state, diags := ctx.Apply() @@ -8141,7 +8144,7 @@ func TestContext2Apply_targetedModuleUnrelatedOutputs(t *testing.T) { Targets: []addrs.Targetable{ addrs.RootModuleInstance.Child("child2", addrs.NoKey), }, - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ { Path: []string{"root"}, @@ -8164,7 +8167,7 @@ func TestContext2Apply_targetedModuleUnrelatedOutputs(t *testing.T) { Resources: map[string]*ResourceState{}, }, }, - }, + }), }) if _, diags := ctx.Plan(); diags.HasErrors() { @@ -8229,7 +8232,7 @@ func TestContext2Apply_targetedModuleResource(t *testing.T) { t.Fatalf("diags: %s", diags.Err()) } - mod := state.ModuleByPath(addrs.RootModuleInstance.Child("child", addrs.NoKey)) + mod := state.Module(addrs.RootModuleInstance.Child("child", addrs.NoKey)) if mod == nil || len(mod.Resources) != 1 { t.Fatalf("expected 1 resource, got: %#v", mod) } @@ -8400,7 +8403,7 @@ func TestContext2Apply_createBefore_depends(t *testing.T) { p := testProvider("aws") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -8426,7 +8429,7 @@ func TestContext2Apply_createBefore_depends(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, Hooks: []Hook{h}, @@ -8441,7 +8444,7 @@ func TestContext2Apply_createBefore_depends(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf("plan: %s", p) + t.Logf("plan: %s", legacyDiffComparisonString(p.Changes)) } h.Active = true @@ -8464,15 +8467,15 @@ func TestContext2Apply_createBefore_depends(t *testing.T) { // Test that things were managed _in the right order_ order := h.States diffs := h.Diffs - if order[0].ID != "" || diffs[0].Destroy { + if order[0].GetAttr("id").AsString() != "" || diffs[0].Action == plans.Delete { t.Fatalf("should create new instance first: %#v", order) } - if order[1].ID != "baz" { + if order[1].GetAttr("id").AsString() != "baz" { t.Fatalf("update must happen after create: %#v", order) } - if order[2].ID != "bar" || !diffs[2].Destroy { + if order[2].GetAttr("id").AsString() != "bar" || diffs[2].Action != plans.Delete { t.Fatalf("destroy must happen after update: %#v", order) } } @@ -8512,7 +8515,7 @@ func TestContext2Apply_singleDestroy(t *testing.T) { return testApplyFn(info, s, d) } p.DiffFn = testDiffFn - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -8538,7 +8541,7 @@ func TestContext2Apply_singleDestroy(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, Hooks: []Hook{h}, @@ -8584,9 +8587,11 @@ func TestContext2Apply_issue7824(t *testing.T) { }, } + m, snap := testModuleWithSnapshot(t, "issue-7824") + // Apply cleanly step 0 ctx := testContext2(t, &ContextOpts{ - Config: testModule(t, "issue-7824"), + Config: m, ProviderResolver: ResourceProviderResolverFixed( map[string]ResourceProviderFactory{ "template": testProviderFuncFixed(p), @@ -8600,23 +8605,17 @@ func TestContext2Apply_issue7824(t *testing.T) { } // Write / Read plan to simulate running it through a Plan file - var buf bytes.Buffer - if err := WritePlan(plan, &buf); err != nil { - t.Fatalf("err: %s", err) - } - - planFromFile, err := ReadPlan(&buf) + ctxOpts, err := contextOptsForPlanViaFile(snap, ctx.State(), plan) if err != nil { - t.Fatalf("err: %s", err) + t.Fatalf("failed to round-trip through planfile: %s", err) } - ctx, diags = planFromFile.Context(&ContextOpts{ - ProviderResolver: ResourceProviderResolverFixed( - map[string]ResourceProviderFactory{ - "template": testProviderFuncFixed(p), - }, - ), - }) + ctxOpts.ProviderResolver = ResourceProviderResolverFixed( + map[string]ResourceProviderFactory{ + "template": testProviderFuncFixed(p), + }, + ) + ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } @@ -8639,9 +8638,11 @@ func TestContext2Apply_issue5254(t *testing.T) { p.ApplyFn = testApplyFn p.DiffFn = testDiffFn + m, snap := testModuleWithSnapshot(t, "issue-5254/step-0") + // Apply cleanly step 0 ctx := testContext2(t, &ContextOpts{ - Config: testModule(t, "issue-5254/step-0"), + Config: m, ProviderResolver: ResourceProviderResolverFixed( map[string]ResourceProviderFactory{ "template": testProviderFuncFixed(p), @@ -8676,23 +8677,17 @@ func TestContext2Apply_issue5254(t *testing.T) { } // Write / Read plan to simulate running it through a Plan file - var buf bytes.Buffer - if err := WritePlan(plan, &buf); err != nil { - t.Fatalf("err: %s", err) - } - - planFromFile, err := ReadPlan(&buf) + ctxOpts, err := contextOptsForPlanViaFile(snap, state, plan) if err != nil { - t.Fatalf("err: %s", err) + t.Fatalf("failed to round-trip through planfile: %s", err) } - ctx, diags = planFromFile.Context(&ContextOpts{ - ProviderResolver: ResourceProviderResolverFixed( - map[string]ResourceProviderFactory{ - "template": testProviderFuncFixed(p), - }, - ), - }) + ctxOpts.ProviderResolver = ResourceProviderResolverFixed( + map[string]ResourceProviderFactory{ + "template": testProviderFuncFixed(p), + }, + ) + ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } @@ -8727,8 +8722,9 @@ func TestContext2Apply_targetedWithTaintedInState(t *testing.T) { p := testProvider("aws") p.DiffFn = testDiffFn p.ApplyFn = testApplyFn + m, snap := testModuleWithSnapshot(t, "apply-tainted-targets") ctx := testContext2(t, &ContextOpts{ - Config: testModule(t, "apply-tainted-targets"), + Config: m, ProviderResolver: ResourceProviderResolverFixed( map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -8739,7 +8735,7 @@ func TestContext2Apply_targetedWithTaintedInState(t *testing.T) { addrs.ManagedResourceMode, "aws_instance", "iambeingadded", ), }, - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -8754,7 +8750,7 @@ func TestContext2Apply_targetedWithTaintedInState(t *testing.T) { }, }, }, - }, + }), }) plan, diags := ctx.Plan() @@ -8763,24 +8759,17 @@ func TestContext2Apply_targetedWithTaintedInState(t *testing.T) { } // Write / Read plan to simulate running it through a Plan file - var buf bytes.Buffer - if err := WritePlan(plan, &buf); err != nil { - t.Fatalf("err: %s", err) - } - - planFromFile, err := ReadPlan(&buf) + ctxOpts, err := contextOptsForPlanViaFile(snap, ctx.State(), plan) if err != nil { - t.Fatalf("err: %s", err) + t.Fatalf("failed to round-trip through planfile: %s", err) } - ctx, diags = planFromFile.Context(&ContextOpts{ - Config: testModule(t, "apply-tainted-targets"), - ProviderResolver: ResourceProviderResolverFixed( - map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - ), - }) + ctxOpts.ProviderResolver = ResourceProviderResolverFixed( + map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + ) + ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } @@ -8829,7 +8818,7 @@ func TestContext2Apply_ignoreChangesCreate(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(p.String()) + t.Logf(legacyDiffComparisonString(p.Changes)) } state, diags := ctx.Apply() @@ -8881,7 +8870,7 @@ func TestContext2Apply_ignoreChangesWithDep(t *testing.T) { return nil, nil } } - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -8929,7 +8918,7 @@ func TestContext2Apply_ignoreChangesWithDep(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -8980,7 +8969,7 @@ func TestContext2Apply_ignoreChangesWildcard(t *testing.T) { if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(p.String()) + t.Logf(legacyDiffComparisonString(p.Changes)) } state, diags := ctx.Apply() @@ -9009,12 +8998,12 @@ aws_instance.foo: // https://github.com/hashicorp/terraform/issues/7378 func TestContext2Apply_destroyNestedModuleWithAttrsReferencingResource(t *testing.T) { - m := testModule(t, "apply-destroy-nested-module-with-attrs") + m, snap := testModuleWithSnapshot(t, "apply-destroy-nested-module-with-attrs") p := testProvider("null") p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - var state *State + var state *states.State var diags tfdiags.Diagnostics { ctx := testContext2(t, &ContextOpts{ @@ -9054,23 +9043,17 @@ func TestContext2Apply_destroyNestedModuleWithAttrsReferencingResource(t *testin t.Fatalf("destroy plan err: %s", diags.Err()) } - var buf bytes.Buffer - if err := WritePlan(plan, &buf); err != nil { - t.Fatalf("plan write err: %s", err) - } - - planFromFile, err := ReadPlan(&buf) + ctxOpts, err := contextOptsForPlanViaFile(snap, state, plan) if err != nil { - t.Fatalf("plan read err: %s", err) + t.Fatalf("failed to round-trip through planfile: %s", err) } - ctx, diags = planFromFile.Context(&ContextOpts{ - ProviderResolver: ResourceProviderResolverFixed( - map[string]ResourceProviderFactory{ - "null": testProviderFuncFixed(p), - }, - ), - }) + ctxOpts.ProviderResolver = ResourceProviderResolverFixed( + map[string]ResourceProviderFactory{ + "null": testProviderFuncFixed(p), + }, + ) + ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } @@ -9150,9 +9133,13 @@ func TestContext2Apply_dataDependsOn(t *testing.T) { t.Fatalf("diags: %s", diags.Err()) } - root := state.ModuleByPath(addrs.RootModuleInstance) - actual := root.Resources["data.null_data_source.read"].Primary.Attributes["foo"] - + root := state.Module(addrs.RootModuleInstance) + var attrs map[string]interface{} + err := json.Unmarshal(root.Resources["data.null_data_source.read"].Instances[addrs.NoKey].Current.AttrsJSON, &attrs) + if err != nil { + t.Fatal(err) + } + actual := attrs["foo"] expected := "APPLIED" if actual != expected { t.Fatalf("bad:\n%s", strings.TrimSpace(state.String())) @@ -9184,10 +9171,10 @@ func TestContext2Apply_terraformWorkspace(t *testing.T) { t.Fatalf("diags: %s", diags.Err()) } - actual := state.RootModule().Outputs["output"] - expected := "foo" + actual := state.RootModule().OutputValues["output"] + expected := cty.StringVal("foo") if actual == nil || actual.Value != expected { - t.Fatalf("bad: \n%s", actual) + t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected) } } @@ -9215,8 +9202,8 @@ func TestContext2Apply_multiRef(t *testing.T) { t.Fatalf("err: %s", diags.Err()) } - deps := state.Modules[0].Resources["aws_instance.other"].Dependencies - if len(deps) > 1 || deps[0] != "aws_instance.create" { + deps := state.Modules[""].Resources["aws_instance.other"].Instances[addrs.NoKey].Current.Dependencies + if len(deps) > 1 || deps[0].String() != "aws_instance.create" { t.Fatalf("expected 1 depends_on entry for aws_instance.create, got %q", deps) } } @@ -9247,7 +9234,7 @@ func TestContext2Apply_targetedModuleRecursive(t *testing.T) { t.Fatalf("err: %s", diags.Err()) } - mod := state.ModuleByPath( + mod := state.Module( addrs.RootModuleInstance.Child("child", addrs.NoKey).Child("subchild", addrs.NoKey), ) if mod == nil { @@ -9313,7 +9300,7 @@ func TestContext2Apply_destroyWithLocals(t *testing.T) { d, err := testDiffFn(info, s, c) return d, err } - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -9338,7 +9325,7 @@ func TestContext2Apply_destroyWithLocals(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -9434,7 +9421,7 @@ func TestContext2Apply_destroyWithProviders(t *testing.T) { p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -9456,7 +9443,7 @@ func TestContext2Apply_destroyWithProviders(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -9475,7 +9462,10 @@ func TestContext2Apply_destroyWithProviders(t *testing.T) { } // correct the state - s.Modules[2].Resources["aws_instance.child"].Provider = "provider.aws.bar" + s.Modules["module.mod.module.removed"].Resources["aws_instance.child"].ProviderConfig = addrs.ProviderConfig{ + Type: "aws", + Alias: "bar", + }.Absolute(addrs.RootModuleInstance) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatal(diags.Err()) @@ -9500,13 +9490,13 @@ func TestContext2Apply_providersFromState(t *testing.T) { for _, tc := range []struct { name string - state *State + state *states.State output string err bool }{ { name: "add implicit provider", - state: &State{ + state: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root"}, @@ -9521,7 +9511,7 @@ func TestContext2Apply_providersFromState(t *testing.T) { }, }, }, - }, + }), err: false, output: "", }, @@ -9529,7 +9519,7 @@ func TestContext2Apply_providersFromState(t *testing.T) { // an aliased provider must be in the config to remove a resource { name: "add aliased provider", - state: &State{ + state: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root"}, @@ -9544,7 +9534,7 @@ func TestContext2Apply_providersFromState(t *testing.T) { }, }, }, - }, + }), err: true, }, @@ -9552,7 +9542,7 @@ func TestContext2Apply_providersFromState(t *testing.T) { // allowed even without an alias { name: "add unaliased module provider", - state: &State{ + state: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "child"}, @@ -9567,7 +9557,7 @@ func TestContext2Apply_providersFromState(t *testing.T) { }, }, }, - }, + }), err: true, }, } { @@ -9607,7 +9597,7 @@ func TestContext2Apply_providersFromState(t *testing.T) { } func TestContext2Apply_plannedInterpolatedCount(t *testing.T) { - m := testModule(t, "apply-interpolated-count") + m, snap := testModuleWithSnapshot(t, "apply-interpolated-count") p := testProvider("aws") p.ApplyFn = testApplyFn @@ -9619,7 +9609,7 @@ func TestContext2Apply_plannedInterpolatedCount(t *testing.T) { }, ) - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -9634,7 +9624,7 @@ func TestContext2Apply_plannedInterpolatedCount(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -9650,21 +9640,15 @@ func TestContext2Apply_plannedInterpolatedCount(t *testing.T) { // We'll marshal and unmarshal the plan here, to ensure that we have // a clean new context as would be created if we separately ran // terraform plan -out=tfplan && terraform apply tfplan - var planBuf bytes.Buffer - err := WritePlan(plan, &planBuf) + ctxOpts, err := contextOptsForPlanViaFile(snap, ctx.State(), plan) if err != nil { - t.Fatalf("failed to write plan: %s", err) - } - plan, err = ReadPlan(&planBuf) - if err != nil { - t.Fatalf("failed to read plan: %s", err) + t.Fatalf("failed to round-trip through planfile: %s", err) } - ctx, diags = plan.Context(&ContextOpts{ - ProviderResolver: providerResolver, - }) + ctxOpts.ProviderResolver = providerResolver + ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { - t.Fatalf("failed to create context for plan: %s", diags.Err()) + t.Fatalf("err: %s", diags.Err()) } // Applying the plan should now succeed @@ -9675,7 +9659,7 @@ func TestContext2Apply_plannedInterpolatedCount(t *testing.T) { } func TestContext2Apply_plannedDestroyInterpolatedCount(t *testing.T) { - m := testModule(t, "plan-destroy-interpolated-count") + m, snap := testModuleWithSnapshot(t, "plan-destroy-interpolated-count") p := testProvider("aws") p.ApplyFn = testApplyFn @@ -9687,7 +9671,7 @@ func TestContext2Apply_plannedDestroyInterpolatedCount(t *testing.T) { }, ) - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -9715,7 +9699,7 @@ func TestContext2Apply_plannedDestroyInterpolatedCount(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -9732,22 +9716,16 @@ func TestContext2Apply_plannedDestroyInterpolatedCount(t *testing.T) { // We'll marshal and unmarshal the plan here, to ensure that we have // a clean new context as would be created if we separately ran // terraform plan -out=tfplan && terraform apply tfplan - var planBuf bytes.Buffer - err := WritePlan(plan, &planBuf) + ctxOpts, err := contextOptsForPlanViaFile(snap, ctx.State(), plan) if err != nil { - t.Fatalf("failed to write plan: %s", err) - } - plan, err = ReadPlan(&planBuf) - if err != nil { - t.Fatalf("failed to read plan: %s", err) + t.Fatalf("failed to round-trip through planfile: %s", err) } - ctx, diags = plan.Context(&ContextOpts{ - ProviderResolver: providerResolver, - Destroy: true, - }) + ctxOpts.ProviderResolver = providerResolver + ctxOpts.Destroy = true + ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { - t.Fatalf("failed to create context for plan: %s", diags.Err()) + t.Fatalf("err: %s", diags.Err()) } // Applying the plan should now succeed @@ -9770,7 +9748,7 @@ func TestContext2Apply_scaleInMultivarRef(t *testing.T) { }, ) - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -9795,15 +9773,15 @@ func TestContext2Apply_scaleInMultivarRef(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: providerResolver, State: s, Variables: InputValues{ - "count": { - Value: cty.NumberIntVal(0), + "instance_count": { + Value: cty.NumberIntVal(0), SourceType: ValueFromCaller, }, }, diff --git a/terraform/context_import.go b/terraform/context_import.go index 5b1aabc48..313e9094f 100644 --- a/terraform/context_import.go +++ b/terraform/context_import.go @@ -3,6 +3,7 @@ package terraform import ( "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" ) @@ -40,7 +41,7 @@ type ImportTarget struct { // Further, this operation also gracefully handles partial state. If during // an import there is a failure, all previously imported resources remain // imported. -func (c *Context) Import(opts *ImportOpts) (*State, tfdiags.Diagnostics) { +func (c *Context) Import(opts *ImportOpts) (*states.State, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // Hold a lock since we can modify our own state here @@ -78,8 +79,5 @@ func (c *Context) Import(opts *ImportOpts) (*State, tfdiags.Diagnostics) { return c.state, diags } - // Clean the state - c.state.prune() - return c.state, diags } diff --git a/terraform/context_import_test.go b/terraform/context_import_test.go index 70cde6702..2e6be4471 100644 --- a/terraform/context_import_test.go +++ b/terraform/context_import_test.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs/configschema" + "github.com/hashicorp/terraform/states" "github.com/zclconf/go-cty/cty" ) @@ -102,21 +103,22 @@ func TestContextImport_collision(t *testing.T) { }, ), - State: &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root"}, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - }, - }, + State: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "id": "bar", }, + Status: states.ObjectReady, }, - }, - }, + addrs.ProviderConfig{Type: "aws"}.Absolute(addrs.RootModuleInstance), + ) + }), }) p.ImportStateReturn = []*InstanceState{ @@ -595,21 +597,22 @@ func TestContextImport_moduleDiff(t *testing.T) { }, ), - State: &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root", "bar"}, - Resources: map[string]*ResourceState{ - "aws_instance.bar": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - }, - }, + State: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.Module{"bar"}.UnkeyedInstanceShim()), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "id": "bar", }, + Status: states.ObjectReady, }, - }, - }, + addrs.ProviderConfig{Type: "aws"}.Absolute(addrs.RootModuleInstance), + ) + }), }) p.ImportStateReturn = []*InstanceState{ @@ -652,21 +655,22 @@ func TestContextImport_moduleExisting(t *testing.T) { }, ), - State: &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root", "foo"}, - Resources: map[string]*ResourceState{ - "aws_instance.bar": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", - }, - }, + State: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.Module{"foo"}.UnkeyedInstanceShim()), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "id": "bar", }, + Status: states.ObjectReady, }, - }, - }, + addrs.ProviderConfig{Type: "aws"}.Absolute(addrs.RootModuleInstance), + ) + }), }) p.ImportStateReturn = []*InstanceState{ diff --git a/terraform/context_input_test.go b/terraform/context_input_test.go index 30cabde69..455d8288f 100644 --- a/terraform/context_input_test.go +++ b/terraform/context_input_test.go @@ -7,6 +7,10 @@ import ( "sync" "testing" + "github.com/hashicorp/terraform/addrs" + + "github.com/hashicorp/terraform/states" + "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/configs/configschema" @@ -681,39 +685,37 @@ func TestContext2Input_varPartiallyComputed(t *testing.T) { }, }, UIInput: input, - State: &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "i-abc123", - Attributes: map[string]string{ - "id": "i-abc123", - }, - }, - }, + State: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "id": "i-abc123", }, + Status: states.ObjectReady, }, - &ModuleState{ - Path: append(rootModulePath, "child"), - Resources: map[string]*ResourceState{ - "aws_instance.mod": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "i-bcd345", - Attributes: map[string]string{ - "id": "i-bcd345", - "value": "one,i-abc123", - }, - }, - }, + addrs.ProviderConfig{Type: "aws"}.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "mode", + }.Instance(addrs.NoKey).Absolute(addrs.Module{"child"}.UnkeyedInstanceShim()), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "id": "i-bcd345", + "value": "one,i-abc123", }, + Status: states.ObjectReady, }, - }, - }, + addrs.ProviderConfig{Type: "aws"}.Absolute(addrs.RootModuleInstance), + ) + }), }) if diags := ctx.Input(InputModeStd); diags.HasErrors() { @@ -850,26 +852,25 @@ func TestContext2Input_dataSourceRequiresRefresh(t *testing.T) { } p.ReadDataDiffFn = testDataDiffFn - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "data.null_data_source.bar": &ResourceState{ - Type: "null_data_source", - Primary: &InstanceState{ - ID: "-", - Attributes: map[string]string{ - "foo.#": "1", - "foo.0": "a", - // foo.1 exists in the data source, but needs to be refreshed. - }, - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "null_data_source", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "id": "-", + "foo.#": "1", + "foo.0": "a", + // foo.1 exists in the data source, but needs to be refreshed. }, + Status: states.ObjectReady, }, - }, - } + addrs.ProviderConfig{Type: "null"}.Absolute(addrs.RootModuleInstance), + ) + }) ctx := testContext2(t, &ContextOpts{ Config: m, diff --git a/terraform/context_plan_test.go b/terraform/context_plan_test.go index a898f652c..e395e02c5 100644 --- a/terraform/context_plan_test.go +++ b/terraform/context_plan_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/addrs" @@ -38,15 +39,15 @@ func TestContext2Plan_basic(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - if len(plan.Diff.RootModule().Resources) < 2 { - t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources) + if l := len(plan.Changes.Resources); l < 2 { + t.Fatalf("wrong number of resources %d; want fewer than two\n%s", l, spew.Sdump(plan.Changes.Resources)) } if !reflect.DeepEqual(plan.ProviderSHA256s, ctx.providerSHA256s) { t.Errorf("wrong ProviderSHA256s %#v; want %#v", plan.ProviderSHA256s, ctx.providerSHA256s) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -58,7 +59,7 @@ func TestContext2Plan_createBefore_deposed(t *testing.T) { p := testProvider("aws") p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root"}, @@ -75,7 +76,7 @@ func TestContext2Plan_createBefore_deposed(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -92,7 +93,7 @@ func TestContext2Plan_createBefore_deposed(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(` DIFF: @@ -133,7 +134,7 @@ func TestContext2Plan_createBefore_maintainRoot(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(` DIFF: @@ -175,7 +176,7 @@ func TestContext2Plan_emptyDiff(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanEmptyStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -200,7 +201,7 @@ func TestContext2Plan_escapedVar(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanEscapedVarStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -225,7 +226,7 @@ func TestContext2Plan_minimal(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanEmptyStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -250,7 +251,7 @@ func TestContext2Plan_modules(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanModulesStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -287,7 +288,7 @@ func TestContext2Plan_moduleCycle(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanModuleCycleStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -314,7 +315,7 @@ func TestContext2Plan_moduleDeadlock(t *testing.T) { t.Fatalf("err: %s", err) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(` DIFF: @@ -351,7 +352,7 @@ func TestContext2Plan_moduleInput(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanModuleInputStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -376,7 +377,7 @@ func TestContext2Plan_moduleInputComputed(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanModuleInputComputedStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -407,7 +408,7 @@ func TestContext2Plan_moduleInputFromVar(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanModuleInputVarStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -444,7 +445,7 @@ func TestContext2Plan_moduleMultiVar(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanModuleMultiVarStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -455,7 +456,7 @@ func TestContext2Plan_moduleOrphans(t *testing.T) { m := testModule(t, "plan-modules-remove") p := testProvider("aws") p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "child"}, @@ -469,7 +470,7 @@ func TestContext2Plan_moduleOrphans(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -485,7 +486,7 @@ func TestContext2Plan_moduleOrphans(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanModuleOrphansStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -498,7 +499,7 @@ func TestContext2Plan_moduleOrphansWithProvisioner(t *testing.T) { p := testProvider("aws") pr := testProvisioner() p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root"}, @@ -534,7 +535,7 @@ func TestContext2Plan_moduleOrphansWithProvisioner(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -553,7 +554,7 @@ func TestContext2Plan_moduleOrphansWithProvisioner(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(` DIFF: @@ -810,7 +811,7 @@ func TestContext2Plan_moduleProviderVar(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanModuleProviderVarStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -835,7 +836,7 @@ func TestContext2Plan_moduleVar(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanModuleVarStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -917,7 +918,7 @@ func TestContext2Plan_moduleVarComputed(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanModuleVarComputedStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -945,7 +946,7 @@ func TestContext2Plan_nil(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -959,7 +960,7 @@ func TestContext2Plan_nil(t *testing.T) { }, }, }, - }, + }), }) plan, diags := ctx.Plan() @@ -967,8 +968,8 @@ func TestContext2Plan_nil(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - if len(plan.Diff.RootModule().Resources) != 0 { - t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources) + if len(plan.Changes.Resources) != 0 { + t.Fatalf("bad: %#v", plan.Changes.Resources) } } @@ -983,7 +984,7 @@ func TestContext2Plan_preventDestroy_bad(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -997,15 +998,17 @@ func TestContext2Plan_preventDestroy_bad(t *testing.T) { }, }, }, - }, + }), }) plan, err := ctx.Plan() expectedErr := "aws_instance.foo has lifecycle.prevent_destroy" if !strings.Contains(fmt.Sprintf("%s", err), expectedErr) { - t.Fatalf("expected err would contain %q\nerr: %s\nplan: %s", - expectedErr, err, plan) + if plan != nil { + t.Logf(legacyDiffComparisonString(plan.Changes)) + } + t.Fatalf("expected err would contain %q\nerr: %s", expectedErr, err) } } @@ -1020,7 +1023,7 @@ func TestContext2Plan_preventDestroy_good(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1034,7 +1037,7 @@ func TestContext2Plan_preventDestroy_good(t *testing.T) { }, }, }, - }, + }), }) plan, diags := ctx.Plan() @@ -1042,8 +1045,8 @@ func TestContext2Plan_preventDestroy_good(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - if !plan.Diff.Empty() { - t.Fatalf("Expected empty plan, got %s", plan.String()) + if !plan.Changes.Empty() { + t.Fatalf("Expected empty plan, got %s", legacyDiffComparisonString(plan.Changes)) } } @@ -1058,7 +1061,7 @@ func TestContext2Plan_preventDestroy_countBad(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1078,15 +1081,17 @@ func TestContext2Plan_preventDestroy_countBad(t *testing.T) { }, }, }, - }, + }), }) plan, err := ctx.Plan() expectedErr := "aws_instance.foo[1] has lifecycle.prevent_destroy" if !strings.Contains(fmt.Sprintf("%s", err), expectedErr) { - t.Fatalf("expected err would contain %q\nerr: %s\nplan: %s", - expectedErr, err, plan) + if plan != nil { + t.Logf(legacyDiffComparisonString(plan.Changes)) + } + t.Fatalf("expected err would contain %q\nerr: %s", expectedErr, err) } } @@ -1111,7 +1116,7 @@ func TestContext2Plan_preventDestroy_countGood(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1131,7 +1136,7 @@ func TestContext2Plan_preventDestroy_countGood(t *testing.T) { }, }, }, - }, + }), }) plan, diags := ctx.Plan() @@ -1139,8 +1144,8 @@ func TestContext2Plan_preventDestroy_countGood(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - if plan.Diff.Empty() { - t.Fatalf("Expected non-empty plan, got %s", plan.String()) + if plan.Changes.Empty() { + t.Fatalf("Expected non-empty plan, got %s", legacyDiffComparisonString(plan.Changes)) } } @@ -1165,7 +1170,7 @@ func TestContext2Plan_preventDestroy_countGoodNoChange(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1183,7 +1188,7 @@ func TestContext2Plan_preventDestroy_countGoodNoChange(t *testing.T) { }, }, }, - }, + }), }) plan, diags := ctx.Plan() @@ -1191,8 +1196,8 @@ func TestContext2Plan_preventDestroy_countGoodNoChange(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - if !plan.Diff.Empty() { - t.Fatalf("Expected empty plan, got %s", plan.String()) + if !plan.Changes.Empty() { + t.Fatalf("Expected empty plan, got %s", legacyDiffComparisonString(plan.Changes)) } } @@ -1207,7 +1212,7 @@ func TestContext2Plan_preventDestroy_destroyPlan(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1221,16 +1226,18 @@ func TestContext2Plan_preventDestroy_destroyPlan(t *testing.T) { }, }, }, - }, + }), Destroy: true, }) - plan, err := ctx.Plan() + plan, diags := ctx.Plan() expectedErr := "aws_instance.foo has lifecycle.prevent_destroy" - if !strings.Contains(fmt.Sprintf("%s", err), expectedErr) { - t.Fatalf("expected err would contain %q\nerr: %s\nplan: %s", - expectedErr, err, plan) + if !strings.Contains(fmt.Sprintf("%s", diags.Err()), expectedErr) { + if plan != nil { + t.Logf(legacyDiffComparisonString(plan.Changes)) + } + t.Fatalf("expected err would contain %q\nerr: %s", expectedErr, diags.Err()) } } @@ -1275,7 +1282,7 @@ func TestContext2Plan_computed(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanComputedStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -1319,33 +1326,29 @@ func TestContext2Plan_computedDataResource(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - if got := len(plan.Diff.Modules); got != 1 { - t.Fatalf("got %d modules; want 1", got) - } - - moduleDiff := plan.Diff.Modules[0] - - if _, ok := moduleDiff.Resources["aws_instance.foo"]; !ok { + if rc := plan.Changes.ResourceInstance(addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "foo"}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)); rc == nil { t.Fatalf("missing diff for aws_instance.foo") } - iDiff, ok := moduleDiff.Resources["data.aws_vpc.bar"] - if !ok { + rcs := plan.Changes.ResourceInstance(addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "aws_vpc", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)) + if rcs == nil { t.Fatalf("missing diff for data.aws_vpc.bar") } - expectedDiff := &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "id": { - NewComputed: true, - RequiresNew: true, - Type: DiffAttrOutput, - }, - }, + rc, err := rcs.Decode(cty.Object(map[string]cty.Type{"id": cty.String})) + if err != nil { + t.Fatal(err) } - if same, _ := expectedDiff.Same(iDiff); !same { + want := cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }) + if got := rc.After; !got.RawEquals(want) { t.Fatalf( - "incorrect diff for data.aws_vpc.bar\ngot: %#v\nwant: %#v", - iDiff, expectedDiff, + "incorrect new value for data.aws_vpc.bar\ngot: %#v\nwant: %#v", + got, want, ) } } @@ -1387,17 +1390,16 @@ func TestContext2Plan_computedDataCountResource(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - if got := len(plan.Diff.Modules); got != 1 { - t.Fatalf("got %d modules; want 1", got) - } - - moduleDiff := plan.Diff.Modules[0] - // make sure we created 3 "bar"s for i := 0; i < 3; i++ { - resource := fmt.Sprintf("data.aws_vpc.bar.%d", i) - if _, ok := moduleDiff.Resources[resource]; !ok { - t.Fatalf("missing diff for %s", resource) + addr := addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "aws_vpc", + Name: "bar", + }.Instance(addrs.IntKey(i)).Absolute(addrs.RootModuleInstance) + + if rcs := plan.Changes.ResourceInstance(addr); rcs == nil { + t.Fatalf("missing changes for %s", addr) } } } @@ -1420,17 +1422,16 @@ func TestContext2Plan_localValueCount(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - if got := len(plan.Diff.Modules); got != 1 { - t.Fatalf("got %d modules; want 1", got) - } - - moduleDiff := plan.Diff.Modules[0] - - // make sure we created 3 "bar"s + // make sure we created 3 "foo"s for i := 0; i < 3; i++ { - resource := fmt.Sprintf("test_resource.foo.%d", i) - if _, ok := moduleDiff.Resources[resource]; !ok { - t.Fatalf("missing diff for %s", resource) + addr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "foo", + }.Instance(addrs.IntKey(i)).Absolute(addrs.RootModuleInstance) + + if rcs := plan.Changes.ResourceInstance(addr); rcs == nil { + t.Fatalf("missing changes for %s", addr) } } } @@ -1482,7 +1483,7 @@ func TestContext2Plan_dataSourceTypeMismatch(t *testing.T) { ctx := testContext2(t, &ContextOpts{ Config: m, // Pretend like we ran a Refresh and the AZs data source was populated. - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1501,7 +1502,7 @@ func TestContext2Plan_dataSourceTypeMismatch(t *testing.T) { }, }, }, - }, + }), ProviderResolver: ResourceProviderResolverFixed( map[string]ResourceProviderFactory{ "aws": testProviderFuncFixed(p), @@ -1521,113 +1522,121 @@ func TestContext2Plan_dataSourceTypeMismatch(t *testing.T) { } func TestContext2Plan_dataResourceBecomesComputed(t *testing.T) { - m := testModule(t, "plan-data-resource-becomes-computed") - p := testProvider("aws") + t.Fatal("not yet updated for new provider interface") + /* + m := testModule(t, "plan-data-resource-becomes-computed") + p := testProvider("aws") - p.GetSchemaReturn = &ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ - "aws_instance": { - Attributes: map[string]*configschema.Attribute{ - "foo": {Type: cty.String, Optional: true}, - "computed": {Type: cty.String, Computed: true}, + p.GetSchemaReturn = &ProviderSchema{ + ResourceTypes: map[string]*configschema.Block{ + "aws_instance": { + Attributes: map[string]*configschema.Attribute{ + "foo": {Type: cty.String, Optional: true}, + "computed": {Type: cty.String, Computed: true}, + }, }, }, - }, - DataSources: map[string]*configschema.Block{ - "aws_data_resource": { - Attributes: map[string]*configschema.Attribute{ - "foo": {Type: cty.String, Optional: true}, + DataSources: map[string]*configschema.Block{ + "aws_data_resource": { + Attributes: map[string]*configschema.Attribute{ + "foo": {Type: cty.String, Optional: true}, + }, }, }, - }, - } - p.DiffFn = func(info *InstanceInfo, state *InstanceState, config *ResourceConfig) (*InstanceDiff, error) { - if info.Type != "aws_instance" { - t.Fatalf("don't know how to diff %s", info.Id) - return nil, nil } + p.DiffFn = func(info *InstanceInfo, state *InstanceState, config *ResourceConfig) (*InstanceDiff, error) { + if info.Type != "aws_instance" { + t.Fatalf("don't know how to diff %s", info.Id) + return nil, nil + } - return &InstanceDiff{ + return &InstanceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "computed": &ResourceAttrDiff{ + Old: "", + New: "", + NewComputed: true, + }, + }, + }, nil + } + p.ReadDataDiffReturn = &InstanceDiff{ Attributes: map[string]*ResourceAttrDiff{ - "computed": &ResourceAttrDiff{ + "foo": &ResourceAttrDiff{ Old: "", New: "", NewComputed: true, }, }, - }, nil - } - p.ReadDataDiffReturn = &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "", - New: "", - NewComputed: true, - }, - }, - } + } - ctx := testContext2(t, &ContextOpts{ - Config: m, - ProviderResolver: ResourceProviderResolverFixed( - map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - ), - State: &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "data.aws_data_resource.foo": &ResourceState{ - Type: "aws_data_resource", - Primary: &InstanceState{ - ID: "i-abc123", - Attributes: map[string]string{ - "id": "i-abc123", - "value": "baz", + ctx := testContext2(t, &ContextOpts{ + Config: m, + ProviderResolver: ResourceProviderResolverFixed( + map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + ), + State: mustShimLegacyState(&State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "data.aws_data_resource.foo": &ResourceState{ + Type: "aws_data_resource", + Primary: &InstanceState{ + ID: "i-abc123", + Attributes: map[string]string{ + "id": "i-abc123", + "value": "baz", + }, }, }, }, }, }, - }, - }, - }) + }), + }) - plan, diags := ctx.Plan() - if diags.HasErrors() { - t.Fatalf("unexpected errors: %s", diags.Err()) - } + plan, diags := ctx.Plan() + if diags.HasErrors() { + t.Fatalf("unexpected errors: %s", diags.Err()) + } - if got := len(plan.Diff.Modules); got != 1 { - t.Fatalf("got %d modules; want 1", got) - } + if !p.ReadDataDiffCalled { + t.Fatal("ReadDataDiff wasn't called, but should've been") + } + if got, want := p.ReadDataDiffInfo.Id, "data.aws_data_resource.foo"; got != want { + t.Fatalf("ReadDataDiff info id is %s; want %s", got, want) + } - if !p.ReadDataDiffCalled { - t.Fatal("ReadDataDiff wasn't called, but should've been") - } - if got, want := p.ReadDataDiffInfo.Id, "data.aws_data_resource.foo"; got != want { - t.Fatalf("ReadDataDiff info id is %s; want %s", got, want) - } + rcs := plan.Changes.ResourceInstance(addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "aws_data_resource", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)) + if rcs == nil { + t.Fatalf("missing diff for data.aws_data_resource.foo") + } - moduleDiff := plan.Diff.Modules[0] + schema := p.GetSchemaReturn + rtSchema := schema.DataSources["aws_data_resource"] - iDiff, ok := moduleDiff.Resources["data.aws_data_resource.foo"] - if !ok { - t.Fatalf("missing diff for data.aws_data_resource.foo") - } + rc, err := rcs.Decode(rtSchema.ImpliedType()) + if err != nil { + t.Fatal(err) + } - // This is added by the diff but we want to verify that we got - // the same diff as above minus the dynamic stuff. - delete(iDiff.Attributes, "id") - - if same, _ := p.ReadDataDiffReturn.Same(iDiff); !same { - t.Fatalf( - "incorrect diff for data.data_resource.foo\ngot: %#v\nwant: %#v", - iDiff, p.ReadDataDiffReturn, - ) - } + // FIXME: Update this once p.ReadDataDiffReturn is replaced with its + // new equivalent in the new provider interface, and then compare + // the cty.Values directly. + if same, _ := p.ReadDataDiffReturn.Same(iDiff); !same { + t.Fatalf( + "incorrect diff for data.data_resource.foo\ngot: %#v\nwant: %#v", + iDiff, p.ReadDataDiffReturn, + ) + } + */ } func TestContext2Plan_computedList(t *testing.T) { @@ -1661,7 +1670,7 @@ func TestContext2Plan_computedList(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanComputedListStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -1688,7 +1697,7 @@ func TestContext2Plan_computedMultiIndex(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanComputedMultiIndexStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -1713,11 +1722,11 @@ func TestContext2Plan_count(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - if len(plan.Diff.RootModule().Resources) < 6 { - t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources) + if len(plan.Changes.Resources) < 6 { + t.Fatalf("bad: %#v", plan.Changes.Resources) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanCountStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -1783,7 +1792,7 @@ func TestContext2Plan_countModuleStatic(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(` DIFF: @@ -1819,7 +1828,7 @@ func TestContext2Plan_countModuleStaticGrandchild(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(` DIFF: @@ -1855,7 +1864,7 @@ func TestContext2Plan_countIndex(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanCountIndexStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -1886,7 +1895,7 @@ func TestContext2Plan_countVar(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanCountVarStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -1921,7 +1930,7 @@ func TestContext2Plan_countZero(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanCountZeroStr) if actual != expected { t.Logf("expected:\n%s", expected) @@ -1947,7 +1956,7 @@ func TestContext2Plan_countOneIndex(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanCountOneIndexStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -1958,7 +1967,7 @@ func TestContext2Plan_countDecreaseToOne(t *testing.T) { m := testModule(t, "plan-count-dec") p := testProvider("aws") p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1988,7 +1997,7 @@ func TestContext2Plan_countDecreaseToOne(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2004,7 +2013,7 @@ func TestContext2Plan_countDecreaseToOne(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanCountDecreaseStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -2015,7 +2024,7 @@ func TestContext2Plan_countIncreaseFromNotSet(t *testing.T) { m := testModule(t, "plan-count-inc") p := testProvider("aws") p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2033,7 +2042,7 @@ func TestContext2Plan_countIncreaseFromNotSet(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2049,7 +2058,7 @@ func TestContext2Plan_countIncreaseFromNotSet(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanCountIncreaseStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -2060,7 +2069,7 @@ func TestContext2Plan_countIncreaseFromOne(t *testing.T) { m := testModule(t, "plan-count-inc") p := testProvider("aws") p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2078,7 +2087,7 @@ func TestContext2Plan_countIncreaseFromOne(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2094,7 +2103,7 @@ func TestContext2Plan_countIncreaseFromOne(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanCountIncreaseFromOneStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -2110,7 +2119,7 @@ func TestContext2Plan_countIncreaseFromOneCorrupted(t *testing.T) { m := testModule(t, "plan-count-inc") p := testProvider("aws") p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2138,7 +2147,7 @@ func TestContext2Plan_countIncreaseFromOneCorrupted(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2154,7 +2163,7 @@ func TestContext2Plan_countIncreaseFromOneCorrupted(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanCountIncreaseFromOneCorruptedStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -2184,7 +2193,7 @@ func TestContext2Plan_countIncreaseWithSplatReference(t *testing.T) { } p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2228,7 +2237,7 @@ func TestContext2Plan_countIncreaseWithSplatReference(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2244,7 +2253,7 @@ func TestContext2Plan_countIncreaseWithSplatReference(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(` DIFF: @@ -2279,7 +2288,7 @@ func TestContext2Plan_destroy(t *testing.T) { m := testModule(t, "plan-destroy") p := testProvider("aws") p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2299,7 +2308,7 @@ func TestContext2Plan_destroy(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2316,11 +2325,11 @@ func TestContext2Plan_destroy(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - if len(plan.Diff.RootModule().Resources) != 2 { - t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources) + if len(plan.Changes.Resources) != 2 { + t.Fatalf("bad: %#v", plan.Changes.Resources) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanDestroyStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -2331,7 +2340,7 @@ func TestContext2Plan_moduleDestroy(t *testing.T) { m := testModule(t, "plan-module-destroy") p := testProvider("aws") p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2356,7 +2365,7 @@ func TestContext2Plan_moduleDestroy(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2373,7 +2382,7 @@ func TestContext2Plan_moduleDestroy(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanModuleDestroyStr) if actual != expected { t.Fatalf("bad:\n%s\n\nexpected:\n\n%s", actual, expected) @@ -2385,7 +2394,7 @@ func TestContext2Plan_moduleDestroyCycle(t *testing.T) { m := testModule(t, "plan-module-destroy-gh-1835") p := testProvider("aws") p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "a_module"}, @@ -2410,7 +2419,7 @@ func TestContext2Plan_moduleDestroyCycle(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2427,7 +2436,7 @@ func TestContext2Plan_moduleDestroyCycle(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanModuleDestroyCycleStr) if actual != expected { t.Fatalf("bad:\n%s\n\nexpected:\n\n%s", actual, expected) @@ -2438,7 +2447,7 @@ func TestContext2Plan_moduleDestroyMultivar(t *testing.T) { m := testModule(t, "plan-module-destroy-multivar") p := testProvider("aws") p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2462,7 +2471,7 @@ func TestContext2Plan_moduleDestroyMultivar(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2479,7 +2488,7 @@ func TestContext2Plan_moduleDestroyMultivar(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanModuleDestroyMultivarStr) if actual != expected { t.Fatalf("bad:\n%s\n\nexpected:\n\n%s", actual, expected) @@ -2521,7 +2530,7 @@ func TestContext2Plan_pathVar(t *testing.T) { t.Fatalf("err: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanPathVarStr) // Warning: this ordering REALLY matters for this test. The @@ -2541,7 +2550,7 @@ func TestContext2Plan_pathVar(t *testing.T) { func TestContext2Plan_diffVar(t *testing.T) { m := testModule(t, "plan-diffvar") p := testProvider("aws") - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2558,7 +2567,7 @@ func TestContext2Plan_diffVar(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2592,7 +2601,7 @@ func TestContext2Plan_diffVar(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanDiffVarStr) if actual != expected { t.Fatalf("actual:\n%s\n\nexpected:\n%s", actual, expected) @@ -2657,7 +2666,7 @@ func TestContext2Plan_orphan(t *testing.T) { m := testModule(t, "plan-orphan") p := testProvider("aws") p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2671,7 +2680,7 @@ func TestContext2Plan_orphan(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2687,7 +2696,7 @@ func TestContext2Plan_orphan(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanOrphanStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -2719,7 +2728,7 @@ func TestContext2Plan_state(t *testing.T) { m := testModule(t, "plan-good") p := testProvider("aws") p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2733,7 +2742,7 @@ func TestContext2Plan_state(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2749,11 +2758,11 @@ func TestContext2Plan_state(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - if len(plan.Diff.RootModule().Resources) < 2 { - t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources) + if len(plan.Changes.Resources) < 2 { + t.Fatalf("bad: %#v", plan.Changes.Resources) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanStateStr) if actual != expected { t.Fatalf("bad:\n%s\n\nexpected:\n\n%s", actual, expected) @@ -2764,7 +2773,7 @@ func TestContext2Plan_taint(t *testing.T) { m := testModule(t, "plan-taint") p := testProvider("aws") p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2786,7 +2795,7 @@ func TestContext2Plan_taint(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2802,7 +2811,7 @@ func TestContext2Plan_taint(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanTaintStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -2824,7 +2833,7 @@ func TestContext2Plan_taintIgnoreChanges(t *testing.T) { p.ApplyFn = testApplyFn p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2843,7 +2852,7 @@ func TestContext2Plan_taintIgnoreChanges(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2859,7 +2868,7 @@ func TestContext2Plan_taintIgnoreChanges(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanTaintIgnoreChangesStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) @@ -2871,7 +2880,7 @@ func TestContext2Plan_taintDestroyInterpolatedCountRace(t *testing.T) { m := testModule(t, "plan-taint-interpolated-count") p := testProvider("aws") p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -2894,7 +2903,7 @@ func TestContext2Plan_taintDestroyInterpolatedCountRace(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -2911,7 +2920,7 @@ func TestContext2Plan_taintDestroyInterpolatedCountRace(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(` DIFF: @@ -2956,7 +2965,7 @@ func TestContext2Plan_targeted(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(` DIFF: @@ -2996,7 +3005,7 @@ func TestContext2Plan_targetedCrossModule(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(` DIFF: @@ -3052,7 +3061,7 @@ func TestContext2Plan_targetedModuleWithProvider(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(` DIFF: @@ -3079,7 +3088,7 @@ func TestContext2Plan_targetedOrphan(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -3099,7 +3108,7 @@ func TestContext2Plan_targetedOrphan(t *testing.T) { }, }, }, - }, + }), Destroy: true, Targets: []addrs.Targetable{ addrs.RootModuleInstance.Resource( @@ -3113,7 +3122,7 @@ func TestContext2Plan_targetedOrphan(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(`DIFF: DESTROY: aws_instance.orphan @@ -3142,7 +3151,7 @@ func TestContext2Plan_targetedModuleOrphan(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "child"}, @@ -3162,7 +3171,7 @@ func TestContext2Plan_targetedModuleOrphan(t *testing.T) { }, }, }, - }, + }), Destroy: true, Targets: []addrs.Targetable{ addrs.RootModuleInstance.Child("child", addrs.NoKey).Resource( @@ -3176,7 +3185,7 @@ func TestContext2Plan_targetedModuleOrphan(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(`DIFF: module.child: @@ -3219,7 +3228,7 @@ func TestContext2Plan_targetedModuleUntargetedVariable(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(` DIFF: @@ -3290,14 +3299,14 @@ func TestContext2Plan_targetedOverTen(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: resources, }, }, - }, + }), Targets: []addrs.Targetable{ addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.IntKey(1), @@ -3310,7 +3319,7 @@ func TestContext2Plan_targetedOverTen(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) sort.Strings(expectedState) expected := strings.TrimSpace(` DIFF: @@ -3419,7 +3428,7 @@ func TestContext2Plan_ignoreChanges(t *testing.T) { } p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -3434,7 +3443,7 @@ func TestContext2Plan_ignoreChanges(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -3456,11 +3465,11 @@ func TestContext2Plan_ignoreChanges(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - if len(plan.Diff.RootModule().Resources) < 1 { - t.Fatalf("bad: %#v", plan.Diff.RootModule().Resources) + if len(plan.Changes.Resources) < 1 { + t.Fatalf("bad: %#v", plan.Changes.Resources) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanIgnoreChangesStr) if actual != expected { t.Fatalf("bad:\n%s\n\nexpected\n\n%s", actual, expected) @@ -3482,7 +3491,7 @@ func TestContext2Plan_ignoreChangesWildcard(t *testing.T) { } p.DiffFn = testDiffFn - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -3500,7 +3509,7 @@ func TestContext2Plan_ignoreChangesWildcard(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -3526,11 +3535,11 @@ func TestContext2Plan_ignoreChangesWildcard(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - if len(plan.Diff.RootModule().Resources) > 0 { - t.Fatalf("unexpected resource diffs in root module: %s", spew.Sdump(plan.Diff.RootModule().Resources)) + if len(plan.Changes.Resources) > 0 { + t.Fatalf("unexpected resource diffs in root module: %s", spew.Sdump(plan.Changes.Resources)) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanIgnoreChangesWildcardStr) if actual != expected { t.Fatalf("bad:\n%s\n\nexpected\n\n%s", actual, expected) @@ -3643,7 +3652,7 @@ func TestContext2Plan_computedValueInMap(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanComputedValueInMap) if actual != expected { t.Fatalf("bad:\n%s\n\nexpected\n\n%s", actual, expected) @@ -3686,7 +3695,7 @@ func TestContext2Plan_moduleVariableFromSplat(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(testTerraformPlanModuleVariableFromSplat) if actual != expected { t.Fatalf("bad:\n%s\n\nexpected\n\n%s", actual, expected) @@ -3730,23 +3739,29 @@ func TestContext2Plan_createBeforeDestroy_depends_datasource(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - if got := len(plan.Diff.Modules); got != 1 { - t.Fatalf("got %d modules; want 1", got) - } + for i := 0; i < 2; i++ { + { + addr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.IntKey(i)).Absolute(addrs.RootModuleInstance) - moduleDiff := plan.Diff.Modules[0] + if rcs := plan.Changes.ResourceInstance(addr); rcs == nil { + t.Fatalf("missing changes for %s", addr) + } + } + { + addr := addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "aws_vpc", + Name: "bar", + }.Instance(addrs.IntKey(i)).Absolute(addrs.RootModuleInstance) - if _, ok := moduleDiff.Resources["aws_instance.foo.0"]; !ok { - t.Fatalf("missing diff for aws_instance.foo.0") - } - if _, ok := moduleDiff.Resources["aws_instance.foo.1"]; !ok { - t.Fatalf("missing diff for aws_instance.foo.1") - } - if _, ok := moduleDiff.Resources["data.aws_vpc.bar.0"]; !ok { - t.Fatalf("missing diff for data.aws_vpc.bar.0") - } - if _, ok := moduleDiff.Resources["data.aws_vpc.bar.1"]; !ok { - t.Fatalf("missing diff for data.aws_vpc.bar.1") + if rcs := plan.Changes.ResourceInstance(addr); rcs == nil { + t.Fatalf("missing changes for %s", addr) + } + } } } @@ -3779,12 +3794,20 @@ func TestContext2Plan_listOrder(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - rDiffs := plan.Diff.Modules[0].Resources - rDiffA := rDiffs["aws_instance.a"] - rDiffB := rDiffs["aws_instance.b"] + changes := plan.Changes + rDiffA := changes.ResourceInstance(addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "a", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)) + rDiffB := changes.ResourceInstance(addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "b", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)) - if !rDiffA.Equal(rDiffB) { - t.Fatal("aws_instance.a and aws_instance.b diffs should match:\n", plan) + if !cmp.Equal(rDiffA, rDiffB) { + t.Fatal("aws_instance.a and aws_instance.b diffs should match:\n", legacyDiffComparisonString(plan.Changes)) } } @@ -3813,7 +3836,7 @@ func TestContext2Plan_ignoreChangesWithFlatmaps(t *testing.T) { }, }, } - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -3835,7 +3858,7 @@ func TestContext2Plan_ignoreChangesWithFlatmaps(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -3851,7 +3874,7 @@ func TestContext2Plan_ignoreChangesWithFlatmaps(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.Diff.String()) + actual := strings.TrimSpace(legacyDiffComparisonString(plan.Changes)) expected := strings.TrimSpace(testTFPlanDiffIgnoreChangesWithFlatmaps) if actual != expected { t.Fatalf("bad:\n%s\n\nexpected\n\n%s", actual, expected) @@ -3870,7 +3893,7 @@ func TestContext2Plan_resourceNestedCount(t *testing.T) { p.RefreshFn = func(i *InstanceInfo, is *InstanceState) (*InstanceState, error) { return is, nil } - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -3898,7 +3921,7 @@ func TestContext2Plan_resourceNestedCount(t *testing.T) { "aws_instance.bar.0": &ResourceState{ Type: "aws_instance", Provider: "provider.aws", - Dependencies: []string{"aws_instance.foo.*"}, + Dependencies: []string{"aws_instance.foo"}, Primary: &InstanceState{ ID: "bar0", Attributes: map[string]string{ @@ -3909,7 +3932,7 @@ func TestContext2Plan_resourceNestedCount(t *testing.T) { "aws_instance.bar.1": &ResourceState{ Type: "aws_instance", Provider: "provider.aws", - Dependencies: []string{"aws_instance.foo.*"}, + Dependencies: []string{"aws_instance.foo"}, Primary: &InstanceState{ ID: "bar1", Attributes: map[string]string{ @@ -3920,7 +3943,7 @@ func TestContext2Plan_resourceNestedCount(t *testing.T) { "aws_instance.baz.0": &ResourceState{ Type: "aws_instance", Provider: "provider.aws", - Dependencies: []string{"aws_instance.bar.*"}, + Dependencies: []string{"aws_instance.bar"}, Primary: &InstanceState{ ID: "baz0", Attributes: map[string]string{ @@ -3931,7 +3954,7 @@ func TestContext2Plan_resourceNestedCount(t *testing.T) { "aws_instance.baz.1": &ResourceState{ Type: "aws_instance", Provider: "provider.aws", - Dependencies: []string{"aws_instance.bar.*"}, + Dependencies: []string{"aws_instance.bar"}, Primary: &InstanceState{ ID: "baz1", Attributes: map[string]string{ @@ -3942,7 +3965,7 @@ func TestContext2Plan_resourceNestedCount(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -3968,7 +3991,7 @@ func TestContext2Plan_resourceNestedCount(t *testing.T) { t.Fatalf("plan errors: %s", diags.Err()) } - actual := strings.TrimSpace(plan.String()) + actual := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), plan.Changes)) expected := strings.TrimSpace(` DIFF: diff --git a/terraform/context_refresh_test.go b/terraform/context_refresh_test.go index 8ddb69361..133f5b5e2 100644 --- a/terraform/context_refresh_test.go +++ b/terraform/context_refresh_test.go @@ -15,57 +15,60 @@ import ( ) func TestContext2Refresh(t *testing.T) { - p := testProvider("aws") - m := testModule(t, "refresh-basic") - ctx := testContext2(t, &ContextOpts{ - Config: m, - ProviderResolver: ResourceProviderResolverFixed( - map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - ), - State: &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.web": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", + t.Fatalf("not yet updated for new provider interface") + /* + p := testProvider("aws") + m := testModule(t, "refresh-basic") + ctx := testContext2(t, &ContextOpts{ + Config: m, + ProviderResolver: ResourceProviderResolverFixed( + map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + ), + State: mustShimLegacyState(&State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, }, }, }, }, - }, - }, - }) + }), + }) - p.RefreshFn = nil - p.RefreshReturn = &InstanceState{ - ID: "foo", - } - - s, err := ctx.Refresh() - mod := s.RootModule() - if err != nil { - t.Fatalf("err: %s", err) - } - if !p.RefreshCalled { - t.Fatal("refresh should be called") - } - if p.RefreshState.ID != "foo" { - t.Fatalf("bad: %#v", p.RefreshState) - } - if !reflect.DeepEqual(mod.Resources["aws_instance.web"].Primary, p.RefreshReturn) { - t.Fatalf("bad: %#v %#v", mod.Resources["aws_instance.web"], p.RefreshReturn) - } - - for _, r := range mod.Resources { - if r.Type == "" { - t.Fatalf("no type: %#v", r) + p.RefreshFn = nil + p.RefreshReturn = &InstanceState{ + ID: "foo", } - } + + s, err := ctx.Refresh() + mod := s.RootModule() + if err != nil { + t.Fatalf("err: %s", err) + } + if !p.RefreshCalled { + t.Fatal("refresh should be called") + } + if p.RefreshState.ID != "foo" { + t.Fatalf("bad: %#v", p.RefreshState) + } + if !reflect.DeepEqual(mod.Resources["aws_instance.web"].Primary, p.RefreshReturn) { + t.Fatalf("bad: %#v %#v", mod.Resources["aws_instance.web"], p.RefreshReturn) + } + + for _, r := range mod.Resources { + if r.Type == "" { + t.Fatalf("no type: %#v", r) + } + } + */ } func TestContext2Refresh_dataComputedModuleVar(t *testing.T) { @@ -168,7 +171,7 @@ func TestContext2Refresh_targeted(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -180,7 +183,7 @@ func TestContext2Refresh_targeted(t *testing.T) { }, }, }, - }, + }), Targets: []addrs.Targetable{ addrs.RootModuleInstance.Resource( addrs.ManagedResourceMode, "aws_instance", "me", @@ -249,7 +252,7 @@ func TestContext2Refresh_targetedCount(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -263,7 +266,7 @@ func TestContext2Refresh_targetedCount(t *testing.T) { }, }, }, - }, + }), Targets: []addrs.Targetable{ addrs.RootModuleInstance.Resource( addrs.ManagedResourceMode, "aws_instance", "me", @@ -340,7 +343,7 @@ func TestContext2Refresh_targetedCountIndex(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -354,7 +357,7 @@ func TestContext2Refresh_targetedCountIndex(t *testing.T) { }, }, }, - }, + }), Targets: []addrs.Targetable{ addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "me", addrs.IntKey(0), @@ -426,7 +429,7 @@ func TestContext2Refresh_delete(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -440,7 +443,7 @@ func TestContext2Refresh_delete(t *testing.T) { }, }, }, - }, + }), }) p.RefreshFn = nil @@ -496,7 +499,7 @@ func TestContext2Refresh_hook(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -510,7 +513,7 @@ func TestContext2Refresh_hook(t *testing.T) { }, }, }, - }, + }), }) if _, diags := ctx.Refresh(); diags.HasErrors() { @@ -527,7 +530,7 @@ func TestContext2Refresh_hook(t *testing.T) { func TestContext2Refresh_modules(t *testing.T) { p := testProvider("aws") m := testModule(t, "refresh-modules") - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -554,7 +557,7 @@ func TestContext2Refresh_modules(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -687,7 +690,7 @@ func TestContext2Refresh_output(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -712,7 +715,7 @@ func TestContext2Refresh_output(t *testing.T) { }, }, }, - }, + }), }) p.RefreshFn = func(info *InstanceInfo, s *InstanceState) (*InstanceState, error) { @@ -741,7 +744,7 @@ func TestContext2Refresh_outputPartial(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -756,7 +759,7 @@ func TestContext2Refresh_outputPartial(t *testing.T) { Outputs: map[string]*OutputState{}, }, }, - }, + }), }) p.RefreshFn = nil @@ -789,61 +792,64 @@ func TestContext2Refresh_outputPartial(t *testing.T) { } func TestContext2Refresh_stateBasic(t *testing.T) { - p := testProvider("aws") - m := testModule(t, "refresh-basic") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.web": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "bar", + t.Fatalf("not yet updated for new provider interface") + /* + p := testProvider("aws") + m := testModule(t, "refresh-basic") + state := mustShimLegacyState(&State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + }, }, }, }, }, - }, - } - ctx := testContext2(t, &ContextOpts{ - Config: m, - ProviderResolver: ResourceProviderResolverFixed( - map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - ), - State: state, - }) + }) + ctx := testContext2(t, &ContextOpts{ + Config: m, + ProviderResolver: ResourceProviderResolverFixed( + map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + ), + State: state, + }) - p.RefreshFn = nil - p.RefreshReturn = &InstanceState{ - ID: "foo", - } + p.RefreshFn = nil + p.RefreshReturn = &InstanceState{ + ID: "foo", + } - s, diags := ctx.Refresh() - if diags.HasErrors() { - t.Fatalf("refresh errors: %s", diags.Err()) - } - originalMod := state.RootModule() - mod := s.RootModule() - if !p.RefreshCalled { - t.Fatal("refresh should be called") - } - if !reflect.DeepEqual(p.RefreshState, originalMod.Resources["aws_instance.web"].Primary) { - t.Fatalf( - "bad:\n\n%#v\n\n%#v", - p.RefreshState, - originalMod.Resources["aws_instance.web"].Primary) - } - if !reflect.DeepEqual(mod.Resources["aws_instance.web"].Primary, p.RefreshReturn) { - t.Fatalf("bad: %#v", mod.Resources) - } + s, diags := ctx.Refresh() + if diags.HasErrors() { + t.Fatalf("refresh errors: %s", diags.Err()) + } + originalMod := state.RootModule() + mod := s.RootModule() + if !p.RefreshCalled { + t.Fatal("refresh should be called") + } + if !reflect.DeepEqual(p.RefreshState, originalMod.Resources["aws_instance.web"].Primary) { + t.Fatalf( + "bad:\n\n%#v\n\n%#v", + p.RefreshState, + originalMod.Resources["aws_instance.web"].Primary) + } + if !reflect.DeepEqual(mod.Resources["aws_instance.web"].Primary, p.RefreshReturn) { + t.Fatalf("bad: %#v", mod.Resources) + } + */ } func TestContext2Refresh_dataOrphan(t *testing.T) { p := testProvider("null") - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -858,7 +864,7 @@ func TestContext2Refresh_dataOrphan(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ ProviderResolver: ResourceProviderResolverFixed( map[string]ResourceProviderFactory{ @@ -877,91 +883,94 @@ func TestContext2Refresh_dataOrphan(t *testing.T) { } func TestContext2Refresh_dataState(t *testing.T) { - m := testModule(t, "refresh-data-resource-basic") + t.Fatalf("not yet updated for new provider interface") + /* + m := testModule(t, "refresh-data-resource-basic") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - // Intentionally no resources since data resources are - // supposed to refresh themselves even if they didn't - // already exist. - Resources: map[string]*ResourceState{}, + state := mustShimLegacyState(&State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + // Intentionally no resources since data resources are + // supposed to refresh themselves even if they didn't + // already exist. + Resources: map[string]*ResourceState{}, + }, }, - }, - } + }) - p := testProvider("null") - p.GetSchemaReturn = &ProviderSchema{ - Provider: &configschema.Block{}, - DataSources: map[string]*configschema.Block{ - "null_data_source": { - Attributes: map[string]*configschema.Attribute{ - "inputs": { - Type: cty.Map(cty.String), - Optional: true, + p := testProvider("null") + p.GetSchemaReturn = &ProviderSchema{ + Provider: &configschema.Block{}, + DataSources: map[string]*configschema.Block{ + "null_data_source": { + Attributes: map[string]*configschema.Attribute{ + "inputs": { + Type: cty.Map(cty.String), + Optional: true, + }, }, }, }, - }, - } + } - ctx := testContext2(t, &ContextOpts{ - Config: m, - ProviderResolver: ResourceProviderResolverFixed( - map[string]ResourceProviderFactory{ - "null": testProviderFuncFixed(p), + ctx := testContext2(t, &ContextOpts{ + Config: m, + ProviderResolver: ResourceProviderResolverFixed( + map[string]ResourceProviderFactory{ + "null": testProviderFuncFixed(p), + }, + ), + State: state, + }) + + p.ReadDataDiffFn = nil + p.ReadDataDiffReturn = &InstanceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "inputs.#": { + Old: "0", + New: "1", + Type: DiffAttrInput, + }, + "inputs.test": { + Old: "", + New: "yes", + Type: DiffAttrInput, + }, + "outputs.#": { + Old: "", + New: "", + NewComputed: true, + Type: DiffAttrOutput, + }, }, - ), - State: state, - }) + } - p.ReadDataDiffFn = nil - p.ReadDataDiffReturn = &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "inputs.#": { - Old: "0", - New: "1", - Type: DiffAttrInput, - }, - "inputs.test": { - Old: "", - New: "yes", - Type: DiffAttrInput, - }, - "outputs.#": { - Old: "", - New: "", - NewComputed: true, - Type: DiffAttrOutput, - }, - }, - } + p.ReadDataApplyFn = nil + p.ReadDataApplyReturn = &InstanceState{ + ID: "-", + } - p.ReadDataApplyFn = nil - p.ReadDataApplyReturn = &InstanceState{ - ID: "-", - } + s, diags := ctx.Refresh() + if diags.HasErrors() { + t.Fatalf("refresh errors: %s", diags.Err()) + } - s, diags := ctx.Refresh() - if diags.HasErrors() { - t.Fatalf("refresh errors: %s", diags.Err()) - } + if !p.ReadDataDiffCalled { + t.Fatal("ReadDataDiff should have been called") + } + if !p.ReadDataApplyCalled { + t.Fatal("ReadDataApply should have been called") + } - if !p.ReadDataDiffCalled { - t.Fatal("ReadDataDiff should have been called") - } - if !p.ReadDataApplyCalled { - t.Fatal("ReadDataApply should have been called") - } - - mod := s.RootModule() - if got := mod.Resources["data.null_data_source.testing"].Primary.ID; got != "-" { - t.Fatalf("resource id is %q; want %s", got, "-") - } - if !reflect.DeepEqual(mod.Resources["data.null_data_source.testing"].Primary, p.ReadDataApplyReturn) { - t.Fatalf("bad: %#v", mod.Resources) - } + mod := s.RootModule() + if got := mod.Resources["data.null_data_source.testing"].Primary.ID; got != "-" { + t.Fatalf("resource id is %q; want %s", got, "-") + } + if !reflect.DeepEqual(mod.Resources["data.null_data_source.testing"].Primary, p.ReadDataApplyReturn) { + t.Fatalf("bad: %#v", mod.Resources) + } + */ } func TestContext2Refresh_dataStateRefData(t *testing.T) { @@ -985,7 +994,7 @@ func TestContext2Refresh_dataStateRefData(t *testing.T) { } m := testModule(t, "refresh-data-ref-data") - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -995,7 +1004,7 @@ func TestContext2Refresh_dataStateRefData(t *testing.T) { Resources: map[string]*ResourceState{}, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -1024,7 +1033,7 @@ func TestContext2Refresh_dataStateRefData(t *testing.T) { func TestContext2Refresh_tainted(t *testing.T) { p := testProvider("aws") m := testModule(t, "refresh-basic") - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1039,7 +1048,7 @@ func TestContext2Refresh_tainted(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -1086,8 +1095,7 @@ func TestContext2Refresh_unknownProvider(t *testing.T) { ProviderResolver: ResourceProviderResolverFixed( map[string]ResourceProviderFactory{}, ), - Shadow: true, - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1101,7 +1109,7 @@ func TestContext2Refresh_unknownProvider(t *testing.T) { }, }, }, - }, + }), }) if !diags.HasErrors() { @@ -1114,76 +1122,79 @@ func TestContext2Refresh_unknownProvider(t *testing.T) { } func TestContext2Refresh_vars(t *testing.T) { - p := testProvider("aws") - p.GetSchemaReturn = &ProviderSchema{ - Provider: &configschema.Block{}, - ResourceTypes: map[string]*configschema.Block{ - "aws_instance": { - Attributes: map[string]*configschema.Attribute{ - "ami": { - Type: cty.String, - Optional: true, - }, - "id": { - Type: cty.String, - Computed: true, - }, - }, - }, - }, - } - - m := testModule(t, "refresh-vars") - ctx := testContext2(t, &ContextOpts{ - Config: m, - ProviderResolver: ResourceProviderResolverFixed( - map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - ), - State: &State{ - - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "aws_instance.web": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, + t.Fatalf("not yet updated for new provider interface") + /* + p := testProvider("aws") + p.GetSchemaReturn = &ProviderSchema{ + Provider: &configschema.Block{}, + ResourceTypes: map[string]*configschema.Block{ + "aws_instance": { + Attributes: map[string]*configschema.Attribute{ + "ami": { + Type: cty.String, + Optional: true, + }, + "id": { + Type: cty.String, + Computed: true, }, }, }, }, - }, - }) - - p.RefreshFn = nil - p.RefreshReturn = &InstanceState{ - ID: "foo", - } - - s, diags := ctx.Refresh() - if diags.HasErrors() { - t.Fatalf("refresh errors: %s", diags.Err()) - } - mod := s.RootModule() - if !p.RefreshCalled { - t.Fatal("refresh should be called") - } - if p.RefreshState.ID != "foo" { - t.Fatalf("bad: %#v", p.RefreshState) - } - if !reflect.DeepEqual(mod.Resources["aws_instance.web"].Primary, p.RefreshReturn) { - t.Fatalf("bad: %#v", mod.Resources["aws_instance.web"]) - } - - for _, r := range mod.Resources { - if r.Type == "" { - t.Fatalf("no type: %#v", r) } - } + + m := testModule(t, "refresh-vars") + ctx := testContext2(t, &ContextOpts{ + Config: m, + ProviderResolver: ResourceProviderResolverFixed( + map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + ), + State: mustShimLegacyState(&State{ + + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }), + }) + + p.RefreshFn = nil + p.RefreshReturn = &InstanceState{ + ID: "foo", + } + + s, diags := ctx.Refresh() + if diags.HasErrors() { + t.Fatalf("refresh errors: %s", diags.Err()) + } + mod := s.RootModule() + if !p.RefreshCalled { + t.Fatal("refresh should be called") + } + if p.RefreshState.ID != "foo" { + t.Fatalf("bad: %#v", p.RefreshState) + } + if !reflect.DeepEqual(mod.Resources["aws_instance.web"].Primary, p.RefreshReturn) { + t.Fatalf("bad: %#v", mod.Resources["aws_instance.web"]) + } + + for _, r := range mod.Resources { + if r.Addr.Type == "" { + t.Fatalf("no type: %#v", r) + } + } + */ } func TestContext2Refresh_orphanModule(t *testing.T) { @@ -1209,7 +1220,7 @@ func TestContext2Refresh_orphanModule(t *testing.T) { }, } - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1278,7 +1289,7 @@ func TestContext2Refresh_orphanModule(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -1356,7 +1367,7 @@ func TestContext2Refresh_noDiffHookOnScaleOut(t *testing.T) { }, } - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1380,7 +1391,7 @@ func TestContext2Refresh_noDiffHookOnScaleOut(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, @@ -1417,7 +1428,7 @@ func TestContext2Refresh_updateProviderInState(t *testing.T) { }, } - s := &State{ + s := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -1432,7 +1443,7 @@ func TestContext2Refresh_updateProviderInState(t *testing.T) { }, }, }, - } + }) ctx := testContext2(t, &ContextOpts{ Config: m, diff --git a/terraform/context_test.go b/terraform/context_test.go index a966d2d0e..fbd303ad1 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -1,19 +1,31 @@ package terraform import ( + "bufio" + "bytes" "fmt" + "io/ioutil" + "os" + "path/filepath" + "sort" "strings" "testing" "time" + "github.com/hashicorp/go-version" "github.com/hashicorp/hil" - "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/configs" "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/hcl2shim" + "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/configs/configload" + "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/flatmap" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/plans/planfile" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/states/statefile" tfversion "github.com/hashicorp/terraform/version" ) @@ -97,68 +109,8 @@ func TestNewContextRequiredVersion(t *testing.T) { } } -func TestNewContextState(t *testing.T) { - cases := map[string]struct { - Input *ContextOpts - Err bool - }{ - "empty TFVersion": { - &ContextOpts{ - State: &State{}, - }, - false, - }, - - "past TFVersion": { - &ContextOpts{ - State: &State{TFVersion: "0.1.2"}, - }, - false, - }, - - "equal TFVersion": { - &ContextOpts{ - State: &State{TFVersion: tfversion.Version}, - }, - false, - }, - - "future TFVersion": { - &ContextOpts{ - State: &State{TFVersion: "99.99.99"}, - }, - true, - }, - - "future TFVersion, allowed": { - &ContextOpts{ - State: &State{TFVersion: "99.99.99"}, - StateFutureAllowed: true, - }, - false, - }, - } - - for k, tc := range cases { - ctx, err := NewContext(tc.Input) - if (err != nil) != tc.Err { - t.Fatalf("%s: err: %s", k, err) - } - if err != nil { - continue - } - - // Version should always be set to our current - if ctx.state.TFVersion != tfversion.Version { - t.Fatalf("%s: state not set to current version", k) - } - } -} - func testContext2(t *testing.T, opts *ContextOpts) *Context { t.Helper() - // Enable the shadow graph - opts.Shadow = true ctx, diags := NewContext(opts) if diags.HasErrors() { @@ -434,13 +386,13 @@ func testProvisioner() *MockResourceProvisioner { return p } -func checkStateString(t *testing.T, state *State, expected string) { +func checkStateString(t *testing.T, state *states.State, expected string) { t.Helper() actual := strings.TrimSpace(state.String()) expected = strings.TrimSpace(expected) if actual != expected { - t.Fatalf("state does not match! actual:\n%s\n\nexpected:\n%s", actual, expected) + t.Fatalf("incorrect state\ngot:\n%s\n\nwant:\n%s", actual, expected) } } @@ -696,6 +648,290 @@ func testProviderSchema(name string) *ProviderSchema { } +// contextForPlanViaFile is a helper that creates a temporary plan file, then +// reads it back in again and produces a ContextOpts object containing the +// planned changes, prior state and config from the plan file. +// +// This is intended for testing the separated plan/apply workflow in a more +// convenient way than spelling out all of these steps every time. Normally +// only the command and backend packages need to deal with such things, but +// our context tests try to exercise lots of stuff at once and so having them +// round-trip things through on-disk files is often an important part of +// fully representing an old bug in a regression test. +func contextOptsForPlanViaFile(configSnap *configload.Snapshot, state *states.State, plan *plans.Plan) (*ContextOpts, error) { + dir, err := ioutil.TempDir("", "terraform-contextForPlanViaFile") + if err != nil { + return nil, err + } + defer os.RemoveAll(dir) + + // We'll just create a dummy statefile.File here because we're not going + // to run through any of the codepaths that care about Lineage/Serial/etc + // here anyway. + stateFile := &statefile.File{ + State: state, + } + + filename := filepath.Join(dir, "tfplan") + err = planfile.Create(filename, configSnap, stateFile, plan) + if err != nil { + return nil, err + } + + pr, err := planfile.Open(filename) + if err != nil { + return nil, err + } + + config, diags := pr.ReadConfig() + if diags.HasErrors() { + return nil, diags.Err() + } + + stateFile, err = pr.ReadStateFile() + if err != nil { + return nil, err + } + + plan, err = pr.ReadPlan() + if err != nil { + return nil, err + } + + vars := make(InputValues) + for name, vv := range plan.VariableValues { + val, err := vv.Decode(cty.DynamicPseudoType) + if err != nil { + return nil, fmt.Errorf("can't decode value for variable %q: %s", name, err) + } + vars[name] = &InputValue{ + Value: val, + SourceType: ValueFromPlan, + } + } + + return &ContextOpts{ + Config: config, + State: stateFile.State, + Changes: plan.Changes, + Variables: vars, + Targets: plan.TargetAddrs, + ProviderSHA256s: plan.ProviderSHA256s, + }, nil +} + +// shimLegacyState is a helper that takes the legacy state type and +// converts it to the new state type. +// +// This is implemented as a state file upgrade, so it will not preserve +// parts of the state structure that are not included in a serialized state, +// such as the resolved results of any local values, outputs in non-root +// modules, etc. +func shimLegacyState(legacy *State) (*states.State, error) { + if legacy == nil { + return nil, nil + } + var buf bytes.Buffer + err := WriteState(legacy, &buf) + if err != nil { + return nil, err + } + f, err := statefile.Read(&buf) + if err != nil { + return nil, err + } + return f.State, err +} + +// mustShimLegacyState is a wrapper around ShimLegacyState that panics if +// the conversion does not succeed. This is primarily intended for tests where +// the given legacy state is an object constructed within the test. +func mustShimLegacyState(legacy *State) *states.State { + ret, err := shimLegacyState(legacy) + if err != nil { + panic(err) + } + return ret +} + +// legacyPlanComparisonString produces a string representation of the changes +// from a plan and a given state togther, as was formerly produced by the +// String method of terraform.Plan. +// +// This is here only for compatibility with existing tests that predate our +// new plan and state types, and should not be used in new tests. Instead, use +// a library like "cmp" to do a deep equality check and diff on the two +// data structures. +func legacyPlanComparisonString(state *states.State, changes *plans.Changes) string { + return fmt.Sprintf( + "DIFF:\n\n%s\n\nSTATE:\n\n%s", + legacyDiffComparisonString(changes), + state.String(), + ) +} + +// legacyDiffComparisonString produces a string representation of the changes +// from a planned changes object, as was formerly produced by the String method +// of terraform.Diff. +// +// This is here only for compatibility with existing tests that predate our +// new plan types, and should not be used in new tests. Instead, use a library +// like "cmp" to do a deep equality check and diff on the two data structures. +func legacyDiffComparisonString(changes *plans.Changes) string { + // The old string representation of a plan was grouped by module, but + // our new plan structure is not grouped in that way and so we'll need + // to preprocess it in order to produce that grouping. + type ResourceChanges struct { + Current *plans.ResourceInstanceChangeSrc + Deposed map[states.DeposedKey]*plans.ResourceInstanceChangeSrc + } + byModule := map[string]map[string]*ResourceChanges{} + resourceKeys := map[string][]string{} + var moduleKeys []string + for _, rc := range changes.Resources { + moduleKey := rc.Addr.Module.String() + if _, exists := byModule[moduleKey]; !exists { + moduleKeys = append(moduleKeys, moduleKey) + byModule[moduleKey] = make(map[string]*ResourceChanges) + } + resourceKey := rc.Addr.Resource.String() + if _, exists := byModule[moduleKey][resourceKey]; !exists { + resourceKeys[moduleKey] = append(resourceKeys[moduleKey], resourceKey) + byModule[moduleKey][resourceKey] = &ResourceChanges{ + Deposed: make(map[states.DeposedKey]*plans.ResourceInstanceChangeSrc), + } + } + + if rc.DeposedKey == states.NotDeposed { + byModule[moduleKey][resourceKey].Current = rc + } else { + byModule[moduleKey][resourceKey].Deposed[rc.DeposedKey] = rc + } + } + sort.Strings(moduleKeys) + for _, ks := range resourceKeys { + sort.Strings(ks) + } + + var buf bytes.Buffer + + for _, moduleKey := range moduleKeys { + rcs := byModule[moduleKey] + var mBuf bytes.Buffer + + for _, resourceKey := range resourceKeys[moduleKey] { + rc := rcs[resourceKey] + + crud := "UPDATE" + if rc.Current != nil { + switch rc.Current.Action { + case plans.Replace: + crud = "DESTROY/CREATE" + case plans.Delete: + crud = "DESTROY" + case plans.Create: + crud = "CREATE" + } + } else { + // We must be working on a deposed object then, in which + // case destroying is the only possible action. + crud = "DESTROY" + } + + extra := "" + if rc.Current == nil && len(rc.Deposed) > 0 { + extra = " (deposed only)" + } + + fmt.Fprintf( + &mBuf, "%s: %s%s\n", + crud, resourceKey, extra, + ) + + attrNames := map[string]bool{} + var oldAttrs map[string]string + var newAttrs map[string]string + if before := rc.Current.Before; before != nil { + ty, err := before.ImpliedType() + if err == nil { + val, err := before.Decode(ty) + if err == nil { + oldAttrs = hcl2shim.FlatmapValueFromHCL2(val) + for k := range oldAttrs { + attrNames[k] = true + } + } + } + } + if after := rc.Current.After; after != nil { + ty, err := after.ImpliedType() + if err == nil { + val, err := after.Decode(ty) + if err == nil { + newAttrs = hcl2shim.FlatmapValueFromHCL2(val) + for k := range newAttrs { + attrNames[k] = true + } + } + } + } + if oldAttrs == nil { + oldAttrs = make(map[string]string) + } + if newAttrs == nil { + newAttrs = make(map[string]string) + } + + attrNamesOrder := make([]string, 0, len(attrNames)) + keyLen := 0 + for n := range attrNames { + attrNamesOrder = append(attrNamesOrder, n) + if len(n) > keyLen { + keyLen = len(n) + } + } + + for _, attrK := range attrNamesOrder { + v := newAttrs[attrK] + u := oldAttrs[attrK] + + if v == config.UnknownVariableValue { + v = "" + } + // NOTE: we don't support here because we would + // need schema to do that. Excluding sensitive values + // is now done at the UI layer, and so should not be tested + // at the core layer. + + updateMsg := "" + // TODO: Mark " (forces new resource)" in updateMsg when appropriate. + + fmt.Fprintf( + &mBuf, " %s:%s %#v => %#v%s\n", + attrK, + strings.Repeat(" ", keyLen-len(attrK)), + u, v, + updateMsg, + ) + } + } + + if moduleKey == "" { // root module + buf.Write(mBuf.Bytes()) + buf.WriteByte('\n') + continue + } + + fmt.Fprintf(&buf, "%s:\n", moduleKey) + s := bufio.NewScanner(&mBuf) + for s.Scan() { + buf.WriteString(fmt.Sprintf(" %s\n", s.Text())) + } + } + + return buf.String() +} + const testContextGraph = ` root: root aws_instance.bar diff --git a/terraform/context_validate_test.go b/terraform/context_validate_test.go index aa4ecef4a..ba47f112f 100644 --- a/terraform/context_validate_test.go +++ b/terraform/context_validate_test.go @@ -5,6 +5,8 @@ import ( "strings" "testing" + "github.com/hashicorp/terraform/states" + "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/addrs" @@ -423,7 +425,7 @@ func TestContext2Validate_moduleProviderInheritOrphan(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "child"}, @@ -437,7 +439,7 @@ func TestContext2Validate_moduleProviderInheritOrphan(t *testing.T) { }, }, }, - }, + }), }) p.ValidateFn = func(c *ResourceConfig) ([]string, []error) { @@ -552,7 +554,7 @@ func TestContext2Validate_orphans(t *testing.T) { } m := testModule(t, "validate-good") - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -566,7 +568,7 @@ func TestContext2Validate_orphans(t *testing.T) { }, }, }, - } + }) c := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -867,7 +869,7 @@ func TestContext2Validate_tainted(t *testing.T) { } m := testModule(t, "validate-good") - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -882,7 +884,7 @@ func TestContext2Validate_tainted(t *testing.T) { }, }, }, - } + }) c := testContext2(t, &ContextOpts{ Config: m, ProviderResolver: ResourceProviderResolverFixed( @@ -931,7 +933,7 @@ func TestContext2Validate_targetedDestroy(t *testing.T) { Provisioners: map[string]ResourceProvisionerFactory{ "shell": testProvisionerFuncFixed(pr), }, - State: &State{ + State: mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -941,7 +943,7 @@ func TestContext2Validate_targetedDestroy(t *testing.T) { }, }, }, - }, + }), Targets: []addrs.Targetable{ addrs.RootModuleInstance.Resource( addrs.ManagedResourceMode, "aws_instance", "foo", @@ -1125,7 +1127,7 @@ func TestContext2Validate_PlanGraphBuilder(t *testing.T) { graph, diags := (&PlanGraphBuilder{ Config: c.config, - State: NewState(), + State: states.NewState(), Components: c.components, Schemas: c.schemas, Targets: c.targets, diff --git a/terraform/debug.go b/terraform/debug.go deleted file mode 100644 index 265339f63..000000000 --- a/terraform/debug.go +++ /dev/null @@ -1,523 +0,0 @@ -package terraform - -import ( - "archive/tar" - "bytes" - "compress/gzip" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - "sync" - "time" -) - -// DebugInfo is the global handler for writing the debug archive. All methods -// are safe to call concurrently. Setting DebugInfo to nil will disable writing -// the debug archive. All methods are safe to call on the nil value. -var dbug *debugInfo - -// SetDebugInfo initializes the debug handler with a backing file in the -// provided directory. This must be called before any other terraform package -// operations or not at all. Once his is called, CloseDebugInfo should be -// called before program exit. -func SetDebugInfo(path string) error { - if os.Getenv("TF_DEBUG") == "" { - return nil - } - - di, err := newDebugInfoFile(path) - if err != nil { - return err - } - - dbug = di - return nil -} - -// CloseDebugInfo is the exported interface to Close the debug info handler. -// The debug handler needs to be closed before program exit, so we export this -// function to be deferred in the appropriate entrypoint for our executable. -func CloseDebugInfo() error { - return dbug.Close() -} - -// newDebugInfoFile initializes the global debug handler with a backing file in -// the provided directory. -func newDebugInfoFile(dir string) (*debugInfo, error) { - err := os.MkdirAll(dir, 0755) - if err != nil { - return nil, err - } - - // FIXME: not guaranteed unique, but good enough for now - name := fmt.Sprintf("debug-%s", time.Now().Format("2006-01-02-15-04-05.999999999")) - archivePath := filepath.Join(dir, name+".tar.gz") - - f, err := os.OpenFile(archivePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) - if err != nil { - return nil, err - } - return newDebugInfo(name, f) -} - -// newDebugInfo initializes the global debug handler. -func newDebugInfo(name string, w io.Writer) (*debugInfo, error) { - gz := gzip.NewWriter(w) - - d := &debugInfo{ - name: name, - w: w, - gz: gz, - tar: tar.NewWriter(gz), - } - - // create the subdirs we need - topHdr := &tar.Header{ - Name: name, - Typeflag: tar.TypeDir, - Mode: 0755, - } - graphsHdr := &tar.Header{ - Name: name + "/graphs", - Typeflag: tar.TypeDir, - Mode: 0755, - } - err := d.tar.WriteHeader(topHdr) - // if the first errors, the second will too - err = d.tar.WriteHeader(graphsHdr) - if err != nil { - return nil, err - } - - return d, nil -} - -// debugInfo provides various methods for writing debug information to a -// central archive. The debugInfo struct should be initialized once before any -// output is written, and Close should be called before program exit. All -// exported methods on debugInfo will be safe for concurrent use. The exported -// methods are also all safe to call on a nil pointer, so that there is no need -// for conditional blocks before writing debug information. -// -// Each write operation done by the debugInfo will flush the gzip.Writer and -// tar.Writer, and call Sync() or Flush() on the output writer as needed. This -// ensures that as much data as possible is written to storage in the event of -// a crash. The append format of the tar file, and the stream format of the -// gzip writer allow easy recovery f the data in the event that the debugInfo -// is not closed before program exit. -type debugInfo struct { - sync.Mutex - - // archive root directory name - name string - - // current operation phase - phase string - - // step is monotonic counter for for recording the order of operations - step int - - // flag to protect Close() - closed bool - - // the debug log output is in a tar.gz format, written to the io.Writer w - w io.Writer - gz *gzip.Writer - tar *tar.Writer -} - -// Set the name of the current operational phase in the debug handler. Each file -// in the archive will contain the name of the phase in which it was created, -// i.e. "input", "apply", "plan", "refresh", "validate" -func (d *debugInfo) SetPhase(phase string) { - if d == nil { - return - } - d.Lock() - defer d.Unlock() - - d.phase = phase -} - -// Close the debugInfo, finalizing the data in storage. This closes the -// tar.Writer, the gzip.Wrtier, and if the output writer is an io.Closer, it is -// also closed. -func (d *debugInfo) Close() error { - if d == nil { - return nil - } - - d.Lock() - defer d.Unlock() - - if d.closed { - return nil - } - d.closed = true - - d.tar.Close() - d.gz.Close() - - if c, ok := d.w.(io.Closer); ok { - return c.Close() - } - return nil -} - -// debug buffer is an io.WriteCloser that will write itself to the debug -// archive when closed. -type debugBuffer struct { - debugInfo *debugInfo - name string - buf bytes.Buffer -} - -func (b *debugBuffer) Write(d []byte) (int, error) { - return b.buf.Write(d) -} - -func (b *debugBuffer) Close() error { - return b.debugInfo.WriteFile(b.name, b.buf.Bytes()) -} - -// ioutils only has a noop ReadCloser -type nopWriteCloser struct{} - -func (nopWriteCloser) Write([]byte) (int, error) { return 0, nil } -func (nopWriteCloser) Close() error { return nil } - -// NewFileWriter returns an io.WriteClose that will be buffered and written to -// the debug archive when closed. -func (d *debugInfo) NewFileWriter(name string) io.WriteCloser { - if d == nil { - return nopWriteCloser{} - } - - return &debugBuffer{ - debugInfo: d, - name: name, - } -} - -type syncer interface { - Sync() error -} - -type flusher interface { - Flush() error -} - -// Flush the tar.Writer and the gzip.Writer. Flush() or Sync() will be called -// on the output writer if they are available. -func (d *debugInfo) flush() { - d.tar.Flush() - d.gz.Flush() - - if f, ok := d.w.(flusher); ok { - f.Flush() - } - - if s, ok := d.w.(syncer); ok { - s.Sync() - } -} - -// WriteFile writes data as a single file to the debug arhive. -func (d *debugInfo) WriteFile(name string, data []byte) error { - if d == nil { - return nil - } - - d.Lock() - defer d.Unlock() - return d.writeFile(name, data) -} - -func (d *debugInfo) writeFile(name string, data []byte) error { - defer d.flush() - path := fmt.Sprintf("%s/%d-%s-%s", d.name, d.step, d.phase, name) - d.step++ - - hdr := &tar.Header{ - Name: path, - Mode: 0644, - Size: int64(len(data)), - } - err := d.tar.WriteHeader(hdr) - if err != nil { - return err - } - - _, err = d.tar.Write(data) - return err -} - -// DebugHook implements all methods of the terraform.Hook interface, and writes -// the arguments to a file in the archive. When a suitable format for the -// argument isn't available, the argument is encoded using json.Marshal. If the -// debug handler is nil, all DebugHook methods are noop, so no time is spent in -// marshaling the data structures. -type DebugHook struct{} - -func (*DebugHook) PreApply(ii *InstanceInfo, is *InstanceState, id *InstanceDiff) (HookAction, error) { - if dbug == nil { - return HookActionContinue, nil - } - - var buf bytes.Buffer - - if ii != nil { - buf.WriteString(ii.HumanId() + "\n") - } - - if is != nil { - buf.WriteString(is.String() + "\n") - } - - idCopy, err := id.Copy() - if err != nil { - return HookActionContinue, err - } - js, err := json.MarshalIndent(idCopy, "", " ") - if err != nil { - return HookActionContinue, err - } - buf.Write(js) - - dbug.WriteFile("hook-PreApply", buf.Bytes()) - - return HookActionContinue, nil -} - -func (*DebugHook) PostApply(ii *InstanceInfo, is *InstanceState, err error) (HookAction, error) { - if dbug == nil { - return HookActionContinue, nil - } - - var buf bytes.Buffer - - if ii != nil { - buf.WriteString(ii.HumanId() + "\n") - } - - if is != nil { - buf.WriteString(is.String() + "\n") - } - - if err != nil { - buf.WriteString(err.Error()) - } - - dbug.WriteFile("hook-PostApply", buf.Bytes()) - - return HookActionContinue, nil -} - -func (*DebugHook) PreDiff(ii *InstanceInfo, is *InstanceState) (HookAction, error) { - if dbug == nil { - return HookActionContinue, nil - } - - var buf bytes.Buffer - if ii != nil { - buf.WriteString(ii.HumanId() + "\n") - } - - if is != nil { - buf.WriteString(is.String()) - buf.WriteString("\n") - } - dbug.WriteFile("hook-PreDiff", buf.Bytes()) - - return HookActionContinue, nil -} - -func (*DebugHook) PostDiff(ii *InstanceInfo, id *InstanceDiff) (HookAction, error) { - if dbug == nil { - return HookActionContinue, nil - } - - var buf bytes.Buffer - if ii != nil { - buf.WriteString(ii.HumanId() + "\n") - } - - idCopy, err := id.Copy() - if err != nil { - return HookActionContinue, err - } - js, err := json.MarshalIndent(idCopy, "", " ") - if err != nil { - return HookActionContinue, err - } - buf.Write(js) - - dbug.WriteFile("hook-PostDiff", buf.Bytes()) - - return HookActionContinue, nil -} - -func (*DebugHook) PreProvisionResource(ii *InstanceInfo, is *InstanceState) (HookAction, error) { - if dbug == nil { - return HookActionContinue, nil - } - - var buf bytes.Buffer - if ii != nil { - buf.WriteString(ii.HumanId() + "\n") - } - - if is != nil { - buf.WriteString(is.String()) - buf.WriteString("\n") - } - dbug.WriteFile("hook-PreProvisionResource", buf.Bytes()) - - return HookActionContinue, nil -} - -func (*DebugHook) PostProvisionResource(ii *InstanceInfo, is *InstanceState) (HookAction, error) { - if dbug == nil { - return HookActionContinue, nil - } - - var buf bytes.Buffer - if ii != nil { - buf.WriteString(ii.HumanId()) - buf.WriteString("\n") - } - - if is != nil { - buf.WriteString(is.String()) - buf.WriteString("\n") - } - dbug.WriteFile("hook-PostProvisionResource", buf.Bytes()) - return HookActionContinue, nil -} - -func (*DebugHook) PreProvision(ii *InstanceInfo, s string) (HookAction, error) { - if dbug == nil { - return HookActionContinue, nil - } - - var buf bytes.Buffer - if ii != nil { - buf.WriteString(ii.HumanId()) - buf.WriteString("\n") - } - buf.WriteString(s + "\n") - - dbug.WriteFile("hook-PreProvision", buf.Bytes()) - return HookActionContinue, nil -} - -func (*DebugHook) PostProvision(ii *InstanceInfo, s string, err error) (HookAction, error) { - if dbug == nil { - return HookActionContinue, nil - } - - var buf bytes.Buffer - if ii != nil { - buf.WriteString(ii.HumanId() + "\n") - } - buf.WriteString(s + "\n") - - dbug.WriteFile("hook-PostProvision", buf.Bytes()) - return HookActionContinue, nil -} - -func (*DebugHook) ProvisionOutput(ii *InstanceInfo, s1 string, s2 string) { - if dbug == nil { - return - } - - var buf bytes.Buffer - if ii != nil { - buf.WriteString(ii.HumanId()) - buf.WriteString("\n") - } - buf.WriteString(s1 + "\n") - buf.WriteString(s2 + "\n") - - dbug.WriteFile("hook-ProvisionOutput", buf.Bytes()) -} - -func (*DebugHook) PreRefresh(ii *InstanceInfo, is *InstanceState) (HookAction, error) { - if dbug == nil { - return HookActionContinue, nil - } - - var buf bytes.Buffer - if ii != nil { - buf.WriteString(ii.HumanId() + "\n") - } - - if is != nil { - buf.WriteString(is.String()) - buf.WriteString("\n") - } - dbug.WriteFile("hook-PreRefresh", buf.Bytes()) - return HookActionContinue, nil -} - -func (*DebugHook) PostRefresh(ii *InstanceInfo, is *InstanceState) (HookAction, error) { - if dbug == nil { - return HookActionContinue, nil - } - - var buf bytes.Buffer - if ii != nil { - buf.WriteString(ii.HumanId()) - buf.WriteString("\n") - } - - if is != nil { - buf.WriteString(is.String()) - buf.WriteString("\n") - } - dbug.WriteFile("hook-PostRefresh", buf.Bytes()) - return HookActionContinue, nil -} - -func (*DebugHook) PreImportState(ii *InstanceInfo, s string) (HookAction, error) { - if dbug == nil { - return HookActionContinue, nil - } - - var buf bytes.Buffer - if ii != nil { - buf.WriteString(ii.HumanId()) - buf.WriteString("\n") - } - buf.WriteString(s + "\n") - - dbug.WriteFile("hook-PreImportState", buf.Bytes()) - return HookActionContinue, nil -} - -func (*DebugHook) PostImportState(ii *InstanceInfo, iss []*InstanceState) (HookAction, error) { - if dbug == nil { - return HookActionContinue, nil - } - - var buf bytes.Buffer - - if ii != nil { - buf.WriteString(ii.HumanId() + "\n") - } - - for _, is := range iss { - if is != nil { - buf.WriteString(is.String() + "\n") - } - } - dbug.WriteFile("hook-PostImportState", buf.Bytes()) - return HookActionContinue, nil -} - -// skip logging this for now, since it could be huge -func (*DebugHook) PostStateUpdate(*State) (HookAction, error) { - return HookActionContinue, nil -} diff --git a/terraform/debug_test.go b/terraform/debug_test.go deleted file mode 100644 index 58d194b8e..000000000 --- a/terraform/debug_test.go +++ /dev/null @@ -1,187 +0,0 @@ -package terraform - -import ( - "archive/tar" - "bytes" - "compress/gzip" - "io" - "io/ioutil" - "regexp" - "strings" - "testing" - - "github.com/zclconf/go-cty/cty" - - "github.com/hashicorp/terraform/configs/configschema" -) - -// debugInfo should be safe when nil -func TestDebugInfo_nil(t *testing.T) { - var d *debugInfo - - d.SetPhase("none") - d.WriteFile("none", nil) - d.Close() -} - -func TestDebugInfo_basicFile(t *testing.T) { - var w bytes.Buffer - debug, err := newDebugInfo("test-debug-info", &w) - if err != nil { - t.Fatal(err) - } - debug.SetPhase("test") - - fileData := map[string][]byte{ - "file1": []byte("file 1 data"), - "file2": []byte("file 2 data"), - "file3": []byte("file 3 data"), - } - - for f, d := range fileData { - err = debug.WriteFile(f, d) - if err != nil { - t.Fatal(err) - } - } - - err = debug.Close() - if err != nil { - t.Fatal(err) - } - - gz, err := gzip.NewReader(&w) - if err != nil { - t.Fatal(err) - } - tr := tar.NewReader(gz) - - for { - hdr, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - t.Fatal(err) - } - - // get the filename part of the archived file - name := regexp.MustCompile(`\w+$`).FindString(hdr.Name) - data := fileData[name] - - delete(fileData, name) - - tarData, err := ioutil.ReadAll(tr) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(data, tarData) { - t.Fatalf("got '%s' for file '%s'", tarData, name) - } - } - - for k := range fileData { - t.Fatalf("didn't find file %s", k) - } -} - -// Test that we get logs and graphs from a walk. We're not looking for anything -// specific, since the output is going to change in the near future. -func TestDebug_plan(t *testing.T) { - var out bytes.Buffer - d, err := newDebugInfo("test-debug-info", &out) - if err != nil { - t.Fatal(err) - } - // set the global debug value - dbug = d - - // run a basic plan - m := testModule(t, "plan-good") - p := mockProviderWithResourceTypeSchema("aws_instance", &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "num": { - Type: cty.Number, - Optional: true, - }, - "foo": { - Type: cty.Number, - Optional: true, - }, - }, - }) - p.DiffFn = testDiffFn - ctx := testContext2(t, &ContextOpts{ - Config: m, - ProviderResolver: ResourceProviderResolverFixed( - map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(p), - }, - ), - }) - - _, diags := ctx.Plan() - if diags.HasErrors() { - t.Fatalf("err: %s", diags.Err()) - } - - err = CloseDebugInfo() - if err != nil { - t.Fatal(err) - } - - gz, err := gzip.NewReader(&out) - if err != nil { - t.Fatal(err) - } - tr := tar.NewReader(gz) - - graphLogs := 0 - for { - hdr, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - t.Fatal(err) - } - - // record any file that contains data - if hdr.Size > 0 { - if strings.HasSuffix(hdr.Name, "graph.json") { - graphLogs++ - } - } - } - - if graphLogs == 0 { - t.Fatal("no json graphs") - } -} - -// verify that no hooks panic on nil input -func TestDebugHook_nilArgs(t *testing.T) { - // make sure debug isn't nil, so the hooks try to execute - var w bytes.Buffer - var err error - dbug, err = newDebugInfo("test-debug-info", &w) - if err != nil { - t.Fatal(err) - } - - var h DebugHook - h.PostApply(nil, nil, nil) - h.PostDiff(nil, nil) - h.PostImportState(nil, nil) - h.PostProvision(nil, "", nil) - h.PostProvisionResource(nil, nil) - h.PostRefresh(nil, nil) - h.PostStateUpdate(nil) - h.PreApply(nil, nil, nil) - h.PreDiff(nil, nil) - h.PreImportState(nil, "") - h.PreProvision(nil, "") - h.PreProvisionResource(nil, nil) - h.PreRefresh(nil, nil) - h.ProvisionOutput(nil, "", "") -} diff --git a/terraform/eval_apply.go b/terraform/eval_apply.go index 4108ab9de..ddb1457ce 100644 --- a/terraform/eval_apply.go +++ b/terraform/eval_apply.go @@ -5,134 +5,115 @@ import ( "log" "github.com/hashicorp/go-multierror" - "github.com/zclconf/go-cty/cty/gocty" "github.com/hashicorp/terraform/addrs" - "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/configs" - "github.com/hashicorp/terraform/tfdiags" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" ) // EvalApply is an EvalNode implementation that writes the diff to // the full diff. type EvalApply struct { Addr addrs.ResourceInstance - State **InstanceState - Diff **InstanceDiff + State **states.ResourceInstanceObject + Change **plans.ResourceInstanceChange Provider *ResourceProvider - Output **InstanceState + Output **states.ResourceInstanceObject CreateNew *bool Error *error } // TODO: test func (n *EvalApply) Eval(ctx EvalContext) (interface{}, error) { - diff := *n.Diff - provider := *n.Provider - state := *n.State + return nil, fmt.Errorf("EvalApply is not yet updated for the new state and plan types") + /* + diff := *n.Diff + provider := *n.Provider + state := *n.State - // The provider API still expects our legacy InstanceInfo type, so we must shim it. - legacyInfo := NewInstanceInfo(n.Addr.Absolute(ctx.Path())) + // The provider API still expects our legacy InstanceInfo type, so we must shim it. + legacyInfo := NewInstanceInfo(n.Addr.Absolute(ctx.Path())) + + if state == nil { + state = &states.ResourceInstanceObject{} + } + + // Flag if we're creating a new instance + if n.CreateNew != nil { + *n.CreateNew = state.ID == "" && !diff.GetDestroy() || diff.RequiresNew() + } + + // With the completed diff, apply! + log.Printf("[DEBUG] apply %s: executing Apply", n.Addr) + state, err := provider.Apply(legacyInfo, state, diff) + if state == nil { + state = new(InstanceState) + } + state.init() + + // Force the "id" attribute to be our ID + if state.ID != "" { + state.Attributes["id"] = state.ID + } + + // If the value is the unknown variable value, then it is an error. + // In this case we record the error and remove it from the state + for ak, av := range state.Attributes { + if av == config.UnknownVariableValue { + err = multierror.Append(err, fmt.Errorf( + "Attribute with unknown value: %s", ak)) + delete(state.Attributes, ak) + } + } + + // If the provider produced an InstanceState with an empty id then + // that really means that there's no state at all. + // FIXME: Change the provider protocol so that the provider itself returns + // a null in this case, and stop treating the ID as special. + if state.ID == "" { + state = nil + } + + // Write the final state + if n.Output != nil { + *n.Output = state + } + + // If there are no errors, then we append it to our output error + // if we have one, otherwise we just output it. + if err != nil { + if n.Error != nil { + helpfulErr := fmt.Errorf("%s: %s", n.Addr, err.Error()) + *n.Error = multierror.Append(*n.Error, helpfulErr) + } else { + return nil, err + } + } - if diff.Empty() { - log.Printf("[DEBUG] apply %s: diff is empty, so skipping.", n.Addr) return nil, nil - } - - // Remove any output values from the diff - for k, ad := range diff.CopyAttributes() { - if ad.Type == DiffAttrOutput { - diff.DelAttribute(k) - } - } - - // If the state is nil, make it non-nil - if state == nil { - state = new(InstanceState) - } - state.init() - - // Flag if we're creating a new instance - if n.CreateNew != nil { - *n.CreateNew = state.ID == "" && !diff.GetDestroy() || diff.RequiresNew() - } - - // With the completed diff, apply! - log.Printf("[DEBUG] apply %s: executing Apply", n.Addr) - state, err := provider.Apply(legacyInfo, state, diff) - if state == nil { - state = new(InstanceState) - } - state.init() - - // Force the "id" attribute to be our ID - if state.ID != "" { - state.Attributes["id"] = state.ID - } - - // If the value is the unknown variable value, then it is an error. - // In this case we record the error and remove it from the state - for ak, av := range state.Attributes { - if av == config.UnknownVariableValue { - err = multierror.Append(err, fmt.Errorf( - "Attribute with unknown value: %s", ak)) - delete(state.Attributes, ak) - } - } - - // If the provider produced an InstanceState with an empty id then - // that really means that there's no state at all. - // FIXME: Change the provider protocol so that the provider itself returns - // a null in this case, and stop treating the ID as special. - if state.ID == "" { - state = nil - } - - // Write the final state - if n.Output != nil { - *n.Output = state - } - - // If there are no errors, then we append it to our output error - // if we have one, otherwise we just output it. - if err != nil { - if n.Error != nil { - helpfulErr := fmt.Errorf("%s: %s", n.Addr, err.Error()) - *n.Error = multierror.Append(*n.Error, helpfulErr) - } else { - return nil, err - } - } - - return nil, nil + */ } // EvalApplyPre is an EvalNode implementation that does the pre-Apply work type EvalApplyPre struct { - Addr addrs.ResourceInstance - State **InstanceState - Diff **InstanceDiff + Addr addrs.ResourceInstance + Gen states.Generation + State **states.ResourceInstanceObject + Change **plans.ResourceInstanceChange } // TODO: test func (n *EvalApplyPre) Eval(ctx EvalContext) (interface{}, error) { - state := *n.State - diff := *n.Diff + change := *n.Change + absAddr := n.Addr.Absolute(ctx.Path()) - // The hook API still uses our legacy InstanceInfo type, so we must - // shim it. - legacyInfo := NewInstanceInfo(n.Addr.Absolute(ctx.Path())) + if resourceHasUserVisibleApply(n.Addr) { + priorState := change.Before + plannedNewState := change.After - // If the state is nil, make it non-nil - if state == nil { - state = new(InstanceState) - } - state.init() - - if resourceHasUserVisibleApply(legacyInfo) { - // Call post-apply hook err := ctx.Hook(func(h Hook) (HookAction, error) { - return h.PreApply(legacyInfo, state, diff) + return h.PreApply(absAddr, n.Gen, change.Action, priorState, plannedNewState) }) if err != nil { return nil, err @@ -145,7 +126,8 @@ func (n *EvalApplyPre) Eval(ctx EvalContext) (interface{}, error) { // EvalApplyPost is an EvalNode implementation that does the post-Apply work type EvalApplyPost struct { Addr addrs.ResourceInstance - State **InstanceState + Gen states.Generation + State **states.ResourceInstanceObject Error *error } @@ -153,17 +135,19 @@ type EvalApplyPost struct { func (n *EvalApplyPost) Eval(ctx EvalContext) (interface{}, error) { state := *n.State - // The hook API still uses our legacy InstanceInfo type, so we must - // shim it. - legacyInfo := NewInstanceInfo(n.Addr.Absolute(ctx.Path())) + if resourceHasUserVisibleApply(n.Addr) { + absAddr := n.Addr.Absolute(ctx.Path()) + newState := state.Value + var err error + if n.Error != nil { + err = *n.Error + } - if resourceHasUserVisibleApply(legacyInfo) { - // Call post-apply hook - err := ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostApply(legacyInfo, state, *n.Error) + hookErr := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PostApply(absAddr, n.Gen, newState, err) }) - if err != nil { - return nil, err + if hookErr != nil { + return nil, hookErr } } @@ -175,15 +159,13 @@ func (n *EvalApplyPost) Eval(ctx EvalContext) (interface{}, error) { // // Certain resources do apply actions only as an implementation detail, so // these should not be advertised to code outside of this package. -func resourceHasUserVisibleApply(info *InstanceInfo) bool { - addr := info.ResourceAddress() - +func resourceHasUserVisibleApply(addr addrs.ResourceInstance) bool { // Only managed resources have user-visible apply actions. // In particular, this excludes data resources since we "apply" these // only as an implementation detail of removing them from state when // they are destroyed. (When reading, they don't get here at all because // we present them as "Refresh" actions.) - return addr.Mode == config.ManagedResourceMode + return addr.ContainingResource().Mode == addrs.ManagedResourceMode } // EvalApplyProvisioners is an EvalNode implementation that executes @@ -193,7 +175,7 @@ func resourceHasUserVisibleApply(info *InstanceInfo) bool { // ApplyProvisioner (single) that is looped over. type EvalApplyProvisioners struct { Addr addrs.ResourceInstance - State **InstanceState + State **states.ResourceInstanceObject ResourceConfig *configs.Resource CreateNew *bool Error *error @@ -204,15 +186,13 @@ type EvalApplyProvisioners struct { // TODO: test func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) { + absAddr := n.Addr.Absolute(ctx.Path()) state := *n.State if state == nil { log.Printf("[TRACE] EvalApplyProvisioners: %s has no state, so skipping provisioners", n.Addr) return nil, nil } - // The hook API still uses the legacy InstanceInfo type, so we need to shim it. - legacyInfo := NewInstanceInfo(n.Addr.Absolute(ctx.Path())) - if n.CreateNew != nil && !*n.CreateNew { // If we're not creating a new resource, then don't run provisioners return nil, nil @@ -229,7 +209,7 @@ func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) { if n.Error != nil && *n.Error != nil { if taint { - state.Tainted = true + state.Status = states.ObjectTainted } // We're already tainted, so just return out @@ -239,7 +219,7 @@ func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) { { // Call pre hook err := ctx.Hook(func(h Hook) (HookAction, error) { - return h.PreProvisionResource(legacyInfo, state) + return h.PreProvisionInstance(absAddr, state.Value) }) if err != nil { return nil, err @@ -251,7 +231,7 @@ func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) { err := n.apply(ctx, provs) if err != nil { if taint { - state.Tainted = true + state.Status = states.ObjectTainted } *n.Error = multierror.Append(*n.Error, err) @@ -261,7 +241,7 @@ func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) { { // Call post hook err := ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostProvisionResource(legacyInfo, state) + return h.PostProvisionInstance(absAddr, state.Value) }) if err != nil { return nil, err @@ -294,122 +274,126 @@ func (n *EvalApplyProvisioners) filterProvisioners() []*configs.Provisioner { } func (n *EvalApplyProvisioners) apply(ctx EvalContext, provs []*configs.Provisioner) error { - instanceAddr := n.Addr - state := *n.State + return fmt.Errorf("EvalApplyProvisioners.apply not yet updated for new types") + /* + instanceAddr := n.Addr + absAddr := instanceAddr.Absolute(ctx.Path()) + state := *n.State - // The hook API still uses the legacy InstanceInfo type, so we need to shim it. - legacyInfo := NewInstanceInfo(n.Addr.Absolute(ctx.Path())) + // The hook API still uses the legacy InstanceInfo type, so we need to shim it. + legacyInfo := NewInstanceInfo(n.Addr.Absolute(ctx.Path())) - // Store the original connection info, restore later - origConnInfo := state.Ephemeral.ConnInfo - defer func() { - state.Ephemeral.ConnInfo = origConnInfo - }() + // Store the original connection info, restore later + origConnInfo := state.Ephemeral.ConnInfo + defer func() { + state.Ephemeral.ConnInfo = origConnInfo + }() - var diags tfdiags.Diagnostics + var diags tfdiags.Diagnostics - for _, prov := range provs { - // Get the provisioner - provisioner := ctx.Provisioner(prov.Type) - schema := ctx.ProvisionerSchema(prov.Type) + for _, prov := range provs { + // Get the provisioner + provisioner := ctx.Provisioner(prov.Type) + schema := ctx.ProvisionerSchema(prov.Type) - keyData := EvalDataForInstanceKey(instanceAddr.Key) + keyData := EvalDataForInstanceKey(instanceAddr.Key) - // Evaluate the main provisioner configuration. - config, _, configDiags := ctx.EvaluateBlock(prov.Config, schema, instanceAddr, keyData) - diags = diags.Append(configDiags) + // Evaluate the main provisioner configuration. + config, _, configDiags := ctx.EvaluateBlock(prov.Config, schema, instanceAddr, keyData) + diags = diags.Append(configDiags) - // A provisioner may not have a connection block - if prov.Connection != nil { - connInfo, _, connInfoDiags := ctx.EvaluateBlock(prov.Connection.Config, connectionBlockSupersetSchema, instanceAddr, keyData) - diags = diags.Append(connInfoDiags) + // A provisioner may not have a connection block + if prov.Connection != nil { + connInfo, _, connInfoDiags := ctx.EvaluateBlock(prov.Connection.Config, connectionBlockSupersetSchema, instanceAddr, keyData) + diags = diags.Append(connInfoDiags) - if configDiags.HasErrors() || connInfoDiags.HasErrors() { - continue - } - - // Merge the connection information, and also lower everything to strings - // for compatibility with the communicator API. - overlay := make(map[string]string) - if origConnInfo != nil { - for k, v := range origConnInfo { - overlay[k] = v - } - } - for it := connInfo.ElementIterator(); it.Next(); { - kv, vv := it.Element() - var k, v string - - // there are no unset or null values in a connection block, and - // everything needs to map to a string. - if vv.IsNull() { + if configDiags.HasErrors() || connInfoDiags.HasErrors() { continue } - err := gocty.FromCtyValue(kv, &k) - if err != nil { - // Should never happen, because connectionBlockSupersetSchema requires all primitives - panic(err) + // Merge the connection information, and also lower everything to strings + // for compatibility with the communicator API. + overlay := make(map[string]string) + if origConnInfo != nil { + for k, v := range origConnInfo { + overlay[k] = v + } } - err = gocty.FromCtyValue(vv, &v) - if err != nil { - // Should never happen, because connectionBlockSupersetSchema requires all primitives - panic(err) + for it := connInfo.ElementIterator(); it.Next(); { + kv, vv := it.Element() + var k, v string + + // there are no unset or null values in a connection block, and + // everything needs to map to a string. + if vv.IsNull() { + continue + } + + err := gocty.FromCtyValue(kv, &k) + if err != nil { + // Should never happen, because connectionBlockSupersetSchema requires all primitives + panic(err) + } + err = gocty.FromCtyValue(vv, &v) + if err != nil { + // Should never happen, because connectionBlockSupersetSchema requires all primitives + panic(err) + } + + overlay[k] = v } - overlay[k] = v + state.Ephemeral.ConnInfo = overlay } - state.Ephemeral.ConnInfo = overlay - } + { + // Call pre hook + err := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PreProvisionInstanceStep(absAddr, prov.Type) + }) + if err != nil { + return err + } + } - { - // Call pre hook - err := ctx.Hook(func(h Hook) (HookAction, error) { - return h.PreProvision(legacyInfo, prov.Type) + // The output function + outputFn := func(msg string) { + ctx.Hook(func(h Hook) (HookAction, error) { + h.ProvisionOutput(absAddr, prov.Type, msg) + return HookActionContinue, nil + }) + } + + // The provisioner API still uses our legacy ResourceConfig type, so + // we need to shim it. + legacyRC := NewResourceConfigShimmed(config, schema) + + // Invoke the Provisioner + output := CallbackUIOutput{OutputFn: outputFn} + applyErr := provisioner.Apply(&output, state, legacyRC) + + // Call post hook + hookErr := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PostProvisionInstanceStep(absAddr, prov.Type, applyErr) }) - if err != nil { - return err + + // Handle the error before we deal with the hook + if applyErr != nil { + // Determine failure behavior + switch prov.OnFailure { + case configs.ProvisionerOnFailureContinue: + log.Printf("[INFO] apply %s [%s]: error during provision, but continuing as requested in configuration", n.Addr, prov.Type) + case configs.ProvisionerOnFailureFail: + return applyErr + } + } + + // Deal with the hook + if hookErr != nil { + return hookErr } } - // The output function - outputFn := func(msg string) { - ctx.Hook(func(h Hook) (HookAction, error) { - h.ProvisionOutput(legacyInfo, prov.Type, msg) - return HookActionContinue, nil - }) - } - - // The provisioner API still uses our legacy ResourceConfig type, so - // we need to shim it. - legacyRC := NewResourceConfigShimmed(config, schema) - - // Invoke the Provisioner - output := CallbackUIOutput{OutputFn: outputFn} - applyErr := provisioner.Apply(&output, state, legacyRC) - - // Call post hook - hookErr := ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostProvision(legacyInfo, prov.Type, applyErr) - }) - - // Handle the error before we deal with the hook - if applyErr != nil { - // Determine failure behavior - switch prov.OnFailure { - case configs.ProvisionerOnFailureContinue: - log.Printf("[INFO] apply %s [%s]: error during provision, but continuing as requested in configuration", n.Addr, prov.Type) - case configs.ProvisionerOnFailureFail: - return applyErr - } - } - - // Deal with the hook - if hookErr != nil { - return hookErr - } - } - - return diags.ErrWithWarnings() + return diags.ErrWithWarnings() + */ } diff --git a/terraform/eval_check_prevent_destroy.go b/terraform/eval_check_prevent_destroy.go index 04b4f206d..fb397a257 100644 --- a/terraform/eval_check_prevent_destroy.go +++ b/terraform/eval_check_prevent_destroy.go @@ -3,6 +3,8 @@ package terraform import ( "fmt" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/terraform/addrs" @@ -16,18 +18,18 @@ import ( type EvalCheckPreventDestroy struct { Addr addrs.ResourceInstance Config *configs.Resource - Diff **InstanceDiff + Change **plans.ResourceInstanceChange } func (n *EvalCheckPreventDestroy) Eval(ctx EvalContext) (interface{}, error) { - if n.Diff == nil || *n.Diff == nil || n.Config == nil || n.Config.Managed == nil { + if n.Change == nil || *n.Change == nil || n.Config == nil || n.Config.Managed == nil { return nil, nil } - diff := *n.Diff + change := *n.Change preventDestroy := n.Config.Managed.PreventDestroy - if diff.GetDestroy() && preventDestroy { + if (change.Action == plans.Delete || change.Action == plans.Replace) && preventDestroy { var diags tfdiags.Diagnostics diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, diff --git a/terraform/eval_context.go b/terraform/eval_context.go index 21255e67f..40c5958ff 100644 --- a/terraform/eval_context.go +++ b/terraform/eval_context.go @@ -1,12 +1,12 @@ package terraform import ( - "sync" - "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/lang" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" "github.com/zclconf/go-cty/cty" ) @@ -121,11 +121,11 @@ type EvalContext interface { // previously-set keys that are not present in the new map. SetModuleCallArguments(addrs.ModuleCallInstance, map[string]cty.Value) - // Diff returns the global diff as well as the lock that should - // be used to modify that diff. - Diff() (*Diff, *sync.RWMutex) + // Changes returns the writer object that can be used to write new proposed + // changes into the global changes set. + Changes() *plans.ChangesSync - // State returns the global state as well as the lock that should - // be used to modify that state. - State() (*State, *sync.RWMutex) + // State returns a wrapper object that provides safe concurrent access to + // the global state. + State() *states.SyncState } diff --git a/terraform/eval_context_builtin.go b/terraform/eval_context_builtin.go index 465504ba9..2a1ab5ce5 100644 --- a/terraform/eval_context_builtin.go +++ b/terraform/eval_context_builtin.go @@ -6,6 +6,10 @@ import ( "log" "sync" + "github.com/hashicorp/terraform/plans" + + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/lang" @@ -54,10 +58,8 @@ type BuiltinEvalContext struct { ProviderLock *sync.Mutex ProvisionerCache map[string]ResourceProvisioner ProvisionerLock *sync.Mutex - DiffValue *Diff - DiffLock *sync.RWMutex - StateValue *State - StateLock *sync.RWMutex + ChangesValue *plans.ChangesSync + StateValue *states.SyncState once sync.Once } @@ -318,12 +320,12 @@ func (ctx *BuiltinEvalContext) SetModuleCallArguments(n addrs.ModuleCallInstance } } -func (ctx *BuiltinEvalContext) Diff() (*Diff, *sync.RWMutex) { - return ctx.DiffValue, ctx.DiffLock +func (ctx *BuiltinEvalContext) Changes() *plans.ChangesSync { + return ctx.ChangesValue } -func (ctx *BuiltinEvalContext) State() (*State, *sync.RWMutex) { - return ctx.StateValue, ctx.StateLock +func (ctx *BuiltinEvalContext) State() *states.SyncState { + return ctx.StateValue } func (ctx *BuiltinEvalContext) init() { diff --git a/terraform/eval_context_mock.go b/terraform/eval_context_mock.go index 99e3cc3ad..31c93a455 100644 --- a/terraform/eval_context_mock.go +++ b/terraform/eval_context_mock.go @@ -1,8 +1,6 @@ package terraform import ( - "sync" - "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcldec" "github.com/zclconf/go-cty/cty" @@ -12,6 +10,8 @@ import ( "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/lang" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" ) @@ -127,13 +127,11 @@ type MockEvalContext struct { SetModuleCallArgumentsModule addrs.ModuleCallInstance SetModuleCallArgumentsValues map[string]cty.Value - DiffCalled bool - DiffDiff *Diff - DiffLock *sync.RWMutex + ChangesCalled bool + ChangesChanges *plans.ChangesSync StateCalled bool - StateState *State - StateLock *sync.RWMutex + StateState *states.SyncState } // MockEvalContext implements EvalContext @@ -338,12 +336,12 @@ func (c *MockEvalContext) SetModuleCallArguments(n addrs.ModuleCallInstance, val c.SetModuleCallArgumentsValues = values } -func (c *MockEvalContext) Diff() (*Diff, *sync.RWMutex) { - c.DiffCalled = true - return c.DiffDiff, c.DiffLock +func (c *MockEvalContext) Changes() *plans.ChangesSync { + c.ChangesCalled = true + return c.ChangesChanges } -func (c *MockEvalContext) State() (*State, *sync.RWMutex) { +func (c *MockEvalContext) State() *states.SyncState { c.StateCalled = true - return c.StateState, c.StateLock + return c.StateState } diff --git a/terraform/eval_count.go b/terraform/eval_count.go index ea0f22d4f..a4d7b1535 100644 --- a/terraform/eval_count.go +++ b/terraform/eval_count.go @@ -113,39 +113,10 @@ func evaluateResourceCountExpression(expr hcl.Expression, ctx EvalContext) (int, // on the state. The caller must therefore not also be holding a state lock, // or this function will block forever awaiting the lock. func fixResourceCountSetTransition(ctx EvalContext, addr addrs.Resource, countEnabled bool) { - huntAddr := addr.Instance(addrs.NoKey) - replaceAddr := addr.Instance(addrs.IntKey(0)) - if !countEnabled { - huntAddr, replaceAddr = replaceAddr, huntAddr - } - path := ctx.Path() - - // The state still uses our legacy internal address string format, so we - // need to shim here. - huntKey := NewLegacyResourceInstanceAddress(huntAddr.Absolute(path)).stateId() - replaceKey := NewLegacyResourceInstanceAddress(replaceAddr.Absolute(path)).stateId() - - state, lock := ctx.State() - lock.Lock() - defer lock.Unlock() - - mod := state.ModuleByPath(path) - if mod == nil { - return + state := ctx.State() + changed := state.MaybeFixUpResourceInstanceAddressForCount(addr.Absolute(path), countEnabled) + if changed { + log.Printf("[TRACE] renamed first %s instance in transient state due to count argument change", addr) } - - rs, ok := mod.Resources[huntKey] - if !ok { - return - } - - // If the replacement key also exists then we do nothing and keep both. - if _, ok := mod.Resources[replaceKey]; ok { - return - } - - mod.Resources[replaceKey] = rs - delete(mod.Resources, huntKey) - log.Printf("[TRACE] renamed %s to %s in transient state due to count argument change", huntKey, replaceKey) } diff --git a/terraform/eval_count_boundary.go b/terraform/eval_count_boundary.go index 91e2b904e..5cc52d2a3 100644 --- a/terraform/eval_count_boundary.go +++ b/terraform/eval_count_boundary.go @@ -1,6 +1,7 @@ package terraform import ( + "fmt" "log" ) @@ -13,23 +14,26 @@ type EvalCountFixZeroOneBoundaryGlobal struct{} // TODO: test func (n *EvalCountFixZeroOneBoundaryGlobal) Eval(ctx EvalContext) (interface{}, error) { - // Get the state and lock it since we'll potentially modify it - state, lock := ctx.State() - lock.Lock() - defer lock.Unlock() + return nil, fmt.Errorf("EvalCountFixZeroOneBoundaryGlobal not yet updated for new state types") + /* + // Get the state and lock it since we'll potentially modify it + state, lock := ctx.State() + lock.Lock() + defer lock.Unlock() - // Prune the state since we require a clean state to work - state.prune() + // Prune the state since we require a clean state to work + state.prune() - // Go through each modules since the boundaries are restricted to a - // module scope. - for _, m := range state.Modules { - if err := n.fixModule(m); err != nil { - return nil, err + // Go through each modules since the boundaries are restricted to a + // module scope. + for _, m := range state.Modules { + if err := n.fixModule(m); err != nil { + return nil, err + } } - } - return nil, nil + return nil, nil + */ } func (n *EvalCountFixZeroOneBoundaryGlobal) fixModule(m *ModuleState) error { diff --git a/terraform/eval_diff.go b/terraform/eval_diff.go index ced760df2..74f2c2cff 100644 --- a/terraform/eval_diff.go +++ b/terraform/eval_diff.go @@ -11,66 +11,69 @@ import ( "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" - "github.com/hashicorp/terraform/tfdiags" - "github.com/hashicorp/terraform/version" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" ) // EvalCompareDiff is an EvalNode implementation that compares two diffs // and errors if the diffs are not equal. type EvalCompareDiff struct { Addr addrs.ResourceInstance - One, Two **InstanceDiff + One, Two **plans.ResourceInstanceChange } // TODO: test func (n *EvalCompareDiff) Eval(ctx EvalContext) (interface{}, error) { - one, two := *n.One, *n.Two + return nil, fmt.Errorf("TODO: Replace EvalCompareDiff with EvalCheckPlannedState") + /* + one, two := *n.One, *n.Two - // If either are nil, let them be empty - if one == nil { - one = new(InstanceDiff) - one.init() - } - if two == nil { - two = new(InstanceDiff) - two.init() - } - oneId, _ := one.GetAttribute("id") - twoId, _ := two.GetAttribute("id") - one.DelAttribute("id") - two.DelAttribute("id") - defer func() { - if oneId != nil { - one.SetAttribute("id", oneId) + // If either are nil, let them be empty + if one == nil { + one = new(InstanceDiff) + one.init() } - if twoId != nil { - two.SetAttribute("id", twoId) + if two == nil { + two = new(InstanceDiff) + two.init() } - }() + oneId, _ := one.GetAttribute("id") + twoId, _ := two.GetAttribute("id") + one.DelAttribute("id") + two.DelAttribute("id") + defer func() { + if oneId != nil { + one.SetAttribute("id", oneId) + } + if twoId != nil { + two.SetAttribute("id", twoId) + } + }() - if same, reason := one.Same(two); !same { - log.Printf("[ERROR] %s: diffs didn't match", n.Addr) - log.Printf("[ERROR] %s: reason: %s", n.Addr, reason) - log.Printf("[ERROR] %s: diff one: %#v", n.Addr, one) - log.Printf("[ERROR] %s: diff two: %#v", n.Addr, two) - return nil, fmt.Errorf( - "%s: diffs didn't match during apply. This is a bug with "+ - "Terraform and should be reported as a GitHub Issue.\n"+ - "\n"+ - "Please include the following information in your report:\n"+ - "\n"+ - " Terraform Version: %s\n"+ - " Resource ID: %s\n"+ - " Mismatch reason: %s\n"+ - " Diff One (usually from plan): %#v\n"+ - " Diff Two (usually from apply): %#v\n"+ - "\n"+ - "Also include as much context as you can about your config, state, "+ - "and the steps you performed to trigger this error.\n", - n.Addr, version.Version, n.Addr, reason, one, two) - } + if same, reason := one.Same(two); !same { + log.Printf("[ERROR] %s: diffs didn't match", n.Addr) + log.Printf("[ERROR] %s: reason: %s", n.Addr, reason) + log.Printf("[ERROR] %s: diff one: %#v", n.Addr, one) + log.Printf("[ERROR] %s: diff two: %#v", n.Addr, two) + return nil, fmt.Errorf( + "%s: diffs didn't match during apply. This is a bug with "+ + "Terraform and should be reported as a GitHub Issue.\n"+ + "\n"+ + "Please include the following information in your report:\n"+ + "\n"+ + " Terraform Version: %s\n"+ + " Resource ID: %s\n"+ + " Mismatch reason: %s\n"+ + " Diff One (usually from plan): %#v\n"+ + " Diff Two (usually from apply): %#v\n"+ + "\n"+ + "Also include as much context as you can about your config, state, "+ + "and the steps you performed to trigger this error.\n", + n.Addr, version.Version, n.Addr, reason, one, two) + } - return nil, nil + return nil, nil + */ } // EvalDiff is an EvalNode implementation that does a refresh for @@ -80,152 +83,155 @@ type EvalDiff struct { Config *configs.Resource Provider *ResourceProvider ProviderSchema **ProviderSchema - State **InstanceState - PreviousDiff **InstanceDiff + State **states.ResourceInstanceObject + PreviousDiff **plans.ResourceInstanceChange - OutputDiff **InstanceDiff - OutputValue *cty.Value - OutputState **InstanceState + OutputChange **plans.ResourceInstanceChange + OutputValue *cty.Value + OutputState **states.ResourceInstanceObject Stub bool } // TODO: test func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) { - state := *n.State - config := *n.Config - provider := *n.Provider - providerSchema := *n.ProviderSchema + return nil, fmt.Errorf("EvalDiff not yet updated for new state and plan types") + /* + state := *n.State + config := *n.Config + provider := *n.Provider + providerSchema := *n.ProviderSchema - if providerSchema == nil { - return nil, fmt.Errorf("provider schema is unavailable for %s", n.Addr) - } + if providerSchema == nil { + return nil, fmt.Errorf("provider schema is unavailable for %s", n.Addr) + } - var diags tfdiags.Diagnostics + var diags tfdiags.Diagnostics - // The provider and hook APIs still expect our legacy InstanceInfo type. - legacyInfo := NewInstanceInfo(n.Addr.Absolute(ctx.Path())) + // The provider and hook APIs still expect our legacy InstanceInfo type. + legacyInfo := NewInstanceInfo(n.Addr.Absolute(ctx.Path())) - // State still uses legacy-style internal ids, so we need to shim to get - // a suitable key to use. - stateId := NewLegacyResourceInstanceAddress(n.Addr.Absolute(ctx.Path())).stateId() + // State still uses legacy-style internal ids, so we need to shim to get + // a suitable key to use. + stateId := NewLegacyResourceInstanceAddress(n.Addr.Absolute(ctx.Path())).stateId() - // Call pre-diff hook - if !n.Stub { - err := ctx.Hook(func(h Hook) (HookAction, error) { - return h.PreDiff(legacyInfo, state) + // Call pre-diff hook + if !n.Stub { + err := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PreDiff(legacyInfo, state) + }) + if err != nil { + return nil, err + } + } + + // The state for the diff must never be nil + diffState := state + if diffState == nil { + diffState = new(InstanceState) + } + diffState.init() + + // Evaluate the configuration + schema := providerSchema.ResourceTypes[n.Addr.Resource.Type] + if schema == nil { + // Should be caught during validation, so we don't bother with a pretty error here + return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type) + } + keyData := EvalDataForInstanceKey(n.Addr.Key) + configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData) + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + return nil, diags.Err() + } + + // The provider API still expects our legacy ResourceConfig type. + legacyRC := NewResourceConfigShimmed(configVal, schema) + + // Diff! + diff, err := provider.Diff(legacyInfo, diffState, legacyRC) + if err != nil { + return nil, err + } + if diff == nil { + diff = new(InstanceDiff) + } + + // Set DestroyDeposed if we have deposed instances + _, err = readInstanceFromState(ctx, stateId, nil, func(rs *ResourceState) (*InstanceState, error) { + if len(rs.Deposed) > 0 { + diff.DestroyDeposed = true + } + + return nil, nil }) if err != nil { return nil, err } - } - // The state for the diff must never be nil - diffState := state - if diffState == nil { - diffState = new(InstanceState) - } - diffState.init() + // Preserve the DestroyTainted flag + if n.PreviousDiff != nil { + diff.SetTainted((*n.PreviousDiff).GetDestroyTainted()) + } - // Evaluate the configuration - schema := providerSchema.ResourceTypes[n.Addr.Resource.Type] - if schema == nil { - // Should be caught during validation, so we don't bother with a pretty error here - return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type) - } - keyData := EvalDataForInstanceKey(n.Addr.Key) - configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData) - diags = diags.Append(configDiags) - if configDiags.HasErrors() { - return nil, diags.Err() - } + // Require a destroy if there is an ID and it requires new. + if diff.RequiresNew() && state != nil && state.ID != "" { + diff.SetDestroy(true) + } - // The provider API still expects our legacy ResourceConfig type. - legacyRC := NewResourceConfigShimmed(configVal, schema) + // If we're creating a new resource, compute its ID + if diff.RequiresNew() || state == nil || state.ID == "" { + var oldID string + if state != nil { + oldID = state.Attributes["id"] + } - // Diff! - diff, err := provider.Diff(legacyInfo, diffState, legacyRC) - if err != nil { - return nil, err - } - if diff == nil { - diff = new(InstanceDiff) - } + // Add diff to compute new ID + diff.init() + diff.SetAttribute("id", &ResourceAttrDiff{ + Old: oldID, + NewComputed: true, + RequiresNew: true, + Type: DiffAttrOutput, + }) + } - // Set DestroyDeposed if we have deposed instances - _, err = readInstanceFromState(ctx, stateId, nil, func(rs *ResourceState) (*InstanceState, error) { - if len(rs.Deposed) > 0 { - diff.DestroyDeposed = true + // filter out ignored attributes + if err := n.processIgnoreChanges(diff); err != nil { + return nil, err + } + + // Call post-refresh hook + if !n.Stub { + err = ctx.Hook(func(h Hook) (HookAction, error) { + return h.PostDiff(legacyInfo, diff) + }) + if err != nil { + return nil, err + } + } + + // Update our output if we care + if n.OutputDiff != nil { + *n.OutputDiff = diff + } + + if n.OutputValue != nil { + *n.OutputValue = configVal + } + + // Update the state if we care + if n.OutputState != nil { + *n.OutputState = state + + // Merge our state so that the state is updated with our plan + if !diff.Empty() && n.OutputState != nil { + *n.OutputState = state.MergeDiff(diff) + } } return nil, nil - }) - if err != nil { - return nil, err - } - - // Preserve the DestroyTainted flag - if n.PreviousDiff != nil { - diff.SetTainted((*n.PreviousDiff).GetDestroyTainted()) - } - - // Require a destroy if there is an ID and it requires new. - if diff.RequiresNew() && state != nil && state.ID != "" { - diff.SetDestroy(true) - } - - // If we're creating a new resource, compute its ID - if diff.RequiresNew() || state == nil || state.ID == "" { - var oldID string - if state != nil { - oldID = state.Attributes["id"] - } - - // Add diff to compute new ID - diff.init() - diff.SetAttribute("id", &ResourceAttrDiff{ - Old: oldID, - NewComputed: true, - RequiresNew: true, - Type: DiffAttrOutput, - }) - } - - // filter out ignored attributes - if err := n.processIgnoreChanges(diff); err != nil { - return nil, err - } - - // Call post-refresh hook - if !n.Stub { - err = ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostDiff(legacyInfo, diff) - }) - if err != nil { - return nil, err - } - } - - // Update our output if we care - if n.OutputDiff != nil { - *n.OutputDiff = diff - } - - if n.OutputValue != nil { - *n.OutputValue = configVal - } - - // Update the state if we care - if n.OutputState != nil { - *n.OutputState = state - - // Merge our state so that the state is updated with our plan - if !diff.Empty() && n.OutputState != nil { - *n.OutputState = state.MergeDiff(diff) - } - } - - return nil, nil + */ } func (n *EvalDiff) processIgnoreChanges(diff *InstanceDiff) error { @@ -425,45 +431,68 @@ func groupContainers(d *InstanceDiff) map[string]flatAttrDiff { // EvalDiffDestroy is an EvalNode implementation that returns a plain // destroy diff. type EvalDiffDestroy struct { - Addr addrs.ResourceInstance - State **InstanceState - Output **InstanceDiff - OutputState **InstanceState + Addr addrs.ResourceInstance + DeposedKey states.DeposedKey + State **states.ResourceInstanceObject + ProviderAddr addrs.AbsProviderConfig + + Output **plans.ResourceInstanceChange + OutputState **states.ResourceInstanceObject } // TODO: test func (n *EvalDiffDestroy) Eval(ctx EvalContext) (interface{}, error) { + absAddr := n.Addr.Absolute(ctx.Path()) state := *n.State - // If there is no state or we don't have an ID, we're already destroyed - if state == nil || state.ID == "" { + // If there is no state or our attributes object is null then we're already + // destroyed. + if state == nil || state.Value.IsNull() { return nil, nil } - // The provider and hook APIs still expect our legacy InstanceInfo type. - legacyInfo := NewInstanceInfo(n.Addr.Absolute(ctx.Path())) - // Call pre-diff hook err := ctx.Hook(func(h Hook) (HookAction, error) { - return h.PreDiff(legacyInfo, state) + return h.PreDiff( + absAddr, n.DeposedKey.Generation(), + state.Value, + cty.NullVal(cty.DynamicPseudoType), + ) }) if err != nil { return nil, err } - // The diff - diff := &InstanceDiff{Destroy: true} + // Change is always the same for a destroy. We don't need the provider's + // help for this one. + // TODO: Should we give the provider an opportunity to veto this? + change := &plans.ResourceInstanceChange{ + Addr: absAddr, + DeposedKey: n.DeposedKey, + Change: plans.Change{ + Action: plans.Delete, + Before: state.Value, + After: cty.NullVal(cty.DynamicPseudoType), + }, + ProviderAddr: n.ProviderAddr, + } // Call post-diff hook err = ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostDiff(legacyInfo, diff) + return h.PostDiff( + absAddr, + n.DeposedKey.Generation(), + change.Action, + change.Before, + change.After, + ) }) if err != nil { return nil, err } // Update our output - *n.Output = diff + *n.Output = change if n.OutputState != nil { // Record our proposed new state, which is nil because we're destroying. @@ -481,113 +510,92 @@ type EvalDiffDestroyModule struct { // TODO: test func (n *EvalDiffDestroyModule) Eval(ctx EvalContext) (interface{}, error) { - diff, lock := ctx.Diff() + return nil, fmt.Errorf("EvalDiffDestroyModule not yet updated for new plan types") + /* + diff, lock := ctx.Diff() - // Acquire the lock so that we can do this safely concurrently - lock.Lock() - defer lock.Unlock() + // Acquire the lock so that we can do this safely concurrently + lock.Lock() + defer lock.Unlock() - // Write the diff - modDiff := diff.ModuleByPath(n.Path) - if modDiff == nil { - modDiff = diff.AddModule(n.Path) - } - modDiff.Destroy = true - - return nil, nil -} - -// EvalFilterDiff is an EvalNode implementation that filters the diff -// according to some filter. -type EvalFilterDiff struct { - // Input and output - Diff **InstanceDiff - Output **InstanceDiff - - // Destroy, if true, will only include a destroy diff if it is set. - Destroy bool -} - -func (n *EvalFilterDiff) Eval(ctx EvalContext) (interface{}, error) { - if *n.Diff == nil { - return nil, nil - } - - input := *n.Diff - result := new(InstanceDiff) - - if n.Destroy { - if input.GetDestroy() || input.RequiresNew() { - result.SetDestroy(true) + // Write the diff + modDiff := diff.ModuleByPath(n.Path) + if modDiff == nil { + modDiff = diff.AddModule(n.Path) } - } + modDiff.Destroy = true - if n.Output != nil { - *n.Output = result - } - - return nil, nil + return nil, nil + */ } -// EvalReadDiff is an EvalNode implementation that writes the diff to -// the full diff. +// EvalReadDiff is an EvalNode implementation that retrieves the planned +// change for a particular resource instance object. type EvalReadDiff struct { - Name string - Diff **InstanceDiff + Addr addrs.ResourceInstance + DeposedKey states.DeposedKey + Change **plans.ResourceInstanceChange } func (n *EvalReadDiff) Eval(ctx EvalContext) (interface{}, error) { - diff, lock := ctx.Diff() + return nil, fmt.Errorf("EvalReadDiff not yet updated for new plan types") + /* + diff, lock := ctx.Diff() - // Acquire the lock so that we can do this safely concurrently - lock.Lock() - defer lock.Unlock() + // Acquire the lock so that we can do this safely concurrently + lock.Lock() + defer lock.Unlock() + + // Write the diff + modDiff := diff.ModuleByPath(ctx.Path()) + if modDiff == nil { + return nil, nil + } + + *n.Diff = modDiff.Resources[n.Name] - // Write the diff - modDiff := diff.ModuleByPath(ctx.Path()) - if modDiff == nil { return nil, nil - } - - *n.Diff = modDiff.Resources[n.Name] - - return nil, nil + */ } -// EvalWriteDiff is an EvalNode implementation that writes the diff to -// the full diff. +// EvalWriteDiff is an EvalNode implementation that saves a planned change +// for an instance object into the set of global planned changes. type EvalWriteDiff struct { - Name string - Diff **InstanceDiff + Addr addrs.ResourceInstance + DeposedKey states.DeposedKey + Change **plans.ResourceInstanceChange } // TODO: test func (n *EvalWriteDiff) Eval(ctx EvalContext) (interface{}, error) { - diff, lock := ctx.Diff() + return nil, fmt.Errorf("EvalWriteDiff not yet updated for new plan types") + /* + diff, lock := ctx.Diff() - // The diff to write, if its empty it should write nil - var diffVal *InstanceDiff - if n.Diff != nil { - diffVal = *n.Diff - } - if diffVal.Empty() { - diffVal = nil - } + // The diff to write, if its empty it should write nil + var diffVal *InstanceDiff + if n.Diff != nil { + diffVal = *n.Diff + } + if diffVal.Empty() { + diffVal = nil + } - // Acquire the lock so that we can do this safely concurrently - lock.Lock() - defer lock.Unlock() + // Acquire the lock so that we can do this safely concurrently + lock.Lock() + defer lock.Unlock() - // Write the diff - modDiff := diff.ModuleByPath(ctx.Path()) - if modDiff == nil { - modDiff = diff.AddModule(ctx.Path()) - } - if diffVal != nil { - modDiff.Resources[n.Name] = diffVal - } else { - delete(modDiff.Resources, n.Name) - } + // Write the diff + modDiff := diff.ModuleByPath(ctx.Path()) + if modDiff == nil { + modDiff = diff.AddModule(ctx.Path()) + } + if diffVal != nil { + modDiff.Resources[n.Name] = diffVal + } else { + delete(modDiff.Resources, n.Name) + } - return nil, nil + return nil, nil + */ } diff --git a/terraform/eval_diff_test.go b/terraform/eval_diff_test.go index ed0128ac6..2c49dd74b 100644 --- a/terraform/eval_diff_test.go +++ b/terraform/eval_diff_test.go @@ -2,7 +2,6 @@ package terraform import ( "fmt" - "reflect" "testing" "github.com/hashicorp/hcl2/hcl/hclsyntax" @@ -11,78 +10,6 @@ import ( "github.com/hashicorp/terraform/configs" ) -func TestEvalFilterDiff(t *testing.T) { - ctx := new(MockEvalContext) - - cases := []struct { - Node *EvalFilterDiff - Input *InstanceDiff - Output *InstanceDiff - }{ - // With no settings, it returns an empty diff - { - &EvalFilterDiff{}, - &InstanceDiff{Destroy: true}, - &InstanceDiff{}, - }, - - // Destroy - { - &EvalFilterDiff{Destroy: true}, - &InstanceDiff{Destroy: false}, - &InstanceDiff{Destroy: false}, - }, - { - &EvalFilterDiff{Destroy: true}, - &InstanceDiff{Destroy: true}, - &InstanceDiff{Destroy: true}, - }, - { - &EvalFilterDiff{Destroy: true}, - &InstanceDiff{ - Destroy: true, - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{}, - }, - }, - &InstanceDiff{Destroy: true}, - }, - { - &EvalFilterDiff{Destroy: true}, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - RequiresNew: true, - }, - }, - }, - &InstanceDiff{Destroy: true}, - }, - { - &EvalFilterDiff{Destroy: true}, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{}, - }, - }, - &InstanceDiff{Destroy: false}, - }, - } - - for i, tc := range cases { - var output *InstanceDiff - tc.Node.Diff = &tc.Input - tc.Node.Output = &output - if _, err := tc.Node.Eval(ctx); err != nil { - t.Fatalf("err: %s", err) - } - - if !reflect.DeepEqual(output, tc.Output) { - t.Fatalf("bad: %d\n\n%#v", i, output) - } - } -} - func TestProcessIgnoreChanges(t *testing.T) { var evalDiff *EvalDiff var instanceDiff *InstanceDiff diff --git a/terraform/eval_import_state.go b/terraform/eval_import_state.go index 23f88155c..0e5ee8c5e 100644 --- a/terraform/eval_import_state.go +++ b/terraform/eval_import_state.go @@ -2,9 +2,9 @@ package terraform import ( "fmt" - "log" "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" ) @@ -12,64 +12,66 @@ import ( // ImportState operation on a provider. This will return the imported // states but won't modify any actual state. type EvalImportState struct { + Addr addrs.ResourceInstance Provider *ResourceProvider - Info *InstanceInfo Id string - Output *[]*InstanceState + Output *[]*states.ImportedObject } // TODO: test func (n *EvalImportState) Eval(ctx EvalContext) (interface{}, error) { - provider := *n.Provider + return nil, fmt.Errorf("EvalImportState not yet updated for new state/provider types") + /* + absAddr := n.Addr.Absolute(ctx.Path()) + provider := *n.Provider - { - // Call pre-import hook - err := ctx.Hook(func(h Hook) (HookAction, error) { - return h.PreImportState(n.Info, n.Id) - }) + { + // Call pre-import hook + err := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PreImportState(absAddr, n.Id) + }) + if err != nil { + return nil, err + } + } + + // Import! + state, err := provider.ImportState(n.Info, n.Id) if err != nil { - return nil, err + return nil, fmt.Errorf("import %s (id: %s): %s", absAddr.String(), n.Id, err) } - } - // Import! - state, err := provider.ImportState(n.Info, n.Id) - if err != nil { - return nil, fmt.Errorf( - "import %s (id: %s): %s", n.Info.HumanId(), n.Id, err) - } - - for _, s := range state { - if s == nil { - log.Printf("[TRACE] EvalImportState: import %s %q produced a nil state", n.Info.HumanId(), n.Id) - continue + for _, s := range state { + if s == nil { + log.Printf("[TRACE] EvalImportState: import %s %q produced a nil state", absAddr.String(), n.Id) + continue + } + log.Printf("[TRACE] EvalImportState: import %s %q produced state for %s with id %q", absAddr.String(), n.Id, s.Ephemeral.Type, s.ID) } - log.Printf("[TRACE] EvalImportState: import %s %q produced state for %s with id %q", n.Info.HumanId(), n.Id, s.Ephemeral.Type, s.ID) - } - if n.Output != nil { - *n.Output = state - } - - { - // Call post-import hook - err := ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostImportState(n.Info, state) - }) - if err != nil { - return nil, err + if n.Output != nil { + *n.Output = state } - } - return nil, nil + { + // Call post-import hook + err := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PostImportState(n.Info, state) + }) + if err != nil { + return nil, err + } + } + + return nil, nil + */ } // EvalImportStateVerify verifies the state after ImportState and // after the refresh to make sure it is non-nil and valid. type EvalImportStateVerify struct { Addr addrs.ResourceInstance - Id string - State **InstanceState + State **states.ResourceInstanceObject } // TODO: test @@ -77,13 +79,13 @@ func (n *EvalImportStateVerify) Eval(ctx EvalContext) (interface{}, error) { var diags tfdiags.Diagnostics state := *n.State - if state.Empty() { + if state.Value.IsNull() { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Cannot import non-existent remote object", fmt.Sprintf( - "While attempting to import an existing object to %s, the provider detected that no object exists with the id %q. Only pre-existing objects can be imported; check that the id is correct and that it is associated with the provider's configured region or endpoint, or use \"terraform apply\" to create a new remote object for this resource.", - n.Addr.String(), n.Id, + "While attempting to import an existing object to %s, the provider detected that no object exists with the given id. Only pre-existing objects can be imported; check that the id is correct and that it is associated with the provider's configured region or endpoint, or use \"terraform apply\" to create a new remote object for this resource.", + n.Addr.String(), ), )) } diff --git a/terraform/eval_local.go b/terraform/eval_local.go index 6f21f0308..bfed17816 100644 --- a/terraform/eval_local.go +++ b/terraform/eval_local.go @@ -5,7 +5,6 @@ import ( "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/terraform/addrs" - "github.com/hashicorp/terraform/config/hcl2shim" "github.com/zclconf/go-cty/cty" ) @@ -23,30 +22,12 @@ func (n *EvalLocal) Eval(ctx EvalContext) (interface{}, error) { return nil, diags.Err() } - state, lock := ctx.State() + state := ctx.State() if state == nil { return nil, fmt.Errorf("cannot write local value to nil state") } - // Get a write lock so we can access the state - lock.Lock() - defer lock.Unlock() - - // Look for the module state. If we don't have one, create it. - mod := state.ModuleByPath(ctx.Path()) - if mod == nil { - mod = state.AddModule(ctx.Path()) - } - - // Lower the value to the legacy form that our state structures still expect. - // FIXME: Update mod.Locals to be a map[string]cty.Value . - legacyVal := hcl2shim.ConfigValueFromHCL2(val) - - if mod.Locals == nil { - // initialize - mod.Locals = map[string]interface{}{} - } - mod.Locals[n.Addr.Name] = legacyVal + state.SetLocalValue(n.Addr.Absolute(ctx.Path()), val) return nil, nil } @@ -59,22 +40,11 @@ type EvalDeleteLocal struct { } func (n *EvalDeleteLocal) Eval(ctx EvalContext) (interface{}, error) { - state, lock := ctx.State() + state := ctx.State() if state == nil { return nil, nil } - // Get a write lock so we can access this instance - lock.Lock() - defer lock.Unlock() - - // Look for the module state. If we don't have one, create it. - mod := state.ModuleByPath(ctx.Path()) - if mod == nil { - return nil, nil - } - - delete(mod.Locals, n.Addr.Name) - + state.RemoveLocalValue(n.Addr.Absolute(ctx.Path())) return nil, nil } diff --git a/terraform/eval_local_test.go b/terraform/eval_local_test.go index 4e644b917..d1cdfdfaa 100644 --- a/terraform/eval_local_test.go +++ b/terraform/eval_local_test.go @@ -2,17 +2,16 @@ package terraform import ( "reflect" - "sync" "testing" - "github.com/hashicorp/terraform/config/hcl2shim" - + "github.com/davecgh/go-spew/spew" "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl/hclsyntax" + "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/addrs" - - "github.com/davecgh/go-spew/spew" + "github.com/hashicorp/terraform/config/hcl2shim" + "github.com/hashicorp/terraform/states" ) func TestEvalLocal_impl(t *testing.T) { @@ -49,8 +48,7 @@ func TestEvalLocal(t *testing.T) { Expr: expr, } ctx := &MockEvalContext{ - StateState: &State{}, - StateLock: &sync.RWMutex{}, + StateState: states.NewState().SyncWrapper(), EvaluateExprResult: hcl2shim.HCL2ValueFromConfigValue(test.Want), } @@ -64,10 +62,10 @@ func TestEvalLocal(t *testing.T) { } } - ms := ctx.StateState.ModuleByPath(addrs.RootModuleInstance) - gotLocals := ms.Locals - wantLocals := map[string]interface{}{ - "foo": test.Want, + ms := ctx.StateState.Module(addrs.RootModuleInstance) + gotLocals := ms.LocalValues + wantLocals := map[string]cty.Value{ + "foo": hcl2shim.HCL2ValueFromConfigValue(test.Want), } if !reflect.DeepEqual(gotLocals, wantLocals) { diff --git a/terraform/eval_output.go b/terraform/eval_output.go index c1b501a55..8bd626143 100644 --- a/terraform/eval_output.go +++ b/terraform/eval_output.go @@ -1,17 +1,12 @@ package terraform import ( - "fmt" "log" "github.com/hashicorp/hcl2/hcl" "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/convert" - "github.com/zclconf/go-cty/cty/gocty" "github.com/hashicorp/terraform/addrs" - "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/config/hcl2shim" ) // EvalDeleteOutput is an EvalNode implementation that deletes an output @@ -22,23 +17,12 @@ type EvalDeleteOutput struct { // TODO: test func (n *EvalDeleteOutput) Eval(ctx EvalContext) (interface{}, error) { - state, lock := ctx.State() + state := ctx.State() if state == nil { return nil, nil } - // Get a write lock so we can access this instance - lock.Lock() - defer lock.Unlock() - - // Look for the module state. If we don't have one, create it. - mod := state.ModuleByPath(ctx.Path()) - if mod == nil { - return nil, nil - } - - delete(mod.Outputs, n.Addr.Name) - + state.RemoveOutputValue(n.Addr.Absolute(ctx.Path())) return nil, nil } @@ -59,98 +43,26 @@ func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) { val, diags := ctx.EvaluateExpr(n.Expr, cty.DynamicPseudoType, nil) // We'll handle errors below, after we have loaded the module. - state, lock := ctx.State() + state := ctx.State() if state == nil { - return nil, fmt.Errorf("cannot write state to nil state") + return nil, nil } - // Get a write lock so we can access this instance - lock.Lock() - defer lock.Unlock() - // Look for the module state. If we don't have one, create it. - mod := state.ModuleByPath(ctx.Path()) - if mod == nil { - mod = state.AddModule(ctx.Path()) - } + addr := n.Addr.Absolute(ctx.Path()) // handling the interpolation error if diags.HasErrors() { if n.ContinueOnErr || flagWarnOutputErrors { log.Printf("[ERROR] Output interpolation %q failed: %s", n.Addr.Name, diags.Err()) // if we're continuing, make sure the output is included, and - // marked as unknown - mod.Outputs[n.Addr.Name] = &OutputState{ - Type: "string", - Value: config.UnknownVariableValue, - } + // marked as unknown. If the evaluator was able to find a type + // for the value in spite of the error then we'll use it. + state.SetOutputValue(addr, cty.UnknownVal(val.Type()), n.Sensitive) return nil, EvalEarlyExitError{} } return nil, diags.Err() } - ty := val.Type() - switch { - case ty.IsPrimitiveType(): - // For now we record all primitive types as strings, for compatibility - // with our existing state formats. - // FIXME: Revise the state format to support any type. - var valueTyped string - switch { - case !val.IsKnown(): - // Legacy handling of unknown values as a special string. - valueTyped = config.UnknownVariableValue - case val.IsNull(): - // State doesn't currently support null, so we'll save as empty string. - valueTyped = "" - default: - strVal, err := convert.Convert(val, cty.String) - if err != nil { - // Should never happen, because all primitives can convert to string. - return nil, fmt.Errorf("cannot marshal %#v for storage in state: %s", val, err) - } - err = gocty.FromCtyValue(strVal, &valueTyped) - if err != nil { - // Should never happen, because we already converted to string. - return nil, fmt.Errorf("cannot marshal %#v for storage in state: %s", val, err) - } - } - mod.Outputs[n.Addr.Name] = &OutputState{ - Type: "string", - Sensitive: n.Sensitive, - Value: valueTyped, - } - case ty.IsListType() || ty.IsTupleType() || ty.IsSetType(): - // For now we'll use our legacy storage forms for list-like types. - // This produces a []interface{}. - valueTyped := hcl2shim.ConfigValueFromHCL2(val) - mod.Outputs[n.Addr.Name] = &OutputState{ - Type: "list", - Sensitive: n.Sensitive, - Value: valueTyped, - } - case ty.IsMapType() || ty.IsObjectType(): - // For now we'll use our legacy storage forms for map-like types. - // This produces a map[string]interface{}. - valueTyped := hcl2shim.ConfigValueFromHCL2(val) - mod.Outputs[n.Addr.Name] = &OutputState{ - Type: "map", - Sensitive: n.Sensitive, - Value: valueTyped, - } - case ty == cty.DynamicPseudoType || !val.IsWhollyKnown(): - // While we're still using our existing state format, we can't represent - // partially-unknown values properly, so we'll just stub the whole - // thing out. - // FIXME: After the state format is revised, remove this special case - // and just store the unknown value directly. - mod.Outputs[n.Addr.Name] = &OutputState{ - Type: "unknown", - Sensitive: n.Sensitive, - Value: hcl2shim.UnknownVariableValue, - } - default: - return nil, fmt.Errorf("output %s is not a valid type (%s)", n.Addr.Name, ty.FriendlyName()) - } - + state.SetOutputValue(addr, val, n.Sensitive) return nil, nil } diff --git a/terraform/eval_output_test.go b/terraform/eval_output_test.go index 87d3fe5a3..7d21bbd2e 100644 --- a/terraform/eval_output_test.go +++ b/terraform/eval_output_test.go @@ -1,17 +1,17 @@ package terraform import ( - "sync" "testing" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/addrs" "github.com/zclconf/go-cty/cty" ) func TestEvalWriteMapOutput(t *testing.T) { ctx := new(MockEvalContext) - ctx.StateState = NewState() - ctx.StateLock = new(sync.RWMutex) + ctx.StateState = states.NewState().SyncWrapper() cases := []struct { name string diff --git a/terraform/eval_provider.go b/terraform/eval_provider.go index 59d5d3b42..d985c1bdd 100644 --- a/terraform/eval_provider.go +++ b/terraform/eval_provider.go @@ -127,6 +127,11 @@ type EvalGetProvider struct { } func (n *EvalGetProvider) Eval(ctx EvalContext) (interface{}, error) { + if n.Addr.ProviderConfig.Type == "" { + // Should never happen + panic("EvalGetProvider used with uninitialized provider configuration address") + } + result := ctx.Provider(n.Addr) if result == nil { return nil, fmt.Errorf("provider %s not initialized", n.Addr) diff --git a/terraform/eval_read_data.go b/terraform/eval_read_data.go index c0c37d0e0..c8bf3476b 100644 --- a/terraform/eval_read_data.go +++ b/terraform/eval_read_data.go @@ -3,11 +3,13 @@ package terraform import ( "fmt" - "github.com/hashicorp/terraform/tfdiags" + "github.com/hashicorp/terraform/states" + + "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" - "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/plans" ) // EvalReadDataDiff is an EvalNode implementation that executes a data @@ -18,111 +20,114 @@ type EvalReadDataDiff struct { Provider *ResourceProvider ProviderSchema **ProviderSchema - Output **InstanceDiff + Output **plans.ResourceInstanceChange OutputValue *cty.Value - OutputState **InstanceState + OutputState **states.ResourceInstanceObject // Set Previous when re-evaluating diff during apply, to ensure that // the "Destroy" flag is preserved. - Previous **InstanceDiff + Previous **plans.ResourceInstanceChange } func (n *EvalReadDataDiff) Eval(ctx EvalContext) (interface{}, error) { - // TODO: test + return nil, fmt.Errorf("EvalReadDataDiff not yet updated for new state/plan/provider types") + /* + absAddr := n.Addr.Absolute(ctx.Path()) - if n.ProviderSchema == nil || *n.ProviderSchema == nil { - return nil, fmt.Errorf("provider schema not available for %s", n.Addr) - } - - var diags tfdiags.Diagnostics - - // The provider and hook APIs still expect our legacy InstanceInfo type. - legacyInfo := NewInstanceInfo(n.Addr.Absolute(ctx.Path())) - - err := ctx.Hook(func(h Hook) (HookAction, error) { - return h.PreDiff(legacyInfo, nil) - }) - if err != nil { - return nil, err - } - - var diff *InstanceDiff - var configVal cty.Value - - if n.Previous != nil && *n.Previous != nil && (*n.Previous).GetDestroy() { - // If we're re-diffing for a diff that was already planning to - // destroy, then we'll just continue with that plan. - diff = &InstanceDiff{Destroy: true} - } else { - provider := *n.Provider - config := *n.Config - providerSchema := *n.ProviderSchema - schema := providerSchema.DataSources[n.Addr.Resource.Type] - if schema == nil { - // Should be caught during validation, so we don't bother with a pretty error here - return nil, fmt.Errorf("provider does not support data source %q", n.Addr.Resource.Type) + if n.ProviderSchema == nil || *n.ProviderSchema == nil { + return nil, fmt.Errorf("provider schema not available for %s", n.Addr) } - keyData := EvalDataForInstanceKey(n.Addr.Key) + var diags tfdiags.Diagnostics - var configDiags tfdiags.Diagnostics - configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema, nil, keyData) - diags = diags.Append(configDiags) - if configDiags.HasErrors() { - return nil, diags.Err() - } + // The provider API still expects our legacy InstanceInfo type. + legacyInfo := NewInstanceInfo(n.Addr.Absolute(ctx.Path())) - // The provider API still expects our legacy ResourceConfig type. - legacyRC := NewResourceConfigShimmed(configVal, schema) - - var err error - diff, err = provider.ReadDataDiff(legacyInfo, legacyRC) + err := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PreDiff(absAddr, cty.NullVal(cty.DynamicPseudoType), cty.NullVal(cty.DynamicPseudoType)) + }) if err != nil { - diags = diags.Append(err) - return nil, diags.Err() - } - if diff == nil { - diff = new(InstanceDiff) + return nil, err } - // if id isn't explicitly set then it's always computed, because we're - // always "creating a new resource". - diff.init() - if _, ok := diff.Attributes["id"]; !ok { - diff.SetAttribute("id", &ResourceAttrDiff{ - Old: "", - NewComputed: true, - RequiresNew: true, - Type: DiffAttrOutput, - }) + var diff *InstanceDiff + var configVal cty.Value + + if n.Previous != nil && *n.Previous != nil && (*n.Previous).GetDestroy() { + // If we're re-diffing for a diff that was already planning to + // destroy, then we'll just continue with that plan. + diff = &InstanceDiff{Destroy: true} + } else { + provider := *n.Provider + config := *n.Config + providerSchema := *n.ProviderSchema + schema := providerSchema.DataSources[n.Addr.Resource.Type] + if schema == nil { + // Should be caught during validation, so we don't bother with a pretty error here + return nil, fmt.Errorf("provider does not support data source %q", n.Addr.Resource.Type) + } + + keyData := EvalDataForInstanceKey(n.Addr.Key) + + var configDiags tfdiags.Diagnostics + configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema, nil, keyData) + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + return nil, diags.Err() + } + + // The provider API still expects our legacy ResourceConfig type. + legacyRC := NewResourceConfigShimmed(configVal, schema) + + var err error + diff, err = provider.ReadDataDiff(legacyInfo, legacyRC) + if err != nil { + diags = diags.Append(err) + return nil, diags.Err() + } + if diff == nil { + diff = new(InstanceDiff) + } + + // if id isn't explicitly set then it's always computed, because we're + // always "creating a new resource". + diff.init() + if _, ok := diff.Attributes["id"]; !ok { + diff.SetAttribute("id", &ResourceAttrDiff{ + Old: "", + NewComputed: true, + RequiresNew: true, + Type: DiffAttrOutput, + }) + } } - } - err = ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostDiff(legacyInfo, diff) - }) - if err != nil { - return nil, err - } - - *n.Output = diff - - if n.OutputValue != nil { - *n.OutputValue = configVal - } - - if n.OutputState != nil { - state := &InstanceState{} - *n.OutputState = state - - // Apply the diff to the returned state, so the state includes - // any attribute values that are not computed. - if !diff.Empty() && n.OutputState != nil { - *n.OutputState = state.MergeDiff(diff) + err = ctx.Hook(func(h Hook) (HookAction, error) { + return h.PostDiff(legacyInfo, diff) + }) + if err != nil { + return nil, err } - } - return nil, diags.ErrWithWarnings() + *n.Output = diff + + if n.OutputValue != nil { + *n.OutputValue = configVal + } + + if n.OutputState != nil { + state := &InstanceState{} + *n.OutputState = state + + // Apply the diff to the returned state, so the state includes + // any attribute values that are not computed. + if !diff.Empty() && n.OutputState != nil { + *n.OutputState = state.MergeDiff(diff) + } + } + + return nil, diags.ErrWithWarnings() + */ } // EvalReadDataApply is an EvalNode implementation that executes a data @@ -130,55 +135,58 @@ func (n *EvalReadDataDiff) Eval(ctx EvalContext) (interface{}, error) { type EvalReadDataApply struct { Addr addrs.ResourceInstance Provider *ResourceProvider - Output **InstanceState - Diff **InstanceDiff + Output **states.ResourceInstanceObject + Change **plans.ResourceInstanceChange } func (n *EvalReadDataApply) Eval(ctx EvalContext) (interface{}, error) { - // TODO: test - provider := *n.Provider - diff := *n.Diff + return nil, fmt.Errorf("EvalReadDataApply not yet updated for new state/plan/provider types") + /* + provider := *n.Provider + change := *n.Change + absAddr := n.Addr.Absolute(ctx.Path()) - // The provider and hook APIs still expect our legacy InstanceInfo type. - legacyInfo := NewInstanceInfo(n.Addr.Absolute(ctx.Path())) + // The provider and hook APIs still expect our legacy InstanceInfo type. + legacyInfo := NewInstanceInfo(n.Addr.Absolute(ctx.Path())) - // If the diff is for *destroying* this resource then we'll - // just drop its state and move on, since data resources don't - // support an actual "destroy" action. - if diff != nil && diff.GetDestroy() { - if n.Output != nil { - *n.Output = nil + // If the diff is for *destroying* this resource then we'll + // just drop its state and move on, since data resources don't + // support an actual "destroy" action. + if diff != nil && diff.GetDestroy() { + if n.Output != nil { + *n.Output = nil + } + return nil, nil } + + // For the purpose of external hooks we present a data apply as a + // "Refresh" rather than an "Apply" because creating a data source + // is presented to users/callers as a "read" operation. + err := ctx.Hook(func(h Hook) (HookAction, error) { + // We don't have a state yet, so we'll just give the hook an + // empty one to work with. + return h.PreRefresh(absAddr, cty.NullVal(cty.DynamicPseudoType)) + }) + if err != nil { + return nil, err + } + + state, err := provider.ReadDataApply(legacyInfo, diff) + if err != nil { + return nil, fmt.Errorf("%s: %s", n.Addr.Absolute(ctx.Path()).String(), err) + } + + err = ctx.Hook(func(h Hook) (HookAction, error) { + return h.PostRefresh(absAddr, state) + }) + if err != nil { + return nil, err + } + + if n.Output != nil { + *n.Output = state + } + return nil, nil - } - - // For the purpose of external hooks we present a data apply as a - // "Refresh" rather than an "Apply" because creating a data source - // is presented to users/callers as a "read" operation. - err := ctx.Hook(func(h Hook) (HookAction, error) { - // We don't have a state yet, so we'll just give the hook an - // empty one to work with. - return h.PreRefresh(legacyInfo, &InstanceState{}) - }) - if err != nil { - return nil, err - } - - state, err := provider.ReadDataApply(legacyInfo, diff) - if err != nil { - return nil, fmt.Errorf("%s: %s", n.Addr.Absolute(ctx.Path()).String(), err) - } - - err = ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostRefresh(legacyInfo, state) - }) - if err != nil { - return nil, err - } - - if n.Output != nil { - *n.Output = state - } - - return nil, nil + */ } diff --git a/terraform/eval_refresh.go b/terraform/eval_refresh.go index fab32f779..842ebc577 100644 --- a/terraform/eval_refresh.go +++ b/terraform/eval_refresh.go @@ -5,6 +5,7 @@ import ( "log" "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/states" ) // EvalRefresh is an EvalNode implementation that does a refresh for @@ -12,17 +13,14 @@ import ( type EvalRefresh struct { Addr addrs.ResourceInstance Provider *ResourceProvider - State **InstanceState - Output **InstanceState + State **states.ResourceInstanceObject + Output **states.ResourceInstanceObject } // TODO: test func (n *EvalRefresh) Eval(ctx EvalContext) (interface{}, error) { - provider := *n.Provider state := *n.State - - // The provider and hook APIs still expect our legacy InstanceInfo type. - legacyInfo := NewInstanceInfo(n.Addr.Absolute(ctx.Path())) + absAddr := n.Addr.Absolute(ctx.Path()) // If we have no state, we don't do any refreshing if state == nil { @@ -32,14 +30,18 @@ func (n *EvalRefresh) Eval(ctx EvalContext) (interface{}, error) { // Call pre-refresh hook err := ctx.Hook(func(h Hook) (HookAction, error) { - return h.PreRefresh(legacyInfo, state) + return h.PreRefresh(absAddr, states.CurrentGen, state.Value) }) if err != nil { return nil, err } // Refresh! - state, err = provider.Refresh(legacyInfo, state) + priorVal := state.Value + // TODO: Shim our new state type into the old one + //provider := *n.Provider + //state, err = provider.Refresh(legacyInfo, state) + return nil, fmt.Errorf("EvalRefresh is not yet updated for new state type") if err != nil { return nil, fmt.Errorf("%s: %s", n.Addr.Absolute(ctx.Path()), err.Error()) } @@ -49,7 +51,7 @@ func (n *EvalRefresh) Eval(ctx EvalContext) (interface{}, error) { // Call post-refresh hook err = ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostRefresh(legacyInfo, state) + return h.PostRefresh(absAddr, states.CurrentGen, priorVal, state.Value) }) if err != nil { return nil, err diff --git a/terraform/eval_state.go b/terraform/eval_state.go index 596e5659e..791ca2517 100644 --- a/terraform/eval_state.go +++ b/terraform/eval_state.go @@ -2,94 +2,121 @@ package terraform import ( "fmt" - "log" "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/states" ) // EvalReadState is an EvalNode implementation that reads the -// primary InstanceState for a specific resource out of the state. +// current object for a specific instance in the state. type EvalReadState struct { - Name string - Output **InstanceState + // Addr is the address of the instance to read state for. + Addr addrs.ResourceInstance + + // ProviderSchema is the schema for the provider given in Provider. + ProviderSchema **ProviderSchema + + // Provider is the provider that will subsequently perform actions on + // the the state object. This is used to perform any schema upgrades + // that might be required to prepare the stored data for use. + Provider *ResourceProvider + + // Output will be written with a pointer to the retrieved object. + Output **states.ResourceInstanceObject } func (n *EvalReadState) Eval(ctx EvalContext) (interface{}, error) { - return readInstanceFromState(ctx, n.Name, n.Output, func(rs *ResourceState) (*InstanceState, error) { - return rs.Primary, nil - }) + if n.Provider == nil || *n.Provider == nil { + panic("EvalReadState used with no Provider object") + } + if n.ProviderSchema == nil || *n.ProviderSchema == nil { + panic("EvalReadState used with no ProviderSchema object") + } + + absAddr := n.Addr.Absolute(ctx.Path()) + src := ctx.State().ResourceInstanceObject(absAddr, states.CurrentGen) + if src == nil { + // Presumably we only have deposed objects, then. + return nil, nil + } + + // TODO: Update n.ResourceTypeSchema to be a providers.Schema and then + // check the version number here and upgrade if necessary. + /* + if src.SchemaVersion < n.ResourceTypeSchema.Version { + // TODO: Implement schema upgrades + return nil, fmt.Errorf("schema upgrading is not yet implemented to take state from version %d to version %d", src.SchemaVersion, n.ResourceTypeSchema.Version) + } + */ + + schema := (*n.ProviderSchema).ResourceTypes[absAddr.Resource.Resource.Type] + obj, err := src.Decode(schema.ImpliedType()) + if err != nil { + return nil, err + } + if n.Output != nil { + *n.Output = obj + } + return obj, nil } // EvalReadStateDeposed is an EvalNode implementation that reads the // deposed InstanceState for a specific resource out of the state type EvalReadStateDeposed struct { - Name string - Output **InstanceState - // Index indicates which instance in the Deposed list to target, or -1 for - // the last item. - Index int + // Addr is the address of the instance to read state for. + Addr addrs.ResourceInstance + + // Key identifies which deposed object we will read. + Key states.DeposedKey + + // ProviderSchema is the schema for the provider given in Provider. + ProviderSchema **ProviderSchema + + // Provider is the provider that will subsequently perform actions on + // the the state object. This is used to perform any schema upgrades + // that might be required to prepare the stored data for use. + Provider *ResourceProvider + + // Output will be written with a pointer to the retrieved object. + Output **states.ResourceInstanceObject } func (n *EvalReadStateDeposed) Eval(ctx EvalContext) (interface{}, error) { - return readInstanceFromState(ctx, n.Name, n.Output, func(rs *ResourceState) (*InstanceState, error) { - // Get the index. If it is negative, then we get the last one - idx := n.Index - if idx < 0 { - idx = len(rs.Deposed) - 1 - } - if idx >= 0 && idx < len(rs.Deposed) { - return rs.Deposed[idx], nil - } else { - return nil, fmt.Errorf("bad deposed index: %d, for resource: %#v", idx, rs) - } - }) -} - -// Does the bulk of the work for the various flavors of ReadState eval nodes. -// Each node just provides a reader function to get from the ResourceState to the -// InstanceState, and this takes care of all the plumbing. -func readInstanceFromState( - ctx EvalContext, - resourceName string, - output **InstanceState, - readerFn func(*ResourceState) (*InstanceState, error), -) (*InstanceState, error) { - state, lock := ctx.State() - - // Get a read lock so we can access this instance - lock.RLock() - defer lock.RUnlock() - - // Look for the module state. If we don't have one, then it doesn't matter. - mod := state.ModuleByPath(ctx.Path()) - if mod == nil { + key := n.Key + if key == states.NotDeposed { + return nil, fmt.Errorf("EvalReadStateDeposed used with no instance key; this is a bug in Terraform and should be reported") + } + absAddr := n.Addr.Absolute(ctx.Path()) + src := ctx.State().ResourceInstanceObject(absAddr, key) + if src == nil { + // Presumably we only have deposed objects, then. return nil, nil } - // Look for the resource state. If we don't have one, then it is okay. - rs := mod.Resources[resourceName] - if rs == nil { - return nil, nil - } + // TODO: Update n.ResourceTypeSchema to be a providers.Schema and then + // check the version number here and upgrade if necessary. + /* + if src.SchemaVersion < n.ResourceTypeSchema.Version { + // TODO: Implement schema upgrades + return nil, fmt.Errorf("schema upgrading is not yet implemented to take state from version %d to version %d", src.SchemaVersion, n.ResourceTypeSchema.Version) + } + */ - // Use the delegate function to get the instance state from the resource state - is, err := readerFn(rs) + schema := (*n.ProviderSchema).ResourceTypes[absAddr.Resource.Resource.Type] + obj, err := src.Decode(schema.ImpliedType()) if err != nil { return nil, err } - - // Write the result to the output pointer - if output != nil { - *output = is + if n.Output != nil { + *n.Output = obj } - - return is, nil + return obj, nil } -// EvalRequireState is an EvalNode implementation that early exits -// if the state doesn't have an ID. +// EvalRequireState is an EvalNode implementation that exits early if the given +// object is null. type EvalRequireState struct { - State **InstanceState + State **states.ResourceInstanceObject } func (n *EvalRequireState) Eval(ctx EvalContext) (interface{}, error) { @@ -98,7 +125,7 @@ func (n *EvalRequireState) Eval(ctx EvalContext) (interface{}, error) { } state := *n.State - if state == nil || state.ID == "" { + if state == nil || state.Value.IsNull() { return nil, EvalEarlyExitError{} } @@ -110,12 +137,14 @@ func (n *EvalRequireState) Eval(ctx EvalContext) (interface{}, error) { type EvalUpdateStateHook struct{} func (n *EvalUpdateStateHook) Eval(ctx EvalContext) (interface{}, error) { - state, lock := ctx.State() - - // Get a full lock. Even calling something like WriteState can modify - // (prune) the state, so we need the full lock. - lock.Lock() - defer lock.Unlock() + // In principle we could grab the lock here just long enough to take a + // deep copy and then pass that to our hooks below, but we'll instead + // hold the hook for the duration to avoid the potential confusing + // situation of us racing to call PostStateUpdate concurrently with + // different state snapshots. + stateSync := ctx.State() + state := stateSync.Lock().DeepCopy() + defer stateSync.Unlock() // Call the hook err := ctx.Hook(func(h Hook) (HookAction, error) { @@ -128,177 +157,176 @@ func (n *EvalUpdateStateHook) Eval(ctx EvalContext) (interface{}, error) { return nil, nil } -// EvalWriteState is an EvalNode implementation that writes the -// primary InstanceState for a specific resource into the state. +// EvalWriteState is an EvalNode implementation that saves the given object +// as the current object for the selected resource instance. type EvalWriteState struct { - Name string - ResourceType string - Provider addrs.AbsProviderConfig - Dependencies []string - State **InstanceState + // Addr is the address of the instance to read state for. + Addr addrs.ResourceInstance + + // State is the object state to save. + State **states.ResourceInstanceObject + + // ProviderSchema is the schema for the provider given in ProviderAddr. + ProviderSchema **ProviderSchema + + // ProviderAddr is the address of the provider configuration that + // produced the given object. + ProviderAddr addrs.AbsProviderConfig } func (n *EvalWriteState) Eval(ctx EvalContext) (interface{}, error) { - return writeInstanceToState(ctx, n.Name, n.ResourceType, n.Provider.String(), n.Dependencies, - func(rs *ResourceState) error { - if *n.State != nil { - log.Printf("[TRACE] EvalWriteState: %s has non-nil state", n.Name) - } else { - log.Printf("[TRACE] EvalWriteState: %s has nil state", n.Name) - } - rs.Primary = *n.State - return nil - }, - ) + if n.State == nil { + // Note that a pointer _to_ nil is valid here, indicating the total + // absense of an object as we'd see during destroy. + panic("EvalWriteState used with no ResourceInstanceObject") + } + + absAddr := n.Addr.Absolute(ctx.Path()) + state := ctx.State() + + obj := *n.State + if obj == nil { + // No need to encode anything: we'll just write it directly. + state.SetResourceInstanceCurrent(absAddr, nil, n.ProviderAddr) + return nil, nil + } + if n.ProviderSchema == nil || *n.ProviderSchema == nil { + // Should never happen, unless our state object is nil + panic("EvalWriteState used with pointer to nil ProviderSchema object") + } + + // TODO: Update this to use providers.Schema and populate the real + // schema version in the second argument to Encode below. + schema := (*n.ProviderSchema).ResourceTypes[absAddr.Resource.Resource.Type] + if schema == nil { + // It shouldn't be possible to get this far in any real scenario + // without a schema, but we might end up here in contrived tests that + // fail to set up their world properly. + return nil, fmt.Errorf("failed to encode %s in state: no resource type schema available", absAddr) + } + src, err := obj.Encode(schema.ImpliedType(), 0) + if err != nil { + return nil, fmt.Errorf("failed to encode %s in state: %s", absAddr, err) + } + + state.SetResourceInstanceCurrent(absAddr, src, n.ProviderAddr) + return nil, nil } // EvalWriteStateDeposed is an EvalNode implementation that writes // an InstanceState out to the Deposed list of a resource in the state. type EvalWriteStateDeposed struct { - Name string - ResourceType string - Provider string - Dependencies []string - State **InstanceState - // Index indicates which instance in the Deposed list to target, or -1 to append. - Index int + // Addr is the address of the instance to read state for. + Addr addrs.ResourceInstance + + // Key indicates which deposed object to write to. + Key states.DeposedKey + + // State is the object state to save. + State **states.ResourceInstanceObject + + // ProviderSchema is the schema for the provider given in ProviderAddr. + ProviderSchema **ProviderSchema + + // ProviderAddr is the address of the provider configuration that + // produced the given object. + ProviderAddr addrs.AbsProviderConfig } func (n *EvalWriteStateDeposed) Eval(ctx EvalContext) (interface{}, error) { - return writeInstanceToState(ctx, n.Name, n.ResourceType, n.Provider, n.Dependencies, - func(rs *ResourceState) error { - if n.Index == -1 { - rs.Deposed = append(rs.Deposed, *n.State) - } else { - rs.Deposed[n.Index] = *n.State - } - return nil - }, - ) -} - -// Pulls together the common tasks of the EvalWriteState nodes. All the args -// are passed directly down from the EvalNode along with a `writer` function -// which is yielded the *ResourceState and is responsible for writing an -// InstanceState to the proper field in the ResourceState. -func writeInstanceToState( - ctx EvalContext, - resourceName string, - resourceType string, - provider string, - dependencies []string, - writerFn func(*ResourceState) error, -) (*InstanceState, error) { - state, lock := ctx.State() - if state == nil { - return nil, fmt.Errorf("cannot write state to nil state") + if n.State == nil { + // Note that a pointer _to_ nil is valid here, indicating the total + // absense of an object as we'd see during destroy. + panic("EvalWriteStateDeposed used with no ResourceInstanceObject") } - // Get a write lock so we can access this instance - lock.Lock() - defer lock.Unlock() + absAddr := n.Addr.Absolute(ctx.Path()) + key := n.Key + state := ctx.State() - // Look for the module state. If we don't have one, create it. - mod := state.ModuleByPath(ctx.Path()) - if mod == nil { - mod = state.AddModule(ctx.Path()) + if key == states.NotDeposed { + // should never happen + return nil, fmt.Errorf("can't save deposed object for %s without a deposed key; this is a bug in Terraform that should be reported", absAddr) } - // Look for the resource state. - rs := mod.Resources[resourceName] - if rs == nil { - rs = &ResourceState{} - rs.init() - mod.Resources[resourceName] = rs + obj := *n.State + if obj == nil { + // No need to encode anything: we'll just write it directly. + state.SetResourceInstanceCurrent(absAddr, nil, n.ProviderAddr) + return nil, nil } - rs.Type = resourceType - rs.Dependencies = dependencies - rs.Provider = provider - log.Printf("[TRACE] Saving state for %s, managed by %s", resourceName, provider) - - if err := writerFn(rs); err != nil { - return nil, err + if n.ProviderSchema == nil || *n.ProviderSchema == nil { + // Should never happen, unless our state object is nil + panic("EvalWriteStateDeposed used with no ProviderSchema object") } + // TODO: Update this to use providers.Schema and populate the real + // schema version in the second argument to Encode below. + schema := (*n.ProviderSchema).ResourceTypes[absAddr.Resource.Resource.Type] + if schema == nil { + // It shouldn't be possible to get this far in any real scenario + // without a schema, but we might end up here in contrived tests that + // fail to set up their world properly. + return nil, fmt.Errorf("failed to encode %s in state: no resource type schema available", absAddr) + } + src, err := obj.Encode(schema.ImpliedType(), 0) + if err != nil { + return nil, fmt.Errorf("failed to encode %s in state: %s", absAddr, err) + } + + state.SetResourceInstanceDeposed(absAddr, key, src, n.ProviderAddr) return nil, nil } -// EvalDeposeState is an EvalNode implementation that takes the primary -// out of a state and makes it Deposed. This is done at the beginning of -// create-before-destroy calls so that the create can create while preserving -// the old state of the to-be-destroyed resource. +// EvalDeposeState is an EvalNode implementation that moves the current object +// for the given instance to instead be a deposed object, leaving the instance +// with no current object. +// This is used at the beginning of a create-before-destroy replace action so +// that the create can create while preserving the old state of the +// to-be-destroyed object. type EvalDeposeState struct { - Name string + Addr addrs.ResourceInstance + + // OutputKey, if non-nil, will be written with the deposed object key that + // was generated for the object. This can then be passed to + // EvalUndeposeState.Key so it knows which deposed instance to forget. + OutputKey *states.DeposedKey } // TODO: test func (n *EvalDeposeState) Eval(ctx EvalContext) (interface{}, error) { - state, lock := ctx.State() + absAddr := n.Addr.Absolute(ctx.Path()) + state := ctx.State() - // Get a read lock so we can access this instance - lock.RLock() - defer lock.RUnlock() + key := state.DeposeResourceInstanceObject(absAddr) - // Look for the module state. If we don't have one, then it doesn't matter. - mod := state.ModuleByPath(ctx.Path()) - if mod == nil { - return nil, nil + if n.OutputKey != nil { + *n.OutputKey = key } - // Look for the resource state. If we don't have one, then it is okay. - rs := mod.Resources[n.Name] - if rs == nil { - return nil, nil - } - - // If we don't have a primary, we have nothing to depose - if rs.Primary == nil { - return nil, nil - } - - // Depose - rs.Deposed = append(rs.Deposed, rs.Primary) - rs.Primary = nil - return nil, nil } -// EvalUndeposeState is an EvalNode implementation that reads the -// InstanceState for a specific resource out of the state. +// EvalUndeposeState is an EvalNode implementation that forgets a particular +// deposed object from the state, causing Terraform to no longer track it. +// +// Users of this must ensure that the upstream object that the object was +// tracking has been deleted in the remote system before this node is +// evaluated. type EvalUndeposeState struct { - Name string - State **InstanceState + Addr addrs.ResourceInstance + + // Key is a pointer to the deposed object key that should be forgotten + // from the state, which must be non-nil. + Key *states.DeposedKey } // TODO: test func (n *EvalUndeposeState) Eval(ctx EvalContext) (interface{}, error) { - state, lock := ctx.State() + absAddr := n.Addr.Absolute(ctx.Path()) + state := ctx.State() - // Get a read lock so we can access this instance - lock.RLock() - defer lock.RUnlock() - - // Look for the module state. If we don't have one, then it doesn't matter. - mod := state.ModuleByPath(ctx.Path()) - if mod == nil { - return nil, nil - } - - // Look for the resource state. If we don't have one, then it is okay. - rs := mod.Resources[n.Name] - if rs == nil { - return nil, nil - } - - // If we don't have any desposed resource, then we don't have anything to do - if len(rs.Deposed) == 0 { - return nil, nil - } - - // Undepose - idx := len(rs.Deposed) - 1 - rs.Primary = rs.Deposed[idx] - rs.Deposed[idx] = *n.State + state.ForgetResourceInstanceDeposed(absAddr, *n.Key) return nil, nil } diff --git a/terraform/eval_state_test.go b/terraform/eval_state_test.go index b4872c7f7..9d5062d3b 100644 --- a/terraform/eval_state_test.go +++ b/terraform/eval_state_test.go @@ -1,17 +1,21 @@ package terraform import ( - "sync" "testing" + "github.com/davecgh/go-spew/spew" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/configs/configschema" + "github.com/hashicorp/terraform/states" ) func TestEvalRequireState(t *testing.T) { ctx := new(MockEvalContext) cases := []struct { - State *InstanceState + State *states.ResourceInstanceObject Exit bool }{ { @@ -19,11 +23,21 @@ func TestEvalRequireState(t *testing.T) { true, }, { - &InstanceState{}, + &states.ResourceInstanceObject{ + Value: cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + })), + Status: states.ObjectReady, + }, true, }, { - &InstanceState{ID: "foo"}, + &states.ResourceInstanceObject{ + Value: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + Status: states.ObjectReady, + }, false, }, } @@ -51,10 +65,12 @@ func TestEvalRequireState(t *testing.T) { func TestEvalUpdateStateHook(t *testing.T) { mockHook := new(MockHook) + state := states.NewState() + state.Module(addrs.RootModuleInstance).SetLocalValue("foo", cty.StringVal("hello")) + ctx := new(MockEvalContext) ctx.HookHook = mockHook - ctx.StateState = &State{Serial: 42} - ctx.StateLock = new(sync.RWMutex) + ctx.StateState = state.SyncWrapper() node := &EvalUpdateStateHook{} if _, err := node.Eval(ctx); err != nil { @@ -64,13 +80,24 @@ func TestEvalUpdateStateHook(t *testing.T) { if !mockHook.PostStateUpdateCalled { t.Fatal("should call PostStateUpdate") } - if mockHook.PostStateUpdateState.Serial != 42 { - t.Fatalf("bad: %#v", mockHook.PostStateUpdateState) + if mockHook.PostStateUpdateState.LocalValue(addrs.LocalValue{Name: "foo"}.Absolute(addrs.RootModuleInstance)) != cty.StringVal("hello") { + t.Fatalf("wrong state passed to hook: %s", spew.Sdump(mockHook.PostStateUpdateState)) } } func TestEvalReadState(t *testing.T) { - var output *InstanceState + var output *states.ResourceInstanceObject + mockProvider := mockProviderWithResourceTypeSchema("aws_instance", &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Optional: true, + }, + }, + }) + providerSchema := mockProvider.GetSchemaReturn + provider := ResourceProvider(mockProvider) + cases := map[string]struct { Resources map[string]*ResourceState Node EvalNode @@ -85,7 +112,14 @@ func TestEvalReadState(t *testing.T) { }, }, Node: &EvalReadState{ - Name: "aws_instance.bar", + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "bar", + }.Instance(addrs.NoKey), + Provider: &provider, + ProviderSchema: &providerSchema, + Output: &output, }, ExpectedInstanceId: "i-abc123", @@ -99,58 +133,87 @@ func TestEvalReadState(t *testing.T) { }, }, Node: &EvalReadStateDeposed{ - Name: "aws_instance.bar", + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "bar", + }.Instance(addrs.NoKey), + Key: states.DeposedKey("00000001"), // shim from legacy state assigns 0th deposed index this key + Provider: &provider, + ProviderSchema: &providerSchema, + Output: &output, - Index: 0, }, ExpectedInstanceId: "i-abc123", }, } for k, c := range cases { - ctx := new(MockEvalContext) - ctx.StateState = &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: c.Resources, + t.Run(k, func(t *testing.T) { + ctx := new(MockEvalContext) + state := mustShimLegacyState(&State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: c.Resources, + }, }, - }, - } - ctx.StateLock = new(sync.RWMutex) - ctx.PathPath = addrs.RootModuleInstance + }) + ctx.StateState = state.SyncWrapper() + ctx.PathPath = addrs.RootModuleInstance - result, err := c.Node.Eval(ctx) - if err != nil { - t.Fatalf("[%s] Got err: %#v", k, err) - } + result, err := c.Node.Eval(ctx) + if err != nil { + t.Fatalf("[%s] Got err: %#v", k, err) + } - expected := c.ExpectedInstanceId - if !(result != nil && result.(*InstanceState).ID == expected) { - t.Fatalf("[%s] Expected return with ID %#v, got: %#v", k, expected, result) - } + expected := c.ExpectedInstanceId + if !(result != nil && instanceObjectIdForTests(result.(*states.ResourceInstanceObject)) == expected) { + t.Fatalf("[%s] Expected return with ID %#v, got: %#v", k, expected, result) + } - if !(output != nil && output.ID == expected) { - t.Fatalf("[%s] Expected output with ID %#v, got: %#v", k, expected, output) - } + if !(output != nil && output.Value.GetAttr("id") == cty.StringVal(expected)) { + t.Fatalf("[%s] Expected output with ID %#v, got: %#v", k, expected, output) + } - output = nil + output = nil + }) } } func TestEvalWriteState(t *testing.T) { - state := &State{} + state := states.NewState() ctx := new(MockEvalContext) - ctx.StateState = state - ctx.StateLock = new(sync.RWMutex) + ctx.StateState = state.SyncWrapper() ctx.PathPath = addrs.RootModuleInstance - is := &InstanceState{ID: "i-abc123"} + mockProvider := mockProviderWithResourceTypeSchema("aws_instance", &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Optional: true, + }, + }, + }) + providerSchema := mockProvider.GetSchemaReturn + + obj := &states.ResourceInstanceObject{ + Value: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-abc123"), + }), + Status: states.ObjectReady, + } node := &EvalWriteState{ - Name: "restype.resname", - ResourceType: "restype", - State: &is, - Provider: addrs.RootModuleInstance.ProviderConfigDefault("res"), + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + }.Instance(addrs.NoKey), + + State: &obj, + + ProviderSchema: &providerSchema, + ProviderAddr: addrs.RootModuleInstance.ProviderConfigDefault("aws"), } _, err := node.Eval(ctx) if err != nil { @@ -158,25 +221,46 @@ func TestEvalWriteState(t *testing.T) { } checkStateString(t, state, ` -restype.resname: +aws_instance.foo: ID = i-abc123 - provider = provider.res + provider = provider.aws `) } func TestEvalWriteStateDeposed(t *testing.T) { - state := &State{} + state := states.NewState() ctx := new(MockEvalContext) - ctx.StateState = state - ctx.StateLock = new(sync.RWMutex) + ctx.StateState = state.SyncWrapper() ctx.PathPath = addrs.RootModuleInstance - is := &InstanceState{ID: "i-abc123"} + mockProvider := mockProviderWithResourceTypeSchema("aws_instance", &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Optional: true, + }, + }, + }) + providerSchema := mockProvider.GetSchemaReturn + + obj := &states.ResourceInstanceObject{ + Value: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-abc123"), + }), + Status: states.ObjectReady, + } node := &EvalWriteStateDeposed{ - Name: "restype.resname", - ResourceType: "restype", - State: &is, - Index: -1, + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + }.Instance(addrs.NoKey), + Key: states.DeposedKey("deadbeef"), + + State: &obj, + + ProviderSchema: &providerSchema, + ProviderAddr: addrs.RootModuleInstance.ProviderConfigDefault("aws"), } _, err := node.Eval(ctx) if err != nil { @@ -184,8 +268,9 @@ func TestEvalWriteStateDeposed(t *testing.T) { } checkStateString(t, state, ` -restype.resname: (1 deposed) +aws_instance.foo: (1 deposed) ID = - Deposed ID 1 = i-abc123 + provider = provider.aws + Deposed ID deadbeef = i-abc123 `) } diff --git a/terraform/evaluate.go b/terraform/evaluate.go index ba1b02022..20dfbdfb2 100644 --- a/terraform/evaluate.go +++ b/terraform/evaluate.go @@ -5,7 +5,6 @@ import ( "log" "os" "strconv" - "strings" "sync" "github.com/agext/levenshtein" @@ -14,10 +13,10 @@ import ( "github.com/zclconf/go-cty/cty/convert" "github.com/hashicorp/terraform/addrs" - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/config/hcl2shim" "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/lang" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" ) @@ -52,11 +51,9 @@ type Evaluator struct { // This must not be mutated during evaluation. Schemas *Schemas - // State is the current state. During some operations this structure - // is mutated concurrently, and so it must be accessed only while holding - // StateLock. - State *State - StateLock *sync.RWMutex + // State is the current state, embedded in a wrapper that ensures that + // it can be safely accessed and modified concurrently. + State *states.SyncState } // Scope creates an evaluation scope for the given module path and optional @@ -273,28 +270,12 @@ func (d *evaluationStateData) GetLocalValue(addr addrs.LocalValue, rng tfdiags.S return cty.DynamicVal, diags } - // Now we'll retrieve the value from the state, which means we need to hold - // the state lock. - d.Evaluator.StateLock.RLock() - defer d.Evaluator.StateLock.RUnlock() - - ms := d.Evaluator.State.ModuleByPath(d.ModulePath) - if ms == nil { + val := d.Evaluator.State.LocalValue(addr.Absolute(d.ModulePath)) + if val == cty.NilVal { // Not evaluated yet? - return cty.DynamicVal, diags + val = cty.DynamicVal } - rawV, exists := ms.Locals[addr.Name] - if !exists { - // Not evaluated yet? - return cty.DynamicVal, diags - } - - // The state structures haven't yet been updated to the new type system, - // so we'll need to shim here. - // FIXME: Remove this once ms.Locals is itself a map[string]cty.Value. - val := hcl2shim.HCL2ValueFromConfigValue(rawV) - return val, diags } @@ -316,36 +297,17 @@ func (d *evaluationStateData) GetModuleInstance(addr addrs.ModuleCallInstance, r } outputConfigs := moduleConfig.Module.Outputs - // Now we'll retrieve the values from the state, which means we need to hold - // the state lock. - d.Evaluator.StateLock.RLock() - defer d.Evaluator.StateLock.RUnlock() - - ms := d.Evaluator.State.ModuleByPath(moduleAddr) - if ms == nil { - // Not evaluated yet? - // We'll return an unknown value of a suitable object type so that we - // can still detect attempts to access outputs that aren't defined. - attrs := map[string]cty.Type{} - for name := range outputConfigs { - attrs[name] = cty.DynamicPseudoType - } - return cty.UnknownVal(cty.Object(attrs)), diags - } - vals := map[string]cty.Value{} - for name := range outputConfigs { - os, exists := ms.Outputs[name] - if !exists { + for n := range outputConfigs { + addr := addrs.OutputValue{Name: n}.Absolute(moduleAddr) + os := d.Evaluator.State.OutputValue(addr) + if os == nil { // Not evaluated yet? - vals[name] = cty.DynamicVal + vals[n] = cty.DynamicVal continue } - // The state structures haven't yet been updated to the new type system, - // so we'll need to shim here. - // FIXME: Remove this once ms.Outputs itself contains cty.Value. - vals[name] = hcl2shim.HCL2ValueFromConfigValue(os.Value) + vals[n] = os.Value } return cty.ObjectVal(vals), diags } @@ -387,30 +349,13 @@ func (d *evaluationStateData) GetModuleInstanceOutput(addr addrs.ModuleCallOutpu return cty.DynamicVal, diags } - // Now we'll retrieve the value from the state, which means we need to hold - // the state lock. - d.Evaluator.StateLock.RLock() - defer d.Evaluator.StateLock.RUnlock() - - ms := d.Evaluator.State.ModuleByPath(moduleAddr) - if ms == nil { + os := d.Evaluator.State.OutputValue(absAddr) + if os == nil { // Not evaluated yet? return cty.DynamicVal, diags } - os, exists := ms.Outputs[addr.Name] - if !exists { - // Not evaluated yet? - return cty.DynamicVal, diags - } - - // The state structures haven't yet been updated to the new type system, - // so we'll need to shim here. - // FIXME: Remove this once ms.Outputs itself contains cty.Value. - val := hcl2shim.HCL2ValueFromConfigValue(os.Value) - - return val, diags - + return os.Value, diags } func (d *evaluationStateData) GetPathAttr(addr addrs.PathAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { @@ -467,7 +412,7 @@ func (d *evaluationStateData) GetResourceInstance(addr addrs.ResourceInstance, r // the instances of a particular resource. The reference resolver can't // resolve the ambiguity itself, so we must do it in here. - // First we'll consult the configuration to see if an output of this + // First we'll consult the configuration to see if an resource of this // name is declared at all. moduleAddr := d.ModulePath moduleConfig := d.Evaluator.Config.DescendentForInstance(moduleAddr) @@ -488,83 +433,95 @@ func (d *evaluationStateData) GetResourceInstance(addr addrs.ResourceInstance, r return cty.DynamicVal, diags } - // We need to shim our address to the legacy form still used in the state structs. - addrKey := NewLegacyResourceInstanceAddress(addr.Absolute(d.ModulePath)).stateId() + // First we'll find the state for the resource as a whole, and decide + // from there whether we're going to interpret the given address as a + // resource or a resource instance address. + rs := d.Evaluator.State.Resource(addr.ContainingResource().Absolute(d.ModulePath)) - // We'll get the values for the instance(s) from state, so we'll need a read lock. - d.Evaluator.StateLock.RLock() - defer d.Evaluator.StateLock.RUnlock() + if rs == nil { + schema := d.getResourceSchema(addr.ContainingResource(), config.ProviderConfigAddr().Absolute(d.ModulePath)) - ms := d.Evaluator.State.ModuleByPath(d.ModulePath) - if ms == nil { - // If we have no module state in the apply walk, that suggests we've hit - // a rather awkward edge-case: the resource this variable refers to - // has count = 0 and is the only resource processed so far on this walk, - // and so we've ended up not creating any resource states yet. We don't - // create a module state until the first resource is written into it, - // so the module state doesn't exist when we get here. - // - // In this case we act as we would if we had been passed a module - // with an empty resource state map. - ms = &ModuleState{} + // If it doesn't exist at all then we can't reliably determine whether + // single-instance or whole-resource interpretation was intended, but + // we can decide this partially... + if addr.Key != addrs.NoKey { + // If there's an instance key then the user must be intending + // single-instance interpretation, and so we can return a + // properly-typed unknown value to help with type checking. + return cty.UnknownVal(schema.ImpliedType()), diags + } + + // otherwise we must return DynamicVal so that both interpretations + // can proceed without generating errors, and we'll deal with this + // in a later step where more information is gathered. + // (In practice we should only end up here during the validate walk, + // since later walks should have at least partial states populated + // for all resources in the configuration.) + return cty.DynamicVal, diags } - // Note that the state structs currently have confusing legacy names: - // ResourceState is actually the state for what we call an "instance" - // elsewhere, and then InstanceState is the state for a particular _phase_ - // of that instance (primary vs. deposed). This should be addressed when - // we revise the state structs to natively support the HCL type system. - rs := ms.Resources[addrKey] + schema := d.getResourceSchema(addr.ContainingResource(), rs.ProviderConfig) - var providerAddr addrs.AbsProviderConfig - if rs != nil { - var err error - providerAddr, err = rs.ProviderAddr() - if err != nil { - // This indicates corruption of or tampering with the state file + // If we are able to automatically convert to the "right" type of instance + // key for this each mode then we'll do so, to match with how we generally + // treat values elsewhere in the language. This allows code below to + // assume that any possible conversions have already been dealt with and + // just worry about validation. + key := d.coerceInstanceKey(addr.Key, rs.EachMode) + + multi := false + + switch rs.EachMode { + case states.NoEach: + if key != addrs.NoKey { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: `Invalid provider address in state`, - Detail: fmt.Sprintf("The state for the referenced resource refers to a syntactically-invalid provider address %q. This can occur if the state data is incorrectly edited by hand.", rs.Provider), + Summary: "Invalid resource index", + Detail: fmt.Sprintf("Resource %s does not have either \"count\" or \"for_each\" set, so it cannot be indexed.", addr.ContainingResource()), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } + case states.EachList: + multi = key != addrs.NoKey + if _, ok := addr.Key.(addrs.IntKey); !multi && !ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid resource index", + Detail: fmt.Sprintf("Resource %s must be indexed with a number value.", addr.ContainingResource()), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } + case states.EachMap: + multi = key != addrs.NoKey + if _, ok := addr.Key.(addrs.IntKey); !multi && !ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid resource index", + Detail: fmt.Sprintf("Resource %s must be indexed with a string value.", addr.ContainingResource()), Subject: rng.ToHCL().Ptr(), }) return cty.DynamicVal, diags } - } else { - // Must assume a provider address from the config, then. - // This result is usually ignored since we'll probably end up in - // the getResourceInstancesAll path after this (if our instance - // actually has a key). However, we can also end up here in strange - // cases like "terraform console", which might be used before a - // particular resource has been created in state at all. - providerAddr = config.ProviderConfigAddr().Absolute(d.ModulePath) } - // If we have an exact match for the requested instance and it has non-nil - // primary data then we'll use it directly. This is the easy path. - if rs != nil && rs.Primary != nil { + if !multi { log.Printf("[TRACE] GetResourceInstance: %s is a single instance", addr) - return d.getResourceInstanceSingle(addr, rng, rs.Primary, providerAddr) + is := rs.Instance(key) + if is == nil { + return cty.UnknownVal(schema.ImpliedType()), diags + } + return d.getResourceInstanceSingle(addr, rng, is, config, rs.ProviderConfig) } - // If we get down here then we might have a request for the list of all - // instances of a particular resource, but only if we have a no-key address. - // If we have a _keyed_ address then instead it's a single instance that - // isn't evaluated yet. - if addr.Key != addrs.NoKey { - log.Printf("[TRACE] GetResourceInstance: %s is pending", addr) - return d.getResourceInstancePending(addr, rng, providerAddr) - } - - return d.getResourceInstancesAll(addr.ContainingResource(), config, ms, providerAddr) + log.Printf("[TRACE] GetResourceInstance: %s has multiple keyed instances", addr) + return d.getResourceInstancesAll(addr.ContainingResource(), rng, config, rs, rs.ProviderConfig) } -func (d *evaluationStateData) getResourceInstanceSingle(addr addrs.ResourceInstance, rng tfdiags.SourceRange, is *InstanceState, providerAddr addrs.AbsProviderConfig) (cty.Value, tfdiags.Diagnostics) { +func (d *evaluationStateData) getResourceInstanceSingle(addr addrs.ResourceInstance, rng tfdiags.SourceRange, is *states.ResourceInstance, config *configs.Resource, providerAddr addrs.AbsProviderConfig) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - // To properly decode the "flatmap"-based values from the state, we need - // to know the resource's schema, which we should already have cached - // from when the provider was initialized. schema := d.getResourceSchema(addr.ContainingResource(), providerAddr) if schema == nil { // This shouldn't happen, since validation before we get here should've @@ -579,163 +536,122 @@ func (d *evaluationStateData) getResourceInstanceSingle(addr addrs.ResourceInsta } ty := schema.ImpliedType() - if is == nil { + if is == nil || is.Current == nil { // Assume we're dealing with an instance that hasn't been created yet. return cty.UnknownVal(ty), diags } - flatmapVal := is.Attributes - val, err := hcl2shim.HCL2ValueFromFlatmap(flatmapVal, ty) + ios, err := is.Current.Decode(ty) if err != nil { - // A value in the flatmap value could not be conformed to the schema + // This shouldn't happen, since by the time we get here + // we should've upgraded the state data already. diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: `Invalid value in state`, - Detail: fmt.Sprintf("The state data stored for %s does not conform to the resource schema: %s", addr, err), - Subject: rng.ToHCL().Ptr(), + Summary: "Invalid resource instance data in state", + Detail: fmt.Sprintf("Instance %s data could not be decoded from the state: %s.", addr.Absolute(d.ModulePath), err), + Subject: &config.DeclRange, }) return cty.UnknownVal(ty), diags } - return val, diags + return ios.Value, nil } -func (d *evaluationStateData) getResourceInstancesAll(addr addrs.Resource, config *configs.Resource, ms *ModuleState, providerAddr addrs.AbsProviderConfig) (cty.Value, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - rng := tfdiags.SourceRangeFromHCL(config.DeclRange) - hasCount := config.Count != nil - - // Currently the only multi-instance construct we support is "count", which - // ensures that all of the instances will have integer keys, and so we - // can produce a tuple value of them. - // - // The legacy state structs are not designed to unambigiously represent - // a list of instances associated with a resource, and so we need to infer - // what exists based on which keys we find. Our returned tuple is therefore - // long enough to accommodate the highest index we find, and may contain - // unknown values filling in any "gaps" for instances that have been - // tainted or not yet created. - - // Keys in the resources map are resource addresses followed by a period - // and then an integer index. Keys without an integer index are possible - // too, but we already took care of those in GetResourceInstance by - // branching directly into getResourceInstanceSingle, so we know that - // we're dealing with keyed instances here. - prefix := addr.String() + "." - length := 0 - instanceVals := map[addrs.InstanceKey]cty.Value{} - for fullKey, rs := range ms.Resources { - if !strings.HasPrefix(fullKey, prefix) { - continue - } - if rs.Primary == nil { - continue - } - - keyStr := fullKey[len(prefix):] - var key addrs.InstanceKey - if i, err := strconv.Atoi(keyStr); err == nil { - key = addrs.IntKey(i) - if i >= length { - length = i + 1 - } - } else { - key = addrs.StringKey(keyStr) - } - - // In this case we'll ignore our given providerAddr, since it was - // for a single unkeyed ResourceState, not the keyed one we have now. - providerAddr, err := rs.ProviderAddr() - if err != nil { - // This indicates corruption of or tampering with the state file - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `Invalid provider address in state`, - Detail: fmt.Sprintf("The state for %s refers to a syntactically-invalid provider address %q. This can occur if the state data is incorrectly edited by hand.", addr.Instance(key), rs.Provider), - Subject: rng.ToHCL().Ptr(), - }) - continue - } - - val, instanceDiags := d.getResourceInstanceSingle(addr.Instance(key), rng, rs.Primary, providerAddr) - diags = diags.Append(instanceDiags) - - instanceVals[key] = val - } - - if length == 0 && !hasCount { - // If we have nothing at all and the configuration lacks a count - // argument then we'll assume that we're dealing with a resource that - // is pending creation (e.g. during the validate walk) and that it - // will eventually have only one unkeyed instance. - // In this case we _do_ use the given providerAddr, since that - // is for the unkeyed instance we found in GetResourceInstance. - log.Printf("[TRACE] GetResourceInstance: %s has no instances yet", addr) - return d.getResourceInstanceSingle(addr.Instance(addrs.NoKey), rng, nil, providerAddr) - } - - log.Printf("[TRACE] GetResourceInstance: %s has multiple keyed instances (%d)", addr, length) - - // TODO: In future, when for_each is implemented, we'll need to decide here - // whether to return a tuple value or an object value. However, by that - // time we should've revised the state structs so we can see unambigously - // which to use, rather than trying to guess based on the presence of - // keys. - - valsSeq := make([]cty.Value, length) - for i := 0; i < length; i++ { - val, exists := instanceVals[addrs.IntKey(i)] - if exists { - valsSeq[i] = val - } else { - // FIXME: Ideally we'd return an unknown value of the schema's - // implied type here, but this shim-ish implementation of resource - // evaluation is already tricky enough so we'll just cheat for - // now. Once we refactor for the new state format, reorganize this - // code so that the schema is available here. - valsSeq[i] = cty.DynamicVal // not yet known - } - } - - // We use a tuple rather than a list here because resource schemas may - // include dynamically-typed attributes, which will then cause each - // instance to potentially have a different runtime type. - return cty.TupleVal(valsSeq), diags -} - -func (d *evaluationStateData) getResourceInstancePending(addr addrs.ResourceInstance, rng tfdiags.SourceRange, providerAddr addrs.AbsProviderConfig) (cty.Value, tfdiags.Diagnostics) { +func (d *evaluationStateData) getResourceInstancesAll(addr addrs.Resource, rng tfdiags.SourceRange, config *configs.Resource, rs *states.Resource, providerAddr addrs.AbsProviderConfig) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - // We'd ideally like to return a properly-typed unknown value here, in - // order to give the type checker maximum information to detect type - // mismatches even if concrete values aren't yet known. - // - // To do this we need to know the resource's schema, which we should - // already have cached from when the provider was initialized. However, we - // first need to look in configuration to find out which provider address - // will be responsible for creating this. - moduleConfig := d.Evaluator.Config.DescendentForInstance(d.ModulePath) - if moduleConfig == nil { - // should never happen, since we can't be evaluating in a module - // that wasn't mentioned in configuration. - panic(fmt.Sprintf("reference to instance from %s, which has no configuration", d.ModulePath)) - } - - // Everything after here is best-effort: if we can't gather enough - // information to return a typed value then we'll give up and return an - // entirely-untyped value, assuming that we're in a special situation - // such as accessing an orphaned resource, which should get error-checked - // elsewhere. - rc := moduleConfig.Module.ResourceByAddr(addr.ContainingResource()) - if rc == nil { - return cty.DynamicVal, diags - } - schema := d.getResourceSchema(addr.ContainingResource(), providerAddr) + schema := d.getResourceSchema(addr, providerAddr) if schema == nil { + // This shouldn't happen, since validation before we get here should've + // taken care of it, but we'll show a reasonable error message anyway. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Missing resource type schema`, + Detail: fmt.Sprintf("No schema is available for %s in %s. This is a bug in Terraform and should be reported.", addr, providerAddr), + Subject: rng.ToHCL().Ptr(), + }) return cty.DynamicVal, diags } - return cty.UnknownVal(schema.ImpliedType()), diags + switch rs.EachMode { + + case states.EachList: + // We need to infer the length of our resulting tuple by searching + // for the max IntKey in our instances map. + length := 0 + for k := range rs.Instances { + if ik, ok := k.(addrs.IntKey); ok { + if int(ik) >= length { + length = int(ik) + 1 + } + } + } + + vals := make([]cty.Value, length) + for i := 0; i < length; i++ { + ty := schema.ImpliedType() + key := addrs.IntKey(i) + is, exists := rs.Instances[key] + if exists { + ios, err := is.Current.Decode(ty) + if err != nil { + // This shouldn't happen, since by the time we get here + // we should've upgraded the state data already. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid resource instance data in state", + Detail: fmt.Sprintf("Instance %s data could not be decoded from the state: %s.", addr.Instance(key).Absolute(d.ModulePath), err), + Subject: &config.DeclRange, + }) + continue + } + vals[i] = ios.Value + } else { + // There shouldn't normally be "gaps" in our list but we'll + // allow it under the assumption that we're in a weird situation + // where e.g. someone has run "terraform state mv" to reorder + // a list and left a hole behind. + vals[i] = cty.UnknownVal(schema.ImpliedType()) + } + } + + // We use a tuple rather than a list here because resource schemas may + // include dynamically-typed attributes, which will then cause each + // instance to potentially have a different runtime type even though + // they all conform to the static schema. + return cty.TupleVal(vals), diags + + case states.EachMap: + ty := schema.ImpliedType() + vals := make(map[string]cty.Value, len(rs.Instances)) + for k, is := range rs.Instances { + if sk, ok := k.(addrs.StringKey); ok { + ios, err := is.Current.Decode(ty) + if err != nil { + // This shouldn't happen, since by the time we get here + // we should've upgraded the state data already. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid resource instance data in state", + Detail: fmt.Sprintf("Instance %s data could not be decoded from the state: %s.", addr.Instance(k).Absolute(d.ModulePath), err), + Subject: &config.DeclRange, + }) + continue + } + vals[string(sk)] = ios.Value + } + } + + // We use an object rather than a map here because resource schemas may + // include dynamically-typed attributes, which will then cause each + // instance to potentially have a different runtime type even though + // they all conform to the static schema. + return cty.ObjectVal(vals), diags + + default: + // Should never happen since caller should deal with other modes + panic(fmt.Sprintf("unsupported EachMode %s", rs.EachMode)) + } } func (d *evaluationStateData) getResourceSchema(addr addrs.Resource, providerAddr addrs.AbsProviderConfig) *configschema.Block { @@ -753,6 +669,43 @@ func (d *evaluationStateData) getResourceSchema(addr addrs.Resource, providerAdd } } +// coerceInstanceKey attempts to convert the given key to the type expected +// for the given EachMode. +// +// If the key is already of the correct type or if it cannot be converted then +// it is returned verbatim. If conversion is required and possible, the +// converted value is returned. Callers should not try to determine if +// conversion was possible, should instead just check if the result is of +// the expected type. +func (d *evaluationStateData) coerceInstanceKey(key addrs.InstanceKey, mode states.EachMode) addrs.InstanceKey { + if key == addrs.NoKey { + // An absent key can't be converted + return key + } + + switch mode { + case states.NoEach: + // No conversions possible at all + return key + case states.EachMap: + if intKey, isInt := key.(addrs.IntKey); isInt { + return addrs.StringKey(strconv.Itoa(int(intKey))) + } + return key + case states.EachList: + if strKey, isStr := key.(addrs.StringKey); isStr { + i, err := strconv.Atoi(string(strKey)) + if err != nil { + return key + } + return addrs.IntKey(i) + } + return key + default: + return key + } +} + func (d *evaluationStateData) GetTerraformAttr(addr addrs.TerraformAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics switch addr.Name { diff --git a/terraform/graph.go b/terraform/graph.go index 717745b12..58d45a7b6 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -51,10 +51,6 @@ func (g *Graph) walk(walker GraphWalker) tfdiags.Diagnostics { debugName = g.debugName + "-" + debugName } - debugBuf := dbug.NewFileWriter(debugName) - g.SetDebugWriter(debugBuf) - defer debugBuf.Close() - // Walk the graph. var walkFn dag.WalkFunc walkFn = func(v dag.Vertex) (diags tfdiags.Diagnostics) { diff --git a/terraform/graph_builder.go b/terraform/graph_builder.go index 76ae5be6e..66b21f300 100644 --- a/terraform/graph_builder.go +++ b/terraform/graph_builder.go @@ -33,14 +33,6 @@ func (b *BasicGraphBuilder) Build(path addrs.ModuleInstance) (*Graph, tfdiags.Di var diags tfdiags.Diagnostics g := &Graph{Path: path} - debugName := "graph.json" - if b.Name != "" { - debugName = b.Name + "-" + debugName - } - debugBuf := dbug.NewFileWriter(debugName) - g.SetDebugWriter(debugBuf) - defer debugBuf.Close() - var lastStepStr string for _, step := range b.Steps { if step == nil { diff --git a/terraform/graph_builder_apply.go b/terraform/graph_builder_apply.go index 2b27f30a4..0d7b310a8 100644 --- a/terraform/graph_builder_apply.go +++ b/terraform/graph_builder_apply.go @@ -4,6 +4,8 @@ import ( "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" ) @@ -18,11 +20,11 @@ type ApplyGraphBuilder struct { // Config is the configuration tree that the diff was built from. Config *configs.Config - // Diff is the diff to apply. - Diff *Diff + // Changes describes the changes that we need apply. + Changes *plans.Changes // State is the current state - State *State + State *states.State // Components is a factory for the plug-in components (providers and // provisioners) available for use. @@ -76,7 +78,7 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { // Creates all the nodes represented in the diff. &DiffTransformer{ Concrete: concreteResource, - Diff: b.Diff, + Changes: b.Changes, }, // Create orphan output nodes diff --git a/terraform/graph_builder_apply_test.go b/terraform/graph_builder_apply_test.go index dc87b5bc8..a43b70545 100644 --- a/terraform/graph_builder_apply_test.go +++ b/terraform/graph_builder_apply_test.go @@ -4,6 +4,8 @@ import ( "strings" "testing" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/addrs" ) @@ -12,54 +14,30 @@ func TestApplyGraphBuilder_impl(t *testing.T) { } func TestApplyGraphBuilder(t *testing.T) { - diff := &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: []string{"root"}, - Resources: map[string]*InstanceDiff{ - // Verify noop doesn't show up in graph - "test_object.noop": &InstanceDiff{}, - - "test_object.create": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "test_string": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, - - "test_object.other": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "test_string": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, + changes := &plans.Changes{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: mustResourceInstanceAddr("test_object.create"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, }, }, - - &ModuleDiff{ - Path: []string{"root", "child"}, - Resources: map[string]*InstanceDiff{ - "test_object.create": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "test_string": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, - - "test_object.other": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "test_string": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, + { + Addr: mustResourceInstanceAddr("test_object.other"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Update, + }, + }, + { + Addr: mustResourceInstanceAddr("module.child.test_object.create"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + }, + }, + { + Addr: mustResourceInstanceAddr("module.child.test_object.other"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, }, }, }, @@ -67,7 +45,7 @@ func TestApplyGraphBuilder(t *testing.T) { b := &ApplyGraphBuilder{ Config: testModule(t, "graph-builder-apply-basic"), - Diff: diff, + Changes: changes, Components: simpleMockComponentFactory(), Schemas: simpleTestSchemas(), DisableReduce: true, @@ -92,28 +70,18 @@ func TestApplyGraphBuilder(t *testing.T) { // This tests the ordering of two resources where a non-CBD depends // on a CBD. GH-11349. func TestApplyGraphBuilder_depCbd(t *testing.T) { - diff := &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: []string{"root"}, - Resources: map[string]*InstanceDiff{"test_object.A": &InstanceDiff{Destroy: true, - Attributes: map[string]*ResourceAttrDiff{ - "test_string": &ResourceAttrDiff{ - Old: "", - New: "foo", - RequiresNew: true, - }, - }, + changes := &plans.Changes{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: mustResourceInstanceAddr("test_object.A"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Replace, }, - - "test_object.B": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "test_string": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, + }, + { + Addr: mustResourceInstanceAddr("test_object.B"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Update, }, }, }, @@ -121,7 +89,7 @@ func TestApplyGraphBuilder_depCbd(t *testing.T) { b := &ApplyGraphBuilder{ Config: testModule(t, "graph-builder-apply-dep-cbd"), - Diff: diff, + Changes: changes, Components: simpleMockComponentFactory(), Schemas: simpleTestSchemas(), DisableReduce: true, @@ -156,30 +124,18 @@ func TestApplyGraphBuilder_depCbd(t *testing.T) { // This tests the ordering of two resources that are both CBD that // require destroy/create. func TestApplyGraphBuilder_doubleCBD(t *testing.T) { - diff := &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: []string{"root"}, - Resources: map[string]*InstanceDiff{ - "test_object.A": &InstanceDiff{ - Destroy: true, - Attributes: map[string]*ResourceAttrDiff{ - "test_string": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, - - "test_object.B": &InstanceDiff{ - Destroy: true, - Attributes: map[string]*ResourceAttrDiff{ - "test_string": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, + changes := &plans.Changes{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: mustResourceInstanceAddr("test_object.A"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Replace, + }, + }, + { + Addr: mustResourceInstanceAddr("test_object.B"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Replace, }, }, }, @@ -187,7 +143,7 @@ func TestApplyGraphBuilder_doubleCBD(t *testing.T) { b := &ApplyGraphBuilder{ Config: testModule(t, "graph-builder-apply-double-cbd"), - Diff: diff, + Changes: changes, Components: simpleMockComponentFactory(), Schemas: simpleTestSchemas(), DisableReduce: true, @@ -212,24 +168,24 @@ func TestApplyGraphBuilder_doubleCBD(t *testing.T) { // This tests the ordering of two resources being destroyed that depend // on each other from only state. GH-11749 func TestApplyGraphBuilder_destroyStateOnly(t *testing.T) { - diff := &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: []string{"root", "child"}, - Resources: map[string]*InstanceDiff{ - "test_object.A": &InstanceDiff{ - Destroy: true, - }, - - "test_object.B": &InstanceDiff{ - Destroy: true, - }, + changes := &plans.Changes{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: mustResourceInstanceAddr("module.child.test_object.A"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + }, + }, + { + Addr: mustResourceInstanceAddr("module.child.test_object.B"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, }, }, }, } - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "child"}, @@ -240,6 +196,7 @@ func TestApplyGraphBuilder_destroyStateOnly(t *testing.T) { ID: "foo", Attributes: map[string]string{}, }, + Provider: "provider.test", }, "test_object.B": &ResourceState{ @@ -249,26 +206,27 @@ func TestApplyGraphBuilder_destroyStateOnly(t *testing.T) { Attributes: map[string]string{}, }, Dependencies: []string{"test_object.A"}, + Provider: "provider.test", }, }, }, }, - } + }) b := &ApplyGraphBuilder{ Config: testModule(t, "empty"), - Diff: diff, + Changes: changes, State: state, Components: simpleMockComponentFactory(), Schemas: simpleTestSchemas(), DisableReduce: true, } - g, err := b.Build(addrs.RootModuleInstance) - if err != nil { - t.Fatalf("err: %s", err) + g, diags := b.Build(addrs.RootModuleInstance) + if diags.HasErrors() { + t.Fatalf("err: %s", diags.Err()) } - t.Logf("Graph: %s", g.String()) + t.Logf("Graph:\n%s", g.String()) if g.Path.String() != addrs.RootModuleInstance.String() { t.Fatalf("wrong path %q", g.Path.String()) @@ -282,23 +240,18 @@ func TestApplyGraphBuilder_destroyStateOnly(t *testing.T) { // This tests the ordering of destroying a single count of a resource. func TestApplyGraphBuilder_destroyCount(t *testing.T) { - diff := &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: []string{"root"}, - Resources: map[string]*InstanceDiff{ - "test_object.A.1": &InstanceDiff{ - Destroy: true, - }, - - "test_object.B": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "name": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, + changes := &plans.Changes{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: mustResourceInstanceAddr("test_object.A[1]"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + }, + }, + { + Addr: mustResourceInstanceAddr("test_object.B"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Update, }, }, }, @@ -306,7 +259,7 @@ func TestApplyGraphBuilder_destroyCount(t *testing.T) { b := &ApplyGraphBuilder{ Config: testModule(t, "graph-builder-apply-count"), - Diff: diff, + Changes: changes, Components: simpleMockComponentFactory(), Schemas: simpleTestSchemas(), DisableReduce: true, @@ -329,23 +282,18 @@ func TestApplyGraphBuilder_destroyCount(t *testing.T) { } func TestApplyGraphBuilder_moduleDestroy(t *testing.T) { - diff := &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: []string{"root", "A"}, - Resources: map[string]*InstanceDiff{ - "test_object.foo": &InstanceDiff{ - Destroy: true, - }, + changes := &plans.Changes{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: mustResourceInstanceAddr("module.A.test_object.foo"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, }, }, - - &ModuleDiff{ - Path: []string{"root", "B"}, - Resources: map[string]*InstanceDiff{ - "test_object.foo": &InstanceDiff{ - Destroy: true, - }, + { + Addr: mustResourceInstanceAddr("module.B.test_object.foo"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, }, }, }, @@ -353,7 +301,7 @@ func TestApplyGraphBuilder_moduleDestroy(t *testing.T) { b := &ApplyGraphBuilder{ Config: testModule(t, "graph-builder-apply-module-destroy"), - Diff: diff, + Changes: changes, Components: simpleMockComponentFactory(), Schemas: simpleTestSchemas(), } @@ -371,19 +319,12 @@ func TestApplyGraphBuilder_moduleDestroy(t *testing.T) { } func TestApplyGraphBuilder_provisioner(t *testing.T) { - diff := &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: []string{"root"}, - Resources: map[string]*InstanceDiff{ - "test_object.foo": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "test_string": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, + changes := &plans.Changes{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: mustResourceInstanceAddr("test_object.foo"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, }, }, }, @@ -391,7 +332,7 @@ func TestApplyGraphBuilder_provisioner(t *testing.T) { b := &ApplyGraphBuilder{ Config: testModule(t, "graph-builder-apply-provisioner"), - Diff: diff, + Changes: changes, Components: simpleMockComponentFactory(), Schemas: simpleTestSchemas(), } @@ -410,14 +351,12 @@ func TestApplyGraphBuilder_provisioner(t *testing.T) { } func TestApplyGraphBuilder_provisionerDestroy(t *testing.T) { - diff := &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: []string{"root"}, - Resources: map[string]*InstanceDiff{ - "test_object.foo": &InstanceDiff{ - Destroy: true, - }, + changes := &plans.Changes{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: mustResourceInstanceAddr("test_object.foo"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, }, }, }, @@ -426,7 +365,7 @@ func TestApplyGraphBuilder_provisionerDestroy(t *testing.T) { b := &ApplyGraphBuilder{ Destroy: true, Config: testModule(t, "graph-builder-apply-provisioner"), - Diff: diff, + Changes: changes, Components: simpleMockComponentFactory(), Schemas: simpleTestSchemas(), } @@ -445,32 +384,18 @@ func TestApplyGraphBuilder_provisionerDestroy(t *testing.T) { } func TestApplyGraphBuilder_targetModule(t *testing.T) { - diff := &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: []string{"root"}, - Resources: map[string]*InstanceDiff{ - "test_object.foo": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "test_string": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, + changes := &plans.Changes{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: mustResourceInstanceAddr("test_object.foo"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Update, }, }, - &ModuleDiff{ - Path: []string{"root", "child2"}, - Resources: map[string]*InstanceDiff{ - "test_object.foo": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "test_string": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, + { + Addr: mustResourceInstanceAddr("module.child2.test_object.foo"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Update, }, }, }, @@ -478,7 +403,7 @@ func TestApplyGraphBuilder_targetModule(t *testing.T) { b := &ApplyGraphBuilder{ Config: testModule(t, "graph-builder-apply-target-module"), - Diff: diff, + Changes: changes, Components: simpleMockComponentFactory(), Schemas: simpleTestSchemas(), Targets: []addrs.Targetable{ diff --git a/terraform/graph_builder_destroy_plan.go b/terraform/graph_builder_destroy_plan.go index 80568363d..59c09669e 100644 --- a/terraform/graph_builder_destroy_plan.go +++ b/terraform/graph_builder_destroy_plan.go @@ -4,6 +4,7 @@ import ( "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" ) @@ -17,17 +18,25 @@ type DestroyPlanGraphBuilder struct { Config *configs.Config // State is the current state - State *State + State *states.State - // Targets are resources to target - Targets []addrs.Targetable + // Components is a factory for the plug-in components (providers and + // provisioners) available for use. + Components contextComponentFactory // Schemas is the repository of schemas we will draw from to analyse // the configuration. Schemas *Schemas + // Targets are resources to target + Targets []addrs.Targetable + // Validate will do structural validation of the graph. Validate bool + + // ConcreteProvider, if set, gets an opportunity to specialize an + // abstract provider node. + ConcreteProvider ConcreteProviderNodeFunc } // See GraphBuilder @@ -48,7 +57,7 @@ func (b *DestroyPlanGraphBuilder) Steps() []GraphTransformer { } steps := []GraphTransformer{ - // Creates all the nodes represented in the state. + // Creates nodes for the resource instances tracked in the state. &StateTransformer{ Concrete: concreteResourceInstance, State: b.State, @@ -57,6 +66,8 @@ func (b *DestroyPlanGraphBuilder) Steps() []GraphTransformer { // Attach the configuration to any resources &AttachResourceConfigTransformer{Config: b.Config}, + TransformProviders(b.Components.ResourceProviders(), b.ConcreteProvider, b.Config), + // Destruction ordering. We require this only so that // targeting below will prune the correct things. &DestroyEdgeTransformer{ diff --git a/terraform/graph_builder_eval.go b/terraform/graph_builder_eval.go index 848859dff..eb6c897bf 100644 --- a/terraform/graph_builder_eval.go +++ b/terraform/graph_builder_eval.go @@ -4,6 +4,7 @@ import ( "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" ) @@ -27,7 +28,7 @@ type EvalGraphBuilder struct { Config *configs.Config // State is the current state - State *State + State *states.State // Components is a factory for the plug-in components (providers and // provisioners) available for use. diff --git a/terraform/graph_builder_plan.go b/terraform/graph_builder_plan.go index 135d52324..fd063c5ed 100644 --- a/terraform/graph_builder_plan.go +++ b/terraform/graph_builder_plan.go @@ -3,12 +3,11 @@ package terraform import ( "sync" - "github.com/hashicorp/terraform/tfdiags" - "github.com/hashicorp/terraform/addrs" - "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/tfdiags" ) // PlanGraphBuilder implements GraphBuilder and is responsible for building @@ -27,7 +26,7 @@ type PlanGraphBuilder struct { Config *configs.Config // State is the current state - State *State + State *states.State // Components is a factory for the plug-in components (providers and // provisioners) available for use. diff --git a/terraform/graph_builder_refresh.go b/terraform/graph_builder_refresh.go index 630a2af2f..84975314f 100644 --- a/terraform/graph_builder_refresh.go +++ b/terraform/graph_builder_refresh.go @@ -3,6 +3,7 @@ package terraform import ( "log" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/addrs" @@ -26,8 +27,8 @@ type RefreshGraphBuilder struct { // Config is the configuration tree. Config *configs.Config - // State is the current state - State *State + // State is the prior state + State *states.State // Components is a factory for the plug-in components (providers and // provisioners) available for use. diff --git a/terraform/graph_builder_refresh_test.go b/terraform/graph_builder_refresh_test.go index 6be874529..e27b383e5 100644 --- a/terraform/graph_builder_refresh_test.go +++ b/terraform/graph_builder_refresh_test.go @@ -11,7 +11,7 @@ func TestRefreshGraphBuilder_configOrphans(t *testing.T) { m := testModule(t, "refresh-config-orphan") - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -67,7 +67,7 @@ func TestRefreshGraphBuilder_configOrphans(t *testing.T) { }, }, }, - } + }) b := &RefreshGraphBuilder{ Config: m, diff --git a/terraform/graph_walk_context.go b/terraform/graph_walk_context.go index 9e0df140d..feb8d7305 100644 --- a/terraform/graph_walk_context.go +++ b/terraform/graph_walk_context.go @@ -7,12 +7,12 @@ import ( "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/tfdiags" - "github.com/hashicorp/terraform/addrs" - + "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/tfdiags" ) // ContextGraphWalker is the GraphWalker implementation used with the @@ -22,6 +22,8 @@ type ContextGraphWalker struct { // Configurable values Context *Context + State *states.SyncState // Used for safe concurrent access to state + Changes *plans.ChangesSync // Used for safe concurrent writes to changes Operation walkOperation StopContext context.Context RootVariableValues InputValues @@ -63,8 +65,7 @@ func (w *ContextGraphWalker) EnterPath(path addrs.ModuleInstance) EvalContext { Meta: w.Context.meta, Config: w.Context.config, Operation: w.Operation, - State: w.Context.state, - StateLock: &w.Context.stateLock, + State: w.State, Schemas: w.Context.schemas, VariableValues: w.variableValues, VariableValuesLock: &w.variableValuesLock, @@ -82,10 +83,8 @@ func (w *ContextGraphWalker) EnterPath(path addrs.ModuleInstance) EvalContext { ProviderLock: &w.providerLock, ProvisionerCache: w.provisionerCache, ProvisionerLock: &w.provisionerLock, - DiffValue: w.Context.diff, - DiffLock: &w.Context.diffLock, - StateValue: w.Context.state, - StateLock: &w.Context.stateLock, + ChangesValue: w.Changes, + StateValue: w.State, Evaluator: evaluator, VariableValues: w.variableValues, VariableValuesLock: &w.variableValuesLock, diff --git a/terraform/hook.go b/terraform/hook.go index ab11e8ee0..fd8965cde 100644 --- a/terraform/hook.go +++ b/terraform/hook.go @@ -1,5 +1,13 @@ package terraform +import ( + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" +) + // HookAction is an enum of actions that can be taken as a result of a hook // callback. This allows you to modify the behavior of Terraform at runtime. type HookAction byte @@ -21,42 +29,56 @@ const ( // NilHook into your struct, which implements all of the interface but does // nothing. Then, override only the functions you want to implement. type Hook interface { - // PreApply and PostApply are called before and after a single - // resource is applied. The error argument in PostApply is the + // PreApply and PostApply are called before and after an action for a + // single instance is applied. The error argument in PostApply is the // error, if any, that was returned from the provider Apply call itself. - PreApply(*InstanceInfo, *InstanceState, *InstanceDiff) (HookAction, error) - PostApply(*InstanceInfo, *InstanceState, error) (HookAction, error) + PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) + PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (HookAction, error) - // PreDiff and PostDiff are called before and after a single resource - // resource is diffed. - PreDiff(*InstanceInfo, *InstanceState) (HookAction, error) - PostDiff(*InstanceInfo, *InstanceDiff) (HookAction, error) + // PreDiff and PostDiff are called before and after a provider is given + // the opportunity to customize the proposed new state to produce the + // planned new state. + PreDiff(addr addrs.AbsResourceInstance, gen states.Generation, priorState, proposedNewState cty.Value) (HookAction, error) + PostDiff(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) - // Provisioning hooks + // The provisioning hooks signal both the overall start end end of + // provisioning for a particular instance and of each of the individual + // configured provisioners for each instance. The sequence of these + // for a given instance might look something like this: // - // All should be self-explanatory. ProvisionOutput is called with - // output sent back by the provisioners. This will be called multiple - // times as output comes in, but each call should represent a line of - // output. The ProvisionOutput method cannot control whether the - // hook continues running. - PreProvisionResource(*InstanceInfo, *InstanceState) (HookAction, error) - PostProvisionResource(*InstanceInfo, *InstanceState) (HookAction, error) - PreProvision(*InstanceInfo, string) (HookAction, error) - PostProvision(*InstanceInfo, string, error) (HookAction, error) - ProvisionOutput(*InstanceInfo, string, string) + // PreProvisionInstance(aws_instance.foo[1], ...) + // PreProvisionInstanceStep(aws_instance.foo[1], "file") + // PostProvisionInstanceStep(aws_instance.foo[1], "file", nil) + // PreProvisionInstanceStep(aws_instance.foo[1], "remote-exec") + // ProvisionOutput(aws_instance.foo[1], "remote-exec", "Installing foo...") + // ProvisionOutput(aws_instance.foo[1], "remote-exec", "Configuring bar...") + // PostProvisionInstanceStep(aws_instance.foo[1], "remote-exec", nil) + // PostProvisionInstance(aws_instance.foo[1], ...) + // + // ProvisionOutput is called with output sent back by the provisioners. + // This will be called multiple times as output comes in, with each call + // representing one line of output. It cannot control whether the + // provisioner continues running. + PreProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) + PostProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) + PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (HookAction, error) + PostProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string, err error) (HookAction, error) + ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, line string) // PreRefresh and PostRefresh are called before and after a single // resource state is refreshed, respectively. - PreRefresh(*InstanceInfo, *InstanceState) (HookAction, error) - PostRefresh(*InstanceInfo, *InstanceState) (HookAction, error) - - // PostStateUpdate is called after the state is updated. - PostStateUpdate(*State) (HookAction, error) + PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (HookAction, error) + PostRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value, newState cty.Value) (HookAction, error) // PreImportState and PostImportState are called before and after - // a single resource's state is being improted. - PreImportState(*InstanceInfo, string) (HookAction, error) - PostImportState(*InstanceInfo, []*InstanceState) (HookAction, error) + // (respectively) each state import operation for a given resource address. + PreImportState(addr addrs.AbsResourceInstance, importID string) (HookAction, error) + PostImportState(addr addrs.AbsResourceInstance, imported []*states.ImportedObject) (HookAction, error) + + // PostStateUpdate is called each time the state is updated. It receives + // a deep copy of the state, which it may therefore access freely without + // any need for locks to protect from concurrent writes from the caller. + PostStateUpdate(new *states.State) (HookAction, error) } // NilHook is a Hook implementation that does nothing. It exists only to @@ -64,59 +86,60 @@ type Hook interface { // and only implement the functions you are interested in. type NilHook struct{} -func (*NilHook) PreApply(*InstanceInfo, *InstanceState, *InstanceDiff) (HookAction, error) { +var _ Hook = (*NilHook)(nil) + +func (*NilHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PostApply(*InstanceInfo, *InstanceState, error) (HookAction, error) { +func (*NilHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PreDiff(*InstanceInfo, *InstanceState) (HookAction, error) { +func (*NilHook) PreDiff(addr addrs.AbsResourceInstance, gen states.Generation, priorState, proposedNewState cty.Value) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PostDiff(*InstanceInfo, *InstanceDiff) (HookAction, error) { +func (*NilHook) PostDiff(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PreProvisionResource(*InstanceInfo, *InstanceState) (HookAction, error) { +func (*NilHook) PreProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PostProvisionResource(*InstanceInfo, *InstanceState) (HookAction, error) { +func (*NilHook) PostProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PreProvision(*InstanceInfo, string) (HookAction, error) { +func (*NilHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PostProvision(*InstanceInfo, string, error) (HookAction, error) { +func (*NilHook) PostProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string, err error) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) ProvisionOutput( - *InstanceInfo, string, string) { +func (*NilHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, line string) { } -func (*NilHook) PreRefresh(*InstanceInfo, *InstanceState) (HookAction, error) { +func (*NilHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PostRefresh(*InstanceInfo, *InstanceState) (HookAction, error) { +func (*NilHook) PostRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value, newState cty.Value) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PreImportState(*InstanceInfo, string) (HookAction, error) { +func (*NilHook) PreImportState(addr addrs.AbsResourceInstance, importID string) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PostImportState(*InstanceInfo, []*InstanceState) (HookAction, error) { +func (*NilHook) PostImportState(addr addrs.AbsResourceInstance, imported []*states.ImportedObject) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PostStateUpdate(*State) (HookAction, error) { +func (*NilHook) PostStateUpdate(new *states.State) (HookAction, error) { return HookActionContinue, nil } diff --git a/terraform/hook_mock.go b/terraform/hook_mock.go index 0e4640067..9585bf80e 100644 --- a/terraform/hook_mock.go +++ b/terraform/hook_mock.go @@ -1,245 +1,273 @@ package terraform -import "sync" +import ( + "sync" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" +) // MockHook is an implementation of Hook that can be used for tests. // It records all of its function calls. type MockHook struct { sync.Mutex - PreApplyCalled bool - PreApplyInfo *InstanceInfo - PreApplyDiff *InstanceDiff - PreApplyState *InstanceState - PreApplyReturn HookAction - PreApplyError error + PreApplyCalled bool + PreApplyAddr addrs.AbsResourceInstance + PreApplyGen states.Generation + PreApplyAction plans.Action + PreApplyPriorState cty.Value + PreApplyPlannedState cty.Value + PreApplyReturn HookAction + PreApplyError error PostApplyCalled bool - PostApplyInfo *InstanceInfo - PostApplyState *InstanceState + PostApplyAddr addrs.AbsResourceInstance + PostApplyGen states.Generation + PostApplyNewState cty.Value PostApplyError error PostApplyReturn HookAction PostApplyReturnError error - PostApplyFn func(*InstanceInfo, *InstanceState, error) (HookAction, error) + PostApplyFn func(addrs.AbsResourceInstance, states.Generation, cty.Value, error) (HookAction, error) - PreDiffCalled bool - PreDiffInfo *InstanceInfo - PreDiffState *InstanceState - PreDiffReturn HookAction - PreDiffError error + PreDiffCalled bool + PreDiffAddr addrs.AbsResourceInstance + PreDiffGen states.Generation + PreDiffPriorState cty.Value + PreDiffProposedState cty.Value + PreDiffReturn HookAction + PreDiffError error - PostDiffCalled bool - PostDiffInfo *InstanceInfo - PostDiffDiff *InstanceDiff - PostDiffReturn HookAction - PostDiffError error + PostDiffCalled bool + PostDiffAddr addrs.AbsResourceInstance + PostDiffGen states.Generation + PostDiffAction plans.Action + PostDiffPriorState cty.Value + PostDiffPlannedState cty.Value + PostDiffReturn HookAction + PostDiffError error - PreProvisionResourceCalled bool - PreProvisionResourceInfo *InstanceInfo - PreProvisionInstanceState *InstanceState - PreProvisionResourceReturn HookAction - PreProvisionResourceError error + PreProvisionInstanceCalled bool + PreProvisionInstanceAddr addrs.AbsResourceInstance + PreProvisionInstanceState cty.Value + PreProvisionInstanceReturn HookAction + PreProvisionInstanceError error - PostProvisionResourceCalled bool - PostProvisionResourceInfo *InstanceInfo - PostProvisionInstanceState *InstanceState - PostProvisionResourceReturn HookAction - PostProvisionResourceError error + PostProvisionInstanceCalled bool + PostProvisionInstanceAddr addrs.AbsResourceInstance + PostProvisionInstanceState cty.Value + PostProvisionInstanceReturn HookAction + PostProvisionInstanceError error - PreProvisionCalled bool - PreProvisionInfo *InstanceInfo - PreProvisionProvisionerId string - PreProvisionReturn HookAction - PreProvisionError error + PreProvisionInstanceStepCalled bool + PreProvisionInstanceStepAddr addrs.AbsResourceInstance + PreProvisionInstanceStepProvisionerType string + PreProvisionInstanceStepReturn HookAction + PreProvisionInstanceStepError error - PostProvisionCalled bool - PostProvisionInfo *InstanceInfo - PostProvisionProvisionerId string - PostProvisionErrorArg error - PostProvisionReturn HookAction - PostProvisionError error + PostProvisionInstanceStepCalled bool + PostProvisionInstanceStepAddr addrs.AbsResourceInstance + PostProvisionInstanceStepProvisionerType string + PostProvisionInstanceStepErrorArg error + PostProvisionInstanceStepReturn HookAction + PostProvisionInstanceStepError error - ProvisionOutputCalled bool - ProvisionOutputInfo *InstanceInfo - ProvisionOutputProvisionerId string - ProvisionOutputMessage string + ProvisionOutputCalled bool + ProvisionOutputAddr addrs.AbsResourceInstance + ProvisionOutputProvisionerType string + ProvisionOutputMessage string - PostRefreshCalled bool - PostRefreshInfo *InstanceInfo - PostRefreshState *InstanceState - PostRefreshReturn HookAction - PostRefreshError error + PreRefreshCalled bool + PreRefreshAddr addrs.AbsResourceInstance + PreRefreshGen states.Generation + PreRefreshPriorState cty.Value + PreRefreshReturn HookAction + PreRefreshError error - PreRefreshCalled bool - PreRefreshInfo *InstanceInfo - PreRefreshState *InstanceState - PreRefreshReturn HookAction - PreRefreshError error + PostRefreshCalled bool + PostRefreshAddr addrs.AbsResourceInstance + PostRefreshGen states.Generation + PostRefreshPriorState cty.Value + PostRefreshNewState cty.Value + PostRefreshReturn HookAction + PostRefreshError error PreImportStateCalled bool - PreImportStateInfo *InstanceInfo - PreImportStateId string + PreImportStateAddr addrs.AbsResourceInstance + PreImportStateID string PreImportStateReturn HookAction PreImportStateError error - PostImportStateCalled bool - PostImportStateInfo *InstanceInfo - PostImportStateState []*InstanceState - PostImportStateReturn HookAction - PostImportStateError error + PostImportStateCalled bool + PostImportStateAddr addrs.AbsResourceInstance + PostImportStateNewStates []*states.ImportedObject + PostImportStateReturn HookAction + PostImportStateError error PostStateUpdateCalled bool - PostStateUpdateState *State + PostStateUpdateState *states.State PostStateUpdateReturn HookAction PostStateUpdateError error } -func (h *MockHook) PreApply(n *InstanceInfo, s *InstanceState, d *InstanceDiff) (HookAction, error) { +var _ Hook = (*MockHook)(nil) + +func (h *MockHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { h.Lock() defer h.Unlock() h.PreApplyCalled = true - h.PreApplyInfo = n - h.PreApplyDiff = d - h.PreApplyState = s + h.PreApplyAddr = addr + h.PreApplyGen = gen + h.PreApplyAction = action + h.PreApplyPriorState = priorState + h.PreApplyPlannedState = plannedNewState return h.PreApplyReturn, h.PreApplyError } -func (h *MockHook) PostApply(n *InstanceInfo, s *InstanceState, e error) (HookAction, error) { +func (h *MockHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (HookAction, error) { h.Lock() defer h.Unlock() h.PostApplyCalled = true - h.PostApplyInfo = n - h.PostApplyState = s - h.PostApplyError = e + h.PostApplyAddr = addr + h.PostApplyGen = gen + h.PostApplyNewState = newState + h.PostApplyError = err if h.PostApplyFn != nil { - return h.PostApplyFn(n, s, e) + return h.PostApplyFn(addr, gen, newState, err) } return h.PostApplyReturn, h.PostApplyReturnError } -func (h *MockHook) PreDiff(n *InstanceInfo, s *InstanceState) (HookAction, error) { +func (h *MockHook) PreDiff(addr addrs.AbsResourceInstance, gen states.Generation, priorState, proposedNewState cty.Value) (HookAction, error) { h.Lock() defer h.Unlock() h.PreDiffCalled = true - h.PreDiffInfo = n - h.PreDiffState = s + h.PreDiffAddr = addr + h.PreDiffGen = gen + h.PreDiffPriorState = priorState + h.PreDiffProposedState = proposedNewState return h.PreDiffReturn, h.PreDiffError } -func (h *MockHook) PostDiff(n *InstanceInfo, d *InstanceDiff) (HookAction, error) { +func (h *MockHook) PostDiff(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { h.Lock() defer h.Unlock() h.PostDiffCalled = true - h.PostDiffInfo = n - h.PostDiffDiff = d + h.PostDiffAddr = addr + h.PostDiffGen = gen + h.PostDiffAction = action + h.PostDiffPriorState = priorState + h.PostDiffPlannedState = plannedNewState return h.PostDiffReturn, h.PostDiffError } -func (h *MockHook) PreProvisionResource(n *InstanceInfo, s *InstanceState) (HookAction, error) { +func (h *MockHook) PreProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) { h.Lock() defer h.Unlock() - h.PreProvisionResourceCalled = true - h.PreProvisionResourceInfo = n - h.PreProvisionInstanceState = s - return h.PreProvisionResourceReturn, h.PreProvisionResourceError + h.PreProvisionInstanceCalled = true + h.PreProvisionInstanceAddr = addr + h.PreProvisionInstanceState = state + return h.PreProvisionInstanceReturn, h.PreProvisionInstanceError } -func (h *MockHook) PostProvisionResource(n *InstanceInfo, s *InstanceState) (HookAction, error) { +func (h *MockHook) PostProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) { h.Lock() defer h.Unlock() - h.PostProvisionResourceCalled = true - h.PostProvisionResourceInfo = n - h.PostProvisionInstanceState = s - return h.PostProvisionResourceReturn, h.PostProvisionResourceError + h.PostProvisionInstanceCalled = true + h.PostProvisionInstanceAddr = addr + h.PostProvisionInstanceState = state + return h.PostProvisionInstanceReturn, h.PostProvisionInstanceError } -func (h *MockHook) PreProvision(n *InstanceInfo, provId string) (HookAction, error) { +func (h *MockHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (HookAction, error) { h.Lock() defer h.Unlock() - h.PreProvisionCalled = true - h.PreProvisionInfo = n - h.PreProvisionProvisionerId = provId - return h.PreProvisionReturn, h.PreProvisionError + h.PreProvisionInstanceStepCalled = true + h.PreProvisionInstanceStepAddr = addr + h.PreProvisionInstanceStepProvisionerType = typeName + return h.PreProvisionInstanceStepReturn, h.PreProvisionInstanceStepError } -func (h *MockHook) PostProvision(n *InstanceInfo, provId string, err error) (HookAction, error) { +func (h *MockHook) PostProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string, err error) (HookAction, error) { h.Lock() defer h.Unlock() - h.PostProvisionCalled = true - h.PostProvisionInfo = n - h.PostProvisionProvisionerId = provId - h.PostProvisionErrorArg = err - return h.PostProvisionReturn, h.PostProvisionError + h.PostProvisionInstanceStepCalled = true + h.PostProvisionInstanceStepAddr = addr + h.PostProvisionInstanceStepProvisionerType = typeName + h.PostProvisionInstanceStepErrorArg = err + return h.PostProvisionInstanceStepReturn, h.PostProvisionInstanceStepError } -func (h *MockHook) ProvisionOutput( - n *InstanceInfo, - provId string, - msg string) { +func (h *MockHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, line string) { h.Lock() defer h.Unlock() h.ProvisionOutputCalled = true - h.ProvisionOutputInfo = n - h.ProvisionOutputProvisionerId = provId - h.ProvisionOutputMessage = msg + h.ProvisionOutputAddr = addr + h.ProvisionOutputProvisionerType = typeName + h.ProvisionOutputMessage = line } -func (h *MockHook) PreRefresh(n *InstanceInfo, s *InstanceState) (HookAction, error) { +func (h *MockHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (HookAction, error) { h.Lock() defer h.Unlock() h.PreRefreshCalled = true - h.PreRefreshInfo = n - h.PreRefreshState = s + h.PreRefreshAddr = addr + h.PreRefreshGen = gen + h.PreRefreshPriorState = priorState return h.PreRefreshReturn, h.PreRefreshError } -func (h *MockHook) PostRefresh(n *InstanceInfo, s *InstanceState) (HookAction, error) { +func (h *MockHook) PostRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value, newState cty.Value) (HookAction, error) { h.Lock() defer h.Unlock() h.PostRefreshCalled = true - h.PostRefreshInfo = n - h.PostRefreshState = s + h.PostRefreshAddr = addr + h.PostRefreshPriorState = priorState + h.PostRefreshNewState = newState return h.PostRefreshReturn, h.PostRefreshError } -func (h *MockHook) PreImportState(info *InstanceInfo, id string) (HookAction, error) { +func (h *MockHook) PreImportState(addr addrs.AbsResourceInstance, importID string) (HookAction, error) { h.Lock() defer h.Unlock() h.PreImportStateCalled = true - h.PreImportStateInfo = info - h.PreImportStateId = id + h.PreImportStateAddr = addr + h.PreImportStateID = importID return h.PreImportStateReturn, h.PreImportStateError } -func (h *MockHook) PostImportState(info *InstanceInfo, s []*InstanceState) (HookAction, error) { +func (h *MockHook) PostImportState(addr addrs.AbsResourceInstance, imported []*states.ImportedObject) (HookAction, error) { h.Lock() defer h.Unlock() h.PostImportStateCalled = true - h.PostImportStateInfo = info - h.PostImportStateState = s + h.PostImportStateAddr = addr + h.PostImportStateNewStates = imported return h.PostImportStateReturn, h.PostImportStateError } -func (h *MockHook) PostStateUpdate(s *State) (HookAction, error) { +func (h *MockHook) PostStateUpdate(new *states.State) (HookAction, error) { h.Lock() defer h.Unlock() h.PostStateUpdateCalled = true - h.PostStateUpdateState = s + h.PostStateUpdateState = new return h.PostStateUpdateReturn, h.PostStateUpdateError } diff --git a/terraform/hook_stop.go b/terraform/hook_stop.go index 104d0098a..917a12681 100644 --- a/terraform/hook_stop.go +++ b/terraform/hook_stop.go @@ -2,6 +2,12 @@ package terraform import ( "sync/atomic" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" ) // stopHook is a private Hook implementation that Terraform uses to @@ -10,58 +16,60 @@ type stopHook struct { stop uint32 } -func (h *stopHook) PreApply(*InstanceInfo, *InstanceState, *InstanceDiff) (HookAction, error) { +var _ Hook = (*stopHook)(nil) + +func (h *stopHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { return h.hook() } -func (h *stopHook) PostApply(*InstanceInfo, *InstanceState, error) (HookAction, error) { +func (h *stopHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (HookAction, error) { return h.hook() } -func (h *stopHook) PreDiff(*InstanceInfo, *InstanceState) (HookAction, error) { +func (h *stopHook) PreDiff(addr addrs.AbsResourceInstance, gen states.Generation, priorState, proposedNewState cty.Value) (HookAction, error) { return h.hook() } -func (h *stopHook) PostDiff(*InstanceInfo, *InstanceDiff) (HookAction, error) { +func (h *stopHook) PostDiff(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { return h.hook() } -func (h *stopHook) PreProvisionResource(*InstanceInfo, *InstanceState) (HookAction, error) { +func (h *stopHook) PreProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) { return h.hook() } -func (h *stopHook) PostProvisionResource(*InstanceInfo, *InstanceState) (HookAction, error) { +func (h *stopHook) PostProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) { return h.hook() } -func (h *stopHook) PreProvision(*InstanceInfo, string) (HookAction, error) { +func (h *stopHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (HookAction, error) { return h.hook() } -func (h *stopHook) PostProvision(*InstanceInfo, string, error) (HookAction, error) { +func (h *stopHook) PostProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string, err error) (HookAction, error) { return h.hook() } -func (h *stopHook) ProvisionOutput(*InstanceInfo, string, string) { +func (h *stopHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, line string) { } -func (h *stopHook) PreRefresh(*InstanceInfo, *InstanceState) (HookAction, error) { +func (h *stopHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (HookAction, error) { return h.hook() } -func (h *stopHook) PostRefresh(*InstanceInfo, *InstanceState) (HookAction, error) { +func (h *stopHook) PostRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value, newState cty.Value) (HookAction, error) { return h.hook() } -func (h *stopHook) PreImportState(*InstanceInfo, string) (HookAction, error) { +func (h *stopHook) PreImportState(addr addrs.AbsResourceInstance, importID string) (HookAction, error) { return h.hook() } -func (h *stopHook) PostImportState(*InstanceInfo, []*InstanceState) (HookAction, error) { +func (h *stopHook) PostImportState(addr addrs.AbsResourceInstance, imported []*states.ImportedObject) (HookAction, error) { return h.hook() } -func (h *stopHook) PostStateUpdate(*State) (HookAction, error) { +func (h *stopHook) PostStateUpdate(new *states.State) (HookAction, error) { return h.hook() } diff --git a/terraform/hook_test.go b/terraform/hook_test.go index caa4f8087..48316b53c 100644 --- a/terraform/hook_test.go +++ b/terraform/hook_test.go @@ -2,6 +2,12 @@ package terraform import ( "testing" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" ) func TestNilHook_impl(t *testing.T) { @@ -15,6 +21,8 @@ type testHook struct { Calls []*testHookCall } +var _ Hook = (*testHook)(nil) + // testHookCall represents a single call in testHook. // This hook just logs string names to make it easy to write "want" expressions // in tests that can DeepEqual against the real calls. @@ -23,73 +31,71 @@ type testHookCall struct { InstanceID string } -func (h *testHook) PreApply(i *InstanceInfo, s *InstanceState, d *InstanceDiff) (HookAction, error) { - h.Calls = append(h.Calls, &testHookCall{"PreApply", i.ResourceAddress().String()}) +func (h *testHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { + h.Calls = append(h.Calls, &testHookCall{"PreApply", addr.String()}) return HookActionContinue, nil } -func (h *testHook) PostApply(i *InstanceInfo, s *InstanceState, err error) (HookAction, error) { - h.Calls = append(h.Calls, &testHookCall{"PostApply", i.ResourceAddress().String()}) +func (h *testHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (HookAction, error) { + h.Calls = append(h.Calls, &testHookCall{"PostApply", addr.String()}) return HookActionContinue, nil } -func (h *testHook) PreDiff(i *InstanceInfo, s *InstanceState) (HookAction, error) { - h.Calls = append(h.Calls, &testHookCall{"PreDiff", i.ResourceAddress().String()}) +func (h *testHook) PreDiff(addr addrs.AbsResourceInstance, gen states.Generation, priorState, proposedNewState cty.Value) (HookAction, error) { + h.Calls = append(h.Calls, &testHookCall{"PreDiff", addr.String()}) return HookActionContinue, nil } -func (h *testHook) PostDiff(i *InstanceInfo, d *InstanceDiff) (HookAction, error) { - h.Calls = append(h.Calls, &testHookCall{"PostDiff", i.ResourceAddress().String()}) +func (h *testHook) PostDiff(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { + h.Calls = append(h.Calls, &testHookCall{"PostDiff", addr.String()}) return HookActionContinue, nil } -func (h *testHook) PreProvisionResource(i *InstanceInfo, s *InstanceState) (HookAction, error) { - h.Calls = append(h.Calls, &testHookCall{"PreProvisionResource", i.ResourceAddress().String()}) +func (h *testHook) PreProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) { + h.Calls = append(h.Calls, &testHookCall{"PreProvisionInstance", addr.String()}) return HookActionContinue, nil } -func (h *testHook) PostProvisionResource(i *InstanceInfo, s *InstanceState) (HookAction, error) { - h.Calls = append(h.Calls, &testHookCall{"PostProvisionResource", i.ResourceAddress().String()}) +func (h *testHook) PostProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) { + h.Calls = append(h.Calls, &testHookCall{"PostProvisionInstance", addr.String()}) return HookActionContinue, nil } -func (h *testHook) PreProvision(i *InstanceInfo, n string) (HookAction, error) { - h.Calls = append(h.Calls, &testHookCall{"PreProvision", i.ResourceAddress().String()}) +func (h *testHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (HookAction, error) { + h.Calls = append(h.Calls, &testHookCall{"PreProvisionInstanceStep", addr.String()}) return HookActionContinue, nil } -func (h *testHook) PostProvision(i *InstanceInfo, n string, err error) (HookAction, error) { - h.Calls = append(h.Calls, &testHookCall{"PostProvision", i.ResourceAddress().String()}) +func (h *testHook) PostProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string, err error) (HookAction, error) { + h.Calls = append(h.Calls, &testHookCall{"PostProvisionInstanceStep", addr.String()}) return HookActionContinue, nil } -func (h *testHook) ProvisionOutput(i *InstanceInfo, n string, m string) { - h.Calls = append(h.Calls, &testHookCall{"ProvisionOutput", i.ResourceAddress().String()}) +func (h *testHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, line string) { + h.Calls = append(h.Calls, &testHookCall{"ProvisionOutput", addr.String()}) } -func (h *testHook) PreRefresh(i *InstanceInfo, s *InstanceState) (HookAction, error) { - h.Calls = append(h.Calls, &testHookCall{"PreRefresh", i.ResourceAddress().String()}) +func (h *testHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (HookAction, error) { + h.Calls = append(h.Calls, &testHookCall{"PreRefresh", addr.String()}) return HookActionContinue, nil } -func (h *testHook) PostRefresh(i *InstanceInfo, s *InstanceState) (HookAction, error) { - h.Calls = append(h.Calls, &testHookCall{"PostRefresh", i.ResourceAddress().String()}) +func (h *testHook) PostRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value, newState cty.Value) (HookAction, error) { + h.Calls = append(h.Calls, &testHookCall{"PostRefresh", addr.String()}) return HookActionContinue, nil } -func (h *testHook) PreImportState(i *InstanceInfo, n string) (HookAction, error) { - h.Calls = append(h.Calls, &testHookCall{"PreImportState", i.ResourceAddress().String()}) +func (h *testHook) PreImportState(addr addrs.AbsResourceInstance, importID string) (HookAction, error) { + h.Calls = append(h.Calls, &testHookCall{"PreImportState", addr.String()}) return HookActionContinue, nil } -func (h *testHook) PostImportState(i *InstanceInfo, ss []*InstanceState) (HookAction, error) { - h.Calls = append(h.Calls, &testHookCall{"PostImportState", i.ResourceAddress().String()}) +func (h *testHook) PostImportState(addr addrs.AbsResourceInstance, imported []*states.ImportedObject) (HookAction, error) { + h.Calls = append(h.Calls, &testHookCall{"PostImportState", addr.String()}) return HookActionContinue, nil } -func (h *testHook) PostStateUpdate(s *State) (HookAction, error) { +func (h *testHook) PostStateUpdate(new *states.State) (HookAction, error) { h.Calls = append(h.Calls, &testHookCall{"PostStateUpdate", ""}) return HookActionContinue, nil } - -var _ Hook = new(testHook) diff --git a/terraform/module_dependencies.go b/terraform/module_dependencies.go index bc8e7dca7..66a68c7de 100644 --- a/terraform/module_dependencies.go +++ b/terraform/module_dependencies.go @@ -2,11 +2,12 @@ package terraform import ( version "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/config/module" + + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/moduledeps" "github.com/hashicorp/terraform/plugin/discovery" + "github.com/hashicorp/terraform/states" ) // ConfigTreeDependencies returns the dependencies of the tree of modules @@ -14,7 +15,7 @@ import ( // // Both configuration and state are required because there can be resources // implied by instances in the state that no longer exist in config. -func ConfigTreeDependencies(root *configs.Config, state *State) *moduledeps.Module { +func ConfigTreeDependencies(root *configs.Config, state *states.State) *moduledeps.Module { // First we walk the configuration tree to build the overall structure // and capture the explicit/implicit/inherited provider dependencies. deps := configTreeConfigDependencies(root, nil) @@ -155,15 +156,14 @@ func configTreeConfigDependencies(root *configs.Config, inheritProviders map[str return ret } -func configTreeMergeStateDependencies(root *moduledeps.Module, state *State) { +func configTreeMergeStateDependencies(root *moduledeps.Module, state *states.State) { if state == nil { return } - findModule := func(path []string) *moduledeps.Module { + findModule := func(path addrs.ModuleInstance) *moduledeps.Module { module := root - realPath := normalizeModulePath(path) - for _, step := range realPath { // skip initial "root" + for _, step := range path { var next *moduledeps.Module for _, cm := range module.Children { if cm.Name == step.Name { @@ -175,7 +175,8 @@ func configTreeMergeStateDependencies(root *moduledeps.Module, state *State) { if next == nil { // If we didn't find a next node, we'll need to make one next = &moduledeps.Module{ - Name: step.Name, + Name: step.Name, + Providers: make(moduledeps.Providers), } module.Children = append(module.Children, next) } @@ -186,15 +187,11 @@ func configTreeMergeStateDependencies(root *moduledeps.Module, state *State) { } for _, ms := range state.Modules { - module := findModule(ms.Path) + module := findModule(ms.Addr) - for _, is := range ms.Resources { - fullName := config.ResourceProviderFullName(is.Type, is.Provider) - inst := moduledeps.ProviderInstance(fullName) + for _, rs := range ms.Resources { + inst := moduledeps.ProviderInstance(rs.ProviderConfig.ProviderConfig.StringCompact()) if _, exists := module.Providers[inst]; !exists { - if module.Providers == nil { - module.Providers = make(moduledeps.Providers) - } module.Providers[inst] = moduledeps.ProviderDependency{ Constraints: discovery.AllVersions, Reason: moduledeps.ProviderDependencyFromState, @@ -203,109 +200,3 @@ func configTreeMergeStateDependencies(root *moduledeps.Module, state *State) { } } } - -// ModuleTreeDependencies returns the dependencies of the tree of modules -// described by the given configuration tree and state. -// -// Both configuration and state are required because there can be resources -// implied by instances in the state that no longer exist in config. -// -// This function will panic if any invalid version constraint strings are -// present in the configuration. This is guaranteed not to happen for any -// configuration that has passed a call to Config.Validate(). -func ModuleTreeDependencies(root *module.Tree, state *State) *moduledeps.Module { - // First we walk the configuration tree to build the overall structure - // and capture the explicit/implicit/inherited provider dependencies. - deps := moduleTreeConfigDependencies(root, nil) - - // Next we walk over the resources in the state to catch any additional - // dependencies created by existing resources that are no longer in config. - // Most things we find in state will already be present in 'deps', but - // we're interested in the rare thing that isn't. - moduleTreeMergeStateDependencies(deps, state) - - return deps -} - -func moduleTreeConfigDependencies(root *module.Tree, inheritProviders map[string]*config.ProviderConfig) *moduledeps.Module { - if root == nil { - // If no config is provided, we'll make a synthetic root. - // This isn't necessarily correct if we're called with a nil that - // *isn't* at the root, but in practice that can never happen. - return &moduledeps.Module{ - Name: "root", - } - } - - ret := &moduledeps.Module{ - Name: root.Name(), - } - - cfg := root.Config() - providerConfigs := cfg.ProviderConfigsByFullName() - - // Provider dependencies - { - providers := make(moduledeps.Providers, len(providerConfigs)) - - // Any providerConfigs elements are *explicit* provider dependencies, - // which is the only situation where the user might provide an actual - // version constraint. We'll take care of these first. - for fullName, pCfg := range providerConfigs { - inst := moduledeps.ProviderInstance(fullName) - versionSet := discovery.AllVersions - if pCfg.Version != "" { - versionSet = discovery.ConstraintStr(pCfg.Version).MustParse() - } - providers[inst] = moduledeps.ProviderDependency{ - Constraints: versionSet, - Reason: moduledeps.ProviderDependencyExplicit, - } - } - - // Each resource in the configuration creates an *implicit* provider - // dependency, though we'll only record it if there isn't already - // an explicit dependency on the same provider. - for _, rc := range cfg.Resources { - fullName := rc.ProviderFullName() - inst := moduledeps.ProviderInstance(fullName) - if _, exists := providers[inst]; exists { - // Explicit dependency already present - continue - } - - reason := moduledeps.ProviderDependencyImplicit - if _, inherited := inheritProviders[fullName]; inherited { - reason = moduledeps.ProviderDependencyInherited - } - - providers[inst] = moduledeps.ProviderDependency{ - Constraints: discovery.AllVersions, - Reason: reason, - } - } - - ret.Providers = providers - } - - childInherit := make(map[string]*config.ProviderConfig) - for k, v := range inheritProviders { - childInherit[k] = v - } - for k, v := range providerConfigs { - childInherit[k] = v - } - for _, c := range root.Children() { - ret.Children = append(ret.Children, moduleTreeConfigDependencies(c, childInherit)) - } - - return ret -} - -func moduleTreeMergeStateDependencies(root *moduledeps.Module, state *State) { - // This is really just the same logic as configTreeMergeStateDependencies - // but we retain this old name just to keep the symmetry until we've - // removed all of these "moduleTree..." versions that use the legacy - // configuration structs. - configTreeMergeStateDependencies(root, state) -} diff --git a/terraform/module_dependencies_test.go b/terraform/module_dependencies_test.go index 8a1af1417..e79777e24 100644 --- a/terraform/module_dependencies_test.go +++ b/terraform/module_dependencies_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/go-test/deep" + "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/moduledeps" "github.com/hashicorp/terraform/plugin/discovery" @@ -223,7 +224,8 @@ func TestModuleTreeDependencies(t *testing.T) { }, Children: []*moduledeps.Module{ { - Name: "child", + Name: "child", + Providers: make(moduledeps.Providers), Children: []*moduledeps.Module{ { Name: "grandchild", @@ -248,7 +250,7 @@ func TestModuleTreeDependencies(t *testing.T) { root = testModule(t, test.ConfigDir) } - got := ConfigTreeDependencies(root, test.State) + got := ConfigTreeDependencies(root, mustShimLegacyState(test.State)) for _, problem := range deep.Equal(got, test.Want) { t.Error(problem) } diff --git a/terraform/node_data_destroy.go b/terraform/node_data_destroy.go index e71806363..477fd0aba 100644 --- a/terraform/node_data_destroy.go +++ b/terraform/node_data_destroy.go @@ -1,5 +1,9 @@ package terraform +import ( + "github.com/hashicorp/terraform/states" +) + // NodeDestroyableDataResource represents a resource that is "destroyable": // it is ready to be destroyed. type NodeDestroyableDataResource struct { @@ -10,13 +14,10 @@ type NodeDestroyableDataResource struct { func (n *NodeDestroyableDataResource) EvalTree() EvalNode { addr := n.ResourceInstanceAddr() - // stateId is the ID to put into the state - stateId := NewLegacyResourceInstanceAddress(addr).stateId() - // Just destroy it. - var state *InstanceState + var state *states.ResourceInstanceObject return &EvalWriteState{ - Name: stateId, + Addr: addr.Resource, State: &state, // state is nil here } } diff --git a/terraform/node_data_refresh.go b/terraform/node_data_refresh.go index 7e4c1a917..278a0ac39 100644 --- a/terraform/node_data_refresh.go +++ b/terraform/node_data_refresh.go @@ -2,6 +2,8 @@ package terraform import ( "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" "github.com/zclconf/go-cty/cty" ) @@ -34,10 +36,10 @@ func (n *NodeRefreshableDataResource) DynamicExpand(ctx EvalContext) (*Graph, er // if we're transitioning whether "count" is set at all. fixResourceCountSetTransition(ctx, n.ResourceAddr().Resource, count != -1) - // Grab the state which we read - state, lock := ctx.State() - lock.RLock() - defer lock.RUnlock() + // Our graph transformers require access to the full state, so we'll + // temporarily lock it while we work on this. + state := ctx.State().Lock() + defer ctx.State().Unlock() // The concrete resource factory we'll use concreteResource := func(a *NodeAbstractResourceInstance) dag.Vertex { @@ -114,54 +116,31 @@ type NodeRefreshableDataResourceInstance struct { func (n *NodeRefreshableDataResourceInstance) EvalTree() EvalNode { addr := n.ResourceInstanceAddr() - // State still uses legacy-style internal ids, so we need to shim to get - // a suitable key to use. - stateId := NewLegacyResourceInstanceAddress(addr).stateId() - - // Get the state if we have it. If not, we'll build it. - rs := n.ResourceState - if rs == nil { - rs = &ResourceState{ - Type: addr.Resource.Resource.Type, - Provider: n.ResolvedProvider.String(), - } - } - - // If we have a configuration then we'll build a fresh state. - if n.Config != nil { - rs = &ResourceState{ - Type: addr.Resource.Resource.Type, - Provider: n.ResolvedProvider.String(), - Dependencies: n.StateReferences(), - } - } - // These variables are the state for the eval sequence below, and are // updated through pointers. var provider ResourceProvider var providerSchema *ProviderSchema - var diff *InstanceDiff - var state *InstanceState + var change *plans.ResourceInstanceChange + var state *states.ResourceInstanceObject var configVal cty.Value return &EvalSequence{ Nodes: []EvalNode{ + &EvalGetProvider{ + Addr: n.ResolvedProvider, + Output: &provider, + Schema: &providerSchema, + }, + // Always destroy the existing state first, since we must // make sure that values from a previous read will not // get interpolated if we end up needing to defer our // loading until apply time. &EvalWriteState{ - Name: stateId, - ResourceType: rs.Type, - Provider: n.ResolvedProvider, - Dependencies: rs.Dependencies, - State: &state, // state is nil here - }, - - &EvalGetProvider{ - Addr: n.ResolvedProvider, - Output: &provider, - Schema: &providerSchema, + Addr: addr.Resource, + ProviderAddr: n.ResolvedProvider, + State: &state, // a pointer to nil, here + ProviderSchema: &providerSchema, }, &EvalReadDataDiff{ @@ -169,7 +148,7 @@ func (n *NodeRefreshableDataResourceInstance) EvalTree() EvalNode { Config: n.Config, Provider: &provider, ProviderSchema: &providerSchema, - Output: &diff, + Output: &change, OutputValue: &configVal, OutputState: &state, }, @@ -198,17 +177,16 @@ func (n *NodeRefreshableDataResourceInstance) EvalTree() EvalNode { &EvalReadDataApply{ Addr: addr.Resource, - Diff: &diff, + Change: &change, Provider: &provider, Output: &state, }, &EvalWriteState{ - Name: stateId, - ResourceType: rs.Type, - Provider: n.ResolvedProvider, - Dependencies: rs.Dependencies, - State: &state, + Addr: addr.Resource, + ProviderAddr: n.ResolvedProvider, + State: &state, + ProviderSchema: &providerSchema, }, &EvalUpdateStateHook{}, diff --git a/terraform/node_data_refresh_test.go b/terraform/node_data_refresh_test.go index ed00303c1..fc6573335 100644 --- a/terraform/node_data_refresh_test.go +++ b/terraform/node_data_refresh_test.go @@ -1,19 +1,17 @@ package terraform import ( - "sync" "testing" - "github.com/hashicorp/terraform/addrs" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/addrs" ) func TestNodeRefreshableDataResourceDynamicExpand_scaleOut(t *testing.T) { - var stateLock sync.RWMutex - m := testModule(t, "refresh-data-scale-inout") - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -37,7 +35,7 @@ func TestNodeRefreshableDataResourceDynamicExpand_scaleOut(t *testing.T) { }, }, }, - } + }) n := &NodeRefreshableDataResource{ NodeAbstractResource: &NodeAbstractResource{ @@ -52,8 +50,7 @@ func TestNodeRefreshableDataResourceDynamicExpand_scaleOut(t *testing.T) { g, err := n.DynamicExpand(&MockEvalContext{ PathPath: addrs.RootModuleInstance, - StateState: state, - StateLock: &stateLock, + StateState: state.SyncWrapper(), // DynamicExpand will call EvaluateExpr to evaluate the "count" // expression, which is just a literal number 3 in the fixture config @@ -79,11 +76,9 @@ root - terraform.graphNodeRoot } func TestNodeRefreshableDataResourceDynamicExpand_scaleIn(t *testing.T) { - var stateLock sync.RWMutex - m := testModule(t, "refresh-data-scale-inout") - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -123,7 +118,7 @@ func TestNodeRefreshableDataResourceDynamicExpand_scaleIn(t *testing.T) { }, }, }, - } + }) n := &NodeRefreshableDataResource{ NodeAbstractResource: &NodeAbstractResource{ @@ -138,8 +133,7 @@ func TestNodeRefreshableDataResourceDynamicExpand_scaleIn(t *testing.T) { g, err := n.DynamicExpand(&MockEvalContext{ PathPath: addrs.RootModuleInstance, - StateState: state, - StateLock: &stateLock, + StateState: state.SyncWrapper(), // DynamicExpand will call EvaluateExpr to evaluate the "count" // expression, which is just a literal number 3 in the fixture config diff --git a/terraform/node_module_removed.go b/terraform/node_module_removed.go index f681aa32e..cb55a1a88 100644 --- a/terraform/node_module_removed.go +++ b/terraform/node_module_removed.go @@ -2,7 +2,6 @@ package terraform import ( "fmt" - "log" "github.com/hashicorp/terraform/addrs" ) @@ -33,7 +32,7 @@ func (n *NodeModuleRemoved) Path() addrs.ModuleInstance { func (n *NodeModuleRemoved) EvalTree() EvalNode { return &EvalOpFilter{ Ops: []walkOperation{walkRefresh, walkApply, walkDestroy}, - Node: &EvalDeleteModule{ + Node: &EvalCheckModuleRemoved{ Addr: n.Addr, }, } @@ -64,42 +63,19 @@ func (n *NodeModuleRemoved) References() []*addrs.Reference { } } -// EvalDeleteModule is an EvalNode implementation that removes an empty module -// entry from the state. -type EvalDeleteModule struct { +// EvalCheckModuleRemoved is an EvalNode implementation that verifies that +// a module has been removed from the state as expected. +type EvalCheckModuleRemoved struct { Addr addrs.ModuleInstance } -func (n *EvalDeleteModule) Eval(ctx EvalContext) (interface{}, error) { - state, lock := ctx.State() - if state == nil { - return nil, nil - } - - // Get a write lock so we can access this instance - lock.Lock() - defer lock.Unlock() - - // Make sure we have a clean state - // Destroyed resources aren't deleted, they're written with an ID of "". - state.prune() - - // find the module and delete it - for i, m := range state.Modules { - // Since state is still using our old-style []string path representation, - // comparison is a little awkward. This can be simplified once state - // is updated to use addrs.ModuleInstance too. - if normalizeModulePath(m.Path).String() != n.Addr.String() { - continue - } - if !m.Empty() { - // a targeted apply may leave module resources even without a config, - // so just log this and return. - log.Printf("[DEBUG] not removing %s from state: not empty", n.Addr) - break - } - state.Modules = append(state.Modules[:i], state.Modules[i+1:]...) - break +func (n *EvalCheckModuleRemoved) Eval(ctx EvalContext) (interface{}, error) { + mod := ctx.State().Module(n.Addr) + if mod != nil { + // If we get here then that indicates a bug either in the states + // module or in an earlier step of the graph walk, since we should've + // pruned out the module when the last resource was removed from it. + return nil, fmt.Errorf("leftover module %s in state that should have been removed; this is a bug in Terraform and should be reported", n.Addr) } return nil, nil } diff --git a/terraform/node_resource_abstract.go b/terraform/node_resource_abstract.go index 82e584164..cfd636532 100644 --- a/terraform/node_resource_abstract.go +++ b/terraform/node_resource_abstract.go @@ -4,14 +4,13 @@ import ( "fmt" "log" - "github.com/hashicorp/hcl2/hcl" - "github.com/hashicorp/hcl2/hcl/hclsyntax" - "github.com/hashicorp/terraform/addrs" - "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/dag" "github.com/hashicorp/terraform/lang" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/tfdiags" ) // ConcreteResourceNodeFunc is a callback type used to convert an @@ -93,7 +92,7 @@ type NodeAbstractResourceInstance struct { // interfaces if you're running those transforms, but also be explicitly // set if you already have that information. - ResourceState *ResourceState // the ResourceState for this instance + ResourceState *states.Resource } var ( @@ -224,26 +223,30 @@ func (n *NodeAbstractResourceInstance) References() []*addrs.Reference { // Otherwise, if we have state then we'll use the values stored in state // as a fallback. - if s := n.ResourceState; s != nil { - // State is still storing dependencies as old-style strings, so we'll - // need to do a little work here to massage this to the form we now - // want. - var result []*addrs.Reference - for _, legacyDep := range s.Dependencies { - traversal, diags := hclsyntax.ParseTraversalAbs([]byte(legacyDep), "", hcl.Pos{}) - if diags.HasErrors() { - log.Printf("[ERROR] Can't parse %q from dependencies in state as a reference: invalid syntax", legacyDep) - continue - } - ref, err := addrs.ParseRef(traversal) - if err != nil { - log.Printf("[ERROR] Can't parse %q from dependencies in state as a reference: invalid syntax", legacyDep) - continue - } + if rs := n.ResourceState; rs != nil { + if s := rs.Instance(n.InstanceKey); s != nil { + // State is still storing dependencies as old-style strings, so we'll + // need to do a little work here to massage this to the form we now + // want. + var result []*addrs.Reference + for _, addr := range s.Current.Dependencies { + if addr == nil { + // Should never happen; indicates a bug in the state loader + panic(fmt.Sprintf("dependencies for current object on %s contains nil address", n.ResourceInstanceAddr())) + } - result = append(result, ref) + // This is a little weird: we need to manufacture an addrs.Reference + // with a fake range here because the state isn't something we can + // make source references into. + result = append(result, &addrs.Reference{ + Subject: addr, + SourceRange: tfdiags.SourceRange{ + Filename: "(state file)", + }, + }) + } + return result } - return result } // If we have neither config nor state then we have no references. @@ -252,7 +255,7 @@ func (n *NodeAbstractResourceInstance) References() []*addrs.Reference { // converts an instance address to the legacy dotted notation func dottedInstanceAddr(tr addrs.ResourceInstance) string { - // For historical reasons, state uses dot-separated instance keys, + // The legacy state format uses dot-separated instance keys, // rather than bracketed as in our modern syntax. var suffix string switch tk := tr.Key.(type) { @@ -329,26 +332,13 @@ func (n *NodeAbstractResourceInstance) ProvidedBy() (addrs.AbsProviderConfig, bo } // If we have state, then we will use the provider from there - if n.ResourceState != nil && n.ResourceState.Provider != "" { - traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(n.ResourceState.Provider), "", hcl.Pos{}) - if parseDiags.HasErrors() { - log.Printf("[ERROR] %s has syntax-invalid provider address %q", n.Addr, n.ResourceState.Provider) - goto Guess - } - - addr, diags := addrs.ParseAbsProviderConfig(traversal) - if diags.HasErrors() { - log.Printf("[ERROR] %s has content-invalid provider address %q", n.Addr, n.ResourceState.Provider) - goto Guess - } - + if n.ResourceState != nil { // An address from the state must match exactly, since we must ensure // we refresh/destroy a resource with the same provider configuration // that created it. - return addr, true + return n.ResourceState.ProviderConfig, true } -Guess: // Use our type and containing module path to guess a provider configuration address return n.Addr.Resource.DefaultProviderConfig().Absolute(n.Path()), false } @@ -399,7 +389,7 @@ func (n *NodeAbstractResource) SetTargets(targets []addrs.Targetable) { } // GraphNodeAttachResourceState -func (n *NodeAbstractResourceInstance) AttachResourceState(s *ResourceState) { +func (n *NodeAbstractResourceInstance) AttachResourceState(s *states.Resource) { n.ResourceState = s } diff --git a/terraform/node_resource_apply.go b/terraform/node_resource_apply.go index 52c9af40b..3375c96b0 100644 --- a/terraform/node_resource_apply.go +++ b/terraform/node_resource_apply.go @@ -3,6 +3,10 @@ package terraform import ( "fmt" + "github.com/hashicorp/terraform/states" + + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" "github.com/zclconf/go-cty/cty" @@ -118,29 +122,24 @@ func (n *NodeApplyableResourceInstance) EvalTree() EvalNode { func (n *NodeApplyableResourceInstance) evalTreeDataResource(addr addrs.AbsResourceInstance, stateId string, stateDeps []string) EvalNode { var provider ResourceProvider var providerSchema *ProviderSchema - var diff *InstanceDiff - var state *InstanceState + var change *plans.ResourceInstanceChange + var state *states.ResourceInstanceObject var configVal cty.Value return &EvalSequence{ Nodes: []EvalNode{ // Get the saved diff for apply &EvalReadDiff{ - Name: stateId, - Diff: &diff, + Addr: addr.Resource, + Change: &change, }, // Stop early if we don't actually have a diff &EvalIf{ If: func(ctx EvalContext) (bool, error) { - if diff == nil { + if change == nil { return true, EvalEarlyExitError{} } - - if diff.GetAttributesLen() == 0 { - return true, EvalEarlyExitError{} - } - return true, nil }, Then: EvalNoop{}, @@ -159,31 +158,30 @@ func (n *NodeApplyableResourceInstance) evalTreeDataResource(addr addrs.AbsResou Config: n.Config, Provider: &provider, ProviderSchema: &providerSchema, - Output: &diff, + Output: &change, OutputValue: &configVal, OutputState: &state, }, &EvalReadDataApply{ Addr: addr.Resource, - Diff: &diff, + Change: &change, Provider: &provider, Output: &state, }, &EvalWriteState{ - Name: stateId, - ResourceType: n.Config.Type, - Provider: n.ResolvedProvider, - Dependencies: stateDeps, - State: &state, + Addr: addr.Resource, + ProviderAddr: n.ResolvedProvider, + ProviderSchema: &providerSchema, + State: &state, }, // Clear the diff now that we've applied it, so // later nodes won't see a diff that's now a no-op. &EvalWriteDiff{ - Name: stateId, - Diff: nil, + Addr: addr.Resource, + Change: nil, }, &EvalUpdateStateHook{}, @@ -196,19 +194,20 @@ func (n *NodeApplyableResourceInstance) evalTreeManagedResource(addr addrs.AbsRe // evaluation. Most of this are written to by-address below. var provider ResourceProvider var providerSchema *ProviderSchema - var diff, diffApply *InstanceDiff - var state *InstanceState + var diff, diffApply *plans.ResourceInstanceChange + var state *states.ResourceInstanceObject var err error var createNew bool var createBeforeDestroyEnabled bool var configVal cty.Value + var deposedKey states.DeposedKey return &EvalSequence{ Nodes: []EvalNode{ // Get the saved diff for apply &EvalReadDiff{ - Name: stateId, - Diff: &diffApply, + Addr: addr.Resource, + Change: &diffApply, }, // We don't want to do any destroys @@ -218,12 +217,9 @@ func (n *NodeApplyableResourceInstance) evalTreeManagedResource(addr addrs.AbsRe if diffApply == nil { return true, EvalEarlyExitError{} } - - if diffApply.GetDestroy() && diffApply.GetAttributesLen() == 0 { + if diffApply.Action == plans.Delete { return true, EvalEarlyExitError{} } - - diffApply.SetDestroy(false) return true, nil }, Then: EvalNoop{}, @@ -233,17 +229,16 @@ func (n *NodeApplyableResourceInstance) evalTreeManagedResource(addr addrs.AbsRe If: func(ctx EvalContext) (bool, error) { destroy := false if diffApply != nil { - destroy = diffApply.GetDestroy() || diffApply.RequiresNew() + destroy = (diffApply.Action == plans.Delete || diffApply.Action == plans.Replace) } - if destroy && n.createBeforeDestroy() { createBeforeDestroyEnabled = true } - return createBeforeDestroyEnabled, nil }, Then: &EvalDeposeState{ - Name: stateId, + Addr: addr.Resource, + OutputKey: &deposedKey, }, }, @@ -253,7 +248,10 @@ func (n *NodeApplyableResourceInstance) evalTreeManagedResource(addr addrs.AbsRe Schema: &providerSchema, }, &EvalReadState{ - Name: stateId, + Addr: addr.Resource, + Provider: &provider, + ProviderSchema: &providerSchema, + Output: &state, }, @@ -265,15 +263,15 @@ func (n *NodeApplyableResourceInstance) evalTreeManagedResource(addr addrs.AbsRe Provider: &provider, ProviderSchema: &providerSchema, State: &state, - OutputDiff: &diffApply, + OutputChange: &diffApply, OutputValue: &configVal, OutputState: &state, }, // Get the saved diff &EvalReadDiff{ - Name: stateId, - Diff: &diff, + Addr: addr.Resource, + Change: &diff, }, // Compare the diffs @@ -289,31 +287,33 @@ func (n *NodeApplyableResourceInstance) evalTreeManagedResource(addr addrs.AbsRe Schema: &providerSchema, }, &EvalReadState{ - Name: stateId, + Addr: addr.Resource, + Provider: &provider, + ProviderSchema: &providerSchema, + Output: &state, }, // Call pre-apply hook &EvalApplyPre{ - Addr: addr.Resource, - State: &state, - Diff: &diffApply, + Addr: addr.Resource, + State: &state, + Change: &diffApply, }, &EvalApply{ Addr: addr.Resource, State: &state, - Diff: &diffApply, + Change: &diffApply, Provider: &provider, Output: &state, Error: &err, CreateNew: &createNew, }, &EvalWriteState{ - Name: stateId, - ResourceType: n.Config.Type, - Provider: n.ResolvedProvider, - Dependencies: stateDeps, - State: &state, + Addr: addr.Resource, + ProviderAddr: n.ResolvedProvider, + ProviderSchema: &providerSchema, + State: &state, }, &EvalApplyProvisioners{ Addr: addr.Resource, @@ -323,20 +323,19 @@ func (n *NodeApplyableResourceInstance) evalTreeManagedResource(addr addrs.AbsRe Error: &err, When: configs.ProvisionerWhenCreate, }, + &EvalWriteState{ + Addr: addr.Resource, + ProviderAddr: n.ResolvedProvider, + ProviderSchema: &providerSchema, + State: &state, + }, &EvalIf{ If: func(ctx EvalContext) (bool, error) { return createBeforeDestroyEnabled && err != nil, nil }, Then: &EvalUndeposeState{ - Name: stateId, - State: &state, - }, - Else: &EvalWriteState{ - Name: stateId, - ResourceType: n.Config.Type, - Provider: n.ResolvedProvider, - Dependencies: stateDeps, - State: &state, + Addr: addr.Resource, + Key: &deposedKey, }, }, @@ -344,8 +343,8 @@ func (n *NodeApplyableResourceInstance) evalTreeManagedResource(addr addrs.AbsRe // don't see a diff that is already complete. There // is no longer a diff! &EvalWriteDiff{ - Name: stateId, - Diff: nil, + Addr: addr.Resource, + Change: nil, }, &EvalApplyPost{ diff --git a/terraform/node_resource_destroy.go b/terraform/node_resource_destroy.go index 2c0af9013..cee16c567 100644 --- a/terraform/node_resource_destroy.go +++ b/terraform/node_resource_destroy.go @@ -1,14 +1,25 @@ package terraform import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/states" ) // NodeDestroyResource represents a resource that is to be destroyed. type NodeDestroyResourceInstance struct { *NodeAbstractResourceInstance + // If DeposedKey is set to anything other than states.NotDeposed then + // this node destroys a deposed object of the associated instance + // rather than its current object. + DeposedKey states.DeposedKey + CreateBeforeDestroyOverride *bool } @@ -105,12 +116,14 @@ func (n *NodeDestroyResourceInstance) References() []*addrs.Reference { // GraphNodeDynamicExpandable func (n *NodeDestroyResourceInstance) DynamicExpand(ctx EvalContext) (*Graph, error) { - // stateId is the legacy-style ID to put into the state - stateId := NewLegacyResourceInstanceAddress(n.ResourceInstanceAddr()).stateId() + if n.DeposedKey != states.NotDeposed { + return nil, fmt.Errorf("NodeDestroyResourceInstance not yet updated to deal with explicit DeposedKey") + } - state, lock := ctx.State() - lock.RLock() - defer lock.RUnlock() + // Our graph transformers require direct access to read the entire state + // structure, so we'll lock the whole state for the duration of this work. + state := ctx.State().Lock() + defer ctx.State().Unlock() // Start creating the steps steps := make([]GraphTransformer, 0, 5) @@ -118,7 +131,7 @@ func (n *NodeDestroyResourceInstance) DynamicExpand(ctx EvalContext) (*Graph, er // We want deposed resources in the state to be destroyed steps = append(steps, &DeposedTransformer{ State: state, - View: stateId, + InstanceAddr: n.ResourceInstanceAddr(), ResolvedProvider: n.ResolvedProvider, }) @@ -143,20 +156,20 @@ func (n *NodeDestroyResourceInstance) DynamicExpand(ctx EvalContext) (*Graph, er func (n *NodeDestroyResourceInstance) EvalTree() EvalNode { addr := n.ResourceInstanceAddr() - // stateId is the legacy-style ID to put into the state - stateId := NewLegacyResourceInstanceAddress(n.ResourceInstanceAddr()).stateId() - // Get our state rs := n.ResourceState - if rs == nil { - rs = &ResourceState{ - Provider: n.ResolvedProvider.String(), - } + var is *states.ResourceInstance + if rs != nil { + is = rs.Instance(n.InstanceKey) + } + if is == nil { + log.Printf("[WARN] NodeDestroyResourceInstance for %s with no state", addr) } - var diffApply *InstanceDiff + var changeApply *plans.ResourceInstanceChange var provider ResourceProvider - var state *InstanceState + var providerSchema *ProviderSchema + var state *states.ResourceInstanceObject var err error return &EvalOpFilter{ Ops: []walkOperation{walkApply, walkDestroy}, @@ -164,22 +177,17 @@ func (n *NodeDestroyResourceInstance) EvalTree() EvalNode { Nodes: []EvalNode{ // Get the saved diff for apply &EvalReadDiff{ - Name: stateId, - Diff: &diffApply, - }, - - // Filter the diff so we only get the destroy - &EvalFilterDiff{ - Diff: &diffApply, - Output: &diffApply, - Destroy: true, + Addr: addr.Resource, + Change: &changeApply, }, // If we're not destroying, then compare diffs &EvalIf{ If: func(ctx EvalContext) (bool, error) { - if diffApply != nil && diffApply.GetDestroy() { - return true, nil + if changeApply != nil { + if changeApply.Action == plans.Delete || changeApply.Action == plans.Replace { + return true, nil + } } return true, EvalEarlyExitError{} @@ -190,10 +198,13 @@ func (n *NodeDestroyResourceInstance) EvalTree() EvalNode { &EvalGetProvider{ Addr: n.ResolvedProvider, Output: &provider, + Schema: &providerSchema, }, &EvalReadState{ - Name: stateId, - Output: &state, + Addr: addr.Resource, + Output: &state, + Provider: &provider, + ProviderSchema: &providerSchema, }, &EvalRequireState{ State: &state, @@ -201,15 +212,15 @@ func (n *NodeDestroyResourceInstance) EvalTree() EvalNode { // Call pre-apply hook &EvalApplyPre{ - Addr: addr.Resource, - State: &state, - Diff: &diffApply, + Addr: addr.Resource, + State: &state, + Change: &changeApply, }, // Run destroy provisioners if not tainted &EvalIf{ If: func(ctx EvalContext) (bool, error) { - if state != nil && state.Tainted { + if state != nil && state.Status == states.ObjectTainted { return false, nil } @@ -247,25 +258,24 @@ func (n *NodeDestroyResourceInstance) EvalTree() EvalNode { Then: &EvalReadDataApply{ Addr: addr.Resource, - Diff: &diffApply, + Change: &changeApply, Provider: &provider, Output: &state, }, Else: &EvalApply{ Addr: addr.Resource, State: &state, - Diff: &diffApply, + Change: &changeApply, Provider: &provider, Output: &state, Error: &err, }, }, &EvalWriteState{ - Name: stateId, - ResourceType: addr.Resource.Resource.Type, - Provider: n.ResolvedProvider, - Dependencies: rs.Dependencies, - State: &state, + Addr: addr.Resource, + ProviderAddr: n.ResolvedProvider, + ProviderSchema: &providerSchema, + State: &state, }, &EvalApplyPost{ Addr: addr.Resource, diff --git a/terraform/node_resource_destroy_test.go b/terraform/node_resource_destroy_test.go index aabe9d58a..25b363d62 100644 --- a/terraform/node_resource_destroy_test.go +++ b/terraform/node_resource_destroy_test.go @@ -2,15 +2,13 @@ package terraform import ( "strings" - "sync" "testing" "github.com/hashicorp/terraform/addrs" ) func TestNodeDestroyResourceDynamicExpand_deposedCount(t *testing.T) { - var stateLock sync.RWMutex - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -36,7 +34,7 @@ func TestNodeDestroyResourceDynamicExpand_deposedCount(t *testing.T) { }, }, }, - } + }) m := testModule(t, "apply-cbd-count") n := &NodeDestroyResourceInstance{ @@ -48,14 +46,13 @@ func TestNodeDestroyResourceDynamicExpand_deposedCount(t *testing.T) { Config: m.Module.ManagedResources["aws_instance.bar"], }, InstanceKey: addrs.IntKey(0), - ResourceState: state.Modules[0].Resources["aws_instance.bar.0"], + ResourceState: state.Modules[""].Resources["aws_instance.bar[0]"], }, } g, err := n.DynamicExpand(&MockEvalContext{ PathPath: addrs.RootModuleInstance, - StateState: state, - StateLock: &stateLock, + StateState: state.SyncWrapper(), }) if err != nil { t.Fatalf("err: %s", err) @@ -63,7 +60,7 @@ func TestNodeDestroyResourceDynamicExpand_deposedCount(t *testing.T) { got := strings.TrimSpace(g.String()) want := strings.TrimSpace(` -aws_instance.bar[0] (deposed #0) +aws_instance.bar[0] (deposed 00000001) `) if got != want { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", got, want) diff --git a/terraform/node_resource_plan.go b/terraform/node_resource_plan.go index 2087944a6..ba9364734 100644 --- a/terraform/node_resource_plan.go +++ b/terraform/node_resource_plan.go @@ -34,10 +34,10 @@ func (n *NodePlannableResource) DynamicExpand(ctx EvalContext) (*Graph, error) { // if we're transitioning whether "count" is set at all. fixResourceCountSetTransition(ctx, n.ResourceAddr().Resource, count != -1) - // Grab the state which we read - state, lock := ctx.State() - lock.RLock() - defer lock.RUnlock() + // Our graph transformers require access to the full state, so we'll + // temporarily lock it while we work on this. + state := ctx.State().Lock() + defer ctx.State().Unlock() // The concrete resource factory we'll use concreteResource := func(a *NodeAbstractResourceInstance) dag.Vertex { diff --git a/terraform/node_resource_plan_destroy.go b/terraform/node_resource_plan_destroy.go index 66ffe04f0..fa87861e3 100644 --- a/terraform/node_resource_plan_destroy.go +++ b/terraform/node_resource_plan_destroy.go @@ -1,7 +1,12 @@ package terraform import ( + "fmt" + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" ) // NodePlanDestroyableResourceInstance represents a resource that is ready @@ -20,6 +25,7 @@ var ( _ GraphNodeAttachResourceConfig = (*NodePlanDestroyableResourceInstance)(nil) _ GraphNodeAttachResourceState = (*NodePlanDestroyableResourceInstance)(nil) _ GraphNodeEvalable = (*NodePlanDestroyableResourceInstance)(nil) + _ GraphNodeProviderConsumer = (*NodePlanDestroyableResourceInstance)(nil) ) // GraphNodeDestroyer @@ -32,34 +38,47 @@ func (n *NodePlanDestroyableResourceInstance) DestroyAddr() *addrs.AbsResourceIn func (n *NodePlanDestroyableResourceInstance) EvalTree() EvalNode { addr := n.ResourceInstanceAddr() - // State still uses legacy-style internal ids, so we need to shim to get - // a suitable key to use. - stateId := NewLegacyResourceInstanceAddress(addr).stateId() - // Declare a bunch of variables that are used for state during - // evaluation. Most of this are written to by-address below. - var diff *InstanceDiff - var state *InstanceState + // evaluation. These are written to by address in the EvalNodes we + // declare below. + var provider ResourceProvider + var providerSchema *ProviderSchema + var change *plans.ResourceInstanceChange + var state *states.ResourceInstanceObject + + if n.ResolvedProvider.ProviderConfig.Type == "" { + // Should never happen; indicates that the graph was not constructed + // correctly since we didn't get our provider attached. + panic(fmt.Sprintf("%T %q was not assigned a resolved provider", n, dag.VertexName(n))) + } return &EvalSequence{ Nodes: []EvalNode{ + &EvalGetProvider{ + Addr: n.ResolvedProvider, + Output: &provider, + Schema: &providerSchema, + }, &EvalReadState{ - Name: stateId, + Addr: addr.Resource, + Provider: &provider, + ProviderSchema: &providerSchema, + Output: &state, }, &EvalDiffDestroy{ Addr: addr.Resource, State: &state, - Output: &diff, + Output: &change, }, &EvalCheckPreventDestroy{ Addr: addr.Resource, Config: n.Config, - Diff: &diff, + Change: &change, }, &EvalWriteDiff{ - Name: stateId, - Diff: &diff, + Addr: addr.Resource, + Change: &change, }, }, } diff --git a/terraform/node_resource_plan_instance.go b/terraform/node_resource_plan_instance.go index b3c3d66fa..e7c95c72c 100644 --- a/terraform/node_resource_plan_instance.go +++ b/terraform/node_resource_plan_instance.go @@ -3,6 +3,9 @@ package terraform import ( "fmt" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/addrs" "github.com/zclconf/go-cty/cty" ) @@ -51,23 +54,26 @@ func (n *NodePlannableResourceInstance) evalTreeDataResource(addr addrs.AbsResou config := n.Config var provider ResourceProvider var providerSchema *ProviderSchema - var diff *InstanceDiff - var state *InstanceState + var change *plans.ResourceInstanceChange + var state *states.ResourceInstanceObject var configVal cty.Value return &EvalSequence{ Nodes: []EvalNode{ - &EvalReadState{ - Name: stateId, - Output: &state, - }, - &EvalGetProvider{ Addr: n.ResolvedProvider, Output: &provider, Schema: &providerSchema, }, + &EvalReadState{ + Addr: addr.Resource, + Provider: &provider, + ProviderSchema: &providerSchema, + + Output: &state, + }, + &EvalValidateSelfRef{ Addr: addr.Resource, Config: config.Config, @@ -79,22 +85,21 @@ func (n *NodePlannableResourceInstance) evalTreeDataResource(addr addrs.AbsResou Config: n.Config, Provider: &provider, ProviderSchema: &providerSchema, - Output: &diff, + Output: &change, OutputValue: &configVal, OutputState: &state, }, &EvalWriteState{ - Name: stateId, - ResourceType: n.Config.Type, - Provider: n.ResolvedProvider, - Dependencies: stateDeps, - State: &state, + Addr: addr.Resource, + ProviderAddr: n.ResolvedProvider, + ProviderSchema: &providerSchema, + State: &state, }, &EvalWriteDiff{ - Name: stateId, - Diff: &diff, + Addr: addr.Resource, + Change: &change, }, }, } @@ -104,22 +109,25 @@ func (n *NodePlannableResourceInstance) evalTreeManagedResource(addr addrs.AbsRe config := n.Config var provider ResourceProvider var providerSchema *ProviderSchema - var diff *InstanceDiff - var state *InstanceState + var change *plans.ResourceInstanceChange + var state *states.ResourceInstanceObject return &EvalSequence{ Nodes: []EvalNode{ - &EvalReadState{ - Name: stateId, - Output: &state, - }, - &EvalGetProvider{ Addr: n.ResolvedProvider, Output: &provider, Schema: &providerSchema, }, + &EvalReadState{ + Addr: addr.Resource, + Provider: &provider, + ProviderSchema: &providerSchema, + + Output: &state, + }, + &EvalValidateSelfRef{ Addr: addr.Resource, Config: config.Config, @@ -132,24 +140,23 @@ func (n *NodePlannableResourceInstance) evalTreeManagedResource(addr addrs.AbsRe Provider: &provider, ProviderSchema: &providerSchema, State: &state, - OutputDiff: &diff, + OutputChange: &change, OutputState: &state, }, &EvalCheckPreventDestroy{ Addr: addr.Resource, Config: n.Config, - Diff: &diff, + Change: &change, }, &EvalWriteState{ - Name: stateId, - ResourceType: n.Config.Type, - Provider: n.ResolvedProvider, - Dependencies: stateDeps, - State: &state, + Addr: addr.Resource, + ProviderAddr: n.ResolvedProvider, + State: &state, + ProviderSchema: &providerSchema, }, &EvalWriteDiff{ - Name: stateId, - Diff: &diff, + Addr: addr.Resource, + Change: &change, }, }, } diff --git a/terraform/node_resource_plan_orphan.go b/terraform/node_resource_plan_orphan.go index 09aec8c82..4380a42a2 100644 --- a/terraform/node_resource_plan_orphan.go +++ b/terraform/node_resource_plan_orphan.go @@ -1,5 +1,10 @@ package terraform +import ( + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" +) + // NodePlannableResourceInstanceOrphan represents a resource that is "applyable": // it is ready to be applied and is represented by a diff. type NodePlannableResourceInstanceOrphan struct { @@ -29,41 +34,47 @@ func (n *NodePlannableResourceInstanceOrphan) Name() string { func (n *NodePlannableResourceInstanceOrphan) EvalTree() EvalNode { addr := n.ResourceInstanceAddr() - // State still uses legacy-style internal ids, so we need to shim to get - // a suitable key to use. - stateId := NewLegacyResourceInstanceAddress(addr).stateId() - // Declare a bunch of variables that are used for state during // evaluation. Most of this are written to by-address below. - var diff *InstanceDiff - var state *InstanceState + var change *plans.ResourceInstanceChange + var state *states.ResourceInstanceObject + var provider ResourceProvider + var providerSchema *ProviderSchema return &EvalSequence{ Nodes: []EvalNode{ + &EvalGetProvider{ + Addr: n.ResolvedProvider, + Output: &provider, + Schema: &providerSchema, + }, &EvalReadState{ - Name: stateId, + Addr: addr.Resource, + Provider: &provider, + ProviderSchema: &providerSchema, + Output: &state, }, &EvalDiffDestroy{ Addr: addr.Resource, State: &state, - Output: &diff, + Output: &change, OutputState: &state, // Will point to a nil state after this complete, signalling destroyed }, &EvalCheckPreventDestroy{ Addr: addr.Resource, Config: n.Config, - Diff: &diff, + Change: &change, }, &EvalWriteDiff{ - Name: stateId, - Diff: &diff, + Addr: addr.Resource, + Change: &change, }, &EvalWriteState{ - Name: stateId, - ResourceType: addr.Resource.Resource.Type, - Provider: n.ResolvedProvider, - State: &state, + Addr: addr.Resource, + ProviderAddr: n.ResolvedProvider, + ProviderSchema: &providerSchema, + State: &state, }, }, } diff --git a/terraform/node_resource_refresh.go b/terraform/node_resource_refresh.go index 3337867f8..36b0d966f 100644 --- a/terraform/node_resource_refresh.go +++ b/terraform/node_resource_refresh.go @@ -3,6 +3,10 @@ package terraform import ( "fmt" + "github.com/hashicorp/terraform/plans" + + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/dag" "github.com/hashicorp/terraform/tfdiags" @@ -37,10 +41,10 @@ func (n *NodeRefreshableManagedResource) DynamicExpand(ctx EvalContext) (*Graph, // if we're transitioning whether "count" is set at all. fixResourceCountSetTransition(ctx, n.ResourceAddr().Resource, count != -1) - // Grab the state which we read - state, lock := ctx.State() - lock.RLock() - defer lock.RUnlock() + // Our graph transformers require access to the full state, so we'll + // temporarily lock it while we work on this. + state := ctx.State().Lock() + defer ctx.State().Unlock() // The concrete resource factory we'll use concreteResource := func(a *NodeAbstractResourceInstance) dag.Vertex { @@ -155,14 +159,11 @@ func (n *NodeRefreshableManagedResourceInstance) EvalTree() EvalNode { func (n *NodeRefreshableManagedResourceInstance) evalTreeManagedResource() EvalNode { addr := n.ResourceInstanceAddr() - // State still uses legacy-style internal ids, so we need to shim to get - // a suitable key to use. - stateId := NewLegacyResourceInstanceAddress(addr).stateId() - // Declare a bunch of variables that are used for state during // evaluation. Most of this are written to by-address below. var provider ResourceProvider - var state *InstanceState + var providerSchema *ProviderSchema + var state *states.ResourceInstanceObject // This happened during initial development. All known cases were // fixed and tested but as a sanity check let's assert here. @@ -177,14 +178,18 @@ func (n *NodeRefreshableManagedResourceInstance) evalTreeManagedResource() EvalN return &EvalSequence{ Nodes: []EvalNode{ - &EvalReadState{ - Name: stateId, - Output: &state, - }, - &EvalGetProvider{ Addr: n.ResolvedProvider, Output: &provider, + Schema: &providerSchema, + }, + + &EvalReadState{ + Addr: addr.Resource, + Provider: &provider, + ProviderSchema: &providerSchema, + + Output: &state, }, &EvalRefresh{ @@ -195,11 +200,10 @@ func (n *NodeRefreshableManagedResourceInstance) evalTreeManagedResource() EvalN }, &EvalWriteState{ - Name: stateId, - ResourceType: n.ResourceState.Type, - Provider: n.ResolvedProvider, - Dependencies: n.ResourceState.Dependencies, - State: &state, + Addr: addr.Resource, + ProviderAddr: n.ResolvedProvider, + ProviderSchema: &providerSchema, + State: &state, }, }, } @@ -223,46 +227,45 @@ func (n *NodeRefreshableManagedResourceInstance) evalTreeManagedResourceNoState( // evaluation. Most of this are written to by-address below. var provider ResourceProvider var providerSchema *ProviderSchema - var diff *InstanceDiff - var state *InstanceState - - // State still uses legacy-style internal ids, so we need to shim to get - // a suitable key to use. - stateID := NewLegacyResourceInstanceAddress(addr).stateId() + var change *plans.ResourceInstanceChange + var state *states.ResourceInstanceObject // Determine the dependencies for the state. - stateDeps := n.StateReferences() + // TODO: Update StateReferences to return []addrs.Referenceable + //state.Dependencies = n.StateReferences() return &EvalSequence{ Nodes: []EvalNode{ - &EvalReadState{ - Name: stateID, - Output: &state, - }, - &EvalGetProvider{ Addr: n.ResolvedProvider, Output: &provider, Schema: &providerSchema, }, + &EvalReadState{ + Addr: addr.Resource, + Provider: &provider, + ProviderSchema: &providerSchema, + + Output: &state, + }, + &EvalDiff{ Addr: addr.Resource, Config: n.Config, Provider: &provider, ProviderSchema: &providerSchema, State: &state, - OutputDiff: &diff, + OutputChange: &change, OutputState: &state, Stub: true, }, &EvalWriteState{ - Name: stateID, - ResourceType: n.Config.Type, - Provider: n.ResolvedProvider, - Dependencies: stateDeps, - State: &state, + Addr: addr.Resource, + ProviderAddr: n.ResolvedProvider, + ProviderSchema: &providerSchema, + State: &state, }, }, } diff --git a/terraform/node_resource_refresh_test.go b/terraform/node_resource_refresh_test.go index d1109d02c..006eed848 100644 --- a/terraform/node_resource_refresh_test.go +++ b/terraform/node_resource_refresh_test.go @@ -2,22 +2,18 @@ package terraform import ( "reflect" - "sync" "testing" + "github.com/davecgh/go-spew/spew" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/addrs" - - "github.com/davecgh/go-spew/spew" ) func TestNodeRefreshableManagedResourceDynamicExpand_scaleOut(t *testing.T) { - var stateLock sync.RWMutex - m := testModule(t, "refresh-resource-scale-inout") - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -41,7 +37,7 @@ func TestNodeRefreshableManagedResourceDynamicExpand_scaleOut(t *testing.T) { }, }, }, - } + }).SyncWrapper() n := &NodeRefreshableManagedResource{ NodeAbstractResource: &NodeAbstractResource{ @@ -55,7 +51,6 @@ func TestNodeRefreshableManagedResourceDynamicExpand_scaleOut(t *testing.T) { g, err := n.DynamicExpand(&MockEvalContext{ PathPath: addrs.RootModuleInstance, StateState: state, - StateLock: &stateLock, // DynamicExpand will call EvaluateExpr to evaluate the "count" // expression, which is just a literal number 3 in the fixture config @@ -81,11 +76,9 @@ root - terraform.graphNodeRoot } func TestNodeRefreshableManagedResourceDynamicExpand_scaleIn(t *testing.T) { - var stateLock sync.RWMutex - m := testModule(t, "refresh-resource-scale-inout") - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, @@ -125,7 +118,7 @@ func TestNodeRefreshableManagedResourceDynamicExpand_scaleIn(t *testing.T) { }, }, }, - } + }).SyncWrapper() n := &NodeRefreshableManagedResource{ NodeAbstractResource: &NodeAbstractResource{ @@ -139,7 +132,6 @@ func TestNodeRefreshableManagedResourceDynamicExpand_scaleIn(t *testing.T) { g, err := n.DynamicExpand(&MockEvalContext{ PathPath: addrs.RootModuleInstance, StateState: state, - StateLock: &stateLock, // DynamicExpand will call EvaluateExpr to evaluate the "count" // expression, which is just a literal number 3 in the fixture config diff --git a/terraform/plan.go b/terraform/plan.go index fb3ef6195..af04c6cd4 100644 --- a/terraform/plan.go +++ b/terraform/plan.go @@ -3,20 +3,13 @@ package terraform import ( "bytes" "encoding/gob" - "errors" "fmt" "io" - "log" "sync" - "github.com/hashicorp/hcl2/hcl" - "github.com/hashicorp/hcl2/hcl/hclsyntax" "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" - "github.com/hashicorp/terraform/tfdiags" - "github.com/hashicorp/terraform/version" ) func init() { @@ -84,88 +77,6 @@ type Plan struct { once sync.Once } -// Context returns a Context with the data encapsulated in this plan. -// -// The following fields in opts are overridden by the plan: Config, -// Diff, Variables. -// -// If State is not provided, it is set from the plan. If it _is_ provided, -// it must be Equal to the state stored in plan, but may have a newer -// serial. -func (p *Plan) Context(opts *ContextOpts) (*Context, tfdiags.Diagnostics) { - var err error - opts, err = p.contextOpts(opts) - if err != nil { - var diags tfdiags.Diagnostics - diags = diags.Append(err) - return nil, diags - } - return NewContext(opts) -} - -// contextOpts mutates the given base ContextOpts in place to use input -// objects obtained from the receiving plan. -func (p *Plan) contextOpts(base *ContextOpts) (*ContextOpts, error) { - opts := base - - opts.Diff = p.Diff - opts.Config = p.Config - opts.ProviderSHA256s = p.ProviderSHA256s - opts.Destroy = p.Destroy - - if len(p.Targets) != 0 { - // We're still using target strings in the Plan struct, so we need to - // convert to our address representation here. - // FIXME: Change the Plan struct to use addrs.Targetable itself, and - // then handle these conversions when we read/write plans on disk. - targets := make([]addrs.Targetable, len(p.Targets)) - for i, targetStr := range p.Targets { - traversal, travDiags := hclsyntax.ParseTraversalAbs([]byte(targetStr), "", hcl.Pos{}) - if travDiags.HasErrors() { - return nil, travDiags - } - target, targDiags := addrs.ParseTarget(traversal) - if targDiags.HasErrors() { - return nil, targDiags.Err() - } - targets[i] = target.Subject - } - opts.Targets = targets - } - - if opts.State == nil { - opts.State = p.State - } else if !opts.State.Equal(p.State) { - // Even if we're overriding the state, it should be logically equal - // to what's in plan. The only valid change to have made by the time - // we get here is to have incremented the serial. - // - // Due to the fact that serialization may change the representation of - // the state, there is little chance that these aren't actually equal. - // Log the error condition for reference, but continue with the state - // we have. - log.Println("[WARN] Plan state and ContextOpts state are not equal") - } - - thisVersion := version.String() - if p.TerraformVersion != "" && p.TerraformVersion != thisVersion { - return nil, fmt.Errorf( - "plan was created with a different version of Terraform (created with %s, but running %s)", - p.TerraformVersion, thisVersion, - ) - } - - opts.Variables = make(InputValues) - for k, v := range p.Vars { - opts.Variables[k] = &InputValue{ - Value: v, - SourceType: ValueFromPlan, - } - } - - return opts, nil -} - func (p *Plan) String() string { buf := new(bytes.Buffer) buf.WriteString("DIFF:\n\n") @@ -202,65 +113,10 @@ const planFormatVersion byte = 2 // ReadPlan reads a plan structure out of a reader in the format that // was written by WritePlan. func ReadPlan(src io.Reader) (*Plan, error) { - var result *Plan - var err error - n := 0 - - // Verify the magic bytes - magic := make([]byte, len(planFormatMagic)) - for n < len(magic) { - n, err = src.Read(magic[n:]) - if err != nil { - return nil, fmt.Errorf("error while reading magic bytes: %s", err) - } - } - if string(magic) != planFormatMagic { - return nil, fmt.Errorf("not a valid plan file") - } - - // Verify the version is something we can read - var formatByte [1]byte - n, err = src.Read(formatByte[:]) - if err != nil { - return nil, err - } - if n != len(formatByte) { - return nil, errors.New("failed to read plan version byte") - } - - if formatByte[0] != planFormatVersion { - return nil, fmt.Errorf("unknown plan file version: %d", formatByte[0]) - } - - dec := gob.NewDecoder(src) - if err := dec.Decode(&result); err != nil { - return nil, err - } - - return result, nil + return nil, fmt.Errorf("terraform.ReadPlan is no longer in use; use planfile.Open instead") } // WritePlan writes a plan somewhere in a binary format. func WritePlan(d *Plan, dst io.Writer) error { - return fmt.Errorf("plan serialization is temporarily disabled, pending implementation of the new file format") - - // Write the magic bytes so we can determine the file format later - n, err := dst.Write([]byte(planFormatMagic)) - if err != nil { - return err - } - if n != len(planFormatMagic) { - return errors.New("failed to write plan format magic bytes") - } - - // Write a version byte so we can iterate on version at some point - n, err = dst.Write([]byte{planFormatVersion}) - if err != nil { - return err - } - if n != 1 { - return errors.New("failed to write plan version byte") - } - - return gob.NewEncoder(dst).Encode(d) + return fmt.Errorf("terraform.WritePlan is no longer in use; use planfile.Create instead") } diff --git a/terraform/plan_test.go b/terraform/plan_test.go deleted file mode 100644 index d0c1c38a8..000000000 --- a/terraform/plan_test.go +++ /dev/null @@ -1,186 +0,0 @@ -package terraform - -import ( - "bytes" - "reflect" - "strings" - "testing" - - "github.com/hashicorp/terraform/addrs" - - "github.com/hashicorp/terraform/configs" - "github.com/zclconf/go-cty/cty" -) - -func TestPlanContextOpts(t *testing.T) { - plan := &Plan{ - Diff: &Diff{ - Modules: []*ModuleDiff{ - { - Path: []string{"test"}, - }, - }, - }, - Config: configs.NewEmptyConfig(), - State: &State{ - TFVersion: "sigil", - }, - Vars: map[string]cty.Value{"foo": cty.StringVal("bar")}, - Targets: []string{"baz.bar"}, - - TerraformVersion: VersionString(), - ProviderSHA256s: map[string][]byte{ - "test": []byte("placeholder"), - }, - } - - got, err := plan.contextOpts(&ContextOpts{}) - if err != nil { - t.Fatalf("error creating context: %s", err) - } - - want := &ContextOpts{ - Diff: plan.Diff, - Config: plan.Config, - State: plan.State, - Variables: InputValues{ - "foo": &InputValue{ - Value: cty.StringVal("bar"), - SourceType: ValueFromPlan, - }, - }, - Targets: []addrs.Targetable{ - addrs.RootModuleInstance.Resource(addrs.ManagedResourceMode, "baz", "bar"), - }, - ProviderSHA256s: plan.ProviderSHA256s, - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("wrong result\ngot: %#v\nwant %#v", got, want) - } -} - -func TestReadWritePlan(t *testing.T) { - plan := &Plan{ - Config: testModule(t, "new-good"), - Diff: &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: rootModulePath, - Resources: map[string]*InstanceDiff{ - "nodeA": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "foo", - New: "bar", - }, - "bar": &ResourceAttrDiff{ - Old: "foo", - NewComputed: true, - }, - "longfoo": &ResourceAttrDiff{ - Old: "foo", - New: "bar", - RequiresNew: true, - }, - }, - - Meta: map[string]interface{}{ - "foo": []interface{}{1, 2, 3}, - }, - }, - }, - }, - }, - }, - State: &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "foo": &ResourceState{ - Primary: &InstanceState{ - ID: "bar", - }, - }, - }, - }, - }, - }, - Vars: map[string]cty.Value{ - "foo": cty.StringVal("bar"), - }, - } - - buf := new(bytes.Buffer) - if err := WritePlan(plan, buf); err != nil { - t.Fatalf("err: %s", err) - } - - actual, err := ReadPlan(buf) - if err != nil { - t.Fatalf("err: %s", err) - } - - actualStr := strings.TrimSpace(actual.String()) - expectedStr := strings.TrimSpace(plan.String()) - if actualStr != expectedStr { - t.Fatalf("bad:\n\n%s\n\nexpected:\n\n%s", actualStr, expectedStr) - } -} - -func TestPlanContextOptsOverrideStateGood(t *testing.T) { - plan := &Plan{ - Diff: &Diff{ - Modules: []*ModuleDiff{ - { - Path: []string{"test"}, - }, - }, - }, - Config: configs.NewEmptyConfig(), - State: &State{ - TFVersion: "sigil", - Serial: 1, - }, - Vars: map[string]cty.Value{"foo": cty.StringVal("bar")}, - Targets: []string{"baz.bar"}, - - TerraformVersion: VersionString(), - ProviderSHA256s: map[string][]byte{ - "test": []byte("placeholder"), - }, - } - - base := &ContextOpts{ - State: &State{ - TFVersion: "sigil", - Serial: 2, - }, - } - - got, err := plan.contextOpts(base) - if err != nil { - t.Fatalf("error creating context: %s", err) - } - - want := &ContextOpts{ - Diff: plan.Diff, - Config: plan.Config, - State: base.State, - Variables: InputValues{ - "foo": &InputValue{ - Value: cty.StringVal("bar"), - SourceType: ValueFromPlan, - }, - }, - Targets: []addrs.Targetable{ - addrs.RootModuleInstance.Resource(addrs.ManagedResourceMode, "baz", "bar"), - }, - ProviderSHA256s: plan.ProviderSHA256s, - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("wrong result\ngot: %#v\nwant %#v", got, want) - } -} diff --git a/terraform/schemas.go b/terraform/schemas.go index 27ff440c0..b00f48062 100644 --- a/terraform/schemas.go +++ b/terraform/schemas.go @@ -4,9 +4,10 @@ import ( "fmt" "log" - "github.com/hashicorp/terraform/addrs" - "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/configs/configschema" + "github.com/hashicorp/terraform/providers" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" ) @@ -91,7 +92,7 @@ func (ss *Schemas) ProvisionerConfig(name string) *configschema.Block { // either misbehavior on the part of one of the providers or of the provider // protocol itself. When returned with errors, the returned schemas object is // still valid but may be incomplete. -func LoadSchemas(config *configs.Config, state *State, components contextComponentFactory) (*Schemas, error) { +func LoadSchemas(config *configs.Config, state *states.State, components contextComponentFactory) (*Schemas, error) { schemas := &Schemas{ providers: map[string]*ProviderSchema{}, provisioners: map[string]*configschema.Block{}, @@ -106,7 +107,7 @@ func LoadSchemas(config *configs.Config, state *State, components contextCompone return schemas, diags.Err() } -func loadProviderSchemas(schemas map[string]*ProviderSchema, config *configs.Config, state *State, components contextComponentFactory) tfdiags.Diagnostics { +func loadProviderSchemas(schemas map[string]*ProviderSchema, config *configs.Config, state *states.State, components contextComponentFactory) tfdiags.Diagnostics { var diags tfdiags.Diagnostics ensure := func(typeName string) { @@ -171,33 +172,9 @@ func loadProviderSchemas(schemas map[string]*ProviderSchema, config *configs.Con } if state != nil { - // TODO: After adapting this to use *states.State, use - // providers.AddressedTypes(state.ProviderAddrs()) to collect - // our list of required provider types. - for _, ms := range state.Modules { - for rsKey, rs := range ms.Resources { - providerAddrStr := rs.Provider - providerAddr, addrDiags := addrs.ParseAbsProviderConfigStr(providerAddrStr) - if addrDiags.HasErrors() { - // Should happen only if someone has tampered manually with - // the state, since we always write valid provider addrs. - moduleAddrStr := normalizeModulePath(ms.Path).String() - if moduleAddrStr == "" { - moduleAddrStr = "the root module" - } - // For now this is a warning, since there are many existing - // test fixtures that have invalid provider configurations. - // There's a check deeper in Terraform that makes this a - // failure when an empty/invalid provider string is present - // in practice. - log.Printf("[WARN] LoadSchemas: Resource %s in %s has invalid provider address %q in its state", rsKey, moduleAddrStr, providerAddrStr) - diags = diags.Append( - tfdiags.SimpleWarning(fmt.Sprintf("Resource %s in %s has invalid provider address %q in its state", rsKey, moduleAddrStr, providerAddrStr)), - ) - continue - } - ensure(providerAddr.ProviderConfig.Type) - } + needed := providers.AddressedTypesAbs(state.ProviderAddrs()) + for _, typeName := range needed { + ensure(typeName) } } diff --git a/terraform/state.go b/terraform/state.go index 7f74a9663..33064749e 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -16,6 +16,9 @@ import ( "strings" "sync" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-uuid" "github.com/hashicorp/go-version" @@ -23,8 +26,8 @@ import ( "github.com/hashicorp/hcl2/hcl/hclsyntax" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/tfdiags" tfversion "github.com/hashicorp/terraform/version" "github.com/mitchellh/copystructure" @@ -787,6 +790,9 @@ func (s *BackendState) Empty() bool { // given schema. func (s *BackendState) Config(schema *configschema.Block) (cty.Value, error) { ty := schema.ImpliedType() + if s == nil { + return cty.NullVal(ty), nil + } return ctyjson.Unmarshal(s.ConfigRaw, ty) } @@ -805,6 +811,24 @@ func (s *BackendState) SetConfig(val cty.Value, schema *configschema.Block) erro return nil } +// ForPlan produces an alternative representation of the reciever that is +// suitable for storing in a plan. The current workspace must additionally +// be provided, to be stored alongside the backend configuration. +// +// The backend configuration schema is required in order to properly +// encode the backend-specific configuration settings. +func (s *BackendState) ForPlan(schema *configschema.Block, workspaceName string) (*plans.Backend, error) { + if s == nil { + return nil, nil + } + + configVal, err := s.Config(schema) + if err != nil { + return nil, errwrap.Wrapf("failed to decode backend config: {{err}}", err) + } + return plans.NewBackend(s.Type, configVal, schema, workspaceName) +} + // RemoteState is used to track the information about a remote // state store that we push/pull state to. type RemoteState struct { @@ -2176,29 +2200,6 @@ func (s moduleStateSort) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -// CheckStateVersion returns error diagnostics if the state is not compatible -// with the current version of Terraform Core. -func CheckStateVersion(state *State, allowFuture bool) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - if state == nil { - return diags - } - - if state.FromFutureTerraform() && !allowFuture { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Incompatible Terraform state format", - fmt.Sprintf( - "For safety reasons, Terraform will not run operations against a state that was written by a future Terraform version. Your current version is %s, but the state requires at least %s. To proceed, upgrade Terraform to a suitable version.", - tfversion.String(), state.TFVersion, - ), - )) - } - - return diags -} - const stateValidateErrMultiModule = ` Multiple modules with the same path: %s diff --git a/terraform/state_upgrade_v1_to_v2_test.go b/terraform/state_upgrade_v1_to_v2_test.go deleted file mode 100644 index a660ae898..000000000 --- a/terraform/state_upgrade_v1_to_v2_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package terraform - -import ( - "os" - "path/filepath" - "testing" -) - -func TestReadStateV1ToV2_noPath(t *testing.T) { - f, err := os.Open(filepath.Join(fixtureDir, "state-upgrade", "v1-to-v2-empty-path.tfstate")) - if err != nil { - t.Fatalf("err: %s", err) - } - defer f.Close() - - s, err := ReadState(f) - if err != nil { - t.Fatalf("err: %s", err) - } - - checkStateString(t, s, "") -} diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 626c7c479..6fbc7eec9 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -2,7 +2,6 @@ package terraform import ( "flag" - "fmt" "io" "io/ioutil" "log" @@ -12,12 +11,16 @@ import ( "sync" "testing" - "github.com/hashicorp/terraform/configs/configload" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" - + "github.com/hashicorp/terraform/configs/configload" "github.com/hashicorp/terraform/helper/experiment" "github.com/hashicorp/terraform/helper/logging" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" ) // This is the directory where our test fixtures are. @@ -81,6 +84,12 @@ func tempEnv(t *testing.T, k string, v string) func() { func testModule(t *testing.T, name string) *configs.Config { t.Helper() + c, _ := testModuleWithSnapshot(t, name) + return c +} + +func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *configload.Snapshot) { + t.Helper() dir := filepath.Join(fixtureDir, name) @@ -97,12 +106,12 @@ func testModule(t *testing.T, name string) *configs.Config { t.Fatal(diags.Error()) } - config, diags := loader.LoadConfig(dir) + config, snap, diags := loader.LoadConfigWithSnapshot(dir) if diags.HasErrors() { t.Fatal(diags.Error()) } - return config + return config, snap } // testModuleInline takes a map of path -> config strings and yields a config @@ -137,10 +146,8 @@ func testModuleInline(t *testing.T, sources map[string]string) *configs.Config { } } - // FIXME: We're not dealing with the cleanup function here because - // this testModule function is used all over and so we don't want to - // change its interface at this late stage. - loader, _ := configload.NewLoaderForTests(t) + loader, cleanup := configload.NewLoaderForTests(t) + defer cleanup() // Test modules usually do not refer to remote sources, and for local // sources only this ultimately just records all of the module paths @@ -158,16 +165,6 @@ func testModuleInline(t *testing.T, sources map[string]string) *configs.Config { return config } -func testStringMatch(t *testing.T, s fmt.Stringer, expected string) { - t.Helper() - - actual := strings.TrimSpace(s.String()) - expected = strings.TrimSpace(expected) - if actual != expected { - t.Fatalf("Actual\n\n%s\n\nExpected:\n\n%s", actual, expected) - } -} - func testProviderFuncFixed(rp ResourceProvider) ResourceProviderFactory { return func() (ResourceProvider, error) { return rp, nil @@ -180,6 +177,30 @@ func testProvisionerFuncFixed(rp ResourceProvisioner) ResourceProvisionerFactory } } +func mustResourceInstanceAddr(s string) addrs.AbsResourceInstance { + addr, diags := addrs.ParseAbsResourceInstanceStr(s) + if diags.HasErrors() { + panic(diags.Err()) + } + return addr +} + +func instanceObjectIdForTests(obj *states.ResourceInstanceObject) string { + v := obj.Value + if v.IsNull() || !v.IsKnown() { + return "" + } + idVal := v.GetAttr("id") + if idVal.IsNull() || !idVal.IsKnown() { + return "" + } + idVal, err := convert.Convert(idVal, cty.String) + if err != nil { + return "" // placeholder value + } + return idVal.AsString() +} + // HookRecordApplyOrder is a test hook that records the order of applies // by recording the PreApply event. type HookRecordApplyOrder struct { @@ -188,17 +209,14 @@ type HookRecordApplyOrder struct { Active bool IDs []string - States []*InstanceState - Diffs []*InstanceDiff + States []cty.Value + Diffs []*plans.Change l sync.Mutex } -func (h *HookRecordApplyOrder) PreApply( - info *InstanceInfo, - s *InstanceState, - d *InstanceDiff) (HookAction, error) { - if d.Empty() { +func (h *HookRecordApplyOrder) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { + if plannedNewState.RawEquals(priorState) { return HookActionContinue, nil } @@ -206,9 +224,13 @@ func (h *HookRecordApplyOrder) PreApply( h.l.Lock() defer h.l.Unlock() - h.IDs = append(h.IDs, info.Id) - h.Diffs = append(h.Diffs, d) - h.States = append(h.States, s) + h.IDs = append(h.IDs, addr.String()) + h.Diffs = append(h.Diffs, &plans.Change{ + Action: action, + Before: priorState, + After: plannedNewState, + }) + h.States = append(h.States, priorState) } return HookActionContinue, nil diff --git a/terraform/test-fixtures/apply-resource-scale-in/main.tf b/terraform/test-fixtures/apply-resource-scale-in/main.tf index da5ac8e4d..00e53fb9f 100644 --- a/terraform/test-fixtures/apply-resource-scale-in/main.tf +++ b/terraform/test-fixtures/apply-resource-scale-in/main.tf @@ -1,13 +1,13 @@ -variable "count" {} +variable "instance_count" {} resource "aws_instance" "one" { - count = "${var.count}" + count = var.instance_count } locals { - "one_id" = "${element(concat(aws_instance.one.*.id, list("")), 0)}" + one_id = element(concat(aws_instance.one.*.id, list("")), 0) } resource "aws_instance" "two" { - val = "${local.one_id}" + val = local.one_id } diff --git a/terraform/transform_attach_state.go b/terraform/transform_attach_state.go index 600119878..0adb05877 100644 --- a/terraform/transform_attach_state.go +++ b/terraform/transform_attach_state.go @@ -4,6 +4,7 @@ import ( "log" "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/states" ) // GraphNodeAttachResourceState is an interface that can be implemented @@ -19,51 +20,46 @@ type GraphNodeAttachResourceState interface { GraphNodeResourceInstance // Sets the state - AttachResourceState(*ResourceState) + AttachResourceState(*states.Resource) } // AttachStateTransformer goes through the graph and attaches // state to nodes that implement the interfaces above. type AttachStateTransformer struct { - State *State // State is the root state + State *states.State // State is the root state } func (t *AttachStateTransformer) Transform(g *Graph) error { // If no state, then nothing to do if t.State == nil { - log.Printf("[DEBUG] Not attaching any state: state is nil") + log.Printf("[DEBUG] Not attaching any node states: overall state is nil") return nil } - filter := &StateFilter{State: t.State} for _, v := range g.Vertices() { - // Only care about nodes requesting we're adding state + // Nodes implement this interface to request state attachment. an, ok := v.(GraphNodeAttachResourceState) if !ok { continue } addr := an.ResourceInstanceAddr() - // Get the module state - results, err := filter.Filter(addr.String()) - if err != nil { - return err + rs := t.State.Resource(addr.ContainingResource()) + if rs == nil { + log.Printf("[DEBUG] Resource state not found for node %q, instance %s", dag.VertexName(v), addr) + continue } - // Attach the first resource state we get - found := false - for _, result := range results { - if rs, ok := result.Value.(*ResourceState); ok { - log.Printf("[DEBUG] Attaching resource state to %q: %#v", dag.VertexName(v), rs) - an.AttachResourceState(rs) - found = true - break - } + is := rs.Instance(addr.Resource.Key) + if is == nil { + // We don't actually need this here, since we'll attach the whole + // resource state, but we still check because it'd be weird + // for the specific instance we're attaching to not to exist. + log.Printf("[DEBUG] Resource instance state not found for node %q, instance %s", dag.VertexName(v), addr) + continue } - if !found { - log.Printf("[DEBUG] Resource state not found for %q: %s", dag.VertexName(v), addr) - } + an.AttachResourceState(rs) } return nil diff --git a/terraform/transform_deposed.go b/terraform/transform_deposed.go index 05b5ea07c..729cf5403 100644 --- a/terraform/transform_deposed.go +++ b/terraform/transform_deposed.go @@ -4,62 +4,46 @@ import ( "fmt" "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" ) -// DeposedTransformer is a GraphTransformer that adds deposed resources -// to the graph. +// DeposedTransformer is a GraphTransformer that adds nodes to the graph for +// the deposed objects associated with a given resource instance. type DeposedTransformer struct { - // State is the global state. We'll automatically find the correct - // ModuleState based on the Graph.Path that is being transformed. - State *State + // State is the global state, from which we'll retrieve the state for + // the instance given in InstanceAddr. + State *states.State - // View, if non-empty, is the ModuleState.View used around the state - // to find deposed resources. - View string + // InstanceAddr is the address of the instance whose deposed objects will + // have graph nodes created. + InstanceAddr addrs.AbsResourceInstance // The provider used by the resourced which were deposed ResolvedProvider addrs.AbsProviderConfig } func (t *DeposedTransformer) Transform(g *Graph) error { - state := t.State.ModuleByPath(g.Path) - if state == nil { - // If there is no state for our module there can't be any deposed - // resources, since they live in the state. + rs := t.State.Resource(t.InstanceAddr.ContainingResource()) + if rs == nil { + // If the resource has no state then there can't be deposed objects. + return nil + } + is := rs.Instances[t.InstanceAddr.Resource.Key] + if is == nil { + // If the instance has no state then there can't be deposed objects. return nil } - // If we have a view, apply it now - if t.View != "" { - state = state.View(t.View) - } + providerAddr := rs.ProviderConfig - // Go through all the resources in our state to look for deposed resources - for k, rs := range state.Resources { - // If we have no deposed resources, then move on - if len(rs.Deposed) == 0 { - continue - } - - legacyAddr, err := parseResourceAddressInternal(k) - if err != nil { - return fmt.Errorf("invalid instance key %q in state: %s", k, err) - } - addr := legacyAddr.AbsResourceInstanceAddr() - - providerAddr, err := rs.ProviderAddr() - if err != nil { - return fmt.Errorf("invalid instance provider address %q in state: %s", rs.Provider, err) - } - - for i := range rs.Deposed { - g.Add(&graphNodeDeposedResource{ - Addr: addr, - Index: i, - RecordedProvider: providerAddr, - ResolvedProvider: providerAddr, - }) - } + for k := range is.Deposed { + g.Add(&graphNodeDeposedResource{ + Addr: t.InstanceAddr, + DeposedKey: k, + RecordedProvider: providerAddr, + ResolvedProvider: t.ResolvedProvider, + }) } return nil @@ -68,7 +52,7 @@ func (t *DeposedTransformer) Transform(g *Graph) error { // graphNodeDeposedResource is the graph vertex representing a deposed resource. type graphNodeDeposedResource struct { Addr addrs.AbsResourceInstance - Index int // Index into the "deposed" list in state + DeposedKey states.DeposedKey RecordedProvider addrs.AbsProviderConfig ResolvedProvider addrs.AbsProviderConfig } @@ -79,7 +63,7 @@ var ( ) func (n *graphNodeDeposedResource) Name() string { - return fmt.Sprintf("%s (deposed #%d)", n.Addr.String(), n.Index) + return fmt.Sprintf("%s (deposed %s)", n.Addr.String(), n.DeposedKey) } func (n *graphNodeDeposedResource) ProvidedBy() (addrs.AbsProviderConfig, bool) { @@ -97,12 +81,11 @@ func (n *graphNodeDeposedResource) EvalTree() EvalNode { addr := n.Addr var provider ResourceProvider - var state *InstanceState + var providerSchema *ProviderSchema + var state *states.ResourceInstanceObject seq := &EvalSequence{Nodes: make([]EvalNode, 0, 5)} - stateKey := NewLegacyResourceInstanceAddress(addr).stateId() - // Refresh the resource seq.Nodes = append(seq.Nodes, &EvalOpFilter{ Ops: []walkOperation{walkRefresh}, @@ -111,11 +94,12 @@ func (n *graphNodeDeposedResource) EvalTree() EvalNode { &EvalGetProvider{ Addr: n.ResolvedProvider, Output: &provider, + Schema: &providerSchema, }, &EvalReadStateDeposed{ - Name: stateKey, + Addr: addr.Resource, + Key: n.DeposedKey, Output: &state, - Index: n.Index, }, &EvalRefresh{ Addr: addr.Resource, @@ -124,18 +108,18 @@ func (n *graphNodeDeposedResource) EvalTree() EvalNode { Output: &state, }, &EvalWriteStateDeposed{ - Name: stateKey, - ResourceType: n.Addr.Resource.Resource.Type, - Provider: n.ResolvedProvider.String(), // FIXME: Change underlying struct to use addrs.AbsProviderConfig itself - State: &state, - Index: n.Index, + Addr: addr.Resource, + Key: n.DeposedKey, + ProviderAddr: n.ResolvedProvider, + ProviderSchema: &providerSchema, + State: &state, }, }, }, }) // Apply - var diff *InstanceDiff + var change *plans.ResourceInstanceChange var err error seq.Nodes = append(seq.Nodes, &EvalOpFilter{ Ops: []walkOperation{walkApply, walkDestroy}, @@ -146,25 +130,27 @@ func (n *graphNodeDeposedResource) EvalTree() EvalNode { Output: &provider, }, &EvalReadStateDeposed{ - Name: stateKey, - Output: &state, - Index: n.Index, + Addr: addr.Resource, + Output: &state, + Key: n.DeposedKey, + Provider: &provider, + ProviderSchema: &providerSchema, }, &EvalDiffDestroy{ Addr: addr.Resource, State: &state, - Output: &diff, + Output: &change, }, // Call pre-apply hook &EvalApplyPre{ - Addr: addr.Resource, - State: &state, - Diff: &diff, + Addr: addr.Resource, + State: &state, + Change: &change, }, &EvalApply{ Addr: addr.Resource, State: &state, - Diff: &diff, + Change: &change, Provider: &provider, Output: &state, Error: &err, @@ -173,11 +159,11 @@ func (n *graphNodeDeposedResource) EvalTree() EvalNode { // was successfully destroyed it will be pruned. If it was not, it will // be caught on the next run. &EvalWriteStateDeposed{ - Name: stateKey, - ResourceType: n.Addr.Resource.Resource.Type, - Provider: n.ResolvedProvider.String(), // FIXME: Change underlying struct to use addrs.AbsProviderConfig itself - State: &state, - Index: n.Index, + Addr: addr.Resource, + Key: n.DeposedKey, + ProviderAddr: n.ResolvedProvider, + ProviderSchema: &providerSchema, + State: &state, }, &EvalApplyPost{ Addr: addr.Resource, diff --git a/terraform/transform_destroy_cbd.go b/terraform/transform_destroy_cbd.go index f4cc75ced..3a729c8db 100644 --- a/terraform/transform_destroy_cbd.go +++ b/terraform/transform_destroy_cbd.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/states" ) // GraphNodeDestroyerCBD must be implemented by nodes that might be @@ -53,7 +54,7 @@ type CBDEdgeTransformer struct { // Module and State are only needed to look up dependencies in // any way possible. Either can be nil if not availabile. Config *configs.Config - State *State + State *states.State // If configuration is present then Schemas is required in order to // obtain schema information from providers and provisioners so we can diff --git a/terraform/transform_destroy_edge.go b/terraform/transform_destroy_edge.go index 029601f52..7fb415bdf 100644 --- a/terraform/transform_destroy_edge.go +++ b/terraform/transform_destroy_edge.go @@ -4,6 +4,7 @@ import ( "log" "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/dag" @@ -43,7 +44,7 @@ type DestroyEdgeTransformer struct { // These are needed to properly build the graph of dependencies // to determine what a destroy node depends on. Any of these can be nil. Config *configs.Config - State *State + State *states.State // If configuration is present then Schemas is required in order to // obtain schema information from providers and provisioners in order @@ -53,8 +54,8 @@ type DestroyEdgeTransformer struct { func (t *DestroyEdgeTransformer) Transform(g *Graph) error { // Build a map of what is being destroyed (by address string) to - // the list of destroyers. In general there will only be one destroyer - // but to make it more robust we support multiple. + // the list of destroyers. Usually there will be at most one destroyer + // per node, but we allow multiple if present for completeness. destroyers := make(map[string][]GraphNodeDestroyer) destroyerAddrs := make(map[string]addrs.AbsResourceInstance) for _, v := range g.Vertices() { diff --git a/terraform/transform_diff.go b/terraform/transform_diff.go index cfcc87751..67a94fed9 100644 --- a/terraform/transform_diff.go +++ b/terraform/transform_diff.go @@ -5,76 +5,81 @@ import ( "log" "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/states" ) -// DiffTransformer is a GraphTransformer that adds the elements of -// the diff to the graph. -// -// This transform is used for example by the ApplyGraphBuilder to ensure -// that only resources that are being modified are represented in the graph. +// DiffTransformer is a GraphTransformer that adds graph nodes representing +// each of the resource changes described in the given Changes object. type DiffTransformer struct { Concrete ConcreteResourceInstanceNodeFunc - Diff *Diff + Changes *plans.Changes } func (t *DiffTransformer) Transform(g *Graph) error { - // If the diff is nil or empty (nil is empty) then do nothing - if t.Diff.Empty() { + if t.Changes == nil || len(t.Changes.Resources) == 0 { + // Nothing to do! return nil } // Go through all the modules in the diff. - log.Printf("[TRACE] DiffTransformer: starting") - var nodes []dag.Vertex - for _, m := range t.Diff.Modules { - log.Printf("[TRACE] DiffTransformer: Module: %s", m) - // TODO: If this is a destroy diff then add a module destroy node + log.Printf("[TRACE] DiffTransformer starting") - // Go through all the resources in this module. - for name, inst := range m.Resources { - log.Printf("[TRACE] DiffTransformer: Resource %q: %#v", name, inst) + for _, rc := range t.Changes.Resources { + addr := rc.Addr + dk := rc.DeposedKey - // We have changes! This is a create or update operation. - // First grab the address so we have a unique way to - // reference this resource. - legacyAddr, err := parseResourceAddressInternal(name) - if err != nil { - panic(fmt.Sprintf( - "Error parsing internal name, this is a bug: %q", name)) - } - - // legacyAddr is relative even though the legacy ResourceAddress is - // usually absolute, so we need to do some trickery here to get - // a new-style absolute address in the right module. - // FIXME: Clean this up once the "Diff" types are updated to use - // our new address types. - addr := legacyAddr.AbsResourceInstanceAddr() - addr = addr.Resource.Absolute(normalizeModulePath(m.Path)) - - // If we're destroying, add the destroy node - if inst.Destroy || inst.GetDestroyDeposed() { - abstract := NewNodeAbstractResourceInstance(addr) - g.Add(&NodeDestroyResourceInstance{NodeAbstractResourceInstance: abstract}) - } - - // If we have changes, then add the applyable version - if len(inst.Attributes) > 0 { - // Add the resource to the graph - abstract := NewNodeAbstractResourceInstance(addr) - var node dag.Vertex = abstract - if f := t.Concrete; f != nil { - node = f(abstract) - } - - nodes = append(nodes, node) - } + // Depending on the action we'll need some different combinations of + // nodes, because destroying uses a special node type separate from + // other actions. + var update, delete bool + switch rc.Action { + case plans.Delete: + delete = true + case plans.Replace: + update = true + delete = true + default: + update = true } + + if update { + // All actions except destroying the node type chosen by t.Concrete + abstract := NewNodeAbstractResourceInstance(addr) + var node dag.Vertex = abstract + if f := t.Concrete; f != nil { + node = f(abstract) + } + + if dk != states.NotDeposed { + // The only valid action for deposed objects is to destroy them. + // Entering this branch suggests a bug in the plan phase that + // proposed this change. + return fmt.Errorf("invalid %s action for deposed object on %s: only Delete is allowed", rc.Action, addr) + } + + log.Printf("[TRACE] DiffTransformer: %s will be represented by %s", addr, dag.VertexName(node)) + g.Add(node) + } + + if delete { + // Destroying always uses this destroy-specific node type. + abstract := NewNodeAbstractResourceInstance(addr) + node := &NodeDestroyResourceInstance{ + NodeAbstractResourceInstance: abstract, + DeposedKey: dk, + } + if dk == states.NotDeposed { + log.Printf("[TRACE] DiffTransformer: %s will be represented for destruction by %s", addr, dag.VertexName(node)) + } else { + log.Printf("[TRACE] DiffTransformer: %s deposed object %s will be represented for destruction by %s", addr, dk, dag.VertexName(node)) + } + g.Add(node) + } + } - // Add all the nodes to the graph - for _, n := range nodes { - g.Add(n) - } + log.Printf("[TRACE] DiffTransformer complete") return nil } diff --git a/terraform/transform_diff_test.go b/terraform/transform_diff_test.go index b6a54c06f..a83a5d75f 100644 --- a/terraform/transform_diff_test.go +++ b/terraform/transform_diff_test.go @@ -4,7 +4,10 @@ import ( "strings" "testing" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/plans" ) func TestDiffTransformer_nilDiff(t *testing.T) { @@ -21,20 +24,32 @@ func TestDiffTransformer_nilDiff(t *testing.T) { func TestDiffTransformer(t *testing.T) { g := Graph{Path: addrs.RootModuleInstance} + + beforeVal, err := plans.NewDynamicValue(cty.StringVal(""), cty.String) + if err != nil { + t.Fatal(err) + } + afterVal, err := plans.NewDynamicValue(cty.StringVal(""), cty.String) + if err != nil { + t.Fatal(err) + } + tf := &DiffTransformer{ - Diff: &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: []string{"root"}, - Resources: map[string]*InstanceDiff{ - "aws_instance.foo": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "name": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, + Changes: &plans.Changes{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.ProviderConfig{ + Type: "aws", + }.Absolute(addrs.RootModuleInstance), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Update, + Before: beforeVal, + After: afterVal, }, }, }, diff --git a/terraform/transform_import_state.go b/terraform/transform_import_state.go index 6efebefb1..1fdef1872 100644 --- a/terraform/transform_import_state.go +++ b/terraform/transform_import_state.go @@ -3,6 +3,8 @@ package terraform import ( "fmt" + "github.com/hashicorp/terraform/states" + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/tfdiags" ) @@ -31,7 +33,7 @@ type graphNodeImportState struct { ProviderAddr addrs.AbsProviderConfig // Provider address given by the user, or implied by the resource type ResolvedProvider addrs.AbsProviderConfig // provider node address after resolution - states []*InstanceState + states []*states.ImportedObject } var ( @@ -68,7 +70,6 @@ func (n *graphNodeImportState) Path() addrs.ModuleInstance { // GraphNodeEvalable impl. func (n *graphNodeImportState) EvalTree() EvalNode { var provider ResourceProvider - info := NewInstanceInfo(n.Addr) // Reset our states n.states = nil @@ -81,8 +82,8 @@ func (n *graphNodeImportState) EvalTree() EvalNode { Output: &provider, }, &EvalImportState{ + Addr: n.Addr.Resource, Provider: &provider, - Info: info, Id: n.ID, Output: &n.states, }, @@ -110,7 +111,7 @@ func (n *graphNodeImportState) DynamicExpand(ctx EvalContext) (*Graph, error) { addrs := make([]addrs.AbsResourceInstance, len(n.states)) for i, state := range n.states { addr := n.Addr - if t := state.Ephemeral.Type; t != "" { + if t := state.ResourceType; t != "" { addr.Resource.Resource.Type = t } @@ -128,29 +129,17 @@ func (n *graphNodeImportState) DynamicExpand(ctx EvalContext) (*Graph, error) { } // Verify that all the addresses are clear - state, lock := ctx.State() - lock.RLock() - defer lock.RUnlock() - filter := &StateFilter{State: state} + state := ctx.State() for _, addr := range addrs { - result, err := filter.Filter(addr.String()) - if err != nil { - diags = diags.Append(fmt.Errorf("Error while checking for existing %s in state: %s", addr, err)) + existing := state.ResourceInstance(addr) + if existing != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Resource already managed by Terraform", + fmt.Sprintf("Terraform is already managing a remote object for %s. To import to this address you must first remove the existing object from the state.", addr), + )) continue } - - // Go through the filter results and it is an error if we find - // a matching InstanceState, meaning that we would have a collision. - for _, r := range result { - if is, ok := r.Value.(*InstanceState); ok { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Resource already managed by Terraform", - fmt.Sprintf("Terraform is already managing a remote object for %s, with the id %q. To import to this address you must first remove the existing object from the state.", addr, is.ID), - )) - continue - } - } } if diags.HasErrors() { // Bail out early, then. @@ -184,7 +173,7 @@ func (n *graphNodeImportState) DynamicExpand(ctx EvalContext) (*Graph, error) { // and adding a resource to the state once it is imported. type graphNodeImportStateSub struct { TargetAddr addrs.AbsResourceInstance - State *InstanceState + State *states.ImportedObject ResolvedProvider addrs.AbsProviderConfig } @@ -194,7 +183,7 @@ var ( ) func (n *graphNodeImportStateSub) Name() string { - return fmt.Sprintf("import %s result: %s", n.TargetAddr, n.State.ID) + return fmt.Sprintf("import %s result", n.TargetAddr) } func (n *graphNodeImportStateSub) Path() addrs.ModuleInstance { @@ -204,24 +193,21 @@ func (n *graphNodeImportStateSub) Path() addrs.ModuleInstance { // GraphNodeEvalable impl. func (n *graphNodeImportStateSub) EvalTree() EvalNode { // If the Ephemeral type isn't set, then it is an error - if n.State.Ephemeral.Type == "" { - err := fmt.Errorf("import of %s didn't set type for %q", n.TargetAddr.String(), n.State.ID) + if n.State.ResourceType == "" { + err := fmt.Errorf("import of %s didn't set type", n.TargetAddr.String()) return &EvalReturnError{Error: &err} } - // DeepCopy so we're only modifying our local copy - state := n.State.DeepCopy() + state := n.State.AsInstanceObject() - // Key is the resource key - key := NewLegacyResourceInstanceAddress(n.TargetAddr).stateId() - - // The eval sequence var provider ResourceProvider + var providerSchema *ProviderSchema return &EvalSequence{ Nodes: []EvalNode{ &EvalGetProvider{ Addr: n.ResolvedProvider, Output: &provider, + Schema: &providerSchema, }, &EvalRefresh{ Addr: n.TargetAddr.Resource, @@ -231,14 +217,13 @@ func (n *graphNodeImportStateSub) EvalTree() EvalNode { }, &EvalImportStateVerify{ Addr: n.TargetAddr.Resource, - Id: n.State.ID, State: &state, }, &EvalWriteState{ - Name: key, - ResourceType: n.TargetAddr.Resource.Resource.Type, - Provider: n.ResolvedProvider, - State: &state, + Addr: n.TargetAddr.Resource, + ProviderAddr: n.ResolvedProvider, + ProviderSchema: &providerSchema, + State: &state, }, }, } diff --git a/terraform/transform_orphan_count.go b/terraform/transform_orphan_count.go index 87cef8aa2..eec762e55 100644 --- a/terraform/transform_orphan_count.go +++ b/terraform/transform_orphan_count.go @@ -5,6 +5,7 @@ import ( "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/states" ) // OrphanResourceCountTransformer is a GraphTransformer that adds orphans @@ -19,33 +20,18 @@ type OrphanResourceCountTransformer struct { Count int // Actual count of the resource, or -1 if count is not set at all Addr addrs.AbsResource // Addr of the resource to look for orphans - State *State // Full global state + State *states.State // Full global state } func (t *OrphanResourceCountTransformer) Transform(g *Graph) error { - - // Grab the module in the state just for this resource address - ms := t.State.ModuleByPath(t.Addr.Module) - if ms == nil { - // If no state, there can't be orphans - return nil + rs := t.State.Resource(t.Addr) + if rs == nil { + return nil // Resource doesn't exist in state, so nothing to do! } haveKeys := make(map[addrs.InstanceKey]struct{}) - for legacyAddrStr := range ms.Resources { - legacyAddr, err := parseResourceAddressInternal(legacyAddrStr) - if err != nil { - return err - } - legacyAddr.Path = ms.Path[1:] - addr := legacyAddr.AbsResourceInstanceAddr() - - if addr.ContainingResource().String() != t.Addr.String() { - // Not one of our instances - continue - } - - haveKeys[addr.Resource.Key] = struct{}{} + for key := range rs.Instances { + haveKeys[key] = struct{}{} } if t.Count < 0 { diff --git a/terraform/transform_orphan_count_test.go b/terraform/transform_orphan_count_test.go index dd4819357..55e6914da 100644 --- a/terraform/transform_orphan_count_test.go +++ b/terraform/transform_orphan_count_test.go @@ -8,7 +8,7 @@ import ( ) func TestOrphanResourceCountTransformer(t *testing.T) { - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root"}, @@ -36,7 +36,7 @@ func TestOrphanResourceCountTransformer(t *testing.T) { }, }, }, - } + }) g := Graph{Path: addrs.RootModuleInstance} @@ -62,7 +62,7 @@ func TestOrphanResourceCountTransformer(t *testing.T) { } func TestOrphanResourceCountTransformer_zero(t *testing.T) { - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root"}, @@ -90,7 +90,7 @@ func TestOrphanResourceCountTransformer_zero(t *testing.T) { }, }, }, - } + }) g := Graph{Path: addrs.RootModuleInstance} @@ -116,7 +116,7 @@ func TestOrphanResourceCountTransformer_zero(t *testing.T) { } func TestOrphanResourceCountTransformer_oneNoIndex(t *testing.T) { - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root"}, @@ -144,7 +144,7 @@ func TestOrphanResourceCountTransformer_oneNoIndex(t *testing.T) { }, }, }, - } + }) g := Graph{Path: addrs.RootModuleInstance} @@ -170,7 +170,7 @@ func TestOrphanResourceCountTransformer_oneNoIndex(t *testing.T) { } func TestOrphanResourceCountTransformer_oneIndex(t *testing.T) { - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root"}, @@ -198,7 +198,7 @@ func TestOrphanResourceCountTransformer_oneIndex(t *testing.T) { }, }, }, - } + }) g := Graph{Path: addrs.RootModuleInstance} @@ -224,7 +224,7 @@ func TestOrphanResourceCountTransformer_oneIndex(t *testing.T) { } func TestOrphanResourceCountTransformer_zeroAndNone(t *testing.T) { - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root"}, @@ -252,7 +252,7 @@ func TestOrphanResourceCountTransformer_zeroAndNone(t *testing.T) { }, }, }, - } + }) g := Graph{Path: addrs.RootModuleInstance} @@ -278,7 +278,7 @@ func TestOrphanResourceCountTransformer_zeroAndNone(t *testing.T) { } func TestOrphanResourceCountTransformer_zeroAndNoneCount(t *testing.T) { - state := &State{ + state := mustShimLegacyState(&State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root"}, @@ -306,7 +306,7 @@ func TestOrphanResourceCountTransformer_zeroAndNoneCount(t *testing.T) { }, }, }, - } + }) g := Graph{Path: addrs.RootModuleInstance} diff --git a/terraform/transform_orphan_output.go b/terraform/transform_orphan_output.go index ad5ae2543..c67540934 100644 --- a/terraform/transform_orphan_output.go +++ b/terraform/transform_orphan_output.go @@ -3,7 +3,9 @@ package terraform import ( "log" + "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/states" ) // OrphanOutputTransformer finds the outputs that aren't present @@ -11,7 +13,7 @@ import ( // for deletion. type OrphanOutputTransformer struct { Config *configs.Config // Root of config tree - State *State // State is the root state + State *states.State // State is the root state } func (t *OrphanOutputTransformer) Transform(g *Graph) error { @@ -28,12 +30,12 @@ func (t *OrphanOutputTransformer) Transform(g *Graph) error { return nil } -func (t *OrphanOutputTransformer) transform(g *Graph, ms *ModuleState) error { +func (t *OrphanOutputTransformer) transform(g *Graph, ms *states.Module) error { if ms == nil { return nil } - moduleAddr := normalizeModulePath(ms.Path) + moduleAddr := ms.Addr // Get the config for this path, which is nil if the entire module has been // removed. @@ -42,10 +44,15 @@ func (t *OrphanOutputTransformer) transform(g *Graph, ms *ModuleState) error { outputs = c.Module.Outputs } - // add all the orphaned outputs to the graph - for _, addr := range ms.RemovedOutputs(outputs) { + // An output is "orphaned" if it's present in the state but not declared + // in the configuration. + for name := range ms.OutputValues { + if _, exists := outputs[name]; exists { + continue + } + g.Add(&NodeOutputOrphan{ - Addr: addr.Absolute(moduleAddr), + Addr: addrs.OutputValue{Name: name}.Absolute(moduleAddr), }) } diff --git a/terraform/transform_orphan_resource.go b/terraform/transform_orphan_resource.go index d4b25faae..6a7995395 100644 --- a/terraform/transform_orphan_resource.go +++ b/terraform/transform_orphan_resource.go @@ -3,6 +3,7 @@ package terraform import ( "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/states" ) // OrphanResourceTransformer is a GraphTransformer that adds resource @@ -16,7 +17,7 @@ type OrphanResourceTransformer struct { // State is the global state. We require the global state to // properly find module orphans at our path. - State *State + State *states.State // Config is the root node in the configuration tree. We'll look up // the appropriate note in this tree using the path in each node. @@ -28,6 +29,11 @@ func (t *OrphanResourceTransformer) Transform(g *Graph) error { // If the entire state is nil, there can't be any orphans return nil } + if t.Config == nil { + // Should never happen: we can't be doing any Terraform operations + // without at least an empty configuration. + panic("OrpahResourceTransformer used without setting Config") + } // Go through the modules and for each module transform in order // to add the orphan. @@ -40,30 +46,44 @@ func (t *OrphanResourceTransformer) Transform(g *Graph) error { return nil } -func (t *OrphanResourceTransformer) transform(g *Graph, ms *ModuleState) error { +func (t *OrphanResourceTransformer) transform(g *Graph, ms *states.Module) error { if ms == nil { return nil } - path := normalizeModulePath(ms.Path) + moduleAddr := ms.Addr - // Get the configuration for this path. The configuration might be + // Get the configuration for this module. The configuration might be // nil if the module was removed from the configuration. This is okay, // this just means that every resource is an orphan. var m *configs.Module - if c := t.Config.DescendentForInstance(path); c != nil { + if c := t.Config.DescendentForInstance(moduleAddr); c != nil { m = c.Module } - // Go through the orphans and add them all to the state - for _, relAddr := range ms.Orphans(m) { - addr := relAddr.Absolute(path) - abstract := NewNodeAbstractResourceInstance(addr) - var node dag.Vertex = abstract - if f := t.Concrete; f != nil { - node = f(abstract) + // An "orphan" is a resource that is in the state but not the configuration, + // so we'll walk the state resources and try to correlate each of them + // with a configuration block. Each orphan gets a node in the graph whose + // type is decided by t.Concrete. + // + // We don't handle orphans related to changes in the "count" and "for_each" + // pseudo-arguments here. They are handled by OrphanResourceCountTransformer. + for _, rs := range ms.Resources { + if m != nil { + if r := m.ResourceByAddr(rs.Addr); r != nil { + continue + } + } + + for key := range rs.Instances { + addr := rs.Addr.Instance(key).Absolute(moduleAddr) + abstract := NewNodeAbstractResourceInstance(addr) + var node dag.Vertex = abstract + if f := t.Concrete; f != nil { + node = f(abstract) + } + g.Add(node) } - g.Add(node) } return nil diff --git a/terraform/transform_orphan_resource_test.go b/terraform/transform_orphan_resource_test.go index b3f8de1ad..b0af2b4cc 100644 --- a/terraform/transform_orphan_resource_test.go +++ b/terraform/transform_orphan_resource_test.go @@ -6,35 +6,49 @@ import ( "testing" "github.com/hashicorp/terraform/addrs" - "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/states" ) func TestOrphanResourceTransformer(t *testing.T) { mod := testModule(t, "transform-orphan-basic") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root"}, - Resources: map[string]*ResourceState{ - "aws_instance.web": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - // The orphan - "aws_instance.db": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "web", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "id": "foo", }, + Status: states.ObjectReady, }, - }, - } + addrs.ProviderConfig{ + Type: "aws", + }.Absolute(addrs.RootModuleInstance), + ) + + // The orphan + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "db", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "id": "foo", + }, + Status: states.ObjectReady, + }, + addrs.ProviderConfig{ + Type: "aws", + }.Absolute(addrs.RootModuleInstance), + ) + }) g := Graph{Path: addrs.RootModuleInstance} { @@ -62,56 +76,43 @@ func TestOrphanResourceTransformer(t *testing.T) { } } -func TestOrphanResourceTransformer_nilModule(t *testing.T) { - mod := testModule(t, "transform-orphan-basic") - state := &State{ - Modules: []*ModuleState{nil}, - } - - g := Graph{Path: addrs.RootModuleInstance} - { - tf := &ConfigTransformer{Config: mod} - if err := tf.Transform(&g); err != nil { - t.Fatalf("err: %s", err) - } - } - - { - tf := &OrphanResourceTransformer{ - Concrete: testOrphanResourceConcreteFunc, - State: state, - Config: mod, - } - if err := tf.Transform(&g); err != nil { - t.Fatalf("err: %s", err) - } - } -} - func TestOrphanResourceTransformer_countGood(t *testing.T) { mod := testModule(t, "transform-orphan-count") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root"}, - Resources: map[string]*ResourceState{ - "aws_instance.foo.0": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - "aws_instance.foo.1": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + }.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "id": "foo", }, + Status: states.ObjectReady, }, - }, - } + addrs.ProviderConfig{ + Type: "aws", + }.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + }.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "id": "foo", + }, + Status: states.ObjectReady, + }, + addrs.ProviderConfig{ + Type: "aws", + }.Absolute(addrs.RootModuleInstance), + ) + }) g := Graph{Path: addrs.RootModuleInstance} { @@ -141,28 +142,40 @@ func TestOrphanResourceTransformer_countGood(t *testing.T) { func TestOrphanResourceTransformer_countBad(t *testing.T) { mod := testModule(t, "transform-orphan-count-empty") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root"}, - Resources: map[string]*ResourceState{ - "aws_instance.foo.0": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - - "aws_instance.foo.1": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + }.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "id": "foo", }, + Status: states.ObjectReady, }, - }, - } + addrs.ProviderConfig{ + Type: "aws", + }.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + }.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "id": "foo", + }, + Status: states.ObjectReady, + }, + addrs.ProviderConfig{ + Type: "aws", + }.Absolute(addrs.RootModuleInstance), + ) + }) g := Graph{Path: addrs.RootModuleInstance} { @@ -192,33 +205,40 @@ func TestOrphanResourceTransformer_countBad(t *testing.T) { func TestOrphanResourceTransformer_modules(t *testing.T) { mod := testModule(t, "transform-orphan-modules") - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root"}, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "id": "foo", }, + Status: states.ObjectReady, }, - - &ModuleState{ - Path: []string{"root", "child"}, - Resources: map[string]*ResourceState{ - "aws_instance.web": &ResourceState{ - Type: "aws_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, + addrs.ProviderConfig{ + Type: "aws", + }.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "web", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey)), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "id": "foo", }, + Status: states.ObjectReady, }, - }, - } + addrs.ProviderConfig{ + Type: "aws", + }.Absolute(addrs.RootModuleInstance), + ) + }) g := Graph{Path: addrs.RootModuleInstance} { @@ -239,10 +259,10 @@ func TestOrphanResourceTransformer_modules(t *testing.T) { } } - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTransformOrphanResourceModulesStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) + got := strings.TrimSpace(g.String()) + want := strings.TrimSpace(testTransformOrphanResourceModulesStr) + if got != want { + t.Fatalf("wrong state result\ngot:\n%s\n\nwant:\n%s", got, want) } } diff --git a/terraform/transform_provider.go b/terraform/transform_provider.go index 01cd9fddf..a5c97f632 100644 --- a/terraform/transform_provider.go +++ b/terraform/transform_provider.go @@ -185,7 +185,7 @@ func (t *ProviderTransformer) Transform(g *Graph) error { key = target.(GraphNodeProvider).ProviderAddr().String() } - log.Printf("[DEBUG] %s needs %s", dag.VertexName(v), dag.VertexName(target)) + log.Printf("[DEBUG] ProviderTransformer: %q (%T) needs %s", dag.VertexName(v), v, dag.VertexName(target)) if pv, ok := v.(GraphNodeProviderConsumer); ok { pv.SetProvider(target.ProviderAddr()) } diff --git a/terraform/transform_provisioner_test.go b/terraform/transform_provisioner_test.go index 546c9b697..1c2c4f772 100644 --- a/terraform/transform_provisioner_test.go +++ b/terraform/transform_provisioner_test.go @@ -5,8 +5,8 @@ import ( "testing" "github.com/hashicorp/terraform/addrs" - "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/states" ) func TestMissingProvisionerTransformer(t *testing.T) { @@ -57,31 +57,44 @@ func TestMissingProvisionerTransformer_module(t *testing.T) { return a } - var state State - state.init() - state.Modules = []*ModuleState{ - &ModuleState{ - Path: []string{"root"}, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Primary: &InstanceState{ID: "foo"}, + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "id": "foo", }, + Status: states.ObjectReady, }, - }, - - &ModuleState{ - Path: []string{"root", "child"}, - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - Primary: &InstanceState{ID: "foo"}, + addrs.ProviderConfig{ + Type: "aws", + }.Absolute(addrs.RootModuleInstance), + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey)), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "id": "foo", }, + Status: states.ObjectReady, }, - }, - } + addrs.ProviderConfig{ + Type: "aws", + }.Absolute(addrs.RootModuleInstance), + ) + }) tf := &StateTransformer{ Concrete: concreteResource, - State: &state, + State: state, } if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) diff --git a/terraform/transform_reference.go b/terraform/transform_reference.go index 4215ad587..23bc8cd20 100644 --- a/terraform/transform_reference.go +++ b/terraform/transform_reference.go @@ -400,6 +400,10 @@ func NewReferenceMap(vs []dag.Vertex) *ReferenceMap { // Go through and cache them for _, ref := range rn.References() { + if ref.Subject == nil { + // Should never happen + panic(fmt.Sprintf("%T.References returned reference with nil subject", rn)) + } key := m.referenceMapKey(v, ref.Subject) edges[key] = append(edges[key], v) } diff --git a/terraform/transform_removed_modules.go b/terraform/transform_removed_modules.go index e7444612e..ee71387e2 100644 --- a/terraform/transform_removed_modules.go +++ b/terraform/transform_removed_modules.go @@ -4,13 +4,14 @@ import ( "log" "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/states" ) // RemovedModuleTransformer implements GraphTransformer to add nodes indicating // when a module was removed from the configuration. type RemovedModuleTransformer struct { Config *configs.Config // root node in the config tree - State *State + State *states.State } func (t *RemovedModuleTransformer) Transform(g *Graph) error { @@ -20,14 +21,13 @@ func (t *RemovedModuleTransformer) Transform(g *Graph) error { } for _, m := range t.State.Modules { - path := normalizeModulePath(m.Path) - cc := t.Config.DescendentForInstance(path) + cc := t.Config.DescendentForInstance(m.Addr) if cc != nil { continue } - log.Printf("[DEBUG] %s is no longer in configuration\n", path) - g.Add(&NodeModuleRemoved{Addr: path}) + log.Printf("[DEBUG] %s is no longer in configuration\n", m.Addr) + g.Add(&NodeModuleRemoved{Addr: m.Addr}) } return nil } diff --git a/terraform/transform_state.go b/terraform/transform_state.go index 162e87e35..bd6f0d163 100644 --- a/terraform/transform_state.go +++ b/terraform/transform_state.go @@ -1,10 +1,10 @@ package terraform import ( - "fmt" "log" "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/states" ) // StateTransformer is a GraphTransformer that adds the elements of @@ -15,53 +15,36 @@ import ( type StateTransformer struct { Concrete ConcreteResourceInstanceNodeFunc - State *State + State *states.State } func (t *StateTransformer) Transform(g *Graph) error { - // If the state is nil or empty (nil is empty) then do nothing - if t.State.Empty() { + if !t.State.HasResources() { + log.Printf("[TRACE] StateTransformer: state is empty, so nothing to do") return nil } - // Go through all the modules in the diff. log.Printf("[TRACE] StateTransformer: starting") - var nodes []dag.Vertex for _, ms := range t.State.Modules { - log.Printf("[TRACE] StateTransformer: Module: %v", ms.Path) + moduleAddr := ms.Addr - // Go through all the resources in this module. - for name, rs := range ms.Resources { - log.Printf("[TRACE] StateTransformer: Resource %q: %#v", name, rs) + for _, rs := range ms.Resources { + resourceAddr := rs.Addr.Absolute(moduleAddr) - // State hasn't yet been updated to our new address format, so - // we need to shim this. - legacyAddr, err := parseResourceAddressInternal(name) - if err != nil { - // Indicates someone has tampered with the state file - return fmt.Errorf("invalid resource address %q in state", name) + for key := range rs.Instances { + addr := resourceAddr.Instance(key) + + abstract := NewNodeAbstractResourceInstance(addr) + var node dag.Vertex = abstract + if f := t.Concrete; f != nil { + node = f(abstract) + } + + g.Add(node) + log.Printf("[TRACE] StateTransformer: added %T for %s", node, addr) } - // Very important: add the module path for this resource to - // the address. Remove "root" from it. - legacyAddr.Path = ms.Path[1:] - - addr := legacyAddr.AbsResourceInstanceAddr() - - // Add the resource to the graph - abstract := NewNodeAbstractResourceInstance(addr) - var node dag.Vertex = abstract - if f := t.Concrete; f != nil { - node = f(abstract) - } - - nodes = append(nodes, node) } } - // Add all the nodes to the graph - for _, n := range nodes { - g.Add(n) - } - return nil } diff --git a/terraform/ui_output_provisioner.go b/terraform/ui_output_provisioner.go index 878a03122..fff964f4b 100644 --- a/terraform/ui_output_provisioner.go +++ b/terraform/ui_output_provisioner.go @@ -1,15 +1,19 @@ package terraform +import ( + "github.com/hashicorp/terraform/addrs" +) + // ProvisionerUIOutput is an implementation of UIOutput that calls a hook // for the output so that the hooks can handle it. type ProvisionerUIOutput struct { - Info *InstanceInfo - Type string - Hooks []Hook + InstanceAddr addrs.AbsResourceInstance + ProvisionerType string + Hooks []Hook } func (o *ProvisionerUIOutput) Output(msg string) { for _, h := range o.Hooks { - h.ProvisionOutput(o.Info, o.Type, msg) + h.ProvisionOutput(o.InstanceAddr, o.ProvisionerType, msg) } } diff --git a/terraform/ui_output_provisioner_test.go b/terraform/ui_output_provisioner_test.go index dc1d00c21..b01f0a30b 100644 --- a/terraform/ui_output_provisioner_test.go +++ b/terraform/ui_output_provisioner_test.go @@ -2,6 +2,8 @@ package terraform import ( "testing" + + "github.com/hashicorp/terraform/addrs" ) func TestProvisionerUIOutput_impl(t *testing.T) { @@ -11,20 +13,24 @@ func TestProvisionerUIOutput_impl(t *testing.T) { func TestProvisionerUIOutputOutput(t *testing.T) { hook := new(MockHook) output := &ProvisionerUIOutput{ - Info: nil, - Type: "foo", - Hooks: []Hook{hook}, + InstanceAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_thing", + Name: "test", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProvisionerType: "foo", + Hooks: []Hook{hook}, } output.Output("bar") if !hook.ProvisionOutputCalled { - t.Fatal("should be called") + t.Fatal("hook.ProvisionOutput was not called, and should've been") } - if hook.ProvisionOutputProvisionerId != "foo" { - t.Fatalf("bad: %#v", hook.ProvisionOutputProvisionerId) + if got, want := hook.ProvisionOutputProvisionerType, "foo"; got != want { + t.Fatalf("wrong provisioner type\ngot: %q\nwant: %q", got, want) } - if hook.ProvisionOutputMessage != "bar" { - t.Fatalf("bad: %#v", hook.ProvisionOutputMessage) + if got, want := hook.ProvisionOutputMessage, "bar"; got != want { + t.Fatalf("wrong output message\ngot: %q\nwant: %q", got, want) } }