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
|
||||
Config hcl.Body
|
||||
|
||||
TypeRange 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
|
||||
// duplicate declarations.
|
||||
type File struct {
|
||||
CoreVersionConstraints []*VersionConstraint
|
||||
CoreVersionConstraints []VersionConstraint
|
||||
|
||||
Backends []*Backend
|
||||
ProviderConfigs []*Provider
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
package configs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl2/gohcl"
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
||||
"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.
|
||||
type Variable struct {
|
||||
Name string
|
||||
|
@ -15,6 +22,82 @@ type Variable struct {
|
|||
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.
|
||||
type Output struct {
|
||||
Name string
|
||||
|
@ -26,6 +109,46 @@ type Output struct {
|
|||
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.
|
||||
// The "locals" block itself is not represented, because it serves only to
|
||||
// provide context for us to interpret its contents.
|
||||
|
@ -35,3 +158,61 @@ type Local struct {
|
|||
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl2/gohcl"
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
||||
)
|
||||
|
||||
// Provider represents a "provider" block in a module or file. A provider
|
||||
|
@ -9,8 +13,9 @@ import (
|
|||
// configurations for each actual provider.
|
||||
type Provider struct {
|
||||
Name string
|
||||
NameRange hcl.Range
|
||||
Alias string
|
||||
AliasRange hcl.Range
|
||||
AliasRange *hcl.Range // nil if no alias set
|
||||
|
||||
Version VersionConstraint
|
||||
|
||||
|
@ -19,6 +24,39 @@ type Provider struct {
|
|||
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
|
||||
// provider version without actually configuring that provider. This is used in
|
||||
// child modules that expect a provider to be passed in from their parent.
|
||||
|
@ -26,3 +64,30 @@ type ProviderRequirement struct {
|
|||
Name string
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl2/gohcl"
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
||||
)
|
||||
|
||||
// ManagedResource represents a "resource" block in a module or file.
|
||||
|
@ -12,7 +16,7 @@ type ManagedResource struct {
|
|||
Count hcl.Expression
|
||||
ForEach hcl.Expression
|
||||
|
||||
ProviderConfigAddr hcl.Traversal
|
||||
ProviderConfigRef *ProviderConfigRef
|
||||
|
||||
DependsOn []hcl.Traversal
|
||||
|
||||
|
@ -24,6 +28,130 @@ type ManagedResource struct {
|
|||
IgnoreChanges []hcl.Traversal
|
||||
|
||||
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.
|
||||
|
@ -34,53 +162,196 @@ type DataResource struct {
|
|||
Count hcl.Expression
|
||||
ForEach hcl.Expression
|
||||
|
||||
ProviderConfigAddr hcl.Traversal
|
||||
ProviderConfigRef *ProviderConfigRef
|
||||
|
||||
DependsOn []hcl.Traversal
|
||||
|
||||
DeclRange hcl.Range
|
||||
TypeRange hcl.Range
|
||||
}
|
||||
|
||||
// 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
|
||||
func decodeDataBlock(block *hcl.Block) (*DataResource, hcl.Diagnostics) {
|
||||
r := &DataResource{
|
||||
Type: block.Labels[0],
|
||||
Name: block.Labels[1],
|
||||
DeclRange: block.DefRange,
|
||||
TypeRange: block.LabelRanges[0],
|
||||
}
|
||||
|
||||
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
|
||||
// "resource" or "provisioner" block in a module or file.
|
||||
type Connection struct {
|
||||
Type string
|
||||
Config hcl.Body
|
||||
|
||||
DeclRange hcl.Range
|
||||
type ProviderConfigRef struct {
|
||||
Name string
|
||||
NameRange hcl.Range
|
||||
Alias string
|
||||
AliasRange *hcl.Range // nil if alias not set
|
||||
}
|
||||
|
||||
// ProvisionerWhen is an enum for valid values for when to run provisioners.
|
||||
type ProvisionerWhen int
|
||||
func decodeProviderConfigRef(attr *hcl.Attribute) (*ProviderConfigRef, hcl.Diagnostics) {
|
||||
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 (
|
||||
ProvisionerWhenInvalid ProvisionerWhen = iota
|
||||
ProvisionerWhenCreate
|
||||
ProvisionerWhenDestroy
|
||||
)
|
||||
if len(traversal) < 1 && len(traversal) > 2 {
|
||||
// A provider 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(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
|
||||
// for provisioners.
|
||||
type ProvisionerOnFailure int
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
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 (
|
||||
ProvisionerOnFailureInvalid ProvisionerOnFailure = iota
|
||||
ProvisionerOnFailureContinue
|
||||
ProvisionerOnFailureFail
|
||||
)
|
||||
if len(traversal) > 1 {
|
||||
aliasStep, ok := traversal[1].(hcl.TraverseAttr)
|
||||
if !ok {
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
version "github.com/hashicorp/go-version"
|
||||
"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
|
||||
|
@ -10,6 +14,48 @@ import (
|
|||
// a source range so that a helpful diagnostic can be printed in the event
|
||||
// that a particular constraint does not match.
|
||||
type VersionConstraint struct {
|
||||
Required []version.Constraints
|
||||
Required version.Constraints
|
||||
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