| // Copyright (c) The OpenTofu Authors | |
| // SPDX-License-Identifier: MPL-2.0 | |
| // Copyright (c) 2023 HashiCorp, Inc. | |
| // SPDX-License-Identifier: MPL-2.0 | |
| package views | |
| import ( | |
| encJson "encoding/json" | |
| "fmt" | |
| "os" | |
| "github.com/hashicorp/go-hclog" | |
| "github.com/opentofu/opentofu/internal/command/jsonentities" | |
| "github.com/opentofu/opentofu/internal/command/views/json" | |
| "github.com/opentofu/opentofu/internal/tfdiags" | |
| tfversion "github.com/opentofu/opentofu/version" | |
| ) | |
| // This version describes the schema of JSON UI messages. This version must be | |
| // updated after making any changes to this view, the jsonHook, or any of the | |
| // command/views/json package. | |
| const JSON_UI_VERSION = "1.2" | |
| // NewJSONView creates a new JSONView that wraps the logger configured for the JSON output. | |
| // This method is meant to be called only from the views that are root level, meaning that | |
| // are created for the invoked OpenTofu command. | |
| // The other utilitary views (eg: Backend, StateLocker, jsonHook, etc) needs to get JSONView | |
| // from a root level view. That's because when this method is called, it prints a log entry | |
| // with the OpenTofu version. By invoking this method multiple times in a run, will produce | |
| // multiple version log entries, which might disrupt any other external parsing tool that | |
| // might rely on the machine readable output. | |
| func NewJSONView(view *View, out *os.File) *JSONView { | |
| if out == nil { | |
| out = view.streams.Stdout.File | |
| } | |
| log := hclog.New(&hclog.LoggerOptions{ | |
| Name: "tofu.ui", | |
| Output: out, | |
| JSONFormat: true, | |
| JSONEscapeDisabled: true, | |
| }) | |
| jv := &JSONView{ | |
| log: log, | |
| view: view, | |
| } | |
| jv.Version() | |
| return jv | |
| } | |
| type JSONView struct { | |
| // hclog is used for all output in JSON UI mode. The logger has an internal | |
| // mutex to ensure that messages are not interleaved. | |
| log hclog.Logger | |
| // We hold a reference to the view entirely to allow us to access the | |
| // ConfigSources function pointer, in order to render source snippets into | |
| // diagnostics. This is even more unfortunate than the same reference in the | |
| // view. | |
| // | |
| // Do not be tempted to dereference the configSource value upon logger init, | |
| // as it will likely be updated later. | |
| view *View | |
| } | |
| func (v *JSONView) Version() { | |
| version := tfversion.String() | |
| v.log.Info( | |
| fmt.Sprintf("OpenTofu %s", version), | |
| "type", json.MessageVersion, | |
| "tofu", version, | |
| "ui", JSON_UI_VERSION, | |
| ) | |
| } | |
| func (v *JSONView) Log(message string) { | |
| v.log.Info(message, "type", json.MessageLog) | |
| } | |
| func (v *JSONView) StateDump(state string) { | |
| v.log.Info( | |
| "Emergency state dump", | |
| "type", json.MessageLog, | |
| "state", encJson.RawMessage(state), | |
| ) | |
| } | |
| func (v *JSONView) Diagnostics(diags tfdiags.Diagnostics, metadata ...any) { | |
| sources := v.view.configSources() | |
| for _, diag := range diags { | |
| diagnostic := jsonentities.NewDiagnostic(diag, sources) | |
| args := []any{"type", json.MessageDiagnostic, "diagnostic", diagnostic} | |
| args = append(args, metadata...) | |
| switch diag.Severity() { | |
| case tfdiags.Warning: | |
| v.log.Warn(fmt.Sprintf("Warning: %s", diag.Description().Summary), args...) | |
| default: | |
| v.log.Error(fmt.Sprintf("Error: %s", diag.Description().Summary), args...) | |
| } | |
| } | |
| } | |
| func (v *JSONView) PlannedChange(c *jsonentities.ResourceInstanceChange) { | |
| v.log.Info( | |
| c.String(), | |
| "type", json.MessagePlannedChange, | |
| "change", c, | |
| ) | |
| } | |
| func (v *JSONView) ResourceDrift(c *jsonentities.ResourceInstanceChange) { | |
| v.log.Info( | |
| fmt.Sprintf("%s: Drift detected (%s)", c.Resource.Addr, c.Action), | |
| "type", json.MessageResourceDrift, | |
| "change", c, | |
| ) | |
| } | |
| func (v *JSONView) ChangeSummary(cs *json.ChangeSummary) { | |
| v.log.Info( | |
| cs.String(), | |
| "type", json.MessageChangeSummary, | |
| "changes", cs, | |
| ) | |
| } | |
| func (v *JSONView) Hook(h json.Hook) { | |
| v.log.Info( | |
| h.String(), | |
| "type", h.HookType(), | |
| "hook", h, | |
| ) | |
| } | |
| func (v *JSONView) Outputs(outputs jsonentities.Outputs) { | |
| v.log.Info( | |
| outputs.String(), | |
| "type", json.MessageOutputs, | |
| "outputs", outputs, | |
| ) | |
| } | |
| // Output is designed for supporting command.WrappedUi | |
| func (v *JSONView) Output(message string) { | |
| v.log.Info(message, "type", "output") | |
| } | |
| // Info is designed for supporting command.WrappedUi | |
| func (v *JSONView) Info(message string) { | |
| v.log.Info(message) | |
| } | |
| // Warn is designed for supporting command.WrappedUi | |
| func (v *JSONView) Warn(message string) { | |
| v.log.Warn(message) | |
| } | |
| // Error is designed for supporting command.WrappedUi | |
| func (v *JSONView) Error(message string) { | |
| v.log.Error(message) | |
| } |