package hcldec import ( "bytes" "fmt" "sort" "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" "github.com/zclconf/go-cty/cty/function" ) // A Spec is a description of how to decode a hcl.Body to a cty.Value. // // The various other types in this package whose names end in "Spec" are // the spec implementations. The most common top-level spec is ObjectSpec, // which decodes body content into a cty.Value of an object type. type Spec interface { // Perform the decode operation on the given body, in the context of // the given block (which might be null), using the given eval context. // // "block" is provided only by the nested calls performed by the spec // types that work on block bodies. decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) // Return the cty.Type that should be returned when decoding a body with // this spec. impliedType() cty.Type // Call the given callback once for each of the nested specs that would // get decoded with the same body and block as the receiver. This should // not descend into the nested specs used when decoding blocks. visitSameBodyChildren(cb visitFunc) // Determine the source range of the value that would be returned for the // spec in the given content, in the context of the given block // (which might be null). If the corresponding item is missing, return // a place where it might be inserted. sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range } type visitFunc func(spec Spec) // An ObjectSpec is a Spec that produces a cty.Value of an object type whose // attributes correspond to the keys of the spec map. type ObjectSpec map[string]Spec // attrSpec is implemented by specs that require attributes from the body. type attrSpec interface { attrSchemata() []hcl.AttributeSchema } // blockSpec is implemented by specs that require blocks from the body. type blockSpec interface { blockHeaderSchemata() []hcl.BlockHeaderSchema nestedSpec() Spec } // specNeedingVariables is implemented by specs that can use variables // from the EvalContext, to declare which variables they need. type specNeedingVariables interface { variablesNeeded(content *hcl.BodyContent) []hcl.Traversal } func (s ObjectSpec) visitSameBodyChildren(cb visitFunc) { for _, c := range s { cb(c) } } func (s ObjectSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { vals := make(map[string]cty.Value, len(s)) var diags hcl.Diagnostics for k, spec := range s { var kd hcl.Diagnostics vals[k], kd = spec.decode(content, blockLabels, ctx) diags = append(diags, kd...) } return cty.ObjectVal(vals), diags } func (s ObjectSpec) impliedType() cty.Type { if len(s) == 0 { return cty.EmptyObject } attrTypes := make(map[string]cty.Type) for k, childSpec := range s { attrTypes[k] = childSpec.impliedType() } return cty.Object(attrTypes) } func (s ObjectSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { // This is not great, but the best we can do. In practice, it's rather // strange to ask for the source range of an entire top-level body, since // that's already readily available to the caller. return content.MissingItemRange } // A TupleSpec is a Spec that produces a cty.Value of a tuple type whose // elements correspond to the elements of the spec slice. type TupleSpec []Spec func (s TupleSpec) visitSameBodyChildren(cb visitFunc) { for _, c := range s { cb(c) } } func (s TupleSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { vals := make([]cty.Value, len(s)) var diags hcl.Diagnostics for i, spec := range s { var ed hcl.Diagnostics vals[i], ed = spec.decode(content, blockLabels, ctx) diags = append(diags, ed...) } return cty.TupleVal(vals), diags } func (s TupleSpec) impliedType() cty.Type { if len(s) == 0 { return cty.EmptyTuple } attrTypes := make([]cty.Type, len(s)) for i, childSpec := range s { attrTypes[i] = childSpec.impliedType() } return cty.Tuple(attrTypes) } func (s TupleSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { // This is not great, but the best we can do. In practice, it's rather // strange to ask for the source range of an entire top-level body, since // that's already readily available to the caller. return content.MissingItemRange } // An AttrSpec is a Spec that evaluates a particular attribute expression in // the body and returns its resulting value converted to the requested type, // or produces a diagnostic if the type is incorrect. type AttrSpec struct { Name string Type cty.Type Required bool } func (s *AttrSpec) visitSameBodyChildren(cb visitFunc) { // leaf node } // specNeedingVariables implementation func (s *AttrSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal { attr, exists := content.Attributes[s.Name] if !exists { return nil } return attr.Expr.Variables() } // attrSpec implementation func (s *AttrSpec) attrSchemata() []hcl.AttributeSchema { return []hcl.AttributeSchema{ { Name: s.Name, Required: s.Required, }, } } func (s *AttrSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { attr, exists := content.Attributes[s.Name] if !exists { return content.MissingItemRange } return attr.Expr.Range() } func (s *AttrSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { attr, exists := content.Attributes[s.Name] if !exists { // We don't need to check required and emit a diagnostic here, because // that would already have happened when building "content". return cty.NullVal(s.Type), nil } val, diags := attr.Expr.Value(ctx) convVal, err := convert.Convert(val, s.Type) if err != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Incorrect attribute value type", Detail: fmt.Sprintf( "Inappropriate value for attribute %q: %s.", s.Name, err.Error(), ), Subject: attr.Expr.StartRange().Ptr(), Context: hcl.RangeBetween(attr.NameRange, attr.Expr.StartRange()).Ptr(), }) // We'll return an unknown value of the _correct_ type so that the // incomplete result can still be used for some analysis use-cases. val = cty.UnknownVal(s.Type) } else { val = convVal } return val, diags } func (s *AttrSpec) impliedType() cty.Type { return s.Type } // A LiteralSpec is a Spec that produces the given literal value, ignoring // the given body. type LiteralSpec struct { Value cty.Value } func (s *LiteralSpec) visitSameBodyChildren(cb visitFunc) { // leaf node } func (s *LiteralSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { return s.Value, nil } func (s *LiteralSpec) impliedType() cty.Type { return s.Value.Type() } func (s *LiteralSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { // No sensible range to return for a literal, so the caller had better // ensure it doesn't cause any diagnostics. return hcl.Range{ Filename: "", } } // An ExprSpec is a Spec that evaluates the given expression, ignoring the // given body. type ExprSpec struct { Expr hcl.Expression } func (s *ExprSpec) visitSameBodyChildren(cb visitFunc) { // leaf node } // specNeedingVariables implementation func (s *ExprSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal { return s.Expr.Variables() } func (s *ExprSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { return s.Expr.Value(ctx) } func (s *ExprSpec) impliedType() cty.Type { // We can't know the type of our expression until we evaluate it return cty.DynamicPseudoType } func (s *ExprSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { return s.Expr.Range() } // A BlockSpec is a Spec that produces a cty.Value by decoding the contents // of a single nested block of a given type, using a nested spec. // // If the Required flag is not set, the nested block may be omitted, in which // case a null value is produced. If it _is_ set, an error diagnostic is // produced if there are no nested blocks of the given type. type BlockSpec struct { TypeName string Nested Spec Required bool } func (s *BlockSpec) visitSameBodyChildren(cb visitFunc) { // leaf node ("Nested" does not use the same body) } // blockSpec implementation func (s *BlockSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema { return []hcl.BlockHeaderSchema{ { Type: s.TypeName, LabelNames: findLabelSpecs(s.Nested), }, } } // blockSpec implementation func (s *BlockSpec) nestedSpec() Spec { return s.Nested } // specNeedingVariables implementation func (s *BlockSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal { var childBlock *hcl.Block for _, candidate := range content.Blocks { if candidate.Type != s.TypeName { continue } childBlock = candidate break } if childBlock == nil { return nil } return Variables(childBlock.Body, s.Nested) } func (s *BlockSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { var diags hcl.Diagnostics var childBlock *hcl.Block for _, candidate := range content.Blocks { if candidate.Type != s.TypeName { continue } if childBlock != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Duplicate %s block", s.TypeName), Detail: fmt.Sprintf( "Only one block of type %q is allowed. Previous definition was at %s.", s.TypeName, childBlock.DefRange.String(), ), Subject: &candidate.DefRange, }) break } childBlock = candidate } if childBlock == nil { if s.Required { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Missing %s block", s.TypeName), Detail: fmt.Sprintf( "A block of type %q is required here.", s.TypeName, ), Subject: &content.MissingItemRange, }) } return cty.NullVal(s.Nested.impliedType()), diags } if s.Nested == nil { panic("BlockSpec with no Nested Spec") } val, _, childDiags := decode(childBlock.Body, labelsForBlock(childBlock), ctx, s.Nested, false) diags = append(diags, childDiags...) return val, diags } func (s *BlockSpec) impliedType() cty.Type { return s.Nested.impliedType() } func (s *BlockSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { var childBlock *hcl.Block for _, candidate := range content.Blocks { if candidate.Type != s.TypeName { continue } childBlock = candidate break } if childBlock == nil { return content.MissingItemRange } return sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested) } // A BlockListSpec is a Spec that produces a cty list of the results of // decoding all of the nested blocks of a given type, using a nested spec. type BlockListSpec struct { TypeName string Nested Spec MinItems int MaxItems int } func (s *BlockListSpec) visitSameBodyChildren(cb visitFunc) { // leaf node ("Nested" does not use the same body) } // blockSpec implementation func (s *BlockListSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema { return []hcl.BlockHeaderSchema{ { Type: s.TypeName, LabelNames: findLabelSpecs(s.Nested), }, } } // blockSpec implementation func (s *BlockListSpec) nestedSpec() Spec { return s.Nested } // specNeedingVariables implementation func (s *BlockListSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal { var ret []hcl.Traversal for _, childBlock := range content.Blocks { if childBlock.Type != s.TypeName { continue } ret = append(ret, Variables(childBlock.Body, s.Nested)...) } return ret } func (s *BlockListSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { var diags hcl.Diagnostics if s.Nested == nil { panic("BlockListSpec with no Nested Spec") } var elems []cty.Value var sourceRanges []hcl.Range for _, childBlock := range content.Blocks { if childBlock.Type != s.TypeName { continue } val, _, childDiags := decode(childBlock.Body, labelsForBlock(childBlock), ctx, s.Nested, false) diags = append(diags, childDiags...) elems = append(elems, val) sourceRanges = append(sourceRanges, sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested)) } if len(elems) < s.MinItems { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Insufficient %s blocks", s.TypeName), Detail: fmt.Sprintf("At least %d %q blocks are required.", s.MinItems, s.TypeName), Subject: &content.MissingItemRange, }) } else if s.MaxItems > 0 && len(elems) > s.MaxItems { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Too many %s blocks", s.TypeName), Detail: fmt.Sprintf("No more than %d %q blocks are allowed", s.MaxItems, s.TypeName), Subject: &sourceRanges[s.MaxItems], }) } var ret cty.Value if len(elems) == 0 { ret = cty.ListValEmpty(s.Nested.impliedType()) } else { // Since our target is a list, all of the decoded elements must have the // same type or cty.ListVal will panic below. Different types can arise // if there is an attribute spec of type cty.DynamicPseudoType in the // nested spec; all given values must be convertable to a single type // in order for the result to be considered valid. etys := make([]cty.Type, len(elems)) for i, v := range elems { etys[i] = v.Type() } ety, convs := convert.UnifyUnsafe(etys) if ety == cty.NilType { // FIXME: This is a pretty terrible error message. diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Unconsistent argument types in %s blocks", s.TypeName), Detail: "Corresponding attributes in all blocks of this type must be the same.", Subject: &sourceRanges[0], }) return cty.DynamicVal, diags } for i, v := range elems { if convs[i] != nil { newV, err := convs[i](v) if err != nil { // FIXME: This is a pretty terrible error message. diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Unconsistent argument types in %s blocks", s.TypeName), Detail: fmt.Sprintf("Block with index %d has inconsistent argument types: %s.", i, err), Subject: &sourceRanges[i], }) // Bail early here so we won't panic below in cty.ListVal return cty.DynamicVal, diags } elems[i] = newV } } ret = cty.ListVal(elems) } return ret, diags } func (s *BlockListSpec) impliedType() cty.Type { return cty.List(s.Nested.impliedType()) } func (s *BlockListSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { // We return the source range of the _first_ block of the given type, // since they are not guaranteed to form a contiguous range. var childBlock *hcl.Block for _, candidate := range content.Blocks { if candidate.Type != s.TypeName { continue } childBlock = candidate break } if childBlock == nil { return content.MissingItemRange } return sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested) } // A BlockTupleSpec is a Spec that produces a cty tuple of the results of // decoding all of the nested blocks of a given type, using a nested spec. // // This is similar to BlockListSpec, but it permits the nested blocks to have // different result types in situations where cty.DynamicPseudoType attributes // are present. type BlockTupleSpec struct { TypeName string Nested Spec MinItems int MaxItems int } func (s *BlockTupleSpec) visitSameBodyChildren(cb visitFunc) { // leaf node ("Nested" does not use the same body) } // blockSpec implementation func (s *BlockTupleSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema { return []hcl.BlockHeaderSchema{ { Type: s.TypeName, LabelNames: findLabelSpecs(s.Nested), }, } } // blockSpec implementation func (s *BlockTupleSpec) nestedSpec() Spec { return s.Nested } // specNeedingVariables implementation func (s *BlockTupleSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal { var ret []hcl.Traversal for _, childBlock := range content.Blocks { if childBlock.Type != s.TypeName { continue } ret = append(ret, Variables(childBlock.Body, s.Nested)...) } return ret } func (s *BlockTupleSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { var diags hcl.Diagnostics if s.Nested == nil { panic("BlockListSpec with no Nested Spec") } var elems []cty.Value var sourceRanges []hcl.Range for _, childBlock := range content.Blocks { if childBlock.Type != s.TypeName { continue } val, _, childDiags := decode(childBlock.Body, labelsForBlock(childBlock), ctx, s.Nested, false) diags = append(diags, childDiags...) elems = append(elems, val) sourceRanges = append(sourceRanges, sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested)) } if len(elems) < s.MinItems { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Insufficient %s blocks", s.TypeName), Detail: fmt.Sprintf("At least %d %q blocks are required.", s.MinItems, s.TypeName), Subject: &content.MissingItemRange, }) } else if s.MaxItems > 0 && len(elems) > s.MaxItems { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Too many %s blocks", s.TypeName), Detail: fmt.Sprintf("No more than %d %q blocks are allowed", s.MaxItems, s.TypeName), Subject: &sourceRanges[s.MaxItems], }) } var ret cty.Value if len(elems) == 0 { ret = cty.EmptyTupleVal } else { ret = cty.TupleVal(elems) } return ret, diags } func (s *BlockTupleSpec) impliedType() cty.Type { // We can't predict our type, because we don't know how many blocks // there will be until we decode. return cty.DynamicPseudoType } func (s *BlockTupleSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { // We return the source range of the _first_ block of the given type, // since they are not guaranteed to form a contiguous range. var childBlock *hcl.Block for _, candidate := range content.Blocks { if candidate.Type != s.TypeName { continue } childBlock = candidate break } if childBlock == nil { return content.MissingItemRange } return sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested) } // A BlockSetSpec is a Spec that produces a cty set of the results of // decoding all of the nested blocks of a given type, using a nested spec. type BlockSetSpec struct { TypeName string Nested Spec MinItems int MaxItems int } func (s *BlockSetSpec) visitSameBodyChildren(cb visitFunc) { // leaf node ("Nested" does not use the same body) } // blockSpec implementation func (s *BlockSetSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema { return []hcl.BlockHeaderSchema{ { Type: s.TypeName, LabelNames: findLabelSpecs(s.Nested), }, } } // blockSpec implementation func (s *BlockSetSpec) nestedSpec() Spec { return s.Nested } // specNeedingVariables implementation func (s *BlockSetSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal { var ret []hcl.Traversal for _, childBlock := range content.Blocks { if childBlock.Type != s.TypeName { continue } ret = append(ret, Variables(childBlock.Body, s.Nested)...) } return ret } func (s *BlockSetSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { var diags hcl.Diagnostics if s.Nested == nil { panic("BlockSetSpec with no Nested Spec") } var elems []cty.Value var sourceRanges []hcl.Range for _, childBlock := range content.Blocks { if childBlock.Type != s.TypeName { continue } val, _, childDiags := decode(childBlock.Body, labelsForBlock(childBlock), ctx, s.Nested, false) diags = append(diags, childDiags...) elems = append(elems, val) sourceRanges = append(sourceRanges, sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested)) } if len(elems) < s.MinItems { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Insufficient %s blocks", s.TypeName), Detail: fmt.Sprintf("At least %d %q blocks are required.", s.MinItems, s.TypeName), Subject: &content.MissingItemRange, }) } else if s.MaxItems > 0 && len(elems) > s.MaxItems { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Too many %s blocks", s.TypeName), Detail: fmt.Sprintf("No more than %d %q blocks are allowed", s.MaxItems, s.TypeName), Subject: &sourceRanges[s.MaxItems], }) } var ret cty.Value if len(elems) == 0 { ret = cty.SetValEmpty(s.Nested.impliedType()) } else { // Since our target is a set, all of the decoded elements must have the // same type or cty.SetVal will panic below. Different types can arise // if there is an attribute spec of type cty.DynamicPseudoType in the // nested spec; all given values must be convertable to a single type // in order for the result to be considered valid. etys := make([]cty.Type, len(elems)) for i, v := range elems { etys[i] = v.Type() } ety, convs := convert.UnifyUnsafe(etys) if ety == cty.NilType { // FIXME: This is a pretty terrible error message. diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Unconsistent argument types in %s blocks", s.TypeName), Detail: "Corresponding attributes in all blocks of this type must be the same.", Subject: &sourceRanges[0], }) return cty.DynamicVal, diags } for i, v := range elems { if convs[i] != nil { newV, err := convs[i](v) if err != nil { // FIXME: This is a pretty terrible error message. diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Unconsistent argument types in %s blocks", s.TypeName), Detail: fmt.Sprintf("Block with index %d has inconsistent argument types: %s.", i, err), Subject: &sourceRanges[i], }) // Bail early here so we won't panic below in cty.ListVal return cty.DynamicVal, diags } elems[i] = newV } } ret = cty.SetVal(elems) } return ret, diags } func (s *BlockSetSpec) impliedType() cty.Type { return cty.Set(s.Nested.impliedType()) } func (s *BlockSetSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { // We return the source range of the _first_ block of the given type, // since they are not guaranteed to form a contiguous range. var childBlock *hcl.Block for _, candidate := range content.Blocks { if candidate.Type != s.TypeName { continue } childBlock = candidate break } if childBlock == nil { return content.MissingItemRange } return sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested) } // A BlockMapSpec is a Spec that produces a cty map of the results of // decoding all of the nested blocks of a given type, using a nested spec. // // One level of map structure is created for each of the given label names. // There must be at least one given label name. type BlockMapSpec struct { TypeName string LabelNames []string Nested Spec } func (s *BlockMapSpec) visitSameBodyChildren(cb visitFunc) { // leaf node ("Nested" does not use the same body) } // blockSpec implementation func (s *BlockMapSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema { return []hcl.BlockHeaderSchema{ { Type: s.TypeName, LabelNames: append(s.LabelNames, findLabelSpecs(s.Nested)...), }, } } // blockSpec implementation func (s *BlockMapSpec) nestedSpec() Spec { return s.Nested } // specNeedingVariables implementation func (s *BlockMapSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal { var ret []hcl.Traversal for _, childBlock := range content.Blocks { if childBlock.Type != s.TypeName { continue } ret = append(ret, Variables(childBlock.Body, s.Nested)...) } return ret } func (s *BlockMapSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { var diags hcl.Diagnostics if s.Nested == nil { panic("BlockMapSpec with no Nested Spec") } if ImpliedType(s).HasDynamicTypes() { panic("cty.DynamicPseudoType attributes may not be used inside a BlockMapSpec") } elems := map[string]interface{}{} for _, childBlock := range content.Blocks { if childBlock.Type != s.TypeName { continue } childLabels := labelsForBlock(childBlock) val, _, childDiags := decode(childBlock.Body, childLabels[len(s.LabelNames):], ctx, s.Nested, false) targetMap := elems for _, key := range childBlock.Labels[:len(s.LabelNames)-1] { if _, exists := targetMap[key]; !exists { targetMap[key] = make(map[string]interface{}) } targetMap = targetMap[key].(map[string]interface{}) } diags = append(diags, childDiags...) key := childBlock.Labels[len(s.LabelNames)-1] if _, exists := targetMap[key]; exists { labelsBuf := bytes.Buffer{} for _, label := range childBlock.Labels { fmt.Fprintf(&labelsBuf, " %q", label) } diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Duplicate %s block", s.TypeName), Detail: fmt.Sprintf( "A block for %s%s was already defined. The %s labels must be unique.", s.TypeName, labelsBuf.String(), s.TypeName, ), Subject: &childBlock.DefRange, }) continue } targetMap[key] = val } if len(elems) == 0 { return cty.MapValEmpty(s.Nested.impliedType()), diags } var ctyMap func(map[string]interface{}, int) cty.Value ctyMap = func(raw map[string]interface{}, depth int) cty.Value { vals := make(map[string]cty.Value, len(raw)) if depth == 1 { for k, v := range raw { vals[k] = v.(cty.Value) } } else { for k, v := range raw { vals[k] = ctyMap(v.(map[string]interface{}), depth-1) } } return cty.MapVal(vals) } return ctyMap(elems, len(s.LabelNames)), diags } func (s *BlockMapSpec) impliedType() cty.Type { ret := s.Nested.impliedType() for _ = range s.LabelNames { ret = cty.Map(ret) } return ret } func (s *BlockMapSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { // We return the source range of the _first_ block of the given type, // since they are not guaranteed to form a contiguous range. var childBlock *hcl.Block for _, candidate := range content.Blocks { if candidate.Type != s.TypeName { continue } childBlock = candidate break } if childBlock == nil { return content.MissingItemRange } return sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested) } // A BlockObjectSpec is a Spec that produces a cty object of the results of // decoding all of the nested blocks of a given type, using a nested spec. // // One level of object structure is created for each of the given label names. // There must be at least one given label name. // // This is similar to BlockMapSpec, but it permits the nested blocks to have // different result types in situations where cty.DynamicPseudoType attributes // are present. type BlockObjectSpec struct { TypeName string LabelNames []string Nested Spec } func (s *BlockObjectSpec) visitSameBodyChildren(cb visitFunc) { // leaf node ("Nested" does not use the same body) } // blockSpec implementation func (s *BlockObjectSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema { return []hcl.BlockHeaderSchema{ { Type: s.TypeName, LabelNames: append(s.LabelNames, findLabelSpecs(s.Nested)...), }, } } // blockSpec implementation func (s *BlockObjectSpec) nestedSpec() Spec { return s.Nested } // specNeedingVariables implementation func (s *BlockObjectSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal { var ret []hcl.Traversal for _, childBlock := range content.Blocks { if childBlock.Type != s.TypeName { continue } ret = append(ret, Variables(childBlock.Body, s.Nested)...) } return ret } func (s *BlockObjectSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { var diags hcl.Diagnostics if s.Nested == nil { panic("BlockObjectSpec with no Nested Spec") } elems := map[string]interface{}{} for _, childBlock := range content.Blocks { if childBlock.Type != s.TypeName { continue } childLabels := labelsForBlock(childBlock) val, _, childDiags := decode(childBlock.Body, childLabels[len(s.LabelNames):], ctx, s.Nested, false) targetMap := elems for _, key := range childBlock.Labels[:len(s.LabelNames)-1] { if _, exists := targetMap[key]; !exists { targetMap[key] = make(map[string]interface{}) } targetMap = targetMap[key].(map[string]interface{}) } diags = append(diags, childDiags...) key := childBlock.Labels[len(s.LabelNames)-1] if _, exists := targetMap[key]; exists { labelsBuf := bytes.Buffer{} for _, label := range childBlock.Labels { fmt.Fprintf(&labelsBuf, " %q", label) } diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Duplicate %s block", s.TypeName), Detail: fmt.Sprintf( "A block for %s%s was already defined. The %s labels must be unique.", s.TypeName, labelsBuf.String(), s.TypeName, ), Subject: &childBlock.DefRange, }) continue } targetMap[key] = val } if len(elems) == 0 { return cty.EmptyObjectVal, diags } var ctyObj func(map[string]interface{}, int) cty.Value ctyObj = func(raw map[string]interface{}, depth int) cty.Value { vals := make(map[string]cty.Value, len(raw)) if depth == 1 { for k, v := range raw { vals[k] = v.(cty.Value) } } else { for k, v := range raw { vals[k] = ctyObj(v.(map[string]interface{}), depth-1) } } return cty.ObjectVal(vals) } return ctyObj(elems, len(s.LabelNames)), diags } func (s *BlockObjectSpec) impliedType() cty.Type { // We can't predict our type, since we don't know how many blocks are // present and what labels they have until we decode. return cty.DynamicPseudoType } func (s *BlockObjectSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { // We return the source range of the _first_ block of the given type, // since they are not guaranteed to form a contiguous range. var childBlock *hcl.Block for _, candidate := range content.Blocks { if candidate.Type != s.TypeName { continue } childBlock = candidate break } if childBlock == nil { return content.MissingItemRange } return sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested) } // A BlockAttrsSpec is a Spec that interprets a single block as if it were // a map of some element type. That is, each attribute within the block // becomes a key in the resulting map and the attribute's value becomes the // element value, after conversion to the given element type. The resulting // value is a cty.Map of the given element type. // // This spec imposes a validation constraint that there be exactly one block // of the given type name and that this block may contain only attributes. The // block does not accept any labels. // // This is an alternative to an AttrSpec of a map type for situations where // block syntax is desired. Note that block syntax does not permit dynamic // keys, construction of the result via a "for" expression, etc. In most cases // an AttrSpec is preferred if the desired result is a map whose keys are // chosen by the user rather than by schema. type BlockAttrsSpec struct { TypeName string ElementType cty.Type Required bool } func (s *BlockAttrsSpec) visitSameBodyChildren(cb visitFunc) { // leaf node } // blockSpec implementation func (s *BlockAttrsSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema { return []hcl.BlockHeaderSchema{ { Type: s.TypeName, LabelNames: nil, }, } } // blockSpec implementation func (s *BlockAttrsSpec) nestedSpec() Spec { // This is an odd case: we aren't actually going to apply a nested spec // in this case, since we're going to interpret the body directly as // attributes, but we need to return something non-nil so that the // decoder will recognize this as a block spec. We won't actually be // using this for anything at decode time. return noopSpec{} } // specNeedingVariables implementation func (s *BlockAttrsSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal { block, _ := s.findBlock(content) if block == nil { return nil } var vars []hcl.Traversal attrs, diags := block.Body.JustAttributes() if diags.HasErrors() { return nil } for _, attr := range attrs { vars = append(vars, attr.Expr.Variables()...) } // We'll return the variables references in source order so that any // error messages that result are also in source order. sort.Slice(vars, func(i, j int) bool { return vars[i].SourceRange().Start.Byte < vars[j].SourceRange().Start.Byte }) return vars } func (s *BlockAttrsSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { var diags hcl.Diagnostics block, other := s.findBlock(content) if block == nil { if s.Required { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Missing %s block", s.TypeName), Detail: fmt.Sprintf( "A block of type %q is required here.", s.TypeName, ), Subject: &content.MissingItemRange, }) } return cty.NullVal(cty.Map(s.ElementType)), diags } if other != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Duplicate %s block", s.TypeName), Detail: fmt.Sprintf( "Only one block of type %q is allowed. Previous definition was at %s.", s.TypeName, block.DefRange.String(), ), Subject: &other.DefRange, }) } attrs, attrDiags := block.Body.JustAttributes() diags = append(diags, attrDiags...) if len(attrs) == 0 { return cty.MapValEmpty(s.ElementType), diags } vals := make(map[string]cty.Value, len(attrs)) for name, attr := range attrs { attrVal, attrDiags := attr.Expr.Value(ctx) diags = append(diags, attrDiags...) attrVal, err := convert.Convert(attrVal, s.ElementType) if err != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid attribute value", Detail: fmt.Sprintf("Invalid value for attribute of %q block: %s.", s.TypeName, err), Subject: attr.Expr.Range().Ptr(), }) attrVal = cty.UnknownVal(s.ElementType) } vals[name] = attrVal } return cty.MapVal(vals), diags } func (s *BlockAttrsSpec) impliedType() cty.Type { return cty.Map(s.ElementType) } func (s *BlockAttrsSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { block, _ := s.findBlock(content) if block == nil { return content.MissingItemRange } return block.DefRange } func (s *BlockAttrsSpec) findBlock(content *hcl.BodyContent) (block *hcl.Block, other *hcl.Block) { for _, candidate := range content.Blocks { if candidate.Type != s.TypeName { continue } if block != nil { return block, candidate } block = candidate } return block, nil } // A BlockLabelSpec is a Spec that returns a cty.String representing the // label of the block its given body belongs to, if indeed its given body // belongs to a block. It is a programming error to use this in a non-block // context, so this spec will panic in that case. // // This spec only works in the nested spec within a BlockSpec, BlockListSpec, // BlockSetSpec or BlockMapSpec. // // The full set of label specs used against a particular block must have a // consecutive set of indices starting at zero. The maximum index found // defines how many labels the corresponding blocks must have in cty source. type BlockLabelSpec struct { Index int Name string } func (s *BlockLabelSpec) visitSameBodyChildren(cb visitFunc) { // leaf node } func (s *BlockLabelSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { if s.Index >= len(blockLabels) { panic("BlockListSpec used in non-block context") } return cty.StringVal(blockLabels[s.Index].Value), nil } func (s *BlockLabelSpec) impliedType() cty.Type { return cty.String // labels are always strings } func (s *BlockLabelSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { if s.Index >= len(blockLabels) { panic("BlockListSpec used in non-block context") } return blockLabels[s.Index].Range } func findLabelSpecs(spec Spec) []string { maxIdx := -1 var names map[int]string var visit visitFunc visit = func(s Spec) { if ls, ok := s.(*BlockLabelSpec); ok { if maxIdx < ls.Index { maxIdx = ls.Index } if names == nil { names = make(map[int]string) } names[ls.Index] = ls.Name } s.visitSameBodyChildren(visit) } visit(spec) if maxIdx < 0 { return nil // no labels at all } ret := make([]string, maxIdx+1) for i := range ret { name := names[i] if name == "" { // Should never happen if the spec is conformant, since we require // consecutive indices starting at zero. name = fmt.Sprintf("missing%02d", i) } ret[i] = name } return ret } // DefaultSpec is a spec that wraps two specs, evaluating the primary first // and then evaluating the default if the primary returns a null value. // // The two specifications must have the same implied result type for correct // operation. If not, the result is undefined. // // Any requirements imposed by the "Default" spec apply even if "Primary" does // not return null. For example, if the "Default" spec is for a required // attribute then that attribute is always required, regardless of the result // of the "Primary" spec. // // The "Default" spec must not describe a nested block, since otherwise the // result of ChildBlockTypes would not be decidable without evaluation. If // the default spec _does_ describe a nested block then the result is // undefined. type DefaultSpec struct { Primary Spec Default Spec } func (s *DefaultSpec) visitSameBodyChildren(cb visitFunc) { cb(s.Primary) cb(s.Default) } func (s *DefaultSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { val, diags := s.Primary.decode(content, blockLabels, ctx) if val.IsNull() { var moreDiags hcl.Diagnostics val, moreDiags = s.Default.decode(content, blockLabels, ctx) diags = append(diags, moreDiags...) } return val, diags } func (s *DefaultSpec) impliedType() cty.Type { return s.Primary.impliedType() } // attrSpec implementation func (s *DefaultSpec) attrSchemata() []hcl.AttributeSchema { // We must pass through the union of both of our nested specs so that // we'll have both values available in the result. var ret []hcl.AttributeSchema if as, ok := s.Primary.(attrSpec); ok { ret = append(ret, as.attrSchemata()...) } if as, ok := s.Default.(attrSpec); ok { ret = append(ret, as.attrSchemata()...) } return ret } // blockSpec implementation func (s *DefaultSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema { // Only the primary spec may describe a block, since otherwise // our nestedSpec method below can't know which to return. if bs, ok := s.Primary.(blockSpec); ok { return bs.blockHeaderSchemata() } return nil } // blockSpec implementation func (s *DefaultSpec) nestedSpec() Spec { if bs, ok := s.Primary.(blockSpec); ok { return bs.nestedSpec() } return nil } func (s *DefaultSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { // We can't tell from here which of the two specs will ultimately be used // in our result, so we'll just assume the first. This is usually the right // choice because the default is often a literal spec that doesn't have a // reasonable source range to return anyway. return s.Primary.sourceRange(content, blockLabels) } // TransformExprSpec is a spec that wraps another and then evaluates a given // hcl.Expression on the result. // // The implied type of this spec is determined by evaluating the expression // with an unknown value of the nested spec's implied type, which may cause // the result to be imprecise. This spec should not be used in situations where // precise result type information is needed. type TransformExprSpec struct { Wrapped Spec Expr hcl.Expression TransformCtx *hcl.EvalContext VarName string } func (s *TransformExprSpec) visitSameBodyChildren(cb visitFunc) { cb(s.Wrapped) } func (s *TransformExprSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { wrappedVal, diags := s.Wrapped.decode(content, blockLabels, ctx) if diags.HasErrors() { // We won't try to run our function in this case, because it'll probably // generate confusing additional errors that will distract from the // root cause. return cty.UnknownVal(s.impliedType()), diags } chiCtx := s.TransformCtx.NewChild() chiCtx.Variables = map[string]cty.Value{ s.VarName: wrappedVal, } resultVal, resultDiags := s.Expr.Value(chiCtx) diags = append(diags, resultDiags...) return resultVal, diags } func (s *TransformExprSpec) impliedType() cty.Type { wrappedTy := s.Wrapped.impliedType() chiCtx := s.TransformCtx.NewChild() chiCtx.Variables = map[string]cty.Value{ s.VarName: cty.UnknownVal(wrappedTy), } resultVal, _ := s.Expr.Value(chiCtx) return resultVal.Type() } func (s *TransformExprSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { // We'll just pass through our wrapped range here, even though that's // not super-accurate, because there's nothing better to return. return s.Wrapped.sourceRange(content, blockLabels) } // TransformFuncSpec is a spec that wraps another and then evaluates a given // cty function with the result. The given function must expect exactly one // argument, where the result of the wrapped spec will be passed. // // The implied type of this spec is determined by type-checking the function // with an unknown value of the nested spec's implied type, which may cause // the result to be imprecise. This spec should not be used in situations where // precise result type information is needed. // // If the given function produces an error when run, this spec will produce // a non-user-actionable diagnostic message. It's the caller's responsibility // to ensure that the given function cannot fail for any non-error result // of the wrapped spec. type TransformFuncSpec struct { Wrapped Spec Func function.Function } func (s *TransformFuncSpec) visitSameBodyChildren(cb visitFunc) { cb(s.Wrapped) } func (s *TransformFuncSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { wrappedVal, diags := s.Wrapped.decode(content, blockLabels, ctx) if diags.HasErrors() { // We won't try to run our function in this case, because it'll probably // generate confusing additional errors that will distract from the // root cause. return cty.UnknownVal(s.impliedType()), diags } resultVal, err := s.Func.Call([]cty.Value{wrappedVal}) if err != nil { // This is not a good example of a diagnostic because it is reporting // a programming error in the calling application, rather than something // an end-user could act on. diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Transform function failed", Detail: fmt.Sprintf("Decoder transform returned an error: %s", err), Subject: s.sourceRange(content, blockLabels).Ptr(), }) return cty.UnknownVal(s.impliedType()), diags } return resultVal, diags } func (s *TransformFuncSpec) impliedType() cty.Type { wrappedTy := s.Wrapped.impliedType() resultTy, err := s.Func.ReturnType([]cty.Type{wrappedTy}) if err != nil { // Should never happen with a correctly-configured spec return cty.DynamicPseudoType } return resultTy } func (s *TransformFuncSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { // We'll just pass through our wrapped range here, even though that's // not super-accurate, because there's nothing better to return. return s.Wrapped.sourceRange(content, blockLabels) } // noopSpec is a placeholder spec that does nothing, used in situations where // a non-nil placeholder spec is required. It is not exported because there is // no reason to use it directly; it is always an implementation detail only. type noopSpec struct { } func (s noopSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { return cty.NullVal(cty.DynamicPseudoType), nil } func (s noopSpec) impliedType() cty.Type { return cty.DynamicPseudoType } func (s noopSpec) visitSameBodyChildren(cb visitFunc) { // nothing to do } func (s noopSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { // No useful range for a noopSpec, and nobody should be calling this anyway. return hcl.Range{ Filename: "noopSpec", } }