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  `