packer/command/hcl2_upgrade.go
Lucas Bajolet 68ddb2f89d
hcl2_upgrade: add share_image_gallery workaround (#12087)
In azure templates, the shared_image_gallery was mistakenly considered
an attribute while this is supposed to be a block.

This is due to a heuristic we use for deciding whether a JSON object is
to be translated to an attribute or a block that fell short as the
shared_image_gallery does not contain complex types.

This cannot be fixed trivially for the general case, so we add this
entity to the list of workarounds until we can implement something more
robust.
2022-10-27 18:09:40 -04:00

1285 lines
37 KiB
Go

package command
import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
texttemplate "text/template"
"text/template/parse"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/hcl/v2/hclwrite"
awscommon "github.com/hashicorp/packer-plugin-amazon/builder/common"
hcl2shim "github.com/hashicorp/packer-plugin-sdk/hcl2helper"
"github.com/hashicorp/packer-plugin-sdk/template"
"github.com/hashicorp/packer/packer"
"github.com/mitchellh/mapstructure"
"github.com/posener/complete"
"github.com/zclconf/go-cty/cty"
)
type HCL2UpgradeCommand struct {
Meta
}
func (c *HCL2UpgradeCommand) Run(args []string) int {
ctx, cleanup := handleTermInterrupt(c.Ui)
defer cleanup()
cfg, ret := c.ParseArgs(args)
if ret != 0 {
return ret
}
return c.RunContext(ctx, cfg)
}
func (c *HCL2UpgradeCommand) ParseArgs(args []string) (*HCL2UpgradeArgs, int) {
var cfg HCL2UpgradeArgs
flags := c.Meta.FlagSet("hcl2_upgrade", FlagSetNone)
flags.Usage = func() { c.Ui.Say(c.Help()) }
cfg.AddFlagSets(flags)
if err := flags.Parse(args); err != nil {
return &cfg, 1
}
args = flags.Args()
if len(args) != 1 {
flags.Usage()
return &cfg, 1
}
cfg.Path = args[0]
if cfg.OutputFile == "" {
cfg.OutputFile = cfg.Path + ".pkr.hcl"
}
return &cfg, 0
}
const (
hcl2UpgradeFileHeader = `# This file was autogenerated by the 'packer hcl2_upgrade' command. We
# recommend double checking that everything is correct before going forward. We
# also recommend treating this file as disposable. The HCL2 blocks in this
# file can be moved to other files. For example, the variable blocks could be
# moved to their own 'variables.pkr.hcl' file, etc. Those files need to be
# suffixed with '.pkr.hcl' to be visible to Packer. To use multiple files at
# once they also need to be in the same folder. 'packer inspect folder/'
# will describe to you what is in that folder.
# Avoid mixing go templating calls ( for example ` + "```{{ upper(`string`) }}```" + ` )
# and HCL2 calls (for example '${ var.string_value_example }' ). They won't be
# executed together and the outcome will be unknown.
`
inputVarHeader = `
# All generated input variables will be of 'string' type as this is how Packer JSON
# views them; you can change their type later on. Read the variables type
# constraints documentation
# https://www.packer.io/docs/templates/hcl_templates/variables#type-constraints for more info.`
localsVarHeader = `
# All locals variables are generated from variables that uses expressions
# that are not allowed in HCL2 variables.
# Read the documentation for locals blocks here:
# https://www.packer.io/docs/templates/hcl_templates/blocks/locals`
packerBlockHeader = `
# See https://www.packer.io/docs/templates/hcl_templates/blocks/packer for more info
`
sourcesHeader = `
# source blocks are generated from your builders; a source can be referenced in
# build blocks. A build block runs provisioner and post-processors on a
# source. Read the documentation for source blocks here:
# https://www.packer.io/docs/templates/hcl_templates/blocks/source`
buildHeader = `
# a build block invokes sources and runs provisioning steps on them. The
# documentation for build blocks can be found here:
# https://www.packer.io/docs/templates/hcl_templates/blocks/build
`
amazonAmiDataHeader = `
# The amazon-ami data block is generated from your amazon builder source_ami_filter; a data
# from this block can be referenced in source and locals blocks.
# Read the documentation for data blocks here:
# https://www.packer.io/docs/templates/hcl_templates/blocks/data
# Read the documentation for the Amazon AMI Data Source here:
# https://www.packer.io/plugins/datasources/amazon/ami`
amazonSecretsManagerDataHeader = `
# The amazon-secretsmanager data block is generated from your aws_secretsmanager template function; a data
# from this block can be referenced in source and locals blocks.
# Read the documentation for data blocks here:
# https://www.packer.io/docs/templates/hcl_templates/blocks/data
# Read the documentation for the Amazon Secrets Manager Data Source here:
# https://www.packer.io/plugins/datasources/amazon/secretsmanager`
)
var (
amazonSecretsManagerMap = map[string]map[string]interface{}{}
localsVariableMap = map[string]string{}
timestamp = false
isotime = false
strftime = false
)
type BlockParser interface {
Parse(*template.Template) error
Write(*bytes.Buffer)
}
func (c *HCL2UpgradeCommand) RunContext(_ context.Context, cla *HCL2UpgradeArgs) int {
var output io.Writer
if err := os.MkdirAll(filepath.Dir(cla.OutputFile), 0755); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to create output directory: %v", err))
return 1
}
if f, err := os.Create(cla.OutputFile); err == nil {
output = f
defer f.Close()
} else {
c.Ui.Error(fmt.Sprintf("Failed to create output file: %v", err))
return 1
}
if cla.WithAnnotations {
if _, err := output.Write([]byte(hcl2UpgradeFileHeader)); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to write to file: %v", err))
return 1
}
}
hdl, ret := c.GetConfigFromJSON(&cla.MetaArgs)
if ret != 0 {
c.Ui.Error("Failed to get config from JSON")
return 1
}
core := hdl.(*CoreWrapper).Core
if err := core.Initialize(); err != nil {
c.Ui.Error(fmt.Sprintf("Ignoring following initialization error: %v", err))
}
tpl := core.Template
// Parse blocks
packerBlock := &PackerParser{
WithAnnotations: cla.WithAnnotations,
}
if err := packerBlock.Parse(tpl); err != nil {
c.Ui.Error(fmt.Sprintf("Ignoring following Parse error: %v", err))
ret = 1
}
variables := &VariableParser{
WithAnnotations: cla.WithAnnotations,
}
if err := variables.Parse(tpl); err != nil {
c.Ui.Error(fmt.Sprintf("Ignoring following variables.Parse error: %v", err))
ret = 1
}
locals := &LocalsParser{
LocalsOut: variables.localsOut,
WithAnnotations: cla.WithAnnotations,
}
if err := locals.Parse(tpl); err != nil {
c.Ui.Error(fmt.Sprintf("Ignoring following locals.Parse error: %v", err))
ret = 1
}
builders := []*template.Builder{}
{
// sort builders to avoid map's randomness
for _, builder := range tpl.Builders {
builders = append(builders, builder)
}
}
sort.Slice(builders, func(i, j int) bool {
return builders[i].Type+builders[i].Name < builders[j].Type+builders[j].Name
})
amazonAmiDatasource := &AmazonAmiDatasourceParser{
Builders: builders,
WithAnnotations: cla.WithAnnotations,
}
if err := amazonAmiDatasource.Parse(tpl); err != nil {
c.Ui.Error(fmt.Sprintf("Ignoring following amazonAmiDatasource.Parse error: %v", err))
ret = 1
}
sources := &SourceParser{
Builders: builders,
BuilderPlugins: c.Meta.CoreConfig.Components.PluginConfig.Builders,
WithAnnotations: cla.WithAnnotations,
}
if err := sources.Parse(tpl); err != nil {
c.Ui.Error(fmt.Sprintf("Ignoring following sources.Parse error: %v", err))
ret = 1
}
build := &BuildParser{
Builders: builders,
WithAnnotations: cla.WithAnnotations,
}
if err := build.Parse(tpl); err != nil {
c.Ui.Error(fmt.Sprintf("Ignoring following build.Parse error: %v", err))
ret = 1
}
amazonSecretsDatasource := &AmazonSecretsDatasourceParser{
WithAnnotations: cla.WithAnnotations,
}
if err := amazonSecretsDatasource.Parse(tpl); err != nil {
c.Ui.Error(fmt.Sprintf("Ignoring following amazonSecretsDatasource.Parse error: %v", err))
ret = 1
}
// Write file
out := &bytes.Buffer{}
for _, block := range []BlockParser{
packerBlock,
variables,
amazonSecretsDatasource,
amazonAmiDatasource,
locals,
sources,
build,
} {
block.Write(out)
}
if _, err := output.Write(hclwrite.Format(out.Bytes())); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to write to file: %v", err))
return 1
}
c.Ui.Say(fmt.Sprintf("Successfully created %s. Exit %d", cla.OutputFile, ret))
return ret
}
type UnhandleableArgumentError struct {
Call string
Correspondance string
Docs string
}
func (uc UnhandleableArgumentError) Error() string {
return fmt.Sprintf(`unhandled %q call:
# there is no way to automatically upgrade the %[1]q call.
# Please manually upgrade to %s
# Visit %s for more infos.`, uc.Call, uc.Correspondance, uc.Docs)
}
func fallbackReturn(err error, s []byte) []byte {
if strings.Contains(err.Error(), "unhandled") {
return append([]byte(fmt.Sprintf("\n# %s\n", err)), s...)
}
return append([]byte(fmt.Sprintf("\n# could not parse template for following block: %q\n", err)), s...)
}
// reTemplate writes a new template to `out` and escapes all unknown variables
// so that we don't interpret them later on when interpreting the template
func reTemplate(nd parse.Node, out io.Writer, funcs texttemplate.FuncMap) error {
switch node := nd.(type) {
case *parse.ActionNode:
// Leave pipes as-is
if len(node.Pipe.Cmds) > 1 {
fmt.Fprintf(out, "%s", node.String())
return nil
}
cmd := node.Pipe.Cmds[0]
args := cmd.Args
if len(args) > 1 {
// Function calls with parameters are left aside
fmt.Fprintf(out, "%s", node.String())
return nil
}
_, ok := funcs[args[0].String()]
if ok {
// Known functions left as-is
fmt.Fprintf(out, "%s", node.String())
return nil
}
// Escape anything that isn't in the func map
fmt.Fprintf(out, "{{ \"{{\" }} %s {{ \"}}\" }}", cmd.String())
// TODO maybe node.Pipe.Decls? Though in Packer templates they're not
// supported officially so they can be left aside for now
case *parse.ListNode:
for _, child := range node.Nodes {
err := reTemplate(child, out, funcs)
if err != nil {
return err
}
}
case *parse.TextNode:
_, err := fmt.Fprintf(out, "%s", node.Text)
if err != nil {
return err
}
default:
return fmt.Errorf("unhandled node type %s", reflect.TypeOf(nd))
}
return nil
}
// transposeTemplatingCalls executes parts of blocks as go template files and replaces
// their result with their hcl2 variant. If something goes wrong the template
// containing the go template string is returned.
func transposeTemplatingCalls(s []byte) []byte {
funcErrors := &multierror.Error{
ErrorFormat: func(es []error) string {
if len(es) == 1 {
return fmt.Sprintf("# 1 error occurred upgrading the following block:\n\t# %s\n", es[0])
}
points := make([]string, len(es))
for i, err := range es {
if i == len(es)-1 {
points[i] = fmt.Sprintf("# %s", err)
continue
}
points[i] = fmt.Sprintf("# %s\n", err)
}
return fmt.Sprintf(
"# %d errors occurred upgrading the following block:\n\t%s",
len(es), strings.Join(points, "\n\t"))
},
}
funcMap := texttemplate.FuncMap{
"aws_secretsmanager": func(a ...string) string {
if len(a) == 2 {
for key, config := range amazonSecretsManagerMap {
nameOk := config["name"] == a[0]
keyOk := config["key"] == a[1]
if nameOk && keyOk {
return fmt.Sprintf("${data.amazon-secretsmanager.%s.value}", key)
}
}
id := fmt.Sprintf("autogenerated_%d", len(amazonSecretsManagerMap)+1)
amazonSecretsManagerMap[id] = map[string]interface{}{
"name": a[0],
"key": a[1],
}
return fmt.Sprintf("${data.amazon-secretsmanager.%s.value}", id)
}
for key, config := range amazonSecretsManagerMap {
nameOk := config["name"] == a[0]
if nameOk {
return fmt.Sprintf("${data.amazon-secretsmanager.%s.value}", key)
}
}
id := fmt.Sprintf("autogenerated_%d", len(amazonSecretsManagerMap)+1)
amazonSecretsManagerMap[id] = map[string]interface{}{
"name": a[0],
}
return fmt.Sprintf("${data.amazon-secretsmanager.%s.value}", id)
},
"timestamp": func() string {
timestamp = true
return "${local.timestamp}"
},
"isotime": func(a ...string) string {
if len(a) == 0 {
// returns rfc3339 formatted string.
return "${timestamp()}"
}
// otherwise a valid isotime func has one input.
isotime = true
return fmt.Sprintf("${legacy_isotime(\"%s\")}", a[0])
},
"strftime": func(a ...string) string {
if len(a) == 0 {
// returns rfc3339 formatted string.
return "${timestamp()}"
}
strftime = true
return fmt.Sprintf("${legacy_strftime(\"%s\")}", a[0])
},
"user": func(in string) string {
if _, ok := localsVariableMap[in]; ok {
// variable is now a local
return fmt.Sprintf("${local.%s}", in)
}
return fmt.Sprintf("${var.%s}", in)
},
"env": func(in string) string {
return fmt.Sprintf("${env(%q)}", in)
},
"build": func(a string) string {
return fmt.Sprintf("${build.%s}", a)
},
"data": func(a string) string {
return fmt.Sprintf("${data.%s}", a)
},
"template_dir": func() string {
return "${path.root}"
},
"pwd": func() string {
return "${path.cwd}"
},
"packer_version": func() string {
return "${packer.version}"
},
"uuid": func() string {
return "${uuidv4()}"
},
"lower": func(a string) (string, error) {
funcErrors = multierror.Append(funcErrors, UnhandleableArgumentError{
"lower",
"`lower(var.example)`",
"https://www.packer.io/docs/templates/hcl_templates/functions/string/lower",
})
return fmt.Sprintf("{{ lower `%s` }}", a), nil
},
"upper": func(a string) (string, error) {
funcErrors = multierror.Append(funcErrors, UnhandleableArgumentError{
"upper",
"`upper(var.example)`",
"https://www.packer.io/docs/templates/hcl_templates/functions/string/upper",
})
return fmt.Sprintf("{{ upper `%s` }}", a), nil
},
"split": func(a, b string, n int) (string, error) {
funcErrors = multierror.Append(funcErrors, UnhandleableArgumentError{
"split",
"`split(separator, string)`",
"https://www.packer.io/docs/templates/hcl_templates/functions/string/split",
})
return fmt.Sprintf("{{ split `%s` `%s` %d }}", a, b, n), nil
},
"replace": func(a, b string, n int, c string) (string, error) {
funcErrors = multierror.Append(funcErrors, UnhandleableArgumentError{
"replace",
"`replace(string, substring, replacement)` or `regex_replace(string, substring, replacement)`",
"https://www.packer.io/docs/templates/hcl_templates/functions/string/replace or https://www.packer.io/docs/templates/hcl_templates/functions/string/regex_replace",
})
return fmt.Sprintf("{{ replace `%s` `%s` `%s` %d }}", a, b, c, n), nil
},
"replace_all": func(a, b, c string) (string, error) {
funcErrors = multierror.Append(funcErrors, UnhandleableArgumentError{
"replace_all",
"`replace(string, substring, replacement)` or `regex_replace(string, substring, replacement)`",
"https://www.packer.io/docs/templates/hcl_templates/functions/string/replace or https://www.packer.io/docs/templates/hcl_templates/functions/string/regex_replace",
})
return fmt.Sprintf("{{ replace_all `%s` `%s` `%s` }}", a, b, c), nil
},
"clean_resource_name": func(a string) (string, error) {
funcErrors = multierror.Append(funcErrors, UnhandleableArgumentError{
"clean_resource_name",
"use custom validation rules, `replace(string, substring, replacement)` or `regex_replace(string, substring, replacement)`",
"https://packer.io/docs/templates/hcl_templates/variables#custom-validation-rules" +
" , https://www.packer.io/docs/templates/hcl_templates/functions/string/replace" +
" or https://www.packer.io/docs/templates/hcl_templates/functions/string/regex_replace",
})
return fmt.Sprintf("{{ clean_resource_name `%s` }}", a), nil
},
"build_name": func() string {
return "${build.name}"
},
"build_type": func() string {
return "${build.type}"
},
}
tpl, err := texttemplate.New("hcl2_upgrade").
Funcs(funcMap).
Parse(string(s))
if err != nil {
if strings.Contains(err.Error(), "unexpected \"\\\\\" in operand") {
// This error occurs if the operand in the text template used
// escaped quoting \" instead of bactick quoting `
// Create a regex to do a string replace on this block, to fix
// quoting.
q := fixQuoting(string(s))
unquoted := []byte(q)
tpl, err = texttemplate.New("hcl2_upgrade").
Funcs(funcMap).
Parse(string(unquoted))
if err != nil {
return fallbackReturn(err, unquoted)
}
} else {
return fallbackReturn(err, s)
}
}
retempl := &bytes.Buffer{}
if err := reTemplate(tpl.Root, retempl, funcMap); err != nil {
return fallbackReturn(err, s)
}
tpl, err = texttemplate.New("hcl2_upgrade").
Funcs(funcMap).
Parse(retempl.String())
str := &bytes.Buffer{}
if err := tpl.Execute(str, nil); err != nil {
return fallbackReturn(err, s)
}
out := str.Bytes()
if funcErrors.Len() > 0 {
return append([]byte(fmt.Sprintf("\n%s", funcErrors)), out...)
}
return out
}
// variableTransposeTemplatingCalls executes parts of blocks as go template files and replaces
// their result with their hcl2 variant for variables block only. If something goes wrong the template
// containing the go template string is returned.
// In variableTransposeTemplatingCalls the definition of aws_secretsmanager function will create a data source
// with the same name as the variable.
func variableTransposeTemplatingCalls(s []byte) (isLocal bool, body []byte) {
setIsLocal := func(a ...string) string {
isLocal = true
return ""
}
// Make locals from variables using valid template engine,
// expect the ones using only 'env'
// ref: https://www.packer.io/docs/templates/legacy_json_templates/engine#template-engine
funcMap := texttemplate.FuncMap{
"aws_secretsmanager": setIsLocal,
"timestamp": setIsLocal,
"isotime": setIsLocal,
"strftime": setIsLocal,
"user": setIsLocal,
"env": func(in string) string {
return fmt.Sprintf("${env(%q)}", in)
},
"template_dir": setIsLocal,
"pwd": setIsLocal,
"packer_version": setIsLocal,
"uuid": setIsLocal,
"lower": setIsLocal,
"upper": setIsLocal,
"split": func(_, _ string, _ int) (string, error) {
isLocal = true
return "", nil
},
"replace": func(_, _ string, _ int, _ string) (string, error) {
isLocal = true
return "", nil
},
"replace_all": func(_, _, _ string) (string, error) {
isLocal = true
return "", nil
},
}
tpl, err := texttemplate.New("hcl2_upgrade").
Funcs(funcMap).
Parse(string(s))
if err != nil {
if strings.Contains(err.Error(), "unexpected \"\\\\\" in operand") {
// This error occurs if the operand in the text template used
// escaped quoting \" instead of bactick quoting `
// Create a regex to do a string replace on this block, to fix
// quoting.
q := fixQuoting(string(s))
unquoted := []byte(q)
tpl, err = texttemplate.New("hcl2_upgrade").
Funcs(funcMap).
Parse(string(unquoted))
if err != nil {
return isLocal, fallbackReturn(err, unquoted)
}
} else {
return isLocal, fallbackReturn(err, s)
}
}
retempl := &bytes.Buffer{}
if err := reTemplate(tpl.Root, retempl, funcMap); err != nil {
return isLocal, fallbackReturn(err, s)
}
tpl, err = texttemplate.New("hcl2_upgrade").
Funcs(funcMap).
Parse(retempl.String())
str := &bytes.Buffer{}
if err := tpl.Execute(str, nil); err != nil {
return isLocal, fallbackReturn(err, s)
}
return isLocal, str.Bytes()
}
func jsonBodyToHCL2Body(out *hclwrite.Body, kvs map[string]interface{}) {
ks := []string{}
for k := range kvs {
ks = append(ks, k)
}
sort.Strings(ks)
for _, k := range ks {
value := kvs[k]
switch value := value.(type) {
case map[string]interface{}:
var mostComplexElem interface{}
for _, randomElem := range value {
if k == "linux_options" || k == "network_interface" || k == "shared_image_gallery" {
break
}
// HACK: we take the most complex element of that map because
// in HCL2, map of objects can be bodies, for example:
// map containing object: source_ami_filter {} ( body )
// simple string/string map: tags = {} ) ( attribute )
//
// if we could not find an object in this map then it's most
// likely a plain map and so we guess it should be and
// attribute. Though now if value refers to something that is
// an object but only contains a string or a bool; we could
// generate a faulty object. For example a (somewhat invalid)
// source_ami_filter where only `most_recent` is set.
switch randomElem.(type) {
case string, int, float64, bool:
if mostComplexElem != nil {
continue
}
mostComplexElem = randomElem
default:
mostComplexElem = randomElem
}
}
switch mostComplexElem.(type) {
case string, int, float64, bool:
out.SetAttributeValue(k, hcl2shim.HCL2ValueFromConfigValue(value))
default:
nestedBlockBody := out.AppendNewBlock(k, nil).Body()
jsonBodyToHCL2Body(nestedBlockBody, value)
}
case map[string]string, map[string]int, map[string]float64:
out.SetAttributeValue(k, hcl2shim.HCL2ValueFromConfigValue(value))
case []interface{}:
if len(value) == 0 {
continue
}
var mostComplexElem interface{}
for _, randomElem := range value {
// HACK: we take the most complex element of that slice because
// in hcl2 slices of plain types can be arrays, for example:
// simple string type: owners = ["0000000000"]
// object: launch_block_device_mappings {}
switch randomElem.(type) {
case string, int, float64, bool:
if mostComplexElem != nil {
continue
}
mostComplexElem = randomElem
default:
mostComplexElem = randomElem
}
}
switch mostComplexElem.(type) {
case map[string]interface{}:
// this is an object in a slice; so we unwrap it. We
// could try to remove any 's' suffix in the key, but
// this might not work everywhere.
for i := range value {
value := value[i].(map[string]interface{})
nestedBlockBody := out.AppendNewBlock(k, nil).Body()
jsonBodyToHCL2Body(nestedBlockBody, value)
}
continue
default:
out.SetAttributeValue(k, hcl2shim.HCL2ValueFromConfigValue(value))
}
default:
out.SetAttributeValue(k, hcl2shim.HCL2ValueFromConfigValue(value))
}
}
}
func isSensitiveVariable(key string, vars []*template.Variable) bool {
for _, v := range vars {
if v.Key == key {
return true
}
}
return false
}
func (*HCL2UpgradeCommand) Help() string {
helpText := `
Usage: packer hcl2_upgrade [options] TEMPLATE
Will transform your JSON template into an HCL2 configuration.
Options:
-output-file=path Set output file name. By default this will be the
TEMPLATE name with ".pkr.hcl" appended to it. To be a
valid Packer HCL template, it must have the suffix
".pkr.hcl"
-with-annotations Add helper annotation comments to the file to help new
HCL2 users understand the template format.
`
return strings.TrimSpace(helpText)
}
func (*HCL2UpgradeCommand) Synopsis() string {
return "transform a JSON template into an HCL2 configuration"
}
func (*HCL2UpgradeCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictNothing
}
func (*HCL2UpgradeCommand) AutocompleteFlags() complete.Flags {
return complete.Flags{}
}
// Specific blocks parser responsible to parse and write the block
type PackerParser struct {
WithAnnotations bool
out []byte
}
func (p *PackerParser) Parse(tpl *template.Template) error {
if tpl.MinVersion != "" {
fileContent := hclwrite.NewEmptyFile()
body := fileContent.Body()
packerBody := body.AppendNewBlock("packer", nil).Body()
packerBody.SetAttributeValue("required_version", cty.StringVal(fmt.Sprintf(">= %s", tpl.MinVersion)))
p.out = fileContent.Bytes()
}
return nil
}
func (p *PackerParser) Write(out *bytes.Buffer) {
if len(p.out) > 0 {
if p.WithAnnotations {
out.Write([]byte(packerBlockHeader))
}
out.Write(p.out)
}
}
type VariableParser struct {
WithAnnotations bool
variablesOut []byte
localsOut []byte
}
func makeLocal(variable *template.Variable, sensitive bool, localBody *hclwrite.Body, localsContent *hclwrite.File, hasLocals *bool) []byte {
if sensitive {
// Create Local block because this is sensitive
sensitiveLocalContent := hclwrite.NewEmptyFile()
body := sensitiveLocalContent.Body()
body.AppendNewline()
sensitiveLocalBody := body.AppendNewBlock("local", []string{variable.Key}).Body()
sensitiveLocalBody.SetAttributeValue("sensitive", cty.BoolVal(true))
sensitiveLocalBody.SetAttributeValue("expression", hcl2shim.HCL2ValueFromConfigValue(variable.Default))
localsVariableMap[variable.Key] = "local"
return sensitiveLocalContent.Bytes()
}
localBody.SetAttributeValue(variable.Key, hcl2shim.HCL2ValueFromConfigValue(variable.Default))
localsVariableMap[variable.Key] = "locals"
*hasLocals = true
return []byte{}
}
func makeVariable(variable *template.Variable, sensitive bool) []byte {
variablesContent := hclwrite.NewEmptyFile()
variablesBody := variablesContent.Body()
variablesBody.AppendNewline()
variableBody := variablesBody.AppendNewBlock("variable", []string{variable.Key}).Body()
variableBody.SetAttributeRaw("type", hclwrite.Tokens{&hclwrite.Token{Bytes: []byte("string")}})
if variable.Default != "" || !variable.Required {
shimmed := hcl2shim.HCL2ValueFromConfigValue(variable.Default)
variableBody.SetAttributeValue("default", shimmed)
}
if sensitive {
variableBody.SetAttributeValue("sensitive", cty.BoolVal(true))
}
return variablesContent.Bytes()
}
func (p *VariableParser) Parse(tpl *template.Template) error {
// Output Locals and Local blocks
localsContent := hclwrite.NewEmptyFile()
localsBody := localsContent.Body()
localsBody.AppendNewline()
localBody := localsBody.AppendNewBlock("locals", nil).Body()
hasLocals := false
if len(p.variablesOut) == 0 {
p.variablesOut = []byte{}
}
if len(p.localsOut) == 0 {
p.localsOut = []byte{}
}
variables := []*template.Variable{}
{
// sort variables to avoid map's randomness
for _, variable := range tpl.Variables {
variables = append(variables, variable)
}
sort.Slice(variables, func(i, j int) bool {
return variables[i].Key < variables[j].Key
})
}
for _, variable := range variables {
// Create new HCL2 "variables" block, and populate the "value"
// field with the "Default" value from the JSON variable.
// Interpolate Jsonval first as an hcl variable to determine if it is
// a local.
isLocal, _ := variableTransposeTemplatingCalls([]byte(variable.Default))
sensitive := false
if isSensitiveVariable(variable.Key, tpl.SensitiveVariables) {
sensitive = true
}
// Create final HCL block and append.
if isLocal {
sensitiveBlocks := makeLocal(variable, sensitive, localBody, localsContent, &hasLocals)
if len(sensitiveBlocks) > 0 {
p.localsOut = append(p.localsOut, transposeTemplatingCalls(sensitiveBlocks)...)
}
continue
}
varbytes := makeVariable(variable, sensitive)
_, out := variableTransposeTemplatingCalls(varbytes)
p.variablesOut = append(p.variablesOut, out...)
}
if hasLocals == true {
p.localsOut = append(p.localsOut, transposeTemplatingCalls(localsContent.Bytes())...)
}
return nil
}
func (p *VariableParser) Write(out *bytes.Buffer) {
if len(p.variablesOut) > 0 {
if p.WithAnnotations {
out.Write([]byte(inputVarHeader))
}
out.Write(p.variablesOut)
}
}
type LocalsParser struct {
WithAnnotations bool
LocalsOut []byte
}
func (p *LocalsParser) Parse(tpl *template.Template) error {
// Locals where parsed with Variables
return nil
}
func (p *LocalsParser) Write(out *bytes.Buffer) {
if timestamp {
_, _ = out.Write([]byte("\n"))
if p.WithAnnotations {
fmt.Fprintln(out, `# "timestamp" template function replacement`)
}
fmt.Fprintln(out, `locals { timestamp = regex_replace(timestamp(), "[- TZ:]", "") }`)
}
if isotime {
fmt.Fprintln(out, `# The "legacy_isotime" function has been provided for backwards compatability, but we recommend switching to the timestamp and formatdate functions.`)
}
if strftime {
fmt.Fprintln(out, `# The "legacy_strftime" function has been provided for backwards compatability, but we recommend switching to the timestamp and formatdate functions.`)
}
if len(p.LocalsOut) > 0 {
if p.WithAnnotations {
out.Write([]byte(localsVarHeader))
}
out.Write(p.LocalsOut)
}
}
type AmazonSecretsDatasourceParser struct {
WithAnnotations bool
out []byte
}
func (p *AmazonSecretsDatasourceParser) Parse(_ *template.Template) error {
if p.out == nil {
p.out = []byte{}
}
keys := make([]string, 0, len(amazonSecretsManagerMap))
for k := range amazonSecretsManagerMap {
keys = append(keys, k)
}
sort.Strings(keys)
for _, dataSourceName := range keys {
datasourceContent := hclwrite.NewEmptyFile()
body := datasourceContent.Body()
body.AppendNewline()
datasourceBody := body.AppendNewBlock("data", []string{"amazon-secretsmanager", dataSourceName}).Body()
jsonBodyToHCL2Body(datasourceBody, amazonSecretsManagerMap[dataSourceName])
p.out = append(p.out, datasourceContent.Bytes()...)
}
return nil
}
func (p *AmazonSecretsDatasourceParser) Write(out *bytes.Buffer) {
if len(p.out) > 0 {
if p.WithAnnotations {
out.Write([]byte(amazonSecretsManagerDataHeader))
}
out.Write(p.out)
}
}
type AmazonAmiDatasourceParser struct {
Builders []*template.Builder
WithAnnotations bool
out []byte
}
func (p *AmazonAmiDatasourceParser) Parse(_ *template.Template) error {
if p.out == nil {
p.out = []byte{}
}
amazonAmiFilters := []map[string]interface{}{}
i := 1
for _, builder := range p.Builders {
if strings.HasPrefix(builder.Type, "amazon-") {
if sourceAmiFilter, ok := builder.Config["source_ami_filter"]; ok {
sourceAmiFilterCfg := map[string]interface{}{}
if err := mapstructure.Decode(sourceAmiFilter, &sourceAmiFilterCfg); err != nil {
return fmt.Errorf("Failed to write amazon-ami data source: %v", err)
}
sourceAmiFilterCfg, err := copyAWSAccessConfig(sourceAmiFilterCfg, builder.Config)
if err != nil {
return err
}
duplicate := false
dataSourceName := fmt.Sprintf("autogenerated_%d", i)
for j, filter := range amazonAmiFilters {
if reflect.DeepEqual(filter, sourceAmiFilterCfg) {
duplicate = true
dataSourceName = fmt.Sprintf("autogenerated_%d", j+1)
continue
}
}
// This is a hack...
// Use templating so that it could be correctly transformed later into a data resource
sourceAmiDataRef := fmt.Sprintf("{{ data `amazon-ami.%s.id` }}", dataSourceName)
if duplicate {
delete(builder.Config, "source_ami_filter")
builder.Config["source_ami"] = sourceAmiDataRef
continue
}
amazonAmiFilters = append(amazonAmiFilters, sourceAmiFilterCfg)
delete(builder.Config, "source_ami_filter")
builder.Config["source_ami"] = sourceAmiDataRef
i++
datasourceContent := hclwrite.NewEmptyFile()
body := datasourceContent.Body()
body.AppendNewline()
sourceBody := body.AppendNewBlock("data", []string{"amazon-ami", dataSourceName}).Body()
jsonBodyToHCL2Body(sourceBody, sourceAmiFilterCfg)
p.out = append(p.out, transposeTemplatingCalls(datasourceContent.Bytes())...)
}
}
}
return nil
}
func copyAWSAccessConfig(sourceAmi map[string]interface{}, builder map[string]interface{}) (map[string]interface{}, error) {
// Transform access config to a map
accessConfigMap := map[string]interface{}{}
if err := mapstructure.Decode(awscommon.AccessConfig{}, &accessConfigMap); err != nil {
return sourceAmi, err
}
for k := range accessConfigMap {
// Copy only access config present in the builder
if v, ok := builder[k]; ok {
sourceAmi[k] = v
}
}
return sourceAmi, nil
}
func (p *AmazonAmiDatasourceParser) Write(out *bytes.Buffer) {
if len(p.out) > 0 {
if p.WithAnnotations {
out.Write([]byte(amazonAmiDataHeader))
}
out.Write(p.out)
}
}
type SourceParser struct {
Builders []*template.Builder
BuilderPlugins packer.BuilderSet
WithAnnotations bool
out []byte
}
func (p *SourceParser) Parse(tpl *template.Template) error {
var unknownBuilders []string
if p.out == nil {
p.out = []byte{}
}
for i, builderCfg := range p.Builders {
sourcesContent := hclwrite.NewEmptyFile()
body := sourcesContent.Body()
body.AppendNewline()
if !p.BuilderPlugins.Has(builderCfg.Type) {
unknownBuilders = append(unknownBuilders, builderCfg.Type)
}
if builderCfg.Name == "" || builderCfg.Name == builderCfg.Type {
builderCfg.Name = fmt.Sprintf("autogenerated_%d", i+1)
}
builderCfg.Name = strings.ReplaceAll(strings.TrimSpace(builderCfg.Name), " ", "_")
sourceBody := body.AppendNewBlock("source", []string{builderCfg.Type, builderCfg.Name}).Body()
jsonBodyToHCL2Body(sourceBody, builderCfg.Config)
p.out = append(p.out, transposeTemplatingCalls(sourcesContent.Bytes())...)
}
if len(unknownBuilders) > 0 {
return fmt.Errorf("unknown builder type(s): %v\n", unknownBuilders)
}
return nil
}
func (p *SourceParser) Write(out *bytes.Buffer) {
if len(p.out) > 0 {
if p.WithAnnotations {
out.Write([]byte(sourcesHeader))
}
out.Write(p.out)
}
}
type BuildParser struct {
Builders []*template.Builder
WithAnnotations bool
provisioners BlockParser
postProcessors BlockParser
out []byte
}
func (p *BuildParser) Parse(tpl *template.Template) error {
if len(p.Builders) == 0 {
return nil
}
buildContent := hclwrite.NewEmptyFile()
buildBody := buildContent.Body()
if tpl.Description != "" {
buildBody.SetAttributeValue("description", cty.StringVal(tpl.Description))
buildBody.AppendNewline()
}
sourceNames := []string{}
for _, builder := range p.Builders {
sourceNames = append(sourceNames, fmt.Sprintf("source.%s.%s", builder.Type, builder.Name))
}
buildBody.SetAttributeValue("sources", hcl2shim.HCL2ValueFromConfigValue(sourceNames))
buildBody.AppendNewline()
p.out = buildContent.Bytes()
p.provisioners = &ProvisionerParser{
WithAnnotations: p.WithAnnotations,
}
if err := p.provisioners.Parse(tpl); err != nil {
return err
}
p.postProcessors = &PostProcessorParser{
WithAnnotations: p.WithAnnotations,
}
if err := p.postProcessors.Parse(tpl); err != nil {
return err
}
return nil
}
func (p *BuildParser) Write(out *bytes.Buffer) {
if len(p.out) > 0 {
if p.WithAnnotations {
out.Write([]byte(buildHeader))
} else {
_, _ = out.Write([]byte("\n"))
}
_, _ = out.Write([]byte("build {\n"))
out.Write(p.out)
p.provisioners.Write(out)
p.postProcessors.Write(out)
_, _ = out.Write([]byte("}\n"))
}
}
type ProvisionerParser struct {
WithAnnotations bool
out []byte
}
func (p *ProvisionerParser) Parse(tpl *template.Template) error {
if p.out == nil {
p.out = []byte{}
}
for _, provisioner := range tpl.Provisioners {
contentBytes := writeProvisioner("provisioner", provisioner)
p.out = append(p.out, transposeTemplatingCalls(contentBytes)...)
}
if tpl.CleanupProvisioner != nil {
contentBytes := writeProvisioner("error-cleanup-provisioner", tpl.CleanupProvisioner)
p.out = append(p.out, transposeTemplatingCalls(contentBytes)...)
}
return nil
}
func writeProvisioner(typeName string, provisioner *template.Provisioner) []byte {
provisionerContent := hclwrite.NewEmptyFile()
body := provisionerContent.Body()
block := body.AppendNewBlock(typeName, []string{provisioner.Type})
cfg := provisioner.Config
if cfg == nil {
cfg = map[string]interface{}{}
}
if len(provisioner.Except) > 0 {
cfg["except"] = provisioner.Except
}
if len(provisioner.Only) > 0 {
cfg["only"] = provisioner.Only
}
if provisioner.MaxRetries != "" {
cfg["max_retries"] = provisioner.MaxRetries
}
if provisioner.Timeout > 0 {
cfg["timeout"] = provisioner.Timeout.String()
}
if provisioner.PauseBefore > 0 {
cfg["pause_before"] = provisioner.PauseBefore.String()
}
body.AppendNewline()
jsonBodyToHCL2Body(block.Body(), cfg)
return provisionerContent.Bytes()
}
func (p *ProvisionerParser) Write(out *bytes.Buffer) {
if len(p.out) > 0 {
out.Write(p.out)
}
}
type PostProcessorParser struct {
WithAnnotations bool
out []byte
}
func (p *PostProcessorParser) Parse(tpl *template.Template) error {
if p.out == nil {
p.out = []byte{}
}
for _, pps := range tpl.PostProcessors {
postProcessorContent := hclwrite.NewEmptyFile()
body := postProcessorContent.Body()
switch len(pps) {
case 0:
continue
case 1:
default:
body = body.AppendNewBlock("post-processors", nil).Body()
}
for _, pp := range pps {
ppBody := body.AppendNewBlock("post-processor", []string{pp.Type}).Body()
if pp.KeepInputArtifact != nil {
ppBody.SetAttributeValue("keep_input_artifact", cty.BoolVal(*pp.KeepInputArtifact))
}
cfg := pp.Config
if cfg == nil {
cfg = map[string]interface{}{}
}
if len(pp.Except) > 0 {
cfg["except"] = pp.Except
}
if len(pp.Only) > 0 {
cfg["only"] = pp.Only
}
if pp.Name != "" && pp.Name != pp.Type {
cfg["name"] = pp.Name
}
jsonBodyToHCL2Body(ppBody, cfg)
}
p.out = append(p.out, transposeTemplatingCalls(postProcessorContent.Bytes())...)
}
return nil
}
func (p *PostProcessorParser) Write(out *bytes.Buffer) {
if len(p.out) > 0 {
out.Write(p.out)
}
}
func fixQuoting(old string) string {
// This regex captures golang template functions that use escaped quotes:
// {{ env \"myvar\" }}
// {{ split `some-string` \"-\" 0 }}
re := regexp.MustCompile(`{{\s*\w*(\s*(\\".*\\")\s*)+\w*\s*}}`)
body := re.ReplaceAllFunc([]byte(old), func(s []byte) []byte {
// Get the capture group
group := re.ReplaceAllString(string(s), `$1`)
unquoted, err := strconv.Unquote(fmt.Sprintf("\"%s\"", group))
if err != nil {
return s
}
return []byte(strings.Replace(string(s), group, unquoted, 1))
})
return string(body)
}