github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/views/operation.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package views
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"strings"
    10  
    11  	"github.com/terramate-io/tf/addrs"
    12  	"github.com/terramate-io/tf/command/arguments"
    13  	"github.com/terramate-io/tf/command/format"
    14  	"github.com/terramate-io/tf/command/jsonformat"
    15  	"github.com/terramate-io/tf/command/jsonplan"
    16  	"github.com/terramate-io/tf/command/jsonprovider"
    17  	"github.com/terramate-io/tf/command/views/json"
    18  	"github.com/terramate-io/tf/plans"
    19  	"github.com/terramate-io/tf/states/statefile"
    20  	"github.com/terramate-io/tf/terraform"
    21  	"github.com/terramate-io/tf/tfdiags"
    22  )
    23  
    24  type Operation interface {
    25  	Interrupted()
    26  	FatalInterrupt()
    27  	Stopping()
    28  	Cancelled(planMode plans.Mode)
    29  
    30  	EmergencyDumpState(stateFile *statefile.File) error
    31  
    32  	PlannedChange(change *plans.ResourceInstanceChangeSrc)
    33  	Plan(plan *plans.Plan, schemas *terraform.Schemas)
    34  	PlanNextStep(planPath string, genConfigPath string)
    35  
    36  	Diagnostics(diags tfdiags.Diagnostics)
    37  }
    38  
    39  func NewOperation(vt arguments.ViewType, inAutomation bool, view *View) Operation {
    40  	switch vt {
    41  	case arguments.ViewHuman:
    42  		return &OperationHuman{view: view, inAutomation: inAutomation}
    43  	default:
    44  		panic(fmt.Sprintf("unknown view type %v", vt))
    45  	}
    46  }
    47  
    48  type OperationHuman struct {
    49  	view *View
    50  
    51  	// inAutomation indicates that commands are being run by an
    52  	// automated system rather than directly at a command prompt.
    53  	//
    54  	// This is a hint not to produce messages that expect that a user can
    55  	// run a follow-up command, perhaps because Terraform is running in
    56  	// some sort of workflow automation tool that abstracts away the
    57  	// exact commands that are being run.
    58  	inAutomation bool
    59  }
    60  
    61  var _ Operation = (*OperationHuman)(nil)
    62  
    63  func (v *OperationHuman) Interrupted() {
    64  	v.view.streams.Println(format.WordWrap(interrupted, v.view.outputColumns()))
    65  }
    66  
    67  func (v *OperationHuman) FatalInterrupt() {
    68  	v.view.streams.Eprintln(format.WordWrap(fatalInterrupt, v.view.errorColumns()))
    69  }
    70  
    71  func (v *OperationHuman) Stopping() {
    72  	v.view.streams.Println("Stopping operation...")
    73  }
    74  
    75  func (v *OperationHuman) Cancelled(planMode plans.Mode) {
    76  	switch planMode {
    77  	case plans.DestroyMode:
    78  		v.view.streams.Println("Destroy cancelled.")
    79  	default:
    80  		v.view.streams.Println("Apply cancelled.")
    81  	}
    82  }
    83  
    84  func (v *OperationHuman) EmergencyDumpState(stateFile *statefile.File) error {
    85  	stateBuf := new(bytes.Buffer)
    86  	jsonErr := statefile.Write(stateFile, stateBuf)
    87  	if jsonErr != nil {
    88  		return jsonErr
    89  	}
    90  	v.view.streams.Eprintln(stateBuf)
    91  	return nil
    92  }
    93  
    94  func (v *OperationHuman) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
    95  	outputs, changed, drift, attrs, err := jsonplan.MarshalForRenderer(plan, schemas)
    96  	if err != nil {
    97  		v.view.streams.Eprintf("Failed to marshal plan to json: %s", err)
    98  		return
    99  	}
   100  
   101  	renderer := jsonformat.Renderer{
   102  		Colorize:            v.view.colorize,
   103  		Streams:             v.view.streams,
   104  		RunningInAutomation: v.inAutomation,
   105  	}
   106  
   107  	jplan := jsonformat.Plan{
   108  		PlanFormatVersion:     jsonplan.FormatVersion,
   109  		ProviderFormatVersion: jsonprovider.FormatVersion,
   110  		OutputChanges:         outputs,
   111  		ResourceChanges:       changed,
   112  		ResourceDrift:         drift,
   113  		ProviderSchemas:       jsonprovider.MarshalForRenderer(schemas),
   114  		RelevantAttributes:    attrs,
   115  	}
   116  
   117  	// Side load some data that we can't extract from the JSON plan.
   118  	var opts []plans.Quality
   119  	if !plan.CanApply() {
   120  		opts = append(opts, plans.NoChanges)
   121  	}
   122  	if plan.Errored {
   123  		opts = append(opts, plans.Errored)
   124  	}
   125  
   126  	renderer.RenderHumanPlan(jplan, plan.UIMode, opts...)
   127  }
   128  
   129  func (v *OperationHuman) PlannedChange(change *plans.ResourceInstanceChangeSrc) {
   130  	// PlannedChange is primarily for machine-readable output in order to
   131  	// get a per-resource-instance change description. We don't use it
   132  	// with OperationHuman because the output of Plan already includes the
   133  	// change details for all resource instances.
   134  }
   135  
   136  // PlanNextStep gives the user some next-steps, unless we're running in an
   137  // automation tool which is presumed to provide its own UI for further actions.
   138  func (v *OperationHuman) PlanNextStep(planPath string, genConfigPath string) {
   139  	if v.inAutomation {
   140  		return
   141  	}
   142  	v.view.outputHorizRule()
   143  
   144  	if genConfigPath != "" {
   145  		v.view.streams.Printf(
   146  			format.WordWrap(
   147  				"\n"+strings.TrimSpace(fmt.Sprintf(planHeaderGenConfig, genConfigPath)),
   148  				v.view.outputColumns(),
   149  			) + "\n")
   150  	}
   151  
   152  	if planPath == "" {
   153  		v.view.streams.Print(
   154  			format.WordWrap(
   155  				"\n"+strings.TrimSpace(planHeaderNoOutput),
   156  				v.view.outputColumns(),
   157  			) + "\n",
   158  		)
   159  	} else {
   160  		v.view.streams.Printf(
   161  			format.WordWrap(
   162  				"\n"+strings.TrimSpace(fmt.Sprintf(planHeaderYesOutput, planPath, planPath)),
   163  				v.view.outputColumns(),
   164  			) + "\n",
   165  		)
   166  	}
   167  }
   168  
   169  func (v *OperationHuman) Diagnostics(diags tfdiags.Diagnostics) {
   170  	v.view.Diagnostics(diags)
   171  }
   172  
   173  type OperationJSON struct {
   174  	view *JSONView
   175  }
   176  
   177  var _ Operation = (*OperationJSON)(nil)
   178  
   179  func (v *OperationJSON) Interrupted() {
   180  	v.view.Log(interrupted)
   181  }
   182  
   183  func (v *OperationJSON) FatalInterrupt() {
   184  	v.view.Log(fatalInterrupt)
   185  }
   186  
   187  func (v *OperationJSON) Stopping() {
   188  	v.view.Log("Stopping operation...")
   189  }
   190  
   191  func (v *OperationJSON) Cancelled(planMode plans.Mode) {
   192  	switch planMode {
   193  	case plans.DestroyMode:
   194  		v.view.Log("Destroy cancelled")
   195  	default:
   196  		v.view.Log("Apply cancelled")
   197  	}
   198  }
   199  
   200  func (v *OperationJSON) EmergencyDumpState(stateFile *statefile.File) error {
   201  	stateBuf := new(bytes.Buffer)
   202  	jsonErr := statefile.Write(stateFile, stateBuf)
   203  	if jsonErr != nil {
   204  		return jsonErr
   205  	}
   206  	v.view.StateDump(stateBuf.String())
   207  	return nil
   208  }
   209  
   210  // Log a change summary and a series of "planned" messages for the changes in
   211  // the plan.
   212  func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
   213  	for _, dr := range plan.DriftedResources {
   214  		// In refresh-only mode, we output all resources marked as drifted,
   215  		// including those which have moved without other changes. In other plan
   216  		// modes, move-only changes will be included in the planned changes, so
   217  		// we skip them here.
   218  		if dr.Action != plans.NoOp || plan.UIMode == plans.RefreshOnlyMode {
   219  			v.view.ResourceDrift(json.NewResourceInstanceChange(dr))
   220  		}
   221  	}
   222  
   223  	cs := &json.ChangeSummary{
   224  		Operation: json.OperationPlanned,
   225  	}
   226  	for _, change := range plan.Changes.Resources {
   227  		if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
   228  			// Avoid rendering data sources on deletion
   229  			continue
   230  		}
   231  
   232  		if change.Importing != nil {
   233  			cs.Import++
   234  		}
   235  
   236  		switch change.Action {
   237  		case plans.Create:
   238  			cs.Add++
   239  		case plans.Delete:
   240  			cs.Remove++
   241  		case plans.Update:
   242  			cs.Change++
   243  		case plans.CreateThenDelete, plans.DeleteThenCreate:
   244  			cs.Add++
   245  			cs.Remove++
   246  		}
   247  
   248  		if change.Action != plans.NoOp || !change.Addr.Equal(change.PrevRunAddr) || change.Importing != nil {
   249  			v.view.PlannedChange(json.NewResourceInstanceChange(change))
   250  		}
   251  	}
   252  
   253  	v.view.ChangeSummary(cs)
   254  
   255  	var rootModuleOutputs []*plans.OutputChangeSrc
   256  	for _, output := range plan.Changes.Outputs {
   257  		if !output.Addr.Module.IsRoot() {
   258  			continue
   259  		}
   260  		rootModuleOutputs = append(rootModuleOutputs, output)
   261  	}
   262  	if len(rootModuleOutputs) > 0 {
   263  		v.view.Outputs(json.OutputsFromChanges(rootModuleOutputs))
   264  	}
   265  }
   266  
   267  func (v *OperationJSON) PlannedChange(change *plans.ResourceInstanceChangeSrc) {
   268  	if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
   269  		// Avoid rendering data sources on deletion
   270  		return
   271  	}
   272  	v.view.PlannedChange(json.NewResourceInstanceChange(change))
   273  }
   274  
   275  // PlanNextStep does nothing for the JSON view as it is a hook for user-facing
   276  // output only applicable to human-readable UI.
   277  func (v *OperationJSON) PlanNextStep(planPath string, genConfigPath string) {
   278  }
   279  
   280  func (v *OperationJSON) Diagnostics(diags tfdiags.Diagnostics) {
   281  	v.view.Diagnostics(diags)
   282  }
   283  
   284  const fatalInterrupt = `
   285  Two interrupts received. Exiting immediately. Note that data loss may have occurred.
   286  `
   287  
   288  const interrupted = `
   289  Interrupt received.
   290  Please wait for Terraform to exit or data loss may occur.
   291  Gracefully shutting down...
   292  `
   293  
   294  const planHeaderNoOutput = `
   295  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.
   296  `
   297  
   298  const planHeaderYesOutput = `
   299  Saved the plan to: %s
   300  
   301  To perform exactly these actions, run the following command to apply:
   302      terraform apply %q
   303  `
   304  
   305  const planHeaderGenConfig = `
   306  Terraform has generated configuration and written it to %s. Please review the configuration and edit it as necessary before adding it to version control.
   307  `