Merge pull request #29559 from hashicorp/jbardin/optional-attrs

Prevent `ObjectWithOptionalAttrs` from escaping
This commit is contained in:
James Bardin 2021-09-13 08:58:11 -04:00 committed by GitHub
commit 8ed9a270e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 328 additions and 118 deletions

View File

@ -24,9 +24,10 @@ func TestParseVariableValuesUndeclared(t *testing.T) {
}
decls := map[string]*configs.Variable{
"declared1": {
Name: "declared1",
Type: cty.String,
ParsingMode: configs.VariableParseLiteral,
Name: "declared1",
Type: cty.String,
ConstraintType: cty.String,
ParsingMode: configs.VariableParseLiteral,
DeclRange: hcl.Range{
Filename: "fake.tf",
Start: hcl.Pos{Line: 2, Column: 1, Byte: 0},
@ -34,9 +35,10 @@ func TestParseVariableValuesUndeclared(t *testing.T) {
},
},
"missing1": {
Name: "missing1",
Type: cty.String,
ParsingMode: configs.VariableParseLiteral,
Name: "missing1",
Type: cty.String,
ConstraintType: cty.String,
ParsingMode: configs.VariableParseLiteral,
DeclRange: hcl.Range{
Filename: "fake.tf",
Start: hcl.Pos{Line: 3, Column: 1, Byte: 0},
@ -44,10 +46,11 @@ func TestParseVariableValuesUndeclared(t *testing.T) {
},
},
"missing2": {
Name: "missing1",
Type: cty.String,
ParsingMode: configs.VariableParseLiteral,
Default: cty.StringVal("default for missing2"),
Name: "missing1",
Type: cty.String,
ConstraintType: cty.String,
ParsingMode: configs.VariableParseLiteral,
Default: cty.StringVal("default for missing2"),
DeclRange: hcl.Range{
Filename: "fake.tf",
Start: hcl.Pos{Line: 4, Column: 1, Byte: 0},

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

View File

@ -198,7 +198,7 @@ func checkModuleExperiments(m *Module) hcl.Diagnostics {
if !m.ActiveExperiments.Has(experiments.ModuleVariableOptionalAttrs) {
for _, v := range m.Variables {
if typeConstraintHasOptionalAttrs(v.Type) {
if typeConstraintHasOptionalAttrs(v.ConstraintType) {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Optional object type attributes are experimental",

View File

@ -51,6 +51,7 @@ func (v *Variable) merge(ov *Variable) hcl.Diagnostics {
}
if ov.Type != cty.NilType {
v.Type = ov.Type
v.ConstraintType = ov.ConstraintType
}
if ov.ParsingMode != 0 {
v.ParsingMode = ov.ParsingMode
@ -67,7 +68,7 @@ func (v *Variable) merge(ov *Variable) hcl.Diagnostics {
// constraint but the converted value cannot. In practice, this situation
// should be rare since most of our conversions are interchangable.
if v.Default != cty.NilVal {
val, err := convert.Convert(v.Default, v.Type)
val, err := convert.Convert(v.Default, v.ConstraintType)
if err != nil {
// What exactly we'll say in the error message here depends on whether
// it was Default or Type that was overridden here.

View File

@ -25,6 +25,7 @@ func TestModuleOverrideVariable(t *testing.T) {
DescriptionSet: true,
Default: cty.StringVal("b_override"),
Type: cty.String,
ConstraintType: cty.String,
ParsingMode: VariableParseLiteral,
DeclRange: hcl.Range{
Filename: "testdata/valid-modules/override-variable/primary.tf",
@ -46,6 +47,7 @@ func TestModuleOverrideVariable(t *testing.T) {
DescriptionSet: true,
Default: cty.StringVal("b_override partial"),
Type: cty.String,
ConstraintType: cty.String,
ParsingMode: VariableParseLiteral,
DeclRange: hcl.Range{
Filename: "testdata/valid-modules/override-variable/primary.tf",

View File

@ -22,7 +22,13 @@ type Variable struct {
Name string
Description string
Default cty.Value
Type cty.Type
// Type is the concrete type of the variable value.
Type cty.Type
// ConstraintType is used for decoding and type conversions, and may
// contain nested ObjectWithOptionalAttr types.
ConstraintType cty.Type
ParsingMode VariableParsingMode
Validations []*VariableValidation
Sensitive bool
@ -45,6 +51,7 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
// or not they are set when we merge.
if !override {
v.Type = cty.DynamicPseudoType
v.ConstraintType = cty.DynamicPseudoType
v.ParsingMode = VariableParseLiteral
}
@ -92,7 +99,8 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
if attr, exists := content.Attributes["type"]; exists {
ty, parseMode, tyDiags := decodeVariableType(attr.Expr)
diags = append(diags, tyDiags...)
v.Type = ty
v.ConstraintType = ty
v.Type = ty.WithoutOptionalAttributesDeep()
v.ParsingMode = parseMode
}
@ -112,9 +120,9 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
// attribute above.
// However, we can't do this if we're in an override file where
// the type might not be set; we'll catch that during merge.
if v.Type != cty.NilType {
if v.ConstraintType != cty.NilType {
var err error
val, err = convert.Convert(val, v.Type)
val, err = convert.Convert(val, v.ConstraintType)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,

View File

@ -1452,12 +1452,12 @@ func TestProposedNew(t *testing.T) {
"map": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{"bar": cty.String}))),
"set": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{"bar": cty.String}))),
"nested_map": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
"inner": cty.ObjectWithOptionalAttrs(map[string]cty.Type{
"inner": cty.Object(map[string]cty.Type{
"optional": cty.String,
"computed": cty.String,
"optional_computed": cty.String,
"required": cty.String,
}, []string{"computed", "optional", "optional_computed"}),
}),
}))),
}),
},

View File

@ -237,12 +237,6 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd
})
return cty.DynamicVal, diags
}
wantType := cty.DynamicPseudoType
if config.Type != cty.NilType {
wantType = config.Type
}
d.Evaluator.VariableValuesLock.Lock()
defer d.Evaluator.VariableValuesLock.Unlock()
@ -262,15 +256,15 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd
if d.Operation == walkValidate {
// Ensure variable sensitivity is captured in the validate walk
if config.Sensitive {
return cty.UnknownVal(wantType).Mark(marks.Sensitive), diags
return cty.UnknownVal(config.Type).Mark(marks.Sensitive), diags
}
return cty.UnknownVal(wantType), diags
return cty.UnknownVal(config.Type), diags
}
moduleAddrStr := d.ModulePath.String()
vals := d.Evaluator.VariableValues[moduleAddrStr]
if vals == nil {
return cty.UnknownVal(wantType), diags
return cty.UnknownVal(config.Type), diags
}
val, isSet := vals[addr.Name]
@ -278,11 +272,11 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd
if config.Default != cty.NilVal {
return config.Default, diags
}
return cty.UnknownVal(wantType), diags
return cty.UnknownVal(config.Type), diags
}
var err error
val, err = convert.Convert(val, wantType)
val, err = convert.Convert(val, config.ConstraintType)
if err != nil {
// We should never get here because this problem should've been caught
// during earlier validation, but we'll do something reasonable anyway.
@ -294,7 +288,7 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd
})
// Stub out our return value so that the semantic checker doesn't
// produce redundant downstream errors.
val = cty.UnknownVal(wantType)
val = cty.UnknownVal(config.Type)
}
// Mark if sensitive

View File

@ -95,15 +95,19 @@ func TestEvaluatorGetInputVariable(t *testing.T) {
Module: &configs.Module{
Variables: map[string]*configs.Variable{
"some_var": {
Name: "some_var",
Sensitive: true,
Default: cty.StringVal("foo"),
Name: "some_var",
Sensitive: true,
Default: cty.StringVal("foo"),
Type: cty.String,
ConstraintType: cty.String,
},
// Avoid double marking a value
"some_other_var": {
Name: "some_other_var",
Sensitive: true,
Default: cty.StringVal("bar"),
Name: "some_other_var",
Sensitive: true,
Default: cty.StringVal("bar"),
Type: cty.String,
ConstraintType: cty.String,
},
},
},

View File

@ -200,7 +200,6 @@ func (n *nodeModuleVariable) DotNode(name string, opts *dag.DotOpts) *dag.DotNod
// validation, and we will not have any expansion module instance
// repetition data.
func (n *nodeModuleVariable) evalModuleCallArgument(ctx EvalContext, validateOnly bool) (map[string]cty.Value, error) {
wantType := n.Config.Type
name := n.Addr.Variable.Name
expr := n.Expr
@ -238,7 +237,7 @@ func (n *nodeModuleVariable) evalModuleCallArgument(ctx EvalContext, validateOnl
// now we can do our own local type conversion and produce an error message
// with better context if it fails.
var convErr error
val, convErr = convert.Convert(val, wantType)
val, convErr = convert.Convert(val, n.Config.ConstraintType)
if convErr != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
@ -251,7 +250,7 @@ func (n *nodeModuleVariable) evalModuleCallArgument(ctx EvalContext, validateOnl
})
// We'll return a placeholder unknown value to avoid producing
// redundant downstream errors.
val = cty.UnknownVal(wantType)
val = cty.UnknownVal(n.Config.Type)
}
vals := make(map[string]cty.Value)

View File

@ -7,6 +7,7 @@ import (
"github.com/go-test/deep"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
@ -16,7 +17,9 @@ func TestNodeModuleVariablePath(t *testing.T) {
n := &nodeModuleVariable{
Addr: addrs.RootModuleInstance.InputVariable("foo"),
Config: &configs.Variable{
Name: "foo",
Name: "foo",
Type: cty.String,
ConstraintType: cty.String,
},
}
@ -31,7 +34,9 @@ func TestNodeModuleVariableReferenceableName(t *testing.T) {
n := &nodeExpandModuleVariable{
Addr: addrs.InputVariable{Name: "foo"},
Config: &configs.Variable{
Name: "foo",
Name: "foo",
Type: cty.String,
ConstraintType: cty.String,
},
}
@ -64,7 +69,9 @@ func TestNodeModuleVariableReference(t *testing.T) {
Addr: addrs.InputVariable{Name: "foo"},
Module: addrs.RootModule.Child("bar"),
Config: &configs.Variable{
Name: "foo",
Name: "foo",
Type: cty.String,
ConstraintType: cty.String,
},
Expr: &hclsyntax.ScopeTraversalExpr{
Traversal: hcl.Traversal{
@ -90,7 +97,9 @@ func TestNodeModuleVariableReference_grandchild(t *testing.T) {
Addr: addrs.InputVariable{Name: "foo"},
Module: addrs.RootModule.Child("bar"),
Config: &configs.Variable{
Name: "foo",
Name: "foo",
Type: cty.String,
ConstraintType: cty.String,
},
Expr: &hclsyntax.ScopeTraversalExpr{
Traversal: hcl.Traversal{

View File

@ -5,6 +5,7 @@ import (
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/zclconf/go-cty/cty"
)
func TestNodeRootVariableExecute(t *testing.T) {
@ -13,7 +14,9 @@ func TestNodeRootVariableExecute(t *testing.T) {
n := &NodeRootVariable{
Addr: addrs.InputVariable{Name: "foo"},
Config: &configs.Variable{
Name: "foo",
Name: "foo",
Type: cty.String,
ConstraintType: cty.String,
},
}

View File

@ -262,10 +262,8 @@ func checkInputVariables(vcs map[string]*configs.Variable, vs InputValues) tfdia
continue
}
wantType := vc.Type
// A given value is valid if it can convert to the desired type.
_, err := convert.Convert(val.Value, wantType)
_, err := convert.Convert(val.Value, vc.ConstraintType)
if err != nil {
switch val.SourceType {
case ValueFromConfig, ValueFromAutoFile, ValueFromNamedFile: