blob: 08b7d2ad745c6bce8e1f329f1d7d97f471ae06e2 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
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.(*packer.Core)
if err := core.Initialize(packer.InitializeOptions{}); 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()
}
// referencedUserVariables executes parts of blocks as go template files finding user variables referenced
// within the template. This function should be called once to extract those variables referenced via the {{user `...`}}
// template function. The resulting map will contain variables defined in the JSON variables property, and some that
// are declared via var-files; to avoid duplicates the results of this function should be reconciled against tpl.Variables.
func referencedUserVariables(s []byte) map[string]*template.Variable {
userVars := make([]string, 0)
funcMap := texttemplate.FuncMap{
"user": func(in string) string {
userVars = append(userVars, in)
return ""
},
}
tpl, err := texttemplate.New("hcl2_upgrade").
Funcs(funcMap).
Parse(string(s))
if err != nil {
return nil
}
if err := tpl.Execute(&bytes.Buffer{}, nil); err != nil {
return nil
}
vars := make(map[string]*template.Variable)
for _, v := range userVars {
vars[v] = &template.Variable{
Key: v,
Required: true,
}
}
return vars
}
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 {
reqPlugins, err := p.generateRequiredPluginsBlock(tpl)
if err != nil {
return err
}
if tpl.MinVersion == "" && reqPlugins == nil {
return nil
}
fileContent := hclwrite.NewEmptyFile()
body := fileContent.Body()
packerBody := body.AppendNewBlock("packer", nil).Body()
if tpl.MinVersion != "" {
packerBody.SetAttributeValue("required_version", cty.StringVal(fmt.Sprintf(">= %s", tpl.MinVersion)))
}
if reqPlugins != nil {
packerBody.AppendBlock(reqPlugins)
}
p.out = fileContent.Bytes()
return nil
}
func gatherPluginsFromTemplate(tpl *template.Template) []string {
plugins := map[string]struct{}{}
for _, b := range tpl.Builders {
for prefix, plugin := range knownPluginPrefixes {
if strings.HasPrefix(b.Type, prefix) {
plugins[plugin] = struct{}{}
}
}
}
for _, p := range tpl.Provisioners {
for prefix, plugin := range knownPluginPrefixes {
if strings.HasPrefix(p.Type, prefix) {
plugins[plugin] = struct{}{}
}
}
}
for _, pps := range tpl.PostProcessors {
for _, pp := range pps {
for prefix, plugin := range knownPluginPrefixes {
if strings.HasPrefix(pp.Type, prefix) {
plugins[plugin] = struct{}{}
}
}
}
}
if len(plugins) == 0 {
return nil
}
retPlugins := make([]string, 0, len(plugins))
for plugin := range plugins {
retPlugins = append(retPlugins, plugin)
}
sort.Strings(retPlugins)
return retPlugins
}
func (p *PackerParser) generateRequiredPluginsBlock(tpl *template.Template) (*hclwrite.Block, error) {
plugins := gatherPluginsFromTemplate(tpl)
if len(plugins) == 0 {
return nil, nil
}
reqPlugins := hclwrite.NewBlock("required_plugins", nil)
for _, plugin := range plugins {
pluginBlock := cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal(plugin),
"version": cty.StringVal("~> 1"),
})
reqPlugins.Body().SetAttributeValue(strings.Replace(plugin, "github.com/hashicorp/", "", 1), pluginBlock)
}
return reqPlugins, 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{}
}
if len(tpl.Variables) == 0 {
tpl.Variables = make(map[string]*template.Variable)
}
// JSON supports variable declaration via var-files.
// User variables that might be defined in a var-file
// but not in the actual JSON template should be accounted for.
userVars := referencedUserVariables(tpl.RawContents)
for name, variable := range userVars {
if _, ok := tpl.Variables[name]; ok {
continue
}
tpl.Variables[name] = variable
}
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. Variables referencing some form of variable expression must be defined as a local in HCL2,
// as variables in HCL2 must have a known value at parsing time.
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)
}