json-output: Add output changes to plan logs
Extend the outputs JSON log message to support an `action` field (and make the `type` and `value` fields optional). This allows us to emit a useful output change summary as part of the plan, bringing the JSON log output into parity with the text output. While we do have access to the before/after values in the output changes, attempting to wedge those into a structured log message is not appropriate. That level of detail can be extracted from the JSON plan output from `terraform show -json`.
This commit is contained in:
parent
2f152f1139
commit
3b33dc1105
|
@ -6,14 +6,16 @@ import (
|
||||||
|
|
||||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
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/states"
|
||||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Output struct {
|
type Output struct {
|
||||||
Sensitive bool `json:"sensitive"`
|
Sensitive bool `json:"sensitive"`
|
||||||
Type json.RawMessage `json:"type"`
|
Type json.RawMessage `json:"type,omitempty"`
|
||||||
Value json.RawMessage `json:"value"`
|
Value json.RawMessage `json:"value,omitempty"`
|
||||||
|
Action ChangeAction `json:"action,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Outputs map[string]Output
|
type Outputs map[string]Output
|
||||||
|
@ -50,6 +52,19 @@ func OutputsFromMap(outputValues map[string]*states.OutputValue) (Outputs, tfdia
|
||||||
return outputs, nil
|
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 {
|
func (o Outputs) String() string {
|
||||||
return fmt.Sprintf("Outputs: %d", len(o))
|
return fmt.Sprintf("Outputs: %d", len(o))
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,9 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
"github.com/hashicorp/terraform/internal/lang/marks"
|
"github.com/hashicorp/terraform/internal/lang/marks"
|
||||||
|
"github.com/hashicorp/terraform/internal/plans"
|
||||||
"github.com/hashicorp/terraform/internal/states"
|
"github.com/hashicorp/terraform/internal/states"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"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) {
|
func TestOutputs_String(t *testing.T) {
|
||||||
outputs := Outputs{
|
outputs := Outputs{
|
||||||
"boop": {
|
"boop": {
|
||||||
|
|
|
@ -195,6 +195,17 @@ func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
|
||||||
}
|
}
|
||||||
|
|
||||||
v.view.ChangeSummary(cs)
|
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 {
|
func (v *OperationJSON) resourceDrift(oldState, newState *states.State, schemas *terraform.Schemas) error {
|
||||||
|
|
|
@ -763,6 +763,90 @@ func TestOperationJSON_planDrift(t *testing.T) {
|
||||||
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
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) {
|
func TestOperationJSON_plannedChange(t *testing.T) {
|
||||||
streams, done := terminal.StreamsForTesting(t)
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
v := &OperationJSON{view: NewJSONView(NewView(streams))}
|
v := &OperationJSON{view: NewJSONView(NewView(streams))}
|
||||||
|
|
|
@ -185,10 +185,11 @@ Terraform outputs a change summary when a plan or apply operation completes. Bot
|
||||||
|
|
||||||
## Outputs
|
## 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
|
- `action`: for planned outputs, the action which will be taken for the output. Values: `noop`, `create`, `update`, `delete`
|
||||||
- `type`: the detected HCL type of the output value
|
- `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
|
- `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.
|
Note that `sensitive` outputs still include the `value` field, and integrating software should respect the sensitivity value as appropriate for the given use case.
|
||||||
|
|
Loading…
Reference in New Issue