| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package views | |
| import ( | |
| encJson "encoding/json" | |
| "fmt" | |
| "github.com/hashicorp/go-hclog" | |
| "github.com/hashicorp/hcl/v2" | |
| "github.com/hashicorp/terraform/internal/command/views/json" | |
| "github.com/hashicorp/terraform/internal/policy" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| tfversion "github.com/hashicorp/terraform/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.3" | |
| func NewJSONView(view *View) *JSONView { | |
| log := hclog.New(&hclog.LoggerOptions{ | |
| Name: "terraform.ui", | |
| Output: view.streams.Stdout.File, | |
| JSONFormat: 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("Terraform %s", version), | |
| "type", json.MessageVersion, | |
| "terraform", 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 ...interface{}) { | |
| sources := v.view.configSources() | |
| for _, diag := range diags { | |
| diagnostic := json.NewDiagnostic(diag, sources) | |
| args := []interface{}{"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 *json.ResourceInstanceChange) { | |
| v.log.Info( | |
| c.String(), | |
| "type", json.MessagePlannedChange, | |
| "change", c, | |
| ) | |
| } | |
| func (v *JSONView) PlannedActionInvocation(action *json.ActionInvocation) { | |
| v.log.Info( | |
| fmt.Sprintf("planned action invocation: %s", action.Action.Action), | |
| "type", json.MessagePlannedActionInvocation, | |
| "invocation", action, | |
| ) | |
| } | |
| func (v *JSONView) AppliedActionInvocation(action *json.ActionInvocation) { | |
| v.log.Info( | |
| fmt.Sprintf("applied action invocation: %s", action.Action.Action), | |
| "type", json.MessageAppliedActionInvocation, | |
| "invocation", action, | |
| ) | |
| } | |
| func (v *JSONView) ResourceDrift(c *json.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 json.Outputs) { | |
| v.log.Info( | |
| outputs.String(), | |
| "type", json.MessageOutputs, | |
| "outputs", outputs, | |
| ) | |
| } | |
| func (v *JSONView) logPolicyResult(addr string, resp policy.EvaluationResponse) { | |
| // Log all the info messages | |
| for _, enforcement := range resp.Enforcements { | |
| if enforcement.Message == "" { | |
| continue | |
| } | |
| var src []byte | |
| if enforcement.LocalRange != nil { | |
| src = v.view.configSources()[enforcement.LocalRange.Filename] | |
| } | |
| info := json.NewPolicyInfo(src, enforcement) | |
| args := []any{ | |
| "type", json.MessagePolicyInfo, | |
| "target_address", addr, | |
| json.MessagePolicyInfo, info, | |
| "@policy", "true", | |
| "result", enforcement.Result.String(), | |
| } | |
| if enforcement.Policy != nil { | |
| args = append(args, "policy_metadata", json.MetadataFromEnforcement(enforcement)) | |
| } | |
| v.log.Info("Policy info", args...) | |
| } | |
| for _, diag := range resp.Diagnostics { | |
| v.logPolicyDiagnostic(diag, "target_address", addr) | |
| } | |
| for _, policy := range resp.Policies { | |
| v.log.Info( | |
| "Policy Result", | |
| "type", json.MessagePolicyEvaluationResult, | |
| "result", policy.Result.String(), | |
| "target_address", addr, | |
| "policy_address", policy.Address, | |
| "@policy", "true", | |
| "policy_metadata", json.MetadataFromPolicy(*policy), | |
| ) | |
| } | |
| } | |
| func (v *JSONView) PolicyResult(addr string, resp policy.EvaluationResponse) { | |
| v.logPolicyResult(addr, resp) | |
| } | |
| // PolicyDiagnostics logs policy diagnostics that are not tied to a specific | |
| // target, such as setup diagnostics (e.g. a failure to connect to the policy | |
| // engine), so no target address is attached. | |
| func (v *JSONView) PolicyDiagnostics(diags policy.Diagnostics) { | |
| for _, diag := range diags { | |
| v.logPolicyDiagnostic(diag) | |
| } | |
| } | |
| // logPolicyDiagnostic logs the policy diagnostics. These are emitted as `policy_diagnostic` messages, | |
| // not standard Terraform `diagnostic` messages. We use the severity level only to | |
| // set the log level for that policy-specific output. | |
| func (v *JSONView) logPolicyDiagnostic(diag tfdiags.Diagnostic, extraArgs ...any) { | |
| sources := v.view.configSources() | |
| diagnostic := json.NewDiagnostic(diag, sources) | |
| args := []any{ | |
| "type", json.MessagePolicyDiagnostic, | |
| "@policy", "true", | |
| json.MessagePolicyDiagnostic, diagnostic, | |
| } | |
| args = append(args, extraArgs...) | |
| extra := tfdiags.ExtraInfo[*policy.PolicyExtra](diag) | |
| if extra != nil { | |
| policyMetadata := json.MetadataFromPolicy(extra.Policy) | |
| if extra.EnforceIndex != nil { | |
| policyMetadata.EnforceIndex = extra.EnforceIndex | |
| } | |
| args = append(args, "policy_metadata", policyMetadata) | |
| args = append(args, "result", extra.Result.String()) | |
| } | |
| switch extra.Severity { | |
| case hcl.DiagWarning: | |
| v.log.Warn(fmt.Sprintf("Warning: %s", diag.Description().Summary), args...) | |
| default: | |
| v.log.Error(fmt.Sprintf("Error: %s", diag.Description().Summary), args...) | |
| } | |
| } | |
| var _ Spacer = (*JSONView)(nil) | |
| // Spacer is a no-op for JSON view, as empty lines are not needed to space-out logs in machine-readable output. | |
| func (v *JSONView) Spacer() { | |
| // do nothing | |
| } |