2023-03-02 15:37:05 -05:00
// Copyright (c) HashiCorp, Inc.
2023-08-10 15:53:29 -07:00
// SPDX-License-Identifier: BUSL-1.1
2023-03-02 15:37:05 -05:00
2020-08-25 10:51:43 +02:00
package command
import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
2021-01-19 10:21:39 +01:00
"reflect"
2021-03-22 02:56:30 -07:00
"regexp"
2020-08-25 10:51:43 +02:00
"sort"
2021-03-22 02:56:30 -07:00
"strconv"
2020-08-25 10:51:43 +02:00
"strings"
texttemplate "text/template"
2022-10-21 16:54:21 -04:00
"text/template/parse"
2020-08-25 10:51:43 +02:00
2021-02-23 16:51:13 +01:00
"github.com/hashicorp/go-multierror"
2020-08-25 10:51:43 +02:00
"github.com/hashicorp/hcl/v2/hclwrite"
2021-03-25 13:37:48 +01:00
awscommon "github.com/hashicorp/packer-plugin-amazon/builder/common"
2021-01-20 10:37:16 +01:00
hcl2shim "github.com/hashicorp/packer-plugin-sdk/hcl2helper"
2020-12-17 13:29:25 -08:00
"github.com/hashicorp/packer-plugin-sdk/template"
2021-02-12 15:37:41 +01:00
"github.com/hashicorp/packer/packer"
2021-01-19 10:21:39 +01:00
"github.com/mitchellh/mapstructure"
2020-08-25 10:51:43 +02:00
"github.com/posener/complete"
"github.com/zclconf/go-cty/cty"
)
const (
2021-01-18 06:08:04 -08:00
hcl2UpgradeFileHeader = `# This file was autogenerated by the 'packer hcl2_upgrade' command. We
2020-08-25 10:51:43 +02:00
# 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.
2020-12-09 03:39:54 -08:00
# Avoid mixing go templating calls ( for example ` + "```{{ upper(`string`) }}```" + ` )
2020-11-10 10:46:20 +01:00
# and HCL2 calls (for example '${ var.string_value_example }' ). They won't be
# executed together and the outcome will be unknown.
`
inputVarHeader = `
2020-10-14 14:52:17 -04:00
# 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
2020-08-25 10:51:43 +02:00
# constraints documentation
2021-02-08 11:28:26 +01:00
# 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`
2020-11-10 10:46:20 +01:00
packerBlockHeader = `
2021-01-14 14:55:19 -08:00
# See https://www.packer.io/docs/templates/hcl_templates/blocks/packer for more info
2020-08-25 10:51:43 +02:00
`
sourcesHeader = `
# source blocks are generated from your builders; a source can be referenced in
2020-10-14 14:52:17 -04:00
# build blocks. A build block runs provisioner and post-processors on a
2020-08-25 10:51:43 +02:00
# source. Read the documentation for source blocks here:
2021-01-14 14:55:19 -08:00
# https://www.packer.io/docs/templates/hcl_templates/blocks/source`
2020-08-25 10:51:43 +02:00
buildHeader = `
2020-10-14 14:52:17 -04:00
# a build block invokes sources and runs provisioning steps on them. The
2020-08-25 10:51:43 +02:00
# documentation for build blocks can be found here:
2021-01-14 14:55:19 -08:00
# https://www.packer.io/docs/templates/hcl_templates/blocks/build
2020-08-25 10:51:43 +02:00
`
2021-02-08 11:28:26 +01:00
2021-01-19 10:21:39 +01:00
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:
2021-02-08 11:28:26 +01:00
# https://www.packer.io/docs/templates/hcl_templates/blocks/data
# Read the documentation for the Amazon AMI Data Source here:
2022-10-21 11:00:58 -04:00
# https://www.packer.io/plugins/datasources/amazon/ami`
2021-02-08 11:28:26 +01:00
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:
2022-10-21 11:00:58 -04:00
# https://www.packer.io/plugins/datasources/amazon/secretsmanager`
2020-08-25 10:51:43 +02:00
)
2021-02-10 14:54:19 +01:00
var (
amazonSecretsManagerMap = map [ string ] map [ string ] interface {}{}
localsVariableMap = map [ string ] string {}
timestamp = false
2021-03-17 16:06:19 -07:00
isotime = false
2021-08-25 14:20:08 +02:00
strftime = false
2021-02-10 14:54:19 +01:00
)
2021-02-08 11:28:26 +01:00
2023-10-19 11:00:41 -04:00
// knownPlugins represent the HashiCorp maintained plugins the we can confidently
// construct a required plugins block for.
var knownPlugins = map [ string ] string {
"amazon" : "github.com/hashicorp/amazon" ,
"ansible" : "github.com/hashicorp/ansible" ,
"azure" : "github.com/hashicorp/azure" ,
"docker" : "github.com/hashicorp/docker" ,
"googlecompute" : "github.com/hashicorp/googlecompute" ,
"qemu" : "github.com/hashicorp/qemu" ,
"vagrant" : "github.com/hashicorp/vagrant" ,
2023-11-28 15:01:04 -05:00
"virtualbox" : "github.com/hashicorp/virtualbox" ,
2023-10-19 11:00:41 -04:00
"vmware" : "github.com/hashicorp/vmware" ,
"vsphere" : "github.com/hashicorp/vsphere" ,
}
// unknownPluginName represents any plugin not in knownPlugins or bundled into Packer
const unknownPluginName string = "unknown"
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" )
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
}
2021-02-12 15:18:53 +01:00
type BlockParser interface {
Parse ( * template . Template ) error
Write ( * bytes . Buffer )
}
2021-02-10 14:54:19 +01:00
func ( c * HCL2UpgradeCommand ) RunContext ( _ context . Context , cla * HCL2UpgradeArgs ) int {
2020-08-25 10:51:43 +02:00
var output io . Writer
2021-10-04 10:58:27 -04:00
if err := os . MkdirAll ( filepath . Dir ( cla . OutputFile ), 0755 ); err != nil {
2020-08-25 10:51:43 +02:00
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
}
2021-02-12 15:18:53 +01:00
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
}
2020-08-25 10:51:43 +02:00
}
hdl , ret := c . GetConfigFromJSON ( & cla . MetaArgs )
if ret != 0 {
2022-10-21 17:03:11 -04:00
c . Ui . Error ( "Failed to get config from JSON" )
2021-08-13 16:08:12 +02:00
return 1
2020-08-25 10:51:43 +02:00
}
2022-09-06 17:26:49 -04:00
core := hdl .( * packer . Core )
if err := core . Initialize ( packer . InitializeOptions {}); err != nil {
2020-11-10 10:46:20 +01:00
c . Ui . Error ( fmt . Sprintf ( "Ignoring following initialization error: %v" , err ))
2020-08-25 10:51:43 +02:00
}
tpl := core . Template
2021-02-12 15:18:53 +01:00
// Parse blocks
packerBlock := & PackerParser {
2021-02-12 15:37:41 +01:00
WithAnnotations : cla . WithAnnotations ,
2021-02-12 15:18:53 +01:00
}
if err := packerBlock . Parse ( tpl ); err != nil {
2021-06-03 13:41:16 +02:00
c . Ui . Error ( fmt . Sprintf ( "Ignoring following Parse error: %v" , err ))
ret = 1
2020-08-25 10:51:43 +02:00
}
2021-02-12 15:18:53 +01:00
variables := & VariableParser {
2021-02-12 15:37:41 +01:00
WithAnnotations : cla . WithAnnotations ,
2021-02-12 15:18:53 +01:00
}
if err := variables . Parse ( tpl ); err != nil {
2021-06-03 13:41:16 +02:00
c . Ui . Error ( fmt . Sprintf ( "Ignoring following variables.Parse error: %v" , err ))
ret = 1
2020-08-25 10:51:43 +02:00
}
2021-02-12 15:18:53 +01:00
locals := & LocalsParser {
LocalsOut : variables . localsOut ,
2021-02-12 15:37:41 +01:00
WithAnnotations : cla . WithAnnotations ,
2021-02-12 15:18:53 +01:00
}
if err := locals . Parse ( tpl ); err != nil {
2021-06-03 13:41:16 +02:00
c . Ui . Error ( fmt . Sprintf ( "Ignoring following locals.Parse error: %v" , err ))
ret = 1
2021-02-09 14:57:42 +01:00
}
2020-08-25 10:51:43 +02:00
builders := [] * template . Builder {}
{
2021-08-25 14:20:08 +02:00
// sort builders to avoid map's randomness
2020-08-25 10:51:43 +02:00
for _ , builder := range tpl . Builders {
builders = append ( builders , builder )
}
}
2021-02-15 17:35:03 +01:00
sort . Slice ( builders , func ( i , j int ) bool {
return builders [ i ]. Type + builders [ i ]. Name < builders [ j ]. Type + builders [ j ]. Name
})
2020-08-25 10:51:43 +02:00
2021-02-12 15:18:53 +01:00
amazonAmiDatasource := & AmazonAmiDatasourceParser {
2021-02-12 15:37:41 +01:00
Builders : builders ,
WithAnnotations : cla . WithAnnotations ,
2021-02-12 15:18:53 +01:00
}
if err := amazonAmiDatasource . Parse ( tpl ); err != nil {
2021-06-03 13:41:16 +02:00
c . Ui . Error ( fmt . Sprintf ( "Ignoring following amazonAmiDatasource.Parse error: %v" , err ))
ret = 1
2021-01-19 10:21:39 +01:00
}
2021-02-12 15:18:53 +01:00
sources := & SourceParser {
Builders : builders ,
BuilderPlugins : c . Meta . CoreConfig . Components . PluginConfig . Builders ,
2021-02-12 15:37:41 +01:00
WithAnnotations : cla . WithAnnotations ,
2021-02-12 15:18:53 +01:00
}
if err := sources . Parse ( tpl ); err != nil {
2021-06-03 13:41:16 +02:00
c . Ui . Error ( fmt . Sprintf ( "Ignoring following sources.Parse error: %v" , err ))
ret = 1
2020-08-25 10:51:43 +02:00
}
2021-02-12 15:18:53 +01:00
build := & BuildParser {
Builders : builders ,
2021-02-12 15:37:41 +01:00
WithAnnotations : cla . WithAnnotations ,
2021-02-12 15:18:53 +01:00
}
if err := build . Parse ( tpl ); err != nil {
2021-06-03 13:41:16 +02:00
c . Ui . Error ( fmt . Sprintf ( "Ignoring following build.Parse error: %v" , err ))
ret = 1
2020-08-25 10:51:43 +02:00
}
2021-02-12 15:18:53 +01:00
amazonSecretsDatasource := & AmazonSecretsDatasourceParser {
2021-02-12 15:37:41 +01:00
WithAnnotations : cla . WithAnnotations ,
2020-08-25 10:51:43 +02:00
}
2021-02-12 15:18:53 +01:00
if err := amazonSecretsDatasource . Parse ( tpl ); err != nil {
2021-06-03 13:41:16 +02:00
c . Ui . Error ( fmt . Sprintf ( "Ignoring following amazonSecretsDatasource.Parse error: %v" , err ))
ret = 1
2021-02-08 11:28:26 +01:00
}
// Write file
out := & bytes . Buffer {}
2021-02-15 17:35:03 +01:00
for _ , block := range [] BlockParser {
packerBlock ,
variables ,
amazonSecretsDatasource ,
amazonAmiDatasource ,
locals ,
sources ,
build ,
} {
block . Write ( out )
2021-02-08 11:28:26 +01:00
}
2021-02-12 15:18:53 +01:00
if _ , err := output . Write ( hclwrite . Format ( out . Bytes ())); err != nil {
c . Ui . Error ( fmt . Sprintf ( "Failed to write to file: %v" , err ))
return 1
2021-02-11 10:23:15 +01:00
}
2021-02-08 11:28:26 +01:00
2021-06-03 13:41:16 +02:00
c . Ui . Say ( fmt . Sprintf ( "Successfully created %s. Exit %d" , cla . OutputFile , ret ))
return ret
2020-08-25 10:51:43 +02:00
}
2020-11-10 10:46:20 +01:00
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 )
}
2021-03-19 02:24:49 -07:00
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 ... )
}
2022-10-21 16:54:21 -04:00
// 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
}
2020-08-25 10:51:43 +02:00
// 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 {
2021-02-23 16:51:13 +01:00
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 ])
}
2021-02-08 11:28:26 +01:00
2021-02-23 16:51:13 +01:00
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" ))
},
2021-02-08 11:28:26 +01:00
}
2021-02-23 16:51:13 +01:00
funcMap := texttemplate . FuncMap {
2021-02-08 11:28:26 +01:00
"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 )
2021-02-23 14:18:43 +01:00
},
"timestamp" : func () string {
2021-02-10 14:54:19 +01:00
timestamp = true
2020-08-25 10:51:43 +02:00
return "${local.timestamp}"
},
2021-02-23 14:18:43 +01:00
"isotime" : func ( a ... string ) string {
2021-03-17 14:35:30 -07:00
if len ( a ) == 0 {
2021-03-17 16:06:19 -07:00
// returns rfc3339 formatted string.
return "${timestamp()}"
2021-03-17 14:35:30 -07:00
}
// otherwise a valid isotime func has one input.
2021-03-17 16:06:19 -07:00
isotime = true
2021-03-17 14:35:30 -07:00
return fmt . Sprintf ( "${legacy_isotime(\"%s\")}" , a [ 0 ])
2020-08-25 10:51:43 +02:00
},
2021-08-25 14:20:08 +02:00
"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 ])
},
2020-08-25 10:51:43 +02:00
"user" : func ( in string ) string {
2021-02-08 11:28:26 +01:00
if _ , ok := localsVariableMap [ in ]; ok {
// variable is now a local
return fmt . Sprintf ( "${local.%s}" , in )
}
2020-08-25 10:51:43 +02:00
return fmt . Sprintf ( "${var.%s}" , in )
},
"env" : func ( in string ) string {
2020-11-11 20:54:22 +01:00
return fmt . Sprintf ( "${env(%q)}" , in )
2020-08-25 10:51:43 +02:00
},
"build" : func ( a string ) string {
return fmt . Sprintf ( "${build.%s}" , a )
},
2021-01-19 10:21:39 +01:00
"data" : func ( a string ) string {
return fmt . Sprintf ( "${data.%s}" , a )
},
2020-11-10 10:46:20 +01:00
"template_dir" : func () string {
2022-10-21 17:03:11 -04:00
return "${path.root}"
2020-11-10 10:46:20 +01:00
},
"pwd" : func () string {
2022-10-21 17:03:11 -04:00
return "${path.cwd}"
2020-11-10 10:46:20 +01:00
},
"packer_version" : func () string {
2022-10-21 17:03:11 -04:00
return "${packer.version}"
2020-11-10 10:46:20 +01:00
},
"uuid" : func () string {
2022-10-21 17:03:11 -04:00
return "${uuidv4()}"
2020-11-10 10:46:20 +01:00
},
2021-02-23 16:51:13 +01:00
"lower" : func ( a string ) ( string , error ) {
funcErrors = multierror . Append ( funcErrors , UnhandleableArgumentError {
2020-11-10 10:46:20 +01:00
"lower" ,
"`lower(var.example)`" ,
2021-01-14 14:55:19 -08:00
"https://www.packer.io/docs/templates/hcl_templates/functions/string/lower" ,
2021-02-23 16:51:13 +01:00
})
return fmt . Sprintf ( "{{ lower `%s` }}" , a ), nil
2020-11-10 10:46:20 +01:00
},
2021-02-23 16:51:13 +01:00
"upper" : func ( a string ) ( string , error ) {
funcErrors = multierror . Append ( funcErrors , UnhandleableArgumentError {
2020-11-10 10:46:20 +01:00
"upper" ,
"`upper(var.example)`" ,
2021-01-14 14:55:19 -08:00
"https://www.packer.io/docs/templates/hcl_templates/functions/string/upper" ,
2021-02-23 16:51:13 +01:00
})
return fmt . Sprintf ( "{{ upper `%s` }}" , a ), nil
2020-11-10 10:46:20 +01:00
},
2021-02-23 16:51:13 +01:00
"split" : func ( a , b string , n int ) ( string , error ) {
funcErrors = multierror . Append ( funcErrors , UnhandleableArgumentError {
2020-11-10 10:46:20 +01:00
"split" ,
"`split(separator, string)`" ,
2021-01-14 14:55:19 -08:00
"https://www.packer.io/docs/templates/hcl_templates/functions/string/split" ,
2021-02-23 16:51:13 +01:00
})
return fmt . Sprintf ( "{{ split `%s` `%s` %d }}" , a , b , n ), nil
2020-11-10 10:46:20 +01:00
},
2021-02-23 16:51:13 +01:00
"replace" : func ( a , b string , n int , c string ) ( string , error ) {
funcErrors = multierror . Append ( funcErrors , UnhandleableArgumentError {
2020-11-10 10:46:20 +01:00
"replace" ,
"`replace(string, substring, replacement)` or `regex_replace(string, substring, replacement)`" ,
2021-01-14 14:55:19 -08:00
"https://www.packer.io/docs/templates/hcl_templates/functions/string/replace or https://www.packer.io/docs/templates/hcl_templates/functions/string/regex_replace" ,
2021-02-23 16:51:13 +01:00
})
return fmt . Sprintf ( "{{ replace `%s` `%s` `%s` %d }}" , a , b , c , n ), nil
2020-11-10 10:46:20 +01:00
},
2021-02-23 16:51:13 +01:00
"replace_all" : func ( a , b , c string ) ( string , error ) {
funcErrors = multierror . Append ( funcErrors , UnhandleableArgumentError {
2020-11-10 10:46:20 +01:00
"replace_all" ,
"`replace(string, substring, replacement)` or `regex_replace(string, substring, replacement)`" ,
2021-01-14 14:55:19 -08:00
"https://www.packer.io/docs/templates/hcl_templates/functions/string/replace or https://www.packer.io/docs/templates/hcl_templates/functions/string/regex_replace" ,
2021-02-23 16:51:13 +01:00
})
return fmt . Sprintf ( "{{ replace_all `%s` `%s` `%s` }}" , a , b , c ), nil
2020-11-10 10:46:20 +01:00
},
2021-02-23 16:51:13 +01:00
"clean_resource_name" : func ( a string ) ( string , error ) {
funcErrors = multierror . Append ( funcErrors , UnhandleableArgumentError {
2020-11-10 10:46:20 +01:00
"clean_resource_name" ,
"use custom validation rules, `replace(string, substring, replacement)` or `regex_replace(string, substring, replacement)`" ,
2021-01-14 14:55:19 -08:00
"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" ,
2021-02-23 16:51:13 +01:00
})
return fmt . Sprintf ( "{{ clean_resource_name `%s` }}" , a ), nil
2020-11-10 10:46:20 +01:00
},
"build_name" : func () string {
2022-10-21 17:03:11 -04:00
return "${build.name}"
2020-11-10 10:46:20 +01:00
},
"build_type" : func () string {
2022-10-21 17:03:11 -04:00
return "${build.type}"
2020-11-10 10:46:20 +01:00
},
2020-08-25 10:51:43 +02:00
}
2021-02-23 16:51:13 +01:00
tpl , err := texttemplate . New ( "hcl2_upgrade" ).
Funcs ( funcMap ).
Parse ( string ( s ))
if err != nil {
2021-03-22 02:56:30 -07:00
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 )
}
2021-02-23 16:51:13 +01:00
}
2022-10-21 16:54:21 -04:00
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 ())
2021-02-23 16:51:13 +01:00
str := & bytes . Buffer {}
2022-10-21 16:54:21 -04:00
if err := tpl . Execute ( str , nil ); err != nil {
2021-03-19 02:24:49 -07:00
return fallbackReturn ( err , s )
2021-02-23 16:51:13 +01:00
}
out := str . Bytes ()
if funcErrors . Len () > 0 {
return append ([] byte ( fmt . Sprintf ( "\n%s" , funcErrors )), out ... )
}
return out
2021-02-08 11:28:26 +01:00
}
// 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 ) {
2021-02-23 14:18:43 +01:00
setIsLocal := func ( a ... string ) string {
2021-02-08 11:28:26 +01:00
isLocal = true
return ""
}
2021-02-23 14:18:43 +01:00
// 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
2021-02-23 16:51:13 +01:00
funcMap := texttemplate . FuncMap {
"aws_secretsmanager" : setIsLocal ,
"timestamp" : setIsLocal ,
"isotime" : setIsLocal ,
2021-08-25 14:20:08 +02:00
"strftime" : setIsLocal ,
2021-02-23 16:51:13 +01:00
"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
},
2021-02-22 17:14:01 +01:00
}
2020-08-25 10:51:43 +02:00
2020-11-10 10:46:20 +01:00
tpl , err := texttemplate . New ( "hcl2_upgrade" ).
2020-08-25 10:51:43 +02:00
Funcs ( funcMap ).
Parse ( string ( s ))
if err != nil {
2021-03-22 02:56:30 -07:00
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 )
}
2020-08-25 10:51:43 +02:00
}
2022-10-21 16:54:21 -04:00
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 ())
2020-08-25 10:51:43 +02:00
str := & bytes . Buffer {}
2022-10-21 16:54:21 -04:00
if err := tpl . Execute ( str , nil ); err != nil {
2021-03-19 02:24:49 -07:00
return isLocal , fallbackReturn ( err , s )
2020-08-25 10:51:43 +02:00
}
2021-02-08 11:28:26 +01:00
return isLocal , str . Bytes ()
2020-08-25 10:51:43 +02:00
}
2022-11-30 15:25:25 -05:00
// 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
}
2020-08-25 10:51:43 +02:00
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 {}:
2020-08-27 16:02:05 +02:00
var mostComplexElem interface {}
for _ , randomElem := range value {
2022-10-27 18:09:40 -04:00
if k == "linux_options" || k == "network_interface" || k == "shared_image_gallery" {
2021-08-25 14:20:08 +02:00
break
}
2020-08-27 16:02:05 +02:00
// 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
}
2020-08-25 10:51:43 +02:00
}
2020-08-27 16:02:05 +02:00
switch mostComplexElem .( type ) {
case string , int , float64 , bool :
2020-08-25 10:51:43 +02:00
out . SetAttributeValue ( k , hcl2shim . HCL2ValueFromConfigValue ( value ))
default :
nestedBlockBody := out . AppendNewBlock ( k , nil ). Body ()
jsonBodyToHCL2Body ( nestedBlockBody , value )
}
2020-08-27 16:02:05 +02:00
case map [ string ] string , map [ string ] int , map [ string ] float64 :
out . SetAttributeValue ( k , hcl2shim . HCL2ValueFromConfigValue ( value ))
2020-08-25 10:51:43 +02:00
case [] interface {}:
if len ( value ) == 0 {
continue
}
2020-08-27 16:02:05 +02:00
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 ) {
2020-08-25 10:51:43 +02:00
case map [ string ] interface {}:
2020-08-27 16:02:05 +02:00
// 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.
2020-08-25 10:51:43 +02:00
for i := range value {
value := value [ i ].( map [ string ] interface {})
nestedBlockBody := out . AppendNewBlock ( k , nil ). Body ()
jsonBodyToHCL2Body ( nestedBlockBody , value )
}
continue
default :
2020-08-27 16:02:05 +02:00
out . SetAttributeValue ( k , hcl2shim . HCL2ValueFromConfigValue ( value ))
2020-08-25 10:51:43 +02:00
}
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 := `
2021-02-15 02:03:44 -08:00
Usage: packer hcl2_upgrade [options] TEMPLATE
2020-08-25 10:51:43 +02:00
2020-10-14 14:52:17 -04:00
Will transform your JSON template into an HCL2 configuration.
2021-02-15 02:03:44 -08:00
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.
2020-08-25 10:51:43 +02:00
`
return strings . TrimSpace ( helpText )
}
func ( * HCL2UpgradeCommand ) Synopsis () string {
2020-10-14 14:52:17 -04:00
return "transform a JSON template into an HCL2 configuration"
2020-08-25 10:51:43 +02:00
}
func ( * HCL2UpgradeCommand ) AutocompleteArgs () complete . Predictor {
return complete . PredictNothing
}
func ( * HCL2UpgradeCommand ) AutocompleteFlags () complete . Flags {
return complete . Flags {}
}
2021-02-12 15:18:53 +01:00
2021-02-12 15:37:41 +01:00
// Specific blocks parser responsible to parse and write the block
2021-02-12 15:18:53 +01:00
type PackerParser struct {
2021-02-12 15:37:41 +01:00
WithAnnotations bool
2021-02-12 15:18:53 +01:00
out [] byte
}
func ( p * PackerParser ) Parse ( tpl * template . Template ) error {
2023-07-14 17:11:54 -04:00
reqPlugins , err := p . generateRequiredPluginsBlock ( tpl )
if err != nil {
return err
2021-02-12 15:18:53 +01:00
}
2023-07-14 17:11:54 -04:00
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 ()
2021-02-12 15:18:53 +01:00
return nil
}
2023-07-14 17:11:54 -04:00
func gatherPluginsFromTemplate ( tpl * template . Template ) [] string {
plugins := map [ string ] struct {}{}
for _ , b := range tpl . Builders {
2023-10-19 11:00:41 -04:00
name := knownPluginComponent ( b . Type )
if name == unknownPluginName {
continue
2023-07-14 17:11:54 -04:00
}
2023-10-19 11:00:41 -04:00
plugins [ knownPlugins [ name ]] = struct {}{}
2023-07-14 17:11:54 -04:00
}
for _ , p := range tpl . Provisioners {
2023-10-19 11:00:41 -04:00
name := knownPluginComponent ( p . Type )
if name == unknownPluginName {
continue
2023-07-14 17:11:54 -04:00
}
2023-10-19 11:00:41 -04:00
plugins [ knownPlugins [ name ]] = struct {}{}
2023-07-14 17:11:54 -04:00
}
for _ , pps := range tpl . PostProcessors {
for _ , pp := range pps {
2023-10-19 11:00:41 -04:00
name := knownPluginComponent ( pp . Type )
if name == unknownPluginName {
continue
2023-07-14 17:11:54 -04:00
}
2023-10-19 11:00:41 -04:00
plugins [ knownPlugins [ name ]] = struct {}{}
2023-07-14 17:11:54 -04:00
}
}
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
}
2021-02-12 15:18:53 +01:00
func ( p * PackerParser ) Write ( out * bytes . Buffer ) {
if len ( p . out ) > 0 {
2021-02-12 15:37:41 +01:00
if p . WithAnnotations {
2021-02-12 15:18:53 +01:00
out . Write ([] byte ( packerBlockHeader ))
}
out . Write ( p . out )
}
}
type VariableParser struct {
2021-02-12 15:37:41 +01:00
WithAnnotations bool
2021-02-12 15:18:53 +01:00
variablesOut [] byte
localsOut [] byte
}
2021-03-19 02:24:49 -07:00
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 ()
}
2021-02-12 15:18:53 +01:00
func ( p * VariableParser ) Parse ( tpl * template . Template ) error {
2021-03-19 02:24:49 -07:00
// Output Locals and Local blocks
2021-02-12 15:18:53 +01:00
localsContent := hclwrite . NewEmptyFile ()
localsBody := localsContent . Body ()
localsBody . AppendNewline ()
localBody := localsBody . AppendNewBlock ( "locals" , nil ). Body ()
2021-03-19 02:24:49 -07:00
hasLocals := false
2021-02-12 15:18:53 +01:00
if len ( p . variablesOut ) == 0 {
p . variablesOut = [] byte {}
}
if len ( p . localsOut ) == 0 {
p . localsOut = [] byte {}
}
2023-02-07 13:23:01 -05:00
if len ( tpl . Variables ) == 0 {
tpl . Variables = make ( map [ string ] * template . Variable )
}
2022-11-30 15:25:25 -05:00
// 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
}
2021-02-12 15:18:53 +01:00
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 {
2021-03-19 02:24:49 -07:00
// Create new HCL2 "variables" block, and populate the "value"
// field with the "Default" value from the JSON variable.
2021-02-12 15:18:53 +01:00
2021-03-19 02:24:49 -07:00
// Interpolate Jsonval first as an hcl variable to determine if it is
2022-11-30 15:25:25 -05:00
// 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.
2021-03-19 02:24:49 -07:00
isLocal , _ := variableTransposeTemplatingCalls ([] byte ( variable . Default ))
2021-02-12 15:18:53 +01:00
sensitive := false
if isSensitiveVariable ( variable . Key , tpl . SensitiveVariables ) {
sensitive = true
}
2021-03-19 02:24:49 -07:00
// Create final HCL block and append.
2021-02-12 15:18:53 +01:00
if isLocal {
2021-03-19 02:24:49 -07:00
sensitiveBlocks := makeLocal ( variable , sensitive , localBody , localsContent , & hasLocals )
if len ( sensitiveBlocks ) > 0 {
p . localsOut = append ( p . localsOut , transposeTemplatingCalls ( sensitiveBlocks ) ... )
2021-02-12 15:18:53 +01:00
}
continue
}
2021-03-19 02:24:49 -07:00
varbytes := makeVariable ( variable , sensitive )
_ , out := variableTransposeTemplatingCalls ( varbytes )
2021-02-12 15:18:53 +01:00
p . variablesOut = append ( p . variablesOut , out ... )
}
2021-03-19 02:24:49 -07:00
if hasLocals == true {
2021-02-12 15:18:53 +01:00
p . localsOut = append ( p . localsOut , transposeTemplatingCalls ( localsContent . Bytes ()) ... )
}
2021-03-19 02:24:49 -07:00
2021-02-12 15:18:53 +01:00
return nil
}
func ( p * VariableParser ) Write ( out * bytes . Buffer ) {
if len ( p . variablesOut ) > 0 {
2021-02-12 15:37:41 +01:00
if p . WithAnnotations {
2021-02-12 15:18:53 +01:00
out . Write ([] byte ( inputVarHeader ))
}
out . Write ( p . variablesOut )
}
}
type LocalsParser struct {
2021-02-12 15:37:41 +01:00
WithAnnotations bool
2021-02-12 15:18:53 +01:00
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" ))
2021-02-12 15:37:41 +01:00
if p . WithAnnotations {
2021-02-12 15:18:53 +01:00
fmt . Fprintln ( out , `# "timestamp" template function replacement` )
}
fmt . Fprintln ( out , `locals { timestamp = regex_replace(timestamp(), "[- TZ:]", "") }` )
}
2021-03-17 16:06:19 -07:00
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.` )
}
2021-08-25 14:20:08 +02:00
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.` )
}
2021-02-12 15:18:53 +01:00
if len ( p . LocalsOut ) > 0 {
2021-02-12 15:37:41 +01:00
if p . WithAnnotations {
2021-02-12 15:18:53 +01:00
out . Write ([] byte ( localsVarHeader ))
}
out . Write ( p . LocalsOut )
}
}
type AmazonSecretsDatasourceParser struct {
2021-02-12 15:37:41 +01:00
WithAnnotations bool
2021-02-12 15:18:53 +01:00
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 {
2021-02-12 15:37:41 +01:00
if p . WithAnnotations {
2021-02-12 15:18:53 +01:00
out . Write ([] byte ( amazonSecretsManagerDataHeader ))
}
out . Write ( p . out )
}
}
type AmazonAmiDatasourceParser struct {
2021-02-12 15:37:41 +01:00
Builders [] * template . Builder
WithAnnotations bool
2021-02-12 15:18:53 +01:00
out [] byte
}
func ( p * AmazonAmiDatasourceParser ) Parse ( _ * template . Template ) error {
if p . out == nil {
p . out = [] byte {}
}
amazonAmiFilters := [] map [ string ] interface {}{}
i := 1
2021-02-12 15:37:41 +01:00
for _ , builder := range p . Builders {
2021-02-12 15:18:53 +01:00
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 )
}
2021-02-15 17:35:03 +01:00
sourceAmiFilterCfg , err := copyAWSAccessConfig ( sourceAmiFilterCfg , builder . Config )
if err != nil {
return err
}
2021-02-12 15:18:53 +01:00
duplicate := false
dataSourceName := fmt . Sprintf ( "autogenerated_%d" , i )
for j , filter := range amazonAmiFilters {
2021-02-15 17:35:03 +01:00
if reflect . DeepEqual ( filter , sourceAmiFilterCfg ) {
2021-02-12 15:18:53 +01:00
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
}
2021-02-15 17:35:03 +01:00
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
}
2021-02-12 15:18:53 +01:00
func ( p * AmazonAmiDatasourceParser ) Write ( out * bytes . Buffer ) {
if len ( p . out ) > 0 {
2021-02-12 15:37:41 +01:00
if p . WithAnnotations {
2021-02-12 15:18:53 +01:00
out . Write ([] byte ( amazonAmiDataHeader ))
}
out . Write ( p . out )
}
}
type SourceParser struct {
2021-02-12 15:37:41 +01:00
Builders [] * template . Builder
BuilderPlugins packer . BuilderSet
WithAnnotations bool
2021-02-12 15:18:53 +01:00
out [] byte
}
func ( p * SourceParser ) Parse ( tpl * template . Template ) error {
if p . out == nil {
p . out = [] byte {}
}
2023-10-19 11:00:41 -04:00
var unknownBuilders [] string
2021-02-12 15:18:53 +01:00
for i , builderCfg := range p . Builders {
sourcesContent := hclwrite . NewEmptyFile ()
body := sourcesContent . Body ()
body . AppendNewline ()
2023-10-19 11:00:41 -04:00
if ! p . BuilderPlugins . Has ( builderCfg . Type ) && knownPluginComponent ( builderCfg . Type ) == unknownPluginName {
2021-06-03 13:41:16 +02:00
unknownBuilders = append ( unknownBuilders , builderCfg . Type )
2021-02-12 15:18:53 +01:00
}
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 ()) ... )
}
2023-10-19 11:00:41 -04:00
// TODO update to output to stderr as opposed to having the command exit 1
2021-06-03 13:41:16 +02:00
if len ( unknownBuilders ) > 0 {
return fmt . Errorf ( "unknown builder type(s): %v\n" , unknownBuilders )
}
2023-10-19 11:00:41 -04:00
2021-02-12 15:18:53 +01:00
return nil
}
func ( p * SourceParser ) Write ( out * bytes . Buffer ) {
if len ( p . out ) > 0 {
2021-02-12 15:37:41 +01:00
if p . WithAnnotations {
2021-02-12 15:18:53 +01:00
out . Write ([] byte ( sourcesHeader ))
}
out . Write ( p . out )
}
}
type BuildParser struct {
2021-02-12 15:37:41 +01:00
Builders [] * template . Builder
WithAnnotations bool
2021-02-12 15:18:53 +01:00
provisioners BlockParser
postProcessors BlockParser
2021-02-12 15:37:41 +01:00
out [] byte
2021-02-12 15:18:53 +01:00
}
func ( p * BuildParser ) Parse ( tpl * template . Template ) error {
2021-02-22 16:07:37 +01:00
if len ( p . Builders ) == 0 {
return nil
}
2021-02-12 15:18:53 +01:00
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 {
2021-02-12 15:37:41 +01:00
WithAnnotations : p . WithAnnotations ,
2021-02-12 15:18:53 +01:00
}
if err := p . provisioners . Parse ( tpl ); err != nil {
return err
}
p . postProcessors = & PostProcessorParser {
2021-02-12 15:37:41 +01:00
WithAnnotations : p . WithAnnotations ,
2021-02-12 15:18:53 +01:00
}
if err := p . postProcessors . Parse ( tpl ); err != nil {
return err
}
return nil
}
func ( p * BuildParser ) Write ( out * bytes . Buffer ) {
if len ( p . out ) > 0 {
2021-02-12 15:37:41 +01:00
if p . WithAnnotations {
2021-02-12 15:18:53 +01:00
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 {
2021-02-12 15:37:41 +01:00
WithAnnotations bool
2021-02-12 15:18:53 +01:00
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 })
2021-03-05 11:10:00 +01:00
2021-02-12 15:18:53 +01:00
cfg := provisioner . Config
2021-03-05 11:10:00 +01:00
if cfg == nil {
cfg = map [ string ] interface {}{}
}
2021-02-12 15:18:53 +01:00
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 ()
}
2021-02-18 15:05:12 +01:00
if provisioner . PauseBefore > 0 {
cfg [ "pause_before" ] = provisioner . PauseBefore . String ()
}
2021-02-12 15:18:53 +01:00
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 {
2021-02-12 15:37:41 +01:00
WithAnnotations bool
2021-02-12 15:18:53 +01:00
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
2021-03-05 11:10:00 +01:00
if cfg == nil {
cfg = map [ string ] interface {}{}
}
2021-02-12 15:18:53 +01:00
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 )
}
}
2021-02-15 01:40:21 -08:00
2021-03-22 02:56:30 -07:00
func fixQuoting ( old string ) string {
// This regex captures golang template functions that use escaped quotes:
// {{ env \"myvar\" }}
2021-06-14 18:58:32 +02:00
// {{ split `some-string` \"-\" 0 }}
re := regexp . MustCompile ( ` {{ \ s *\ w * ( \ s * ( \\ ".*\\" ) \ s * ) +\ w *\ s * }} ` )
2021-03-22 02:56:30 -07:00
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 )
}
2023-10-19 11:00:41 -04:00
func knownPluginComponent ( component string ) string {
for prefix := range knownPlugins {
if strings . HasPrefix ( component , prefix ) {
return prefix
}
}
return unknownPluginName
}