github.com/terraform-linters/tflint@v0.51.2-0.20240520175844-3750771571b6/terraform/tfhcl/expand_spec.go (about)

     1  package tfhcl
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/hashicorp/hcl/v2"
     7  	"github.com/terraform-linters/tflint/terraform/tfdiags"
     8  	"github.com/zclconf/go-cty/cty"
     9  	"github.com/zclconf/go-cty/cty/convert"
    10  	"github.com/zclconf/go-cty/cty/gocty"
    11  )
    12  
    13  type expandDynamicSpec struct {
    14  	blockType      string
    15  	blockTypeRange hcl.Range
    16  	defRange       hcl.Range
    17  	forEachVal     cty.Value
    18  	iteratorName   string
    19  	labelExprs     []hcl.Expression
    20  	contentBody    hcl.Body
    21  }
    22  
    23  func (b *expandBody) decodeDynamicSpec(blockS *hcl.BlockHeaderSchema, rawSpec *hcl.Block) (*expandDynamicSpec, hcl.Diagnostics) {
    24  	var diags hcl.Diagnostics
    25  
    26  	var schema *hcl.BodySchema
    27  	if len(blockS.LabelNames) != 0 {
    28  		schema = dynamicBlockBodySchemaLabels
    29  	} else {
    30  		schema = dynamicBlockBodySchemaNoLabels
    31  	}
    32  
    33  	specContent, specDiags := rawSpec.Body.Content(schema)
    34  	diags = append(diags, specDiags...)
    35  	if specDiags.HasErrors() {
    36  		return nil, diags
    37  	}
    38  
    39  	//// for_each attribute
    40  
    41  	eachAttr := specContent.Attributes["for_each"]
    42  	eachVal, eachDiags := eachAttr.Expr.Value(b.ctx)
    43  	diags = append(diags, eachDiags...)
    44  
    45  	if !eachVal.CanIterateElements() && eachVal.Type() != cty.DynamicPseudoType {
    46  		// We skip this error for DynamicPseudoType because that means we either
    47  		// have a null (which is checked immediately below) or an unknown
    48  		// (which is handled in the expandBody Content methods).
    49  		diags = append(diags, &hcl.Diagnostic{
    50  			Severity:    hcl.DiagError,
    51  			Summary:     "Invalid dynamic for_each value",
    52  			Detail:      fmt.Sprintf("Cannot use a %s value in for_each. An iterable collection is required.", eachVal.Type().FriendlyName()),
    53  			Subject:     eachAttr.Expr.Range().Ptr(),
    54  			Expression:  eachAttr.Expr,
    55  			EvalContext: b.ctx,
    56  		})
    57  		return nil, diags
    58  	}
    59  	if eachVal.IsNull() {
    60  		diags = append(diags, &hcl.Diagnostic{
    61  			Severity:    hcl.DiagError,
    62  			Summary:     "Invalid dynamic for_each value",
    63  			Detail:      "Cannot use a null value in for_each.",
    64  			Subject:     eachAttr.Expr.Range().Ptr(),
    65  			Expression:  eachAttr.Expr,
    66  			EvalContext: b.ctx,
    67  		})
    68  		return nil, diags
    69  	}
    70  
    71  	//// iterator attribute
    72  
    73  	iteratorName := blockS.Type
    74  	if iteratorAttr := specContent.Attributes["iterator"]; iteratorAttr != nil {
    75  		itTraversal, itDiags := hcl.AbsTraversalForExpr(iteratorAttr.Expr)
    76  		diags = append(diags, itDiags...)
    77  		if itDiags.HasErrors() {
    78  			return nil, diags
    79  		}
    80  
    81  		if len(itTraversal) != 1 {
    82  			diags = append(diags, &hcl.Diagnostic{
    83  				Severity: hcl.DiagError,
    84  				Summary:  "Invalid dynamic iterator name",
    85  				Detail:   "Dynamic iterator must be a single variable name.",
    86  				Subject:  itTraversal.SourceRange().Ptr(),
    87  			})
    88  			return nil, diags
    89  		}
    90  
    91  		iteratorName = itTraversal.RootName()
    92  	}
    93  
    94  	var labelExprs []hcl.Expression
    95  	if labelsAttr := specContent.Attributes["labels"]; labelsAttr != nil {
    96  		var labelDiags hcl.Diagnostics
    97  		labelExprs, labelDiags = hcl.ExprList(labelsAttr.Expr)
    98  		diags = append(diags, labelDiags...)
    99  		if labelDiags.HasErrors() {
   100  			return nil, diags
   101  		}
   102  
   103  		if len(labelExprs) > len(blockS.LabelNames) {
   104  			diags = append(diags, &hcl.Diagnostic{
   105  				Severity: hcl.DiagError,
   106  				Summary:  "Extraneous dynamic block label",
   107  				Detail:   fmt.Sprintf("Blocks of type %q require %d label(s).", blockS.Type, len(blockS.LabelNames)),
   108  				Subject:  labelExprs[len(blockS.LabelNames)].Range().Ptr(),
   109  			})
   110  			return nil, diags
   111  		} else if len(labelExprs) < len(blockS.LabelNames) {
   112  			diags = append(diags, &hcl.Diagnostic{
   113  				Severity: hcl.DiagError,
   114  				Summary:  "Insufficient dynamic block labels",
   115  				Detail:   fmt.Sprintf("Blocks of type %q require %d label(s).", blockS.Type, len(blockS.LabelNames)),
   116  				Subject:  labelsAttr.Expr.Range().Ptr(),
   117  			})
   118  			return nil, diags
   119  		}
   120  	}
   121  
   122  	// Since our schema requests only blocks of type "content", we can assume
   123  	// that all entries in specContent.Blocks are content blocks.
   124  	if len(specContent.Blocks) == 0 {
   125  		diags = append(diags, &hcl.Diagnostic{
   126  			Severity: hcl.DiagError,
   127  			Summary:  "Missing dynamic content block",
   128  			Detail:   "A dynamic block must have a nested block of type \"content\" to describe the body of each generated block.",
   129  			Subject:  &specContent.MissingItemRange,
   130  		})
   131  		return nil, diags
   132  	}
   133  	if len(specContent.Blocks) > 1 {
   134  		diags = append(diags, &hcl.Diagnostic{
   135  			Severity: hcl.DiagError,
   136  			Summary:  "Extraneous dynamic content block",
   137  			Detail:   "Only one nested content block is allowed for each dynamic block.",
   138  			Subject:  &specContent.Blocks[1].DefRange,
   139  		})
   140  		return nil, diags
   141  	}
   142  
   143  	return &expandDynamicSpec{
   144  		blockType:      blockS.Type,
   145  		blockTypeRange: rawSpec.LabelRanges[0],
   146  		defRange:       rawSpec.DefRange,
   147  		forEachVal:     eachVal,
   148  		iteratorName:   iteratorName,
   149  		labelExprs:     labelExprs,
   150  		contentBody:    specContent.Blocks[0].Body,
   151  	}, diags
   152  }
   153  
   154  func (s *expandDynamicSpec) newBlock(i *dynamicIteration, ctx *hcl.EvalContext) (*hcl.Block, hcl.Diagnostics) {
   155  	var diags hcl.Diagnostics
   156  	var labels []string
   157  	var labelRanges []hcl.Range
   158  	lCtx := i.EvalContext(ctx)
   159  	for _, labelExpr := range s.labelExprs {
   160  		labelVal, labelDiags := labelExpr.Value(lCtx)
   161  		diags = append(diags, labelDiags...)
   162  		if labelDiags.HasErrors() {
   163  			return nil, diags
   164  		}
   165  
   166  		var convErr error
   167  		labelVal, convErr = convert.Convert(labelVal, cty.String)
   168  		if convErr != nil {
   169  			diags = append(diags, &hcl.Diagnostic{
   170  				Severity:    hcl.DiagError,
   171  				Summary:     "Invalid dynamic block label",
   172  				Detail:      fmt.Sprintf("Cannot use this value as a dynamic block label: %s.", convErr),
   173  				Subject:     labelExpr.Range().Ptr(),
   174  				Expression:  labelExpr,
   175  				EvalContext: lCtx,
   176  			})
   177  			return nil, diags
   178  		}
   179  		if labelVal.IsNull() {
   180  			diags = append(diags, &hcl.Diagnostic{
   181  				Severity:    hcl.DiagError,
   182  				Summary:     "Invalid dynamic block label",
   183  				Detail:      "Cannot use a null value as a dynamic block label.",
   184  				Subject:     labelExpr.Range().Ptr(),
   185  				Expression:  labelExpr,
   186  				EvalContext: lCtx,
   187  			})
   188  			return nil, diags
   189  		}
   190  		if !labelVal.IsKnown() {
   191  			return nil, diags
   192  		}
   193  		if labelVal.IsMarked() {
   194  			diags = append(diags, &hcl.Diagnostic{
   195  				Severity:    hcl.DiagError,
   196  				Summary:     "Invalid dynamic block label",
   197  				Detail:      "Cannot use a marked value as a dynamic block label.",
   198  				Subject:     labelExpr.Range().Ptr(),
   199  				Expression:  labelExpr,
   200  				EvalContext: lCtx,
   201  			})
   202  			return nil, diags
   203  		}
   204  
   205  		labels = append(labels, labelVal.AsString())
   206  		labelRanges = append(labelRanges, labelExpr.Range())
   207  	}
   208  
   209  	block := &hcl.Block{
   210  		Type:        s.blockType,
   211  		TypeRange:   s.blockTypeRange,
   212  		Labels:      labels,
   213  		LabelRanges: labelRanges,
   214  		DefRange:    s.defRange,
   215  		Body:        s.contentBody,
   216  	}
   217  
   218  	return block, diags
   219  }
   220  
   221  type expandMetaArgSpec struct {
   222  	rawBlock   *hcl.Block
   223  	countSet   bool
   224  	countVal   cty.Value
   225  	countNum   int
   226  	forEachSet bool
   227  	forEachVal cty.Value
   228  }
   229  
   230  func (b *expandBody) decodeMetaArgSpec(rawSpec *hcl.Block) (*expandMetaArgSpec, hcl.Diagnostics) {
   231  	spec := &expandMetaArgSpec{rawBlock: rawSpec}
   232  	var diags hcl.Diagnostics
   233  
   234  	specContent, _, specDiags := rawSpec.Body.PartialContent(expandableBlockBodySchema)
   235  	diags = append(diags, specDiags...)
   236  	if specDiags.HasErrors() {
   237  		return spec, diags
   238  	}
   239  
   240  	//// count attribute
   241  
   242  	if countAttr, exists := specContent.Attributes["count"]; exists {
   243  		spec.countSet = true
   244  
   245  		countVal, countDiags := countAttr.Expr.Value(b.ctx)
   246  		diags = append(diags, countDiags...)
   247  		countVal, _ = countVal.Unmark()
   248  
   249  		spec.countVal = countVal
   250  
   251  		// We skip validation for count attribute if the value is unknwon
   252  		if countVal.IsKnown() {
   253  			if countVal.IsNull() {
   254  				diags = append(diags, &hcl.Diagnostic{
   255  					Severity:    hcl.DiagError,
   256  					Summary:     "Invalid count argument",
   257  					Detail:      `The given "count" argument value is null. An integer is required.`,
   258  					Subject:     countAttr.Expr.Range().Ptr(),
   259  					Expression:  countAttr.Expr,
   260  					EvalContext: b.ctx,
   261  				})
   262  				return spec, diags
   263  			}
   264  
   265  			var convErr error
   266  			countVal, convErr = convert.Convert(countVal, cty.Number)
   267  			if convErr != nil {
   268  				diags = diags.Append(&hcl.Diagnostic{
   269  					Severity:    hcl.DiagError,
   270  					Summary:     "Incorrect value type",
   271  					Detail:      fmt.Sprintf("Invalid expression value: %s.", tfdiags.FormatError(convErr)),
   272  					Subject:     countAttr.Expr.Range().Ptr(),
   273  					Expression:  countAttr.Expr,
   274  					EvalContext: b.ctx,
   275  				})
   276  				return spec, diags
   277  			}
   278  
   279  			err := gocty.FromCtyValue(countVal, &spec.countNum)
   280  			if err != nil {
   281  				diags = diags.Append(&hcl.Diagnostic{
   282  					Severity:    hcl.DiagError,
   283  					Summary:     "Invalid count argument",
   284  					Detail:      fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err),
   285  					Subject:     countAttr.Expr.Range().Ptr(),
   286  					Expression:  countAttr.Expr,
   287  					EvalContext: b.ctx,
   288  				})
   289  				return spec, diags
   290  			}
   291  			if spec.countNum < 0 {
   292  				diags = diags.Append(&hcl.Diagnostic{
   293  					Severity:    hcl.DiagError,
   294  					Summary:     "Invalid count argument",
   295  					Detail:      `The given "count" argument value is unsuitable: negative numbers are not supported.`,
   296  					Subject:     countAttr.Expr.Range().Ptr(),
   297  					Expression:  countAttr.Expr,
   298  					EvalContext: b.ctx,
   299  				})
   300  				return spec, diags
   301  			}
   302  		}
   303  	}
   304  
   305  	//// for_each attribute
   306  
   307  	if eachAttr, exists := specContent.Attributes["for_each"]; exists {
   308  		spec.forEachSet = true
   309  
   310  		eachVal, eachDiags := eachAttr.Expr.Value(b.ctx)
   311  		diags = append(diags, eachDiags...)
   312  
   313  		spec.forEachVal = eachVal
   314  
   315  		if !eachVal.CanIterateElements() && eachVal.Type() != cty.DynamicPseudoType {
   316  			// We skip this error for DynamicPseudoType because that means we either
   317  			// have a null (which is checked immediately below) or an unknown
   318  			// (which is handled in the expandBody Content methods).
   319  			diags = diags.Append(&hcl.Diagnostic{
   320  				Severity:    hcl.DiagError,
   321  				Summary:     "The `for_each` value is not iterable",
   322  				Detail:      fmt.Sprintf("`%s` is not iterable", eachVal.GoString()),
   323  				Subject:     eachAttr.Expr.Range().Ptr(),
   324  				Expression:  eachAttr.Expr,
   325  				EvalContext: b.ctx,
   326  			})
   327  			return spec, diags
   328  		}
   329  		if eachVal.IsNull() {
   330  			diags = diags.Append(&hcl.Diagnostic{
   331  				Severity:    hcl.DiagError,
   332  				Summary:     "Invalid for_each argument",
   333  				Detail:      `The given "for_each" argument value is unsuitable: the given "for_each" argument value is null. A map, or set of strings is allowed.`,
   334  				Subject:     eachAttr.Expr.Range().Ptr(),
   335  				Expression:  eachAttr.Expr,
   336  				EvalContext: b.ctx,
   337  			})
   338  			return spec, diags
   339  		}
   340  	}
   341  
   342  	return spec, diags
   343  }