mirror of
https://github.com/hashicorp/packer.git
synced 2026-02-18 18:18:11 -05:00
236 lines
9.9 KiB
Go
236 lines
9.9 KiB
Go
// Copyright IBM Corp. 2013, 2025
|
||
// SPDX-License-Identifier: BUSL-1.1
|
||
|
||
package hcl2template
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/base64"
|
||
|
||
"github.com/hashicorp/go-cty-funcs/cidr"
|
||
"github.com/hashicorp/go-cty-funcs/collection"
|
||
"github.com/hashicorp/go-cty-funcs/crypto"
|
||
"github.com/hashicorp/go-cty-funcs/encoding"
|
||
"github.com/hashicorp/go-cty-funcs/filesystem"
|
||
"github.com/hashicorp/go-cty-funcs/uuid"
|
||
"github.com/hashicorp/hcl/v2/ext/tryfunc"
|
||
"github.com/hashicorp/hcl/v2/ext/typeexpr"
|
||
pkrfunction "github.com/hashicorp/packer/hcl2template/function"
|
||
ctyyaml "github.com/zclconf/go-cty-yaml"
|
||
"github.com/zclconf/go-cty/cty"
|
||
"github.com/zclconf/go-cty/cty/function"
|
||
"github.com/zclconf/go-cty/cty/function/stdlib"
|
||
"golang.org/x/text/encoding/ianaindex"
|
||
)
|
||
|
||
// Functions returns the set of functions that should be used to when
|
||
// evaluating expressions in the receiving scope.
|
||
//
|
||
// basedir is used with file functions and allows a user to reference a file
|
||
// using local path. Usually basedir is the directory in which the config file
|
||
// is located
|
||
func Functions(basedir string) map[string]function.Function {
|
||
|
||
funcs := map[string]function.Function{
|
||
"abs": stdlib.AbsoluteFunc,
|
||
"abspath": filesystem.AbsPathFunc,
|
||
"alltrue": pkrfunction.AllTrue,
|
||
"anytrue": pkrfunction.AnyTrue,
|
||
"aws_secretsmanager": pkrfunction.AWSSecret,
|
||
"aws_secretsmanager_raw": pkrfunction.AWSSecretRaw,
|
||
"basename": filesystem.BasenameFunc,
|
||
"base64decode": encoding.Base64DecodeFunc,
|
||
"base64encode": encoding.Base64EncodeFunc,
|
||
"base64gzip": pkrfunction.Base64GzipFunc,
|
||
"bcrypt": crypto.BcryptFunc,
|
||
"can": tryfunc.CanFunc,
|
||
"ceil": stdlib.CeilFunc,
|
||
"chomp": stdlib.ChompFunc,
|
||
"chunklist": stdlib.ChunklistFunc,
|
||
"cidrhost": cidr.HostFunc,
|
||
"cidrnetmask": cidr.NetmaskFunc,
|
||
"cidrsubnet": cidr.SubnetFunc,
|
||
"cidrsubnets": cidr.SubnetsFunc,
|
||
"coalesce": collection.CoalesceFunc,
|
||
"coalescelist": stdlib.CoalesceListFunc,
|
||
"compact": stdlib.CompactFunc,
|
||
"concat": stdlib.ConcatFunc,
|
||
"consul_key": pkrfunction.ConsulFunc,
|
||
"contains": stdlib.ContainsFunc,
|
||
"convert": typeexpr.ConvertFunc,
|
||
"csvdecode": stdlib.CSVDecodeFunc,
|
||
"dirname": filesystem.DirnameFunc,
|
||
"distinct": stdlib.DistinctFunc,
|
||
"element": stdlib.ElementFunc,
|
||
"endswith": pkrfunction.EndsWithFunc,
|
||
"file": filesystem.MakeFileFunc(basedir, false),
|
||
"filebase64": pkrfunction.Filebase64,
|
||
"fileexists": filesystem.MakeFileExistsFunc(basedir),
|
||
"fileset": filesystem.MakeFileSetFunc(basedir),
|
||
"flatten": stdlib.FlattenFunc,
|
||
"floor": stdlib.FloorFunc,
|
||
"format": stdlib.FormatFunc,
|
||
"formatdate": stdlib.FormatDateFunc,
|
||
"formatlist": stdlib.FormatListFunc,
|
||
"indent": stdlib.IndentFunc,
|
||
"index": pkrfunction.IndexFunc, // stdlib.IndexFunc is not compatible
|
||
"join": stdlib.JoinFunc,
|
||
"jsondecode": stdlib.JSONDecodeFunc,
|
||
"jsonencode": stdlib.JSONEncodeFunc,
|
||
"keys": stdlib.KeysFunc,
|
||
"legacy_isotime": pkrfunction.LegacyIsotimeFunc,
|
||
"legacy_strftime": pkrfunction.LegacyStrftimeFunc,
|
||
"length": pkrfunction.LengthFunc,
|
||
"log": stdlib.LogFunc,
|
||
"lookup": stdlib.LookupFunc,
|
||
"lower": stdlib.LowerFunc,
|
||
"max": stdlib.MaxFunc,
|
||
"md5": crypto.Md5Func,
|
||
"merge": stdlib.MergeFunc,
|
||
"min": stdlib.MinFunc,
|
||
"parseint": stdlib.ParseIntFunc,
|
||
"pathexpand": filesystem.PathExpandFunc,
|
||
"pow": stdlib.PowFunc,
|
||
"range": stdlib.RangeFunc,
|
||
"reverse": stdlib.ReverseListFunc,
|
||
"replace": stdlib.ReplaceFunc,
|
||
"regex": stdlib.RegexFunc,
|
||
"regexall": stdlib.RegexAllFunc,
|
||
"regex_replace": stdlib.RegexReplaceFunc,
|
||
"rsadecrypt": crypto.RsaDecryptFunc,
|
||
"setintersection": stdlib.SetIntersectionFunc,
|
||
"setproduct": stdlib.SetProductFunc,
|
||
"setunion": stdlib.SetUnionFunc,
|
||
"sha1": crypto.Sha1Func,
|
||
"sha256": crypto.Sha256Func,
|
||
"sha512": crypto.Sha512Func,
|
||
"signum": stdlib.SignumFunc,
|
||
"slice": stdlib.SliceFunc,
|
||
"sort": stdlib.SortFunc,
|
||
"split": stdlib.SplitFunc,
|
||
"startswith": pkrfunction.StartsWithFunc,
|
||
"strcontains": pkrfunction.StrContains,
|
||
"strrev": stdlib.ReverseFunc,
|
||
"substr": stdlib.SubstrFunc,
|
||
"sum": pkrfunction.SumFunc,
|
||
"textdecodebase64": TextDecodeBase64Func,
|
||
"textencodebase64": TextEncodeBase64Func,
|
||
"timestamp": pkrfunction.TimestampFunc,
|
||
"timeadd": stdlib.TimeAddFunc,
|
||
"title": stdlib.TitleFunc,
|
||
"trim": stdlib.TrimFunc,
|
||
"trimprefix": stdlib.TrimPrefixFunc,
|
||
"trimspace": stdlib.TrimSpaceFunc,
|
||
"trimsuffix": stdlib.TrimSuffixFunc,
|
||
"try": tryfunc.TryFunc,
|
||
"upper": stdlib.UpperFunc,
|
||
"urlencode": encoding.URLEncodeFunc,
|
||
"uuidv4": uuid.V4Func,
|
||
"uuidv5": uuid.V5Func,
|
||
"values": stdlib.ValuesFunc,
|
||
"vault": pkrfunction.VaultFunc,
|
||
"yamldecode": ctyyaml.YAMLDecodeFunc,
|
||
"yamlencode": ctyyaml.YAMLEncodeFunc,
|
||
"zipmap": stdlib.ZipmapFunc,
|
||
}
|
||
|
||
funcs["templatefile"] = pkrfunction.MakeTemplateFileFunc(basedir, func() map[string]function.Function {
|
||
// The templatefile function prevents recursive calls to itself
|
||
// by copying this map and overwriting the "templatefile" entry.
|
||
return funcs
|
||
})
|
||
|
||
return funcs
|
||
}
|
||
|
||
// TextEncodeBase64Func constructs a function that encodes a string to a target encoding and then to a base64 sequence.
|
||
var TextEncodeBase64Func = function.New(&function.Spec{
|
||
Params: []function.Parameter{
|
||
{
|
||
Name: "string",
|
||
Type: cty.String,
|
||
},
|
||
{
|
||
Name: "encoding",
|
||
Type: cty.String,
|
||
},
|
||
},
|
||
Description: "Encodes the input string (UTF-8) to the destination encoding. The output is base64 to account for cty limiting strings to NFC normalised UTF-8 strings.",
|
||
Type: function.StaticReturnType(cty.String),
|
||
RefineResult: func(rb *cty.RefinementBuilder) *cty.RefinementBuilder { return rb.NotNull() },
|
||
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||
encoding, err := ianaindex.IANA.Encoding(args[1].AsString())
|
||
if err != nil || encoding == nil {
|
||
return cty.UnknownVal(cty.String), function.NewArgErrorf(1, "%q is not a supported IANA encoding name or alias", args[1].AsString())
|
||
}
|
||
|
||
encName, err := ianaindex.IANA.Name(encoding)
|
||
if err != nil { // would be weird, since we just read this encoding out
|
||
encName = args[1].AsString()
|
||
}
|
||
|
||
encoder := encoding.NewEncoder()
|
||
encodedInput, err := encoder.Bytes([]byte(args[0].AsString()))
|
||
if err != nil {
|
||
// The string representations of "err" disclose implementation
|
||
// details of the underlying library, and the main error we might
|
||
// like to return a special message for is unexported as
|
||
// golang.org/x/text/encoding/internal.RepertoireError, so this
|
||
// is just a generic error message for now.
|
||
//
|
||
// We also don't include the string itself in the message because
|
||
// it can typically be very large, contain newline characters,
|
||
// etc.
|
||
return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given string contains characters that cannot be represented in %s", encName)
|
||
}
|
||
|
||
return cty.StringVal(base64.StdEncoding.EncodeToString(encodedInput)), nil
|
||
},
|
||
})
|
||
|
||
// TextDecodeBase64Func constructs a function that decodes a base64 sequence from the source encoding to UTF-8.
|
||
var TextDecodeBase64Func = function.New(&function.Spec{
|
||
Params: []function.Parameter{
|
||
{
|
||
Name: "source",
|
||
Type: cty.String,
|
||
},
|
||
{
|
||
Name: "encoding",
|
||
Type: cty.String,
|
||
},
|
||
},
|
||
Type: function.StaticReturnType(cty.String),
|
||
Description: "Encodes the input base64 blob from an encoding to utf-8. The input is base64 to account for cty limiting strings to NFC normalised UTF-8 strings.",
|
||
RefineResult: func(rb *cty.RefinementBuilder) *cty.RefinementBuilder { return rb.NotNull() },
|
||
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||
encoding, err := ianaindex.IANA.Encoding(args[1].AsString())
|
||
if err != nil || encoding == nil {
|
||
return cty.UnknownVal(cty.String), function.NewArgErrorf(1, "%q is not a supported IANA encoding name or alias", args[1].AsString())
|
||
}
|
||
|
||
encName, err := ianaindex.IANA.Name(encoding)
|
||
if err != nil { // would be weird, since we just read this encoding out
|
||
encName = args[1].AsString()
|
||
}
|
||
|
||
s := args[0].AsString()
|
||
sDec, err := base64.StdEncoding.DecodeString(s)
|
||
if err != nil {
|
||
switch err := err.(type) {
|
||
case base64.CorruptInputError:
|
||
return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given value is has an invalid base64 symbol at offset %d", int(err))
|
||
default:
|
||
return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "invalid source string: %w", err)
|
||
}
|
||
}
|
||
|
||
decoder := encoding.NewDecoder()
|
||
decoded, err := decoder.Bytes(sDec)
|
||
if err != nil || bytes.ContainsRune(decoded, '<27>') {
|
||
return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given string contains symbols that are not defined for %s", encName)
|
||
}
|
||
|
||
return cty.StringVal(string(decoded)), nil
|
||
},
|
||
})
|