github.com/pulumi/terraform@v1.4.0/pkg/command/views/operation.go (about)

     1  package views
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/pulumi/terraform/pkg/addrs"
     9  	"github.com/pulumi/terraform/pkg/command/arguments"
    10  	"github.com/pulumi/terraform/pkg/command/format"
    11  	"github.com/pulumi/terraform/pkg/command/jsonformat"
    12  	"github.com/pulumi/terraform/pkg/command/jsonplan"
    13  	"github.com/pulumi/terraform/pkg/command/jsonprovider"
    14  	"github.com/pulumi/terraform/pkg/command/views/json"
    15  	"github.com/pulumi/terraform/pkg/plans"
    16  	"github.com/pulumi/terraform/pkg/states/statefile"
    17  	"github.com/pulumi/terraform/pkg/terraform"
    18  	"github.com/pulumi/terraform/pkg/tfdiags"
    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  	outputs, changed, drift, attrs, err := jsonplan.MarshalForRenderer(plan, schemas)
    93  	if err != nil {
    94  		v.view.streams.Eprintf("Failed to marshal plan to json: %s", err)
    95  		return
    96  	}
    97  
    98  	renderer := jsonformat.Renderer{
    99  		Colorize:            v.view.colorize,
   100  		Streams:             v.view.streams,
   101  		RunningInAutomation: v.inAutomation,
   102  	}
   103  
   104  	jplan := jsonformat.Plan{
   105  		PlanFormatVersion:     jsonplan.FormatVersion,
   106  		ProviderFormatVersion: jsonprovider.FormatVersion,
   107  		OutputChanges:         outputs,
   108  		ResourceChanges:       changed,
   109  		ResourceDrift:         drift,
   110  		ProviderSchemas:       jsonprovider.MarshalForRenderer(schemas),
   111  		RelevantAttributes:    attrs,
   112  	}
   113  
   114  	// Side load some data that we can't extract from the JSON plan.
   115  	var opts []jsonformat.PlanRendererOpt
   116  	if !plan.CanApply() {
   117  		opts = append(opts, jsonformat.CanNotApply)
   118  	}
   119  	if plan.Errored {
   120  		opts = append(opts, jsonformat.Errored)
   121  	}
   122  
   123  	renderer.RenderHumanPlan(jplan, plan.UIMode, opts...)
   124  }
   125  
   126  func (v *OperationHuman) PlannedChange(change *plans.ResourceInstanceChangeSrc) {
   127  	// PlannedChange is primarily for machine-readable output in order to
   128  	// get a per-resource-instance change description. We don't use it
   129  	// with OperationHuman because the output of Plan already includes the
   130  	// change details for all resource instances.
   131  }
   132  
   133  // PlanNextStep gives the user some next-steps, unless we're running in an
   134  // automation tool which is presumed to provide its own UI for further actions.
   135  func (v *OperationHuman) PlanNextStep(planPath string) {
   136  	if v.inAutomation {
   137  		return
   138  	}
   139  	v.view.outputHorizRule()
   140  
   141  	if planPath == "" {
   142  		v.view.streams.Print(
   143  			"\n" + strings.TrimSpace(format.WordWrap(planHeaderNoOutput, v.view.outputColumns())) + "\n",
   144  		)
   145  	} else {
   146  		v.view.streams.Printf(
   147  			"\n"+strings.TrimSpace(format.WordWrap(planHeaderYesOutput, v.view.outputColumns()))+"\n",
   148  			planPath, planPath,
   149  		)
   150  	}
   151  }
   152  
   153  func (v *OperationHuman) Diagnostics(diags tfdiags.Diagnostics) {
   154  	v.view.Diagnostics(diags)
   155  }
   156  
   157  type OperationJSON struct {
   158  	view *JSONView
   159  }
   160  
   161  var _ Operation = (*OperationJSON)(nil)
   162  
   163  func (v *OperationJSON) Interrupted() {
   164  	v.view.Log(interrupted)
   165  }
   166  
   167  func (v *OperationJSON) FatalInterrupt() {
   168  	v.view.Log(fatalInterrupt)
   169  }
   170  
   171  func (v *OperationJSON) Stopping() {
   172  	v.view.Log("Stopping operation...")
   173  }
   174  
   175  func (v *OperationJSON) Cancelled(planMode plans.Mode) {
   176  	switch planMode {
   177  	case plans.DestroyMode:
   178  		v.view.Log("Destroy cancelled")
   179  	default:
   180  		v.view.Log("Apply cancelled")
   181  	}
   182  }
   183  
   184  func (v *OperationJSON) EmergencyDumpState(stateFile *statefile.File) error {
   185  	stateBuf := new(bytes.Buffer)
   186  	jsonErr := statefile.Write(stateFile, stateBuf)
   187  	if jsonErr != nil {
   188  		return jsonErr
   189  	}
   190  	v.view.StateDump(stateBuf.String())
   191  	return nil
   192  }
   193  
   194  // Log a change summary and a series of "planned" messages for the changes in
   195  // the plan.
   196  func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
   197  	for _, dr := range plan.DriftedResources {
   198  		// In refresh-only mode, we output all resources marked as drifted,
   199  		// including those which have moved without other changes. In other plan
   200  		// modes, move-only changes will be included in the planned changes, so
   201  		// we skip them here.
   202  		if dr.Action != plans.NoOp || plan.UIMode == plans.RefreshOnlyMode {
   203  			v.view.ResourceDrift(json.NewResourceInstanceChange(dr))
   204  		}
   205  	}
   206  
   207  	cs := &json.ChangeSummary{
   208  		Operation: json.OperationPlanned,
   209  	}
   210  	for _, change := range plan.Changes.Resources {
   211  		if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
   212  			// Avoid rendering data sources on deletion
   213  			continue
   214  		}
   215  		switch change.Action {
   216  		case plans.Create:
   217  			cs.Add++
   218  		case plans.Delete:
   219  			cs.Remove++
   220  		case plans.Update:
   221  			cs.Change++
   222  		case plans.CreateThenDelete, plans.DeleteThenCreate:
   223  			cs.Add++
   224  			cs.Remove++
   225  		}
   226  
   227  		if change.Action != plans.NoOp || !change.Addr.Equal(change.PrevRunAddr) {
   228  			v.view.PlannedChange(json.NewResourceInstanceChange(change))
   229  		}
   230  	}
   231  
   232  	v.view.ChangeSummary(cs)
   233  
   234  	var rootModuleOutputs []*plans.OutputChangeSrc
   235  	for _, output := range plan.Changes.Outputs {
   236  		if !output.Addr.Module.IsRoot() {
   237  			continue
   238  		}
   239  		rootModuleOutputs = append(rootModuleOutputs, output)
   240  	}
   241  	if len(rootModuleOutputs) > 0 {
   242  		v.view.Outputs(json.OutputsFromChanges(rootModuleOutputs))
   243  	}
   244  }
   245  
   246  func (v *OperationJSON) PlannedChange(change *plans.ResourceInstanceChangeSrc) {
   247  	if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
   248  		// Avoid rendering data sources on deletion
   249  		return
   250  	}
   251  	v.view.PlannedChange(json.NewResourceInstanceChange(change))
   252  }
   253  
   254  // PlanNextStep does nothing for the JSON view as it is a hook for user-facing
   255  // output only applicable to human-readable UI.
   256  func (v *OperationJSON) PlanNextStep(planPath string) {
   257  }
   258  
   259  func (v *OperationJSON) Diagnostics(diags tfdiags.Diagnostics) {
   260  	v.view.Diagnostics(diags)
   261  }
   262  
   263  const fatalInterrupt = `
   264  Two interrupts received. Exiting immediately. Note that data loss may have occurred.
   265  `
   266  
   267  const interrupted = `
   268  Interrupt received.
   269  Please wait for Terraform to exit or data loss may occur.
   270  Gracefully shutting down...
   271  `
   272  
   273  const planHeaderNoOutput = `
   274  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.
   275  `
   276  
   277  const planHeaderYesOutput = `
   278  Saved the plan to: %s
   279  
   280  To perform exactly these actions, run the following command to apply:
   281      terraform apply %q
   282  `