diff --git a/builder/amazon/common/ami_config.go b/builder/amazon/common/ami_config.go index 1c00eee4d..3e858d1f3 100644 --- a/builder/amazon/common/ami_config.go +++ b/builder/amazon/common/ami_config.go @@ -101,7 +101,7 @@ func (c *AMIConfig) Prepare(accessConfig *AccessConfig, ctx *interpolate.Context errs = append(errs, fmt.Errorf("AMIName should only contain "+ "alphanumeric characters, parentheses (()), square brackets ([]), spaces "+ "( ), periods (.), slashes (/), dashes (-), single quotes ('), at-signs "+ - "(@), or underscores(_). You can use the `clean_ami_name` template "+ + "(@), or underscores(_). You can use the `clean_resource_name` template "+ "filter to automatically clean your ami name.")) } diff --git a/builder/amazon/common/template_funcs.go b/builder/amazon/common/template_funcs.go index 7a0998b34..246a288e0 100644 --- a/builder/amazon/common/template_funcs.go +++ b/builder/amazon/common/template_funcs.go @@ -3,6 +3,8 @@ package common import ( "bytes" "text/template" + + packertpl "github.com/hashicorp/packer/common/template" ) func isalphanumeric(b byte) bool { @@ -36,5 +38,6 @@ func templateCleanAMIName(s string) string { } var TemplateFuncs = template.FuncMap{ - "clean_ami_name": templateCleanAMIName, + "clean_resource_name": templateCleanAMIName, + "clean_ami_name": packertpl.DeprecatedTemplateFunc("clean_ami_name", "clean_resource_name", templateCleanAMIName), } diff --git a/builder/azure/arm/template_funcs.go b/builder/azure/arm/template_funcs.go index 7e0d6601f..1aa9546ab 100644 --- a/builder/azure/arm/template_funcs.go +++ b/builder/azure/arm/template_funcs.go @@ -3,6 +3,8 @@ package arm import ( "bytes" "text/template" + + packertpl "github.com/hashicorp/packer/common/template" ) func isValidByteValue(b byte) bool { @@ -39,5 +41,6 @@ func templateCleanImageName(s string) string { } var TemplateFuncs = template.FuncMap{ - "clean_image_name": templateCleanImageName, + "clean_resource_name": templateCleanImageName, + "clean_image_name": packertpl.DeprecatedTemplateFunc("clean_image_name", "clean_resource_name", templateCleanImageName), } diff --git a/builder/googlecompute/template_funcs.go b/builder/googlecompute/template_funcs.go index 1e0c654c2..14d1c4b2f 100644 --- a/builder/googlecompute/template_funcs.go +++ b/builder/googlecompute/template_funcs.go @@ -3,6 +3,8 @@ package googlecompute import ( "strings" "text/template" + + packertpl "github.com/hashicorp/packer/common/template" ) func isalphanumeric(b byte) bool { @@ -34,5 +36,6 @@ func templateCleanImageName(s string) string { } var TemplateFuncs = template.FuncMap{ - "clean_image_name": templateCleanImageName, + "clean_resource_name": templateCleanImageName, + "clean_image_name": packertpl.DeprecatedTemplateFunc("clean_image_name", "clean_resource_name", templateCleanImageName), } diff --git a/common/template/funcs.go b/common/template/funcs.go new file mode 100644 index 000000000..bf1de23c6 --- /dev/null +++ b/common/template/funcs.go @@ -0,0 +1,19 @@ +package template + +import ( + "log" + "sync" +) + +// DeprecatedTemplateFunc wraps a template func to warn users that it's +// deprecated. The deprecation warning is called only once. +func DeprecatedTemplateFunc(funcName, useInstead string, deprecated func(string) string) func(string) string { + once := sync.Once{} + return func(in string) string { + once.Do(func() { + log.Printf("[WARN]: the `%s` template func is deprecated, please use %s instead", + funcName, useInstead) + }) + return deprecated(in) + } +} diff --git a/fix/fixer.go b/fix/fixer.go index 82c10aa9a..4bcbaac2f 100644 --- a/fix/fixer.go +++ b/fix/fixer.go @@ -42,6 +42,7 @@ func init() { "hyperv-vmxc-typo": new(FixerHypervVmxcTypo), "hyperv-cpu-and-ram": new(FizerHypervCPUandRAM), "vmware-compaction": new(FixerVMwareCompaction), + "clean-image-name": new(FixerCleanImageName), } FixerOrder = []string{ @@ -65,5 +66,6 @@ func init() { "powershell-escapes", "vmware-compaction", "hyperv-cpu-and-ram", + "clean-image-name", } } diff --git a/fix/fixer_clean_image_name.go b/fix/fixer_clean_image_name.go new file mode 100644 index 000000000..cbb85f894 --- /dev/null +++ b/fix/fixer_clean_image_name.go @@ -0,0 +1,62 @@ +package fix + +import ( + "fmt" + "regexp" + + "github.com/mitchellh/mapstructure" +) + +// FixerCleanImageName is a Fixer that replaces the "clean_(image|ami)_name" template +// calls with "clean_resource_name" +type FixerCleanImageName struct{} + +func (FixerCleanImageName) Fix(input map[string]interface{}) (map[string]interface{}, error) { + // Our template type we'll use for this fixer only + type template struct { + Builders []map[string]interface{} + } + + // Decode the input into our structure, if we can + var tpl template + if err := mapstructure.Decode(input, &tpl); err != nil { + return nil, err + } + + re := regexp.MustCompile(`clean_(image|ami)_name`) + + // Go through each builder and replace CreateTime if we can + for _, builder := range tpl.Builders { + for key, value := range builder { + switch v := value.(type) { + case string: + changed := re.ReplaceAllString(v, "clean_resource_name") + builder[key] = changed + case map[string]string: + for k := range v { + v[k] = re.ReplaceAllString(v[k], "clean_resource_name") + } + builder[key] = v + case map[string]interface{}: + for k := range v { + if s, ok := v[k].(string); ok { + v[k] = re.ReplaceAllString(s, "clean_resource_name") + } + } + builder[key] = v + default: + if key == "image_labels" { + + panic(fmt.Sprintf("value: %#v", value)) + } + } + } + } + + input["builders"] = tpl.Builders + return input, nil +} + +func (FixerCleanImageName) Synopsis() string { + return `Replaces /clean_(image|ami)_name/ in builder configs with "clean_resource_name"` +} diff --git a/fix/fixer_clean_image_name_test.go b/fix/fixer_clean_image_name_test.go new file mode 100644 index 000000000..21799ba5f --- /dev/null +++ b/fix/fixer_clean_image_name_test.go @@ -0,0 +1,52 @@ +package fix + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestFixerCleanImageName_Impl(t *testing.T) { + var raw interface{} + raw = new(FixerCleanImageName) + if _, ok := raw.(Fixer); !ok { + t.Fatalf("must be a Fixer") + } +} + +func TestFixerCleanImageName_Fix(t *testing.T) { + var f FixerCleanImageName + + input := map[string]interface{}{ + "builders": []interface{}{ + map[string]interface{}{ + "type": "foo", + "ami_name": "heyo clean_image_name", + "image_labels": map[string]interface{}{ + "name": "test-packer-{{packer_version | clean_image_name}}", + }, + }, + }, + } + + expected := map[string]interface{}{ + "builders": []map[string]interface{}{ + { + "type": "foo", + "ami_name": "heyo clean_resource_name", + "image_labels": map[string]interface{}{ + "name": "test-packer-{{packer_version | clean_resource_name}}", + }, + }, + }, + } + + output, err := f.Fix(input) + if err != nil { + t.Fatalf("err: %s", err) + } + + if diff := cmp.Diff(expected, output); diff != "" { + t.Fatalf("unexpected output: %s", diff) + } +} diff --git a/website/source/docs/templates/engine.html.md b/website/source/docs/templates/engine.html.md index 1cebe5fc5..a16182547 100644 --- a/website/source/docs/templates/engine.html.md +++ b/website/source/docs/templates/engine.html.md @@ -27,10 +27,10 @@ The syntax of templates uses the following conventions: Functions perform operations on and within strings, for example the `{{timestamp}}` function can be used in any string to generate the current timestamp. This is useful for configurations that require unique keys, such as -AMI names. By setting the AMI name to something like -`My Packer AMI {{timestamp}}`, the AMI name will be unique down to the second. -If you need greater than one second granularity, you should use `{{uuid}}`, for -example when you have multiple builders in the same template. +AMI names. By setting the AMI name to something like `My Packer AMI +{{timestamp}}`, the AMI name will be unique down to the second. If you need +greater than one second granularity, you should use `{{uuid}}`, for example +when you have multiple builders in the same template. Here is a full list of the available functions for reference. @@ -54,18 +54,41 @@ Here is a full list of the available functions for reference. - `upper` - Uppercases the string. - `user` - Specifies a user variable. - `packer_version` - Returns Packer version. +- `clean_resource_name` - Image names can only contain certain characters and + have a maximum length, eg 63 on GCE & 80 on Azure. `clean_resource_name` + will convert upper cases to lower cases and replace illegal characters with + a "-" character. Example: + + `"mybuild-{{isotime | clean_image_name}}"` will become + `mybuild-2017-10-18t02-06-30z`. + + Note: Valid Azure image names must match the regex + `^[^_\\W][\\w-._)]{0,79}$` + + Note: Valid GCE image names must match the regex + `(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)` + + This engine does not guarantee that the final image name will match the + regex; it will not truncate your name if it exceeds the maximum number of + allowed characters, and it will not validate that the beginning and end of + the engine's output are valid. For example, `"image_name": {{isotime | + clean_resource_name}}"` will cause your build to fail because the image + name will start with a number, which is why in the above example we prepend + the isotime with "mybuild". #### Specific to Amazon builders: -- `clean_ami_name` - AMI names can only contain certain characters. This - function will replace illegal characters with a '-" character. Example - usage since ":" is not a legal AMI name is: `{{isotime | clean_ami_name}}`. +- `clean_ami_name` - DEPRECATED use `clean_resource_name` instead - AMI names + can only contain certain characters. This function will replace illegal + characters with a '-" character. Example usage since ":" is not a legal AMI + name is: `{{isotime | clean_ami_name}}`. #### Specific to Google Compute builders: -- `clean_image_name` - GCE image names can only contain certain characters - and the maximum length is 63. This function will convert upper cases to - lower cases and replace illegal characters with a "-" character. Example: +- `clean_image_name` - DEPRECATED use `clean_resource_name` instead - GCE + image names can only contain certain characters and the maximum length is + 63. This function will convert upper cases to lower cases and replace + illegal characters with a "-" character. Example: `"mybuild-{{isotime | clean_image_name}}"` will become `mybuild-2017-10-18t02-06-30z`. @@ -82,9 +105,10 @@ Here is a full list of the available functions for reference. #### Specific to Azure builders: -- `clean_image_name` - Azure managed image names can only contain certain - characters and the maximum length is 80. This function will replace illegal - characters with a "-" character. Example: +- `clean_image_name` - DEPRECATED use `clean_resource_name` instead - Azure + managed image names can only contain certain characters and the maximum + length is 80. This function will replace illegal characters with a "-" + character. Example: `"mybuild-{{isotime | clean_image_name}}"` will become `mybuild-2017-10-18t02-06-30z`. @@ -96,9 +120,8 @@ Here is a full list of the available functions for reference. regex; it will not truncate your name if it exceeds 80 characters, and it will not validate that the beginning and end of the engine's output are valid. It will truncate invalid characters from the end of the name when - converting illegal characters. For example, - `"managed_image_name: "My-Name::"` will be converted to - `"managed_image_name: "My-Name"` + converting illegal characters. For example, `"managed_image_name: + "My-Name::"` will be converted to `"managed_image_name: "My-Name"` ## Template variables