package interpolate import ( "errors" "fmt" "os" "path/filepath" "strconv" "strings" "text/template" "time" consulapi "github.com/hashicorp/consul/api" commontpl "github.com/hashicorp/packer/common/template" "github.com/hashicorp/packer/common/uuid" "github.com/hashicorp/packer/helper/common" awssmapi "github.com/hashicorp/packer/template/interpolate/aws/secretsmanager" "github.com/hashicorp/packer/version" strftime "github.com/jehiah/go-strftime" ) // InitTime is the UTC time when this package was initialized. It is // used as the timestamp for all configuration templates so that they // match for a single build. var InitTime time.Time func init() { InitTime = time.Now().UTC() } // Funcs are the interpolation funcs that are available within interpolations. var FuncGens = map[string]interface{}{ "build_name": funcGenBuildName, "build_type": funcGenBuildType, "env": funcGenEnv, "isotime": funcGenIsotime, "strftime": funcGenStrftime, "pwd": funcGenPwd, "split": funcGenSplitter, "template_dir": funcGenTemplateDir, "timestamp": funcGenTimestamp, "uuid": funcGenUuid, "user": funcGenUser, "packer_version": funcGenPackerVersion, "consul_key": funcGenConsul, "vault": funcGenVault, "sed": funcGenSed, "build": funcGenBuild, "aws_secretsmanager": funcGenAwsSecrets, "replace": replace, "replace_all": replace_all, "upper": strings.ToUpper, "lower": strings.ToLower, } var ErrVariableNotSetString = "Error: variable not set:" // FuncGenerator is a function that given a context generates a template // function for the template. type FuncGenerator func(*Context) interface{} // Funcs returns the functions that can be used for interpolation given // a context. func Funcs(ctx *Context) template.FuncMap { result := make(map[string]interface{}) for k, v := range FuncGens { switch v := v.(type) { case func(*Context) interface{}: result[k] = v(ctx) default: result[k] = v } } if ctx != nil { for k, v := range ctx.Funcs { result[k] = v } } return template.FuncMap(result) } func funcGenSplitter(ctx *Context) interface{} { return func(k string, s string, i int) (string, error) { // return func(s string) (string, error) { split := strings.Split(k, s) if len(split) <= i { return "", fmt.Errorf("the substring %d was unavailable using the separator value, %s, only %d values were found", i, s, len(split)) } return split[i], nil } } func funcGenBuildName(ctx *Context) interface{} { return func() (string, error) { if ctx == nil || ctx.BuildName == "" { return "", errors.New("build_name not available") } return ctx.BuildName, nil } } func funcGenBuildType(ctx *Context) interface{} { return func() (string, error) { if ctx == nil || ctx.BuildType == "" { return "", errors.New("build_type not available") } return ctx.BuildType, nil } } func funcGenEnv(ctx *Context) interface{} { return func(k string) (string, error) { if !ctx.EnableEnv { // The error message doesn't have to be that detailed since // semantic checks should catch this. return "", errors.New("env vars are not allowed here") } return os.Getenv(k), nil } } func funcGenIsotime(ctx *Context) interface{} { return func(format ...string) (string, error) { if len(format) == 0 { return InitTime.Format(time.RFC3339), nil } if len(format) > 1 { return "", fmt.Errorf("too many values, 1 needed: %v", format) } return InitTime.Format(format[0]), nil } } func funcGenStrftime(ctx *Context) interface{} { return func(format string) string { return strftime.Format(format, InitTime) } } func funcGenPwd(ctx *Context) interface{} { return func() (string, error) { return os.Getwd() } } func funcGenTemplateDir(ctx *Context) interface{} { return func() (string, error) { if ctx == nil || ctx.TemplatePath == "" { return "", errors.New("template path not available") } path, err := filepath.Abs(filepath.Dir(ctx.TemplatePath)) if err != nil { return "", err } return path, nil } } func passthroughOrInterpolate(data map[interface{}]interface{}, s string) (string, error) { if heldPlace, ok := data[s]; ok { if hp, ok := heldPlace.(string); ok { // If we're in the first interpolation pass, the goal is to // make sure that we pass the value through. // TODO match against an actual string constant if strings.Contains(hp, common.PlaceholderMsg) { return fmt.Sprintf("{{.%s}}", s), nil } else { return hp, nil } } } return "", fmt.Errorf("loaded data, but couldnt find %s in it.", s) } func funcGenBuild(ctx *Context) interface{} { // Depending on where the context data is coming from, it could take a few // different map types. The following switch standardizes the map types // so we can act on them correctly. return func(s string) (string, error) { switch data := ctx.Data.(type) { case map[interface{}]interface{}: return passthroughOrInterpolate(data, s) case map[string]interface{}: // convert to a map[interface{}]interface{} so we can use same // parsing on it passed := make(map[interface{}]interface{}, len(data)) for k, v := range data { passed[k] = v } return passthroughOrInterpolate(passed, s) case map[string]string: // convert to a map[interface{}]interface{} so we can use same // parsing on it passed := make(map[interface{}]interface{}, len(data)) for k, v := range data { passed[k] = v } return passthroughOrInterpolate(passed, s) default: return "", fmt.Errorf("Error validating build variable: the given "+ "variable %s will not be passed into your plugin.", s) } } } func funcGenTimestamp(ctx *Context) interface{} { return func() string { return strconv.FormatInt(InitTime.Unix(), 10) } } func funcGenUser(ctx *Context) interface{} { return func(k string) (string, error) { if ctx == nil || ctx.UserVariables == nil { return "", errors.New("test") } val, ok := ctx.UserVariables[k] if ctx.EnableEnv { // error and retry if we're interpolating UserVariables. But if // we're elsewhere in the template, just return the empty string. if !ok { return "", fmt.Errorf("%s %s", ErrVariableNotSetString, k) } } return val, nil } } func funcGenUuid(ctx *Context) interface{} { return func() string { return uuid.TimeOrderedUUID() } } func funcGenPackerVersion(ctx *Context) interface{} { return func() string { return version.FormattedVersion() } } func funcGenConsul(ctx *Context) interface{} { return func(k string) (string, error) { if !ctx.EnableEnv { // The error message doesn't have to be that detailed since // semantic checks should catch this. return "", errors.New("consul_key is not allowed here") } consulConfig := consulapi.DefaultConfig() client, err := consulapi.NewClient(consulConfig) if err != nil { return "", fmt.Errorf("error getting consul client: %s", err) } q := &consulapi.QueryOptions{} kv, _, err := client.KV().Get(k, q) if err != nil { return "", fmt.Errorf("error reading consul key: %s", err) } if kv == nil { return "", fmt.Errorf("key does not exist at the given path: %s", k) } value := string(kv.Value) if value == "" { return "", fmt.Errorf("value is empty at path %s", k) } return value, nil } } func funcGenVault(ctx *Context) interface{} { return func(path string, key string) (string, error) { // Only allow interpolation from Vault when env vars are being read. if !ctx.EnableEnv { // The error message doesn't have to be that detailed since // semantic checks should catch this. return "", errors.New("Vault vars are only allowed in the variables section") } return commontpl.Vault(path, key) } } func funcGenAwsSecrets(ctx *Context) interface{} { return func(secret ...string) (string, error) { if !ctx.EnableEnv { // The error message doesn't have to be that detailed since // semantic checks should catch this. return "", errors.New("AWS Secrets Manager vars are only allowed in the variables section") } // Check if at least 1 parameter has been used if len(secret) == 0 { return "", errors.New("At least one parameter must be used") } // client uses AWS SDK CredentialChain method. So,credentials can // be loaded from credential file, environment variables, or IAM // roles. client := awssmapi.New( &awssmapi.AWSConfig{}, ) var name, key string name = secret[0] // key is optional if not used we fetch the first // value stored in given secret. If more than two parameters // are passed we take second param and ignore the others if len(secret) > 1 { key = secret[1] } spec := &awssmapi.SecretSpec{ Name: name, Key: key, } s, err := client.GetSecret(spec) if err != nil { return "", fmt.Errorf("Error getting secret: %s", err) } return s, nil } } func funcGenSed(ctx *Context) interface{} { return func(expression string, inputString string) (string, error) { return "", errors.New("template function `sed` is deprecated " + "use `replace` or `replace_all` instead." + "Documentation: https://www.packer.io/docs/templates/engine") } } func replace_all(old, new, src string) string { return strings.ReplaceAll(src, old, new) } func replace(old, new string, n int, src string) string { return strings.Replace(src, old, new, n) }