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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package views
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  
    11  	"github.com/terramate-io/tf/cloud/cloudplan"
    12  	"github.com/terramate-io/tf/command/arguments"
    13  	"github.com/terramate-io/tf/command/jsonformat"
    14  	"github.com/terramate-io/tf/command/jsonplan"
    15  	"github.com/terramate-io/tf/command/jsonprovider"
    16  	"github.com/terramate-io/tf/command/jsonstate"
    17  	"github.com/terramate-io/tf/configs"
    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 Show interface {
    25  	// Display renders the plan, if it is available. If plan is nil, it renders the statefile.
    26  	Display(config *configs.Config, plan *plans.Plan, planJSON *cloudplan.RemotePlanJSON, stateFile *statefile.File, schemas *terraform.Schemas) int
    27  
    28  	// Diagnostics renders early diagnostics, resulting from argument parsing.
    29  	Diagnostics(diags tfdiags.Diagnostics)
    30  }
    31  
    32  func NewShow(vt arguments.ViewType, view *View) Show {
    33  	switch vt {
    34  	case arguments.ViewJSON:
    35  		return &ShowJSON{view: view}
    36  	case arguments.ViewHuman:
    37  		return &ShowHuman{view: view}
    38  	default:
    39  		panic(fmt.Sprintf("unknown view type %v", vt))
    40  	}
    41  }
    42  
    43  type ShowHuman struct {
    44  	view *View
    45  }
    46  
    47  var _ Show = (*ShowHuman)(nil)
    48  
    49  func (v *ShowHuman) Display(config *configs.Config, plan *plans.Plan, planJSON *cloudplan.RemotePlanJSON, stateFile *statefile.File, schemas *terraform.Schemas) int {
    50  	renderer := jsonformat.Renderer{
    51  		Colorize:            v.view.colorize,
    52  		Streams:             v.view.streams,
    53  		RunningInAutomation: v.view.runningInAutomation,
    54  	}
    55  
    56  	// Prefer to display a pre-built JSON plan, if we got one; then, fall back
    57  	// to building one ourselves.
    58  	if planJSON != nil {
    59  		if !planJSON.Redacted {
    60  			v.view.streams.Eprintf("Didn't get renderable JSON plan format for human display")
    61  			return 1
    62  		}
    63  		// The redacted json plan format can be decoded into a jsonformat.Plan
    64  		p := jsonformat.Plan{}
    65  		r := bytes.NewReader(planJSON.JSONBytes)
    66  		if err := json.NewDecoder(r).Decode(&p); err != nil {
    67  			v.view.streams.Eprintf("Couldn't decode renderable JSON plan format: %s", err)
    68  		}
    69  
    70  		v.view.streams.Print(v.view.colorize.Color(planJSON.RunHeader + "\n"))
    71  		renderer.RenderHumanPlan(p, planJSON.Mode, planJSON.Qualities...)
    72  		v.view.streams.Print(v.view.colorize.Color("\n" + planJSON.RunFooter + "\n"))
    73  	} else if plan != nil {
    74  		outputs, changed, drift, attrs, err := jsonplan.MarshalForRenderer(plan, schemas)
    75  		if err != nil {
    76  			v.view.streams.Eprintf("Failed to marshal plan to json: %s", err)
    77  			return 1
    78  		}
    79  
    80  		jplan := jsonformat.Plan{
    81  			PlanFormatVersion:     jsonplan.FormatVersion,
    82  			ProviderFormatVersion: jsonprovider.FormatVersion,
    83  			OutputChanges:         outputs,
    84  			ResourceChanges:       changed,
    85  			ResourceDrift:         drift,
    86  			ProviderSchemas:       jsonprovider.MarshalForRenderer(schemas),
    87  			RelevantAttributes:    attrs,
    88  		}
    89  
    90  		var opts []plans.Quality
    91  		if !plan.CanApply() {
    92  			opts = append(opts, plans.NoChanges)
    93  		}
    94  		if plan.Errored {
    95  			opts = append(opts, plans.Errored)
    96  		}
    97  
    98  		renderer.RenderHumanPlan(jplan, plan.UIMode, opts...)
    99  	} else {
   100  		if stateFile == nil {
   101  			v.view.streams.Println("No state.")
   102  			return 0
   103  		}
   104  
   105  		root, outputs, err := jsonstate.MarshalForRenderer(stateFile, schemas)
   106  		if err != nil {
   107  			v.view.streams.Eprintf("Failed to marshal state to json: %s", err)
   108  			return 1
   109  		}
   110  
   111  		jstate := jsonformat.State{
   112  			StateFormatVersion:    jsonstate.FormatVersion,
   113  			ProviderFormatVersion: jsonprovider.FormatVersion,
   114  			RootModule:            root,
   115  			RootModuleOutputs:     outputs,
   116  			ProviderSchemas:       jsonprovider.MarshalForRenderer(schemas),
   117  		}
   118  
   119  		renderer.RenderHumanState(jstate)
   120  	}
   121  	return 0
   122  }
   123  
   124  func (v *ShowHuman) Diagnostics(diags tfdiags.Diagnostics) {
   125  	v.view.Diagnostics(diags)
   126  }
   127  
   128  type ShowJSON struct {
   129  	view *View
   130  }
   131  
   132  var _ Show = (*ShowJSON)(nil)
   133  
   134  func (v *ShowJSON) Display(config *configs.Config, plan *plans.Plan, planJSON *cloudplan.RemotePlanJSON, stateFile *statefile.File, schemas *terraform.Schemas) int {
   135  	// Prefer to display a pre-built JSON plan, if we got one; then, fall back
   136  	// to building one ourselves.
   137  	if planJSON != nil {
   138  		if planJSON.Redacted {
   139  			v.view.streams.Eprintf("Didn't get external JSON plan format")
   140  			return 1
   141  		}
   142  		v.view.streams.Println(string(planJSON.JSONBytes))
   143  	} else if plan != nil {
   144  		planJSON, err := jsonplan.Marshal(config, plan, stateFile, schemas)
   145  
   146  		if err != nil {
   147  			v.view.streams.Eprintf("Failed to marshal plan to json: %s", err)
   148  			return 1
   149  		}
   150  		v.view.streams.Println(string(planJSON))
   151  	} else {
   152  		// It is possible that there is neither state nor a plan.
   153  		// That's ok, we'll just return an empty object.
   154  		jsonState, err := jsonstate.Marshal(stateFile, schemas)
   155  		if err != nil {
   156  			v.view.streams.Eprintf("Failed to marshal state to json: %s", err)
   157  			return 1
   158  		}
   159  		v.view.streams.Println(string(jsonState))
   160  	}
   161  	return 0
   162  }
   163  
   164  // Diagnostics should only be called if show cannot be executed.
   165  // In this case, we choose to render human-readable diagnostic output,
   166  // primarily for backwards compatibility.
   167  func (v *ShowJSON) Diagnostics(diags tfdiags.Diagnostics) {
   168  	v.view.Diagnostics(diags)
   169  }