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