mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-03-20 22:01:25 -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>
106 lines
3.2 KiB
Go
106 lines
3.2 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 (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclparse"
|
|
"github.com/spf13/afero"
|
|
)
|
|
|
|
// Parser is the main interface to read configuration files and other related
|
|
// files from disk.
|
|
//
|
|
// It retains a cache of all files that are loaded so that they can be used
|
|
// to create source code snippets in diagnostics, etc.
|
|
type Parser struct {
|
|
fs afero.Afero
|
|
p *hclparse.Parser
|
|
}
|
|
|
|
// NewParser creates and returns a new Parser that reads files from the given
|
|
// filesystem. If a nil filesystem is passed then the system's "real" filesystem
|
|
// will be used, via afero.OsFs.
|
|
func NewParser(fs afero.Fs) *Parser {
|
|
if fs == nil {
|
|
fs = afero.OsFs{}
|
|
}
|
|
|
|
return &Parser{
|
|
fs: afero.Afero{Fs: fs},
|
|
p: hclparse.NewParser(),
|
|
}
|
|
}
|
|
|
|
// LoadHCLFile is a low-level method that reads the file at the given path,
|
|
// parses it, and returns the hcl.Body representing its root. In many cases
|
|
// it is better to use one of the other Load*File methods on this type,
|
|
// which additionally decode the root body in some way and return a higher-level
|
|
// construct.
|
|
//
|
|
// If the file cannot be read at all -- e.g. because it does not exist -- then
|
|
// this method will return a nil body and error diagnostics. In this case
|
|
// callers may wish to ignore the provided error diagnostics and produce
|
|
// a more context-sensitive error instead.
|
|
//
|
|
// The file will be parsed using the HCL native syntax unless the filename
|
|
// ends with ".json", in which case the HCL JSON syntax will be used.
|
|
func (p *Parser) LoadHCLFile(path string) (hcl.Body, hcl.Diagnostics) {
|
|
src, err := p.fs.ReadFile(path)
|
|
|
|
if err != nil {
|
|
return nil, hcl.Diagnostics{
|
|
{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Failed to read file",
|
|
Detail: fmt.Sprintf("The file %q could not be read.", path),
|
|
},
|
|
}
|
|
}
|
|
|
|
var file *hcl.File
|
|
var diags hcl.Diagnostics
|
|
switch {
|
|
case strings.HasSuffix(path, ".json"):
|
|
file, diags = p.p.ParseJSON(src, path)
|
|
default:
|
|
file, diags = p.p.ParseHCL(src, path)
|
|
}
|
|
|
|
// If the returned file or body is nil, then we'll return a non-nil empty
|
|
// body so we'll meet our contract that nil means an error reading the file.
|
|
if file == nil || file.Body == nil {
|
|
return hcl.EmptyBody(), diags
|
|
}
|
|
|
|
return file.Body, diags
|
|
}
|
|
|
|
// Sources returns a map of the cached source buffers for all files that
|
|
// have been loaded through this parser, with source filenames (as requested
|
|
// when each file was opened) as the keys.
|
|
func (p *Parser) Sources() map[string]*hcl.File {
|
|
return p.p.Files()
|
|
}
|
|
|
|
// ForceFileSource artificially adds source code to the cache of file sources,
|
|
// as if it had been loaded from the given filename.
|
|
//
|
|
// This should be used only in special situations where configuration is loaded
|
|
// some other way. Most callers should load configuration via methods of
|
|
// Parser, which will update the sources cache automatically.
|
|
func (p *Parser) ForceFileSource(filename string, src []byte) {
|
|
// We'll make a synthetic hcl.File here just so we can reuse the
|
|
// existing cache.
|
|
p.p.AddFile(filename, &hcl.File{
|
|
Body: hcl.EmptyBody(),
|
|
Bytes: src,
|
|
})
|
|
}
|