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:
parent
b865d62bb8
commit
e15ec486bf
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
|
@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
|
@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue