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 }