github.com/meteor/terraform@v0.6.15-0.20210412225145-79ec4bc057c6/backend/local/backend_apply.go (about) 1 package local 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "log" 8 "strings" 9 10 "github.com/hashicorp/errwrap" 11 "github.com/hashicorp/go-multierror" 12 "github.com/hashicorp/terraform/backend" 13 "github.com/hashicorp/terraform/command/clistate" 14 "github.com/hashicorp/terraform/command/format" 15 "github.com/hashicorp/terraform/config/module" 16 "github.com/hashicorp/terraform/state" 17 "github.com/hashicorp/terraform/terraform" 18 ) 19 20 func (b *Local) opApply( 21 ctx context.Context, 22 op *backend.Operation, 23 runningOp *backend.RunningOperation) { 24 log.Printf("[INFO] backend/local: starting Apply operation") 25 26 // If we have a nil module at this point, then set it to an empty tree 27 // to avoid any potential crashes. 28 if op.Plan == nil && op.Module == nil && !op.Destroy { 29 runningOp.Err = fmt.Errorf(strings.TrimSpace(applyErrNoConfig)) 30 return 31 } 32 33 // If we have a nil module at this point, then set it to an empty tree 34 // to avoid any potential crashes. 35 if op.Module == nil { 36 op.Module = module.NewEmptyTree() 37 } 38 39 // Setup our count hook that keeps track of resource changes 40 countHook := new(CountHook) 41 stateHook := new(StateHook) 42 if b.ContextOpts == nil { 43 b.ContextOpts = new(terraform.ContextOpts) 44 } 45 old := b.ContextOpts.Hooks 46 defer func() { b.ContextOpts.Hooks = old }() 47 b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook, stateHook) 48 49 // Get our context 50 tfCtx, opState, err := b.context(op) 51 if err != nil { 52 runningOp.Err = err 53 return 54 } 55 56 if op.LockState { 57 lockCtx, cancel := context.WithTimeout(ctx, op.StateLockTimeout) 58 defer cancel() 59 60 lockInfo := state.NewLockInfo() 61 lockInfo.Operation = op.Type.String() 62 lockID, err := clistate.Lock(lockCtx, opState, lockInfo, b.CLI, b.Colorize()) 63 if err != nil { 64 runningOp.Err = errwrap.Wrapf("Error locking state: {{err}}", err) 65 return 66 } 67 68 defer func() { 69 if err := clistate.Unlock(opState, lockID, b.CLI, b.Colorize()); err != nil { 70 runningOp.Err = multierror.Append(runningOp.Err, err) 71 } 72 }() 73 } 74 75 // Setup the state 76 runningOp.State = tfCtx.State() 77 78 // If we weren't given a plan, then we refresh/plan 79 if op.Plan == nil { 80 // If we're refreshing before apply, perform that 81 if op.PlanRefresh { 82 log.Printf("[INFO] backend/local: apply calling Refresh") 83 _, err := tfCtx.Refresh() 84 if err != nil { 85 runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err) 86 return 87 } 88 } 89 90 // Perform the plan 91 log.Printf("[INFO] backend/local: apply calling Plan") 92 plan, err := tfCtx.Plan() 93 if err != nil { 94 runningOp.Err = errwrap.Wrapf("Error running plan: {{err}}", err) 95 return 96 } 97 98 trivialPlan := plan.Diff == nil || plan.Diff.Empty() 99 if op.ConfirmPlan && !trivialPlan { 100 op.UIOut.Output(strings.TrimSpace(confirmPlanHeader) + "\n") 101 op.UIOut.Output(format.Plan(&format.PlanOpts{ 102 Plan: plan, 103 Color: b.Colorize(), 104 ModuleDepth: -1, 105 })) 106 desc := "Terraform will apply the plan described above.\n" + 107 "Only 'yes' will be accepted to confirm." 108 v, err := op.UIIn.Input(&terraform.InputOpts{ 109 Id: "confirm", 110 Query: "Do you want to apply the plan above?", 111 Description: desc, 112 }) 113 if err != nil { 114 runningOp.Err = errwrap.Wrapf("Error asking for confirmation: {{err}}", err) 115 return 116 } 117 if v != "yes" { 118 runningOp.Err = errors.New("Apply cancelled.") 119 return 120 } 121 } 122 } 123 124 // Setup our hook for continuous state updates 125 stateHook.State = opState 126 127 // Start the apply in a goroutine so that we can be interrupted. 128 var applyState *terraform.State 129 var applyErr error 130 doneCh := make(chan struct{}) 131 go func() { 132 defer close(doneCh) 133 _, applyErr = tfCtx.Apply() 134 // we always want the state, even if apply failed 135 applyState = tfCtx.State() 136 137 /* 138 // Record any shadow errors for later 139 if err := ctx.ShadowError(); err != nil { 140 shadowErr = multierror.Append(shadowErr, multierror.Prefix( 141 err, "apply operation:")) 142 } 143 */ 144 }() 145 146 // Wait for the apply to finish or for us to be interrupted so 147 // we can handle it properly. 148 err = nil 149 select { 150 case <-ctx.Done(): 151 if b.CLI != nil { 152 b.CLI.Output("stopping apply operation...") 153 } 154 155 // Stop execution 156 go tfCtx.Stop() 157 158 // Wait for completion still 159 <-doneCh 160 case <-doneCh: 161 } 162 163 // Store the final state 164 runningOp.State = applyState 165 166 // Persist the state 167 if err := opState.WriteState(applyState); err != nil { 168 runningOp.Err = fmt.Errorf("Failed to save state: %s", err) 169 return 170 } 171 if err := opState.PersistState(); err != nil { 172 runningOp.Err = fmt.Errorf("Failed to save state: %s", err) 173 return 174 } 175 176 if applyErr != nil { 177 runningOp.Err = fmt.Errorf( 178 "Error applying plan:\n\n"+ 179 "%s\n\n"+ 180 "Terraform does not automatically rollback in the face of errors.\n"+ 181 "Instead, your Terraform state file has been partially updated with\n"+ 182 "any resources that successfully completed. Please address the error\n"+ 183 "above and apply again to incrementally change your infrastructure.", 184 multierror.Flatten(applyErr)) 185 return 186 } 187 188 // If we have a UI, output the results 189 if b.CLI != nil { 190 if op.Destroy { 191 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 192 "[reset][bold][green]\n"+ 193 "Destroy complete! Resources: %d destroyed.", 194 countHook.Removed))) 195 } else { 196 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 197 "[reset][bold][green]\n"+ 198 "Apply complete! Resources: %d added, %d changed, %d destroyed.", 199 countHook.Added, 200 countHook.Changed, 201 countHook.Removed))) 202 } 203 204 if countHook.Added > 0 || countHook.Changed > 0 { 205 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 206 "[reset]\n"+ 207 "The state of your infrastructure has been saved to the path\n"+ 208 "below. This state is required to modify and destroy your\n"+ 209 "infrastructure, so keep it safe. To inspect the complete state\n"+ 210 "use the `terraform show` command.\n\n"+ 211 "State path: %s", 212 b.StateOutPath))) 213 } 214 } 215 } 216 217 const applyErrNoConfig = ` 218 No configuration files found! 219 220 Apply requires configuration to be present. Applying without a configuration 221 would mark everything for destruction, which is normally not what is desired. 222 If you would like to destroy everything, please run 'terraform destroy' instead 223 which does not require any configuration files. 224 ` 225 226 const confirmPlanHeader = ` 227 The Terraform execution plan has been generated and is shown below. 228 Resources are shown in alphabetical order for quick scanning. Green resources 229 will be created (or destroyed and then created if an existing resource 230 exists), yellow resources are being changed in-place, and red resources 231 will be destroyed. Cyan entries are data sources to be read. 232 `