github.com/hugorut/terraform@v1.1.3/src/command/views/operation.go (about) 1 package views 2 3 import ( 4 "bytes" 5 "fmt" 6 "strings" 7 8 "github.com/hugorut/terraform/src/addrs" 9 "github.com/hugorut/terraform/src/command/arguments" 10 "github.com/hugorut/terraform/src/command/format" 11 "github.com/hugorut/terraform/src/command/views/json" 12 "github.com/hugorut/terraform/src/plans" 13 "github.com/hugorut/terraform/src/states/statefile" 14 "github.com/hugorut/terraform/src/terraform" 15 "github.com/hugorut/terraform/src/tfdiags" 16 ) 17 18 type Operation interface { 19 Interrupted() 20 FatalInterrupt() 21 Stopping() 22 Cancelled(planMode plans.Mode) 23 24 EmergencyDumpState(stateFile *statefile.File) error 25 26 PlannedChange(change *plans.ResourceInstanceChangeSrc) 27 Plan(plan *plans.Plan, schemas *terraform.Schemas) 28 PlanNextStep(planPath string) 29 30 Diagnostics(diags tfdiags.Diagnostics) 31 } 32 33 func NewOperation(vt arguments.ViewType, inAutomation bool, view *View) Operation { 34 switch vt { 35 case arguments.ViewHuman: 36 return &OperationHuman{view: view, inAutomation: inAutomation} 37 default: 38 panic(fmt.Sprintf("unknown view type %v", vt)) 39 } 40 } 41 42 type OperationHuman struct { 43 view *View 44 45 // inAutomation indicates that commands are being run by an 46 // automated system rather than directly at a command prompt. 47 // 48 // This is a hint not to produce messages that expect that a user can 49 // run a follow-up command, perhaps because Terraform is running in 50 // some sort of workflow automation tool that abstracts away the 51 // exact commands that are being run. 52 inAutomation bool 53 } 54 55 var _ Operation = (*OperationHuman)(nil) 56 57 func (v *OperationHuman) Interrupted() { 58 v.view.streams.Println(format.WordWrap(interrupted, v.view.outputColumns())) 59 } 60 61 func (v *OperationHuman) FatalInterrupt() { 62 v.view.streams.Eprintln(format.WordWrap(fatalInterrupt, v.view.errorColumns())) 63 } 64 65 func (v *OperationHuman) Stopping() { 66 v.view.streams.Println("Stopping operation...") 67 } 68 69 func (v *OperationHuman) Cancelled(planMode plans.Mode) { 70 switch planMode { 71 case plans.DestroyMode: 72 v.view.streams.Println("Destroy cancelled.") 73 default: 74 v.view.streams.Println("Apply cancelled.") 75 } 76 } 77 78 func (v *OperationHuman) EmergencyDumpState(stateFile *statefile.File) error { 79 stateBuf := new(bytes.Buffer) 80 jsonErr := statefile.Write(stateFile, stateBuf) 81 if jsonErr != nil { 82 return jsonErr 83 } 84 v.view.streams.Eprintln(stateBuf) 85 return nil 86 } 87 88 func (v *OperationHuman) Plan(plan *plans.Plan, schemas *terraform.Schemas) { 89 renderPlan(plan, schemas, v.view) 90 } 91 92 func (v *OperationHuman) PlannedChange(change *plans.ResourceInstanceChangeSrc) { 93 // PlannedChange is primarily for machine-readable output in order to 94 // get a per-resource-instance change description. We don't use it 95 // with OperationHuman because the output of Plan already includes the 96 // change details for all resource instances. 97 } 98 99 // PlanNextStep gives the user some next-steps, unless we're running in an 100 // automation tool which is presumed to provide its own UI for further actions. 101 func (v *OperationHuman) PlanNextStep(planPath string) { 102 if v.inAutomation { 103 return 104 } 105 v.view.outputHorizRule() 106 107 if planPath == "" { 108 v.view.streams.Print( 109 "\n" + strings.TrimSpace(format.WordWrap(planHeaderNoOutput, v.view.outputColumns())) + "\n", 110 ) 111 } else { 112 v.view.streams.Printf( 113 "\n"+strings.TrimSpace(format.WordWrap(planHeaderYesOutput, v.view.outputColumns()))+"\n", 114 planPath, planPath, 115 ) 116 } 117 } 118 119 func (v *OperationHuman) Diagnostics(diags tfdiags.Diagnostics) { 120 v.view.Diagnostics(diags) 121 } 122 123 type OperationJSON struct { 124 view *JSONView 125 } 126 127 var _ Operation = (*OperationJSON)(nil) 128 129 func (v *OperationJSON) Interrupted() { 130 v.view.Log(interrupted) 131 } 132 133 func (v *OperationJSON) FatalInterrupt() { 134 v.view.Log(fatalInterrupt) 135 } 136 137 func (v *OperationJSON) Stopping() { 138 v.view.Log("Stopping operation...") 139 } 140 141 func (v *OperationJSON) Cancelled(planMode plans.Mode) { 142 switch planMode { 143 case plans.DestroyMode: 144 v.view.Log("Destroy cancelled") 145 default: 146 v.view.Log("Apply cancelled") 147 } 148 } 149 150 func (v *OperationJSON) EmergencyDumpState(stateFile *statefile.File) error { 151 stateBuf := new(bytes.Buffer) 152 jsonErr := statefile.Write(stateFile, stateBuf) 153 if jsonErr != nil { 154 return jsonErr 155 } 156 v.view.StateDump(stateBuf.String()) 157 return nil 158 } 159 160 // Log a change summary and a series of "planned" messages for the changes in 161 // the plan. 162 func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) { 163 for _, dr := range plan.DriftedResources { 164 // In refresh-only mode, we output all resources marked as drifted, 165 // including those which have moved without other changes. In other plan 166 // modes, move-only changes will be included in the planned changes, so 167 // we skip them here. 168 if dr.Action != plans.NoOp || plan.UIMode == plans.RefreshOnlyMode { 169 v.view.ResourceDrift(json.NewResourceInstanceChange(dr)) 170 } 171 } 172 173 cs := &json.ChangeSummary{ 174 Operation: json.OperationPlanned, 175 } 176 for _, change := range plan.Changes.Resources { 177 if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode { 178 // Avoid rendering data sources on deletion 179 continue 180 } 181 switch change.Action { 182 case plans.Create: 183 cs.Add++ 184 case plans.Delete: 185 cs.Remove++ 186 case plans.Update: 187 cs.Change++ 188 case plans.CreateThenDelete, plans.DeleteThenCreate: 189 cs.Add++ 190 cs.Remove++ 191 } 192 193 if change.Action != plans.NoOp || !change.Addr.Equal(change.PrevRunAddr) { 194 v.view.PlannedChange(json.NewResourceInstanceChange(change)) 195 } 196 } 197 198 v.view.ChangeSummary(cs) 199 200 var rootModuleOutputs []*plans.OutputChangeSrc 201 for _, output := range plan.Changes.Outputs { 202 if !output.Addr.Module.IsRoot() { 203 continue 204 } 205 rootModuleOutputs = append(rootModuleOutputs, output) 206 } 207 if len(rootModuleOutputs) > 0 { 208 v.view.Outputs(json.OutputsFromChanges(rootModuleOutputs)) 209 } 210 } 211 212 func (v *OperationJSON) PlannedChange(change *plans.ResourceInstanceChangeSrc) { 213 if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode { 214 // Avoid rendering data sources on deletion 215 return 216 } 217 v.view.PlannedChange(json.NewResourceInstanceChange(change)) 218 } 219 220 // PlanNextStep does nothing for the JSON view as it is a hook for user-facing 221 // output only applicable to human-readable UI. 222 func (v *OperationJSON) PlanNextStep(planPath string) { 223 } 224 225 func (v *OperationJSON) Diagnostics(diags tfdiags.Diagnostics) { 226 v.view.Diagnostics(diags) 227 } 228 229 const fatalInterrupt = ` 230 Two interrupts received. Exiting immediately. Note that data loss may have occurred. 231 ` 232 233 const interrupted = ` 234 Interrupt received. 235 Please wait for Terraform to exit or data loss may occur. 236 Gracefully shutting down... 237 ` 238 239 const planHeaderNoOutput = ` 240 Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now. 241 ` 242 243 const planHeaderYesOutput = ` 244 Saved the plan to: %s 245 246 To perform exactly these actions, run the following command to apply: 247 terraform apply %q 248 `