474 lines
12 KiB
Go
474 lines
12 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
gohcl2 "github.com/hashicorp/hcl2/gohcl"
|
|
hcl2 "github.com/hashicorp/hcl2/hcl"
|
|
hcl2parse "github.com/hashicorp/hcl2/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
|
|
// absense 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
|
|
// absense of any errors.
|
|
var err error
|
|
if diags.HasErrors() {
|
|
err = diags
|
|
}
|
|
|
|
return config, err
|
|
}
|