-
Notifications
You must be signed in to change notification settings - Fork 9.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mechanisms to allow for future language extension #28709
Conversation
Several top-level block types in the Terraform language have a body where two different schemas are overlayed on top of one another: Terraform first looks for "meta-arguments" that are built into the language, and then evaluates all of the remaining arguments against some externally-defined schema whose content is not fully controlled by Terraform. So far we've been cautiously adding new meta-arguments in these namespaces after research shows us that there are relatively few existing providers or modules that would have functionality masked by those additions, but that isn't really a viable path forward as we prepare to make stronger compatibility promises. In an earlier commit we've introduced the foundational parts of a new language versioning mechanism called "editions" which should allow us to make per-module-opt-in breaking changes in the future, but these shared namespaces remain a liability because it would be annoying if adopting a new edition made it impossible to use a feature of a third-party provider or module that was already using a name that has now become reserved in the new edition. This commit introduces a new syntax intended to be a rarely-used escape hatch for that situation. When we're designing new editions we will do our best to choose names that don't conflict with commonly-used providers and modules, but there are many providers and modules that we cannot see and so there is a risk that any name we might choose could collide with at least one existing provider or module. The automatic migration tool to upgrade an existing module to a new edition should therefore detect that situation and make use of this escaping block syntax in order to retain the existing functionality until all the called providers or modules are updated to no longer use conflicting names. Although we can't put in technical constraints on using this feature for other purposes (because we don't know yet what future editions will add), this mechanism is intentionally not documented for now because it serves no immediate purpose. In effect, this change is just squatting on the syntax of a special block type named "_" so that later editions can make use of it without it _also_ conflicting, creating a confusing nested escaping situation. However, the first time a new edition actually makes use of this syntax we should then document alongside the meta-arguments so folks can understand the meaning of escaping blocks produced by edition upgrade tools.
The current way to refer to a managed resource is to use its resource type name as a top-level symbol in the reference. This is convenient and makes sense given that managed resources are the primary kind of object in Terraform. However, it does mean that an externally-extensible namespace (the set of all possible resource type names) overlaps with a reserved word namespace (the special prefixes like "path", "var", etc), and thus introducing any new reserved prefix in future risks masking an existing resource type so it can't be used anymore. We only intend to introduce new reserved symbols as part of future language editions that each module can opt into separately, and when doing so we will always research to try to choose a name that doesn't overlap with commonly-used providers, but not all providers are visible to us and so there is always a small chance that the name we choose will already be in use by a third-party provider. In preparation for that event, this introduces an alternative way to refer to managed resources that mimics the reference style used for data resources: resource.type.name . When using this form, the second portion is _always_ a resource type name and never a reserved word. There is currently no need to use this because all of the already-reserved symbol names are effectively blocked from use by existing Terraform versions that lack this escape hatch. Therefore there's no explicit documentation about it yet. The intended use for this is that a module upgrade tool for a future language edition would detect references to resource types that have now become reserved words and add the "resource." prefix to keep that functionality working. Existing modules that aren't opted in to the new language edition would keep working without that prefix, thus keeping to compatibility promises.
At the time of this commit we have a proposal #28700 which would, if accepted, need to reserve a new reference prefix to represent template arguments. It seems unlikely that the proposal would be accepted and implemented before Terraform v1.0 creates additional compatibility constraints, and so this pre-emptively reserves a few candidate symbol names to allow something like that proposal to potentially move forward later without requiring a new opt-in language edition. If we do move forward with the proposal then we'll select one of these three reserved names depending on which form of the proposal we decide to move forward with, and then un-reserve the other two. If we decide to not pursue this proposal at all then we'll un-reserve all three once that decision is finalized. It's unlikely that there is any existing provider which has a resource type named either "template", "lazy", or "arg", but in that unlikely event users of that provider can keep using it by adding the "resource." escaping prefix, such as changing "lazy.foo.bar" into "resource.lazy.foo.bar".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These mechanisms all make sense to me! Thanks for the split into multiple commits.
I'm going to lock this pull request because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active contributions. |
The Terraform language has always had a number of situations where reserved words defined in the language can appear in the same location as externally-defined names provided by either providers or modules. These are:
provider
,resource
, anddata
blocks Terraform overlays reserved meta-arguments onto the relevant provider schema, effectively masking any provider-defined argument with a conflicting name.module
blocks Terraform overlays reserved meta-arguments onto the set of input variable names defined in the module. Currently we block modules from declaring such conflicting names, but that means that adding any new meta-arguments here is a breaking change for existing modules, as we previously saw when we reservedcount
in the v0.12 release.provisioner
blocks Terraform overlays reserved meta-arguments onto the provisioner configuration schema, likewise masking any provisioner-defined argument with a conflicting name.aws_instance.foo
, but certain prefixes are reserved for other purposes (such asvar.
,local.
,path.
, etc) which means that a managed resource of that name would not be referenceable.Each of these situations is a liability for future language expansion because it makes any additional reserved word a potential breaking change. While we would always do research to select a name not used in any commonly-used providers or modules, there are lots of providers and modules kept private within organizations and so there is always a risk that we'd pick a name that collides with a provider or module used only within a single organization.
In #27941 we reserved a mechanism which we intend to use in future to allow per-module opt-in to new "editions" of the Terraform language that may have breaking changes such as these, which does at least allow for a gradual module-by-module migration up to a new edition. However, our intent is that there should usually be an largely-automated process for upgrading a module to a newer edition without changing its behavior, and that means that we must provide a way to continue using module and provider features that have been newly-masked by reserved words until there's a newer version of the provider or module that supports a non-reserved alternative name.
This PR, then, proposes two new language additions that will together serve as an escape hatch for all of the situations described earlier, so that if we do introduce a new reserved word that is in use by a provider or module we cannot see we can offer a graceful migration path to still opt in to the new language edition without having to wait for the provider or module to be updated.
The
resource.
PrefixWhen we refer to data resources, we write
data.type.name
which means that there's no ambiguity that the second componenttype
is always the name of a data resource type. Theresource.
prefix aims to get the same result for managed resources, so thatresource.aws_instance.foo
is just an equivalent alias foraws_instance.foo
as long asaws_instance
is never a reserved word (which would be weird!).My intent is that we'd use this as part of the module upgrade tool for any new language edition which introduces a new reserved reference prefix. For example, if we'd introduced a new prefix
foo.
then the upgrade tool would search the module for any references that seem to refer to a resource type called "foo" and rewrite them to have theresource.
prefix, leaving all other references unchanged. For example,foo.bar.baz
would be comeresource.foo.bar.baz
, butaws_instance.foo
would remain justaws_instance.foo
.I don't intend to recommend using this prefix in cases where it isn't needed, because it'll make such references unnecessarily long. Although I didn't implement this here yet, I could imagine a future version of
terraform fmt
automatically trimming off aresource.
prefix for any reference where the resource type name isn't a reserved word in the currently-selected language edition, to help reinforce that this is for upgrade path use only.Meta-argument Escaping Blocks
The idea of "escaping blocks" aims to address all of the variants above which involve a mixture of meta-arguments and normal arguments inside a single HCL block body. This applies to
resource
,data
,provider
, andmodule
blocks at the top level, and also toprovisioner
blocks nested directly insideresource
blocks.This mechanism reserves the special and intentionally-odd-looking block type name
_
(just a single underscore) to contain block body items for which the author intends to avoid interpretation as meta-arguments. For example, if a later language edition introduced a new meta-argumentonly_if
(though there is no current plan to do so), it might be the case that there's an existing resource type or module that already has an argument namedonly_if
which could be "escaped" using an escaping block, like this:Terraform interprets the content of the escaping block as if it were written outside of the escaping block except for bypassing the usual pre-processing to handle meta-arguments, and thus
only_if
above would always be understood as an argument of the resource typeexample
rather than as a meta-argument.Again, my intent is that we'd use this as part of a module upgrade tool for a new language edition which introduces a new meta-argument. It would search the module configuration for any argument names that are now shadowed by the new reserved word and move them into an escaping block to allow the module to retain its existing functionality.
I don't intend to recommend using escaping blocks in situations where they aren't needed, because it's an intentionally-odd-looking syntax to try to help make it obviously distinct from "normal" blocks. Although I didn't implement this here yet, I could imagine a future version of
terraform fmt
automatically hoisting items out of an escaping block if they don't conflict with any relevant meta-argument names in the currently-selected language edition, to help reinforce that this is for upgrade path use only.It's interesting to note here that
dynamic
blocks actually already serve as a funny sort of escaping block which works only for reserved nested block types: aresource
block containing adynamic "lifecycle"
block can produce an equivalent effect as an escaping block with a plainlifecycle
block inside of it. Escaping blocks generalize that idea to include attributes too, and they offer a syntax that is easier to directly map from the normal syntax (retaining attached comments and all) in an upgrade tool.Some Other Loose Ends
Over in #28700 there's an early proposal for a new feature to allow separating template definition from template evaluation, which if accepted would require adding a new reserved reference prefix. It seems unlikely that we'll fully accept and implement that proposal before we adopt a stricter compatibility promise, but it would also be a shame to hold it all the way to a new language edition that's likely at least a year away, and so out of a sense of pragmatism here I've also pre-emptively reserved some prefixes that seem unlikely to already be used as resource type names and that might be what we choose as the prefix for the template proposal, if accepted.
These reserved prefixes are
template.
,lazy.
, andarg.
. Only zero or one of these will ultimately be used, depending on the outcome of the proposal, so the unused ones can be un-reserved later and become available for use as unescaped resource type names again.These additions also serve as a way to "rehearse" the use of the
resource.
escaping prefix to resolve any collisions: in the unlikely event that there's a private provider out there which has a resource type name which conflicts with one of these three, a module using that resource type can be rewritten to say e.g.resource.template.
instead of justtemplate.
and otherwise retain the existing behavior. Since we are prior to stricter compatibility promises there is no explicit opt-in here, but I'd like to make this tradeoff as a compromise to allow a discussion already in progress to conclude relatively promptly, rather than retroactively become subject to a new blocker on its implementation.The
resource.
prefix here does, of course, also theoretically conflict with a resource type literally named "resource". That also seems very unlikely in practice, but if it does arise then users can escape it by writingresource.resource.
, which looks silly but will buy some time to select a more reasonable resource type name without being blocked from upgrading Terraform.Nothing in this PR actually requires the addition of meta-argument escaping blocks yet -- they could in principle be added by the first future language edition that needs them -- but having this mechanism in place will give us some peace of mind that this path is available and also represent concretely in code our intentions for future language expansion.
It's pretty irregular to introduce stuff like this in a patch release, but with stricter compatibility promises imminent I think it's a worthwhile compromise to give us necessarily flexibility for both work currently in progress and future work we've not yet imagined. Therefore I'm proposing to backport this to the v0.15 branch.