diff --git a/internal/command/views/json/output.go b/internal/command/views/json/output.go index 5c9334917..05070984a 100644 --- a/internal/command/views/json/output.go +++ b/internal/command/views/json/output.go @@ -6,14 +6,16 @@ import ( ctyjson "github.com/zclconf/go-cty/cty/json" + "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) type Output struct { Sensitive bool `json:"sensitive"` - Type json.RawMessage `json:"type"` - Value json.RawMessage `json:"value"` + Type json.RawMessage `json:"type,omitempty"` + Value json.RawMessage `json:"value,omitempty"` + Action ChangeAction `json:"action,omitempty"` } type Outputs map[string]Output @@ -50,6 +52,19 @@ func OutputsFromMap(outputValues map[string]*states.OutputValue) (Outputs, tfdia return outputs, nil } +func OutputsFromChanges(changes []*plans.OutputChangeSrc) Outputs { + outputs := make(map[string]Output, len(changes)) + + for _, change := range changes { + outputs[change.Addr.OutputValue.Name] = Output{ + Sensitive: change.Sensitive, + Action: changeAction(change.Action), + } + } + + return outputs +} + func (o Outputs) String() string { return fmt.Sprintf("Outputs: %d", len(o)) } diff --git a/internal/command/views/json/output_test.go b/internal/command/views/json/output_test.go index 5d8571974..f2c220f86 100644 --- a/internal/command/views/json/output_test.go +++ b/internal/command/views/json/output_test.go @@ -5,7 +5,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states" "github.com/zclconf/go-cty/cty" ) @@ -69,6 +71,95 @@ func TestOutputsFromMap(t *testing.T) { } } +func TestOutputsFromChanges(t *testing.T) { + root := addrs.RootModuleInstance + num, err := plans.NewDynamicValue(cty.NumberIntVal(1234), cty.Number) + str, err := plans.NewDynamicValue(cty.StringVal("1234"), cty.String) + if err != nil { + t.Fatalf("unexpected error creating dynamic value: %v", err) + } + + got := OutputsFromChanges([]*plans.OutputChangeSrc{ + // Unchanged output "boop", value 1234 + { + Addr: root.OutputValue("boop"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.NoOp, + Before: num, + After: num, + }, + Sensitive: false, + }, + // New output "beep", value 1234 + { + Addr: root.OutputValue("beep"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: nil, + After: num, + }, + Sensitive: false, + }, + // Deleted output "blorp", prior value 1234 + { + Addr: root.OutputValue("blorp"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: num, + After: nil, + }, + Sensitive: false, + }, + // Updated output "honk", prior value 1234, new value "1234" + { + Addr: root.OutputValue("honk"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Update, + Before: num, + After: str, + }, + Sensitive: false, + }, + // New sensitive output "secret", value "1234" + { + Addr: root.OutputValue("secret"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: nil, + After: str, + }, + Sensitive: true, + }, + }) + + want := Outputs{ + "boop": { + Action: "noop", + Sensitive: false, + }, + "beep": { + Action: "create", + Sensitive: false, + }, + "blorp": { + Action: "delete", + Sensitive: false, + }, + "honk": { + Action: "update", + Sensitive: false, + }, + "secret": { + Action: "create", + Sensitive: true, + }, + } + + if !cmp.Equal(want, got) { + t.Fatalf("unexpected result\n%s", cmp.Diff(want, got)) + } +} + func TestOutputs_String(t *testing.T) { outputs := Outputs{ "boop": { diff --git a/internal/command/views/operation.go b/internal/command/views/operation.go index c617dd187..b38e93b6f 100644 --- a/internal/command/views/operation.go +++ b/internal/command/views/operation.go @@ -195,6 +195,17 @@ func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) { } v.view.ChangeSummary(cs) + + var rootModuleOutputs []*plans.OutputChangeSrc + for _, output := range plan.Changes.Outputs { + if !output.Addr.Module.IsRoot() { + continue + } + rootModuleOutputs = append(rootModuleOutputs, output) + } + if len(rootModuleOutputs) > 0 { + v.view.Outputs(json.OutputsFromChanges(rootModuleOutputs)) + } } func (v *OperationJSON) resourceDrift(oldState, newState *states.State, schemas *terraform.Schemas) error { diff --git a/internal/command/views/operation_test.go b/internal/command/views/operation_test.go index e642dbfab..fd56350c5 100644 --- a/internal/command/views/operation_test.go +++ b/internal/command/views/operation_test.go @@ -763,6 +763,90 @@ func TestOperationJSON_planDrift(t *testing.T) { testJSONViewOutputEquals(t, done(t).Stdout(), want) } +func TestOperationJSON_planOutputChanges(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + v := &OperationJSON{view: NewJSONView(NewView(streams))} + + root := addrs.RootModuleInstance + + plan := &plans.Plan{ + Changes: &plans.Changes{ + Resources: []*plans.ResourceInstanceChangeSrc{}, + Outputs: []*plans.OutputChangeSrc{ + { + Addr: root.OutputValue("boop"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.NoOp, + }, + }, + { + Addr: root.OutputValue("beep"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + }, + }, + { + Addr: root.OutputValue("bonk"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + }, + }, + { + Addr: root.OutputValue("honk"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Update, + }, + Sensitive: true, + }, + }, + }, + } + v.Plan(plan, testSchemas()) + + want := []map[string]interface{}{ + // No resource changes + { + "@level": "info", + "@message": "Plan: 0 to add, 0 to change, 0 to destroy.", + "@module": "terraform.ui", + "type": "change_summary", + "changes": map[string]interface{}{ + "operation": "plan", + "add": float64(0), + "change": float64(0), + "remove": float64(0), + }, + }, + // Output changes + { + "@level": "info", + "@message": "Outputs: 4", + "@module": "terraform.ui", + "type": "outputs", + "outputs": map[string]interface{}{ + "boop": map[string]interface{}{ + "action": "noop", + "sensitive": false, + }, + "beep": map[string]interface{}{ + "action": "create", + "sensitive": false, + }, + "bonk": map[string]interface{}{ + "action": "delete", + "sensitive": false, + }, + "honk": map[string]interface{}{ + "action": "update", + "sensitive": true, + }, + }, + }, + } + + testJSONViewOutputEquals(t, done(t).Stdout(), want) +} + func TestOperationJSON_plannedChange(t *testing.T) { streams, done := terminal.StreamsForTesting(t) v := &OperationJSON{view: NewJSONView(NewView(streams))} diff --git a/website/docs/internals/machine-readable-ui.html.md b/website/docs/internals/machine-readable-ui.html.md index 742e4e9dc..76f675132 100644 --- a/website/docs/internals/machine-readable-ui.html.md +++ b/website/docs/internals/machine-readable-ui.html.md @@ -185,10 +185,11 @@ Terraform outputs a change summary when a plan or apply operation completes. Bot ## Outputs -After a successful apply, a message with type `outputs` contains the values of all root module output values. This message contains an `outputs` object, the keys of which are the output names. The outputs values are objects with the following keys: +After a successful plan or apply, a message with type `outputs` contains the values of all root module output values. This message contains an `outputs` object, the keys of which are the output names. The outputs values are objects with the following keys: -- `value:` the value of the output, encoded in JSON -- `type`: the detected HCL type of the output value +- `action`: for planned outputs, the action which will be taken for the output. Values: `noop`, `create`, `update`, `delete` +- `value`: for applied outputs, the value of the output, encoded in JSON +- `type`: for applied outputs, the detected HCL type of the output value - `sensitive`: boolean value, `true` if the output is sensitive and should be hidden from UI by default Note that `sensitive` outputs still include the `value` field, and integrating software should respect the sensitivity value as appropriate for the given use case.