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) } }