mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-18 12:42:58 -06:00
9847eaa9cf
MinItems and MaxItems are not used on nested types in the protocol, so remove their usage in Terraform to prevent future confusion.
224 lines
6.5 KiB
Go
224 lines
6.5 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.specType().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.specType().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 {
|
|
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.specType()
|
|
ret.Type = ty
|
|
ret.Required = a.Required
|
|
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
|
|
}
|