Files

216 lines
6.1 KiB
Go

package json
import (
"fmt"
"regexp"
"sort"
"strings"
"text/template"
"github.com/hashicorp/hcl/v2"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
)
func isDoneInterpolating(v string) (bool, error) {
// Check for whether the var contains any more references to `user`, wrapped
// in interpolation syntax.
filter := `{{\s*user\s*\x60.*\x60\s*}}`
matched, err := regexp.MatchString(filter, v)
if err != nil {
return false, fmt.Errorf("Can't tell if interpolation is done: %s", err)
}
if matched {
// not done interpolating; there's still a call to "user" in a template
// engine
return false, nil
}
// No more calls to "user" as a template engine, so we're done.
return true, nil
}
func (s *JSONSequentialScheduler) renderVarsRecursively() (*interpolate.Context, error) {
ctx := s.config.Context()
ctx.EnableEnv = true
ctx.UserVariables = make(map[string]string)
shouldRetry := true
changed := false
failedInterpolation := ""
// Why this giant loop? User variables can be recursively defined. For
// example:
// "variables": {
// "foo": "bar",
// "baz": "{{user `foo`}}baz",
// "bang": "bang{{user `baz`}}"
// },
// In this situation, we cannot guarantee that we've added "foo" to
// UserVariables before we try to interpolate "baz" the first time. We need
// to have the option to loop back over in order to add the properly
// interpolated "baz" to the UserVariables map.
// Likewise, we'd need to loop up to two times to properly add "bang",
// since that depends on "baz" being set, which depends on "foo" being set.
// We break out of the while loop either if all our variables have been
// interpolated or if after 100 loops we still haven't succeeded in
// interpolating them. Please don't actually nest your variables in 100
// layers of other variables. Please.
// c.Template.Variables is populated by variables defined within the Template
// itself
// c.variables is populated by variables read in from the command line and
// var-files.
// We need to read the keys from both, then loop over all of them to figure
// out the appropriate interpolations.
repeatMap := make(map[string]string)
allKeys := make([]string, 0)
// load in template variables
for k, v := range s.config.Template.Variables {
repeatMap[k] = v.Default
allKeys = append(allKeys, k)
}
// overwrite template variables with command-line-read variables
for k, v := range s.config.Variables {
repeatMap[k] = v
allKeys = append(allKeys, k)
}
// sort map to force the following loop to be deterministic.
sort.Strings(allKeys)
type keyValue struct {
Key string
Value string
}
sortedMap := make([]keyValue, len(repeatMap))
for _, k := range allKeys {
sortedMap = append(sortedMap, keyValue{k, repeatMap[k]})
}
// Regex to exclude any build function variable or template variable
// from interpolating earlier
// E.g.: {{ .HTTPIP }} won't interpolate now
renderFilter := "{{(\\s|)\\.(.*?)(\\s|)}}"
for i := 0; i < 100; i++ {
shouldRetry = false
changed = false
deleteKeys := []string{}
// First, loop over the variables in the template
for _, kv := range sortedMap {
// Interpolate the default
renderedV, err := interpolate.RenderRegex(kv.Value, ctx, renderFilter)
switch err.(type) {
case nil:
// We only get here if interpolation has succeeded, so something is
// different in this loop than in the last one.
changed = true
s.config.Variables[kv.Key] = renderedV
ctx.UserVariables = s.config.Variables
// Remove fully-interpolated variables from the map, and flag
// variables that still need interpolating for a repeat.
done, err := isDoneInterpolating(kv.Value)
if err != nil {
return ctx, err
}
if done {
deleteKeys = append(deleteKeys, kv.Key)
} else {
shouldRetry = true
}
case template.ExecError:
castError := err.(template.ExecError)
if strings.Contains(castError.Error(), interpolate.ErrVariableNotSetString) {
shouldRetry = true
failedInterpolation = fmt.Sprintf(`"%s": "%s"; error: %s`, kv.Key, kv.Value, err)
} else {
return ctx, err
}
default:
return ctx, fmt.Errorf(
// unexpected interpolation error: abort the run
"error interpolating default value for '%s': %s",
kv.Key, err)
}
}
if !shouldRetry {
break
}
// Clear completed vars from sortedMap before next loop. Do this one
// key at a time because the indices are gonna change ever time you
// delete from the map.
for _, k := range deleteKeys {
for ind, kv := range sortedMap {
if kv.Key == k {
sortedMap = append(sortedMap[:ind], sortedMap[ind+1:]...)
break
}
}
}
deleteKeys = []string{}
}
if !changed && shouldRetry {
return ctx, fmt.Errorf("Failed to interpolate %s: Please make sure that "+
"the variable you're referencing has been defined; Packer treats "+
"all variables used to interpolate other user variables as "+
"required.", failedInterpolation)
}
return ctx, nil
}
func (s *JSONSequentialScheduler) getEvaluatedVariables() ([]string, error) {
if s.config.Variables == nil {
s.config.Variables = make(map[string]string)
}
// Go through the variables and interpolate the environment and
// user variables
ctx, err := s.renderVarsRecursively()
if err != nil {
return nil, err
}
secrets := []string{}
for _, v := range s.config.Template.SensitiveVariables {
secret := ctx.UserVariables[v.Key]
secrets = append(secrets, secret)
}
return secrets, nil
}
func (s *JSONSequentialScheduler) EvaluateVariables() hcl.Diagnostics {
err := s.config.Validate()
if err != nil {
return hcl.Diagnostics{
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid template",
Detail: err.Error(),
},
}
}
secrets, err := s.getEvaluatedVariables()
if err != nil {
return hcl.Diagnostics{
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Failed to evaluate variables",
Detail: err.Error(),
},
}
}
for _, secret := range secrets {
packersdk.LogSecretFilter.Set(secret)
}
return nil
}