mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-29 10:21:01 -06:00
39e609d5fd
Previously we were using the experimental HCL 2 repository, but now we'll shift over to the v2 import path within the main HCL repository as part of actually releasing HCL 2.0 as stable. This is a mechanical search/replace to the new import paths. It also switches to the v2.0.0 release of HCL, which includes some new code that Terraform didn't previously have but should not change any behavior that matters for Terraform's purposes. For the moment the experimental HCL2 repository is still an indirect dependency via terraform-config-inspect, so it remains in our go.sum and vendor directories for the moment. Because terraform-config-inspect uses a much smaller subset of the HCL2 functionality, this does still manage to prune the vendor directory a little. A subsequent release of terraform-config-inspect should allow us to completely remove that old repository in a future commit.
474 lines
12 KiB
Go
474 lines
12 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
hcl2 "github.com/hashicorp/hcl/v2"
|
|
gohcl2 "github.com/hashicorp/hcl/v2/gohcl"
|
|
hcl2parse "github.com/hashicorp/hcl/v2/hclparse"
|
|
"github.com/hashicorp/terraform/configs/hcl2shim"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
// hcl2Configurable is an implementation of configurable that knows
|
|
// how to turn a HCL Body into a *Config object.
|
|
type hcl2Configurable struct {
|
|
SourceFilename string
|
|
Body hcl2.Body
|
|
}
|
|
|
|
// hcl2Loader is a wrapper around a HCL parser that provides a fileLoaderFunc.
|
|
type hcl2Loader struct {
|
|
Parser *hcl2parse.Parser
|
|
}
|
|
|
|
// For the moment we'll just have a global loader since we don't have anywhere
|
|
// better to stash this.
|
|
// TODO: refactor the loader API so that it uses some sort of object we can
|
|
// stash the parser inside.
|
|
var globalHCL2Loader = newHCL2Loader()
|
|
|
|
// newHCL2Loader creates a new hcl2Loader containing a new HCL Parser.
|
|
//
|
|
// HCL parsers retain information about files that are loaded to aid in
|
|
// producing diagnostic messages, so all files within a single configuration
|
|
// should be loaded with the same parser to ensure the availability of
|
|
// full diagnostic information.
|
|
func newHCL2Loader() hcl2Loader {
|
|
return hcl2Loader{
|
|
Parser: hcl2parse.NewParser(),
|
|
}
|
|
}
|
|
|
|
// loadFile is a fileLoaderFunc that knows how to read a HCL2 file and turn it
|
|
// into a hcl2Configurable.
|
|
func (l hcl2Loader) loadFile(filename string) (configurable, []string, error) {
|
|
var f *hcl2.File
|
|
var diags hcl2.Diagnostics
|
|
if strings.HasSuffix(filename, ".json") {
|
|
f, diags = l.Parser.ParseJSONFile(filename)
|
|
} else {
|
|
f, diags = l.Parser.ParseHCLFile(filename)
|
|
}
|
|
if diags.HasErrors() {
|
|
// Return diagnostics as an error; callers may type-assert this to
|
|
// recover the original diagnostics, if it doesn't end up wrapped
|
|
// in another error.
|
|
return nil, nil, diags
|
|
}
|
|
|
|
return &hcl2Configurable{
|
|
SourceFilename: filename,
|
|
Body: f.Body,
|
|
}, nil, nil
|
|
}
|
|
|
|
func (t *hcl2Configurable) Config() (*Config, error) {
|
|
config := &Config{}
|
|
|
|
// these structs are used only for the initial shallow decoding; we'll
|
|
// expand this into the main, public-facing config structs afterwards.
|
|
type atlas struct {
|
|
Name string `hcl:"name"`
|
|
Include *[]string `hcl:"include"`
|
|
Exclude *[]string `hcl:"exclude"`
|
|
}
|
|
type provider struct {
|
|
Name string `hcl:"name,label"`
|
|
Alias *string `hcl:"alias,attr"`
|
|
Version *string `hcl:"version,attr"`
|
|
Config hcl2.Body `hcl:",remain"`
|
|
}
|
|
type module struct {
|
|
Name string `hcl:"name,label"`
|
|
Source string `hcl:"source,attr"`
|
|
Version *string `hcl:"version,attr"`
|
|
Providers *map[string]string `hcl:"providers,attr"`
|
|
Config hcl2.Body `hcl:",remain"`
|
|
}
|
|
type resourceLifecycle struct {
|
|
CreateBeforeDestroy *bool `hcl:"create_before_destroy,attr"`
|
|
PreventDestroy *bool `hcl:"prevent_destroy,attr"`
|
|
IgnoreChanges *[]string `hcl:"ignore_changes,attr"`
|
|
}
|
|
type connection struct {
|
|
Config hcl2.Body `hcl:",remain"`
|
|
}
|
|
type provisioner struct {
|
|
Type string `hcl:"type,label"`
|
|
|
|
When *string `hcl:"when,attr"`
|
|
OnFailure *string `hcl:"on_failure,attr"`
|
|
|
|
Connection *connection `hcl:"connection,block"`
|
|
Config hcl2.Body `hcl:",remain"`
|
|
}
|
|
type managedResource struct {
|
|
Type string `hcl:"type,label"`
|
|
Name string `hcl:"name,label"`
|
|
|
|
CountExpr hcl2.Expression `hcl:"count,attr"`
|
|
Provider *string `hcl:"provider,attr"`
|
|
DependsOn *[]string `hcl:"depends_on,attr"`
|
|
|
|
Lifecycle *resourceLifecycle `hcl:"lifecycle,block"`
|
|
Provisioners []provisioner `hcl:"provisioner,block"`
|
|
Connection *connection `hcl:"connection,block"`
|
|
|
|
Config hcl2.Body `hcl:",remain"`
|
|
}
|
|
type dataResource struct {
|
|
Type string `hcl:"type,label"`
|
|
Name string `hcl:"name,label"`
|
|
|
|
CountExpr hcl2.Expression `hcl:"count,attr"`
|
|
Provider *string `hcl:"provider,attr"`
|
|
DependsOn *[]string `hcl:"depends_on,attr"`
|
|
|
|
Config hcl2.Body `hcl:",remain"`
|
|
}
|
|
type variable struct {
|
|
Name string `hcl:"name,label"`
|
|
|
|
DeclaredType *string `hcl:"type,attr"`
|
|
Default *cty.Value `hcl:"default,attr"`
|
|
Description *string `hcl:"description,attr"`
|
|
Sensitive *bool `hcl:"sensitive,attr"`
|
|
}
|
|
type output struct {
|
|
Name string `hcl:"name,label"`
|
|
|
|
ValueExpr hcl2.Expression `hcl:"value,attr"`
|
|
DependsOn *[]string `hcl:"depends_on,attr"`
|
|
Description *string `hcl:"description,attr"`
|
|
Sensitive *bool `hcl:"sensitive,attr"`
|
|
}
|
|
type locals struct {
|
|
Definitions hcl2.Attributes `hcl:",remain"`
|
|
}
|
|
type backend struct {
|
|
Type string `hcl:"type,label"`
|
|
Config hcl2.Body `hcl:",remain"`
|
|
}
|
|
type terraform struct {
|
|
RequiredVersion *string `hcl:"required_version,attr"`
|
|
Backend *backend `hcl:"backend,block"`
|
|
}
|
|
type topLevel struct {
|
|
Atlas *atlas `hcl:"atlas,block"`
|
|
Datas []dataResource `hcl:"data,block"`
|
|
Modules []module `hcl:"module,block"`
|
|
Outputs []output `hcl:"output,block"`
|
|
Providers []provider `hcl:"provider,block"`
|
|
Resources []managedResource `hcl:"resource,block"`
|
|
Terraform *terraform `hcl:"terraform,block"`
|
|
Variables []variable `hcl:"variable,block"`
|
|
Locals []*locals `hcl:"locals,block"`
|
|
}
|
|
|
|
var raw topLevel
|
|
diags := gohcl2.DecodeBody(t.Body, nil, &raw)
|
|
if diags.HasErrors() {
|
|
// Do some minimal decoding to see if we can at least get the
|
|
// required Terraform version, which might help explain why we
|
|
// couldn't parse the rest.
|
|
if raw.Terraform != nil && raw.Terraform.RequiredVersion != nil {
|
|
config.Terraform = &Terraform{
|
|
RequiredVersion: *raw.Terraform.RequiredVersion,
|
|
}
|
|
}
|
|
|
|
// We return the diags as an implementation of error, which the
|
|
// caller than then type-assert if desired to recover the individual
|
|
// diagnostics.
|
|
// FIXME: The current API gives us no way to return warnings in the
|
|
// absence of any errors.
|
|
return config, diags
|
|
}
|
|
|
|
if raw.Terraform != nil {
|
|
var reqdVersion string
|
|
var backend *Backend
|
|
|
|
if raw.Terraform.RequiredVersion != nil {
|
|
reqdVersion = *raw.Terraform.RequiredVersion
|
|
}
|
|
if raw.Terraform.Backend != nil {
|
|
backend = new(Backend)
|
|
backend.Type = raw.Terraform.Backend.Type
|
|
|
|
// We don't permit interpolations or nested blocks inside the
|
|
// backend config, so we can decode the config early here and
|
|
// get direct access to the values, which is important for the
|
|
// config hashing to work as expected.
|
|
var config map[string]string
|
|
configDiags := gohcl2.DecodeBody(raw.Terraform.Backend.Config, nil, &config)
|
|
diags = append(diags, configDiags...)
|
|
|
|
raw := make(map[string]interface{}, len(config))
|
|
for k, v := range config {
|
|
raw[k] = v
|
|
}
|
|
|
|
var err error
|
|
backend.RawConfig, err = NewRawConfig(raw)
|
|
if err != nil {
|
|
diags = append(diags, &hcl2.Diagnostic{
|
|
Severity: hcl2.DiagError,
|
|
Summary: "Invalid backend configuration",
|
|
Detail: fmt.Sprintf("Error in backend configuration: %s", err),
|
|
})
|
|
}
|
|
}
|
|
|
|
config.Terraform = &Terraform{
|
|
RequiredVersion: reqdVersion,
|
|
Backend: backend,
|
|
}
|
|
}
|
|
|
|
if raw.Atlas != nil {
|
|
var include, exclude []string
|
|
if raw.Atlas.Include != nil {
|
|
include = *raw.Atlas.Include
|
|
}
|
|
if raw.Atlas.Exclude != nil {
|
|
exclude = *raw.Atlas.Exclude
|
|
}
|
|
config.Atlas = &AtlasConfig{
|
|
Name: raw.Atlas.Name,
|
|
Include: include,
|
|
Exclude: exclude,
|
|
}
|
|
}
|
|
|
|
for _, rawM := range raw.Modules {
|
|
m := &Module{
|
|
Name: rawM.Name,
|
|
Source: rawM.Source,
|
|
RawConfig: NewRawConfigHCL2(rawM.Config),
|
|
}
|
|
|
|
if rawM.Version != nil {
|
|
m.Version = *rawM.Version
|
|
}
|
|
|
|
if rawM.Providers != nil {
|
|
m.Providers = *rawM.Providers
|
|
}
|
|
|
|
config.Modules = append(config.Modules, m)
|
|
}
|
|
|
|
for _, rawV := range raw.Variables {
|
|
v := &Variable{
|
|
Name: rawV.Name,
|
|
}
|
|
if rawV.DeclaredType != nil {
|
|
v.DeclaredType = *rawV.DeclaredType
|
|
}
|
|
if rawV.Default != nil {
|
|
v.Default = hcl2shim.ConfigValueFromHCL2(*rawV.Default)
|
|
}
|
|
if rawV.Description != nil {
|
|
v.Description = *rawV.Description
|
|
}
|
|
|
|
config.Variables = append(config.Variables, v)
|
|
}
|
|
|
|
for _, rawO := range raw.Outputs {
|
|
o := &Output{
|
|
Name: rawO.Name,
|
|
}
|
|
|
|
if rawO.Description != nil {
|
|
o.Description = *rawO.Description
|
|
}
|
|
if rawO.DependsOn != nil {
|
|
o.DependsOn = *rawO.DependsOn
|
|
}
|
|
if rawO.Sensitive != nil {
|
|
o.Sensitive = *rawO.Sensitive
|
|
}
|
|
|
|
// The result is expected to be a map like map[string]interface{}{"value": something},
|
|
// so we'll fake that with our hcl2shim.SingleAttrBody shim.
|
|
o.RawConfig = NewRawConfigHCL2(hcl2shim.SingleAttrBody{
|
|
Name: "value",
|
|
Expr: rawO.ValueExpr,
|
|
})
|
|
|
|
config.Outputs = append(config.Outputs, o)
|
|
}
|
|
|
|
for _, rawR := range raw.Resources {
|
|
r := &Resource{
|
|
Mode: ManagedResourceMode,
|
|
Type: rawR.Type,
|
|
Name: rawR.Name,
|
|
}
|
|
if rawR.Lifecycle != nil {
|
|
var l ResourceLifecycle
|
|
if rawR.Lifecycle.CreateBeforeDestroy != nil {
|
|
l.CreateBeforeDestroy = *rawR.Lifecycle.CreateBeforeDestroy
|
|
}
|
|
if rawR.Lifecycle.PreventDestroy != nil {
|
|
l.PreventDestroy = *rawR.Lifecycle.PreventDestroy
|
|
}
|
|
if rawR.Lifecycle.IgnoreChanges != nil {
|
|
l.IgnoreChanges = *rawR.Lifecycle.IgnoreChanges
|
|
}
|
|
r.Lifecycle = l
|
|
}
|
|
if rawR.Provider != nil {
|
|
r.Provider = *rawR.Provider
|
|
}
|
|
if rawR.DependsOn != nil {
|
|
r.DependsOn = *rawR.DependsOn
|
|
}
|
|
|
|
var defaultConnInfo *RawConfig
|
|
if rawR.Connection != nil {
|
|
defaultConnInfo = NewRawConfigHCL2(rawR.Connection.Config)
|
|
}
|
|
|
|
for _, rawP := range rawR.Provisioners {
|
|
p := &Provisioner{
|
|
Type: rawP.Type,
|
|
}
|
|
|
|
switch {
|
|
case rawP.When == nil:
|
|
p.When = ProvisionerWhenCreate
|
|
case *rawP.When == "create":
|
|
p.When = ProvisionerWhenCreate
|
|
case *rawP.When == "destroy":
|
|
p.When = ProvisionerWhenDestroy
|
|
default:
|
|
p.When = ProvisionerWhenInvalid
|
|
}
|
|
|
|
switch {
|
|
case rawP.OnFailure == nil:
|
|
p.OnFailure = ProvisionerOnFailureFail
|
|
case *rawP.When == "fail":
|
|
p.OnFailure = ProvisionerOnFailureFail
|
|
case *rawP.When == "continue":
|
|
p.OnFailure = ProvisionerOnFailureContinue
|
|
default:
|
|
p.OnFailure = ProvisionerOnFailureInvalid
|
|
}
|
|
|
|
if rawP.Connection != nil {
|
|
p.ConnInfo = NewRawConfigHCL2(rawP.Connection.Config)
|
|
} else {
|
|
p.ConnInfo = defaultConnInfo
|
|
}
|
|
|
|
p.RawConfig = NewRawConfigHCL2(rawP.Config)
|
|
|
|
r.Provisioners = append(r.Provisioners, p)
|
|
}
|
|
|
|
// The old loader records the count expression as a weird RawConfig with
|
|
// a single-element map inside. Since the rest of the world is assuming
|
|
// that, we'll mimic it here.
|
|
{
|
|
countBody := hcl2shim.SingleAttrBody{
|
|
Name: "count",
|
|
Expr: rawR.CountExpr,
|
|
}
|
|
|
|
r.RawCount = NewRawConfigHCL2(countBody)
|
|
r.RawCount.Key = "count"
|
|
}
|
|
|
|
r.RawConfig = NewRawConfigHCL2(rawR.Config)
|
|
|
|
config.Resources = append(config.Resources, r)
|
|
|
|
}
|
|
|
|
for _, rawR := range raw.Datas {
|
|
r := &Resource{
|
|
Mode: DataResourceMode,
|
|
Type: rawR.Type,
|
|
Name: rawR.Name,
|
|
}
|
|
|
|
if rawR.Provider != nil {
|
|
r.Provider = *rawR.Provider
|
|
}
|
|
if rawR.DependsOn != nil {
|
|
r.DependsOn = *rawR.DependsOn
|
|
}
|
|
|
|
// The old loader records the count expression as a weird RawConfig with
|
|
// a single-element map inside. Since the rest of the world is assuming
|
|
// that, we'll mimic it here.
|
|
{
|
|
countBody := hcl2shim.SingleAttrBody{
|
|
Name: "count",
|
|
Expr: rawR.CountExpr,
|
|
}
|
|
|
|
r.RawCount = NewRawConfigHCL2(countBody)
|
|
r.RawCount.Key = "count"
|
|
}
|
|
|
|
r.RawConfig = NewRawConfigHCL2(rawR.Config)
|
|
|
|
config.Resources = append(config.Resources, r)
|
|
}
|
|
|
|
for _, rawP := range raw.Providers {
|
|
p := &ProviderConfig{
|
|
Name: rawP.Name,
|
|
}
|
|
|
|
if rawP.Alias != nil {
|
|
p.Alias = *rawP.Alias
|
|
}
|
|
if rawP.Version != nil {
|
|
p.Version = *rawP.Version
|
|
}
|
|
|
|
// The result is expected to be a map like map[string]interface{}{"value": something},
|
|
// so we'll fake that with our hcl2shim.SingleAttrBody shim.
|
|
p.RawConfig = NewRawConfigHCL2(rawP.Config)
|
|
|
|
config.ProviderConfigs = append(config.ProviderConfigs, p)
|
|
}
|
|
|
|
for _, rawL := range raw.Locals {
|
|
names := make([]string, 0, len(rawL.Definitions))
|
|
for n := range rawL.Definitions {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
for _, n := range names {
|
|
attr := rawL.Definitions[n]
|
|
l := &Local{
|
|
Name: n,
|
|
RawConfig: NewRawConfigHCL2(hcl2shim.SingleAttrBody{
|
|
Name: "value",
|
|
Expr: attr.Expr,
|
|
}),
|
|
}
|
|
config.Locals = append(config.Locals, l)
|
|
}
|
|
}
|
|
|
|
// FIXME: The current API gives us no way to return warnings in the
|
|
// absence of any errors.
|
|
var err error
|
|
if diags.HasErrors() {
|
|
err = diags
|
|
}
|
|
|
|
return config, err
|
|
}
|