1package views 2 3import ( 4 "bytes" 5 "fmt" 6 "sort" 7 "strings" 8 9 "github.com/hashicorp/terraform/internal/addrs" 10 "github.com/hashicorp/terraform/internal/command/arguments" 11 "github.com/hashicorp/terraform/internal/command/format" 12 "github.com/hashicorp/terraform/internal/command/views/json" 13 "github.com/hashicorp/terraform/internal/plans" 14 "github.com/hashicorp/terraform/internal/states" 15 "github.com/hashicorp/terraform/internal/states/statefile" 16 "github.com/hashicorp/terraform/internal/terraform" 17 "github.com/hashicorp/terraform/internal/tfdiags" 18 "github.com/zclconf/go-cty/cty" 19) 20 21type 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 36func 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 45type 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 58var _ Operation = (*OperationHuman)(nil) 59 60func (v *OperationHuman) Interrupted() { 61 v.view.streams.Println(format.WordWrap(interrupted, v.view.outputColumns())) 62} 63 64func (v *OperationHuman) FatalInterrupt() { 65 v.view.streams.Eprintln(format.WordWrap(fatalInterrupt, v.view.errorColumns())) 66} 67 68func (v *OperationHuman) Stopping() { 69 v.view.streams.Println("Stopping operation...") 70} 71 72func (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 81func (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 91func (v *OperationHuman) Plan(plan *plans.Plan, schemas *terraform.Schemas) { 92 renderPlan(plan, schemas, v.view) 93} 94 95func (v *OperationHuman) PlannedChange(change *plans.ResourceInstanceChangeSrc) { 96 // PlannedChange is primarily for machine-readable output in order to 97 // get a per-resource-instance change description. We don't use it 98 // with OperationHuman because the output of Plan already includes the 99 // change details for all resource instances. 100} 101 102// PlanNextStep gives the user some next-steps, unless we're running in an 103// automation tool which is presumed to provide its own UI for further actions. 104func (v *OperationHuman) PlanNextStep(planPath string) { 105 if v.inAutomation { 106 return 107 } 108 v.view.outputHorizRule() 109 110 if planPath == "" { 111 v.view.streams.Print( 112 "\n" + strings.TrimSpace(format.WordWrap(planHeaderNoOutput, v.view.outputColumns())) + "\n", 113 ) 114 } else { 115 v.view.streams.Printf( 116 "\n"+strings.TrimSpace(format.WordWrap(planHeaderYesOutput, v.view.outputColumns()))+"\n", 117 planPath, planPath, 118 ) 119 } 120} 121 122func (v *OperationHuman) Diagnostics(diags tfdiags.Diagnostics) { 123 v.view.Diagnostics(diags) 124} 125 126type OperationJSON struct { 127 view *JSONView 128} 129 130var _ Operation = (*OperationJSON)(nil) 131 132func (v *OperationJSON) Interrupted() { 133 v.view.Log(interrupted) 134} 135 136func (v *OperationJSON) FatalInterrupt() { 137 v.view.Log(fatalInterrupt) 138} 139 140func (v *OperationJSON) Stopping() { 141 v.view.Log("Stopping operation...") 142} 143 144func (v *OperationJSON) Cancelled(planMode plans.Mode) { 145 switch planMode { 146 case plans.DestroyMode: 147 v.view.Log("Destroy cancelled") 148 default: 149 v.view.Log("Apply cancelled") 150 } 151} 152 153func (v *OperationJSON) EmergencyDumpState(stateFile *statefile.File) error { 154 stateBuf := new(bytes.Buffer) 155 jsonErr := statefile.Write(stateFile, stateBuf) 156 if jsonErr != nil { 157 return jsonErr 158 } 159 v.view.StateDump(stateBuf.String()) 160 return nil 161} 162 163// Log a change summary and a series of "planned" messages for the changes in 164// the plan. 165func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) { 166 if err := v.resourceDrift(plan.PrevRunState, plan.PriorState, schemas); err != nil { 167 var diags tfdiags.Diagnostics 168 diags = diags.Append(err) 169 v.Diagnostics(diags) 170 } 171 172 cs := &json.ChangeSummary{ 173 Operation: json.OperationPlanned, 174 } 175 for _, change := range plan.Changes.Resources { 176 if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode { 177 // Avoid rendering data sources on deletion 178 continue 179 } 180 switch change.Action { 181 case plans.Create: 182 cs.Add++ 183 case plans.Delete: 184 cs.Remove++ 185 case plans.Update: 186 cs.Change++ 187 case plans.CreateThenDelete, plans.DeleteThenCreate: 188 cs.Add++ 189 cs.Remove++ 190 } 191 192 if change.Action != plans.NoOp { 193 v.view.PlannedChange(json.NewResourceInstanceChange(change)) 194 } 195 } 196 197 v.view.ChangeSummary(cs) 198 199 var rootModuleOutputs []*plans.OutputChangeSrc 200 for _, output := range plan.Changes.Outputs { 201 if !output.Addr.Module.IsRoot() { 202 continue 203 } 204 rootModuleOutputs = append(rootModuleOutputs, output) 205 } 206 if len(rootModuleOutputs) > 0 { 207 v.view.Outputs(json.OutputsFromChanges(rootModuleOutputs)) 208 } 209} 210 211func (v *OperationJSON) resourceDrift(oldState, newState *states.State, schemas *terraform.Schemas) error { 212 if newState.ManagedResourcesEqual(oldState) { 213 // Nothing to do, because we only detect and report drift for managed 214 // resource instances. 215 return nil 216 } 217 var changes []*json.ResourceInstanceChange 218 for _, ms := range oldState.Modules { 219 for _, rs := range ms.Resources { 220 if rs.Addr.Resource.Mode != addrs.ManagedResourceMode { 221 // Drift reporting is only for managed resources 222 continue 223 } 224 225 provider := rs.ProviderConfig.Provider 226 for key, oldIS := range rs.Instances { 227 if oldIS.Current == nil { 228 // Not interested in instances that only have deposed objects 229 continue 230 } 231 addr := rs.Addr.Instance(key) 232 newIS := newState.ResourceInstance(addr) 233 234 schema, _ := schemas.ResourceTypeConfig( 235 provider, 236 addr.Resource.Resource.Mode, 237 addr.Resource.Resource.Type, 238 ) 239 if schema == nil { 240 return fmt.Errorf("no schema found for %s (in provider %s)", addr, provider) 241 } 242 ty := schema.ImpliedType() 243 244 oldObj, err := oldIS.Current.Decode(ty) 245 if err != nil { 246 return fmt.Errorf("failed to decode previous run data for %s: %s", addr, err) 247 } 248 249 var newObj *states.ResourceInstanceObject 250 if newIS != nil && newIS.Current != nil { 251 newObj, err = newIS.Current.Decode(ty) 252 if err != nil { 253 return fmt.Errorf("failed to decode refreshed data for %s: %s", addr, err) 254 } 255 } 256 257 var oldVal, newVal cty.Value 258 oldVal = oldObj.Value 259 if newObj != nil { 260 newVal = newObj.Value 261 } else { 262 newVal = cty.NullVal(ty) 263 } 264 265 if oldVal.RawEquals(newVal) { 266 // No drift if the two values are semantically equivalent 267 continue 268 } 269 270 // We can only detect updates and deletes as drift. 271 action := plans.Update 272 if newVal.IsNull() { 273 action = plans.Delete 274 } 275 276 change := &plans.ResourceInstanceChangeSrc{ 277 Addr: addr, 278 ChangeSrc: plans.ChangeSrc{ 279 Action: action, 280 }, 281 } 282 changes = append(changes, json.NewResourceInstanceChange(change)) 283 } 284 } 285 } 286 287 // Sort the change structs lexically by address to give stable output 288 sort.Slice(changes, func(i, j int) bool { return changes[i].Resource.Addr < changes[j].Resource.Addr }) 289 290 for _, change := range changes { 291 v.view.ResourceDrift(change) 292 } 293 294 return nil 295} 296 297func (v *OperationJSON) PlannedChange(change *plans.ResourceInstanceChangeSrc) { 298 if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode { 299 // Avoid rendering data sources on deletion 300 return 301 } 302 v.view.PlannedChange(json.NewResourceInstanceChange(change)) 303} 304 305// PlanNextStep does nothing for the JSON view as it is a hook for user-facing 306// output only applicable to human-readable UI. 307func (v *OperationJSON) PlanNextStep(planPath string) { 308} 309 310func (v *OperationJSON) Diagnostics(diags tfdiags.Diagnostics) { 311 v.view.Diagnostics(diags) 312} 313 314const fatalInterrupt = ` 315Two interrupts received. Exiting immediately. Note that data loss may have occurred. 316` 317 318const interrupted = ` 319Interrupt received. 320Please wait for Terraform to exit or data loss may occur. 321Gracefully shutting down... 322` 323 324const planHeaderNoOutput = ` 325Note: 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. 326` 327 328const planHeaderYesOutput = ` 329Saved the plan to: %s 330 331To perform exactly these actions, run the following command to apply: 332 terraform apply %q 333` 334