Merge pull request #29312 from hashicorp/alisdair/json-output-planned-output-change

json-output: Add output changes to plan logs
This commit is contained in:
Alisdair McDiarmid 2021-08-06 10:10:05 -04:00 committed by GitHub
commit 10e431487f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 207 additions and 5 deletions

View File

@ -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))
} }

View File

@ -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": {

View File

@ -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 {

View File

@ -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))}

View File

@ -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.