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