configs: Accept and minimally validate a "language" argument

We expect that in order to continue to evolve the language without
breaking existing modules we will at some point need to have a way to mark
when a particular module is expecting a newer interpretation of the
language.

Although it's too early to do any deep preparation for that, this commit
aims to proactively reserve an argument named "language" inside
"terraform" blocks, which currently only accepts the keyword TF2021 that
is intended to represent "the edition of the Terraform language as defined
in 2021".

That argument also defaults to TF2021 if not set, so in practice there's
no real reason to set this today, but this minimal validation today is
intended to give better feedback to users of older Terraform versions in
the event that we introduce a new language edition later and they try to
use an module incompatible with their Terraform version.
This commit is contained in:
Martin Atkins 2021-02-26 09:48:42 -08:00
parent 54cc4dadf6
commit b5adc33075
5 changed files with 67 additions and 5 deletions

View File

@ -5,6 +5,7 @@ import (
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/experiments" "github.com/hashicorp/terraform/experiments"
"github.com/hashicorp/terraform/version"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
) )
@ -25,6 +26,49 @@ func sniffActiveExperiments(body hcl.Body) (experiments.Set, hcl.Diagnostics) {
content, _, blockDiags := block.Body.PartialContent(configFileExperimentsSniffBlockSchema) content, _, blockDiags := block.Body.PartialContent(configFileExperimentsSniffBlockSchema)
diags = append(diags, blockDiags...) diags = append(diags, blockDiags...)
if attr, exists := content.Attributes["language"]; exists {
// We don't yet have a sense of selecting an edition of the
// language, but we're reserving this syntax for now so that
// if and when we do this later older versions of Terraform
// will emit a more helpful error message than just saying
// this attribute doesn't exist. Handling this as part of
// experiments is a bit odd for now but justified by the
// fact that a future fuller implementation of switchable
// languages would be likely use a similar implementation
// strategy as experiments, and thus would lead to this
// function being refactored to deal with both concerns at
// once. We'll see, though!
kw := hcl.ExprAsKeyword(attr.Expr)
currentVersion := version.SemVer.String()
const firstEdition = "TF2021"
switch {
case kw == "": // (the expression wasn't a keyword at all)
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid language edition",
Detail: fmt.Sprintf(
"The language argument expects a bare language edition keyword. Terraform %s supports only language edition %s, which is the default.",
currentVersion, firstEdition,
),
Subject: attr.Expr.Range().Ptr(),
})
case kw != firstEdition:
rel := "different"
if kw > firstEdition { // would be weird for this not to be true, but it's user input so anything goes
rel = "newer"
}
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unsupported language edition",
Detail: fmt.Sprintf(
"Terraform v%s only supports language edition %s. This module requires a %s version of Terraform CLI.",
currentVersion, firstEdition, rel,
),
Subject: attr.Expr.Range().Ptr(),
})
}
}
attr, exists := content.Attributes["experiments"] attr, exists := content.Attributes["experiments"]
if !exists { if !exists {
continue continue

View File

@ -58,8 +58,8 @@ func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnost
content, contentDiags := block.Body.Content(terraformBlockSchema) content, contentDiags := block.Body.Content(terraformBlockSchema)
diags = append(diags, contentDiags...) diags = append(diags, contentDiags...)
// We ignore the "terraform_version" and "experiments" attributes // We ignore the "terraform_version", "language" and "experiments"
// here because sniffCoreVersionRequirements and // attributes here because sniffCoreVersionRequirements and
// sniffActiveExperiments already dealt with those above. // sniffActiveExperiments already dealt with those above.
for _, innerBlock := range content.Blocks { for _, innerBlock := range content.Blocks {
@ -244,6 +244,7 @@ var terraformBlockSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{ Attributes: []hcl.AttributeSchema{
{Name: "required_version"}, {Name: "required_version"},
{Name: "experiments"}, {Name: "experiments"},
{Name: "language"},
}, },
Blocks: []hcl.BlockHeaderSchema{ Blocks: []hcl.BlockHeaderSchema{
{ {
@ -283,8 +284,7 @@ var configFileVersionSniffBlockSchema = &hcl.BodySchema{
// to decode a single attribute from inside a "terraform" block. // to decode a single attribute from inside a "terraform" block.
var configFileExperimentsSniffBlockSchema = &hcl.BodySchema{ var configFileExperimentsSniffBlockSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{ Attributes: []hcl.AttributeSchema{
{ {Name: "experiments"},
Name: "experiments", {Name: "language"},
},
}, },
} }

View File

@ -0,0 +1,4 @@
terraform {
# The language argument expects a bare keyword, not a string.
language = "TF2021" # ERROR: Invalid language edition
}

View File

@ -0,0 +1,6 @@
terraform {
# If a future change in this repository happens to make TF2038 a valid
# edition then this will start failing; in that case, change this file to
# select a different edition that isn't supported.
language = TF2038 # ERROR: Unsupported language edition
}

View File

@ -0,0 +1,8 @@
terraform {
# If we drop support for TF2021 in a future Terraform release then this
# test will fail. In that case, update this to a newer edition that is
# still supported, because the purpose of this test is to verify that
# we can successfully decode the language argument, not specifically
# that we support TF2021.
language = TF2021
}