github.com/hashicorp/hcl/v2@v2.20.0/ext/dynblock/expand_spec.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package dynblock
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/hashicorp/hcl/v2"
    10  	"github.com/zclconf/go-cty/cty"
    11  	"github.com/zclconf/go-cty/cty/convert"
    12  )
    13  
    14  type expandSpec struct {
    15  	blockType      string
    16  	blockTypeRange hcl.Range
    17  	defRange       hcl.Range
    18  	forEachVal     cty.Value
    19  	iteratorName   string
    20  	labelExprs     []hcl.Expression
    21  	contentBody    hcl.Body
    22  	inherited      map[string]*iteration
    23  }
    24  
    25  func (b *expandBody) decodeSpec(blockS *hcl.BlockHeaderSchema, rawSpec *hcl.Block) (*expandSpec, hcl.Diagnostics) {
    26  	var diags hcl.Diagnostics
    27  
    28  	var schema *hcl.BodySchema
    29  	if len(blockS.LabelNames) != 0 {
    30  		schema = dynamicBlockBodySchemaLabels
    31  	} else {
    32  		schema = dynamicBlockBodySchemaNoLabels
    33  	}
    34  
    35  	specContent, specDiags := rawSpec.Body.Content(schema)
    36  	diags = append(diags, specDiags...)
    37  	if specDiags.HasErrors() {
    38  		return nil, diags
    39  	}
    40  
    41  	//// iterator attribute
    42  
    43  	iteratorName := blockS.Type
    44  	if iteratorAttr := specContent.Attributes["iterator"]; iteratorAttr != nil {
    45  		itTraversal, itDiags := hcl.AbsTraversalForExpr(iteratorAttr.Expr)
    46  		diags = append(diags, itDiags...)
    47  		if itDiags.HasErrors() {
    48  			return nil, diags
    49  		}
    50  
    51  		if len(itTraversal) != 1 {
    52  			diags = append(diags, &hcl.Diagnostic{
    53  				Severity: hcl.DiagError,
    54  				Summary:  "Invalid dynamic iterator name",
    55  				Detail:   "Dynamic iterator must be a single variable name.",
    56  				Subject:  itTraversal.SourceRange().Ptr(),
    57  			})
    58  			return nil, diags
    59  		}
    60  
    61  		iteratorName = itTraversal.RootName()
    62  	}
    63  
    64  	//// for_each attribute
    65  
    66  	eachAttr := specContent.Attributes["for_each"]
    67  	eachVal, eachDiags := eachAttr.Expr.Value(b.forEachCtx)
    68  	diags = append(diags, eachDiags...)
    69  	if diags.HasErrors() {
    70  		return nil, diags
    71  	}
    72  	for _, check := range b.checkForEach {
    73  		moreDiags := check(eachVal, eachAttr.Expr, b.forEachCtx)
    74  		diags = append(diags, moreDiags...)
    75  		if moreDiags.HasErrors() {
    76  			return nil, diags
    77  		}
    78  	}
    79  
    80  	if !eachVal.CanIterateElements() && eachVal.Type() != cty.DynamicPseudoType {
    81  		// We skip this error for DynamicPseudoType because that means we either
    82  		// have a null (which is checked immediately below) or an unknown
    83  		// (which is handled in the expandBody Content methods).
    84  		diags = append(diags, &hcl.Diagnostic{
    85  			Severity:    hcl.DiagError,
    86  			Summary:     "Invalid dynamic for_each value",
    87  			Detail:      fmt.Sprintf("Cannot use a %s value in for_each. An iterable collection is required.", eachVal.Type().FriendlyName()),
    88  			Subject:     eachAttr.Expr.Range().Ptr(),
    89  			Expression:  eachAttr.Expr,
    90  			EvalContext: b.forEachCtx,
    91  		})
    92  		return nil, diags
    93  	}
    94  	if eachVal.IsNull() {
    95  		diags = append(diags, &hcl.Diagnostic{
    96  			Severity:    hcl.DiagError,
    97  			Summary:     "Invalid dynamic for_each value",
    98  			Detail:      "Cannot use a null value in for_each.",
    99  			Subject:     eachAttr.Expr.Range().Ptr(),
   100  			Expression:  eachAttr.Expr,
   101  			EvalContext: b.forEachCtx,
   102  		})
   103  		return nil, diags
   104  	}
   105  
   106  	//// labels attribute
   107  
   108  	var labelExprs []hcl.Expression
   109  	if labelsAttr := specContent.Attributes["labels"]; labelsAttr != nil {
   110  		var labelDiags hcl.Diagnostics
   111  		labelExprs, labelDiags = hcl.ExprList(labelsAttr.Expr)
   112  		diags = append(diags, labelDiags...)
   113  		if labelDiags.HasErrors() {
   114  			return nil, diags
   115  		}
   116  
   117  		if len(labelExprs) > len(blockS.LabelNames) {
   118  			diags = append(diags, &hcl.Diagnostic{
   119  				Severity: hcl.DiagError,
   120  				Summary:  "Extraneous dynamic block label",
   121  				Detail:   fmt.Sprintf("Blocks of type %q require %d label(s).", blockS.Type, len(blockS.LabelNames)),
   122  				Subject:  labelExprs[len(blockS.LabelNames)].Range().Ptr(),
   123  			})
   124  			return nil, diags
   125  		} else if len(labelExprs) < len(blockS.LabelNames) {
   126  			diags = append(diags, &hcl.Diagnostic{
   127  				Severity: hcl.DiagError,
   128  				Summary:  "Insufficient dynamic block labels",
   129  				Detail:   fmt.Sprintf("Blocks of type %q require %d label(s).", blockS.Type, len(blockS.LabelNames)),
   130  				Subject:  labelsAttr.Expr.Range().Ptr(),
   131  			})
   132  			return nil, diags
   133  		}
   134  	}
   135  
   136  	// Since our schema requests only blocks of type "content", we can assume
   137  	// that all entries in specContent.Blocks are content blocks.
   138  	if len(specContent.Blocks) == 0 {
   139  		diags = append(diags, &hcl.Diagnostic{
   140  			Severity: hcl.DiagError,
   141  			Summary:  "Missing dynamic content block",
   142  			Detail:   "A dynamic block must have a nested block of type \"content\" to describe the body of each generated block.",
   143  			Subject:  &specContent.MissingItemRange,
   144  		})
   145  		return nil, diags
   146  	}
   147  	if len(specContent.Blocks) > 1 {
   148  		diags = append(diags, &hcl.Diagnostic{
   149  			Severity: hcl.DiagError,
   150  			Summary:  "Extraneous dynamic content block",
   151  			Detail:   "Only one nested content block is allowed for each dynamic block.",
   152  			Subject:  &specContent.Blocks[1].DefRange,
   153  		})
   154  		return nil, diags
   155  	}
   156  
   157  	return &expandSpec{
   158  		blockType:      blockS.Type,
   159  		blockTypeRange: rawSpec.LabelRanges[0],
   160  		defRange:       rawSpec.DefRange,
   161  		forEachVal:     eachVal,
   162  		iteratorName:   iteratorName,
   163  		labelExprs:     labelExprs,
   164  		contentBody:    specContent.Blocks[0].Body,
   165  	}, diags
   166  }
   167  
   168  func (s *expandSpec) newBlock(i *iteration, ctx *hcl.EvalContext) (*hcl.Block, hcl.Diagnostics) {
   169  	var diags hcl.Diagnostics
   170  	var labels []string
   171  	var labelRanges []hcl.Range
   172  	lCtx := i.EvalContext(ctx)
   173  	for _, labelExpr := range s.labelExprs {
   174  		labelVal, labelDiags := labelExpr.Value(lCtx)
   175  		diags = append(diags, labelDiags...)
   176  		if labelDiags.HasErrors() {
   177  			return nil, diags
   178  		}
   179  
   180  		var convErr error
   181  		labelVal, convErr = convert.Convert(labelVal, cty.String)
   182  		if convErr != nil {
   183  			diags = append(diags, &hcl.Diagnostic{
   184  				Severity:    hcl.DiagError,
   185  				Summary:     "Invalid dynamic block label",
   186  				Detail:      fmt.Sprintf("Cannot use this value as a dynamic block label: %s.", convErr),
   187  				Subject:     labelExpr.Range().Ptr(),
   188  				Expression:  labelExpr,
   189  				EvalContext: lCtx,
   190  			})
   191  			return nil, diags
   192  		}
   193  		if labelVal.IsNull() {
   194  			diags = append(diags, &hcl.Diagnostic{
   195  				Severity:    hcl.DiagError,
   196  				Summary:     "Invalid dynamic block label",
   197  				Detail:      "Cannot use a null value as a dynamic block label.",
   198  				Subject:     labelExpr.Range().Ptr(),
   199  				Expression:  labelExpr,
   200  				EvalContext: lCtx,
   201  			})
   202  			return nil, diags
   203  		}
   204  		if !labelVal.IsKnown() {
   205  			diags = append(diags, &hcl.Diagnostic{
   206  				Severity:    hcl.DiagError,
   207  				Summary:     "Invalid dynamic block label",
   208  				Detail:      "This value is not yet known. Dynamic block labels must be immediately-known values.",
   209  				Subject:     labelExpr.Range().Ptr(),
   210  				Expression:  labelExpr,
   211  				EvalContext: lCtx,
   212  			})
   213  			return nil, diags
   214  		}
   215  
   216  		labels = append(labels, labelVal.AsString())
   217  		labelRanges = append(labelRanges, labelExpr.Range())
   218  	}
   219  
   220  	block := &hcl.Block{
   221  		Type:        s.blockType,
   222  		TypeRange:   s.blockTypeRange,
   223  		Labels:      labels,
   224  		LabelRanges: labelRanges,
   225  		DefRange:    s.defRange,
   226  		Body:        s.contentBody,
   227  	}
   228  
   229  	return block, diags
   230  }