configs: Parser.LoadConfigFile

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.
This commit is contained in:
Martin Atkins 2018-02-02 17:22:25 -08:00
parent b865d62bb8
commit e15ec486bf
11 changed files with 1164 additions and 40 deletions

View File

@ -10,5 +10,15 @@ type Backend struct {
Type string Type string
Config hcl.Body Config hcl.Body
TypeRange hcl.Range
DeclRange hcl.Range DeclRange hcl.Range
} }
func decodeBackendBlock(block *hcl.Block) (*Backend, hcl.Diagnostics) {
return &Backend{
Type: block.Labels[0],
TypeRange: block.LabelRanges[0],
Config: block.Body,
DeclRange: block.DefRange,
}, nil
}

37
configs/depends_on.go Normal file
View File

@ -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
}

View File

@ -49,7 +49,7 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
// analysis of individual elements, but must be built into a Module to detect // analysis of individual elements, but must be built into a Module to detect
// duplicate declarations. // duplicate declarations.
type File struct { type File struct {
CoreVersionConstraints []*VersionConstraint CoreVersionConstraints []VersionConstraint
Backends []*Backend Backends []*Backend
ProviderConfigs []*Provider ProviderConfigs []*Provider

View File

@ -1,18 +1,94 @@
package configs package configs
import ( import (
"github.com/hashicorp/hcl2/gohcl"
"github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
) )
// ModuleCall represents a "module" block in a module or file. // ModuleCall represents a "module" block in a module or file.
type ModuleCall struct { type ModuleCall struct {
Source string Name string
SourceRange hcl.Range
SourceAddr string
SourceAddrRange hcl.Range
Config hcl.Body
Version VersionConstraint Version VersionConstraint
Count hcl.Expression Count hcl.Expression
ForEach hcl.Expression ForEach hcl.Expression
DependsOn []hcl.Traversal
DeclRange hcl.Range 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",
},
},
}

View File

@ -1,10 +1,17 @@
package configs package configs
import ( import (
"fmt"
"github.com/hashicorp/hcl2/gohcl"
"github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
) )
// A consistent detail message for all "not a valid identifier" diagnostics.
const badIdentifierDetail = "A name must start with a letter and may contain only letters, digits, underscores, and dashes."
// Variable represents a "variable" block in a module or file. // Variable represents a "variable" block in a module or file.
type Variable struct { type Variable struct {
Name string Name string
@ -15,6 +22,82 @@ type Variable struct {
DeclRange hcl.Range DeclRange hcl.Range
} }
func decodeVariableBlock(block *hcl.Block) (*Variable, hcl.Diagnostics) {
v := &Variable{
Name: block.Labels[0],
DeclRange: block.DefRange,
}
content, diags := block.Body.Content(variableBlockSchema)
if !hclsyntax.ValidIdentifier(v.Name) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid variable name",
Detail: badIdentifierDetail,
Subject: &block.LabelRanges[0],
})
}
// Don't allow declaration of variables that would conflict with the
// reserved attribute and block type names in a "module" block, since
// these won't be usable for child modules.
for _, attr := range moduleBlockSchema.Attributes {
if attr.Name == v.Name {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid variable name",
Detail: fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", attr.Name),
Subject: &block.LabelRanges[0],
})
}
}
if attr, exists := content.Attributes["description"]; exists {
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Description)
diags = append(diags, valDiags...)
}
if attr, exists := content.Attributes["default"]; exists {
val, valDiags := attr.Expr.Value(nil)
diags = append(diags, valDiags...)
v.Default = val
}
if attr, exists := content.Attributes["type"]; exists {
switch hcl.ExprAsKeyword(attr.Expr) {
case "string":
v.TypeHint = TypeHintString
case "list":
v.TypeHint = TypeHintList
case "map":
v.TypeHint = TypeHintMap
default:
// In our legacy configuration format these keywords would've been
// provided as quoted strings, so we'll generate a special error
// message for that to help those who find outdated examples and
// would otherwise be confused.
if exprIsNativeQuotedString(attr.Expr) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid variable type hint",
Detail: "The type hint keyword must not be given in quotes.",
Subject: attr.Expr.Range().Ptr(),
})
} else {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid variable type hint",
Detail: "The type argument requires one of the following keywords: string, list, or map.",
Subject: attr.Expr.Range().Ptr(),
})
}
}
}
return v, diags
}
// Output represents an "output" block in a module or file. // Output represents an "output" block in a module or file.
type Output struct { type Output struct {
Name string Name string
@ -26,6 +109,46 @@ type Output struct {
DeclRange hcl.Range DeclRange hcl.Range
} }
func decodeOutputBlock(block *hcl.Block) (*Output, hcl.Diagnostics) {
o := &Output{
Name: block.Labels[0],
DeclRange: block.DefRange,
}
content, diags := block.Body.Content(outputBlockSchema)
if !hclsyntax.ValidIdentifier(o.Name) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid output name",
Detail: badIdentifierDetail,
Subject: &block.LabelRanges[0],
})
}
if attr, exists := content.Attributes["description"]; exists {
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Description)
diags = append(diags, valDiags...)
}
if attr, exists := content.Attributes["value"]; exists {
o.Expr = attr.Expr
}
if attr, exists := content.Attributes["sensitive"]; exists {
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Sensitive)
diags = append(diags, valDiags...)
}
if attr, exists := content.Attributes["depends_on"]; exists {
deps, depsDiags := decodeDependsOn(attr)
diags = append(diags, depsDiags...)
o.DependsOn = append(o.DependsOn, deps...)
}
return o, diags
}
// Local represents a single entry from a "locals" block in a module or file. // Local represents a single entry from a "locals" block in a module or file.
// The "locals" block itself is not represented, because it serves only to // The "locals" block itself is not represented, because it serves only to
// provide context for us to interpret its contents. // provide context for us to interpret its contents.
@ -35,3 +158,61 @@ type Local struct {
DeclRange hcl.Range DeclRange hcl.Range
} }
func decodeLocalsBlock(block *hcl.Block) ([]*Local, hcl.Diagnostics) {
attrs, diags := block.Body.JustAttributes()
if len(attrs) == 0 {
return nil, diags
}
locals := make([]*Local, 0, len(attrs))
for name, attr := range attrs {
if !hclsyntax.ValidIdentifier(name) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid local value name",
Detail: badIdentifierDetail,
Subject: &attr.NameRange,
})
}
locals = append(locals, &Local{
Name: name,
Expr: attr.Expr,
DeclRange: attr.Range,
})
}
return locals, diags
}
var variableBlockSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "description",
},
{
Name: "default",
},
{
Name: "type",
},
},
}
var outputBlockSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "description",
},
{
Name: "value",
Required: true,
},
{
Name: "depends_on",
},
{
Name: "sensitive",
},
},
}

235
configs/parser_config.go Normal file
View File

@ -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",
},
},
}

View File

@ -1,7 +1,11 @@
package configs package configs
import ( import (
"fmt"
"github.com/hashicorp/hcl2/gohcl"
"github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
) )
// Provider represents a "provider" block in a module or file. A provider // Provider represents a "provider" block in a module or file. A provider
@ -9,8 +13,9 @@ import (
// configurations for each actual provider. // configurations for each actual provider.
type Provider struct { type Provider struct {
Name string Name string
NameRange hcl.Range
Alias string Alias string
AliasRange hcl.Range AliasRange *hcl.Range // nil if no alias set
Version VersionConstraint Version VersionConstraint
@ -19,6 +24,39 @@ type Provider struct {
DeclRange hcl.Range DeclRange hcl.Range
} }
func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) {
content, config, diags := block.Body.PartialContent(providerBlockSchema)
provider := &Provider{
Name: block.Labels[0],
NameRange: block.LabelRanges[0],
Config: config,
DeclRange: block.DefRange,
}
if attr, exists := content.Attributes["alias"]; exists {
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &provider.Alias)
diags = append(diags, valDiags...)
provider.AliasRange = attr.Expr.Range().Ptr()
if !hclsyntax.ValidIdentifier(provider.Alias) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider configuration alias",
Detail: fmt.Sprintf("An alias must be a valid name. %s", badIdentifierDetail),
})
}
}
if attr, exists := content.Attributes["version"]; exists {
var versionDiags hcl.Diagnostics
provider.Version, versionDiags = decodeVersionConstraint(attr)
diags = append(diags, versionDiags...)
}
return provider, diags
}
// ProviderRequirement represents a declaration of a dependency on a particular // ProviderRequirement represents a declaration of a dependency on a particular
// provider version without actually configuring that provider. This is used in // provider version without actually configuring that provider. This is used in
// child modules that expect a provider to be passed in from their parent. // child modules that expect a provider to be passed in from their parent.
@ -26,3 +64,30 @@ type ProviderRequirement struct {
Name string Name string
Requirement VersionConstraint Requirement VersionConstraint
} }
func decodeRequiredProvidersBlock(block *hcl.Block) ([]*ProviderRequirement, hcl.Diagnostics) {
attrs, diags := block.Body.JustAttributes()
var reqs []*ProviderRequirement
for name, attr := range attrs {
req, reqDiags := decodeVersionConstraint(attr)
diags = append(diags, reqDiags...)
if !diags.HasErrors() {
reqs = append(reqs, &ProviderRequirement{
Name: name,
Requirement: req,
})
}
}
return reqs, diags
}
var providerBlockSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "alias",
},
{
Name: "version",
},
},
}

185
configs/provisioner.go Normal file
View File

@ -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",
},
},
}

View File

@ -1,7 +1,11 @@
package configs package configs
import ( import (
"fmt"
"github.com/hashicorp/hcl2/gohcl"
"github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
) )
// ManagedResource represents a "resource" block in a module or file. // ManagedResource represents a "resource" block in a module or file.
@ -12,7 +16,7 @@ type ManagedResource struct {
Count hcl.Expression Count hcl.Expression
ForEach hcl.Expression ForEach hcl.Expression
ProviderConfigAddr hcl.Traversal ProviderConfigRef *ProviderConfigRef
DependsOn []hcl.Traversal DependsOn []hcl.Traversal
@ -24,6 +28,130 @@ type ManagedResource struct {
IgnoreChanges []hcl.Traversal IgnoreChanges []hcl.Traversal
DeclRange hcl.Range DeclRange hcl.Range
TypeRange hcl.Range
}
func decodeResourceBlock(block *hcl.Block) (*ManagedResource, hcl.Diagnostics) {
r := &ManagedResource{
Type: block.Labels[0],
Name: block.Labels[1],
DeclRange: block.DefRange,
TypeRange: block.LabelRanges[0],
}
content, remain, diags := block.Body.PartialContent(resourceBlockSchema)
r.Config = remain
if !hclsyntax.ValidIdentifier(r.Type) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid resource type name",
Detail: badIdentifierDetail,
Subject: &block.LabelRanges[0],
})
}
if !hclsyntax.ValidIdentifier(r.Name) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid resource name",
Detail: badIdentifierDetail,
Subject: &block.LabelRanges[0],
})
}
if attr, exists := content.Attributes["count"]; exists {
r.Count = attr.Expr
}
if attr, exists := content.Attributes["for_each"]; exists {
r.Count = attr.Expr
}
if attr, exists := content.Attributes["provider"]; exists {
var providerDiags hcl.Diagnostics
r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr)
diags = append(diags, providerDiags...)
}
if attr, exists := content.Attributes["depends_on"]; exists {
deps, depsDiags := decodeDependsOn(attr)
diags = append(diags, depsDiags...)
r.DependsOn = append(r.DependsOn, deps...)
}
var seenLifecycle *hcl.Block
var seenConnection *hcl.Block
for _, block := range content.Blocks {
switch block.Type {
case "lifecycle":
if seenLifecycle != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate lifecycle block",
Detail: fmt.Sprintf("This resource already has a lifecycle block at %s.", seenLifecycle.DefRange),
Subject: &block.DefRange,
})
continue
}
seenLifecycle = block
lcContent, lcDiags := block.Body.Content(resourceLifecycleBlockSchema)
diags = append(diags, lcDiags...)
if attr, exists := lcContent.Attributes["create_before_destroy"]; exists {
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.CreateBeforeDestroy)
diags = append(diags, valDiags...)
}
if attr, exists := lcContent.Attributes["prevent_destroy"]; exists {
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.PreventDestroy)
diags = append(diags, valDiags...)
}
if attr, exists := lcContent.Attributes["ignore_changes"]; exists {
exprs, listDiags := hcl.ExprList(attr.Expr)
diags = append(diags, listDiags...)
for _, expr := range exprs {
traversal, travDiags := hcl.RelTraversalForExpr(expr)
diags = append(diags, travDiags...)
if len(traversal) != 0 {
r.IgnoreChanges = append(r.IgnoreChanges, traversal)
}
}
}
case "connection":
if seenConnection != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate connection block",
Detail: fmt.Sprintf("This resource already has a connection block at %s.", seenConnection.DefRange),
Subject: &block.DefRange,
})
continue
}
seenConnection = block
conn, connDiags := decodeConnectionBlock(block)
diags = append(diags, connDiags...)
r.Connection = conn
case "provisioner":
pv, pvDiags := decodeProvisionerBlock(block)
diags = append(diags, pvDiags...)
if pv != nil {
r.Provisioners = append(r.Provisioners, pv)
}
default:
// Should never happen, because the above cases should always be
// exhaustive for all the types specified in our schema.
continue
}
}
return r, diags
} }
// DataResource represents a "data" block in a module or file. // DataResource represents a "data" block in a module or file.
@ -34,53 +162,196 @@ type DataResource struct {
Count hcl.Expression Count hcl.Expression
ForEach hcl.Expression ForEach hcl.Expression
ProviderConfigAddr hcl.Traversal ProviderConfigRef *ProviderConfigRef
DependsOn []hcl.Traversal DependsOn []hcl.Traversal
DeclRange hcl.Range DeclRange hcl.Range
TypeRange hcl.Range
} }
// Provisioner represents a "provisioner" block when used within a func decodeDataBlock(block *hcl.Block) (*DataResource, hcl.Diagnostics) {
// "resource" block in a module or file. r := &DataResource{
type Provisioner struct { Type: block.Labels[0],
Type string Name: block.Labels[1],
Config hcl.Body DeclRange: block.DefRange,
Connection *Connection TypeRange: block.LabelRanges[0],
When ProvisionerWhen }
OnFailure ProvisionerOnFailure
DeclRange hcl.Range content, remain, diags := block.Body.PartialContent(dataBlockSchema)
r.Config = remain
if !hclsyntax.ValidIdentifier(r.Type) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid data source name",
Detail: badIdentifierDetail,
Subject: &block.LabelRanges[0],
})
}
if !hclsyntax.ValidIdentifier(r.Name) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid data resource name",
Detail: badIdentifierDetail,
Subject: &block.LabelRanges[0],
})
}
if attr, exists := content.Attributes["count"]; exists {
r.Count = attr.Expr
}
if attr, exists := content.Attributes["for_each"]; exists {
r.Count = attr.Expr
}
if attr, exists := content.Attributes["provider"]; exists {
var providerDiags hcl.Diagnostics
r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr)
diags = append(diags, providerDiags...)
}
if attr, exists := content.Attributes["depends_on"]; exists {
deps, depsDiags := decodeDependsOn(attr)
diags = append(diags, depsDiags...)
r.DependsOn = append(r.DependsOn, deps...)
}
for _, block := range content.Blocks {
// Our schema only allows for "lifecycle" blocks, so we can assume
// that this is all we will see here. We don't have any lifecycle
// attributes for data resources currently, so we'll just produce
// an error.
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unsupported lifecycle block",
Detail: "Data resources do not have lifecycle settings, so a lifecycle block is not allowed.",
Subject: &block.DefRange,
})
break
}
return r, diags
} }
// Connection represents a "connection" block when used within either a type ProviderConfigRef struct {
// "resource" or "provisioner" block in a module or file. Name string
type Connection struct { NameRange hcl.Range
Type string Alias string
Config hcl.Body AliasRange *hcl.Range // nil if alias not set
DeclRange hcl.Range
} }
// ProvisionerWhen is an enum for valid values for when to run provisioners. func decodeProviderConfigRef(attr *hcl.Attribute) (*ProviderConfigRef, hcl.Diagnostics) {
type ProvisionerWhen int var diags hcl.Diagnostics
traversal, travDiags := hcl.AbsTraversalForExpr(attr.Expr)
//go:generate stringer -type ProvisionerWhen // AbsTraversalForExpr produces only generic errors, so we'll discard
// the errors given and produce our own with extra context. If we didn't
// get any errors then we might still have warnings, though.
if !travDiags.HasErrors() {
diags = append(diags, travDiags...)
}
const ( if len(traversal) < 1 && len(traversal) > 2 {
ProvisionerWhenInvalid ProvisionerWhen = iota // A provider reference was given as a string literal in the legacy
ProvisionerWhenCreate // configuration language and there are lots of examples out there
ProvisionerWhenDestroy // 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(attr.Expr) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider configuration reference",
Detail: "A provider configuration reference must not be given in quotes.",
Subject: attr.Expr.Range().Ptr(),
})
return nil, diags
}
// ProvisionerOnFailure is an enum for valid values for on_failure options diags = append(diags, &hcl.Diagnostic{
// for provisioners. Severity: hcl.DiagError,
type ProvisionerOnFailure int Summary: "Invalid provider configuration reference",
Detail: fmt.Sprintf("The %s argument requires a provider type name, optionally followed by a period and then a configuration alias.", attr.Name),
Subject: attr.Expr.Range().Ptr(),
})
return nil, diags
}
//go:generate stringer -type ProvisionerOnFailure ret := &ProviderConfigRef{
Name: traversal.RootName(),
NameRange: traversal[0].SourceRange(),
}
const ( if len(traversal) > 1 {
ProvisionerOnFailureInvalid ProvisionerOnFailure = iota aliasStep, ok := traversal[1].(hcl.TraverseAttr)
ProvisionerOnFailureContinue if !ok {
ProvisionerOnFailureFail diags = append(diags, &hcl.Diagnostic{
) Severity: hcl.DiagError,
Summary: "Invalid provider configuration reference",
Detail: "Provider name must either stand alone or be followed by a period and then a configuration alias.",
Subject: traversal[1].SourceRange().Ptr(),
})
return ret, diags
}
ret.Alias = aliasStep.Name
ret.AliasRange = aliasStep.SourceRange().Ptr()
}
return ret, diags
}
var commonResourceAttributes = []hcl.AttributeSchema{
{
Name: "count",
},
{
Name: "for_each",
},
{
Name: "provider",
},
{
Name: "depends_on",
},
}
var resourceBlockSchema = &hcl.BodySchema{
Attributes: commonResourceAttributes,
Blocks: []hcl.BlockHeaderSchema{
{
Type: "lifecycle",
},
{
Type: "connection",
},
{
Type: "provisioner",
LabelNames: []string{"type"},
},
},
}
var dataBlockSchema = &hcl.BodySchema{
Attributes: commonResourceAttributes,
Blocks: []hcl.BlockHeaderSchema{
{
Type: "lifecycle",
},
},
}
var resourceLifecycleBlockSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "create_before_destroy",
},
{
Name: "prevent_destroy",
},
{
Name: "ignore_changes",
},
},
}

18
configs/util.go Normal file
View File

@ -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
}

View File

@ -1,8 +1,12 @@
package configs package configs
import ( import (
"fmt"
version "github.com/hashicorp/go-version" version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
) )
// VersionConstraint represents a version constraint on some resource // VersionConstraint represents a version constraint on some resource
@ -10,6 +14,48 @@ import (
// a source range so that a helpful diagnostic can be printed in the event // a source range so that a helpful diagnostic can be printed in the event
// that a particular constraint does not match. // that a particular constraint does not match.
type VersionConstraint struct { type VersionConstraint struct {
Required []version.Constraints Required version.Constraints
DeclRange hcl.Range DeclRange hcl.Range
} }
func decodeVersionConstraint(attr *hcl.Attribute) (VersionConstraint, hcl.Diagnostics) {
ret := VersionConstraint{
DeclRange: attr.Range,
}
val, diags := attr.Expr.Value(nil)
var err error
val, err = convert.Convert(val, cty.String)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid version constraint",
Detail: fmt.Sprintf("A string value is required for %s.", attr.Name),
Subject: attr.Expr.Range().Ptr(),
})
return ret, diags
}
if val.IsNull() {
// A null version constraint is strange, but we'll just treat it
// like an empty constraint set.
return ret, diags
}
constraintStr := val.AsString()
constraints, err := version.NewConstraint(constraintStr)
if err != nil {
// NewConstraint doesn't return user-friendly errors, so we'll just
// ignore the provided error and produce our own generic one.
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid version constraint",
Detail: "This string does not use correct version constraint syntax.", // Not very actionable :(
Subject: attr.Expr.Range().Ptr(),
})
return ret, diags
}
ret.Required = constraints
return ret, diags
}