github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/command/views/operation.go (about) 1 package views 2 3 import ( 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 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 renderPlan(plan, schemas, v.view) 93 } 94 95 func (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. 104 func (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 122 func (v *OperationHuman) Diagnostics(diags tfdiags.Diagnostics) { 123 v.view.Diagnostics(diags) 124 } 125 126 type OperationJSON struct { 127 view *JSONView 128 } 129 130 var _ Operation = (*OperationJSON)(nil) 131 132 func (v *OperationJSON) Interrupted() { 133 v.view.Log(interrupted) 134 } 135 136 func (v *OperationJSON) FatalInterrupt() { 137 v.view.Log(fatalInterrupt) 138 } 139 140 func (v *OperationJSON) Stopping() { 141 v.view.Log("Stopping operation...") 142 } 143 144 func (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 153 func (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. 165 func (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 211 func (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 297 func (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. 307 func (v *OperationJSON) PlanNextStep(planPath string) { 308 } 309 310 func (v *OperationJSON) Diagnostics(diags tfdiags.Diagnostics) { 311 v.view.Diagnostics(diags) 312 } 313 314 const fatalInterrupt = ` 315 Two interrupts received. Exiting immediately. Note that data loss may have occurred. 316 ` 317 318 const interrupted = ` 319 Interrupt received. 320 Please wait for Terraform to exit or data loss may occur. 321 Gracefully shutting down... 322 ` 323 324 const planHeaderNoOutput = ` 325 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. 326 ` 327 328 const planHeaderYesOutput = ` 329 Saved the plan to: %s 330 331 To perform exactly these actions, run the following command to apply: 332 terraform apply %q 333 `