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  `