2020-08-25 04:51:43 -04:00
package command
import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
texttemplate "text/template"
"github.com/hashicorp/hcl/v2/hclwrite"
2020-12-17 16:29:25 -05:00
"github.com/hashicorp/packer-plugin-sdk/template"
2020-08-25 04:51:43 -04:00
hcl2shim "github.com/hashicorp/packer/hcl2template/shim"
"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 (
2020-10-14 14:52:17 -04:00
hcl2UpgradeFileHeader = ` # This file was autogenerated by the BETA ' packer hcl2_upgrade ' command . We
2020-08-25 04:51:43 -04: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 06:39:54 -05:00
# Avoid mixing go templating calls ( for example ` + " ` ` ` { { upper ( ` string ` ) } } ` ` ` " + ` )
2020-11-10 04:46:20 -05: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 04:51:43 -04:00
# constraints documentation
2021-01-14 17:38:28 -05:00
# https : //www.packer.io//docs/templates/hcl_templates/variables#type-constraints for more info.
2020-11-10 04:46:20 -05:00
`
packerBlockHeader = `
2021-01-14 17:38:28 -05:00
# See https : //www.packer.io//docs/templates/hcl_templates/blocks/packer for more info
2020-08-25 04:51:43 -04: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 04:51:43 -04:00
# source . Read the documentation for source blocks here :
2021-01-14 17:38:28 -05:00
# https : //www.packer.io//docs/templates/hcl_templates/blocks/source`
2020-08-25 04:51:43 -04: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 04:51:43 -04:00
# documentation for build blocks can be found here :
2021-01-14 17:38:28 -05:00
# https : //www.packer.io//docs/templates/hcl_templates/blocks/build
2020-08-25 04:51:43 -04:00
build {
`
)
func ( c * HCL2UpgradeCommand ) RunContext ( buildCtx context . Context , cla * HCL2UpgradeArgs ) int {
out := & bytes . Buffer { }
var output io . Writer
if err := os . MkdirAll ( filepath . Dir ( cla . OutputFile ) , 0 ) ; 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 _ , 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 {
return ret
}
core := hdl . ( * CoreWrapper ) . Core
if err := core . Initialize ( ) ; err != nil {
2020-11-10 04:46:20 -05:00
c . Ui . Error ( fmt . Sprintf ( "Ignoring following initialization error: %v" , err ) )
2020-08-25 04:51:43 -04:00
}
tpl := core . Template
2020-11-10 04:46:20 -05:00
// Packer section
if tpl . MinVersion != "" {
out . Write ( [ ] byte ( packerBlockHeader ) )
fileContent := hclwrite . NewEmptyFile ( )
body := fileContent . Body ( )
packerBody := body . AppendNewBlock ( "packer" , nil ) . Body ( )
packerBody . SetAttributeValue ( "required_version" , cty . StringVal ( fmt . Sprintf ( ">= %s" , tpl . MinVersion ) ) )
out . Write ( fileContent . Bytes ( ) )
}
out . Write ( [ ] byte ( inputVarHeader ) )
2020-08-25 04:51:43 -04:00
// Output variables section
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 {
variablesContent := hclwrite . NewEmptyFile ( )
variablesBody := variablesContent . Body ( )
variableBody := variablesBody . AppendNewBlock ( "variable" , [ ] string { variable . Key } ) . Body ( )
variableBody . SetAttributeRaw ( "type" , hclwrite . Tokens { & hclwrite . Token { Bytes : [ ] byte ( "string" ) } } )
if variable . Default != "" || ! variable . Required {
variableBody . SetAttributeValue ( "default" , hcl2shim . HCL2ValueFromConfigValue ( variable . Default ) )
}
if isSensitiveVariable ( variable . Key , tpl . SensitiveVariables ) {
variableBody . SetAttributeValue ( "sensitive" , cty . BoolVal ( true ) )
}
variablesBody . AppendNewline ( )
out . Write ( transposeTemplatingCalls ( variablesContent . Bytes ( ) ) )
}
fmt . Fprintln ( out , ` # "timestamp" template function replacement ` )
fmt . Fprintln ( out , ` locals { timestamp = regex_replace(timestamp(), "[- TZ:]", "") } ` )
// Output sources section
builders := [ ] * template . Builder { }
{
// sort builders to avoid map's randomnes
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
} )
}
out . Write ( [ ] byte ( sourcesHeader ) )
for i , builderCfg := range builders {
sourcesContent := hclwrite . NewEmptyFile ( )
body := sourcesContent . Body ( )
body . AppendNewline ( )
if ! c . Meta . CoreConfig . Components . BuilderStore . Has ( builderCfg . Type ) {
c . Ui . Error ( fmt . Sprintf ( "unknown builder type: %q\n" , builderCfg . Type ) )
return 1
}
if builderCfg . Name == "" || builderCfg . Name == builderCfg . Type {
builderCfg . Name = fmt . Sprintf ( "autogenerated_%d" , i + 1 )
}
sourceBody := body . AppendNewBlock ( "source" , [ ] string { builderCfg . Type , builderCfg . Name } ) . Body ( )
jsonBodyToHCL2Body ( sourceBody , builderCfg . Config )
_ , _ = out . Write ( transposeTemplatingCalls ( sourcesContent . Bytes ( ) ) )
}
// Output build section
out . Write ( [ ] byte ( buildHeader ) )
buildContent := hclwrite . NewEmptyFile ( )
buildBody := buildContent . Body ( )
if tpl . Description != "" {
buildBody . SetAttributeValue ( "description" , cty . StringVal ( tpl . Description ) )
buildBody . AppendNewline ( )
}
sourceNames := [ ] string { }
for _ , builder := range builders {
sourceNames = append ( sourceNames , fmt . Sprintf ( "source.%s.%s" , builder . Type , builder . Name ) )
}
buildBody . SetAttributeValue ( "sources" , hcl2shim . HCL2ValueFromConfigValue ( sourceNames ) )
buildBody . AppendNewline ( )
_ , _ = buildContent . WriteTo ( out )
for _ , provisioner := range tpl . Provisioners {
provisionerContent := hclwrite . NewEmptyFile ( )
body := provisionerContent . Body ( )
buildBody . AppendNewline ( )
block := body . AppendNewBlock ( "provisioner" , [ ] string { provisioner . Type } )
cfg := provisioner . Config
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 ( )
}
jsonBodyToHCL2Body ( block . Body ( ) , cfg )
out . Write ( transposeTemplatingCalls ( provisionerContent . Bytes ( ) ) )
}
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 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 )
}
_ , _ = out . Write ( transposeTemplatingCalls ( postProcessorContent . Bytes ( ) ) )
}
_ , _ = out . Write ( [ ] byte ( "}\n" ) )
_ , _ = output . Write ( hclwrite . Format ( out . Bytes ( ) ) )
c . Ui . Say ( fmt . Sprintf ( "Successfully created %s " , cla . OutputFile ) )
return 0
}
2020-11-10 04:46:20 -05: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 )
}
2020-08-25 04:51:43 -04: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 {
fallbackReturn := func ( err error ) [ ] byte {
2020-11-10 04:46:20 -05:00
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 ... )
2020-08-25 04:51:43 -04:00
}
funcMap := texttemplate . FuncMap {
"timestamp" : func ( ) string {
return "${local.timestamp}"
} ,
"isotime" : func ( ) string {
return "${local.timestamp}"
} ,
"user" : func ( in string ) string {
return fmt . Sprintf ( "${var.%s}" , in )
} ,
"env" : func ( in string ) string {
2020-11-11 14:54:22 -05:00
return fmt . Sprintf ( "${env(%q)}" , in )
2020-08-25 04:51:43 -04:00
} ,
"build" : func ( a string ) string {
return fmt . Sprintf ( "${build.%s}" , a )
} ,
2020-11-10 04:46:20 -05:00
"template_dir" : func ( ) string {
return fmt . Sprintf ( "${path.root}" )
} ,
"pwd" : func ( ) string {
return fmt . Sprintf ( "${path.cwd}" )
} ,
"packer_version" : func ( ) string {
return fmt . Sprintf ( "${packer.version}" )
} ,
"uuid" : func ( ) string {
return fmt . Sprintf ( "${uuidv4()}" )
} ,
"lower" : func ( _ string ) ( string , error ) {
return "" , UnhandleableArgumentError {
"lower" ,
"`lower(var.example)`" ,
2021-01-14 17:38:28 -05:00
"https://www.packer.io//docs/templates/hcl_templates/functions/string/lower" ,
2020-11-10 04:46:20 -05:00
}
} ,
"upper" : func ( _ string ) ( string , error ) {
return "" , UnhandleableArgumentError {
"upper" ,
"`upper(var.example)`" ,
2021-01-14 17:38:28 -05:00
"https://www.packer.io//docs/templates/hcl_templates/functions/string/upper" ,
2020-11-10 04:46:20 -05:00
}
} ,
"split" : func ( _ , _ string , _ int ) ( string , error ) {
return "" , UnhandleableArgumentError {
"split" ,
"`split(separator, string)`" ,
2021-01-14 17:38:28 -05:00
"https://www.packer.io//docs/templates/hcl_templates/functions/string/split" ,
2020-11-10 04:46:20 -05:00
}
} ,
"replace" : func ( _ , _ , _ string , _ int ) ( string , error ) {
return "" , UnhandleableArgumentError {
"replace" ,
"`replace(string, substring, replacement)` or `regex_replace(string, substring, replacement)`" ,
2021-01-14 17:38:28 -05: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" ,
2020-11-10 04:46:20 -05:00
}
} ,
"replace_all" : func ( _ , _ , _ string ) ( string , error ) {
return "" , UnhandleableArgumentError {
"replace_all" ,
"`replace(string, substring, replacement)` or `regex_replace(string, substring, replacement)`" ,
2021-01-14 17:38:28 -05: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" ,
2020-11-10 04:46:20 -05:00
}
} ,
"clean_resource_name" : func ( _ string ) ( string , error ) {
return "" , UnhandleableArgumentError {
"clean_resource_name" ,
"use custom validation rules, `replace(string, substring, replacement)` or `regex_replace(string, substring, replacement)`" ,
2021-01-14 17:38:28 -05: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" ,
2020-11-10 04:46:20 -05:00
}
} ,
"build_name" : func ( ) string {
return fmt . Sprintf ( "${build.name}" )
} ,
"build_type" : func ( ) string {
return fmt . Sprintf ( "${build.type}" )
} ,
2020-08-25 04:51:43 -04:00
}
2020-11-10 04:46:20 -05:00
tpl , err := texttemplate . New ( "hcl2_upgrade" ) .
2020-08-25 04:51:43 -04:00
Funcs ( funcMap ) .
Parse ( string ( s ) )
if err != nil {
return fallbackReturn ( err )
}
str := & bytes . Buffer { }
v := struct {
HTTPIP string
HTTPPort string
} {
HTTPIP : "{{ .HTTPIP }}" ,
HTTPPort : "{{ .HTTPPort }}" ,
}
if err := tpl . Execute ( str , v ) ; err != nil {
return fallbackReturn ( err )
}
return 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 { } :
2020-08-27 10:02:05 -04:00
var mostComplexElem interface { }
for _ , randomElem := range value {
// 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 04:51:43 -04:00
}
2020-08-27 10:02:05 -04:00
switch mostComplexElem . ( type ) {
case string , int , float64 , bool :
2020-08-25 04:51:43 -04:00
out . SetAttributeValue ( k , hcl2shim . HCL2ValueFromConfigValue ( value ) )
default :
nestedBlockBody := out . AppendNewBlock ( k , nil ) . Body ( )
jsonBodyToHCL2Body ( nestedBlockBody , value )
}
2020-08-27 10:02:05 -04:00
case map [ string ] string , map [ string ] int , map [ string ] float64 :
out . SetAttributeValue ( k , hcl2shim . HCL2ValueFromConfigValue ( value ) )
2020-08-25 04:51:43 -04:00
case [ ] interface { } :
if len ( value ) == 0 {
continue
}
2020-08-27 10:02:05 -04: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 04:51:43 -04:00
case map [ string ] interface { } :
2020-08-27 10:02:05 -04: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 04:51:43 -04: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 10:02:05 -04:00
out . SetAttributeValue ( k , hcl2shim . HCL2ValueFromConfigValue ( value ) )
2020-08-25 04:51:43 -04: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 := `
Usage : packer hcl2_upgrade - output - file = JSON_TEMPLATE . pkr . hcl JSON_TEMPLATE ...
2020-10-14 14:52:17 -04:00
Will transform your JSON template into an HCL2 configuration .
2020-08-25 04:51:43 -04: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 04:51:43 -04:00
}
func ( * HCL2UpgradeCommand ) AutocompleteArgs ( ) complete . Predictor {
return complete . PredictNothing
}
func ( * HCL2UpgradeCommand ) AutocompleteFlags ( ) complete . Flags {
return complete . Flags { }
}