Experiment with -json-into command output option

Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
Christian Mesh
2025-12-17 11:41:05 -05:00
parent 184830c031
commit 853b6f25bb
16 changed files with 212 additions and 60 deletions

View File

@@ -35,6 +35,8 @@ type Plan struct {
// ViewType specifies which output format to use
ViewType ViewType
JsonInto string
// ShowSensitive is used to display the value of variables marked as sensitive.
ShowSensitive bool
}
@@ -59,6 +61,7 @@ func ParsePlan(args []string) (*Plan, tfdiags.Diagnostics) {
var json bool
cmdFlags.BoolVar(&json, "json", false, "json")
cmdFlags.StringVar(&plan.JsonInto, "json-into", "", "json-into")
if err := cmdFlags.Parse(args); err != nil {
diags = diags.Append(tfdiags.Sourceless(
@@ -86,7 +89,7 @@ func ParsePlan(args []string) (*Plan, tfdiags.Diagnostics) {
}
switch {
case json:
case json || plan.JsonInto != "":
plan.ViewType = ViewJSON
default:
plan.ViewType = ViewHuman

View File

@@ -8,6 +8,7 @@ package command
import (
"context"
"fmt"
"os"
"strings"
"github.com/opentofu/opentofu/internal/command/views"
@@ -30,6 +31,7 @@ func (c *GetCommand) Run(args []string) int {
cmdFlags.BoolVar(&update, "update", false, "update")
cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory")
cmdFlags.BoolVar(&c.outputInJSON, "json", false, "json")
cmdFlags.StringVar(&c.outputJSONInto, "json-into", "", "json-into")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
@@ -40,9 +42,22 @@ func (c *GetCommand) Run(args []string) int {
c.Meta.Color = false
c.oldUi = c.Ui
c.Ui = &WrappedUi{
cliUi: c.oldUi,
jsonView: views.NewJSONView(c.View),
outputInJSON: true,
cliUi: c.oldUi,
jsonView: views.NewJSONView(c.View, nil),
onlyOutputInJSON: true,
}
}
if c.outputJSONInto != "" {
out, err := os.OpenFile(c.outputJSONInto, os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
panic(err)
}
c.oldUi = c.Ui
c.Ui = &WrappedUi{
cliUi: c.oldUi,
jsonView: views.NewJSONView(c.View, out),
onlyOutputInJSON: false,
}
}

View File

@@ -9,6 +9,7 @@ import (
"context"
"fmt"
"log"
"os"
"reflect"
"sort"
"strings"
@@ -73,6 +74,7 @@ func (c *InitCommand) Run(args []string) int {
cmdFlags.BoolVar(&c.Meta.ignoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local OpenTofu versions are incompatible")
cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory")
cmdFlags.BoolVar(&c.outputInJSON, "json", false, "json")
cmdFlags.StringVar(&c.outputJSONInto, "json-into", "", "json-into")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
@@ -83,9 +85,22 @@ func (c *InitCommand) Run(args []string) int {
c.Meta.Color = false
c.oldUi = c.Ui
c.Ui = &WrappedUi{
cliUi: c.oldUi,
jsonView: views.NewJSONView(c.View),
outputInJSON: true,
cliUi: c.oldUi,
jsonView: views.NewJSONView(c.View, nil),
onlyOutputInJSON: true,
}
}
if c.outputJSONInto != "" {
out, err := os.OpenFile(c.outputJSONInto, os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
panic(err)
}
c.oldUi = c.Ui
c.Ui = &WrappedUi{
cliUi: c.oldUi,
jsonView: views.NewJSONView(c.View, out),
onlyOutputInJSON: false,
}
}

View File

@@ -299,7 +299,8 @@ type Meta struct {
// state even if the remote and local OpenTofu versions don't match.
ignoreRemoteVersion bool
outputInJSON bool
outputInJSON bool
outputJSONInto string
// Used to cache the root module rootModuleCallCache and known variables.
// This helps prevent duplicate errors/warnings.
@@ -760,8 +761,17 @@ func (m *Meta) showDiagnostics(vals ...interface{}) {
return
}
if m.outputJSONInto != "" {
out, err := os.OpenFile(m.outputJSONInto, os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
panic(err)
}
jsonView := views.NewJSONView(m.View, out)
jsonView.Diagnostics(diags)
return
}
if m.outputInJSON {
jsonView := views.NewJSONView(m.View)
jsonView := views.NewJSONView(m.View, nil)
jsonView.Diagnostics(diags)
return
}

View File

@@ -19,9 +19,9 @@ import (
// implement cli.Ui interface, so that we can make all command support json
// output in a short time.
type WrappedUi struct {
cliUi cli.Ui
jsonView *views.JSONView
outputInJSON bool
cliUi cli.Ui
jsonView *views.JSONView
onlyOutputInJSON bool
}
func (m *WrappedUi) Ask(s string) (string, error) {
@@ -33,32 +33,32 @@ func (m *WrappedUi) AskSecret(s string) (string, error) {
}
func (m *WrappedUi) Output(s string) {
if m.outputInJSON {
m.jsonView.Output(s)
m.jsonView.Output(s)
if m.onlyOutputInJSON {
return
}
m.cliUi.Output(s)
}
func (m *WrappedUi) Info(s string) {
if m.outputInJSON {
m.jsonView.Info(s)
m.jsonView.Info(s)
if m.onlyOutputInJSON {
return
}
m.cliUi.Info(s)
}
func (m *WrappedUi) Error(s string) {
if m.outputInJSON {
m.jsonView.Error(s)
m.jsonView.Error(s)
if m.onlyOutputInJSON {
return
}
m.cliUi.Error(s)
}
func (m *WrappedUi) Warn(s string) {
if m.outputInJSON {
m.jsonView.Warn(s)
m.jsonView.Warn(s)
if m.onlyOutputInJSON {
return
}
m.cliUi.Warn(s)

View File

@@ -43,7 +43,7 @@ func (c *PlanCommand) Run(rawArgs []string) int {
// Instantiate the view, even if there are flag errors, so that we render
// diagnostics according to the desired view
view := views.NewPlan(args.ViewType, c.View)
view := views.NewPlan(args, c.View)
if diags.HasErrors() {
view.Diagnostics(diags)

View File

@@ -33,7 +33,7 @@ func NewApply(vt arguments.ViewType, destroy bool, view *View) Apply {
switch vt {
case arguments.ViewJSON:
return &ApplyJSON{
view: NewJSONView(view),
view: NewJSONView(view, nil),
destroy: destroy,
countHook: &countHook{},
}

View File

@@ -24,7 +24,7 @@ import (
// Test a sequence of hooks associated with creating a resource
func TestJSONHook_create(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
hook := newJSONHook(NewJSONView(NewView(streams)))
hook := newJSONHook(NewJSONView(NewView(streams), nil))
var nowMu sync.Mutex
now := time.Now()
@@ -195,7 +195,7 @@ func TestJSONHook_create(t *testing.T) {
func TestJSONHook_errors(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
hook := newJSONHook(NewJSONView(NewView(streams)))
hook := newJSONHook(NewJSONView(NewView(streams), nil))
addr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
@@ -282,7 +282,7 @@ func TestJSONHook_errors(t *testing.T) {
func TestJSONHook_refresh(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
hook := newJSONHook(NewJSONView(NewView(streams)))
hook := newJSONHook(NewJSONView(NewView(streams), nil))
addr := addrs.Resource{
Mode: addrs.DataResourceMode,
@@ -497,7 +497,7 @@ func TestJSONHook_ephemeral(t *testing.T) {
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
h := newJSONHook(NewJSONView(NewView(streams)))
h := newJSONHook(NewJSONView(NewView(streams), nil))
action, err := tt.preF(h)
if err != nil {

View File

@@ -8,6 +8,7 @@ package views
import (
encJson "encoding/json"
"fmt"
"os"
"github.com/hashicorp/go-hclog"
@@ -22,10 +23,13 @@ import (
// command/views/json package.
const JSON_UI_VERSION = "1.2"
func NewJSONView(view *View) *JSONView {
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: view.streams.Stdout.File,
Output: out,
JSONFormat: true,
JSONEscapeDisabled: true,
})

View File

@@ -27,7 +27,7 @@ import (
// convenient way to test that NewJSONView works.
func TestNewJSONView(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
NewJSONView(NewView(streams))
NewJSONView(NewView(streams), nil)
version := tfversion.String()
want := []map[string]interface{}{
@@ -78,7 +78,7 @@ func TestJSONView_Log(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.caseName, func(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
jv := NewJSONView(NewView(streams), nil)
jv.Log(tc.input)
testJSONViewOutputEquals(t, done(t).Stdout(), tc.want)
})
@@ -89,7 +89,7 @@ func TestJSONView_Log(t *testing.T) {
// complex diagnostics are tested elsewhere.
func TestJSONView_Diagnostics(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
jv := NewJSONView(NewView(streams), nil)
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.Sourceless(
@@ -134,7 +134,7 @@ func TestJSONView_Diagnostics(t *testing.T) {
func TestJSONView_DiagnosticsWithMetadata(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
jv := NewJSONView(NewView(streams), nil)
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.Sourceless(
@@ -181,7 +181,7 @@ func TestJSONView_DiagnosticsWithMetadata(t *testing.T) {
func TestJSONView_PlannedChange(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
jv := NewJSONView(NewView(streams), nil)
foo, diags := addrs.ParseModuleInstanceStr("module.foo")
if len(diags) > 0 {
@@ -222,7 +222,7 @@ func TestJSONView_PlannedChange(t *testing.T) {
func TestJSONView_ResourceDrift(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
jv := NewJSONView(NewView(streams), nil)
foo, diags := addrs.ParseModuleInstanceStr("module.foo")
if len(diags) > 0 {
@@ -263,7 +263,7 @@ func TestJSONView_ResourceDrift(t *testing.T) {
func TestJSONView_ChangeSummary(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
jv := NewJSONView(NewView(streams), nil)
jv.ChangeSummary(&viewsjson.ChangeSummary{
Add: 1,
@@ -293,7 +293,7 @@ func TestJSONView_ChangeSummary(t *testing.T) {
func TestJSONView_ChangeSummaryWithImport(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
jv := NewJSONView(NewView(streams), nil)
jv.ChangeSummary(&viewsjson.ChangeSummary{
Add: 1,
@@ -324,7 +324,7 @@ func TestJSONView_ChangeSummaryWithImport(t *testing.T) {
func TestJSONView_ChangeSummaryWithForget(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
jv := NewJSONView(NewView(streams), nil)
jv.ChangeSummary(&viewsjson.ChangeSummary{
Add: 1,
@@ -355,7 +355,7 @@ func TestJSONView_ChangeSummaryWithForget(t *testing.T) {
func TestJSONView_Hook(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
jv := NewJSONView(NewView(streams), nil)
foo, diags := addrs.ParseModuleInstanceStr("module.foo")
if len(diags) > 0 {
@@ -395,7 +395,7 @@ func TestJSONView_Hook(t *testing.T) {
func TestJSONView_Outputs(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
jv := NewJSONView(NewView(streams), nil)
jv.Outputs(jsonentities.Outputs{
"boop_count": {

View File

@@ -7,6 +7,7 @@ package views
import (
"bytes"
"errors"
"fmt"
"strings"
@@ -49,6 +50,66 @@ func NewOperation(vt arguments.ViewType, inAutomation bool, view *View) Operatio
}
}
type OperationMulti []Operation
var _ Operation = (OperationMulti)(nil)
func (o OperationMulti) Interrupted() {
for _, operation := range o {
operation.Interrupted()
}
}
func (o OperationMulti) FatalInterrupt() {
for _, operation := range o {
operation.FatalInterrupt()
}
}
func (o OperationMulti) Stopping() {
for _, operation := range o {
operation.Stopping()
}
}
func (o OperationMulti) Cancelled(planMode plans.Mode) {
for _, operation := range o {
operation.Cancelled(planMode)
}
}
func (o OperationMulti) EmergencyDumpState(stateFile *statefile.File, enc encryption.StateEncryption) error {
var errs []error
for _, operation := range o {
errs = append(errs, operation.EmergencyDumpState(stateFile, enc))
}
return errors.Join(errs...)
}
func (o OperationMulti) PlannedChange(change *plans.ResourceInstanceChangeSrc) {
for _, operation := range o {
operation.PlannedChange(change)
}
}
func (o OperationMulti) Plan(plan *plans.Plan, schemas *tofu.Schemas) {
for _, operation := range o {
operation.Plan(plan, schemas)
}
}
func (o OperationMulti) PlanNextStep(planPath string, genConfigPath string) {
for _, operation := range o {
operation.PlanNextStep(planPath, genConfigPath)
}
}
func (o OperationMulti) Diagnostics(diags tfdiags.Diagnostics) {
for _, operation := range o {
operation.Diagnostics(diags)
}
}
type OperationHuman struct {
view *View

View File

@@ -508,7 +508,7 @@ func TestOperation_planNextStepInAutomation(t *testing.T) {
// This test is not a realistic stream of messages.
func TestOperationJSON_logs(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
v := &OperationJSON{view: NewJSONView(NewView(streams))}
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
// Added an ephemeral resource change to double-check that it's not
// shown.
@@ -567,7 +567,7 @@ func TestOperationJSON_logs(t *testing.T) {
// we upgrade state format in the future.
func TestOperationJSON_emergencyDumpState(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
v := &OperationJSON{view: NewJSONView(NewView(streams))}
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
stateFile := statefile.New(nil, "foo", 1)
stateBuf := new(bytes.Buffer)
@@ -601,7 +601,7 @@ func TestOperationJSON_emergencyDumpState(t *testing.T) {
func TestOperationJSON_planNoChanges(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
v := &OperationJSON{view: NewJSONView(NewView(streams))}
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
plan := &plans.Plan{
Changes: plans.NewChanges(),
@@ -630,7 +630,7 @@ func TestOperationJSON_planNoChanges(t *testing.T) {
func TestOperationJSON_plan(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
v := &OperationJSON{view: NewJSONView(NewView(streams))}
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
root := addrs.RootModuleInstance
vpc, diags := addrs.ParseModuleInstanceStr("module.vpc")
@@ -799,7 +799,7 @@ func TestOperationJSON_plan(t *testing.T) {
func TestOperationJSON_planWithImport(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
v := &OperationJSON{view: NewJSONView(NewView(streams))}
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
root := addrs.RootModuleInstance
vpc, diags := addrs.ParseModuleInstanceStr("module.vpc")
@@ -947,7 +947,7 @@ func TestOperationJSON_planWithImport(t *testing.T) {
func TestOperationJSON_planDriftWithMove(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
v := &OperationJSON{view: NewJSONView(NewView(streams))}
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
root := addrs.RootModuleInstance
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"}
@@ -1085,7 +1085,7 @@ func TestOperationJSON_planDriftWithMove(t *testing.T) {
func TestOperationJSON_planDriftWithMoveRefreshOnly(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
v := &OperationJSON{view: NewJSONView(NewView(streams))}
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
root := addrs.RootModuleInstance
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"}
@@ -1217,7 +1217,7 @@ func TestOperationJSON_planDriftWithMoveRefreshOnly(t *testing.T) {
func TestOperationJSON_planOutputChanges(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
v := &OperationJSON{view: NewJSONView(NewView(streams))}
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
root := addrs.RootModuleInstance
@@ -1303,7 +1303,7 @@ func TestOperationJSON_planOutputChanges(t *testing.T) {
func TestOperationJSON_plannedChange(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
v := &OperationJSON{view: NewJSONView(NewView(streams))}
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
root := addrs.RootModuleInstance
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "boop"}

View File

@@ -7,6 +7,7 @@ package views
import (
"fmt"
"os"
"github.com/opentofu/opentofu/internal/command/arguments"
"github.com/opentofu/opentofu/internal/tfdiags"
@@ -23,19 +24,62 @@ type Plan interface {
}
// NewPlan returns an initialized Plan implementation for the given ViewType.
func NewPlan(vt arguments.ViewType, view *View) Plan {
switch vt {
func NewPlan(args *arguments.Plan, view *View) Plan {
human := &PlanHuman{
view: view,
inAutomation: view.RunningInAutomation(),
}
switch args.ViewType {
case arguments.ViewJSON:
if args.JsonInto != "" {
out, err := os.OpenFile(args.JsonInto, os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
panic(err)
}
return PlanMulti{human, &PlanJSON{
view: NewJSONView(view, out),
}}
}
return &PlanJSON{
view: NewJSONView(view),
view: NewJSONView(view, nil),
}
case arguments.ViewHuman:
return &PlanHuman{
view: view,
inAutomation: view.RunningInAutomation(),
}
return human
default:
panic(fmt.Sprintf("unknown view type %v", vt))
panic(fmt.Sprintf("unknown view type %v", args.ViewType))
}
}
type PlanMulti []Plan
var _ Plan = (PlanMulti)(nil)
func (p PlanMulti) Operation() Operation {
var operation OperationMulti
for _, plan := range p {
operation = append(operation, plan.Operation())
}
return operation
}
func (p PlanMulti) Hooks() []tofu.Hook {
var hooks []tofu.Hook
for _, plan := range p {
hooks = append(hooks, plan.Hooks()...)
}
return hooks
}
func (p PlanMulti) Diagnostics(diags tfdiags.Diagnostics) {
for _, plan := range p {
plan.Diagnostics(diags)
}
}
func (p PlanMulti) HelpPrompt() {
for _, plan := range p {
plan.HelpPrompt()
}
}

View File

@@ -24,7 +24,7 @@ import (
func TestPlanHuman_operation(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
defer done(t)
v := NewPlan(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)).Operation()
v := NewPlan(&arguments.Plan{ViewType: arguments.ViewHuman}, NewView(streams).SetRunningInAutomation(true)).Operation()
if hv, ok := v.(*OperationHuman); !ok {
t.Fatalf("unexpected return type %t", v)
} else if hv.inAutomation != true {
@@ -36,7 +36,7 @@ func TestPlanHuman_operation(t *testing.T) {
func TestPlanHuman_hooks(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
defer done(t)
v := NewPlan(arguments.ViewHuman, NewView(streams).SetRunningInAutomation((true)))
v := NewPlan(&arguments.Plan{ViewType: arguments.ViewHuman}, NewView(streams).SetRunningInAutomation((true)))
hooks := v.Hooks()
var uiHook *UiHook

View File

@@ -31,7 +31,7 @@ func NewRefresh(vt arguments.ViewType, view *View) Refresh {
switch vt {
case arguments.ViewJSON:
return &RefreshJSON{
view: NewJSONView(view),
view: NewJSONView(view, nil),
}
case arguments.ViewHuman:
return &RefreshHuman{

View File

@@ -78,7 +78,7 @@ func NewTest(vt arguments.ViewType, view *View) Test {
switch vt {
case arguments.ViewJSON:
return &TestJSON{
view: NewJSONView(view),
view: NewJSONView(view, nil),
}
case arguments.ViewHuman:
return &TestHuman{