192 lines
5.2 KiB
Go
192 lines
5.2 KiB
Go
package local
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/errwrap"
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/hashicorp/terraform/backend"
|
|
clistate "github.com/hashicorp/terraform/command/state"
|
|
"github.com/hashicorp/terraform/config/module"
|
|
"github.com/hashicorp/terraform/state"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
)
|
|
|
|
func (b *Local) opApply(
|
|
ctx context.Context,
|
|
op *backend.Operation,
|
|
runningOp *backend.RunningOperation) {
|
|
log.Printf("[INFO] backend/local: starting Apply operation")
|
|
|
|
// If we have a nil module at this point, then set it to an empty tree
|
|
// to avoid any potential crashes.
|
|
if op.Plan == nil && op.Module == nil && !op.Destroy {
|
|
runningOp.Err = fmt.Errorf(strings.TrimSpace(applyErrNoConfig))
|
|
return
|
|
}
|
|
|
|
// If we have a nil module at this point, then set it to an empty tree
|
|
// to avoid any potential crashes.
|
|
if op.Module == nil {
|
|
op.Module = module.NewEmptyTree()
|
|
}
|
|
|
|
// Setup our count hook that keeps track of resource changes
|
|
countHook := new(CountHook)
|
|
stateHook := new(StateHook)
|
|
if b.ContextOpts == nil {
|
|
b.ContextOpts = new(terraform.ContextOpts)
|
|
}
|
|
old := b.ContextOpts.Hooks
|
|
defer func() { b.ContextOpts.Hooks = old }()
|
|
b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook, stateHook)
|
|
|
|
// Get our context
|
|
tfCtx, opState, err := b.context(op)
|
|
if err != nil {
|
|
runningOp.Err = err
|
|
return
|
|
}
|
|
|
|
if op.LockState {
|
|
lockInfo := state.NewLockInfo()
|
|
lockInfo.Operation = op.Type.String()
|
|
lockID, err := clistate.Lock(opState, lockInfo, b.CLI, b.Colorize())
|
|
if err != nil {
|
|
runningOp.Err = errwrap.Wrapf("Error locking state: {{err}}", err)
|
|
return
|
|
}
|
|
|
|
defer func() {
|
|
if err := clistate.Unlock(opState, lockID, b.CLI, b.Colorize()); err != nil {
|
|
runningOp.Err = multierror.Append(runningOp.Err, err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Setup the state
|
|
runningOp.State = tfCtx.State()
|
|
|
|
// If we weren't given a plan, then we refresh/plan
|
|
if op.Plan == nil {
|
|
// If we're refreshing before apply, perform that
|
|
if op.PlanRefresh {
|
|
log.Printf("[INFO] backend/local: apply calling Refresh")
|
|
_, err := tfCtx.Refresh()
|
|
if err != nil {
|
|
runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Perform the plan
|
|
log.Printf("[INFO] backend/local: apply calling Plan")
|
|
if _, err := tfCtx.Plan(); err != nil {
|
|
runningOp.Err = errwrap.Wrapf("Error running plan: {{err}}", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Setup our hook for continuous state updates
|
|
stateHook.State = opState
|
|
|
|
// Start the apply in a goroutine so that we can be interrupted.
|
|
var applyState *terraform.State
|
|
var applyErr error
|
|
doneCh := make(chan struct{})
|
|
go func() {
|
|
defer close(doneCh)
|
|
applyState, applyErr = tfCtx.Apply()
|
|
|
|
/*
|
|
// Record any shadow errors for later
|
|
if err := ctx.ShadowError(); err != nil {
|
|
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
|
|
err, "apply operation:"))
|
|
}
|
|
*/
|
|
}()
|
|
|
|
// Wait for the apply to finish or for us to be interrupted so
|
|
// we can handle it properly.
|
|
err = nil
|
|
select {
|
|
case <-ctx.Done():
|
|
if b.CLI != nil {
|
|
b.CLI.Output("Interrupt received. Gracefully shutting down...")
|
|
}
|
|
|
|
// Stop execution
|
|
go tfCtx.Stop()
|
|
|
|
// Wait for completion still
|
|
<-doneCh
|
|
case <-doneCh:
|
|
}
|
|
|
|
// Store the final state
|
|
runningOp.State = applyState
|
|
|
|
// Persist the state
|
|
if err := opState.WriteState(applyState); err != nil {
|
|
runningOp.Err = fmt.Errorf("Failed to save state: %s", err)
|
|
return
|
|
}
|
|
if err := opState.PersistState(); err != nil {
|
|
runningOp.Err = fmt.Errorf("Failed to save state: %s", err)
|
|
return
|
|
}
|
|
|
|
if applyErr != nil {
|
|
runningOp.Err = fmt.Errorf(
|
|
"Error applying plan:\n\n"+
|
|
"%s\n\n"+
|
|
"Terraform does not automatically rollback in the face of errors.\n"+
|
|
"Instead, your Terraform state file has been partially updated with\n"+
|
|
"any resources that successfully completed. Please address the error\n"+
|
|
"above and apply again to incrementally change your infrastructure.",
|
|
multierror.Flatten(applyErr))
|
|
return
|
|
}
|
|
|
|
// If we have a UI, output the results
|
|
if b.CLI != nil {
|
|
if op.Destroy {
|
|
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
|
|
"[reset][bold][green]\n"+
|
|
"Destroy complete! Resources: %d destroyed.",
|
|
countHook.Removed)))
|
|
} else {
|
|
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
|
|
"[reset][bold][green]\n"+
|
|
"Apply complete! Resources: %d added, %d changed, %d destroyed.",
|
|
countHook.Added,
|
|
countHook.Changed,
|
|
countHook.Removed)))
|
|
}
|
|
|
|
if countHook.Added > 0 || countHook.Changed > 0 {
|
|
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
|
|
"[reset]\n"+
|
|
"The state of your infrastructure has been saved to the path\n"+
|
|
"below. This state is required to modify and destroy your\n"+
|
|
"infrastructure, so keep it safe. To inspect the complete state\n"+
|
|
"use the `terraform show` command.\n\n"+
|
|
"State path: %s",
|
|
b.StateOutPath)))
|
|
}
|
|
}
|
|
}
|
|
|
|
const applyErrNoConfig = `
|
|
No configuration files found!
|
|
|
|
Apply requires configuration to be present. Applying without a configuration
|
|
would mark everything for destruction, which is normally not what is desired.
|
|
If you would like to destroy everything, please run 'terraform destroy' instead
|
|
which does not require any configuration files.
|
|
`
|