addrs: Modeling of "wildcard" instance keys

In today's OpenTofu we consider it an immediate error if we can't determine
which instances are declared for a module call or resource, but in future
we'd like to be able to do something better that involves modeling the
fact that we weren't able to expand something but still being able to make
some predictions about how it will turn out anyway.

These new concepts of "wildcard" instance key and "placeholder" module
instance or resource instance addresses give us a way to represent those
situations elsewhere in the system, although as of this commit nothing is
using these yet.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
Martin Atkins
2025-08-28 14:02:21 -07:00
parent df21c1d8dc
commit 03d6224220
3 changed files with 113 additions and 0 deletions

View File

@@ -89,6 +89,61 @@ func (k StringKey) Value() cty.Value {
return cty.StringVal(string(k))
}
// WildcardKey is a special [InstanceKey] type used to represent zero or more
// _hypothetical_ instances in unusual situations where we don't yet have
// enough information to determine which instance keys exist.
//
// This can be used for [ModuleInstance] and [ResourceInstance] keys, but only
// in situations that are documented as being able to handle placeholder
// addresses for not-yet-expanded objects. No _actual_ object has an address
// with an instance key of this type.
type WildcardKey [1]InstanceKeyType
func (k WildcardKey) instanceKeySigil() {}
// ExpectedKeyType returns the type of key that this wildcard is acting as
// a placeholder for.
//
// If this returns a concrete key type (not [UnknownKeyType]) then we do at
// least know that all of the potential instance keys are of that type,
// even though we don't yet know their values or number.
func (k WildcardKey) ExpectedKeyType() InstanceKeyType {
return k[0]
}
func (k WildcardKey) String() string {
// There isn't any real way to write down a wildcard key because they
// represent an absense of information rather than something directly
// configured, but as a compromise we'll use something resembling HCL's
// "splat expression" syntax since it's at least hopefully somewhat
// familiar to OpenTofu users, and * is a character commonly used
// to represent wildcards in other systems.
return "[*]"
}
// Value returns an unknown value that's possibly constrained to be either
// a number or string if we at least know what instance key type we're
// expecting.
func (k WildcardKey) Value() cty.Value {
switch k.ExpectedKeyType() {
case NoKeyType:
// This case represents an object using the "enabled" meta-argument
// with an unknown value and so there isn't really any sensible
// answer here because we're representing either zero or one instances
// with no key at all, but we'll arbitrarily just return DynamicVal
// as a placeholder.
return cty.DynamicVal
case IntKeyType:
return cty.UnknownVal(cty.Number).RefineNotNull()
case StringKeyType:
return cty.UnknownVal(cty.String).RefineNotNull()
default: // (only UnknownKeyType should be left to handle here)
// If we don't even know what type of instance key we're expecting
// then we can't really say anything about the value at all.
return cty.DynamicVal
}
}
// InstanceKeyLess returns true if the first given instance key i should sort
// before the second key j, and false otherwise.
func InstanceKeyLess(i, j InstanceKey) bool {
@@ -126,6 +181,13 @@ func instanceKeyType(k InstanceKey) InstanceKeyType {
if _, ok := k.(IntKey); ok {
return IntKeyType
}
if k, ok := k.(WildcardKey); ok {
// Because WildcardKey values are placeholders for instance keys
// of some other type rather than keys themselves, there is no
// InstanceKeyType value representing "wildcard" and instead the
// key type of a wildcard key is whatever it's a placeholder for.
return k.ExpectedKeyType()
}
return NoKeyType
}
@@ -140,6 +202,11 @@ const (
NoKeyType InstanceKeyType = 0
IntKeyType InstanceKeyType = 'I'
StringKeyType InstanceKeyType = 'S'
// UnknownKeyType is a special [InstanceKeyType] used only with
// [WildcardKey] in situations where we don't even know what type of
// key we're expecting.
UnknownKeyType InstanceKeyType = '?'
)
// toHCLQuotedString is a helper which formats the given string in a way that

View File

@@ -236,6 +236,21 @@ func (m ModuleInstance) Parent() ModuleInstance {
return m[:len(m)-1]
}
// IsPlaceholder returns true if this address is acting as a placeholder for
// zero or more instances of the module it belongs to, rather than for
// an actual module instance.
//
// Placeholder addresses are only valid in certain contexts, and so should
// be used with care.
func (m ModuleInstance) IsPlaceholder() bool {
for _, step := range m {
if _, ok := step.InstanceKey.(WildcardKey); ok {
return true
}
}
return false
}
// String returns a string representation of the receiver, in the format used
// within e.g. user-provided resource addresses.
//

View File

@@ -142,6 +142,17 @@ func (r ResourceInstance) UniqueKey() UniqueKey {
return r // A ResourceInstance is its own UniqueKey
}
// IsPlaceholder returns true if this address is acting as a placeholder for
// zero or more instances of the resource it belongs to, rather than for
// an actual resource instance.
//
// Placeholder addresses are only valid in certain contexts, and so should
// be used with care.
func (r ResourceInstance) IsPlaceholder() bool {
_, ok := r.Key.(WildcardKey)
return ok
}
func (r ResourceInstance) uniqueKeySigil() {}
// Absolute returns an AbsResourceInstance from the receiver and the given module
@@ -189,6 +200,16 @@ func (r AbsResource) Config() ConfigResource {
}
}
// IsPlaceholder returns true if this address is acting as a placeholder for
// zero or more instances of the resource it belongs to, rather than for
// an actual resource instance.
//
// Placeholder addresses are only valid in certain contexts, and so should
// be used with care.
func (r AbsResource) IsPlaceholder() bool {
return r.Module.IsPlaceholder()
}
// TargetContains implements Targetable by returning true if the given other
// address is either equal to the receiver or is an instance of the
// receiver.
@@ -324,6 +345,16 @@ func (r AbsResourceInstance) AddrType() TargetableAddrType {
return AbsResourceInstanceAddrType
}
// IsPlaceholder returns true if this address is acting as a placeholder for
// zero or more instances of the resource it belongs to, rather than for
// an actual resource instance.
//
// Placeholder addresses are only valid in certain contexts, and so should
// be used with care.
func (r AbsResourceInstance) IsPlaceholder() bool {
return r.Module.IsPlaceholder() || r.Resource.IsPlaceholder()
}
func (r AbsResourceInstance) String() string {
if len(r.Module) == 0 {
return r.Resource.String()