diff --git a/packer/core.go b/packer/core.go index c32c7b318..a3f33a816 100644 --- a/packer/core.go +++ b/packer/core.go @@ -397,12 +397,17 @@ func (c *Core) init() error { allVariables[k] = v } + // 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 // First, loop over the variables in the template for k, v := range allVariables { // Interpolate the default - renderedV, err := interpolate.Render(v, ctx) + renderedV, err := interpolate.RenderRegex(v, ctx, renderFilter) switch err.(type) { case nil: // We only get here if interpolation has succeeded, so something is diff --git a/packer/core_test.go b/packer/core_test.go index bf65f0ffa..90f59de1d 100644 --- a/packer/core_test.go +++ b/packer/core_test.go @@ -143,6 +143,36 @@ func TestCoreBuild_env(t *testing.T) { } } +func TestCoreBuild_IgnoreTemplateVariables(t *testing.T) { + os.Setenv("PACKER_TEST_ENV", "test") + defer os.Setenv("PACKER_TEST_ENV", "") + + config := TestCoreConfig(t) + testCoreTemplate(t, config, fixtureDir("build-ignore-template-variable.json")) + core := TestCore(t, config) + + if core.variables["http_ip"] != "{{ .HTTPIP }}" { + t.Fatalf("bad: User variable http_ip={{ .HTTPIP }} should not be interpolated") + } + + if core.variables["var"] != "test_{{ .PACKER_TEST_TEMP }}" { + t.Fatalf("bad: User variable var should be half interpolated to var=test_{{ .PACKER_TEST_TEMP }} but was var=%s", core.variables["var"]) + } + + if core.variables["array_var"] != "us-west-1,us-west-2" { + t.Fatalf("bad: User variable array_var should be \"us-west-1,us-west-2\" but was %s", core.variables["var"]) + } + + build, err := core.Build("test") + if err != nil { + t.Fatalf("err: %s", err) + } + + if _, err := build.Prepare(); err != nil { + t.Fatalf("err: %s", err) + } +} + func TestCoreBuild_buildNameVar(t *testing.T) { config := TestCoreConfig(t) testCoreTemplate(t, config, fixtureDir("build-var-build-name.json")) diff --git a/packer/test-fixtures/build-ignore-template-variable.json b/packer/test-fixtures/build-ignore-template-variable.json new file mode 100644 index 000000000..b835937ff --- /dev/null +++ b/packer/test-fixtures/build-ignore-template-variable.json @@ -0,0 +1,11 @@ +{ + "variables": { + "var": "{{env `PACKER_TEST_ENV`}}_{{ .PACKER_TEST_TEMP }}", + "http_ip": "{{ .HTTPIP }}", + "array_var": "us-west-1,us-west-2" + }, + + "builders": [{ + "type": "test" + }] +} diff --git a/template/interpolate/i.go b/template/interpolate/i.go index f610611c6..9dcdc4ba8 100644 --- a/template/interpolate/i.go +++ b/template/interpolate/i.go @@ -2,6 +2,9 @@ package interpolate import ( "bytes" + "github.com/google/uuid" + "regexp" + "strings" "text/template" ) @@ -42,8 +45,44 @@ func NewContext() *Context { } // Render is shorthand for constructing an I and calling Render. -func Render(v string, ctx *Context) (string, error) { - return (&I{Value: v}).Render(ctx) +func Render(v string, ctx *Context) (rendered string, err error) { + // Keep interpolating until all variables are done + // Sometimes a variable can been inside another one + for { + rendered, err = (&I{Value: v}).Render(ctx) + if err != nil || rendered == v { + break + } + v = rendered + } + return +} + +// Render is shorthand for constructing an I and calling Render. +// Use regex to filter variables that are not supposed to be interpolated now +func RenderRegex(v string, ctx *Context, regex string) (string, error) { + re := regexp.MustCompile(regex) + matches := re.FindAllStringSubmatch(v, -1) + + // Replace variables to be excluded with a unique UUID + excluded := make(map[string]string) + for _, value := range matches { + id := uuid.New().String() + excluded[id] = value[0] + v = strings.ReplaceAll(v, value[0], id) + } + + rendered, err := (&I{Value: v}).Render(ctx) + if err != nil { + return rendered, err + } + + // Replace back by the UUID the previously excluded values + for id, value := range excluded { + rendered = strings.ReplaceAll(rendered, id, value) + } + + return rendered, nil } // Validate is shorthand for constructing an I and calling Validate.