configschema: do not expose optional attributes

Objects with optional attributes are only used for the decoding of
HCL, and those types should never be exposed elsewhere within
terraform.

Separate the external ImpliedType method from the cty.Type generated
internally for the decoder spec. This unfortunately causes our
ImpliedType method to return a different type than the
hcldec.ImpliedType function, but the former is only used within
terraform for concrete values, while the latter is used to decode
HCL. Renaming the ImpliedType methods could be done to further
differentiate them, but that does cause fairly large diff in the
codebase that does not seem worth the effort at this time.
This commit is contained in:
James Bardin 2021-09-10 09:27:28 -04:00
parent cb5b159228
commit af4f4540a9
3 changed files with 260 additions and 71 deletions

View File

@ -121,7 +121,7 @@ func (b *Block) DecoderSpec() hcldec.Spec {
// implied type more complete, but if there are any
// dynamically-typed attributes inside we must use a tuple
// instead, at the expense of our type then not being predictable.
if blockS.Block.ImpliedType().HasDynamicTypes() {
if blockS.Block.specType().HasDynamicTypes() {
ret[name] = &hcldec.BlockTupleSpec{
TypeName: name,
Nested: childSpec,
@ -155,7 +155,7 @@ func (b *Block) DecoderSpec() hcldec.Spec {
// implied type more complete, but if there are any
// dynamically-typed attributes inside we must use a tuple
// instead, at the expense of our type then not being predictable.
if blockS.Block.ImpliedType().HasDynamicTypes() {
if blockS.Block.specType().HasDynamicTypes() {
ret[name] = &hcldec.BlockObjectSpec{
TypeName: name,
Nested: childSpec,
@ -195,7 +195,7 @@ func (a *Attribute) decoderSpec(name string) hcldec.Spec {
panic("Invalid attribute schema: NestedType and Type cannot both be set. This is a bug in the provider.")
}
ty := a.NestedType.ImpliedType()
ty := a.NestedType.specType()
ret.Type = ty
ret.Required = a.Required || a.NestedType.MinItems > 0
return ret

View File

@ -8,11 +8,23 @@ import (
// ImpliedType returns the cty.Type that would result from decoding a
// configuration block using the receiving block schema.
//
// The type returned from Block.ImpliedType differs from the type returned by
// hcldec.ImpliedType in that there will be no objects with optional
// attributes, since this value is not to be used for the decoding of
// configuration.
//
// ImpliedType always returns a result, even if the given schema is
// inconsistent. Code that creates configschema.Block objects should be
// tested using the InternalValidate method to detect any inconsistencies
// that would cause this method to fall back on defaults and assumptions.
func (b *Block) ImpliedType() cty.Type {
return b.specType().WithoutOptionalAttributesDeep()
}
// specType returns the cty.Type used for decoding a configuration
// block using the receiving block schema. This is the type used internally by
// hcldec to decode configuration.
func (b *Block) specType() cty.Type {
if b == nil {
return cty.EmptyObject
}
@ -41,14 +53,20 @@ func (b *Block) ContainsSensitive() bool {
return false
}
// ImpliedType returns the cty.Type that would result from decoding a NestedType
// Attribute using the receiving block schema.
// ImpliedType returns the cty.Type that would result from decoding a
// NestedType Attribute using the receiving block schema.
//
// ImpliedType always returns a result, even if the given schema is
// inconsistent. Code that creates configschema.Object objects should be tested
// using the InternalValidate method to detect any inconsistencies that would
// cause this method to fall back on defaults and assumptions.
func (o *Object) ImpliedType() cty.Type {
return o.specType().WithoutOptionalAttributesDeep()
}
// specType returns the cty.Type used for decoding a NestedType Attribute using
// the receiving block schema.
func (o *Object) specType() cty.Type {
if o == nil {
return cty.EmptyObject
}
@ -56,7 +74,7 @@ func (o *Object) ImpliedType() cty.Type {
attrTys := make(map[string]cty.Type, len(o.Attributes))
for name, attrS := range o.Attributes {
if attrS.NestedType != nil {
attrTys[name] = attrS.NestedType.ImpliedType()
attrTys[name] = attrS.NestedType.specType()
} else {
attrTys[name] = attrS.Type
}

View File

@ -112,6 +112,36 @@ func TestBlockImpliedType(t *testing.T) {
}),
}),
},
"nested objects with optional attrs": {
&Block{
Attributes: map[string]*Attribute{
"map": {
Optional: true,
NestedType: &Object{
Nesting: NestingMap,
Attributes: map[string]*Attribute{
"optional": {Type: cty.String, Optional: true},
"required": {Type: cty.Number, Required: true},
"computed": {Type: cty.List(cty.Bool), Computed: true},
"optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true},
},
},
},
},
},
// The ImpliedType from the type-level block should not contain any
// optional attributes.
cty.Object(map[string]cty.Type{
"map": cty.Map(cty.Object(
map[string]cty.Type{
"optional": cty.String,
"required": cty.Number,
"computed": cty.List(cty.Bool),
"optional_computed": cty.Map(cty.Bool),
},
)),
}),
},
}
for name, test := range tests {
@ -137,6 +167,211 @@ func TestObjectImpliedType(t *testing.T) {
&Object{},
cty.EmptyObject,
},
"attributes": {
&Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"optional": {Type: cty.String, Optional: true},
"required": {Type: cty.Number, Required: true},
"computed": {Type: cty.List(cty.Bool), Computed: true},
"optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true},
},
},
cty.Object(
map[string]cty.Type{
"optional": cty.String,
"required": cty.Number,
"computed": cty.List(cty.Bool),
"optional_computed": cty.Map(cty.Bool),
},
),
},
"nested attributes": {
&Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"nested_type": {
NestedType: &Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"optional": {Type: cty.String, Optional: true},
"required": {Type: cty.Number, Required: true},
"computed": {Type: cty.List(cty.Bool), Computed: true},
"optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true},
},
},
Optional: true,
},
},
},
cty.Object(map[string]cty.Type{
"nested_type": cty.Object(map[string]cty.Type{
"optional": cty.String,
"required": cty.Number,
"computed": cty.List(cty.Bool),
"optional_computed": cty.Map(cty.Bool),
}),
}),
},
"nested object-type attributes": {
&Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"nested_type": {
NestedType: &Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"optional": {Type: cty.String, Optional: true},
"required": {Type: cty.Number, Required: true},
"computed": {Type: cty.List(cty.Bool), Computed: true},
"optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true},
"object": {
Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
"optional": cty.String,
"required": cty.Number,
}, []string{"optional"}),
},
},
},
Optional: true,
},
},
},
cty.Object(map[string]cty.Type{
"nested_type": cty.Object(map[string]cty.Type{
"optional": cty.String,
"required": cty.Number,
"computed": cty.List(cty.Bool),
"optional_computed": cty.Map(cty.Bool),
"object": cty.Object(map[string]cty.Type{"optional": cty.String, "required": cty.Number}),
}),
}),
},
"NestingList": {
&Object{
Nesting: NestingList,
Attributes: map[string]*Attribute{
"foo": {Type: cty.String, Optional: true},
},
},
cty.List(cty.Object(map[string]cty.Type{"foo": cty.String})),
},
"NestingMap": {
&Object{
Nesting: NestingMap,
Attributes: map[string]*Attribute{
"foo": {Type: cty.String},
},
},
cty.Map(cty.Object(map[string]cty.Type{"foo": cty.String})),
},
"NestingSet": {
&Object{
Nesting: NestingSet,
Attributes: map[string]*Attribute{
"foo": {Type: cty.String},
},
},
cty.Set(cty.Object(map[string]cty.Type{"foo": cty.String})),
},
"deeply nested NestingList": {
&Object{
Nesting: NestingList,
Attributes: map[string]*Attribute{
"foo": {
NestedType: &Object{
Nesting: NestingList,
Attributes: map[string]*Attribute{
"bar": {Type: cty.String},
},
},
},
},
},
cty.List(cty.Object(map[string]cty.Type{"foo": cty.List(cty.Object(map[string]cty.Type{"bar": cty.String}))})),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
got := test.Schema.ImpliedType()
if !got.Equals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
func TestObjectContainsSensitive(t *testing.T) {
tests := map[string]struct {
Schema *Object
Want bool
}{
"object contains sensitive": {
&Object{
Attributes: map[string]*Attribute{
"sensitive": {Sensitive: true},
},
},
true,
},
"no sensitive attrs": {
&Object{
Attributes: map[string]*Attribute{
"insensitive": {},
},
},
false,
},
"nested object contains sensitive": {
&Object{
Attributes: map[string]*Attribute{
"nested": {
NestedType: &Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"sensitive": {Sensitive: true},
},
},
},
},
},
true,
},
"nested obj, no sensitive attrs": {
&Object{
Attributes: map[string]*Attribute{
"nested": {
NestedType: &Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"public": {},
},
},
},
},
},
false,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
got := test.Schema.ContainsSensitive()
if got != test.Want {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
// Nested attribute should return optional object attributes for decoding.
func TestObjectSpecType(t *testing.T) {
tests := map[string]struct {
Schema *Object
Want cty.Type
}{
"attributes": {
&Object{
Nesting: NestingSingle,
@ -265,74 +500,10 @@ func TestObjectImpliedType(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
got := test.Schema.ImpliedType()
got := test.Schema.specType()
if !got.Equals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
func TestObjectContainsSensitive(t *testing.T) {
tests := map[string]struct {
Schema *Object
Want bool
}{
"object contains sensitive": {
&Object{
Attributes: map[string]*Attribute{
"sensitive": {Sensitive: true},
},
},
true,
},
"no sensitive attrs": {
&Object{
Attributes: map[string]*Attribute{
"insensitive": {},
},
},
false,
},
"nested object contains sensitive": {
&Object{
Attributes: map[string]*Attribute{
"nested": {
NestedType: &Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"sensitive": {Sensitive: true},
},
},
},
},
},
true,
},
"nested obj, no sensitive attrs": {
&Object{
Attributes: map[string]*Attribute{
"nested": {
NestedType: &Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"public": {},
},
},
},
},
},
false,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
got := test.Schema.ContainsSensitive()
if got != test.Want {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}