mirror of
https://github.com/hashicorp/terraform.git
synced 2026-03-21 18:10:30 -04:00
Some checks failed
build / Determine intended Terraform version (push) Has been cancelled
build / Determine Go toolchain version (push) Has been cancelled
Quick Checks / Unit Tests (push) Has been cancelled
Quick Checks / Race Tests (push) Has been cancelled
Quick Checks / End-to-end Tests (push) Has been cancelled
Quick Checks / Code Consistency Checks (push) Has been cancelled
build / Generate release metadata (push) Has been cancelled
build / Build for freebsd_386 (push) Has been cancelled
build / Build for linux_386 (push) Has been cancelled
build / Build for openbsd_386 (push) Has been cancelled
build / Build for windows_386 (push) Has been cancelled
build / Build for darwin_amd64 (push) Has been cancelled
build / Build for freebsd_amd64 (push) Has been cancelled
build / Build for linux_amd64 (push) Has been cancelled
build / Build for openbsd_amd64 (push) Has been cancelled
build / Build for solaris_amd64 (push) Has been cancelled
build / Build for windows_amd64 (push) Has been cancelled
build / Build for freebsd_arm (push) Has been cancelled
build / Build for linux_arm (push) Has been cancelled
build / Build for darwin_arm64 (push) Has been cancelled
build / Build for linux_arm64 (push) Has been cancelled
build / Build for windows_arm64 (push) Has been cancelled
build / Build Docker image for linux_386 (push) Has been cancelled
build / Build Docker image for linux_amd64 (push) Has been cancelled
build / Build Docker image for linux_arm (push) Has been cancelled
build / Build Docker image for linux_arm64 (push) Has been cancelled
build / Build e2etest for linux_386 (push) Has been cancelled
build / Build e2etest for windows_386 (push) Has been cancelled
build / Build e2etest for darwin_amd64 (push) Has been cancelled
build / Build e2etest for linux_amd64 (push) Has been cancelled
build / Build e2etest for windows_amd64 (push) Has been cancelled
build / Build e2etest for linux_arm (push) Has been cancelled
build / Build e2etest for darwin_arm64 (push) Has been cancelled
build / Build e2etest for linux_arm64 (push) Has been cancelled
build / Run e2e test for linux_386 (push) Has been cancelled
build / Run e2e test for windows_386 (push) Has been cancelled
build / Run e2e test for darwin_amd64 (push) Has been cancelled
build / Run e2e test for linux_amd64 (push) Has been cancelled
build / Run e2e test for windows_amd64 (push) Has been cancelled
build / Run e2e test for linux_arm (push) Has been cancelled
build / Run e2e test for linux_arm64 (push) Has been cancelled
build / Run terraform-exec test for linux amd64 (push) Has been cancelled
inline type conversions
252 lines
9.2 KiB
Go
252 lines
9.2 KiB
Go
// Copyright IBM Corp. 2014, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package funcs
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/ext/customdecode"
|
|
"github.com/hashicorp/hcl/v2/ext/typeexpr"
|
|
"github.com/hashicorp/terraform/internal/lang/ephemeral"
|
|
"github.com/hashicorp/terraform/internal/lang/marks"
|
|
"github.com/hashicorp/terraform/internal/lang/types"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/convert"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
)
|
|
|
|
// MakeToFunc constructs a "to..." function, like "tostring", which converts
|
|
// its argument to a specific type or type kind.
|
|
//
|
|
// The given type wantTy can be any type constraint that cty's "convert" package
|
|
// would accept. In particular, this means that you can pass
|
|
// cty.List(cty.DynamicPseudoType) to mean "list of any single type", which
|
|
// will then cause cty to attempt to unify all of the element types when given
|
|
// a tuple.
|
|
func MakeToFunc(wantTy cty.Type) function.Function {
|
|
return function.New(&function.Spec{
|
|
Params: []function.Parameter{
|
|
{
|
|
Name: "v",
|
|
// We use DynamicPseudoType rather than wantTy here so that
|
|
// all values will pass through the function API verbatim and
|
|
// we can handle the conversion logic within the Type and
|
|
// Impl functions. This allows us to customize the error
|
|
// messages to be more appropriate for an explicit type
|
|
// conversion, whereas the cty function system produces
|
|
// messages aimed at _implicit_ type conversions.
|
|
Type: cty.DynamicPseudoType,
|
|
AllowNull: true,
|
|
AllowMarked: true,
|
|
AllowDynamicType: true,
|
|
AllowUnknown: true,
|
|
},
|
|
},
|
|
Type: func(args []cty.Value) (cty.Type, error) {
|
|
gotTy := args[0].Type()
|
|
if gotTy.Equals(wantTy) {
|
|
return wantTy, nil
|
|
}
|
|
conv := convert.GetConversionUnsafe(args[0].Type(), wantTy)
|
|
if conv == nil {
|
|
// We'll use some specialized errors for some trickier cases,
|
|
// but most we can handle in a simple way.
|
|
switch {
|
|
case gotTy.IsTupleType() && wantTy.IsTupleType():
|
|
return cty.NilType, function.NewArgErrorf(0, "incompatible tuple type for conversion: %s", convert.MismatchMessage(gotTy, wantTy))
|
|
case gotTy.IsObjectType() && wantTy.IsObjectType():
|
|
return cty.NilType, function.NewArgErrorf(0, "incompatible object type for conversion: %s", convert.MismatchMessage(gotTy, wantTy))
|
|
default:
|
|
return cty.NilType, function.NewArgErrorf(0, "cannot convert %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint())
|
|
}
|
|
}
|
|
// If a conversion is available then everything is fine.
|
|
return wantTy, nil
|
|
},
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
if !args[0].IsKnown() {
|
|
return cty.UnknownVal(retType).WithSameMarks(args[0]), nil
|
|
}
|
|
|
|
ret, err := convert.Convert(args[0], retType)
|
|
if err != nil {
|
|
val, _ := args[0].UnmarkDeep()
|
|
// Because we used GetConversionUnsafe above, conversion can
|
|
// still potentially fail in here. For example, if the user
|
|
// asks to convert the string "a" to bool then we'll
|
|
// optimistically permit it during type checking but fail here
|
|
// once we note that the value isn't either "true" or "false".
|
|
gotTy := val.Type()
|
|
switch {
|
|
case marks.Contains(args[0], marks.Sensitive):
|
|
// Generic message so we won't inadvertently disclose
|
|
// information about sensitive values.
|
|
return cty.NilVal, function.NewArgErrorf(0, "cannot convert this sensitive %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint())
|
|
|
|
case gotTy == cty.String && wantTy == cty.Bool:
|
|
what := "string"
|
|
if !val.IsNull() {
|
|
what = strconv.Quote(val.AsString())
|
|
}
|
|
return cty.NilVal, function.NewArgErrorf(0, `cannot convert %s to bool; only the strings "true" or "false" are allowed`, what)
|
|
case gotTy == cty.String && wantTy == cty.Number:
|
|
what := "string"
|
|
if !val.IsNull() {
|
|
what = strconv.Quote(val.AsString())
|
|
}
|
|
return cty.NilVal, function.NewArgErrorf(0, `cannot convert %s to number; given string must be a decimal representation of a number`, what)
|
|
default:
|
|
return cty.NilVal, function.NewArgErrorf(0, "cannot convert %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint())
|
|
}
|
|
}
|
|
return ret, nil
|
|
},
|
|
})
|
|
}
|
|
|
|
// EphemeralAsNullFunc is a cty function that takes a value of any type and
|
|
// returns a similar value with any ephemeral-marked values anywhere in the
|
|
// structure replaced with a null value of the same type that is not marked
|
|
// as ephemeral.
|
|
//
|
|
// This is intended as a convenience for returning the non-ephemeral parts of
|
|
// a partially-ephemeral data structure through an output value that isn't
|
|
// ephemeral itself.
|
|
var EphemeralAsNullFunc = function.New(&function.Spec{
|
|
Params: []function.Parameter{
|
|
{
|
|
Name: "value",
|
|
Type: cty.DynamicPseudoType,
|
|
AllowDynamicType: true,
|
|
AllowUnknown: true,
|
|
AllowNull: true,
|
|
AllowMarked: true,
|
|
},
|
|
},
|
|
Type: func(args []cty.Value) (cty.Type, error) {
|
|
// This function always preserves the type of the given argument.
|
|
return args[0].Type(), nil
|
|
},
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
return ephemeral.RemoveEphemeralValues(args[0]), nil
|
|
},
|
|
})
|
|
|
|
func EphemeralAsNull(input cty.Value) (cty.Value, error) {
|
|
return EphemeralAsNullFunc.Call([]cty.Value{input})
|
|
}
|
|
|
|
// TypeFunc returns an encapsulated value containing its argument's type. This
|
|
// value is marked to allow us to limit the use of this function at the moment
|
|
// to only a few supported use cases.
|
|
var TypeFunc = function.New(&function.Spec{
|
|
Params: []function.Parameter{
|
|
{
|
|
Name: "value",
|
|
Type: cty.DynamicPseudoType,
|
|
AllowDynamicType: true,
|
|
AllowUnknown: true,
|
|
AllowNull: true,
|
|
},
|
|
},
|
|
Type: function.StaticReturnType(types.TypeType),
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
givenType := args[0].Type()
|
|
return cty.CapsuleVal(types.TypeType, &givenType).Mark(marks.TypeType), nil
|
|
},
|
|
})
|
|
|
|
func Type(input []cty.Value) (cty.Value, error) {
|
|
return TypeFunc.Call(input)
|
|
}
|
|
|
|
// ConvertFunc is a cty function which takes any value as the first argument,
|
|
// and returns the result of converting the first argument to the type
|
|
// constraint literal given as the second argument. We allow type constraint
|
|
// literals by injecting a custom decoder into HCL using a cty capsule type.
|
|
var ConvertFunc = makeConvertFunc()
|
|
|
|
// makeConvertFunc is a constructor function because of the unusual method we
|
|
// have for passing a custom decoder into HCL. We need to be able to declare a
|
|
// recursive closure that can return the same value that it's assigned to, hence
|
|
// there needs some procedural code to construct it.
|
|
func makeConvertFunc() function.Function {
|
|
// We want to be able to use optional and default values in our type
|
|
// constrains, so we need to be able to track both the type and the default
|
|
// values.
|
|
type typeConstraintArg struct {
|
|
Type cty.Type
|
|
Defaults *typeexpr.Defaults
|
|
}
|
|
|
|
var typeConstraintType cty.Type
|
|
typeConstraintType = cty.CapsuleWithOps("type_constraint", reflect.TypeFor[typeConstraintArg](), &cty.CapsuleOps{
|
|
ExtensionData: func(key any) any {
|
|
switch key {
|
|
// HCL will look for a capsule with CustomExpressionDecoder when
|
|
// decoding function arguments, and then insert this decoder
|
|
// allowing us to use our standard type expression syntax.
|
|
case customdecode.CustomExpressionDecoder:
|
|
return customdecode.CustomExpressionDecoderFunc(
|
|
func(expr hcl.Expression, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
ty, defs, diags := typeexpr.TypeConstraintWithDefaults(expr)
|
|
if diags.HasErrors() {
|
|
return cty.NilVal, diags
|
|
}
|
|
return cty.CapsuleVal(typeConstraintType, &typeConstraintArg{Type: ty, Defaults: defs}), nil
|
|
},
|
|
)
|
|
default:
|
|
return nil
|
|
}
|
|
},
|
|
TypeGoString: func(_ reflect.Type) string {
|
|
return "typeConstraint"
|
|
},
|
|
GoString: func(raw any) string {
|
|
tyPtr := raw.(*typeConstraintArg)
|
|
// The GoString value from our constraint will suffice here.
|
|
return fmt.Sprintf("typeConstraint(%#v)", tyPtr.Type)
|
|
},
|
|
})
|
|
|
|
return function.New(&function.Spec{
|
|
Params: []function.Parameter{
|
|
{
|
|
Name: "value",
|
|
Type: cty.DynamicPseudoType,
|
|
AllowNull: true,
|
|
AllowDynamicType: true,
|
|
},
|
|
{
|
|
Name: "type",
|
|
Type: typeConstraintType,
|
|
},
|
|
},
|
|
Type: func(args []cty.Value) (cty.Type, error) {
|
|
constraint := args[1].EncapsulatedValue().(*typeConstraintArg)
|
|
// optional attributes are only used during the conversion process,
|
|
// the final type must be fully defined.
|
|
return constraint.Type.WithoutOptionalAttributesDeep(), nil
|
|
},
|
|
Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
|
|
// the retType parameter tells us the final type, but it does not
|
|
// contain optional attributes or defaults, so we need to extract
|
|
// our typeConstraintArg from the arguments again.
|
|
constraint := args[1].EncapsulatedValue().(*typeConstraintArg)
|
|
v, err := convert.Convert(args[0], constraint.Type)
|
|
if err != nil {
|
|
return cty.NilVal, function.NewArgError(0, err)
|
|
}
|
|
if constraint.Defaults != nil {
|
|
v = constraint.Defaults.Apply(v)
|
|
}
|
|
|
|
return v, nil
|
|
},
|
|
})
|
|
}
|