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 }