github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/command/views/plan.go (about) 1 package views 2 3 import ( 4 "bytes" 5 "fmt" 6 "sort" 7 "strings" 8 9 "github.com/iaas-resource-provision/iaas-rpc/internal/addrs" 10 "github.com/iaas-resource-provision/iaas-rpc/internal/command/arguments" 11 "github.com/iaas-resource-provision/iaas-rpc/internal/command/format" 12 "github.com/iaas-resource-provision/iaas-rpc/internal/plans" 13 "github.com/iaas-resource-provision/iaas-rpc/internal/states" 14 "github.com/iaas-resource-provision/iaas-rpc/internal/terraform" 15 "github.com/iaas-resource-provision/iaas-rpc/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 RPC 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 RPC 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 { 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 counts[change.Action]++ 129 } 130 var changedRootModuleOutputs []*plans.OutputChangeSrc 131 for _, output := range plan.Changes.Outputs { 132 if !output.Addr.Module.IsRoot() { 133 continue 134 } 135 if output.ChangeSrc.Action == plans.NoOp { 136 continue 137 } 138 changedRootModuleOutputs = append(changedRootModuleOutputs, output) 139 } 140 141 if len(counts) == 0 && len(changedRootModuleOutputs) == 0 { 142 // If we didn't find any changes to report at all then this is a 143 // "No changes" plan. How we'll present this depends on whether 144 // the plan is "applyable" and, if so, whether it had refresh changes 145 // that we already would've presented above. 146 147 switch plan.UIMode { 148 case plans.RefreshOnlyMode: 149 if haveRefreshChanges { 150 // We already generated a sufficient prompt about what will 151 // happen if applying this change above, so we don't need to 152 // say anything more. 153 return 154 } 155 156 view.streams.Print( 157 view.colorize.Color("\n[reset][bold][green]No changes.[reset][bold] Your infrastructure still matches the configuration.[reset]\n\n"), 158 ) 159 view.streams.Println(format.WordWrap( 160 "RPC has checked that the real remote objects still match the result of your most recent changes, and found no differences.", 161 view.outputColumns(), 162 )) 163 164 case plans.DestroyMode: 165 if haveRefreshChanges { 166 view.streams.Print(format.HorizontalRule(view.colorize, view.outputColumns())) 167 view.streams.Println("") 168 } 169 view.streams.Print( 170 view.colorize.Color("\n[reset][bold][green]No changes.[reset][bold] No objects need to be destroyed.[reset]\n\n"), 171 ) 172 view.streams.Println(format.WordWrap( 173 "Either you have not created any objects yet or the existing objects were already deleted outside of RPC.", 174 view.outputColumns(), 175 )) 176 177 default: 178 if haveRefreshChanges { 179 view.streams.Print(format.HorizontalRule(view.colorize, view.outputColumns())) 180 view.streams.Println("") 181 } 182 view.streams.Print( 183 view.colorize.Color("\n[reset][bold][green]No changes.[reset][bold] Your infrastructure matches the configuration.[reset]\n\n"), 184 ) 185 186 if haveRefreshChanges && !plan.CanApply() { 187 if plan.CanApply() { 188 // In this case, applying this plan will not change any 189 // remote objects but _will_ update the state to match what 190 // we detected during refresh, so we'll reassure the user 191 // about that. 192 view.streams.Println(format.WordWrap( 193 "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.", 194 view.outputColumns(), 195 )) 196 } else { 197 // In this case we detected changes during refresh but this isn't 198 // a planning mode where we consider those to be applyable. The 199 // user must re-run in refresh-only mode in order to update the 200 // state to match the upstream changes. 201 suggestion := "." 202 if !view.runningInAutomation { 203 // The normal message includes a specific command line to run. 204 suggestion = ":\n terraform apply -refresh-only" 205 } 206 view.streams.Println(format.WordWrap( 207 "Your configuration already matches the changes detected above. If you'd like to update the RPC state to match, create and apply a refresh-only plan"+suggestion, 208 view.outputColumns(), 209 )) 210 } 211 return 212 } 213 214 // If we get down here then we're just in the simple situation where 215 // the plan isn't applyable at all. 216 view.streams.Println(format.WordWrap( 217 "RPC (Resource Provision Center) has compared your real infrastructure against your configuration and found no differences, so no changes are needed.", 218 view.outputColumns(), 219 )) 220 } 221 return 222 } 223 if haveRefreshChanges { 224 view.streams.Print(format.HorizontalRule(view.colorize, view.outputColumns())) 225 view.streams.Println("") 226 } 227 228 if len(counts) != 0 { 229 headerBuf := &bytes.Buffer{} 230 fmt.Fprintf(headerBuf, "\n%s\n", strings.TrimSpace(format.WordWrap(planHeaderIntro, view.outputColumns()))) 231 if counts[plans.Create] > 0 { 232 fmt.Fprintf(headerBuf, "%s create\n", format.DiffActionSymbol(plans.Create)) 233 } 234 if counts[plans.Update] > 0 { 235 fmt.Fprintf(headerBuf, "%s update in-place\n", format.DiffActionSymbol(plans.Update)) 236 } 237 if counts[plans.Delete] > 0 { 238 fmt.Fprintf(headerBuf, "%s destroy\n", format.DiffActionSymbol(plans.Delete)) 239 } 240 if counts[plans.DeleteThenCreate] > 0 { 241 fmt.Fprintf(headerBuf, "%s destroy and then create replacement\n", format.DiffActionSymbol(plans.DeleteThenCreate)) 242 } 243 if counts[plans.CreateThenDelete] > 0 { 244 fmt.Fprintf(headerBuf, "%s create replacement and then destroy\n", format.DiffActionSymbol(plans.CreateThenDelete)) 245 } 246 if counts[plans.Read] > 0 { 247 fmt.Fprintf(headerBuf, "%s read (data resources)\n", format.DiffActionSymbol(plans.Read)) 248 } 249 250 view.streams.Println(view.colorize.Color(headerBuf.String())) 251 252 view.streams.Printf("RPC (Resource Provision Center) will perform the following actions:\n\n") 253 254 // Note: we're modifying the backing slice of this plan object in-place 255 // here. The ordering of resource changes in a plan is not significant, 256 // but we can only do this safely here because we can assume that nobody 257 // is concurrently modifying our changes while we're trying to print it. 258 sort.Slice(rChanges, func(i, j int) bool { 259 iA := rChanges[i].Addr 260 jA := rChanges[j].Addr 261 if iA.String() == jA.String() { 262 return rChanges[i].DeposedKey < rChanges[j].DeposedKey 263 } 264 return iA.Less(jA) 265 }) 266 267 for _, rcs := range rChanges { 268 if rcs.Action == plans.NoOp { 269 continue 270 } 271 272 providerSchema := schemas.ProviderSchema(rcs.ProviderAddr.Provider) 273 if providerSchema == nil { 274 // Should never happen 275 view.streams.Printf("(schema missing for %s)\n\n", rcs.ProviderAddr) 276 continue 277 } 278 rSchema, _ := providerSchema.SchemaForResourceAddr(rcs.Addr.Resource.Resource) 279 if rSchema == nil { 280 // Should never happen 281 view.streams.Printf("(schema missing for %s)\n\n", rcs.Addr) 282 continue 283 } 284 285 view.streams.Println(format.ResourceChange( 286 rcs, 287 rSchema, 288 view.colorize, 289 )) 290 } 291 292 // stats is similar to counts above, but: 293 // - it considers only resource changes 294 // - it simplifies "replace" into both a create and a delete 295 stats := map[plans.Action]int{} 296 for _, change := range rChanges { 297 switch change.Action { 298 case plans.CreateThenDelete, plans.DeleteThenCreate: 299 stats[plans.Create]++ 300 stats[plans.Delete]++ 301 default: 302 stats[change.Action]++ 303 } 304 } 305 view.streams.Printf( 306 view.colorize.Color("[reset][bold]Plan:[reset] %d to add, %d to change, %d to destroy.\n"), 307 stats[plans.Create], stats[plans.Update], stats[plans.Delete], 308 ) 309 } 310 311 // If there is at least one planned change to the root module outputs 312 // then we'll render a summary of those too. 313 if len(changedRootModuleOutputs) > 0 { 314 view.streams.Println( 315 view.colorize.Color("[reset]\n[bold]Changes to Outputs:[reset]") + 316 format.OutputChanges(changedRootModuleOutputs, view.colorize), 317 ) 318 319 if len(counts) == 0 { 320 // If we have output changes but not resource changes then we 321 // won't have output any indication about the changes at all yet, 322 // so we need some extra context about what it would mean to 323 // apply a change that _only_ includes output changes. 324 view.streams.Println(format.WordWrap( 325 "\nYou can apply this plan to save these new output values to the RPC state, without changing any real infrastructure.", 326 view.outputColumns(), 327 )) 328 } 329 } 330 } 331 332 // renderChangesDetectedByRefresh is a part of renderPlan that generates 333 // the note about changes detected by refresh (sometimes considered as "drift"). 334 // 335 // It will only generate output if there's at least one difference detected. 336 // Otherwise, it will produce nothing at all. To help the caller recognize 337 // those two situations incase subsequent output must accommodate it, 338 // renderChangesDetectedByRefresh returns true if it produced at least one 339 // line of output, and guarantees to always produce whole lines terminated 340 // by newline characters. 341 func renderChangesDetectedByRefresh(before, after *states.State, schemas *terraform.Schemas, view *View) bool { 342 // ManagedResourceEqual checks that the state is exactly equal for all 343 // managed resources; but semantically equivalent states, or changes to 344 // deposed instances may not actually represent changes we need to present 345 // to the user, so for now this only serves as a short-circuit to skip 346 // attempting to render the diffs below. 347 if after.ManagedResourcesEqual(before) { 348 return false 349 } 350 351 var diffs []string 352 353 for _, bms := range before.Modules { 354 for _, brs := range bms.Resources { 355 if brs.Addr.Resource.Mode != addrs.ManagedResourceMode { 356 continue // only managed resources can "drift" 357 } 358 addr := brs.Addr 359 prs := after.Resource(brs.Addr) 360 361 provider := brs.ProviderConfig.Provider 362 providerSchema := schemas.ProviderSchema(provider) 363 if providerSchema == nil { 364 // Should never happen 365 view.streams.Printf("(schema missing for %s)\n", provider) 366 continue 367 } 368 rSchema, _ := providerSchema.SchemaForResourceAddr(addr.Resource) 369 if rSchema == nil { 370 // Should never happen 371 view.streams.Printf("(schema missing for %s)\n", addr) 372 continue 373 } 374 375 for key, bis := range brs.Instances { 376 if bis.Current == nil { 377 // No current instance to render here 378 continue 379 } 380 var pis *states.ResourceInstance 381 if prs != nil { 382 pis = prs.Instance(key) 383 } 384 385 diff := format.ResourceInstanceDrift( 386 addr.Instance(key), 387 bis, pis, 388 rSchema, 389 view.colorize, 390 ) 391 if diff != "" { 392 diffs = append(diffs, diff) 393 } 394 } 395 } 396 } 397 398 // If we only have changes regarding deposed instances, or the diff 399 // renderer is suppressing irrelevant changes from the legacy SDK, there 400 // may not have been anything to display to the user. 401 if len(diffs) > 0 { 402 view.streams.Print( 403 view.colorize.Color("[reset]\n[bold][cyan]Note:[reset][bold] Objects have changed outside of RPC[reset]\n\n"), 404 ) 405 view.streams.Print(format.WordWrap( 406 "RPC (Resource Provision Center) detected the following changes made outside of RPC since the last \"iaas-rpc apply\":\n\n", 407 view.outputColumns(), 408 )) 409 410 for _, diff := range diffs { 411 view.streams.Print(diff) 412 } 413 return true 414 } 415 416 return false 417 } 418 419 const planHeaderIntro = ` 420 RPC (Resource Provision Center) used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: 421 `