github.com/opentofu/opentofu@v1.7.1/internal/command/views/show.go (about)

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