2019-06-12 10:07:32 -05:00
package terraform
import (
"fmt"
2019-09-09 17:58:44 -05:00
"github.com/hashicorp/hcl/v2"
2020-10-28 19:52:03 -05:00
"github.com/hashicorp/terraform/lang"
2019-06-12 10:07:32 -05:00
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
)
2020-04-09 09:13:03 -05:00
// evaluateForEachExpression is our standard mechanism for interpreting an
// expression given for a "for_each" argument on a resource or a module. This
// should be called during expansion in order to determine the final keys and
// values.
2019-06-12 10:07:32 -05:00
//
2020-04-09 09:13:03 -05:00
// evaluateForEachExpression differs from evaluateForEachExpressionValue by
// returning an error if the count value is not known, and converting the
// cty.Value to a map[string]cty.Value for compatibility with other calls.
2020-04-08 16:09:17 -05:00
func evaluateForEachExpression ( expr hcl . Expression , ctx EvalContext ) ( forEach map [ string ] cty . Value , diags tfdiags . Diagnostics ) {
2020-10-28 19:52:03 -05:00
forEachVal , diags := evaluateForEachExpressionValue ( expr , ctx , false )
// forEachVal might be unknown, but if it is then there should already
// be an error about it in diags, which we'll return below.
2020-04-08 16:09:17 -05:00
if forEachVal . IsNull ( ) || ! forEachVal . IsKnown ( ) || forEachVal . LengthInt ( ) == 0 {
// we check length, because an empty set return a nil map
return map [ string ] cty . Value { } , diags
}
return forEachVal . AsValueMap ( ) , diags
2019-07-25 10:51:55 -05:00
}
2020-04-08 16:09:17 -05:00
// evaluateForEachExpressionValue is like evaluateForEachExpression
// except that it returns a cty.Value map or set which can be unknown.
2020-10-28 19:52:03 -05:00
func evaluateForEachExpressionValue ( expr hcl . Expression , ctx EvalContext , allowUnknown bool ) ( cty . Value , tfdiags . Diagnostics ) {
2020-04-08 16:09:17 -05:00
var diags tfdiags . Diagnostics
nullMap := cty . NullVal ( cty . Map ( cty . DynamicPseudoType ) )
2019-06-12 10:07:32 -05:00
if expr == nil {
2020-04-08 16:09:17 -05:00
return nullMap , diags
2019-06-12 10:07:32 -05:00
}
2020-10-28 19:52:03 -05:00
refs , moreDiags := lang . ReferencesInExpr ( expr )
diags = diags . Append ( moreDiags )
scope := ctx . EvaluationScope ( nil , EvalDataForNoInstanceKey )
var hclCtx * hcl . EvalContext
if scope != nil {
hclCtx , moreDiags = scope . EvalContext ( refs )
} else {
// This shouldn't happen in real code, but it can unfortunately arise
// in unit tests due to incompletely-implemented mocks. :(
hclCtx = & hcl . EvalContext { }
}
diags = diags . Append ( moreDiags )
if diags . HasErrors ( ) { // Can't continue if we don't even have a valid scope
return nullMap , diags
}
forEachVal , forEachDiags := expr . Value ( hclCtx )
2019-06-12 10:07:32 -05:00
diags = diags . Append ( forEachDiags )
2020-09-04 12:28:21 -05:00
if forEachVal . ContainsMarked ( ) {
diags = diags . Append ( & hcl . Diagnostic {
2020-10-28 19:52:03 -05:00
Severity : hcl . DiagError ,
Summary : "Invalid for_each argument" ,
2020-10-29 10:49:52 -05:00
Detail : "Sensitive variables, or values derived from sensitive variables, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key." ,
2020-10-28 19:52:03 -05:00
Subject : expr . Range ( ) . Ptr ( ) ,
Expression : expr ,
EvalContext : hclCtx ,
2020-09-04 12:28:21 -05:00
} )
}
2019-06-12 10:07:32 -05:00
if diags . HasErrors ( ) {
2020-04-08 16:09:17 -05:00
return nullMap , diags
2019-06-12 10:07:32 -05:00
}
2020-04-08 16:09:17 -05:00
ty := forEachVal . Type ( )
2019-06-12 10:07:32 -05:00
2019-07-25 10:51:55 -05:00
switch {
case forEachVal . IsNull ( ) :
2019-06-12 10:07:32 -05:00
diags = diags . Append ( & hcl . Diagnostic {
2020-10-28 19:52:03 -05:00
Severity : hcl . DiagError ,
Summary : "Invalid for_each argument" ,
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. ` ,
Subject : expr . Range ( ) . Ptr ( ) ,
Expression : expr ,
EvalContext : hclCtx ,
2019-06-12 10:07:32 -05:00
} )
2020-04-08 16:09:17 -05:00
return nullMap , diags
2019-08-28 13:02:11 -05:00
case ! forEachVal . IsKnown ( ) :
2020-10-28 19:52:03 -05:00
if ! allowUnknown {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid for_each argument" ,
Detail : errInvalidForEachUnknownDetail ,
Subject : expr . Range ( ) . Ptr ( ) ,
Expression : expr ,
EvalContext : hclCtx ,
} )
}
2020-04-08 16:09:17 -05:00
// ensure that we have a map, and not a DynamicValue
return cty . UnknownVal ( cty . Map ( cty . DynamicPseudoType ) ) , diags
2019-06-12 10:07:32 -05:00
2020-04-08 16:09:17 -05:00
case ! ( ty . IsMapType ( ) || ty . IsSetType ( ) || ty . IsObjectType ( ) ) :
2019-06-12 10:07:32 -05:00
diags = diags . Append ( & hcl . Diagnostic {
2020-10-28 19:52:03 -05:00
Severity : hcl . DiagError ,
Summary : "Invalid for_each argument" ,
Detail : fmt . Sprintf ( ` The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, or set of strings, and you have provided a value of type %s. ` , ty . FriendlyName ( ) ) ,
Subject : expr . Range ( ) . Ptr ( ) ,
Expression : expr ,
EvalContext : hclCtx ,
2019-06-12 10:07:32 -05:00
} )
2020-04-08 16:09:17 -05:00
return nullMap , diags
2019-06-12 10:07:32 -05:00
2020-04-08 16:09:17 -05:00
case forEachVal . LengthInt ( ) == 0 :
// If the map is empty ({}), return an empty map, because cty will
2020-04-09 10:47:16 -05:00
// return nil when representing {} AsValueMap. This also covers an empty
2020-04-08 16:09:17 -05:00
// set (toset([]))
return forEachVal , diags
2019-09-10 09:37:54 -05:00
}
2020-04-08 16:09:17 -05:00
if ty . IsSetType ( ) {
2020-06-29 08:12:36 -05:00
// since we can't use a set values that are unknown, we treat the
// entire set as unknown
if ! forEachVal . IsWhollyKnown ( ) {
2020-10-28 19:52:03 -05:00
if ! allowUnknown {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid for_each argument" ,
Detail : errInvalidForEachUnknownDetail ,
Subject : expr . Range ( ) . Ptr ( ) ,
Expression : expr ,
EvalContext : hclCtx ,
} )
}
2020-06-29 08:12:36 -05:00
return cty . UnknownVal ( ty ) , diags
}
2020-04-08 16:09:17 -05:00
if ty . ElementType ( ) != cty . String {
2019-06-12 10:07:32 -05:00
diags = diags . Append ( & hcl . Diagnostic {
2020-10-28 19:52:03 -05:00
Severity : hcl . DiagError ,
Summary : "Invalid for_each set argument" ,
Detail : fmt . Sprintf ( ` The given "for_each" argument value is unsuitable: "for_each" supports maps and sets of strings, but you have provided a set containing type %s. ` , forEachVal . Type ( ) . ElementType ( ) . FriendlyName ( ) ) ,
Subject : expr . Range ( ) . Ptr ( ) ,
Expression : expr ,
EvalContext : hclCtx ,
2019-06-12 10:07:32 -05:00
} )
2020-04-08 16:09:17 -05:00
return cty . NullVal ( ty ) , diags
2019-06-12 10:07:32 -05:00
}
2019-08-28 13:02:11 -05:00
2020-02-14 16:20:08 -06:00
// A set of strings may contain null, which makes it impossible to
// convert to a map, so we must return an error
it := forEachVal . ElementIterator ( )
for it . Next ( ) {
item , _ := it . Element ( )
if item . IsNull ( ) {
diags = diags . Append ( & hcl . Diagnostic {
2020-10-28 19:52:03 -05:00
Severity : hcl . DiagError ,
Summary : "Invalid for_each set argument" ,
Detail : fmt . Sprintf ( ` The given "for_each" argument value is unsuitable: "for_each" sets must not contain null values. ` ) ,
Subject : expr . Range ( ) . Ptr ( ) ,
Expression : expr ,
EvalContext : hclCtx ,
2020-02-14 16:20:08 -06:00
} )
2020-04-08 16:09:17 -05:00
return cty . NullVal ( ty ) , diags
2020-02-14 16:20:08 -06:00
}
}
2019-06-12 10:07:32 -05:00
}
2020-04-08 16:09:17 -05:00
return forEachVal , nil
2019-06-12 10:07:32 -05:00
}
2020-10-28 19:52:03 -05:00
const errInvalidForEachUnknownDetail = ` The "for_each" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the for_each depends on. `