// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 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" 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" ) 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 ) // 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", "virtualbox": "github.com/hashicorp/virtualbox", "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 } 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{ // Note: this is always true here as the DAG is only usable for // HCL2 configs, so since the command only works on JSON templates, // we can safely use the phased approach, which changes nothing. UseSequential: true, }); 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 { name := knownPluginComponent(b.Type) if name == unknownPluginName { continue } plugins[knownPlugins[name]] = struct{}{} } for _, p := range tpl.Provisioners { name := knownPluginComponent(p.Type) if name == unknownPluginName { continue } plugins[knownPlugins[name]] = struct{}{} } for _, pps := range tpl.PostProcessors { for _, pp := range pps { name := knownPluginComponent(pp.Type) if name == unknownPluginName { continue } plugins[knownPlugins[name]] = 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 } type AssumeRoleConfig struct { AssumeRoleARN string `mapstructure:"role_arn" required:"false"` AssumeRoleDurationSeconds int `mapstructure:"duration_seconds" required:"false"` AssumeRoleExternalID string `mapstructure:"external_id" required:"false"` AssumeRolePolicy string `mapstructure:"policy" required:"false"` AssumeRolePolicyARNs []string `mapstructure:"policy_arns" required:"false"` AssumeRoleSessionName string `mapstructure:"session_name" required:"false"` AssumeRoleTags map[string]string `mapstructure:"tags" required:"false"` AssumeRoleTransitiveTagKeys []string `mapstructure:"transitive_tag_keys" required:"false"` } type VaultAWSEngineOptions struct { Name string `mapstructure:"name"` RoleARN string `mapstructure:"role_arn"` TTL string `mapstructure:"ttl" required:"false"` EngineName string `mapstructure:"engine_name"` } type AWSPollingConfig struct { MaxAttempts int `mapstructure:"max_attempts" required:"false"` DelaySeconds int `mapstructure:"delay_seconds" required:"false"` } type AwsAccessConfig struct { AccessKey string `mapstructure:"access_key" required:"true"` AssumeRole AssumeRoleConfig `mapstructure:"assume_role" required:"false"` CustomEndpointEc2 string `mapstructure:"custom_endpoint_ec2" required:"false"` CredsFilename string `mapstructure:"shared_credentials_file" required:"false"` DecodeAuthZMessages bool `mapstructure:"decode_authorization_messages" required:"false"` InsecureSkipTLSVerify bool `mapstructure:"insecure_skip_tls_verify" required:"false"` MaxRetries int `mapstructure:"max_retries" required:"false"` MFACode string `mapstructure:"mfa_code" required:"false"` ProfileName string `mapstructure:"profile" required:"false"` RawRegion string `mapstructure:"region" required:"true"` SecretKey string `mapstructure:"secret_key" required:"true"` SkipMetadataApiCheck bool `mapstructure:"skip_metadata_api_check"` SkipCredsValidation bool `mapstructure:"skip_credential_validation"` Token string `mapstructure:"token" required:"false"` VaultAWSEngine VaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false"` PollingConfig *AWSPollingConfig `mapstructure:"aws_polling" required:"false"` } 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(AwsAccessConfig{}, &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 { if p.out == nil { p.out = []byte{} } var unknownBuilders []string for i, builderCfg := range p.Builders { sourcesContent := hclwrite.NewEmptyFile() body := sourcesContent.Body() body.AppendNewline() if !p.BuilderPlugins.Has(builderCfg.Type) && knownPluginComponent(builderCfg.Type) == unknownPluginName { 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())...) } // TODO update to output to stderr as opposed to having the command exit 1 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) } func knownPluginComponent(component string) string { for prefix := range knownPlugins { if strings.HasPrefix(component, prefix) { return prefix } } return unknownPluginName }