diff --git a/configs/experiments.go b/configs/experiments.go index 82ff3bd91..83557a85e 100644 --- a/configs/experiments.go +++ b/configs/experiments.go @@ -5,6 +5,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/experiments" + "github.com/hashicorp/terraform/version" "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) 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"] if !exists { continue diff --git a/configs/parser_config.go b/configs/parser_config.go index 354b96a72..ec66b4cc5 100644 --- a/configs/parser_config.go +++ b/configs/parser_config.go @@ -58,8 +58,8 @@ func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnost content, contentDiags := block.Body.Content(terraformBlockSchema) diags = append(diags, contentDiags...) - // We ignore the "terraform_version" and "experiments" attributes - // here because sniffCoreVersionRequirements and + // We ignore the "terraform_version", "language" and "experiments" + // attributes here because sniffCoreVersionRequirements and // sniffActiveExperiments already dealt with those above. for _, innerBlock := range content.Blocks { @@ -244,6 +244,7 @@ var terraformBlockSchema = &hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ {Name: "required_version"}, {Name: "experiments"}, + {Name: "language"}, }, Blocks: []hcl.BlockHeaderSchema{ { @@ -283,8 +284,7 @@ var configFileVersionSniffBlockSchema = &hcl.BodySchema{ // to decode a single attribute from inside a "terraform" block. var configFileExperimentsSniffBlockSchema = &hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ - { - Name: "experiments", - }, + {Name: "experiments"}, + {Name: "language"}, }, } diff --git a/configs/testdata/error-files/invalid_language_edition.tf b/configs/testdata/error-files/invalid_language_edition.tf new file mode 100644 index 000000000..255aefe0a --- /dev/null +++ b/configs/testdata/error-files/invalid_language_edition.tf @@ -0,0 +1,4 @@ +terraform { + # The language argument expects a bare keyword, not a string. + language = "TF2021" # ERROR: Invalid language edition +} diff --git a/configs/testdata/error-files/unsupported_language_edition.tf b/configs/testdata/error-files/unsupported_language_edition.tf new file mode 100644 index 000000000..0ee616f73 --- /dev/null +++ b/configs/testdata/error-files/unsupported_language_edition.tf @@ -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 +} diff --git a/configs/testdata/valid-files/valid-language-edition.tf b/configs/testdata/valid-files/valid-language-edition.tf new file mode 100644 index 000000000..60f2df3eb --- /dev/null +++ b/configs/testdata/valid-files/valid-language-edition.tf @@ -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 +}