github.com/opentofu/opentofu@v1.7.1/internal/command/jsonformat/renderer.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 jsonformat
     7  
     8  import (
     9  	"fmt"
    10  	"strconv"
    11  
    12  	"github.com/mitchellh/colorstring"
    13  	ctyjson "github.com/zclconf/go-cty/cty/json"
    14  
    15  	"github.com/opentofu/opentofu/internal/command/format"
    16  	"github.com/opentofu/opentofu/internal/command/jsonformat/computed"
    17  	"github.com/opentofu/opentofu/internal/command/jsonformat/differ"
    18  	"github.com/opentofu/opentofu/internal/command/jsonformat/structured"
    19  	"github.com/opentofu/opentofu/internal/command/jsonplan"
    20  	"github.com/opentofu/opentofu/internal/command/jsonprovider"
    21  	"github.com/opentofu/opentofu/internal/command/jsonstate"
    22  	viewsjson "github.com/opentofu/opentofu/internal/command/views/json"
    23  	"github.com/opentofu/opentofu/internal/plans"
    24  	"github.com/opentofu/opentofu/internal/terminal"
    25  )
    26  
    27  type JSONLogType string
    28  
    29  type JSONLog struct {
    30  	Message    string                 `json:"@message"`
    31  	Type       JSONLogType            `json:"type"`
    32  	Diagnostic *viewsjson.Diagnostic  `json:"diagnostic"`
    33  	Outputs    viewsjson.Outputs      `json:"outputs"`
    34  	Hook       map[string]interface{} `json:"hook"`
    35  }
    36  
    37  const (
    38  	LogApplyComplete     JSONLogType = "apply_complete"
    39  	LogApplyErrored      JSONLogType = "apply_errored"
    40  	LogApplyStart        JSONLogType = "apply_start"
    41  	LogChangeSummary     JSONLogType = "change_summary"
    42  	LogDiagnostic        JSONLogType = "diagnostic"
    43  	LogPlannedChange     JSONLogType = "planned_change"
    44  	LogProvisionComplete JSONLogType = "provision_complete"
    45  	LogProvisionErrored  JSONLogType = "provision_errored"
    46  	LogProvisionProgress JSONLogType = "provision_progress"
    47  	LogProvisionStart    JSONLogType = "provision_start"
    48  	LogOutputs           JSONLogType = "outputs"
    49  	LogRefreshComplete   JSONLogType = "refresh_complete"
    50  	LogRefreshStart      JSONLogType = "refresh_start"
    51  	LogResourceDrift     JSONLogType = "resource_drift"
    52  	LogVersion           JSONLogType = "version"
    53  )
    54  
    55  func incompatibleVersions(localVersion, remoteVersion string) bool {
    56  	var parsedLocal, parsedRemote float64
    57  	var err error
    58  
    59  	if parsedLocal, err = strconv.ParseFloat(localVersion, 64); err != nil {
    60  		return false
    61  	}
    62  	if parsedRemote, err = strconv.ParseFloat(remoteVersion, 64); err != nil {
    63  		return false
    64  	}
    65  
    66  	// If the local version is less than the remote version then the remote
    67  	// version might contain things the local version doesn't know about, so
    68  	// we're going to say they are incompatible.
    69  	//
    70  	// So far, we have built the renderer and the json packages to be backwards
    71  	// compatible so if the local version is greater than the remote version
    72  	// then that is okay, we'll still render a complete and correct plan.
    73  	//
    74  	// Note, this might change in the future. For example, if we introduce a
    75  	// new major version in one of the formats the renderer may no longer be
    76  	// backward compatible.
    77  	return parsedLocal < parsedRemote
    78  }
    79  
    80  type Renderer struct {
    81  	Streams  *terminal.Streams
    82  	Colorize *colorstring.Colorize
    83  
    84  	RunningInAutomation bool
    85  }
    86  
    87  func (renderer Renderer) RenderHumanPlan(plan Plan, mode plans.Mode, opts ...plans.Quality) {
    88  	if incompatibleVersions(jsonplan.FormatVersion, plan.PlanFormatVersion) || incompatibleVersions(jsonprovider.FormatVersion, plan.ProviderFormatVersion) {
    89  		renderer.Streams.Println(format.WordWrap(
    90  			renderer.Colorize.Color("\n[bold][red]Warning:[reset][bold] This plan was generated using a different version of OpenTofu, the diff presented here may be missing representations of recent features."),
    91  			renderer.Streams.Stdout.Columns()))
    92  	}
    93  
    94  	plan.renderHuman(renderer, mode, opts...)
    95  }
    96  
    97  func (renderer Renderer) RenderHumanState(state State) {
    98  	if incompatibleVersions(jsonstate.FormatVersion, state.StateFormatVersion) || incompatibleVersions(jsonprovider.FormatVersion, state.ProviderFormatVersion) {
    99  		renderer.Streams.Println(format.WordWrap(
   100  			renderer.Colorize.Color("\n[bold][red]Warning:[reset][bold] This state was retrieved using a different version of OpenTofu, the state presented here maybe missing representations of recent features."),
   101  			renderer.Streams.Stdout.Columns()))
   102  	}
   103  
   104  	if state.Empty() {
   105  		renderer.Streams.Println("The state file is empty. No resources are represented.")
   106  		return
   107  	}
   108  
   109  	opts := computed.NewRenderHumanOpts(renderer.Colorize)
   110  	opts.ShowUnchangedChildren = true
   111  	opts.HideDiffActionSymbols = true
   112  
   113  	state.renderHumanStateModule(renderer, state.RootModule, opts, true)
   114  	state.renderHumanStateOutputs(renderer, opts)
   115  }
   116  
   117  func (renderer Renderer) RenderLog(log *JSONLog) error {
   118  	switch log.Type {
   119  	case LogRefreshComplete,
   120  		LogVersion,
   121  		LogPlannedChange,
   122  		LogProvisionComplete,
   123  		LogProvisionErrored,
   124  		LogApplyErrored:
   125  		// We won't display these types of logs
   126  		return nil
   127  
   128  	case LogApplyStart, LogApplyComplete, LogRefreshStart, LogProvisionStart, LogResourceDrift:
   129  		msg := fmt.Sprintf(renderer.Colorize.Color("[bold]%s[reset]"), log.Message)
   130  		renderer.Streams.Println(msg)
   131  
   132  	case LogDiagnostic:
   133  		diag := format.DiagnosticFromJSON(log.Diagnostic, renderer.Colorize, 78)
   134  		renderer.Streams.Print(diag)
   135  
   136  	case LogOutputs:
   137  		if len(log.Outputs) > 0 {
   138  			renderer.Streams.Println(renderer.Colorize.Color("[bold][green]Outputs:[reset]"))
   139  			for name, output := range log.Outputs {
   140  				change := structured.FromJsonViewsOutput(output)
   141  				ctype, err := ctyjson.UnmarshalType(output.Type)
   142  				if err != nil {
   143  					return err
   144  				}
   145  
   146  				opts := computed.NewRenderHumanOpts(renderer.Colorize)
   147  				opts.ShowUnchangedChildren = true
   148  
   149  				outputDiff := differ.ComputeDiffForType(change, ctype)
   150  				outputStr := outputDiff.RenderHuman(0, opts)
   151  
   152  				msg := fmt.Sprintf("%s = %s", name, outputStr)
   153  				renderer.Streams.Println(msg)
   154  			}
   155  		}
   156  
   157  	case LogProvisionProgress:
   158  		provisioner := log.Hook["provisioner"].(string)
   159  		output := log.Hook["output"].(string)
   160  		resource := log.Hook["resource"].(map[string]interface{})
   161  		resourceAddr := resource["addr"].(string)
   162  
   163  		msg := fmt.Sprintf(renderer.Colorize.Color("[bold]%s: (%s):[reset] %s"),
   164  			resourceAddr, provisioner, output)
   165  		renderer.Streams.Println(msg)
   166  
   167  	case LogChangeSummary:
   168  		// Normally, we will only render the apply change summary since the renderer
   169  		// generates a plan change summary for us
   170  		msg := fmt.Sprintf(renderer.Colorize.Color("[bold][green]%s[reset]"), log.Message)
   171  		renderer.Streams.Println("\n" + msg + "\n")
   172  
   173  	default:
   174  		// If the log type is not a known log type, we will just print the log message
   175  		renderer.Streams.Println(log.Message)
   176  	}
   177  
   178  	return nil
   179  }