mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-05-17 01:03:30 -04:00
Previously we interpreted a "required_version" argument in a "terraform" block as if it were specifying an OpenTofu version constraint, when in reality most modules use this to represent a version constraint for OpenTofu's predecessor instead. The primary effect of this commit is to introduce a new top-level block type called "language" which describes language and implementation compatibility metadata in a way that intentionally differs from what's used by OpenTofu's predecessor. This also causes OpenTofu to ignore the required_version argument unless it appears in an OpenTofu-specific file with a ".tofu" suffix, and makes OpenTofu completely ignore the language edition and experimental feature opt-in options from OpenTofu's predecessor on the assumption that those could continue to evolve independently of changes in OpenTofu. We retain support for using required_versions in .tofu files as a bridge solution for modules that need to remain compatible with OpenTofu versions prior to v1.12. Module authors should keep following the strategy of having both a versions.tf and a versions.tofu file for now, and wait until the OpenTofu v1.11 series is end-of-life before adopting the new "language" block type. I also took this opportunity to simplify how we handle these parts of the configuration, since the OpenTofu project has no immediate plans to use either multiple language editions or language experiments and so for now we can reduce our handling of those language features to just enough that we'd return reasonable error messages if today's OpenTofu is exposed to a module that was written for a newer version of OpenTofu that extends these language features. The cross-cutting plumbing for representing the active experiments for a module is still present so that we can reactivate it later if we need to, but for now that set will always be empty. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
132 lines
3.3 KiB
Go
132 lines
3.3 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package configs
|
|
|
|
import (
|
|
"testing"
|
|
|
|
hcVersion "github.com/hashicorp/go-version"
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hcltest"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
func TestRequiredVersion(t *testing.T) {
|
|
tests := []struct {
|
|
Name string
|
|
CurrentVersion string
|
|
RequiredVersions string
|
|
Err bool
|
|
}{
|
|
{
|
|
"doesn't match",
|
|
"0.1.0",
|
|
"> 0.6.0",
|
|
true,
|
|
},
|
|
{
|
|
"matches",
|
|
"0.7.0",
|
|
"> 0.6.0",
|
|
false,
|
|
},
|
|
{
|
|
"prerelease doesn't match with inequality",
|
|
"0.8.0",
|
|
"> 0.7.0-beta",
|
|
true,
|
|
},
|
|
{
|
|
"prerelease doesn't match with equality",
|
|
"0.7.0",
|
|
"0.7.0-beta",
|
|
true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
|
|
t.Run(test.Name, func(t *testing.T) {
|
|
fakeSourceRange := hcl.Range{
|
|
// This must have a .tofu suffix for the required_version
|
|
// subtest to work, because we only support that legacy form
|
|
// in OpenTofu-specific files.
|
|
Filename: "versions.tofu",
|
|
Start: hcl.InitialPos,
|
|
End: hcl.InitialPos,
|
|
}
|
|
currentVersion := hcVersion.Must(hcVersion.NewVersion(test.CurrentVersion))
|
|
|
|
t.Logf("matching constraint %q against current version %q", test.RequiredVersions, test.CurrentVersion)
|
|
|
|
t.Run("language block", func(t *testing.T) {
|
|
body := hcltest.MockBody(&hcl.BodyContent{
|
|
Blocks: []*hcl.Block{
|
|
{
|
|
Type: "language",
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Blocks: []*hcl.Block{
|
|
{
|
|
Type: "compatible_with",
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"opentofu": {
|
|
Name: "opentofu",
|
|
Expr: hcl.StaticExpr(cty.StringVal(test.RequiredVersions), fakeSourceRange),
|
|
Range: fakeSourceRange,
|
|
NameRange: fakeSourceRange,
|
|
},
|
|
},
|
|
}),
|
|
DefRange: fakeSourceRange,
|
|
TypeRange: fakeSourceRange,
|
|
},
|
|
},
|
|
}),
|
|
DefRange: fakeSourceRange,
|
|
TypeRange: fakeSourceRange,
|
|
},
|
|
},
|
|
})
|
|
diags := checkVersionRequirements(body, currentVersion)
|
|
if test.Err && !diags.HasErrors() {
|
|
t.Error("unexpected success; want error")
|
|
} else if !test.Err && diags.HasErrors() {
|
|
t.Errorf("unexpected error: %s", diags.Error())
|
|
}
|
|
})
|
|
|
|
t.Run("legacy required_version", func(t *testing.T) {
|
|
body := hcltest.MockBody(&hcl.BodyContent{
|
|
Blocks: []*hcl.Block{
|
|
{
|
|
Type: "terraform",
|
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
|
Attributes: hcl.Attributes{
|
|
"required_version": {
|
|
Name: "required_version",
|
|
Expr: hcl.StaticExpr(cty.StringVal(test.RequiredVersions), fakeSourceRange),
|
|
Range: fakeSourceRange,
|
|
NameRange: fakeSourceRange,
|
|
},
|
|
},
|
|
}),
|
|
DefRange: fakeSourceRange,
|
|
TypeRange: fakeSourceRange,
|
|
},
|
|
},
|
|
})
|
|
diags := checkVersionRequirements(body, currentVersion)
|
|
if test.Err && !diags.HasErrors() {
|
|
t.Error("unexpected success; want error")
|
|
} else if !test.Err && diags.HasErrors() {
|
|
t.Errorf("unexpected error: %s", diags.Error())
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|