a21a3f1908
When we did the automatic rewriting from the hashicorp/hcl2 import paths to hashicorp/hcl/v2 import paths, our automatic rewrite script did not run "go fmt" afterwards and so left some imports misordered as a result of the slightly-differently-shaped directory structure in the new repository.
646 lines
16 KiB
Go
646 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/ext/userfunc"
|
|
"github.com/hashicorp/hcl/v2/gohcl"
|
|
"github.com/hashicorp/hcl/v2/hcldec"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
)
|
|
|
|
type specFileContent struct {
|
|
Variables map[string]cty.Value
|
|
Functions map[string]function.Function
|
|
RootSpec hcldec.Spec
|
|
}
|
|
|
|
var specCtx = &hcl.EvalContext{
|
|
Functions: specFuncs,
|
|
}
|
|
|
|
func loadSpecFile(filename string) (specFileContent, hcl.Diagnostics) {
|
|
file, diags := parser.ParseHCLFile(filename)
|
|
if diags.HasErrors() {
|
|
return specFileContent{RootSpec: errSpec}, diags
|
|
}
|
|
|
|
vars, funcs, specBody, declDiags := decodeSpecDecls(file.Body)
|
|
diags = append(diags, declDiags...)
|
|
|
|
spec, specDiags := decodeSpecRoot(specBody)
|
|
diags = append(diags, specDiags...)
|
|
|
|
return specFileContent{
|
|
Variables: vars,
|
|
Functions: funcs,
|
|
RootSpec: spec,
|
|
}, diags
|
|
}
|
|
|
|
func decodeSpecDecls(body hcl.Body) (map[string]cty.Value, map[string]function.Function, hcl.Body, hcl.Diagnostics) {
|
|
funcs, body, diags := userfunc.DecodeUserFunctions(body, "function", func() *hcl.EvalContext {
|
|
return specCtx
|
|
})
|
|
|
|
content, body, moreDiags := body.PartialContent(&hcl.BodySchema{
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{
|
|
Type: "variables",
|
|
},
|
|
},
|
|
})
|
|
diags = append(diags, moreDiags...)
|
|
|
|
vars := make(map[string]cty.Value)
|
|
for _, block := range content.Blocks {
|
|
// We only have one block type in our schema, so we can assume all
|
|
// blocks are of that type.
|
|
attrs, moreDiags := block.Body.JustAttributes()
|
|
diags = append(diags, moreDiags...)
|
|
|
|
for name, attr := range attrs {
|
|
val, moreDiags := attr.Expr.Value(specCtx)
|
|
diags = append(diags, moreDiags...)
|
|
vars[name] = val
|
|
}
|
|
}
|
|
|
|
return vars, funcs, body, diags
|
|
}
|
|
|
|
func decodeSpecRoot(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
|
|
content, diags := body.Content(specSchemaUnlabelled)
|
|
|
|
if len(content.Blocks) == 0 {
|
|
if diags.HasErrors() {
|
|
// If we already have errors then they probably explain
|
|
// why we have no blocks, so we'll skip our additional
|
|
// error message added below.
|
|
return errSpec, diags
|
|
}
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing spec block",
|
|
Detail: "A spec file must have exactly one root block specifying how to map to a JSON value.",
|
|
Subject: body.MissingItemRange().Ptr(),
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
if len(content.Blocks) > 1 {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Extraneous spec block",
|
|
Detail: "A spec file must have exactly one root block specifying how to map to a JSON value.",
|
|
Subject: &content.Blocks[1].DefRange,
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
spec, specDiags := decodeSpecBlock(content.Blocks[0])
|
|
diags = append(diags, specDiags...)
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeSpecBlock(block *hcl.Block) (hcldec.Spec, hcl.Diagnostics) {
|
|
var impliedName string
|
|
if len(block.Labels) > 0 {
|
|
impliedName = block.Labels[0]
|
|
}
|
|
|
|
switch block.Type {
|
|
|
|
case "object":
|
|
return decodeObjectSpec(block.Body)
|
|
|
|
case "array":
|
|
return decodeArraySpec(block.Body)
|
|
|
|
case "attr":
|
|
return decodeAttrSpec(block.Body, impliedName)
|
|
|
|
case "block":
|
|
return decodeBlockSpec(block.Body, impliedName)
|
|
|
|
case "block_list":
|
|
return decodeBlockListSpec(block.Body, impliedName)
|
|
|
|
case "block_set":
|
|
return decodeBlockSetSpec(block.Body, impliedName)
|
|
|
|
case "block_map":
|
|
return decodeBlockMapSpec(block.Body, impliedName)
|
|
|
|
case "block_attrs":
|
|
return decodeBlockAttrsSpec(block.Body, impliedName)
|
|
|
|
case "default":
|
|
return decodeDefaultSpec(block.Body)
|
|
|
|
case "transform":
|
|
return decodeTransformSpec(block.Body)
|
|
|
|
case "literal":
|
|
return decodeLiteralSpec(block.Body)
|
|
|
|
default:
|
|
// Should never happen, because the above cases should be exhaustive
|
|
// for our schema.
|
|
var diags hcl.Diagnostics
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid spec block",
|
|
Detail: fmt.Sprintf("Blocks of type %q are not expected here.", block.Type),
|
|
Subject: &block.TypeRange,
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
}
|
|
|
|
func decodeObjectSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
|
|
content, diags := body.Content(specSchemaLabelled)
|
|
|
|
spec := make(hcldec.ObjectSpec)
|
|
for _, block := range content.Blocks {
|
|
propSpec, propDiags := decodeSpecBlock(block)
|
|
diags = append(diags, propDiags...)
|
|
spec[block.Labels[0]] = propSpec
|
|
}
|
|
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeArraySpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
|
|
content, diags := body.Content(specSchemaUnlabelled)
|
|
|
|
spec := make(hcldec.TupleSpec, 0, len(content.Blocks))
|
|
for _, block := range content.Blocks {
|
|
elemSpec, elemDiags := decodeSpecBlock(block)
|
|
diags = append(diags, elemDiags...)
|
|
spec = append(spec, elemSpec)
|
|
}
|
|
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeAttrSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
|
|
type content struct {
|
|
Name *string `hcl:"name"`
|
|
Type hcl.Expression `hcl:"type"`
|
|
Required *bool `hcl:"required"`
|
|
}
|
|
|
|
var args content
|
|
diags := gohcl.DecodeBody(body, nil, &args)
|
|
if diags.HasErrors() {
|
|
return errSpec, diags
|
|
}
|
|
|
|
spec := &hcldec.AttrSpec{
|
|
Name: impliedName,
|
|
}
|
|
|
|
if args.Required != nil {
|
|
spec.Required = *args.Required
|
|
}
|
|
if args.Name != nil {
|
|
spec.Name = *args.Name
|
|
}
|
|
|
|
var typeDiags hcl.Diagnostics
|
|
spec.Type, typeDiags = evalTypeExpr(args.Type)
|
|
diags = append(diags, typeDiags...)
|
|
|
|
if spec.Name == "" {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing name in attribute spec",
|
|
Detail: "The name attribute is required, to specify the attribute name that is expected in an input HCL file.",
|
|
Subject: body.MissingItemRange().Ptr(),
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeBlockSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
|
|
type content struct {
|
|
TypeName *string `hcl:"block_type"`
|
|
Required *bool `hcl:"required"`
|
|
Nested hcl.Body `hcl:",remain"`
|
|
}
|
|
|
|
var args content
|
|
diags := gohcl.DecodeBody(body, nil, &args)
|
|
if diags.HasErrors() {
|
|
return errSpec, diags
|
|
}
|
|
|
|
spec := &hcldec.BlockSpec{
|
|
TypeName: impliedName,
|
|
}
|
|
|
|
if args.Required != nil {
|
|
spec.Required = *args.Required
|
|
}
|
|
if args.TypeName != nil {
|
|
spec.TypeName = *args.TypeName
|
|
}
|
|
|
|
nested, nestedDiags := decodeBlockNestedSpec(args.Nested)
|
|
diags = append(diags, nestedDiags...)
|
|
spec.Nested = nested
|
|
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeBlockListSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
|
|
type content struct {
|
|
TypeName *string `hcl:"block_type"`
|
|
MinItems *int `hcl:"min_items"`
|
|
MaxItems *int `hcl:"max_items"`
|
|
Nested hcl.Body `hcl:",remain"`
|
|
}
|
|
|
|
var args content
|
|
diags := gohcl.DecodeBody(body, nil, &args)
|
|
if diags.HasErrors() {
|
|
return errSpec, diags
|
|
}
|
|
|
|
spec := &hcldec.BlockListSpec{
|
|
TypeName: impliedName,
|
|
}
|
|
|
|
if args.MinItems != nil {
|
|
spec.MinItems = *args.MinItems
|
|
}
|
|
if args.MaxItems != nil {
|
|
spec.MaxItems = *args.MaxItems
|
|
}
|
|
if args.TypeName != nil {
|
|
spec.TypeName = *args.TypeName
|
|
}
|
|
|
|
nested, nestedDiags := decodeBlockNestedSpec(args.Nested)
|
|
diags = append(diags, nestedDiags...)
|
|
spec.Nested = nested
|
|
|
|
if spec.TypeName == "" {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing block_type in block_list spec",
|
|
Detail: "The block_type attribute is required, to specify the block type name that is expected in an input HCL file.",
|
|
Subject: body.MissingItemRange().Ptr(),
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeBlockSetSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
|
|
type content struct {
|
|
TypeName *string `hcl:"block_type"`
|
|
MinItems *int `hcl:"min_items"`
|
|
MaxItems *int `hcl:"max_items"`
|
|
Nested hcl.Body `hcl:",remain"`
|
|
}
|
|
|
|
var args content
|
|
diags := gohcl.DecodeBody(body, nil, &args)
|
|
if diags.HasErrors() {
|
|
return errSpec, diags
|
|
}
|
|
|
|
spec := &hcldec.BlockSetSpec{
|
|
TypeName: impliedName,
|
|
}
|
|
|
|
if args.MinItems != nil {
|
|
spec.MinItems = *args.MinItems
|
|
}
|
|
if args.MaxItems != nil {
|
|
spec.MaxItems = *args.MaxItems
|
|
}
|
|
if args.TypeName != nil {
|
|
spec.TypeName = *args.TypeName
|
|
}
|
|
|
|
nested, nestedDiags := decodeBlockNestedSpec(args.Nested)
|
|
diags = append(diags, nestedDiags...)
|
|
spec.Nested = nested
|
|
|
|
if spec.TypeName == "" {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing block_type in block_set spec",
|
|
Detail: "The block_type attribute is required, to specify the block type name that is expected in an input HCL file.",
|
|
Subject: body.MissingItemRange().Ptr(),
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeBlockMapSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
|
|
type content struct {
|
|
TypeName *string `hcl:"block_type"`
|
|
Labels []string `hcl:"labels"`
|
|
Nested hcl.Body `hcl:",remain"`
|
|
}
|
|
|
|
var args content
|
|
diags := gohcl.DecodeBody(body, nil, &args)
|
|
if diags.HasErrors() {
|
|
return errSpec, diags
|
|
}
|
|
|
|
spec := &hcldec.BlockMapSpec{
|
|
TypeName: impliedName,
|
|
}
|
|
|
|
if args.TypeName != nil {
|
|
spec.TypeName = *args.TypeName
|
|
}
|
|
spec.LabelNames = args.Labels
|
|
|
|
nested, nestedDiags := decodeBlockNestedSpec(args.Nested)
|
|
diags = append(diags, nestedDiags...)
|
|
spec.Nested = nested
|
|
|
|
if spec.TypeName == "" {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing block_type in block_map spec",
|
|
Detail: "The block_type attribute is required, to specify the block type name that is expected in an input HCL file.",
|
|
Subject: body.MissingItemRange().Ptr(),
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
if len(spec.LabelNames) < 1 {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid block label name list",
|
|
Detail: "A block_map must have at least one label specified.",
|
|
Subject: body.MissingItemRange().Ptr(),
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
if hcldec.ImpliedType(spec).HasDynamicTypes() {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid block_map spec",
|
|
Detail: "A block_map spec may not contain attributes with type 'any'.",
|
|
Subject: body.MissingItemRange().Ptr(),
|
|
})
|
|
}
|
|
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeBlockNestedSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
|
|
content, diags := body.Content(specSchemaUnlabelled)
|
|
|
|
if len(content.Blocks) == 0 {
|
|
if diags.HasErrors() {
|
|
// If we already have errors then they probably explain
|
|
// why we have no blocks, so we'll skip our additional
|
|
// error message added below.
|
|
return errSpec, diags
|
|
}
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing spec block",
|
|
Detail: "A block spec must have exactly one child spec specifying how to decode block contents.",
|
|
Subject: body.MissingItemRange().Ptr(),
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
if len(content.Blocks) > 1 {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Extraneous spec block",
|
|
Detail: "A block spec must have exactly one child spec specifying how to decode block contents.",
|
|
Subject: &content.Blocks[1].DefRange,
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
spec, specDiags := decodeSpecBlock(content.Blocks[0])
|
|
diags = append(diags, specDiags...)
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeBlockAttrsSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
|
|
type content struct {
|
|
TypeName *string `hcl:"block_type"`
|
|
ElementType hcl.Expression `hcl:"element_type"`
|
|
Required *bool `hcl:"required"`
|
|
}
|
|
|
|
var args content
|
|
diags := gohcl.DecodeBody(body, nil, &args)
|
|
if diags.HasErrors() {
|
|
return errSpec, diags
|
|
}
|
|
|
|
spec := &hcldec.BlockAttrsSpec{
|
|
TypeName: impliedName,
|
|
}
|
|
|
|
if args.Required != nil {
|
|
spec.Required = *args.Required
|
|
}
|
|
if args.TypeName != nil {
|
|
spec.TypeName = *args.TypeName
|
|
}
|
|
|
|
var typeDiags hcl.Diagnostics
|
|
spec.ElementType, typeDiags = evalTypeExpr(args.ElementType)
|
|
diags = append(diags, typeDiags...)
|
|
|
|
if spec.TypeName == "" {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing block_type in block_attrs spec",
|
|
Detail: "The block_type attribute is required, to specify the block type name that is expected in an input HCL file.",
|
|
Subject: body.MissingItemRange().Ptr(),
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeLiteralSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
|
|
type content struct {
|
|
Value cty.Value `hcl:"value"`
|
|
}
|
|
|
|
var args content
|
|
diags := gohcl.DecodeBody(body, specCtx, &args)
|
|
if diags.HasErrors() {
|
|
return errSpec, diags
|
|
}
|
|
|
|
return &hcldec.LiteralSpec{
|
|
Value: args.Value,
|
|
}, diags
|
|
}
|
|
|
|
func decodeDefaultSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
|
|
content, diags := body.Content(specSchemaUnlabelled)
|
|
|
|
if len(content.Blocks) == 0 {
|
|
if diags.HasErrors() {
|
|
// If we already have errors then they probably explain
|
|
// why we have no blocks, so we'll skip our additional
|
|
// error message added below.
|
|
return errSpec, diags
|
|
}
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing spec block",
|
|
Detail: "A default block must have at least one nested spec, each specifying a possible outcome.",
|
|
Subject: body.MissingItemRange().Ptr(),
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
if len(content.Blocks) == 1 && !diags.HasErrors() {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagWarning,
|
|
Summary: "Useless default block",
|
|
Detail: "A default block with only one spec is equivalent to using that spec alone.",
|
|
Subject: &content.Blocks[1].DefRange,
|
|
})
|
|
}
|
|
|
|
var spec hcldec.Spec
|
|
for _, block := range content.Blocks {
|
|
candidateSpec, candidateDiags := decodeSpecBlock(block)
|
|
diags = append(diags, candidateDiags...)
|
|
if candidateDiags.HasErrors() {
|
|
continue
|
|
}
|
|
|
|
if spec == nil {
|
|
spec = candidateSpec
|
|
} else {
|
|
spec = &hcldec.DefaultSpec{
|
|
Primary: spec,
|
|
Default: candidateSpec,
|
|
}
|
|
}
|
|
}
|
|
|
|
return spec, diags
|
|
}
|
|
|
|
func decodeTransformSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
|
|
type content struct {
|
|
Result hcl.Expression `hcl:"result"`
|
|
Nested hcl.Body `hcl:",remain"`
|
|
}
|
|
|
|
var args content
|
|
diags := gohcl.DecodeBody(body, nil, &args)
|
|
if diags.HasErrors() {
|
|
return errSpec, diags
|
|
}
|
|
|
|
spec := &hcldec.TransformExprSpec{
|
|
Expr: args.Result,
|
|
VarName: "nested",
|
|
TransformCtx: specCtx,
|
|
}
|
|
|
|
nestedContent, nestedDiags := args.Nested.Content(specSchemaUnlabelled)
|
|
diags = append(diags, nestedDiags...)
|
|
|
|
if len(nestedContent.Blocks) != 1 {
|
|
if nestedDiags.HasErrors() {
|
|
// If we already have errors then they probably explain
|
|
// why we have the wrong number of blocks, so we'll skip our
|
|
// additional error message added below.
|
|
return errSpec, diags
|
|
}
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid transform spec",
|
|
Detail: "A transform spec block must have exactly one nested spec block.",
|
|
Subject: body.MissingItemRange().Ptr(),
|
|
})
|
|
return errSpec, diags
|
|
}
|
|
|
|
nestedSpec, nestedDiags := decodeSpecBlock(nestedContent.Blocks[0])
|
|
diags = append(diags, nestedDiags...)
|
|
spec.Wrapped = nestedSpec
|
|
|
|
return spec, diags
|
|
}
|
|
|
|
var errSpec = &hcldec.LiteralSpec{
|
|
Value: cty.NullVal(cty.DynamicPseudoType),
|
|
}
|
|
|
|
var specBlockTypes = []string{
|
|
"object",
|
|
"array",
|
|
|
|
"literal",
|
|
|
|
"attr",
|
|
|
|
"block",
|
|
"block_list",
|
|
"block_map",
|
|
"block_set",
|
|
|
|
"default",
|
|
"transform",
|
|
}
|
|
|
|
var specSchemaUnlabelled *hcl.BodySchema
|
|
var specSchemaLabelled *hcl.BodySchema
|
|
|
|
var specSchemaLabelledLabels = []string{"key"}
|
|
|
|
func init() {
|
|
specSchemaLabelled = &hcl.BodySchema{
|
|
Blocks: make([]hcl.BlockHeaderSchema, 0, len(specBlockTypes)),
|
|
}
|
|
specSchemaUnlabelled = &hcl.BodySchema{
|
|
Blocks: make([]hcl.BlockHeaderSchema, 0, len(specBlockTypes)),
|
|
}
|
|
|
|
for _, name := range specBlockTypes {
|
|
specSchemaLabelled.Blocks = append(
|
|
specSchemaLabelled.Blocks,
|
|
hcl.BlockHeaderSchema{
|
|
Type: name,
|
|
LabelNames: specSchemaLabelledLabels,
|
|
},
|
|
)
|
|
specSchemaUnlabelled.Blocks = append(
|
|
specSchemaUnlabelled.Blocks,
|
|
hcl.BlockHeaderSchema{
|
|
Type: name,
|
|
},
|
|
)
|
|
}
|
|
}
|