From ac03d359979be8ea98403f1db25f595bca7f7b42 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Mon, 14 Jun 2021 09:19:13 -0400 Subject: [PATCH] jsonplan and jsonstate: include sensitive_values in state representations (#28889) * jsonplan and jsonstate: include sensitive_values in state representations A sensitive_values field has been added to the resource in state and planned values which is a map of all sensitive attributes with the values set to true. It wasn't entirely clear to me if the values in state would suffice, or if we also need to consult the schema - I believe that this is sufficient for state files written since v0.15, and if that's incorrect or insufficient, I'll add in the provider schema check as well. I also updated the documentation, and, since we've considered this before, bumped the FormatVersions for both jsonstate and jsonplan. --- internal/command/jsonplan/plan.go | 92 +------ internal/command/jsonplan/plan_test.go | 213 --------------- internal/command/jsonplan/resource.go | 6 + internal/command/jsonplan/values.go | 37 +++ internal/command/jsonplan/values_test.go | 7 +- internal/command/jsonstate/state.go | 99 ++++++- internal/command/jsonstate/state_test.go | 248 +++++++++++++++++- internal/command/show_test.go | 8 +- .../testdata/show-json-sensitive/output.json | 13 +- .../show-json-state/basic/output.json | 8 +- .../show-json-state/empty/output.json | 2 +- .../show-json-state/modules/output.json | 8 +- .../sensitive-variables/output.json | 5 +- .../show-json/basic-create/output.json | 13 +- .../show-json/basic-delete/output.json | 13 +- .../show-json/basic-update/output.json | 10 +- .../show-json/module-depends-on/output.json | 5 +- .../testdata/show-json/modules/output.json | 16 +- .../multi-resource-update/output.json | 13 +- .../show-json/nested-modules/output.json | 5 +- .../provider-version-no-config/output.json | 13 +- .../show-json/provider-version/output.json | 13 +- .../show-json/requires-replace/output.json | 10 +- .../show-json/sensitive-values/output.json | 7 +- website/docs/internals/json-format.html.md | 11 +- 25 files changed, 497 insertions(+), 378 deletions(-) diff --git a/internal/command/jsonplan/plan.go b/internal/command/jsonplan/plan.go index d46e56949470..ee9a08947ef6 100644 --- a/internal/command/jsonplan/plan.go +++ b/internal/command/jsonplan/plan.go @@ -22,7 +22,7 @@ import ( // FormatVersion represents the version of the json format and will be // incremented for any change to this format that requires changes to a // consuming parser. -const FormatVersion = "0.1" +const FormatVersion = "0.2" // Plan is the top-level representation of the json format of a plan. It includes // the complete config and current state. @@ -259,8 +259,8 @@ func (p *plan) marshalResourceDrift(oldState, newState *states.State, schemas *t } else { newVal = cty.NullVal(ty) } - oldSensitive := sensitiveAsBool(oldVal) - newSensitive := sensitiveAsBool(newVal) + oldSensitive := jsonstate.SensitiveAsBool(oldVal) + newSensitive := jsonstate.SensitiveAsBool(newVal) oldVal, _ = oldVal.UnmarkDeep() newVal, _ = newVal.UnmarkDeep() @@ -367,7 +367,7 @@ func (p *plan) marshalResourceChanges(changes *plans.Changes, schemas *terraform if schema.ContainsSensitive() { marks = append(marks, schema.ValueMarks(changeV.Before, nil)...) } - bs := sensitiveAsBool(changeV.Before.MarkWithPaths(marks)) + bs := jsonstate.SensitiveAsBool(changeV.Before.MarkWithPaths(marks)) beforeSensitive, err = ctyjson.Marshal(bs, bs.Type()) if err != nil { return err @@ -396,7 +396,7 @@ func (p *plan) marshalResourceChanges(changes *plans.Changes, schemas *terraform if schema.ContainsSensitive() { marks = append(marks, schema.ValueMarks(changeV.After, nil)...) } - as := sensitiveAsBool(changeV.After.MarkWithPaths(marks)) + as := jsonstate.SensitiveAsBool(changeV.After.MarkWithPaths(marks)) afterSensitive, err = ctyjson.Marshal(as, as.Type()) if err != nil { return err @@ -682,88 +682,6 @@ func unknownAsBool(val cty.Value) cty.Value { } } -// recursively iterate through a marked cty.Value, replacing sensitive values -// with cty.True and non-sensitive values with cty.False. -// -// The result also normalizes some types: all sequence types are turned into -// tuple types and all mapping types are converted to object types, since we -// assume the result of this is just going to be serialized as JSON (and thus -// lose those distinctions) anyway. -// -// For map/object values, all non-sensitive attribute values will be omitted -// instead of returning false, as this results in a more compact serialization. -func sensitiveAsBool(val cty.Value) cty.Value { - if val.HasMark("sensitive") { - return cty.True - } - - ty := val.Type() - switch { - case val.IsNull(), ty.IsPrimitiveType(), ty.Equals(cty.DynamicPseudoType): - return cty.False - case ty.IsListType() || ty.IsTupleType() || ty.IsSetType(): - if !val.IsKnown() { - // If the collection is unknown we can't say anything about the - // sensitivity of its contents - return cty.EmptyTupleVal - } - length := val.LengthInt() - if length == 0 { - // If there are no elements then we can't have sensitive values - return cty.EmptyTupleVal - } - vals := make([]cty.Value, 0, length) - it := val.ElementIterator() - for it.Next() { - _, v := it.Element() - vals = append(vals, sensitiveAsBool(v)) - } - // The above transform may have changed the types of some of the - // elements, so we'll always use a tuple here in case we've now made - // different elements have different types. Our ultimate goal is to - // marshal to JSON anyway, and all of these sequence types are - // indistinguishable in JSON. - return cty.TupleVal(vals) - case ty.IsMapType() || ty.IsObjectType(): - if !val.IsKnown() { - // If the map/object is unknown we can't say anything about the - // sensitivity of its attributes - return cty.EmptyObjectVal - } - var length int - switch { - case ty.IsMapType(): - length = val.LengthInt() - default: - length = len(val.Type().AttributeTypes()) - } - if length == 0 { - // If there are no elements then we can't have sensitive values - return cty.EmptyObjectVal - } - vals := make(map[string]cty.Value) - it := val.ElementIterator() - for it.Next() { - k, v := it.Element() - s := sensitiveAsBool(v) - // Omit all of the "false"s for non-sensitive values for more - // compact serialization - if !s.RawEquals(cty.False) { - vals[k.AsString()] = s - } - } - // The above transform may have changed the types of some of the - // elements, so we'll always use an object here in case we've now made - // different elements have different types. Our ultimate goal is to - // marshal to JSON anyway, and all of these mapping types are - // indistinguishable in JSON. - return cty.ObjectVal(vals) - default: - // Should never happen, since the above should cover all types - panic(fmt.Sprintf("sensitiveAsBool cannot handle %#v", val)) - } -} - func actionString(action string) []string { switch { case action == "NoOp": diff --git a/internal/command/jsonplan/plan_test.go b/internal/command/jsonplan/plan_test.go index 5656a6231380..cf23187e506f 100644 --- a/internal/command/jsonplan/plan_test.go +++ b/internal/command/jsonplan/plan_test.go @@ -265,219 +265,6 @@ func TestUnknownAsBool(t *testing.T) { } } -func TestSensitiveAsBool(t *testing.T) { - sensitive := "sensitive" - tests := []struct { - Input cty.Value - Want cty.Value - }{ - { - cty.StringVal("hello"), - cty.False, - }, - { - cty.NullVal(cty.String), - cty.False, - }, - { - cty.StringVal("hello").Mark(sensitive), - cty.True, - }, - { - cty.NullVal(cty.String).Mark(sensitive), - cty.True, - }, - - { - cty.NullVal(cty.DynamicPseudoType).Mark(sensitive), - cty.True, - }, - { - cty.NullVal(cty.Object(map[string]cty.Type{"test": cty.String})), - cty.False, - }, - { - cty.NullVal(cty.Object(map[string]cty.Type{"test": cty.String})).Mark(sensitive), - cty.True, - }, - { - cty.DynamicVal, - cty.False, - }, - { - cty.DynamicVal.Mark(sensitive), - cty.True, - }, - - { - cty.ListValEmpty(cty.String), - cty.EmptyTupleVal, - }, - { - cty.ListValEmpty(cty.String).Mark(sensitive), - cty.True, - }, - { - cty.ListVal([]cty.Value{ - cty.StringVal("hello"), - cty.StringVal("friend").Mark(sensitive), - }), - cty.TupleVal([]cty.Value{ - cty.False, - cty.True, - }), - }, - { - cty.SetValEmpty(cty.String), - cty.EmptyTupleVal, - }, - { - cty.SetValEmpty(cty.String).Mark(sensitive), - cty.True, - }, - { - cty.SetVal([]cty.Value{cty.StringVal("hello")}), - cty.TupleVal([]cty.Value{cty.False}), - }, - { - cty.SetVal([]cty.Value{cty.StringVal("hello").Mark(sensitive)}), - cty.True, - }, - { - cty.EmptyTupleVal.Mark(sensitive), - cty.True, - }, - { - cty.TupleVal([]cty.Value{ - cty.StringVal("hello"), - cty.StringVal("friend").Mark(sensitive), - }), - cty.TupleVal([]cty.Value{ - cty.False, - cty.True, - }), - }, - { - cty.MapValEmpty(cty.String), - cty.EmptyObjectVal, - }, - { - cty.MapValEmpty(cty.String).Mark(sensitive), - cty.True, - }, - { - cty.MapVal(map[string]cty.Value{ - "greeting": cty.StringVal("hello"), - "animal": cty.StringVal("horse"), - }), - cty.EmptyObjectVal, - }, - { - cty.MapVal(map[string]cty.Value{ - "greeting": cty.StringVal("hello"), - "animal": cty.StringVal("horse").Mark(sensitive), - }), - cty.ObjectVal(map[string]cty.Value{ - "animal": cty.True, - }), - }, - { - cty.MapVal(map[string]cty.Value{ - "greeting": cty.StringVal("hello"), - "animal": cty.StringVal("horse").Mark(sensitive), - }).Mark(sensitive), - cty.True, - }, - { - cty.EmptyObjectVal, - cty.EmptyObjectVal, - }, - { - cty.ObjectVal(map[string]cty.Value{ - "greeting": cty.StringVal("hello"), - "animal": cty.StringVal("horse"), - }), - cty.EmptyObjectVal, - }, - { - cty.ObjectVal(map[string]cty.Value{ - "greeting": cty.StringVal("hello"), - "animal": cty.StringVal("horse").Mark(sensitive), - }), - cty.ObjectVal(map[string]cty.Value{ - "animal": cty.True, - }), - }, - { - cty.ObjectVal(map[string]cty.Value{ - "greeting": cty.StringVal("hello"), - "animal": cty.StringVal("horse").Mark(sensitive), - }).Mark(sensitive), - cty.True, - }, - { - cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "a": cty.UnknownVal(cty.String), - }), - cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("known").Mark(sensitive), - }), - }), - cty.TupleVal([]cty.Value{ - cty.EmptyObjectVal, - cty.ObjectVal(map[string]cty.Value{ - "a": cty.True, - }), - }), - }, - { - cty.ListVal([]cty.Value{ - cty.MapValEmpty(cty.String), - cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("known").Mark(sensitive), - }), - cty.MapVal(map[string]cty.Value{ - "a": cty.UnknownVal(cty.String), - }), - }), - cty.TupleVal([]cty.Value{ - cty.EmptyObjectVal, - cty.ObjectVal(map[string]cty.Value{ - "a": cty.True, - }), - cty.EmptyObjectVal, - }), - }, - { - cty.ObjectVal(map[string]cty.Value{ - "list": cty.UnknownVal(cty.List(cty.String)), - "set": cty.UnknownVal(cty.Set(cty.Bool)), - "tuple": cty.UnknownVal(cty.Tuple([]cty.Type{cty.String, cty.Number})), - "map": cty.UnknownVal(cty.Map(cty.String)), - "object": cty.UnknownVal(cty.Object(map[string]cty.Type{"a": cty.String})), - }), - cty.ObjectVal(map[string]cty.Value{ - "list": cty.EmptyTupleVal, - "set": cty.EmptyTupleVal, - "tuple": cty.EmptyTupleVal, - "map": cty.EmptyObjectVal, - "object": cty.EmptyObjectVal, - }), - }, - } - - for _, test := range tests { - got := sensitiveAsBool(test.Input) - if !reflect.DeepEqual(got, test.Want) { - t.Errorf( - "wrong result\ninput: %#v\ngot: %#v\nwant: %#v", - test.Input, got, test.Want, - ) - } - } -} - func TestEncodePaths(t *testing.T) { tests := map[string]struct { Input cty.PathSet diff --git a/internal/command/jsonplan/resource.go b/internal/command/jsonplan/resource.go index 33f064fc3676..ca1299c994a7 100644 --- a/internal/command/jsonplan/resource.go +++ b/internal/command/jsonplan/resource.go @@ -1,6 +1,8 @@ package jsonplan import ( + "encoding/json" + "github.com/hashicorp/terraform/internal/addrs" ) @@ -33,6 +35,10 @@ type resource struct { // unknown values are omitted or set to null, making them indistinguishable // from absent values. AttributeValues attributeValues `json:"values,omitempty"` + + // SensitiveValues is similar to AttributeValues, but with all sensitive + // values replaced with true, and all non-sensitive leaf values omitted. + SensitiveValues json.RawMessage `json:"sensitive_values,omitempty"` } // resourceChange is a description of an individual change action that Terraform diff --git a/internal/command/jsonplan/values.go b/internal/command/jsonplan/values.go index 0e3c8ba454ab..d79703215fbf 100644 --- a/internal/command/jsonplan/values.go +++ b/internal/command/jsonplan/values.go @@ -9,6 +9,7 @@ import ( ctyjson "github.com/zclconf/go-cty/cty/json" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/command/jsonstate" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states" @@ -198,6 +199,10 @@ func marshalPlanResources(changes *plans.Changes, ris []addrs.AbsResourceInstanc if err != nil { return nil, err } + + // copy the marked After values so we can use these in marshalSensitiveValues + markedAfter := changeV.After + // The values may be marked, but we must rely on the Sensitive flag // as the decoded value is only an intermediate step in transcoding // this to a json format. @@ -213,6 +218,13 @@ func marshalPlanResources(changes *plans.Changes, ris []addrs.AbsResourceInstanc } } + s := jsonstate.SensitiveAsBool(markedAfter) + v, err := ctyjson.Marshal(s, s.Type()) + if err != nil { + return nil, err + } + resource.SensitiveValues = v + ret = append(ret, resource) } @@ -262,3 +274,28 @@ func marshalPlanModules( return ret, nil } + +// marshalSensitiveValues returns a map of sensitive attributes, with the value +// set to true. It returns nil if the value is nil or if there are no sensitive +// vals. +func marshalSensitiveValues(value cty.Value) map[string]bool { + if value.RawEquals(cty.NilVal) || value.IsNull() { + return nil + } + + ret := make(map[string]bool) + + it := value.ElementIterator() + for it.Next() { + k, v := it.Element() + s := jsonstate.SensitiveAsBool(v) + if !s.RawEquals(cty.False) { + ret[k.AsString()] = true + } + } + + if len(ret) == 0 { + return nil + } + return ret +} diff --git a/internal/command/jsonplan/values_test.go b/internal/command/jsonplan/values_test.go index fded98fcdfb2..8f55b8f81da6 100644 --- a/internal/command/jsonplan/values_test.go +++ b/internal/command/jsonplan/values_test.go @@ -192,7 +192,7 @@ func TestMarshalPlanResources(t *testing.T) { "woozles": cty.UnknownVal(cty.String), "foozles": cty.UnknownVal(cty.String), }), - Want: []resource{resource{ + Want: []resource{{ Address: "test_thing.example", Mode: "managed", Type: "test_thing", @@ -201,6 +201,7 @@ func TestMarshalPlanResources(t *testing.T) { ProviderName: "registry.terraform.io/hashicorp/test", SchemaVersion: 1, AttributeValues: attributeValues{}, + SensitiveValues: json.RawMessage("{}"), }}, Err: false, }, @@ -234,7 +235,7 @@ func TestMarshalPlanResources(t *testing.T) { "woozles": cty.StringVal("baz"), "foozles": cty.StringVal("bat"), }), - Want: []resource{resource{ + Want: []resource{{ Address: "test_thing.example", Mode: "managed", Type: "test_thing", @@ -243,10 +244,10 @@ func TestMarshalPlanResources(t *testing.T) { ProviderName: "registry.terraform.io/hashicorp/test", SchemaVersion: 1, AttributeValues: attributeValues{ - "woozles": json.RawMessage(`"baz"`), "foozles": json.RawMessage(`"bat"`), }, + SensitiveValues: json.RawMessage("{}"), }}, Err: false, }, diff --git a/internal/command/jsonstate/state.go b/internal/command/jsonstate/state.go index 6eedf2bbfd98..57921dfb0c63 100644 --- a/internal/command/jsonstate/state.go +++ b/internal/command/jsonstate/state.go @@ -17,7 +17,7 @@ import ( // FormatVersion represents the version of the json format and will be // incremented for any change to this format that requires changes to a // consuming parser. -const FormatVersion = "0.1" +const FormatVersion = "0.2" // state is the top-level representation of the json format of a terraform // state. @@ -84,6 +84,10 @@ type resource struct { // from absent values. AttributeValues attributeValues `json:"values,omitempty"` + // SensitiveValues is similar to AttributeValues, but with all sensitive + // values replaced with true, and all non-sensitive leaf values omitted. + SensitiveValues json.RawMessage `json:"sensitive_values,omitempty"` + // DependsOn contains a list of the resource's dependencies. The entries are // addresses relative to the containing module. DependsOn []string `json:"depends_on,omitempty"` @@ -193,10 +197,11 @@ func marshalRootModule(s *states.State, schemas *terraform.Schemas) (module, err var err error ret.Address = "" - ret.Resources, err = marshalResources(s.RootModule().Resources, addrs.RootModuleInstance, schemas) + rs, err := marshalResources(s.RootModule().Resources, addrs.RootModuleInstance, schemas) if err != nil { return ret, err } + ret.Resources = rs // build a map of module -> set[child module addresses] moduleChildSet := make(map[string]map[string]struct{}) @@ -324,6 +329,15 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module current.AttributeValues = marshalAttributeValues(riObj.Value) + // Mark the resource instance value with any marks stored in AttrSensitivePaths so we can build the SensitiveValues object + markedVal := riObj.Value.MarkWithPaths(ri.Current.AttrSensitivePaths) + s := SensitiveAsBool(markedVal) + v, err := ctyjson.Marshal(s, s.Type()) + if err != nil { + return nil, err + } + current.SensitiveValues = v + if len(riObj.Dependencies) > 0 { dependencies := make([]string, len(riObj.Dependencies)) for i, v := range riObj.Dependencies { @@ -356,6 +370,15 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module deposed.AttributeValues = marshalAttributeValues(riObj.Value) + // Mark the resource instance value with any marks stored in AttrSensitivePaths so we can build the SensitiveValues object + markedVal := riObj.Value.MarkWithPaths(rios.AttrSensitivePaths) + s := SensitiveAsBool(markedVal) + v, err := ctyjson.Marshal(s, s.Type()) + if err != nil { + return nil, err + } + deposed.SensitiveValues = v + if len(riObj.Dependencies) > 0 { dependencies := make([]string, len(riObj.Dependencies)) for i, v := range riObj.Dependencies { @@ -379,3 +402,75 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module return ret, nil } + +func SensitiveAsBool(val cty.Value) cty.Value { + if val.HasMark("sensitive") { + return cty.True + } + + ty := val.Type() + switch { + case val.IsNull(), ty.IsPrimitiveType(), ty.Equals(cty.DynamicPseudoType): + return cty.False + case ty.IsListType() || ty.IsTupleType() || ty.IsSetType(): + if !val.IsKnown() { + // If the collection is unknown we can't say anything about the + // sensitivity of its contents + return cty.EmptyTupleVal + } + length := val.LengthInt() + if length == 0 { + // If there are no elements then we can't have sensitive values + return cty.EmptyTupleVal + } + vals := make([]cty.Value, 0, length) + it := val.ElementIterator() + for it.Next() { + _, v := it.Element() + vals = append(vals, SensitiveAsBool(v)) + } + // The above transform may have changed the types of some of the + // elements, so we'll always use a tuple here in case we've now made + // different elements have different types. Our ultimate goal is to + // marshal to JSON anyway, and all of these sequence types are + // indistinguishable in JSON. + return cty.TupleVal(vals) + case ty.IsMapType() || ty.IsObjectType(): + if !val.IsKnown() { + // If the map/object is unknown we can't say anything about the + // sensitivity of its attributes + return cty.EmptyObjectVal + } + var length int + switch { + case ty.IsMapType(): + length = val.LengthInt() + default: + length = len(val.Type().AttributeTypes()) + } + if length == 0 { + // If there are no elements then we can't have sensitive values + return cty.EmptyObjectVal + } + vals := make(map[string]cty.Value) + it := val.ElementIterator() + for it.Next() { + k, v := it.Element() + s := SensitiveAsBool(v) + // Omit all of the "false"s for non-sensitive values for more + // compact serialization + if !s.RawEquals(cty.False) { + vals[k.AsString()] = s + } + } + // The above transform may have changed the types of some of the + // elements, so we'll always use an object here in case we've now made + // different elements have different types. Our ultimate goal is to + // marshal to JSON anyway, and all of these mapping types are + // indistinguishable in JSON. + return cty.ObjectVal(vals) + default: + // Should never happen, since the above should cover all types + panic(fmt.Sprintf("sensitiveAsBool cannot handle %#v", val)) + } +} diff --git a/internal/command/jsonstate/state_test.go b/internal/command/jsonstate/state_test.go index 7f7b0d3d15b1..a7e8b3b1d3e7 100644 --- a/internal/command/jsonstate/state_test.go +++ b/internal/command/jsonstate/state_test.go @@ -5,6 +5,7 @@ import ( "reflect" "testing" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/states" @@ -180,7 +181,7 @@ func TestMarshalResources(t *testing.T) { }, testSchemas(), []resource{ - resource{ + { Address: "test_thing.bar", Mode: "managed", Type: "test_thing", @@ -191,6 +192,7 @@ func TestMarshalResources(t *testing.T) { "foozles": json.RawMessage(`null`), "woozles": json.RawMessage(`"confuzles"`), }, + SensitiveValues: json.RawMessage("{}"), }, }, false, @@ -250,7 +252,7 @@ func TestMarshalResources(t *testing.T) { }, testSchemas(), []resource{ - resource{ + { Address: "test_thing.bar[0]", Mode: "managed", Type: "test_thing", @@ -261,6 +263,7 @@ func TestMarshalResources(t *testing.T) { "foozles": json.RawMessage(`null`), "woozles": json.RawMessage(`"confuzles"`), }, + SensitiveValues: json.RawMessage("{}"), }, }, false, @@ -291,7 +294,7 @@ func TestMarshalResources(t *testing.T) { }, testSchemas(), []resource{ - resource{ + { Address: "test_thing.bar[\"rockhopper\"]", Mode: "managed", Type: "test_thing", @@ -302,6 +305,7 @@ func TestMarshalResources(t *testing.T) { "foozles": json.RawMessage(`null`), "woozles": json.RawMessage(`"confuzles"`), }, + SensitiveValues: json.RawMessage("{}"), }, }, false, @@ -319,7 +323,7 @@ func TestMarshalResources(t *testing.T) { Instances: map[addrs.InstanceKey]*states.ResourceInstance{ addrs.NoKey: { Deposed: map[states.DeposedKey]*states.ResourceInstanceObjectSrc{ - states.DeposedKey(deposedKey): &states.ResourceInstanceObjectSrc{ + states.DeposedKey(deposedKey): { Status: states.ObjectReady, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, @@ -334,7 +338,7 @@ func TestMarshalResources(t *testing.T) { }, testSchemas(), []resource{ - resource{ + { Address: "test_thing.bar", Mode: "managed", Type: "test_thing", @@ -346,6 +350,7 @@ func TestMarshalResources(t *testing.T) { "foozles": json.RawMessage(`null`), "woozles": json.RawMessage(`"confuzles"`), }, + SensitiveValues: json.RawMessage("{}"), }, }, false, @@ -363,7 +368,7 @@ func TestMarshalResources(t *testing.T) { Instances: map[addrs.InstanceKey]*states.ResourceInstance{ addrs.NoKey: { Deposed: map[states.DeposedKey]*states.ResourceInstanceObjectSrc{ - states.DeposedKey(deposedKey): &states.ResourceInstanceObjectSrc{ + states.DeposedKey(deposedKey): { Status: states.ObjectReady, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, @@ -382,7 +387,7 @@ func TestMarshalResources(t *testing.T) { }, testSchemas(), []resource{ - resource{ + { Address: "test_thing.bar", Mode: "managed", Type: "test_thing", @@ -393,8 +398,9 @@ func TestMarshalResources(t *testing.T) { "foozles": json.RawMessage(`null`), "woozles": json.RawMessage(`"confuzles"`), }, + SensitiveValues: json.RawMessage("{}"), }, - resource{ + { Address: "test_thing.bar", Mode: "managed", Type: "test_thing", @@ -406,6 +412,7 @@ func TestMarshalResources(t *testing.T) { "foozles": json.RawMessage(`null`), "woozles": json.RawMessage(`"confuzles"`), }, + SensitiveValues: json.RawMessage("{}"), }, }, false, @@ -423,10 +430,12 @@ func TestMarshalResources(t *testing.T) { } else if err != nil { t.Fatalf("unexpected error: %s", err) } - eq := reflect.DeepEqual(got, test.Want) - if !eq { - t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want) + + diff := cmp.Diff(got, test.Want) + if diff != "" { + t.Fatalf("wrong result: %s\n", diff) } + }) } } @@ -629,12 +638,12 @@ func TestMarshalModules_parent_no_resources(t *testing.T) { func testSchemas() *terraform.Schemas { return &terraform.Schemas{ Providers: map[addrs.Provider]*terraform.ProviderSchema{ - addrs.NewDefaultProvider("test"): &terraform.ProviderSchema{ + addrs.NewDefaultProvider("test"): { ResourceTypes: map[string]*configschema.Block{ "test_thing": { Attributes: map[string]*configschema.Attribute{ "woozles": {Type: cty.String, Optional: true, Computed: true}, - "foozles": {Type: cty.String, Optional: true}, + "foozles": {Type: cty.String, Optional: true, Sensitive: true}, }, }, "test_instance": { @@ -649,3 +658,216 @@ func testSchemas() *terraform.Schemas { }, } } + +func TestSensitiveAsBool(t *testing.T) { + sensitive := "sensitive" + tests := []struct { + Input cty.Value + Want cty.Value + }{ + { + cty.StringVal("hello"), + cty.False, + }, + { + cty.NullVal(cty.String), + cty.False, + }, + { + cty.StringVal("hello").Mark(sensitive), + cty.True, + }, + { + cty.NullVal(cty.String).Mark(sensitive), + cty.True, + }, + + { + cty.NullVal(cty.DynamicPseudoType).Mark(sensitive), + cty.True, + }, + { + cty.NullVal(cty.Object(map[string]cty.Type{"test": cty.String})), + cty.False, + }, + { + cty.NullVal(cty.Object(map[string]cty.Type{"test": cty.String})).Mark(sensitive), + cty.True, + }, + { + cty.DynamicVal, + cty.False, + }, + { + cty.DynamicVal.Mark(sensitive), + cty.True, + }, + + { + cty.ListValEmpty(cty.String), + cty.EmptyTupleVal, + }, + { + cty.ListValEmpty(cty.String).Mark(sensitive), + cty.True, + }, + { + cty.ListVal([]cty.Value{ + cty.StringVal("hello"), + cty.StringVal("friend").Mark(sensitive), + }), + cty.TupleVal([]cty.Value{ + cty.False, + cty.True, + }), + }, + { + cty.SetValEmpty(cty.String), + cty.EmptyTupleVal, + }, + { + cty.SetValEmpty(cty.String).Mark(sensitive), + cty.True, + }, + { + cty.SetVal([]cty.Value{cty.StringVal("hello")}), + cty.TupleVal([]cty.Value{cty.False}), + }, + { + cty.SetVal([]cty.Value{cty.StringVal("hello").Mark(sensitive)}), + cty.True, + }, + { + cty.EmptyTupleVal.Mark(sensitive), + cty.True, + }, + { + cty.TupleVal([]cty.Value{ + cty.StringVal("hello"), + cty.StringVal("friend").Mark(sensitive), + }), + cty.TupleVal([]cty.Value{ + cty.False, + cty.True, + }), + }, + { + cty.MapValEmpty(cty.String), + cty.EmptyObjectVal, + }, + { + cty.MapValEmpty(cty.String).Mark(sensitive), + cty.True, + }, + { + cty.MapVal(map[string]cty.Value{ + "greeting": cty.StringVal("hello"), + "animal": cty.StringVal("horse"), + }), + cty.EmptyObjectVal, + }, + { + cty.MapVal(map[string]cty.Value{ + "greeting": cty.StringVal("hello"), + "animal": cty.StringVal("horse").Mark(sensitive), + }), + cty.ObjectVal(map[string]cty.Value{ + "animal": cty.True, + }), + }, + { + cty.MapVal(map[string]cty.Value{ + "greeting": cty.StringVal("hello"), + "animal": cty.StringVal("horse").Mark(sensitive), + }).Mark(sensitive), + cty.True, + }, + { + cty.EmptyObjectVal, + cty.EmptyObjectVal, + }, + { + cty.ObjectVal(map[string]cty.Value{ + "greeting": cty.StringVal("hello"), + "animal": cty.StringVal("horse"), + }), + cty.EmptyObjectVal, + }, + { + cty.ObjectVal(map[string]cty.Value{ + "greeting": cty.StringVal("hello"), + "animal": cty.StringVal("horse").Mark(sensitive), + }), + cty.ObjectVal(map[string]cty.Value{ + "animal": cty.True, + }), + }, + { + cty.ObjectVal(map[string]cty.Value{ + "greeting": cty.StringVal("hello"), + "animal": cty.StringVal("horse").Mark(sensitive), + }).Mark(sensitive), + cty.True, + }, + { + cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "a": cty.UnknownVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("known").Mark(sensitive), + }), + }), + cty.TupleVal([]cty.Value{ + cty.EmptyObjectVal, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.True, + }), + }), + }, + { + cty.ListVal([]cty.Value{ + cty.MapValEmpty(cty.String), + cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal("known").Mark(sensitive), + }), + cty.MapVal(map[string]cty.Value{ + "a": cty.UnknownVal(cty.String), + }), + }), + cty.TupleVal([]cty.Value{ + cty.EmptyObjectVal, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.True, + }), + cty.EmptyObjectVal, + }), + }, + { + cty.ObjectVal(map[string]cty.Value{ + "list": cty.UnknownVal(cty.List(cty.String)), + "set": cty.UnknownVal(cty.Set(cty.Bool)), + "tuple": cty.UnknownVal(cty.Tuple([]cty.Type{cty.String, cty.Number})), + "map": cty.UnknownVal(cty.Map(cty.String)), + "object": cty.UnknownVal(cty.Object(map[string]cty.Type{"a": cty.String})), + }), + cty.ObjectVal(map[string]cty.Value{ + "list": cty.EmptyTupleVal, + "set": cty.EmptyTupleVal, + "tuple": cty.EmptyTupleVal, + "map": cty.EmptyObjectVal, + "object": cty.EmptyObjectVal, + }), + }, + } + + for _, test := range tests { + got := SensitiveAsBool(test.Input) + if !reflect.DeepEqual(got, test.Want) { + t.Errorf( + "wrong result\ninput: %#v\ngot: %#v\nwant: %#v", + test.Input, got, test.Want, + ) + } + } +} diff --git a/internal/command/show_test.go b/internal/command/show_test.go index a26ad3746324..a89d15dea575 100644 --- a/internal/command/show_test.go +++ b/internal/command/show_test.go @@ -515,7 +515,7 @@ func TestShow_json_output_state(t *testing.T) { defer testChdir(t, td)() providerSource, close := newMockProviderSource(t, map[string][]string{ - "test": []string{"1.2.3"}, + "test": {"1.2.3"}, }) defer close() @@ -552,6 +552,7 @@ func TestShow_json_output_state(t *testing.T) { FormatVersion string `json:"format_version,omitempty"` TerraformVersion string `json:"terraform_version"` Values map[string]interface{} `json:"values,omitempty"` + SensitiveValues map[string]bool `json:"sensitive_values,omitempty"` } var got, want state @@ -764,6 +765,7 @@ type plan struct { } type priorState struct { - FormatVersion string `json:"format_version,omitempty"` - Values map[string]interface{} `json:"values,omitempty"` + FormatVersion string `json:"format_version,omitempty"` + Values map[string]interface{} `json:"values,omitempty"` + SensitiveValues map[string]bool `json:"sensitive_values,omitempty"` } diff --git a/internal/command/testdata/show-json-sensitive/output.json b/internal/command/testdata/show-json-sensitive/output.json index f625fb316d87..5f22c4ccf3a2 100644 --- a/internal/command/testdata/show-json-sensitive/output.json +++ b/internal/command/testdata/show-json-sensitive/output.json @@ -1,5 +1,5 @@ { - "format_version": "0.1", + "format_version": "0.2", "variables": { "test_var": { "value": "bar" @@ -25,6 +25,9 @@ "values": { "ami": "bar", "password": "secret" + }, + "sensitive_values": { + "ami": true } }, { @@ -38,6 +41,9 @@ "values": { "ami": "bar", "password": "secret" + }, + "sensitive_values": { + "ami": true } }, { @@ -51,13 +57,16 @@ "values": { "ami": "bar", "password": "secret" + }, + "sensitive_values": { + "ami": true } } ] } }, "prior_state": { - "format_version": "0.1", + "format_version": "0.2", "values": { "outputs": { "test": { diff --git a/internal/command/testdata/show-json-state/basic/output.json b/internal/command/testdata/show-json-state/basic/output.json index 0f41c15469dc..3087ad118050 100644 --- a/internal/command/testdata/show-json-state/basic/output.json +++ b/internal/command/testdata/show-json-state/basic/output.json @@ -1,5 +1,5 @@ { - "format_version": "0.1", + "format_version": "0.2", "terraform_version": "0.12.0", "values": { "root_module": { @@ -15,7 +15,8 @@ "values": { "ami": null, "id": "621124146446964903" - } + }, + "sensitive_values": {} }, { "address": "test_instance.example[1]", @@ -28,7 +29,8 @@ "values": { "ami": null, "id": "4330206298367988603" - } + }, + "sensitive_values": {} } ] } diff --git a/internal/command/testdata/show-json-state/empty/output.json b/internal/command/testdata/show-json-state/empty/output.json index d457a374f9dc..12d30d201356 100644 --- a/internal/command/testdata/show-json-state/empty/output.json +++ b/internal/command/testdata/show-json-state/empty/output.json @@ -1,3 +1,3 @@ { - "format_version": "0.1" + "format_version": "0.2" } \ No newline at end of file diff --git a/internal/command/testdata/show-json-state/modules/output.json b/internal/command/testdata/show-json-state/modules/output.json index 32170538999f..eeee8f6cffbc 100644 --- a/internal/command/testdata/show-json-state/modules/output.json +++ b/internal/command/testdata/show-json-state/modules/output.json @@ -1,5 +1,5 @@ { - "format_version": "0.1", + "format_version": "0.2", "terraform_version": "0.12.0", "values": { "outputs": { @@ -22,7 +22,8 @@ "values": { "ami": "bar-var", "id": null - } + }, + "sensitive_values": {} } ], "address": "module.module_test_bar" @@ -40,7 +41,8 @@ "values": { "ami": "foo-var", "id": null - } + }, + "sensitive_values": {} } ], "address": "module.module_test_foo" diff --git a/internal/command/testdata/show-json-state/sensitive-variables/output.json b/internal/command/testdata/show-json-state/sensitive-variables/output.json index a4e74aa376d0..b133aeef13bf 100644 --- a/internal/command/testdata/show-json-state/sensitive-variables/output.json +++ b/internal/command/testdata/show-json-state/sensitive-variables/output.json @@ -1,5 +1,5 @@ { - "format_version": "0.1", + "format_version": "0.2", "terraform_version": "0.14.0", "values": { "root_module": { @@ -14,6 +14,9 @@ "values": { "id": "621124146446964903", "ami": "abc" + }, + "sensitive_values": { + "ami": true } } ] diff --git a/internal/command/testdata/show-json/basic-create/output.json b/internal/command/testdata/show-json/basic-create/output.json index 017054bccdef..3474443ed386 100644 --- a/internal/command/testdata/show-json/basic-create/output.json +++ b/internal/command/testdata/show-json/basic-create/output.json @@ -1,5 +1,5 @@ { - "format_version": "0.1", + "format_version": "0.2", "variables": { "test_var": { "value": "bar" @@ -24,7 +24,8 @@ "schema_version": 0, "values": { "ami": "bar" - } + }, + "sensitive_values": {} }, { "address": "test_instance.test[1]", @@ -36,7 +37,8 @@ "schema_version": 0, "values": { "ami": "bar" - } + }, + "sensitive_values": {} }, { "address": "test_instance.test[2]", @@ -48,13 +50,14 @@ "schema_version": 0, "values": { "ami": "bar" - } + }, + "sensitive_values": {} } ] } }, "prior_state": { - "format_version": "0.1", + "format_version": "0.2", "values": { "outputs": { "test": { diff --git a/internal/command/testdata/show-json/basic-delete/output.json b/internal/command/testdata/show-json/basic-delete/output.json index 6b29d785f7e4..9ebea2058f78 100644 --- a/internal/command/testdata/show-json/basic-delete/output.json +++ b/internal/command/testdata/show-json/basic-delete/output.json @@ -1,5 +1,5 @@ { - "format_version": "0.1", + "format_version": "0.2", "variables": { "test_var": { "value": "bar" @@ -24,7 +24,8 @@ "values": { "ami": "bar", "id": "placeholder" - } + }, + "sensitive_values": {} } ] } @@ -87,7 +88,7 @@ } }, "prior_state": { - "format_version": "0.1", + "format_version": "0.2", "values": { "outputs": { "test": { @@ -107,7 +108,8 @@ "values": { "ami": "foo", "id": "placeholder" - } + }, + "sensitive_values": {} }, { "address": "test_instance.test-delete", @@ -119,7 +121,8 @@ "values": { "ami": "foo", "id": "placeholder" - } + }, + "sensitive_values": {} } ] } diff --git a/internal/command/testdata/show-json/basic-update/output.json b/internal/command/testdata/show-json/basic-update/output.json index a6779801f97c..2b8bc25e3034 100644 --- a/internal/command/testdata/show-json/basic-update/output.json +++ b/internal/command/testdata/show-json/basic-update/output.json @@ -1,5 +1,5 @@ { - "format_version": "0.1", + "format_version": "0.2", "variables": { "test_var": { "value": "bar" @@ -24,7 +24,8 @@ "values": { "ami": "bar", "id": "placeholder" - } + }, + "sensitive_values": {} } ] } @@ -67,7 +68,7 @@ } }, "prior_state": { - "format_version": "0.1", + "format_version": "0.2", "values": { "outputs": { "test": { @@ -87,7 +88,8 @@ "values": { "ami": "bar", "id": "placeholder" - } + }, + "sensitive_values": {} } ] } diff --git a/internal/command/testdata/show-json/module-depends-on/output.json b/internal/command/testdata/show-json/module-depends-on/output.json index 5bfb89694ec5..cc7ed679f0cc 100644 --- a/internal/command/testdata/show-json/module-depends-on/output.json +++ b/internal/command/testdata/show-json/module-depends-on/output.json @@ -1,5 +1,5 @@ { - "format_version": "0.1", + "format_version": "0.2", "terraform_version": "0.13.1-dev", "planned_values": { "root_module": { @@ -13,7 +13,8 @@ "schema_version": 0, "values": { "ami": "foo-bar" - } + }, + "sensitive_values": {} } ] } diff --git a/internal/command/testdata/show-json/modules/output.json b/internal/command/testdata/show-json/modules/output.json index 445f269c2621..d4cacdafffba 100644 --- a/internal/command/testdata/show-json/modules/output.json +++ b/internal/command/testdata/show-json/modules/output.json @@ -1,5 +1,5 @@ { - "format_version": "0.1", + "format_version": "0.2", "planned_values": { "outputs": { "test": { @@ -20,7 +20,8 @@ "schema_version": 0, "values": { "ami": "bar-var" - } + }, + "sensitive_values": {} } ], "address": "module.module_test_bar" @@ -37,7 +38,8 @@ "schema_version": 0, "values": { "ami": "baz" - } + }, + "sensitive_values": {} }, { "address": "module.module_test_foo.test_instance.test[1]", @@ -49,7 +51,8 @@ "schema_version": 0, "values": { "ami": "baz" - } + }, + "sensitive_values": {} }, { "address": "module.module_test_foo.test_instance.test[2]", @@ -61,7 +64,8 @@ "schema_version": 0, "values": { "ami": "baz" - } + }, + "sensitive_values": {} } ], "address": "module.module_test_foo" @@ -70,7 +74,7 @@ } }, "prior_state": { - "format_version": "0.1", + "format_version": "0.2", "values": { "outputs": { "test": { diff --git a/internal/command/testdata/show-json/multi-resource-update/output.json b/internal/command/testdata/show-json/multi-resource-update/output.json index 564a4d71308a..4bf67352c66d 100644 --- a/internal/command/testdata/show-json/multi-resource-update/output.json +++ b/internal/command/testdata/show-json/multi-resource-update/output.json @@ -1,5 +1,5 @@ { - "format_version": "0.1", + "format_version": "0.2", "terraform_version": "0.13.0", "variables": { "test_var": { @@ -26,7 +26,8 @@ "values": { "ami": "bar", "id": "placeholder" - } + }, + "sensitive_values": {} }, { "address": "test_instance.test[1]", @@ -38,7 +39,8 @@ "schema_version": 0, "values": { "ami": "bar" - } + }, + "sensitive_values": {} } ] } @@ -104,7 +106,7 @@ } }, "prior_state": { - "format_version": "0.1", + "format_version": "0.2", "terraform_version": "0.13.0", "values": { "outputs": { @@ -126,7 +128,8 @@ "values": { "ami": "bar", "id": "placeholder" - } + }, + "sensitive_values": {} } ] } diff --git a/internal/command/testdata/show-json/nested-modules/output.json b/internal/command/testdata/show-json/nested-modules/output.json index 96e6b39e996a..80e7ae3588e9 100644 --- a/internal/command/testdata/show-json/nested-modules/output.json +++ b/internal/command/testdata/show-json/nested-modules/output.json @@ -1,5 +1,5 @@ { - "format_version": "0.1", + "format_version": "0.2", "planned_values": { "root_module": { "child_modules": [ @@ -17,7 +17,8 @@ "schema_version": 0, "values": { "ami": "bar-var" - } + }, + "sensitive_values": {} } ], "address": "module.my_module.module.more" diff --git a/internal/command/testdata/show-json/provider-version-no-config/output.json b/internal/command/testdata/show-json/provider-version-no-config/output.json index 7e0b841f8d9b..64b93ec751c0 100644 --- a/internal/command/testdata/show-json/provider-version-no-config/output.json +++ b/internal/command/testdata/show-json/provider-version-no-config/output.json @@ -1,5 +1,5 @@ { - "format_version": "0.1", + "format_version": "0.2", "variables": { "test_var": { "value": "bar" @@ -24,7 +24,8 @@ "schema_version": 0, "values": { "ami": "bar" - } + }, + "sensitive_values": {} }, { "address": "test_instance.test[1]", @@ -36,7 +37,8 @@ "schema_version": 0, "values": { "ami": "bar" - } + }, + "sensitive_values": {} }, { "address": "test_instance.test[2]", @@ -48,13 +50,14 @@ "schema_version": 0, "values": { "ami": "bar" - } + }, + "sensitive_values": {} } ] } }, "prior_state": { - "format_version": "0.1", + "format_version": "0.2", "values": { "outputs": { "test": { diff --git a/internal/command/testdata/show-json/provider-version/output.json b/internal/command/testdata/show-json/provider-version/output.json index eef936ec30bd..b5369806e933 100644 --- a/internal/command/testdata/show-json/provider-version/output.json +++ b/internal/command/testdata/show-json/provider-version/output.json @@ -1,5 +1,5 @@ { - "format_version": "0.1", + "format_version": "0.2", "variables": { "test_var": { "value": "bar" @@ -24,7 +24,8 @@ "schema_version": 0, "values": { "ami": "bar" - } + }, + "sensitive_values": {} }, { "address": "test_instance.test[1]", @@ -36,7 +37,8 @@ "schema_version": 0, "values": { "ami": "bar" - } + }, + "sensitive_values": {} }, { "address": "test_instance.test[2]", @@ -48,13 +50,14 @@ "schema_version": 0, "values": { "ami": "bar" - } + }, + "sensitive_values": {} } ] } }, "prior_state": { - "format_version": "0.1", + "format_version": "0.2", "values": { "outputs": { "test": { diff --git a/internal/command/testdata/show-json/requires-replace/output.json b/internal/command/testdata/show-json/requires-replace/output.json index 34dddcef26be..077d900b13b0 100644 --- a/internal/command/testdata/show-json/requires-replace/output.json +++ b/internal/command/testdata/show-json/requires-replace/output.json @@ -1,5 +1,5 @@ { - "format_version": "0.1", + "format_version": "0.2", "planned_values": { "root_module": { "resources": [ @@ -12,7 +12,8 @@ "schema_version": 0, "values": { "ami": "force-replace" - } + }, + "sensitive_values": {} } ] } @@ -47,7 +48,7 @@ } ], "prior_state": { - "format_version": "0.1", + "format_version": "0.2", "values": { "root_module": { "resources": [ @@ -61,7 +62,8 @@ "values": { "ami": "bar", "id": "placeholder" - } + }, + "sensitive_values": {} } ] } diff --git a/internal/command/testdata/show-json/sensitive-values/output.json b/internal/command/testdata/show-json/sensitive-values/output.json index 51105382a8ac..d3920743c13c 100644 --- a/internal/command/testdata/show-json/sensitive-values/output.json +++ b/internal/command/testdata/show-json/sensitive-values/output.json @@ -1,5 +1,5 @@ { - "format_version": "0.1", + "format_version": "0.2", "variables": { "test_var": { "value": "boop" @@ -23,6 +23,9 @@ "schema_version": 0, "values": { "ami": "boop" + }, + "sensitive_values": { + "ami": true } } ] @@ -66,7 +69,7 @@ } }, "prior_state": { - "format_version": "0.1", + "format_version": "0.2", "values": { "outputs": { "test": { diff --git a/website/docs/internals/json-format.html.md b/website/docs/internals/json-format.html.md index 7741cd08a0dd..d2c9cff69060 100644 --- a/website/docs/internals/json-format.html.md +++ b/website/docs/internals/json-format.html.md @@ -60,11 +60,11 @@ For ease of consumption by callers, the plan representation includes a partial r ```javascript { - "format_version": "0.1", + "format_version": "0.2", // "prior_state" is a representation of the state that the configuration is // being applied to, using the state representation described above. - "prior_state": , + "prior_state": , // "configuration" is a representation of the configuration being applied to the // prior state, using the configuration representation described above. @@ -236,6 +236,13 @@ The following example illustrates the structure of a ``: "id": "i-abc123", "instance_type": "t2.micro", // etc, etc + }, + + // "sensitive_values" is the JSON representation of the sensitivity of + // the resource's attribute values. Only attributes which are sensitive + // are included in this structure. + "values": { + "id": true, } } ]