mirror of https://github.com/hashicorp/terraform
This is a first pass of decoding of the main Terraform configuration file format. It hasn't yet been tested with any real-world configurations, so it will need to be revised further as we test it more thoroughly.pull/17358/head
parent
b865d62bb8
commit
e15ec486bf
@ -0,0 +1,37 @@
|
||||
package configs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
)
|
||||
|
||||
func decodeDependsOn(attr *hcl.Attribute) ([]hcl.Traversal, hcl.Diagnostics) {
|
||||
var ret []hcl.Traversal
|
||||
exprs, diags := hcl.ExprList(attr.Expr)
|
||||
|
||||
for _, expr := range exprs {
|
||||
// A dependency reference was given as a string literal in the legacy
|
||||
// configuration language and there are lots of examples out there
|
||||
// showing that usage, so we'll sniff for that situation here and
|
||||
// produce a specialized error message for it to help users find
|
||||
// the new correct form.
|
||||
if exprIsNativeQuotedString(expr) {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid explicit dependency reference",
|
||||
Detail: fmt.Sprintf("%s elements must not be given in quotes.", attr.Name),
|
||||
Subject: attr.Expr.Range().Ptr(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
traversal, travDiags := hcl.AbsTraversalForExpr(expr)
|
||||
diags = append(diags, travDiags...)
|
||||
if len(traversal) != 0 {
|
||||
ret = append(ret, traversal)
|
||||
}
|
||||
}
|
||||
|
||||
return ret, diags
|
||||
}
|
||||
@ -1,18 +1,94 @@
|
||||
package configs
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl2/gohcl"
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
||||
)
|
||||
|
||||
// ModuleCall represents a "module" block in a module or file.
|
||||
type ModuleCall struct {
|
||||
Source string
|
||||
SourceRange hcl.Range
|
||||
Name string
|
||||
|
||||
SourceAddr string
|
||||
SourceAddrRange hcl.Range
|
||||
|
||||
Config hcl.Body
|
||||
|
||||
Version VersionConstraint
|
||||
|
||||
Count hcl.Expression
|
||||
ForEach hcl.Expression
|
||||
|
||||
DependsOn []hcl.Traversal
|
||||
|
||||
DeclRange hcl.Range
|
||||
}
|
||||
|
||||
func decodeModuleBlock(block *hcl.Block) (*ModuleCall, hcl.Diagnostics) {
|
||||
mc := &ModuleCall{
|
||||
Name: block.Labels[0],
|
||||
DeclRange: block.DefRange,
|
||||
}
|
||||
|
||||
content, remain, diags := block.Body.PartialContent(moduleBlockSchema)
|
||||
mc.Config = remain
|
||||
|
||||
if !hclsyntax.ValidIdentifier(mc.Name) {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid module instance name",
|
||||
Detail: badIdentifierDetail,
|
||||
Subject: &block.LabelRanges[0],
|
||||
})
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["source"]; exists {
|
||||
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &mc.SourceAddr)
|
||||
diags = append(diags, valDiags...)
|
||||
mc.SourceAddrRange = attr.Expr.Range()
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["version"]; exists {
|
||||
var versionDiags hcl.Diagnostics
|
||||
mc.Version, versionDiags = decodeVersionConstraint(attr)
|
||||
diags = append(diags, versionDiags...)
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["count"]; exists {
|
||||
mc.Count = attr.Expr
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["for_each"]; exists {
|
||||
mc.ForEach = attr.Expr
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["depends_on"]; exists {
|
||||
deps, depsDiags := decodeDependsOn(attr)
|
||||
diags = append(diags, depsDiags...)
|
||||
mc.DependsOn = append(mc.DependsOn, deps...)
|
||||
}
|
||||
|
||||
return mc, diags
|
||||
}
|
||||
|
||||
var moduleBlockSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "source",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "version",
|
||||
},
|
||||
{
|
||||
Name: "count",
|
||||
},
|
||||
{
|
||||
Name: "for_each",
|
||||
},
|
||||
{
|
||||
Name: "depends_on",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -0,0 +1,235 @@
|
||||
package configs
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
)
|
||||
|
||||
// LoadConfigFile reads the file at the given path and parses it as a config
|
||||
// file.
|
||||
//
|
||||
// If the file cannot be read -- for example, if it does not exist -- then
|
||||
// a nil *File will be returned along with error diagnostics. Callers may wish
|
||||
// to disregard the returned diagnostics in this case and instead generate
|
||||
// their own error message(s) with additional context.
|
||||
//
|
||||
// If the returned diagnostics has errors when a non-nil map is returned
|
||||
// then the map may be incomplete but should be valid enough for careful
|
||||
// static analysis.
|
||||
//
|
||||
// This method wraps LoadHCLFile, and so it inherits the syntax selection
|
||||
// behaviors documented for that method.
|
||||
func (p *Parser) LoadConfigFile(path string) (*File, hcl.Diagnostics) {
|
||||
body, diags := p.LoadHCLFile(path)
|
||||
if body == nil {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
file := &File{}
|
||||
|
||||
var reqDiags hcl.Diagnostics
|
||||
file.CoreVersionConstraints, reqDiags = sniffCoreVersionRequirements(body)
|
||||
diags = append(diags, reqDiags...)
|
||||
|
||||
content, contentDiags := body.Content(configFileSchema)
|
||||
diags = append(diags, contentDiags...)
|
||||
|
||||
for _, block := range content.Blocks {
|
||||
switch block.Type {
|
||||
|
||||
case "terraform":
|
||||
content, contentDiags := block.Body.Content(terraformBlockSchema)
|
||||
diags = append(diags, contentDiags...)
|
||||
|
||||
// We ignore the "terraform_version" attribute here because
|
||||
// sniffCoreVersionRequirements already dealt with that above.
|
||||
|
||||
for _, innerBlock := range content.Blocks {
|
||||
switch innerBlock.Type {
|
||||
|
||||
case "backend":
|
||||
backendCfg, cfgDiags := decodeBackendBlock(innerBlock)
|
||||
diags = append(diags, cfgDiags...)
|
||||
if backendCfg != nil {
|
||||
file.Backends = append(file.Backends, backendCfg)
|
||||
}
|
||||
|
||||
case "required_providers":
|
||||
reqs, reqsDiags := decodeRequiredProvidersBlock(innerBlock)
|
||||
diags = append(diags, reqsDiags...)
|
||||
file.ProviderRequirements = append(file.ProviderRequirements, reqs...)
|
||||
|
||||
default:
|
||||
// Should never happen because the above cases should be exhaustive
|
||||
// for all block type names in our schema.
|
||||
continue
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
case "provider":
|
||||
cfg, cfgDiags := decodeProviderBlock(block)
|
||||
diags = append(diags, cfgDiags...)
|
||||
if cfg != nil {
|
||||
file.ProviderConfigs = append(file.ProviderConfigs, cfg)
|
||||
}
|
||||
|
||||
case "variable":
|
||||
cfg, cfgDiags := decodeVariableBlock(block)
|
||||
diags = append(diags, cfgDiags...)
|
||||
if cfg != nil {
|
||||
file.Variables = append(file.Variables, cfg)
|
||||
}
|
||||
|
||||
case "locals":
|
||||
defs, defsDiags := decodeLocalsBlock(block)
|
||||
diags = append(diags, defsDiags...)
|
||||
file.Locals = append(file.Locals, defs...)
|
||||
|
||||
case "output":
|
||||
cfg, cfgDiags := decodeOutputBlock(block)
|
||||
diags = append(diags, cfgDiags...)
|
||||
if cfg != nil {
|
||||
file.Outputs = append(file.Outputs, cfg)
|
||||
}
|
||||
|
||||
case "module":
|
||||
cfg, cfgDiags := decodeModuleBlock(block)
|
||||
diags = append(diags, cfgDiags...)
|
||||
if cfg != nil {
|
||||
file.ModuleCalls = append(file.ModuleCalls, cfg)
|
||||
}
|
||||
|
||||
case "resource":
|
||||
cfg, cfgDiags := decodeResourceBlock(block)
|
||||
diags = append(diags, cfgDiags...)
|
||||
if cfg != nil {
|
||||
file.ManagedResources = append(file.ManagedResources, cfg)
|
||||
}
|
||||
|
||||
case "data":
|
||||
cfg, cfgDiags := decodeDataBlock(block)
|
||||
diags = append(diags, cfgDiags...)
|
||||
if cfg != nil {
|
||||
file.DataResources = append(file.DataResources, cfg)
|
||||
}
|
||||
|
||||
default:
|
||||
// Should never happen because the above cases should be exhaustive
|
||||
// for all block type names in our schema.
|
||||
continue
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return file, diags
|
||||
}
|
||||
|
||||
// sniffCoreVersionRequirements does minimal parsing of the given body for
|
||||
// "terraform" blocks with "required_version" attributes, returning the
|
||||
// requirements found.
|
||||
//
|
||||
// This is intended to maximize the chance that we'll be able to read the
|
||||
// requirements (syntax errors notwithstanding) even if the config file contains
|
||||
// constructs that might've been added in future Terraform versions
|
||||
//
|
||||
// This is a "best effort" sort of method which will return constraints it is
|
||||
// able to find, but may return no constraints at all if the given body is
|
||||
// so invalid that it cannot be decoded at all.
|
||||
func sniffCoreVersionRequirements(body hcl.Body) ([]VersionConstraint, hcl.Diagnostics) {
|
||||
rootContent, _, diags := body.PartialContent(configFileVersionSniffRootSchema)
|
||||
|
||||
var constraints []VersionConstraint
|
||||
|
||||
for _, block := range rootContent.Blocks {
|
||||
content, _, blockDiags := block.Body.PartialContent(configFileVersionSniffBlockSchema)
|
||||
diags = append(diags, blockDiags...)
|
||||
|
||||
attr, exists := content.Attributes["required_version"]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
constraint, constraintDiags := decodeVersionConstraint(attr)
|
||||
diags = append(diags, constraintDiags...)
|
||||
if !constraintDiags.HasErrors() {
|
||||
constraints = append(constraints, constraint)
|
||||
}
|
||||
}
|
||||
|
||||
return constraints, diags
|
||||
}
|
||||
|
||||
// configFileSchema is the schema for the top-level of a config file. We use
|
||||
// the low-level HCL API for this level so we can easily deal with each
|
||||
// block type separately with its own decoding logic.
|
||||
var configFileSchema = &hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "terraform",
|
||||
},
|
||||
{
|
||||
Type: "provider",
|
||||
LabelNames: []string{"name"},
|
||||
},
|
||||
{
|
||||
Type: "variable",
|
||||
LabelNames: []string{"name"},
|
||||
},
|
||||
{
|
||||
Type: "locals",
|
||||
},
|
||||
{
|
||||
Type: "output",
|
||||
LabelNames: []string{"name"},
|
||||
},
|
||||
{
|
||||
Type: "module",
|
||||
LabelNames: []string{"name"},
|
||||
},
|
||||
{
|
||||
Type: "resource",
|
||||
LabelNames: []string{"type", "name"},
|
||||
},
|
||||
{
|
||||
Type: "data",
|
||||
LabelNames: []string{"type", "name"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// terraformBlockSchema is the schema for a top-level "terraform" block in
|
||||
// a configuration file.
|
||||
var terraformBlockSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "required_version",
|
||||
},
|
||||
},
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "backend",
|
||||
LabelNames: []string{"type"},
|
||||
},
|
||||
{
|
||||
Type: "required_providers",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// configFileVersionSniffRootSchema is a schema for sniffCoreVersionRequirements
|
||||
var configFileVersionSniffRootSchema = &hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "terraform",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// configFileVersionSniffBlockSchema is a schema for sniffCoreVersionRequirements
|
||||
var configFileVersionSniffBlockSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "required_version",
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,185 @@
|
||||
package configs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl2/gohcl"
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
)
|
||||
|
||||
// Provisioner represents a "provisioner" block when used within a
|
||||
// "resource" block in a module or file.
|
||||
type Provisioner struct {
|
||||
Type string
|
||||
Config hcl.Body
|
||||
Connection *Connection
|
||||
When ProvisionerWhen
|
||||
OnFailure ProvisionerOnFailure
|
||||
|
||||
DeclRange hcl.Range
|
||||
TypeRange hcl.Range
|
||||
}
|
||||
|
||||
func decodeProvisionerBlock(block *hcl.Block) (*Provisioner, hcl.Diagnostics) {
|
||||
pv := &Provisioner{
|
||||
Type: block.Labels[0],
|
||||
TypeRange: block.LabelRanges[0],
|
||||
DeclRange: block.DefRange,
|
||||
When: ProvisionerWhenCreate,
|
||||
OnFailure: ProvisionerOnFailureFail,
|
||||
}
|
||||
|
||||
content, config, diags := block.Body.PartialContent(provisionerBlockSchema)
|
||||
pv.Config = config
|
||||
|
||||
if attr, exists := content.Attributes["when"]; exists {
|
||||
switch hcl.ExprAsKeyword(attr.Expr) {
|
||||
case "create":
|
||||
pv.When = ProvisionerWhenCreate
|
||||
case "destroy":
|
||||
pv.When = ProvisionerWhenDestroy
|
||||
default:
|
||||
if exprIsNativeQuotedString(attr.Expr) {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid \"when\" keyword",
|
||||
Detail: "The \"when\" argument keyword must not be given in quotes.",
|
||||
Subject: attr.Expr.Range().Ptr(),
|
||||
})
|
||||
} else {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid \"when\" keyword",
|
||||
Detail: "The \"when\" argument requires one of the following keywords: create or destroy.",
|
||||
Subject: attr.Expr.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["on_failure"]; exists {
|
||||
switch hcl.ExprAsKeyword(attr.Expr) {
|
||||
case "continue":
|
||||
pv.OnFailure = ProvisionerOnFailureContinue
|
||||
case "fail":
|
||||
pv.OnFailure = ProvisionerOnFailureFail
|
||||
default:
|
||||
if exprIsNativeQuotedString(attr.Expr) {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid \"on_failure\" keyword",
|
||||
Detail: "The \"on_failure\" argument keyword must not be given in quotes.",
|
||||
Subject: attr.Expr.Range().Ptr(),
|
||||
})
|
||||
} else {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid \"on_failure\" keyword",
|
||||
Detail: "The \"on_failure\" argument requires one of the following keywords: continue or fail.",
|
||||
Subject: attr.Expr.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var seenConnection *hcl.Block
|
||||
for _, block := range content.Blocks {
|
||||
switch block.Type {
|
||||
|
||||
case "connection":
|
||||
if seenConnection != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Duplicate connection block",
|
||||
Detail: fmt.Sprintf("This provisioner already has a connection block at %s.", seenConnection.DefRange),
|
||||
Subject: &block.DefRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
seenConnection = block
|
||||
|
||||
conn, connDiags := decodeConnectionBlock(block)
|
||||
diags = append(diags, connDiags...)
|
||||
pv.Connection = conn
|
||||
|
||||
default:
|
||||
// Should never happen because there are no other block types
|
||||
// declared in our schema.
|
||||
}
|
||||
}
|
||||
|
||||
return pv, diags
|
||||
}
|
||||
|
||||
// Connection represents a "connection" block when used within either a
|
||||
// "resource" or "provisioner" block in a module or file.
|
||||
type Connection struct {
|
||||
Type string
|
||||
Config hcl.Body
|
||||
|
||||
DeclRange hcl.Range
|
||||
TypeRange *hcl.Range // nil if type is not set
|
||||
}
|
||||
|
||||
func decodeConnectionBlock(block *hcl.Block) (*Connection, hcl.Diagnostics) {
|
||||
content, config, diags := block.Body.PartialContent(&hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "type",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
conn := &Connection{
|
||||
Type: "ssh",
|
||||
Config: config,
|
||||
DeclRange: block.DefRange,
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["type"]; exists {
|
||||
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &conn.Type)
|
||||
diags = append(diags, valDiags...)
|
||||
conn.TypeRange = attr.Expr.Range().Ptr()
|
||||
}
|
||||
|
||||
return conn, diags
|
||||
}
|
||||
|
||||
// ProvisionerWhen is an enum for valid values for when to run provisioners.
|
||||
type ProvisionerWhen int
|
||||
|
||||
//go:generate stringer -type ProvisionerWhen
|
||||
|
||||
const (
|
||||
ProvisionerWhenInvalid ProvisionerWhen = iota
|
||||
ProvisionerWhenCreate
|
||||
ProvisionerWhenDestroy
|
||||
)
|
||||
|
||||
// ProvisionerOnFailure is an enum for valid values for on_failure options
|
||||
// for provisioners.
|
||||
type ProvisionerOnFailure int
|
||||
|
||||
//go:generate stringer -type ProvisionerOnFailure
|
||||
|
||||
const (
|
||||
ProvisionerOnFailureInvalid ProvisionerOnFailure = iota
|
||||
ProvisionerOnFailureContinue
|
||||
ProvisionerOnFailureFail
|
||||
)
|
||||
|
||||
var provisionerBlockSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "when",
|
||||
},
|
||||
{
|
||||
Name: "on_failure",
|
||||
},
|
||||
},
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "connection",
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package configs
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
||||
)
|
||||
|
||||
// exprIsNativeQuotedString determines whether the given expression looks like
|
||||
// it's a quoted string in the HCL native syntax.
|
||||
//
|
||||
// This should be used sparingly only for situations where our legacy HCL
|
||||
// decoding would've expected a keyword or reference in quotes but our new
|
||||
// decoding expects the keyword or reference to be provided directly as
|
||||
// an identifier-based expression.
|
||||
func exprIsNativeQuotedString(expr hcl.Expression) bool {
|
||||
_, ok := expr.(*hclsyntax.TemplateExpr)
|
||||
return ok
|
||||
}
|
||||
Loading…
Reference in new issue