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