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