Skip to content
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

Merged
merged 3 commits into from
May 17, 2021

Conversation

apparentlymart
Copy link
Contributor

@apparentlymart apparentlymart commented May 15, 2021

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:

  • In provider, resource, and data blocks Terraform overlays reserved meta-arguments onto the relevant provider schema, effectively masking any provider-defined argument with a conflicting name.
  • In 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 reserved count in the v0.12 release.
  • In provisioner blocks Terraform overlays reserved meta-arguments onto the provisioner configuration schema, likewise masking any provisioner-defined argument with a conflicting name.
  • References to managed resources use the resource type name with no prefix, like aws_instance.foo, but certain prefixes are reserved for other purposes (such as var., 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. Prefix

When we refer to data resources, we write data.type.name which means that there's no ambiguity that the second component type is always the name of a data resource type. The resource. prefix aims to get the same result for managed resources, so that resource.aws_instance.foo is just an equivalent alias for aws_instance.foo as long as aws_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 the resource. prefix, leaving all other references unchanged. For example, foo.bar.baz would be come resource.foo.bar.baz, but aws_instance.foo would remain just aws_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 a resource. 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, and module blocks at the top level, and also to provisioner blocks nested directly inside resource 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-argument only_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 named only_if which could be "escaped" using an escaping block, like this:

resource "example" "foo" {
  # most arguments stay out here to be
  # processed as normal
  beep = "boop"

  _ {
    # Arguments that conflict with a later-added
    # meta-argument can move in here to
    # retain the old interpretation.
    only_if = var.something
  }
}

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 type example 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: a resource block containing a dynamic "lifecycle" block can produce an equivalent effect as an escaping block with a plain lifecycle 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., and arg.. 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 just template. 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 writing resource.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.

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".
@apparentlymart apparentlymart added enhancement config 0.15-backport If you add this label to a PR before merging, backport-assistant will open a new PR once merged labels May 15, 2021
@apparentlymart apparentlymart requested a review from a team May 15, 2021 00:41
@apparentlymart apparentlymart self-assigned this May 15, 2021
Copy link
Contributor

@alisdair alisdair left a 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.

@apparentlymart apparentlymart removed the 0.15-backport If you add this label to a PR before merging, backport-assistant will open a new PR once merged label May 17, 2021
@apparentlymart apparentlymart merged commit 8744f0e into main May 17, 2021
@apparentlymart apparentlymart deleted the f-namespace-escapes branch May 17, 2021 18:22
@apparentlymart
Copy link
Contributor Author

Backported to v0.15:

@github-actions
Copy link
Contributor

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.
If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 17, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants