mirror of
https://github.com/hashicorp/packer.git
synced 2026-09-21 15:31:41 -04:00
In order to be able to change how templates are processed later on, we move the logic that processes templates to a new abstraction: Scheduler. Right now, one implementation of Scheduler exists: sequential. This is essentially the same approach as what we have now embedded in the configs, but is now moved to this structure, so we can change it later.
205 lines
5.9 KiB
Go
205 lines
5.9 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 {
|
|
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
|
|
}
|