opentofu/internal/configs/configschema/decoder_spec.go
Kristin Laemmert 0d80a74539
configs/configschema: fix missing "computed" attributes from NestedObject's ImpliedType (#29172)
* configs/configschema: fix missing "computed" attributes from NestedObject's ImpliedType

listOptionalAttrsFromObject was not including "computed" attributes in the list of optional object attributes. This is now fixed. I've also added some tests and fixed some panics and otherwise bad behavior when bad input is given. One natable change is in ImpliedType, which was panicking on an invalid nesting mode. The comment expressly states that it will return a result even when the schema is inconsistent, so I removed the panic and instead return an empty object.
2021-07-15 13:00:07 -04:00

228 lines
6.8 KiB
Go

package configschema
import (
"runtime"
"sync"
"unsafe"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
)
var mapLabelNames = []string{"key"}
// specCache is a global cache of all the generated hcldec.Spec values for
// Blocks. This cache is used by the Block.DecoderSpec method to memoize calls
// and prevent unnecessary regeneration of the spec, especially when they are
// large and deeply nested.
// Caching these externally rather than within the struct is required because
// Blocks are used by value and copied when working with NestedBlocks, and the
// copying of the value prevents any safe synchronisation of the struct itself.
//
// While we are using the *Block pointer as the cache key, and the Block
// contents are mutable, once a Block is created it is treated as immutable for
// the duration of its life. Because a Block is a representation of a logical
// schema, which cannot change while it's being used, any modifications to the
// schema during execution would be an error.
type specCache struct {
sync.Mutex
specs map[uintptr]hcldec.Spec
}
var decoderSpecCache = specCache{
specs: map[uintptr]hcldec.Spec{},
}
// get returns the Spec associated with eth given Block, or nil if non is
// found.
func (s *specCache) get(b *Block) hcldec.Spec {
s.Lock()
defer s.Unlock()
k := uintptr(unsafe.Pointer(b))
return s.specs[k]
}
// set stores the given Spec as being the result of b.DecoderSpec().
func (s *specCache) set(b *Block, spec hcldec.Spec) {
s.Lock()
defer s.Unlock()
// the uintptr value gets us a unique identifier for each block, without
// tying this to the block value itself.
k := uintptr(unsafe.Pointer(b))
if _, ok := s.specs[k]; ok {
return
}
s.specs[k] = spec
// This must use a finalizer tied to the Block, otherwise we'll continue to
// build up Spec values as the Blocks are recycled.
runtime.SetFinalizer(b, s.delete)
}
// delete removes the spec associated with the given Block.
func (s *specCache) delete(b *Block) {
s.Lock()
defer s.Unlock()
k := uintptr(unsafe.Pointer(b))
delete(s.specs, k)
}
// DecoderSpec returns a hcldec.Spec that can be used to decode a HCL Body
// using the facilities in the hcldec package.
//
// The returned specification is guaranteed to return a value of the same type
// returned by method ImpliedType, but it may contain null values if any of the
// block attributes are defined as optional and/or computed respectively.
func (b *Block) DecoderSpec() hcldec.Spec {
ret := hcldec.ObjectSpec{}
if b == nil {
return ret
}
if spec := decoderSpecCache.get(b); spec != nil {
return spec
}
for name, attrS := range b.Attributes {
ret[name] = attrS.decoderSpec(name)
}
for name, blockS := range b.BlockTypes {
if _, exists := ret[name]; exists {
// This indicates an invalid schema, since it's not valid to define
// both an attribute and a block type of the same name. We assume
// that the provider has already used something like
// InternalValidate to validate their schema.
continue
}
childSpec := blockS.Block.DecoderSpec()
switch blockS.Nesting {
case NestingSingle, NestingGroup:
ret[name] = &hcldec.BlockSpec{
TypeName: name,
Nested: childSpec,
Required: blockS.MinItems == 1,
}
if blockS.Nesting == NestingGroup {
ret[name] = &hcldec.DefaultSpec{
Primary: ret[name],
Default: &hcldec.LiteralSpec{
Value: blockS.EmptyValue(),
},
}
}
case NestingList:
// We prefer to use a list where possible, since it makes our
// 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() {
ret[name] = &hcldec.BlockTupleSpec{
TypeName: name,
Nested: childSpec,
MinItems: blockS.MinItems,
MaxItems: blockS.MaxItems,
}
} else {
ret[name] = &hcldec.BlockListSpec{
TypeName: name,
Nested: childSpec,
MinItems: blockS.MinItems,
MaxItems: blockS.MaxItems,
}
}
case NestingSet:
// We forbid dynamically-typed attributes inside NestingSet in
// InternalValidate, so we don't do anything special to handle that
// here. (There is no set analog to tuple and object types, because
// cty's set implementation depends on knowing the static type in
// order to properly compute its internal hashes.) We assume that
// the provider has already used something like InternalValidate to
// validate their schema.
ret[name] = &hcldec.BlockSetSpec{
TypeName: name,
Nested: childSpec,
MinItems: blockS.MinItems,
MaxItems: blockS.MaxItems,
}
case NestingMap:
// We prefer to use a list where possible, since it makes our
// 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() {
ret[name] = &hcldec.BlockObjectSpec{
TypeName: name,
Nested: childSpec,
LabelNames: mapLabelNames,
}
} else {
ret[name] = &hcldec.BlockMapSpec{
TypeName: name,
Nested: childSpec,
LabelNames: mapLabelNames,
}
}
default:
// Invalid nesting type is just ignored. It's checked by
// InternalValidate. We assume that the provider has already used
// something like InternalValidate to validate their schema.
continue
}
}
decoderSpecCache.set(b, ret)
return ret
}
func (a *Attribute) decoderSpec(name string) hcldec.Spec {
ret := &hcldec.AttrSpec{Name: name}
if a == nil {
return ret
}
if a.NestedType != nil {
// FIXME: a panic() is a bad UX. InternalValidate() can check Attribute
// schemas as well so a fix might be to call it when we get the schema
// from the provider in Context(). Since this could be a breaking
// change, we'd need to communicate well before adding that call.
if a.Type != cty.NilType {
panic("Invalid attribute schema: NestedType and Type cannot both be set. This is a bug in the provider.")
}
ty := a.NestedType.ImpliedType()
ret.Type = ty
ret.Required = a.Required || a.NestedType.MinItems > 0
return ret
}
ret.Type = a.Type
ret.Required = a.Required
return ret
}
// listOptionalAttrsFromObject is a helper function which does *not* recurse
// into NestedType Attributes, because the optional types for each of those will
// belong to their own cty.Object definitions. It is used in other functions
// which themselves handle that recursion.
func listOptionalAttrsFromObject(o *Object) []string {
ret := make([]string, 0)
// This is unlikely to happen outside of tests.
if o == nil {
return ret
}
for name, attr := range o.Attributes {
if attr.Optional || attr.Computed {
ret = append(ret, name)
}
}
return ret
}