diff --git a/internal/addrs/partial_expanded.go b/internal/addrs/partial_expanded.go new file mode 100644 index 0000000000..0908c34f5f --- /dev/null +++ b/internal/addrs/partial_expanded.go @@ -0,0 +1,323 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "strings" +) + +// PartialExpandedModule represents a set of module instances which all share +// a common known parent module instance but the remaining call instance keys +// are not yet known. +type PartialExpandedModule struct { + // expandedPrefix is the initial part of the module address whose expansion + // is already complete and so has exact instance keys. + expandedPrefix ModuleInstance + + // unexpandedSuffix is the remainder of the module address whose instance + // keys are not known yet. This is a slight abuse of type [Module] because + // it's representing a relative path from expandedPrefix rather than a + // path from the root module as usual, so this value must never be exposed + // in the public API of this package. + // + // This can be zero-length in PartialExpandedModule values used as part + // of the internals of a PartialExpandedResource, but should never be + // zero-length in a publicly-exposed PartialExpandedModule because that + // would make this just a degenerate ModuleInstance. + unexpandedSuffix Module +} + +func (m ModuleInstance) UnexpandedChild(call ModuleCall) PartialExpandedModule { + return PartialExpandedModule{ + expandedPrefix: m, + unexpandedSuffix: Module{call.Name}, + } +} + +// MatchesInstance returns true if and only if the given module instance +// belongs to the recieving partially-expanded module address pattern. +func (pem PartialExpandedModule) MatchesInstance(inst ModuleInstance) bool { + // Total length must always match. + if len(inst) != (len(pem.expandedPrefix) + len(pem.unexpandedSuffix)) { + return false + } + + // The known prefix must match exactly. + givenExpandedPrefix := inst[:len(pem.expandedPrefix)] + if !givenExpandedPrefix.Equal(pem.expandedPrefix) { + return false + } + + // The known suffix must match the call names, even though we don't yet + // know the specific instance keys. + givenExpandedSuffix := inst[len(pem.expandedPrefix):] + for i := range pem.unexpandedSuffix { + if pem.unexpandedSuffix[i] != givenExpandedSuffix[i].Name { + return false + } + } + + // If we passed all the filters above then it's a match. + return true +} + +// Module returns the unexpanded module address that this pattern originated +// from. +func (pem PartialExpandedModule) Module() Module { + ret := pem.expandedPrefix.Module() + return append(ret, pem.unexpandedSuffix...) +} + +// KnownPrefix returns the longest possible ModuleInstance address made of +// known segments of this partially-expanded module instance address. +func (pem PartialExpandedModule) KnownPrefix() ModuleInstance { + if len(pem.expandedPrefix) == 0 { + return nil + } + + // Although we can't enforce it with the Go compiler, our convention is + // that we never mutate address values outside of this package and so + // we'll expose our pem.expandedPrefix buffer directly here and trust that + // the caller will play nice with it. However, we do force the unused + // capacity to zero so that the caller can safely construct child addresses, + // which would append new steps to the end. + return pem.expandedPrefix[:len(pem.expandedPrefix):len(pem.expandedPrefix)] +} + +// FirstUnexpandedCall returns the address of the first step in the module +// path whose instance keys are not yet known, discarding any subsequent +// calls beneath it. +func (pem PartialExpandedModule) FirstUnexpandedCall() AbsModuleCall { + // NOTE: This assumes that there's always at least one element in + // unexpandedSuffix because it should only be used with the public-facing + // version of PartialExpandedModule where that contract always holds. It's + // not safe to use this for the PartialExpandedModule value hidden in the + // internals of PartialExpandedResource. + return AbsModuleCall{ + Module: pem.KnownPrefix(), + Call: ModuleCall{ + Name: pem.unexpandedSuffix[0], + }, + } +} + +// UnexpandedSuffix returns the local addresses of all of the calls whose +// instances are not yet expanded, in the module tree traversal order. +// +// Method KnownPrefix concatenated with UnexpandedSuffix (assuming that were +// actually possible) represents the whole module path that the +// PartialExpandedModule encapsulates. +func (pem PartialExpandedModule) UnexpandedSuffix() []ModuleCall { + if len(pem.unexpandedSuffix) == 0 { + // Should never happen for any publicly-visible value of this type, + // because we should always have at least one unexpanded call, + // but we'll allow it anyway since we have a reasonable return value + // for that case. + return nil + } + + // A []ModuleCall is the only representation of a non-rooted chain of + // module calls that we're allowed to export in our public API, and so + // we'll transform our not-quite-allowed unrooted "Module" value in that + // form externally. + ret := make([]ModuleCall, len(pem.unexpandedSuffix)) + for i, name := range pem.unexpandedSuffix { + ret[i].Name = name + } + return ret +} + +// Child returns the address of a child of the receiver that belongs to the +// given module call. +func (pem PartialExpandedModule) Child(call ModuleCall) PartialExpandedModule { + return PartialExpandedModule{ + expandedPrefix: pem.expandedPrefix, + unexpandedSuffix: append(pem.unexpandedSuffix, call.Name), + } +} + +// Resource returns the address of a resource within the receiver. +func (pem PartialExpandedModule) Resource(resource Resource) PartialExpandedResource { + return PartialExpandedResource{ + module: pem, + resource: resource, + } +} + +// String returns a string representation of the pattern where the known +// prefix uses the normal module instance address syntax and the unknown +// suffix steps use a similar syntax but with "[*]" as a placeholder to +// represent instance keys that aren't yet known. +func (pem PartialExpandedModule) String() string { + var buf strings.Builder + if len(pem.expandedPrefix) != 0 { + buf.WriteString(pem.expandedPrefix.String()) + } + for i, callName := range pem.unexpandedSuffix { + if i > 0 || len(pem.unexpandedSuffix) != 0 { + buf.WriteByte('.') + } + buf.WriteString("module.") + buf.WriteString(callName) + buf.WriteString("[*]") + } + return buf.String() +} + +func (per PartialExpandedModule) UniqueKey() UniqueKey { + return partialExpandedModuleKey(per.String()) +} + +type partialExpandedModuleKey string + +var _ UniqueKey = partialExpandedModuleKey("") + +func (partialExpandedModuleKey) uniqueKeySigil() {} + +// PartialExpandedResource represents a set of resource instances which all share +// a common known parent module instance but the remaining call instance keys +// are not yet known and the resource's own instance keys are not yet known. +// +// A PartialExpandedResource with a fully-known module instance address is +// semantically interchangable with an [AbsResource], which is useful when we +// need to represent an assortment of variously-unknown resource instance +// addresses, but [AbsResource] is preferable in situations where the module +// instance address is _always_ known and it's only the resource instance +// key that is not represented. +type PartialExpandedResource struct { + // module is the partially-expanded module instance address that this + // resource belongs to. + // + // This value can actually represent a fully-expanded module if its + // unexpandedSuffix field is zero-length, in which case it's only the + // resource itself that's unexpanded, which would make this equivalent + // to an AbsResource. + // + // We mustn't directly expose this value in the public API because + // external callers must never see a PartialExpandedModule that is + // actually fully-expanded; that should be a ModuleInstance instead. + module PartialExpandedModule + resource Resource +} + +// UnexpandedResource returns the address of a child resource expressed as a +// [PartialExpandedResource]. +// +// The result always has a fully-qualified module instance address and is +// therefore semantically equivalent to an [AbsResource], so this variannt +// should be used only in contexts where we might also be storing resources +// belonging to not-fully-expanded modules and need to use the same static +// address type for all of them. +func (m ModuleInstance) UnexpandedResource(resource Resource) PartialExpandedResource { + return PartialExpandedResource{ + module: PartialExpandedModule{ + expandedPrefix: m, + }, + resource: resource, + } +} + +// UnexpandedResource returns the receiver reinterpreted as a +// [PartialExpandedResource], which is an alternative form we use in situations +// where we might also need to mix in resources belonging to not-yet-fully-known +// module instance addresses. +func (r AbsResource) UnexpandedResource() PartialExpandedResource { + return PartialExpandedResource{ + module: PartialExpandedModule{ + expandedPrefix: r.Module, + }, + resource: r.Resource, + } +} + +// MatchesInstance returns true if and only if the given resource instance +// belongs to the recieving partially-expanded resource address pattern. +func (per PartialExpandedResource) MatchesInstance(inst AbsResourceInstance) bool { + if !per.module.MatchesInstance(inst.Module) { + return false + } + return inst.Resource.Resource.Equal(per.resource) +} + +// MatchesResource returns true if and only if the given resource belongs to +// the recieving partially-expanded resource address pattern. +func (per PartialExpandedResource) MatchesResource(inst AbsResource) bool { + if !per.module.MatchesInstance(inst.Module) { + return false + } + return inst.Resource.Equal(per.resource) +} + +// AbsResource returns the single [AbsResource] that this address represents +// if this pattern is specific enough to match only a single resource, or +// the zero value of AbsResource if not. +// +// The second return value is true if and only if the returned address is valid. +func (per PartialExpandedResource) AbsResource() (AbsResource, bool) { + if len(per.module.unexpandedSuffix) != 0 { + return AbsResource{}, false + } + + return AbsResource{ + Module: per.module.expandedPrefix, + Resource: per.resource, + }, true +} + +// ConfigResource returns the unexpanded resource address that this +// partially-expanded resource address originates from. +func (per PartialExpandedResource) ConfigResource() ConfigResource { + return ConfigResource{ + Module: per.module.Module(), + Resource: per.resource, + } +} + +// Resource returns just the leaf resource address that this partially-expanded +// resource address uses, discarding the containing module instance information +// altogether. +func (per PartialExpandedResource) Resource() Resource { + return per.resource +} + +// KnownModuleInstancePrefix returns the longest possible ModuleInstance address +// made of known segments of the module instances that this set of resource +// instances all belong to. +// +// If the whole module instance address is known and only the resource +// instances are not then this returns the full prefix, which will be the same +// as the module from a successful return value from +// [PartialExpandedResource.AbsResource]. +func (per PartialExpandedResource) KnownModuleInstancePrefix() ModuleInstance { + return per.module.KnownPrefix() +} + +// String returns a string representation of the pattern which uses the special +// placeholder "[*]" to represent positions where instance keys are not yet +// known. +func (per PartialExpandedResource) String() string { + moduleAddr := per.module.String() + if len(moduleAddr) != 0 { + return moduleAddr + "." + per.resource.String() + "[*]" + } + return per.resource.String() + "[*]" +} + +func (per PartialExpandedResource) UniqueKey() UniqueKey { + // If this address is equivalent to an AbsResource address then we'll + // return its instance key here so that function Equivalent will consider + // the two as equivalent. + if ar, ok := per.AbsResource(); ok { + return ar.UniqueKey() + } + // For not-fully-expanded module paths we'll use a distinct address type + // since there is no other address type equivalent to those. + return partialExpandedResourceKey(per.String()) +} + +type partialExpandedResourceKey string + +var _ UniqueKey = partialExpandedModuleKey("") + +func (partialExpandedResourceKey) uniqueKeySigil() {}