github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/command/views/plan.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/plans" 13 "github.com/hashicorp/terraform/internal/states" 14 "github.com/hashicorp/terraform/internal/terraform" 15 "github.com/hashicorp/terraform/internal/tfdiags" 16 ) 17 18 // The Plan view is used for the plan command. 19 type Plan interface { 20 Operation() Operation 21 Hooks() []terraform.Hook 22 23 Diagnostics(diags tfdiags.Diagnostics) 24 HelpPrompt() 25 } 26 27 // NewPlan returns an initialized Plan implementation for the given ViewType. 28 func NewPlan(vt arguments.ViewType, view *View) Plan { 29 switch vt { 30 case arguments.ViewJSON: 31 return &PlanJSON{ 32 view: NewJSONView(view), 33 } 34 case arguments.ViewHuman: 35 return &PlanHuman{ 36 view: view, 37 inAutomation: view.RunningInAutomation(), 38 } 39 default: 40 panic(fmt.Sprintf("unknown view type %v", vt)) 41 } 42 } 43 44 // The PlanHuman implementation renders human-readable text logs, suitable for 45 // a scrolling terminal. 46 type PlanHuman struct { 47 view *View 48 49 inAutomation bool 50 } 51 52 var _ Plan = (*PlanHuman)(nil) 53 54 func (v *PlanHuman) Operation() Operation { 55 return NewOperation(arguments.ViewHuman, v.inAutomation, v.view) 56 } 57 58 func (v *PlanHuman) Hooks() []terraform.Hook { 59 return []terraform.Hook{ 60 NewUiHook(v.view), 61 } 62 } 63 64 func (v *PlanHuman) Diagnostics(diags tfdiags.Diagnostics) { 65 v.view.Diagnostics(diags) 66 } 67 68 func (v *PlanHuman) HelpPrompt() { 69 v.view.HelpPrompt("plan") 70 } 71 72 // The PlanJSON implementation renders streaming JSON logs, suitable for 73 // integrating with other software. 74 type PlanJSON struct { 75 view *JSONView 76 } 77 78 var _ Plan = (*PlanJSON)(nil) 79 80 func (v *PlanJSON) Operation() Operation { 81 return &OperationJSON{view: v.view} 82 } 83 84 func (v *PlanJSON) Hooks() []terraform.Hook { 85 return []terraform.Hook{ 86 newJSONHook(v.view), 87 } 88 } 89 90 func (v *PlanJSON) Diagnostics(diags tfdiags.Diagnostics) { 91 v.view.Diagnostics(diags) 92 } 93 94 func (v *PlanJSON) HelpPrompt() { 95 } 96 97 // The plan renderer is used by the Operation view (for plan and apply 98 // commands) and the Show view (for the show command). 99 func renderPlan(plan *plans.Plan, schemas *terraform.Schemas, view *View) { 100 haveRefreshChanges := renderChangesDetectedByRefresh(plan.PrevRunState, plan.PriorState, schemas, view) 101 if haveRefreshChanges { 102 switch plan.UIMode { 103 case plans.RefreshOnlyMode: 104 view.streams.Println(format.WordWrap( 105 "\nThis is a refresh-only plan, so Terraform will not take any actions to undo these. If you were expecting these changes then you can apply this plan to record the updated values in the Terraform state without changing any remote objects.", 106 view.outputColumns(), 107 )) 108 default: 109 view.streams.Println(format.WordWrap( 110 "\nUnless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes.", 111 view.outputColumns(), 112 )) 113 } 114 } 115 116 counts := map[plans.Action]int{} 117 var rChanges []*plans.ResourceInstanceChangeSrc 118 for _, change := range plan.Changes.Resources { 119 if change.Action == plans.NoOp && !change.Moved() { 120 continue // We don't show anything for no-op changes 121 } 122 if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode { 123 // Avoid rendering data sources on deletion 124 continue 125 } 126 127 rChanges = append(rChanges, change) 128 129 // Don't count move-only changes 130 if change.Action != plans.NoOp { 131 counts[change.Action]++ 132 } 133 } 134 var changedRootModuleOutputs []*plans.OutputChangeSrc 135 for _, output := range plan.Changes.Outputs { 136 if !output.Addr.Module.IsRoot() { 137 continue 138 } 139 if output.ChangeSrc.Action == plans.NoOp { 140 continue 141 } 142 changedRootModuleOutputs = append(changedRootModuleOutputs, output) 143 } 144 145 if len(rChanges) == 0 && len(changedRootModuleOutputs) == 0 { 146 // If we didn't find any changes to report at all then this is a 147 // "No changes" plan. How we'll present this depends on whether 148 // the plan is "applyable" and, if so, whether it had refresh changes 149 // that we already would've presented above. 150 151 switch plan.UIMode { 152 case plans.RefreshOnlyMode: 153 if haveRefreshChanges { 154 // We already generated a sufficient prompt about what will 155 // happen if applying this change above, so we don't need to 156 // say anything more. 157 return 158 } 159 160 view.streams.Print( 161 view.colorize.Color("\n[reset][bold][green]No changes.[reset][bold] Your infrastructure still matches the configuration.[reset]\n\n"), 162 ) 163 view.streams.Println(format.WordWrap( 164 "Terraform has checked that the real remote objects still match the result of your most recent changes, and found no differences.", 165 view.outputColumns(), 166 )) 167 168 case plans.DestroyMode: 169 if haveRefreshChanges { 170 view.streams.Print(format.HorizontalRule(view.colorize, view.outputColumns())) 171 view.streams.Println("") 172 } 173 view.streams.Print( 174 view.colorize.Color("\n[reset][bold][green]No changes.[reset][bold] No objects need to be destroyed.[reset]\n\n"), 175 ) 176 view.streams.Println(format.WordWrap( 177 "Either you have not created any objects yet or the existing objects were already deleted outside of Terraform.", 178 view.outputColumns(), 179 )) 180 181 default: 182 if haveRefreshChanges { 183 view.streams.Print(format.HorizontalRule(view.colorize, view.outputColumns())) 184 view.streams.Println("") 185 } 186 view.streams.Print( 187 view.colorize.Color("\n[reset][bold][green]No changes.[reset][bold] Your infrastructure matches the configuration.[reset]\n\n"), 188 ) 189 190 if haveRefreshChanges && !plan.CanApply() { 191 if plan.CanApply() { 192 // In this case, applying this plan will not change any 193 // remote objects but _will_ update the state to match what 194 // we detected during refresh, so we'll reassure the user 195 // about that. 196 view.streams.Println(format.WordWrap( 197 "Your configuration already matches the changes detected above, so applying this plan will only update the state to include the changes detected above and won't change any real infrastructure.", 198 view.outputColumns(), 199 )) 200 } else { 201 // In this case we detected changes during refresh but this isn't 202 // a planning mode where we consider those to be applyable. The 203 // user must re-run in refresh-only mode in order to update the 204 // state to match the upstream changes. 205 suggestion := "." 206 if !view.runningInAutomation { 207 // The normal message includes a specific command line to run. 208 suggestion = ":\n terraform apply -refresh-only" 209 } 210 view.streams.Println(format.WordWrap( 211 "Your configuration already matches the changes detected above. If you'd like to update the Terraform state to match, create and apply a refresh-only plan"+suggestion, 212 view.outputColumns(), 213 )) 214 } 215 return 216 } 217 218 // If we get down here then we're just in the simple situation where 219 // the plan isn't applyable at all. 220 view.streams.Println(format.WordWrap( 221 "Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.", 222 view.outputColumns(), 223 )) 224 } 225 return 226 } 227 if haveRefreshChanges { 228 view.streams.Print(format.HorizontalRule(view.colorize, view.outputColumns())) 229 view.streams.Println("") 230 } 231 232 if len(counts) > 0 { 233 headerBuf := &bytes.Buffer{} 234 fmt.Fprintf(headerBuf, "\n%s\n", strings.TrimSpace(format.WordWrap(planHeaderIntro, view.outputColumns()))) 235 if counts[plans.Create] > 0 { 236 fmt.Fprintf(headerBuf, "%s create\n", format.DiffActionSymbol(plans.Create)) 237 } 238 if counts[plans.Update] > 0 { 239 fmt.Fprintf(headerBuf, "%s update in-place\n", format.DiffActionSymbol(plans.Update)) 240 } 241 if counts[plans.Delete] > 0 { 242 fmt.Fprintf(headerBuf, "%s destroy\n", format.DiffActionSymbol(plans.Delete)) 243 } 244 if counts[plans.DeleteThenCreate] > 0 { 245 fmt.Fprintf(headerBuf, "%s destroy and then create replacement\n", format.DiffActionSymbol(plans.DeleteThenCreate)) 246 } 247 if counts[plans.CreateThenDelete] > 0 { 248 fmt.Fprintf(headerBuf, "%s create replacement and then destroy\n", format.DiffActionSymbol(plans.CreateThenDelete)) 249 } 250 if counts[plans.Read] > 0 { 251 fmt.Fprintf(headerBuf, "%s read (data resources)\n", format.DiffActionSymbol(plans.Read)) 252 } 253 254 view.streams.Print(view.colorize.Color(headerBuf.String())) 255 } 256 257 if len(rChanges) > 0 { 258 view.streams.Printf("\nTerraform will perform the following actions:\n\n") 259 260 // Note: we're modifying the backing slice of this plan object in-place 261 // here. The ordering of resource changes in a plan is not significant, 262 // but we can only do this safely here because we can assume that nobody 263 // is concurrently modifying our changes while we're trying to print it. 264 sort.Slice(rChanges, func(i, j int) bool { 265 iA := rChanges[i].Addr 266 jA := rChanges[j].Addr 267 if iA.String() == jA.String() { 268 return rChanges[i].DeposedKey < rChanges[j].DeposedKey 269 } 270 return iA.Less(jA) 271 }) 272 273 for _, rcs := range rChanges { 274 if rcs.Action == plans.NoOp && !rcs.Moved() { 275 continue 276 } 277 278 providerSchema := schemas.ProviderSchema(rcs.ProviderAddr.Provider) 279 if providerSchema == nil { 280 // Should never happen 281 view.streams.Printf("(schema missing for %s)\n\n", rcs.ProviderAddr) 282 continue 283 } 284 rSchema, _ := providerSchema.SchemaForResourceAddr(rcs.Addr.Resource.Resource) 285 if rSchema == nil { 286 // Should never happen 287 view.streams.Printf("(schema missing for %s)\n\n", rcs.Addr) 288 continue 289 } 290 291 view.streams.Println(format.ResourceChange( 292 rcs, 293 rSchema, 294 view.colorize, 295 )) 296 } 297 298 // stats is similar to counts above, but: 299 // - it considers only resource changes 300 // - it simplifies "replace" into both a create and a delete 301 stats := map[plans.Action]int{} 302 for _, change := range rChanges { 303 switch change.Action { 304 case plans.CreateThenDelete, plans.DeleteThenCreate: 305 stats[plans.Create]++ 306 stats[plans.Delete]++ 307 default: 308 stats[change.Action]++ 309 } 310 } 311 view.streams.Printf( 312 view.colorize.Color("[reset][bold]Plan:[reset] %d to add, %d to change, %d to destroy.\n"), 313 stats[plans.Create], stats[plans.Update], stats[plans.Delete], 314 ) 315 } 316 317 // If there is at least one planned change to the root module outputs 318 // then we'll render a summary of those too. 319 if len(changedRootModuleOutputs) > 0 { 320 view.streams.Println( 321 view.colorize.Color("[reset]\n[bold]Changes to Outputs:[reset]") + 322 format.OutputChanges(changedRootModuleOutputs, view.colorize), 323 ) 324 325 if len(counts) == 0 { 326 // If we have output changes but not resource changes then we 327 // won't have output any indication about the changes at all yet, 328 // so we need some extra context about what it would mean to 329 // apply a change that _only_ includes output changes. 330 view.streams.Println(format.WordWrap( 331 "\nYou can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.", 332 view.outputColumns(), 333 )) 334 } 335 } 336 } 337 338 // renderChangesDetectedByRefresh is a part of renderPlan that generates 339 // the note about changes detected by refresh (sometimes considered as "drift"). 340 // 341 // It will only generate output if there's at least one difference detected. 342 // Otherwise, it will produce nothing at all. To help the caller recognize 343 // those two situations incase subsequent output must accommodate it, 344 // renderChangesDetectedByRefresh returns true if it produced at least one 345 // line of output, and guarantees to always produce whole lines terminated 346 // by newline characters. 347 func renderChangesDetectedByRefresh(before, after *states.State, schemas *terraform.Schemas, view *View) bool { 348 // ManagedResourceEqual checks that the state is exactly equal for all 349 // managed resources; but semantically equivalent states, or changes to 350 // deposed instances may not actually represent changes we need to present 351 // to the user, so for now this only serves as a short-circuit to skip 352 // attempting to render the diffs below. 353 if after.ManagedResourcesEqual(before) { 354 return false 355 } 356 357 var diffs []string 358 359 for _, bms := range before.Modules { 360 for _, brs := range bms.Resources { 361 if brs.Addr.Resource.Mode != addrs.ManagedResourceMode { 362 continue // only managed resources can "drift" 363 } 364 addr := brs.Addr 365 prs := after.Resource(brs.Addr) 366 367 provider := brs.ProviderConfig.Provider 368 providerSchema := schemas.ProviderSchema(provider) 369 if providerSchema == nil { 370 // Should never happen 371 view.streams.Printf("(schema missing for %s)\n", provider) 372 continue 373 } 374 rSchema, _ := providerSchema.SchemaForResourceAddr(addr.Resource) 375 if rSchema == nil { 376 // Should never happen 377 view.streams.Printf("(schema missing for %s)\n", addr) 378 continue 379 } 380 381 for key, bis := range brs.Instances { 382 if bis.Current == nil { 383 // No current instance to render here 384 continue 385 } 386 var pis *states.ResourceInstance 387 if prs != nil { 388 pis = prs.Instance(key) 389 } 390 391 diff := format.ResourceInstanceDrift( 392 addr.Instance(key), 393 bis, pis, 394 rSchema, 395 view.colorize, 396 ) 397 if diff != "" { 398 diffs = append(diffs, diff) 399 } 400 } 401 } 402 } 403 404 // If we only have changes regarding deposed instances, or the diff 405 // renderer is suppressing irrelevant changes from the legacy SDK, there 406 // may not have been anything to display to the user. 407 if len(diffs) > 0 { 408 view.streams.Print( 409 view.colorize.Color("[reset]\n[bold][cyan]Note:[reset][bold] Objects have changed outside of Terraform[reset]\n\n"), 410 ) 411 view.streams.Print(format.WordWrap( 412 "Terraform detected the following changes made outside of Terraform since the last \"terraform apply\":\n\n", 413 view.outputColumns(), 414 )) 415 416 for _, diff := range diffs { 417 view.streams.Print(diff) 418 } 419 return true 420 } 421 422 return false 423 } 424 425 const planHeaderIntro = ` 426 Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: 427 `