github.com/hashicorp/packer@v1.14.3/command/hcl2_upgrade.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package command 5 6 import ( 7 "bytes" 8 "context" 9 "fmt" 10 "io" 11 "os" 12 "path/filepath" 13 "reflect" 14 "regexp" 15 "sort" 16 "strconv" 17 "strings" 18 texttemplate "text/template" 19 "text/template/parse" 20 21 "github.com/hashicorp/go-multierror" 22 "github.com/hashicorp/hcl/v2/hclwrite" 23 hcl2shim "github.com/hashicorp/packer-plugin-sdk/hcl2helper" 24 "github.com/hashicorp/packer-plugin-sdk/template" 25 "github.com/hashicorp/packer/packer" 26 "github.com/mitchellh/mapstructure" 27 "github.com/posener/complete" 28 "github.com/zclconf/go-cty/cty" 29 ) 30 31 const ( 32 hcl2UpgradeFileHeader = `# This file was autogenerated by the 'packer hcl2_upgrade' command. We 33 # recommend double checking that everything is correct before going forward. We 34 # also recommend treating this file as disposable. The HCL2 blocks in this 35 # file can be moved to other files. For example, the variable blocks could be 36 # moved to their own 'variables.pkr.hcl' file, etc. Those files need to be 37 # suffixed with '.pkr.hcl' to be visible to Packer. To use multiple files at 38 # once they also need to be in the same folder. 'packer inspect folder/' 39 # will describe to you what is in that folder. 40 41 # Avoid mixing go templating calls ( for example ` + "```{{ upper(`string`) }}```" + ` ) 42 # and HCL2 calls (for example '${ var.string_value_example }' ). They won't be 43 # executed together and the outcome will be unknown. 44 ` 45 inputVarHeader = ` 46 # All generated input variables will be of 'string' type as this is how Packer JSON 47 # views them; you can change their type later on. Read the variables type 48 # constraints documentation 49 # https://www.packer.io/docs/templates/hcl_templates/variables#type-constraints for more info.` 50 localsVarHeader = ` 51 # All locals variables are generated from variables that uses expressions 52 # that are not allowed in HCL2 variables. 53 # Read the documentation for locals blocks here: 54 # https://www.packer.io/docs/templates/hcl_templates/blocks/locals` 55 packerBlockHeader = ` 56 # See https://www.packer.io/docs/templates/hcl_templates/blocks/packer for more info 57 ` 58 59 sourcesHeader = ` 60 # source blocks are generated from your builders; a source can be referenced in 61 # build blocks. A build block runs provisioner and post-processors on a 62 # source. Read the documentation for source blocks here: 63 # https://www.packer.io/docs/templates/hcl_templates/blocks/source` 64 65 buildHeader = ` 66 # a build block invokes sources and runs provisioning steps on them. The 67 # documentation for build blocks can be found here: 68 # https://www.packer.io/docs/templates/hcl_templates/blocks/build 69 ` 70 71 amazonAmiDataHeader = ` 72 # The amazon-ami data block is generated from your amazon builder source_ami_filter; a data 73 # from this block can be referenced in source and locals blocks. 74 # Read the documentation for data blocks here: 75 # https://www.packer.io/docs/templates/hcl_templates/blocks/data 76 # Read the documentation for the Amazon AMI Data Source here: 77 # https://www.packer.io/plugins/datasources/amazon/ami` 78 79 amazonSecretsManagerDataHeader = ` 80 # The amazon-secretsmanager data block is generated from your aws_secretsmanager template function; a data 81 # from this block can be referenced in source and locals blocks. 82 # Read the documentation for data blocks here: 83 # https://www.packer.io/docs/templates/hcl_templates/blocks/data 84 # Read the documentation for the Amazon Secrets Manager Data Source here: 85 # https://www.packer.io/plugins/datasources/amazon/secretsmanager` 86 ) 87 88 var ( 89 amazonSecretsManagerMap = map[string]map[string]interface{}{} 90 localsVariableMap = map[string]string{} 91 timestamp = false 92 isotime = false 93 strftime = false 94 ) 95 96 // knownPlugins represent the HashiCorp maintained plugins the we can confidently 97 // construct a required plugins block for. 98 var knownPlugins = map[string]string{ 99 "amazon": "github.com/hashicorp/amazon", 100 "ansible": "github.com/hashicorp/ansible", 101 "azure": "github.com/hashicorp/azure", 102 "docker": "github.com/hashicorp/docker", 103 "googlecompute": "github.com/hashicorp/googlecompute", 104 "qemu": "github.com/hashicorp/qemu", 105 "vagrant": "github.com/hashicorp/vagrant", 106 "virtualbox": "github.com/hashicorp/virtualbox", 107 "vmware": "github.com/hashicorp/vmware", 108 "vsphere": "github.com/hashicorp/vsphere", 109 } 110 111 // unknownPluginName represents any plugin not in knownPlugins or bundled into Packer 112 const unknownPluginName string = "unknown" 113 114 type HCL2UpgradeCommand struct { 115 Meta 116 } 117 118 func (c *HCL2UpgradeCommand) Run(args []string) int { 119 ctx, cleanup := handleTermInterrupt(c.Ui) 120 defer cleanup() 121 122 cfg, ret := c.ParseArgs(args) 123 if ret != 0 { 124 return ret 125 } 126 127 return c.RunContext(ctx, cfg) 128 } 129 130 func (c *HCL2UpgradeCommand) ParseArgs(args []string) (*HCL2UpgradeArgs, int) { 131 var cfg HCL2UpgradeArgs 132 flags := c.Meta.FlagSet("hcl2_upgrade") 133 flags.Usage = func() { c.Ui.Say(c.Help()) } 134 cfg.AddFlagSets(flags) 135 if err := flags.Parse(args); err != nil { 136 return &cfg, 1 137 } 138 args = flags.Args() 139 if len(args) != 1 { 140 flags.Usage() 141 return &cfg, 1 142 } 143 cfg.Path = args[0] 144 if cfg.OutputFile == "" { 145 cfg.OutputFile = cfg.Path + ".pkr.hcl" 146 } 147 return &cfg, 0 148 } 149 150 type BlockParser interface { 151 Parse(*template.Template) error 152 Write(*bytes.Buffer) 153 } 154 155 func (c *HCL2UpgradeCommand) RunContext(_ context.Context, cla *HCL2UpgradeArgs) int { 156 var output io.Writer 157 if err := os.MkdirAll(filepath.Dir(cla.OutputFile), 0755); err != nil { 158 c.Ui.Error(fmt.Sprintf("Failed to create output directory: %v", err)) 159 return 1 160 } 161 if f, err := os.Create(cla.OutputFile); err == nil { 162 output = f 163 defer f.Close() 164 } else { 165 c.Ui.Error(fmt.Sprintf("Failed to create output file: %v", err)) 166 return 1 167 } 168 169 if cla.WithAnnotations { 170 if _, err := output.Write([]byte(hcl2UpgradeFileHeader)); err != nil { 171 c.Ui.Error(fmt.Sprintf("Failed to write to file: %v", err)) 172 return 1 173 } 174 } 175 176 hdl, ret := c.GetConfigFromJSON(&cla.MetaArgs) 177 if ret != 0 { 178 c.Ui.Error("Failed to get config from JSON") 179 return 1 180 } 181 182 core := hdl.(*packer.Core) 183 if err := core.Initialize(packer.InitializeOptions{ 184 // Note: this is always true here as the DAG is only usable for 185 // HCL2 configs, so since the command only works on JSON templates, 186 // we can safely use the phased approach, which changes nothing. 187 UseSequential: true, 188 }); err != nil { 189 c.Ui.Error(fmt.Sprintf("Ignoring following initialization error: %v", err)) 190 } 191 tpl := core.Template 192 193 // Parse blocks 194 packerBlock := &PackerParser{ 195 WithAnnotations: cla.WithAnnotations, 196 } 197 if err := packerBlock.Parse(tpl); err != nil { 198 c.Ui.Error(fmt.Sprintf("Ignoring following Parse error: %v", err)) 199 ret = 1 200 } 201 202 variables := &VariableParser{ 203 WithAnnotations: cla.WithAnnotations, 204 } 205 if err := variables.Parse(tpl); err != nil { 206 c.Ui.Error(fmt.Sprintf("Ignoring following variables.Parse error: %v", err)) 207 ret = 1 208 } 209 210 locals := &LocalsParser{ 211 LocalsOut: variables.localsOut, 212 WithAnnotations: cla.WithAnnotations, 213 } 214 if err := locals.Parse(tpl); err != nil { 215 c.Ui.Error(fmt.Sprintf("Ignoring following locals.Parse error: %v", err)) 216 ret = 1 217 } 218 219 builders := []*template.Builder{} 220 { 221 // sort builders to avoid map's randomness 222 for _, builder := range tpl.Builders { 223 builders = append(builders, builder) 224 } 225 } 226 sort.Slice(builders, func(i, j int) bool { 227 return builders[i].Type+builders[i].Name < builders[j].Type+builders[j].Name 228 }) 229 230 amazonAmiDatasource := &AmazonAmiDatasourceParser{ 231 Builders: builders, 232 WithAnnotations: cla.WithAnnotations, 233 } 234 if err := amazonAmiDatasource.Parse(tpl); err != nil { 235 c.Ui.Error(fmt.Sprintf("Ignoring following amazonAmiDatasource.Parse error: %v", err)) 236 ret = 1 237 } 238 239 sources := &SourceParser{ 240 Builders: builders, 241 BuilderPlugins: c.Meta.CoreConfig.Components.PluginConfig.Builders, 242 WithAnnotations: cla.WithAnnotations, 243 } 244 if err := sources.Parse(tpl); err != nil { 245 c.Ui.Error(fmt.Sprintf("Ignoring following sources.Parse error: %v", err)) 246 ret = 1 247 } 248 249 build := &BuildParser{ 250 Builders: builders, 251 WithAnnotations: cla.WithAnnotations, 252 } 253 if err := build.Parse(tpl); err != nil { 254 c.Ui.Error(fmt.Sprintf("Ignoring following build.Parse error: %v", err)) 255 ret = 1 256 } 257 258 amazonSecretsDatasource := &AmazonSecretsDatasourceParser{ 259 WithAnnotations: cla.WithAnnotations, 260 } 261 if err := amazonSecretsDatasource.Parse(tpl); err != nil { 262 c.Ui.Error(fmt.Sprintf("Ignoring following amazonSecretsDatasource.Parse error: %v", err)) 263 ret = 1 264 } 265 266 // Write file 267 out := &bytes.Buffer{} 268 for _, block := range []BlockParser{ 269 packerBlock, 270 variables, 271 amazonSecretsDatasource, 272 amazonAmiDatasource, 273 locals, 274 sources, 275 build, 276 } { 277 block.Write(out) 278 } 279 280 if _, err := output.Write(hclwrite.Format(out.Bytes())); err != nil { 281 c.Ui.Error(fmt.Sprintf("Failed to write to file: %v", err)) 282 return 1 283 } 284 285 c.Ui.Say(fmt.Sprintf("Successfully created %s. Exit %d", cla.OutputFile, ret)) 286 return ret 287 } 288 289 type UnhandleableArgumentError struct { 290 Call string 291 Correspondance string 292 Docs string 293 } 294 295 func (uc UnhandleableArgumentError) Error() string { 296 return fmt.Sprintf(`unhandled %q call: 297 # there is no way to automatically upgrade the %[1]q call. 298 # Please manually upgrade to %s 299 # Visit %s for more infos.`, uc.Call, uc.Correspondance, uc.Docs) 300 } 301 302 func fallbackReturn(err error, s []byte) []byte { 303 if strings.Contains(err.Error(), "unhandled") { 304 return append([]byte(fmt.Sprintf("\n# %s\n", err)), s...) 305 } 306 307 return append([]byte(fmt.Sprintf("\n# could not parse template for following block: %q\n", err)), s...) 308 } 309 310 // reTemplate writes a new template to `out` and escapes all unknown variables 311 // so that we don't interpret them later on when interpreting the template 312 func reTemplate(nd parse.Node, out io.Writer, funcs texttemplate.FuncMap) error { 313 switch node := nd.(type) { 314 case *parse.ActionNode: 315 // Leave pipes as-is 316 if len(node.Pipe.Cmds) > 1 { 317 fmt.Fprintf(out, "%s", node.String()) 318 return nil 319 } 320 321 cmd := node.Pipe.Cmds[0] 322 args := cmd.Args 323 if len(args) > 1 { 324 // Function calls with parameters are left aside 325 fmt.Fprintf(out, "%s", node.String()) 326 return nil 327 } 328 329 _, ok := funcs[args[0].String()] 330 if ok { 331 // Known functions left as-is 332 fmt.Fprintf(out, "%s", node.String()) 333 return nil 334 } 335 336 // Escape anything that isn't in the func map 337 fmt.Fprintf(out, "{{ \"{{\" }} %s {{ \"}}\" }}", cmd.String()) 338 339 // TODO maybe node.Pipe.Decls? Though in Packer templates they're not 340 // supported officially so they can be left aside for now 341 case *parse.ListNode: 342 for _, child := range node.Nodes { 343 err := reTemplate(child, out, funcs) 344 if err != nil { 345 return err 346 } 347 } 348 case *parse.TextNode: 349 _, err := fmt.Fprintf(out, "%s", node.Text) 350 if err != nil { 351 return err 352 } 353 default: 354 return fmt.Errorf("unhandled node type %s", reflect.TypeOf(nd)) 355 } 356 return nil 357 } 358 359 // transposeTemplatingCalls executes parts of blocks as go template files and replaces 360 // their result with their hcl2 variant. If something goes wrong the template 361 // containing the go template string is returned. 362 func transposeTemplatingCalls(s []byte) []byte { 363 funcErrors := &multierror.Error{ 364 ErrorFormat: func(es []error) string { 365 if len(es) == 1 { 366 return fmt.Sprintf("# 1 error occurred upgrading the following block:\n\t# %s\n", es[0]) 367 } 368 369 points := make([]string, len(es)) 370 for i, err := range es { 371 if i == len(es)-1 { 372 points[i] = fmt.Sprintf("# %s", err) 373 continue 374 } 375 points[i] = fmt.Sprintf("# %s\n", err) 376 } 377 378 return fmt.Sprintf( 379 "# %d errors occurred upgrading the following block:\n\t%s", 380 len(es), strings.Join(points, "\n\t")) 381 }, 382 } 383 384 funcMap := texttemplate.FuncMap{ 385 "aws_secretsmanager": func(a ...string) string { 386 if len(a) == 2 { 387 for key, config := range amazonSecretsManagerMap { 388 nameOk := config["name"] == a[0] 389 keyOk := config["key"] == a[1] 390 if nameOk && keyOk { 391 return fmt.Sprintf("${data.amazon-secretsmanager.%s.value}", key) 392 } 393 } 394 id := fmt.Sprintf("autogenerated_%d", len(amazonSecretsManagerMap)+1) 395 amazonSecretsManagerMap[id] = map[string]interface{}{ 396 "name": a[0], 397 "key": a[1], 398 } 399 return fmt.Sprintf("${data.amazon-secretsmanager.%s.value}", id) 400 } 401 for key, config := range amazonSecretsManagerMap { 402 nameOk := config["name"] == a[0] 403 if nameOk { 404 return fmt.Sprintf("${data.amazon-secretsmanager.%s.value}", key) 405 } 406 } 407 id := fmt.Sprintf("autogenerated_%d", len(amazonSecretsManagerMap)+1) 408 amazonSecretsManagerMap[id] = map[string]interface{}{ 409 "name": a[0], 410 } 411 return fmt.Sprintf("${data.amazon-secretsmanager.%s.value}", id) 412 }, 413 "timestamp": func() string { 414 timestamp = true 415 return "${local.timestamp}" 416 }, 417 "isotime": func(a ...string) string { 418 if len(a) == 0 { 419 // returns rfc3339 formatted string. 420 return "${timestamp()}" 421 } 422 // otherwise a valid isotime func has one input. 423 isotime = true 424 return fmt.Sprintf("${legacy_isotime(\"%s\")}", a[0]) 425 426 }, 427 "strftime": func(a ...string) string { 428 if len(a) == 0 { 429 // returns rfc3339 formatted string. 430 return "${timestamp()}" 431 } 432 strftime = true 433 return fmt.Sprintf("${legacy_strftime(\"%s\")}", a[0]) 434 }, 435 "user": func(in string) string { 436 if _, ok := localsVariableMap[in]; ok { 437 // variable is now a local 438 return fmt.Sprintf("${local.%s}", in) 439 } 440 return fmt.Sprintf("${var.%s}", in) 441 }, 442 "env": func(in string) string { 443 return fmt.Sprintf("${env(%q)}", in) 444 }, 445 "build": func(a string) string { 446 return fmt.Sprintf("${build.%s}", a) 447 }, 448 "data": func(a string) string { 449 return fmt.Sprintf("${data.%s}", a) 450 }, 451 "template_dir": func() string { 452 return "${path.root}" 453 }, 454 "pwd": func() string { 455 return "${path.cwd}" 456 }, 457 "packer_version": func() string { 458 return "${packer.version}" 459 }, 460 "uuid": func() string { 461 return "${uuidv4()}" 462 }, 463 "lower": func(a string) (string, error) { 464 funcErrors = multierror.Append(funcErrors, UnhandleableArgumentError{ 465 "lower", 466 "`lower(var.example)`", 467 "https://www.packer.io/docs/templates/hcl_templates/functions/string/lower", 468 }) 469 return fmt.Sprintf("{{ lower `%s` }}", a), nil 470 }, 471 "upper": func(a string) (string, error) { 472 funcErrors = multierror.Append(funcErrors, UnhandleableArgumentError{ 473 "upper", 474 "`upper(var.example)`", 475 "https://www.packer.io/docs/templates/hcl_templates/functions/string/upper", 476 }) 477 return fmt.Sprintf("{{ upper `%s` }}", a), nil 478 }, 479 "split": func(a, b string, n int) (string, error) { 480 funcErrors = multierror.Append(funcErrors, UnhandleableArgumentError{ 481 "split", 482 "`split(separator, string)`", 483 "https://www.packer.io/docs/templates/hcl_templates/functions/string/split", 484 }) 485 return fmt.Sprintf("{{ split `%s` `%s` %d }}", a, b, n), nil 486 }, 487 "replace": func(a, b string, n int, c string) (string, error) { 488 funcErrors = multierror.Append(funcErrors, UnhandleableArgumentError{ 489 "replace", 490 "`replace(string, substring, replacement)` or `regex_replace(string, substring, replacement)`", 491 "https://www.packer.io/docs/templates/hcl_templates/functions/string/replace or https://www.packer.io/docs/templates/hcl_templates/functions/string/regex_replace", 492 }) 493 return fmt.Sprintf("{{ replace `%s` `%s` `%s` %d }}", a, b, c, n), nil 494 }, 495 "replace_all": func(a, b, c string) (string, error) { 496 funcErrors = multierror.Append(funcErrors, UnhandleableArgumentError{ 497 "replace_all", 498 "`replace(string, substring, replacement)` or `regex_replace(string, substring, replacement)`", 499 "https://www.packer.io/docs/templates/hcl_templates/functions/string/replace or https://www.packer.io/docs/templates/hcl_templates/functions/string/regex_replace", 500 }) 501 return fmt.Sprintf("{{ replace_all `%s` `%s` `%s` }}", a, b, c), nil 502 }, 503 "clean_resource_name": func(a string) (string, error) { 504 funcErrors = multierror.Append(funcErrors, UnhandleableArgumentError{ 505 "clean_resource_name", 506 "use custom validation rules, `replace(string, substring, replacement)` or `regex_replace(string, substring, replacement)`", 507 "https://packer.io/docs/templates/hcl_templates/variables#custom-validation-rules" + 508 " , https://www.packer.io/docs/templates/hcl_templates/functions/string/replace" + 509 " or https://www.packer.io/docs/templates/hcl_templates/functions/string/regex_replace", 510 }) 511 return fmt.Sprintf("{{ clean_resource_name `%s` }}", a), nil 512 }, 513 "build_name": func() string { 514 return "${build.name}" 515 }, 516 "build_type": func() string { 517 return "${build.type}" 518 }, 519 } 520 521 tpl, err := texttemplate.New("hcl2_upgrade"). 522 Funcs(funcMap). 523 Parse(string(s)) 524 525 if err != nil { 526 if strings.Contains(err.Error(), "unexpected \"\\\\\" in operand") { 527 // This error occurs if the operand in the text template used 528 // escaped quoting \" instead of bactick quoting ` 529 // Create a regex to do a string replace on this block, to fix 530 // quoting. 531 q := fixQuoting(string(s)) 532 unquoted := []byte(q) 533 tpl, err = texttemplate.New("hcl2_upgrade"). 534 Funcs(funcMap). 535 Parse(string(unquoted)) 536 if err != nil { 537 return fallbackReturn(err, unquoted) 538 } 539 } else { 540 return fallbackReturn(err, s) 541 } 542 } 543 544 retempl := &bytes.Buffer{} 545 if err := reTemplate(tpl.Root, retempl, funcMap); err != nil { 546 return fallbackReturn(err, s) 547 } 548 549 tpl, err = texttemplate.New("hcl2_upgrade"). 550 Funcs(funcMap). 551 Parse(retempl.String()) 552 553 str := &bytes.Buffer{} 554 555 if err := tpl.Execute(str, nil); err != nil { 556 return fallbackReturn(err, s) 557 } 558 559 out := str.Bytes() 560 561 if funcErrors.Len() > 0 { 562 return append([]byte(fmt.Sprintf("\n%s", funcErrors)), out...) 563 } 564 return out 565 } 566 567 // variableTransposeTemplatingCalls executes parts of blocks as go template files and replaces 568 // their result with their hcl2 variant for variables block only. If something goes wrong the template 569 // containing the go template string is returned. 570 // In variableTransposeTemplatingCalls the definition of aws_secretsmanager function will create a data source 571 // with the same name as the variable. 572 func variableTransposeTemplatingCalls(s []byte) (isLocal bool, body []byte) { 573 setIsLocal := func(a ...string) string { 574 isLocal = true 575 return "" 576 } 577 578 // Make locals from variables using valid template engine, 579 // expect the ones using only 'env' 580 // ref: https://www.packer.io/docs/templates/legacy_json_templates/engine#template-engine 581 funcMap := texttemplate.FuncMap{ 582 "aws_secretsmanager": setIsLocal, 583 "timestamp": setIsLocal, 584 "isotime": setIsLocal, 585 "strftime": setIsLocal, 586 "user": setIsLocal, 587 "env": func(in string) string { 588 return fmt.Sprintf("${env(%q)}", in) 589 }, 590 "template_dir": setIsLocal, 591 "pwd": setIsLocal, 592 "packer_version": setIsLocal, 593 "uuid": setIsLocal, 594 "lower": setIsLocal, 595 "upper": setIsLocal, 596 "split": func(_, _ string, _ int) (string, error) { 597 isLocal = true 598 return "", nil 599 }, 600 "replace": func(_, _ string, _ int, _ string) (string, error) { 601 isLocal = true 602 return "", nil 603 }, 604 "replace_all": func(_, _, _ string) (string, error) { 605 isLocal = true 606 return "", nil 607 }, 608 } 609 610 tpl, err := texttemplate.New("hcl2_upgrade"). 611 Funcs(funcMap). 612 Parse(string(s)) 613 614 if err != nil { 615 if strings.Contains(err.Error(), "unexpected \"\\\\\" in operand") { 616 // This error occurs if the operand in the text template used 617 // escaped quoting \" instead of bactick quoting ` 618 // Create a regex to do a string replace on this block, to fix 619 // quoting. 620 q := fixQuoting(string(s)) 621 unquoted := []byte(q) 622 tpl, err = texttemplate.New("hcl2_upgrade"). 623 Funcs(funcMap). 624 Parse(string(unquoted)) 625 if err != nil { 626 return isLocal, fallbackReturn(err, unquoted) 627 } 628 } else { 629 return isLocal, fallbackReturn(err, s) 630 } 631 } 632 633 retempl := &bytes.Buffer{} 634 if err := reTemplate(tpl.Root, retempl, funcMap); err != nil { 635 return isLocal, fallbackReturn(err, s) 636 } 637 638 tpl, err = texttemplate.New("hcl2_upgrade"). 639 Funcs(funcMap). 640 Parse(retempl.String()) 641 642 str := &bytes.Buffer{} 643 if err := tpl.Execute(str, nil); err != nil { 644 return isLocal, fallbackReturn(err, s) 645 } 646 647 return isLocal, str.Bytes() 648 } 649 650 // referencedUserVariables executes parts of blocks as go template files finding user variables referenced 651 // within the template. This function should be called once to extract those variables referenced via the {{user `...`}} 652 // template function. The resulting map will contain variables defined in the JSON variables property, and some that 653 // are declared via var-files; to avoid duplicates the results of this function should be reconciled against tpl.Variables. 654 func referencedUserVariables(s []byte) map[string]*template.Variable { 655 userVars := make([]string, 0) 656 funcMap := texttemplate.FuncMap{ 657 "user": func(in string) string { 658 userVars = append(userVars, in) 659 return "" 660 }, 661 } 662 663 tpl, err := texttemplate.New("hcl2_upgrade"). 664 Funcs(funcMap). 665 Parse(string(s)) 666 if err != nil { 667 return nil 668 } 669 670 if err := tpl.Execute(&bytes.Buffer{}, nil); err != nil { 671 return nil 672 } 673 674 vars := make(map[string]*template.Variable) 675 for _, v := range userVars { 676 vars[v] = &template.Variable{ 677 Key: v, 678 Required: true, 679 } 680 } 681 return vars 682 } 683 684 func jsonBodyToHCL2Body(out *hclwrite.Body, kvs map[string]interface{}) { 685 ks := []string{} 686 for k := range kvs { 687 ks = append(ks, k) 688 } 689 sort.Strings(ks) 690 691 for _, k := range ks { 692 value := kvs[k] 693 694 switch value := value.(type) { 695 case map[string]interface{}: 696 var mostComplexElem interface{} 697 for _, randomElem := range value { 698 if k == "linux_options" || k == "network_interface" || k == "shared_image_gallery" { 699 break 700 } 701 // HACK: we take the most complex element of that map because 702 // in HCL2, map of objects can be bodies, for example: 703 // map containing object: source_ami_filter {} ( body ) 704 // simple string/string map: tags = {} ) ( attribute ) 705 // 706 // if we could not find an object in this map then it's most 707 // likely a plain map and so we guess it should be and 708 // attribute. Though now if value refers to something that is 709 // an object but only contains a string or a bool; we could 710 // generate a faulty object. For example a (somewhat invalid) 711 // source_ami_filter where only `most_recent` is set. 712 switch randomElem.(type) { 713 case string, int, float64, bool: 714 if mostComplexElem != nil { 715 continue 716 } 717 mostComplexElem = randomElem 718 default: 719 mostComplexElem = randomElem 720 } 721 } 722 723 switch mostComplexElem.(type) { 724 case string, int, float64, bool: 725 out.SetAttributeValue(k, hcl2shim.HCL2ValueFromConfigValue(value)) 726 default: 727 nestedBlockBody := out.AppendNewBlock(k, nil).Body() 728 jsonBodyToHCL2Body(nestedBlockBody, value) 729 } 730 case map[string]string, map[string]int, map[string]float64: 731 out.SetAttributeValue(k, hcl2shim.HCL2ValueFromConfigValue(value)) 732 case []interface{}: 733 if len(value) == 0 { 734 continue 735 } 736 737 var mostComplexElem interface{} 738 for _, randomElem := range value { 739 // HACK: we take the most complex element of that slice because 740 // in hcl2 slices of plain types can be arrays, for example: 741 // simple string type: owners = ["0000000000"] 742 // object: launch_block_device_mappings {} 743 switch randomElem.(type) { 744 case string, int, float64, bool: 745 if mostComplexElem != nil { 746 continue 747 } 748 mostComplexElem = randomElem 749 default: 750 mostComplexElem = randomElem 751 } 752 } 753 switch mostComplexElem.(type) { 754 case map[string]interface{}: 755 // this is an object in a slice; so we unwrap it. We 756 // could try to remove any 's' suffix in the key, but 757 // this might not work everywhere. 758 for i := range value { 759 value := value[i].(map[string]interface{}) 760 nestedBlockBody := out.AppendNewBlock(k, nil).Body() 761 jsonBodyToHCL2Body(nestedBlockBody, value) 762 } 763 continue 764 default: 765 out.SetAttributeValue(k, hcl2shim.HCL2ValueFromConfigValue(value)) 766 } 767 default: 768 out.SetAttributeValue(k, hcl2shim.HCL2ValueFromConfigValue(value)) 769 } 770 } 771 } 772 773 func isSensitiveVariable(key string, vars []*template.Variable) bool { 774 for _, v := range vars { 775 if v.Key == key { 776 return true 777 } 778 } 779 return false 780 } 781 782 func (*HCL2UpgradeCommand) Help() string { 783 helpText := ` 784 Usage: packer hcl2_upgrade [options] TEMPLATE 785 786 Will transform your JSON template into an HCL2 configuration. 787 788 Options: 789 790 -output-file=path Set output file name. By default this will be the 791 TEMPLATE name with ".pkr.hcl" appended to it. To be a 792 valid Packer HCL template, it must have the suffix 793 ".pkr.hcl" 794 -with-annotations Add helper annotation comments to the file to help new 795 HCL2 users understand the template format. 796 ` 797 798 return strings.TrimSpace(helpText) 799 } 800 801 func (*HCL2UpgradeCommand) Synopsis() string { 802 return "transform a JSON template into an HCL2 configuration" 803 } 804 805 func (*HCL2UpgradeCommand) AutocompleteArgs() complete.Predictor { 806 return complete.PredictNothing 807 } 808 809 func (*HCL2UpgradeCommand) AutocompleteFlags() complete.Flags { 810 return complete.Flags{} 811 } 812 813 // Specific blocks parser responsible to parse and write the block 814 815 type PackerParser struct { 816 WithAnnotations bool 817 out []byte 818 } 819 820 func (p *PackerParser) Parse(tpl *template.Template) error { 821 reqPlugins, err := p.generateRequiredPluginsBlock(tpl) 822 if err != nil { 823 return err 824 } 825 826 if tpl.MinVersion == "" && reqPlugins == nil { 827 return nil 828 } 829 830 fileContent := hclwrite.NewEmptyFile() 831 body := fileContent.Body() 832 packerBody := body.AppendNewBlock("packer", nil).Body() 833 834 if tpl.MinVersion != "" { 835 packerBody.SetAttributeValue("required_version", cty.StringVal(fmt.Sprintf(">= %s", tpl.MinVersion))) 836 } 837 838 if reqPlugins != nil { 839 packerBody.AppendBlock(reqPlugins) 840 } 841 842 p.out = fileContent.Bytes() 843 844 return nil 845 } 846 847 func gatherPluginsFromTemplate(tpl *template.Template) []string { 848 plugins := map[string]struct{}{} 849 850 for _, b := range tpl.Builders { 851 name := knownPluginComponent(b.Type) 852 if name == unknownPluginName { 853 continue 854 } 855 plugins[knownPlugins[name]] = struct{}{} 856 } 857 858 for _, p := range tpl.Provisioners { 859 name := knownPluginComponent(p.Type) 860 if name == unknownPluginName { 861 continue 862 } 863 plugins[knownPlugins[name]] = struct{}{} 864 } 865 866 for _, pps := range tpl.PostProcessors { 867 for _, pp := range pps { 868 name := knownPluginComponent(pp.Type) 869 if name == unknownPluginName { 870 continue 871 } 872 plugins[knownPlugins[name]] = struct{}{} 873 } 874 } 875 876 if len(plugins) == 0 { 877 return nil 878 } 879 880 retPlugins := make([]string, 0, len(plugins)) 881 for plugin := range plugins { 882 retPlugins = append(retPlugins, plugin) 883 } 884 885 sort.Strings(retPlugins) 886 887 return retPlugins 888 } 889 890 func (p *PackerParser) generateRequiredPluginsBlock(tpl *template.Template) (*hclwrite.Block, error) { 891 plugins := gatherPluginsFromTemplate(tpl) 892 if len(plugins) == 0 { 893 return nil, nil 894 } 895 896 reqPlugins := hclwrite.NewBlock("required_plugins", nil) 897 for _, plugin := range plugins { 898 pluginBlock := cty.ObjectVal(map[string]cty.Value{ 899 "source": cty.StringVal(plugin), 900 "version": cty.StringVal("~> 1"), 901 }) 902 reqPlugins.Body().SetAttributeValue(strings.Replace(plugin, "github.com/hashicorp/", "", 1), pluginBlock) 903 } 904 905 return reqPlugins, nil 906 } 907 908 func (p *PackerParser) Write(out *bytes.Buffer) { 909 if len(p.out) > 0 { 910 if p.WithAnnotations { 911 out.Write([]byte(packerBlockHeader)) 912 } 913 out.Write(p.out) 914 } 915 } 916 917 type VariableParser struct { 918 WithAnnotations bool 919 variablesOut []byte 920 localsOut []byte 921 } 922 923 func makeLocal(variable *template.Variable, sensitive bool, localBody *hclwrite.Body, localsContent *hclwrite.File, hasLocals *bool) []byte { 924 if sensitive { 925 // Create Local block because this is sensitive 926 sensitiveLocalContent := hclwrite.NewEmptyFile() 927 body := sensitiveLocalContent.Body() 928 body.AppendNewline() 929 sensitiveLocalBody := body.AppendNewBlock("local", []string{variable.Key}).Body() 930 sensitiveLocalBody.SetAttributeValue("sensitive", cty.BoolVal(true)) 931 sensitiveLocalBody.SetAttributeValue("expression", hcl2shim.HCL2ValueFromConfigValue(variable.Default)) 932 localsVariableMap[variable.Key] = "local" 933 return sensitiveLocalContent.Bytes() 934 } 935 localBody.SetAttributeValue(variable.Key, hcl2shim.HCL2ValueFromConfigValue(variable.Default)) 936 localsVariableMap[variable.Key] = "locals" 937 *hasLocals = true 938 return []byte{} 939 } 940 941 func makeVariable(variable *template.Variable, sensitive bool) []byte { 942 variablesContent := hclwrite.NewEmptyFile() 943 variablesBody := variablesContent.Body() 944 variablesBody.AppendNewline() 945 variableBody := variablesBody.AppendNewBlock("variable", []string{variable.Key}).Body() 946 variableBody.SetAttributeRaw("type", hclwrite.Tokens{&hclwrite.Token{Bytes: []byte("string")}}) 947 948 if variable.Default != "" || !variable.Required { 949 shimmed := hcl2shim.HCL2ValueFromConfigValue(variable.Default) 950 variableBody.SetAttributeValue("default", shimmed) 951 } 952 if sensitive { 953 variableBody.SetAttributeValue("sensitive", cty.BoolVal(true)) 954 } 955 956 return variablesContent.Bytes() 957 } 958 959 func (p *VariableParser) Parse(tpl *template.Template) error { 960 // Output Locals and Local blocks 961 localsContent := hclwrite.NewEmptyFile() 962 localsBody := localsContent.Body() 963 localsBody.AppendNewline() 964 localBody := localsBody.AppendNewBlock("locals", nil).Body() 965 hasLocals := false 966 967 if len(p.variablesOut) == 0 { 968 p.variablesOut = []byte{} 969 } 970 if len(p.localsOut) == 0 { 971 p.localsOut = []byte{} 972 } 973 974 if len(tpl.Variables) == 0 { 975 tpl.Variables = make(map[string]*template.Variable) 976 } 977 // JSON supports variable declaration via var-files. 978 // User variables that might be defined in a var-file 979 // but not in the actual JSON template should be accounted for. 980 userVars := referencedUserVariables(tpl.RawContents) 981 for name, variable := range userVars { 982 if _, ok := tpl.Variables[name]; ok { 983 continue 984 } 985 tpl.Variables[name] = variable 986 } 987 988 variables := []*template.Variable{} 989 { 990 // sort variables to avoid map's randomness 991 for _, variable := range tpl.Variables { 992 variables = append(variables, variable) 993 } 994 sort.Slice(variables, func(i, j int) bool { 995 return variables[i].Key < variables[j].Key 996 }) 997 } 998 999 for _, variable := range variables { 1000 // Create new HCL2 "variables" block, and populate the "value" 1001 // field with the "Default" value from the JSON variable. 1002 1003 // Interpolate Jsonval first as an hcl variable to determine if it is 1004 // a local. Variables referencing some form of variable expression must be defined as a local in HCL2, 1005 // as variables in HCL2 must have a known value at parsing time. 1006 isLocal, _ := variableTransposeTemplatingCalls([]byte(variable.Default)) 1007 sensitive := false 1008 if isSensitiveVariable(variable.Key, tpl.SensitiveVariables) { 1009 sensitive = true 1010 } 1011 // Create final HCL block and append. 1012 if isLocal { 1013 sensitiveBlocks := makeLocal(variable, sensitive, localBody, localsContent, &hasLocals) 1014 if len(sensitiveBlocks) > 0 { 1015 p.localsOut = append(p.localsOut, transposeTemplatingCalls(sensitiveBlocks)...) 1016 } 1017 continue 1018 } 1019 varbytes := makeVariable(variable, sensitive) 1020 _, out := variableTransposeTemplatingCalls(varbytes) 1021 p.variablesOut = append(p.variablesOut, out...) 1022 } 1023 1024 if hasLocals == true { 1025 p.localsOut = append(p.localsOut, transposeTemplatingCalls(localsContent.Bytes())...) 1026 } 1027 1028 return nil 1029 } 1030 1031 func (p *VariableParser) Write(out *bytes.Buffer) { 1032 if len(p.variablesOut) > 0 { 1033 if p.WithAnnotations { 1034 out.Write([]byte(inputVarHeader)) 1035 } 1036 out.Write(p.variablesOut) 1037 } 1038 } 1039 1040 type LocalsParser struct { 1041 WithAnnotations bool 1042 LocalsOut []byte 1043 } 1044 1045 func (p *LocalsParser) Parse(tpl *template.Template) error { 1046 // Locals where parsed with Variables 1047 return nil 1048 } 1049 1050 func (p *LocalsParser) Write(out *bytes.Buffer) { 1051 if timestamp { 1052 _, _ = out.Write([]byte("\n")) 1053 if p.WithAnnotations { 1054 fmt.Fprintln(out, `# "timestamp" template function replacement`) 1055 } 1056 fmt.Fprintln(out, `locals { timestamp = regex_replace(timestamp(), "[- TZ:]", "") }`) 1057 } 1058 if isotime { 1059 fmt.Fprintln(out, `# The "legacy_isotime" function has been provided for backwards compatability, but we recommend switching to the timestamp and formatdate functions.`) 1060 } 1061 if strftime { 1062 fmt.Fprintln(out, `# The "legacy_strftime" function has been provided for backwards compatability, but we recommend switching to the timestamp and formatdate functions.`) 1063 } 1064 if len(p.LocalsOut) > 0 { 1065 if p.WithAnnotations { 1066 out.Write([]byte(localsVarHeader)) 1067 } 1068 out.Write(p.LocalsOut) 1069 } 1070 } 1071 1072 type AmazonSecretsDatasourceParser struct { 1073 WithAnnotations bool 1074 out []byte 1075 } 1076 1077 func (p *AmazonSecretsDatasourceParser) Parse(_ *template.Template) error { 1078 if p.out == nil { 1079 p.out = []byte{} 1080 } 1081 1082 keys := make([]string, 0, len(amazonSecretsManagerMap)) 1083 for k := range amazonSecretsManagerMap { 1084 keys = append(keys, k) 1085 } 1086 sort.Strings(keys) 1087 1088 for _, dataSourceName := range keys { 1089 datasourceContent := hclwrite.NewEmptyFile() 1090 body := datasourceContent.Body() 1091 body.AppendNewline() 1092 datasourceBody := body.AppendNewBlock("data", []string{"amazon-secretsmanager", dataSourceName}).Body() 1093 jsonBodyToHCL2Body(datasourceBody, amazonSecretsManagerMap[dataSourceName]) 1094 p.out = append(p.out, datasourceContent.Bytes()...) 1095 } 1096 1097 return nil 1098 } 1099 1100 func (p *AmazonSecretsDatasourceParser) Write(out *bytes.Buffer) { 1101 if len(p.out) > 0 { 1102 if p.WithAnnotations { 1103 out.Write([]byte(amazonSecretsManagerDataHeader)) 1104 } 1105 out.Write(p.out) 1106 } 1107 } 1108 1109 type AmazonAmiDatasourceParser struct { 1110 Builders []*template.Builder 1111 WithAnnotations bool 1112 out []byte 1113 } 1114 1115 func (p *AmazonAmiDatasourceParser) Parse(_ *template.Template) error { 1116 if p.out == nil { 1117 p.out = []byte{} 1118 } 1119 1120 amazonAmiFilters := []map[string]interface{}{} 1121 i := 1 1122 for _, builder := range p.Builders { 1123 if strings.HasPrefix(builder.Type, "amazon-") { 1124 if sourceAmiFilter, ok := builder.Config["source_ami_filter"]; ok { 1125 sourceAmiFilterCfg := map[string]interface{}{} 1126 if err := mapstructure.Decode(sourceAmiFilter, &sourceAmiFilterCfg); err != nil { 1127 return fmt.Errorf("Failed to write amazon-ami data source: %v", err) 1128 } 1129 1130 sourceAmiFilterCfg, err := copyAWSAccessConfig(sourceAmiFilterCfg, builder.Config) 1131 if err != nil { 1132 return err 1133 } 1134 1135 duplicate := false 1136 dataSourceName := fmt.Sprintf("autogenerated_%d", i) 1137 for j, filter := range amazonAmiFilters { 1138 if reflect.DeepEqual(filter, sourceAmiFilterCfg) { 1139 duplicate = true 1140 dataSourceName = fmt.Sprintf("autogenerated_%d", j+1) 1141 continue 1142 } 1143 } 1144 1145 // This is a hack... 1146 // Use templating so that it could be correctly transformed later into a data resource 1147 sourceAmiDataRef := fmt.Sprintf("{{ data `amazon-ami.%s.id` }}", dataSourceName) 1148 1149 if duplicate { 1150 delete(builder.Config, "source_ami_filter") 1151 builder.Config["source_ami"] = sourceAmiDataRef 1152 continue 1153 } 1154 1155 amazonAmiFilters = append(amazonAmiFilters, sourceAmiFilterCfg) 1156 delete(builder.Config, "source_ami_filter") 1157 builder.Config["source_ami"] = sourceAmiDataRef 1158 i++ 1159 1160 datasourceContent := hclwrite.NewEmptyFile() 1161 body := datasourceContent.Body() 1162 body.AppendNewline() 1163 sourceBody := body.AppendNewBlock("data", []string{"amazon-ami", dataSourceName}).Body() 1164 jsonBodyToHCL2Body(sourceBody, sourceAmiFilterCfg) 1165 p.out = append(p.out, transposeTemplatingCalls(datasourceContent.Bytes())...) 1166 } 1167 } 1168 } 1169 return nil 1170 } 1171 1172 type AssumeRoleConfig struct { 1173 AssumeRoleARN string `mapstructure:"role_arn" required:"false"` 1174 AssumeRoleDurationSeconds int `mapstructure:"duration_seconds" required:"false"` 1175 AssumeRoleExternalID string `mapstructure:"external_id" required:"false"` 1176 AssumeRolePolicy string `mapstructure:"policy" required:"false"` 1177 AssumeRolePolicyARNs []string `mapstructure:"policy_arns" required:"false"` 1178 AssumeRoleSessionName string `mapstructure:"session_name" required:"false"` 1179 AssumeRoleTags map[string]string `mapstructure:"tags" required:"false"` 1180 AssumeRoleTransitiveTagKeys []string `mapstructure:"transitive_tag_keys" required:"false"` 1181 } 1182 1183 type VaultAWSEngineOptions struct { 1184 Name string `mapstructure:"name"` 1185 RoleARN string `mapstructure:"role_arn"` 1186 TTL string `mapstructure:"ttl" required:"false"` 1187 EngineName string `mapstructure:"engine_name"` 1188 } 1189 1190 type AWSPollingConfig struct { 1191 MaxAttempts int `mapstructure:"max_attempts" required:"false"` 1192 DelaySeconds int `mapstructure:"delay_seconds" required:"false"` 1193 } 1194 1195 type AwsAccessConfig struct { 1196 AccessKey string `mapstructure:"access_key" required:"true"` 1197 AssumeRole AssumeRoleConfig `mapstructure:"assume_role" required:"false"` 1198 CustomEndpointEc2 string `mapstructure:"custom_endpoint_ec2" required:"false"` 1199 CredsFilename string `mapstructure:"shared_credentials_file" required:"false"` 1200 DecodeAuthZMessages bool `mapstructure:"decode_authorization_messages" required:"false"` 1201 InsecureSkipTLSVerify bool `mapstructure:"insecure_skip_tls_verify" required:"false"` 1202 MaxRetries int `mapstructure:"max_retries" required:"false"` 1203 MFACode string `mapstructure:"mfa_code" required:"false"` 1204 ProfileName string `mapstructure:"profile" required:"false"` 1205 RawRegion string `mapstructure:"region" required:"true"` 1206 SecretKey string `mapstructure:"secret_key" required:"true"` 1207 SkipMetadataApiCheck bool `mapstructure:"skip_metadata_api_check"` 1208 SkipCredsValidation bool `mapstructure:"skip_credential_validation"` 1209 Token string `mapstructure:"token" required:"false"` 1210 VaultAWSEngine VaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false"` 1211 PollingConfig *AWSPollingConfig `mapstructure:"aws_polling" required:"false"` 1212 } 1213 1214 func copyAWSAccessConfig(sourceAmi map[string]interface{}, builder map[string]interface{}) (map[string]interface{}, error) { 1215 // Transform access config to a map 1216 accessConfigMap := map[string]interface{}{} 1217 if err := mapstructure.Decode(AwsAccessConfig{}, &accessConfigMap); err != nil { 1218 return sourceAmi, err 1219 } 1220 1221 for k := range accessConfigMap { 1222 // Copy only access config present in the builder 1223 if v, ok := builder[k]; ok { 1224 sourceAmi[k] = v 1225 } 1226 } 1227 1228 return sourceAmi, nil 1229 } 1230 1231 func (p *AmazonAmiDatasourceParser) Write(out *bytes.Buffer) { 1232 if len(p.out) > 0 { 1233 if p.WithAnnotations { 1234 out.Write([]byte(amazonAmiDataHeader)) 1235 } 1236 out.Write(p.out) 1237 } 1238 } 1239 1240 type SourceParser struct { 1241 Builders []*template.Builder 1242 BuilderPlugins packer.BuilderSet 1243 WithAnnotations bool 1244 out []byte 1245 } 1246 1247 func (p *SourceParser) Parse(tpl *template.Template) error { 1248 if p.out == nil { 1249 p.out = []byte{} 1250 } 1251 1252 var unknownBuilders []string 1253 for i, builderCfg := range p.Builders { 1254 sourcesContent := hclwrite.NewEmptyFile() 1255 body := sourcesContent.Body() 1256 body.AppendNewline() 1257 if !p.BuilderPlugins.Has(builderCfg.Type) && knownPluginComponent(builderCfg.Type) == unknownPluginName { 1258 unknownBuilders = append(unknownBuilders, builderCfg.Type) 1259 } 1260 if builderCfg.Name == "" || builderCfg.Name == builderCfg.Type { 1261 builderCfg.Name = fmt.Sprintf("autogenerated_%d", i+1) 1262 } 1263 builderCfg.Name = strings.ReplaceAll(strings.TrimSpace(builderCfg.Name), " ", "_") 1264 1265 sourceBody := body.AppendNewBlock("source", []string{builderCfg.Type, builderCfg.Name}).Body() 1266 1267 jsonBodyToHCL2Body(sourceBody, builderCfg.Config) 1268 1269 p.out = append(p.out, transposeTemplatingCalls(sourcesContent.Bytes())...) 1270 } 1271 // TODO update to output to stderr as opposed to having the command exit 1 1272 if len(unknownBuilders) > 0 { 1273 return fmt.Errorf("unknown builder type(s): %v\n", unknownBuilders) 1274 } 1275 1276 return nil 1277 } 1278 1279 func (p *SourceParser) Write(out *bytes.Buffer) { 1280 if len(p.out) > 0 { 1281 if p.WithAnnotations { 1282 out.Write([]byte(sourcesHeader)) 1283 } 1284 out.Write(p.out) 1285 } 1286 } 1287 1288 type BuildParser struct { 1289 Builders []*template.Builder 1290 WithAnnotations bool 1291 1292 provisioners BlockParser 1293 postProcessors BlockParser 1294 out []byte 1295 } 1296 1297 func (p *BuildParser) Parse(tpl *template.Template) error { 1298 if len(p.Builders) == 0 { 1299 return nil 1300 } 1301 1302 buildContent := hclwrite.NewEmptyFile() 1303 buildBody := buildContent.Body() 1304 if tpl.Description != "" { 1305 buildBody.SetAttributeValue("description", cty.StringVal(tpl.Description)) 1306 buildBody.AppendNewline() 1307 } 1308 1309 sourceNames := []string{} 1310 for _, builder := range p.Builders { 1311 sourceNames = append(sourceNames, fmt.Sprintf("source.%s.%s", builder.Type, builder.Name)) 1312 } 1313 buildBody.SetAttributeValue("sources", hcl2shim.HCL2ValueFromConfigValue(sourceNames)) 1314 buildBody.AppendNewline() 1315 p.out = buildContent.Bytes() 1316 1317 p.provisioners = &ProvisionerParser{ 1318 WithAnnotations: p.WithAnnotations, 1319 } 1320 if err := p.provisioners.Parse(tpl); err != nil { 1321 return err 1322 } 1323 1324 p.postProcessors = &PostProcessorParser{ 1325 WithAnnotations: p.WithAnnotations, 1326 } 1327 if err := p.postProcessors.Parse(tpl); err != nil { 1328 return err 1329 } 1330 1331 return nil 1332 } 1333 1334 func (p *BuildParser) Write(out *bytes.Buffer) { 1335 if len(p.out) > 0 { 1336 if p.WithAnnotations { 1337 out.Write([]byte(buildHeader)) 1338 } else { 1339 _, _ = out.Write([]byte("\n")) 1340 } 1341 _, _ = out.Write([]byte("build {\n")) 1342 out.Write(p.out) 1343 p.provisioners.Write(out) 1344 p.postProcessors.Write(out) 1345 _, _ = out.Write([]byte("}\n")) 1346 } 1347 } 1348 1349 type ProvisionerParser struct { 1350 WithAnnotations bool 1351 out []byte 1352 } 1353 1354 func (p *ProvisionerParser) Parse(tpl *template.Template) error { 1355 if p.out == nil { 1356 p.out = []byte{} 1357 } 1358 for _, provisioner := range tpl.Provisioners { 1359 contentBytes := writeProvisioner("provisioner", provisioner) 1360 p.out = append(p.out, transposeTemplatingCalls(contentBytes)...) 1361 } 1362 1363 if tpl.CleanupProvisioner != nil { 1364 contentBytes := writeProvisioner("error-cleanup-provisioner", tpl.CleanupProvisioner) 1365 p.out = append(p.out, transposeTemplatingCalls(contentBytes)...) 1366 } 1367 return nil 1368 } 1369 1370 func writeProvisioner(typeName string, provisioner *template.Provisioner) []byte { 1371 provisionerContent := hclwrite.NewEmptyFile() 1372 body := provisionerContent.Body() 1373 block := body.AppendNewBlock(typeName, []string{provisioner.Type}) 1374 1375 cfg := provisioner.Config 1376 if cfg == nil { 1377 cfg = map[string]interface{}{} 1378 } 1379 1380 if len(provisioner.Except) > 0 { 1381 cfg["except"] = provisioner.Except 1382 } 1383 if len(provisioner.Only) > 0 { 1384 cfg["only"] = provisioner.Only 1385 } 1386 if provisioner.MaxRetries != "" { 1387 cfg["max_retries"] = provisioner.MaxRetries 1388 } 1389 if provisioner.Timeout > 0 { 1390 cfg["timeout"] = provisioner.Timeout.String() 1391 } 1392 if provisioner.PauseBefore > 0 { 1393 cfg["pause_before"] = provisioner.PauseBefore.String() 1394 } 1395 body.AppendNewline() 1396 jsonBodyToHCL2Body(block.Body(), cfg) 1397 return provisionerContent.Bytes() 1398 } 1399 1400 func (p *ProvisionerParser) Write(out *bytes.Buffer) { 1401 if len(p.out) > 0 { 1402 out.Write(p.out) 1403 } 1404 } 1405 1406 type PostProcessorParser struct { 1407 WithAnnotations bool 1408 out []byte 1409 } 1410 1411 func (p *PostProcessorParser) Parse(tpl *template.Template) error { 1412 if p.out == nil { 1413 p.out = []byte{} 1414 } 1415 for _, pps := range tpl.PostProcessors { 1416 postProcessorContent := hclwrite.NewEmptyFile() 1417 body := postProcessorContent.Body() 1418 1419 switch len(pps) { 1420 case 0: 1421 continue 1422 case 1: 1423 default: 1424 body = body.AppendNewBlock("post-processors", nil).Body() 1425 } 1426 for _, pp := range pps { 1427 ppBody := body.AppendNewBlock("post-processor", []string{pp.Type}).Body() 1428 if pp.KeepInputArtifact != nil { 1429 ppBody.SetAttributeValue("keep_input_artifact", cty.BoolVal(*pp.KeepInputArtifact)) 1430 } 1431 cfg := pp.Config 1432 if cfg == nil { 1433 cfg = map[string]interface{}{} 1434 } 1435 1436 if len(pp.Except) > 0 { 1437 cfg["except"] = pp.Except 1438 } 1439 if len(pp.Only) > 0 { 1440 cfg["only"] = pp.Only 1441 } 1442 if pp.Name != "" && pp.Name != pp.Type { 1443 cfg["name"] = pp.Name 1444 } 1445 jsonBodyToHCL2Body(ppBody, cfg) 1446 } 1447 1448 p.out = append(p.out, transposeTemplatingCalls(postProcessorContent.Bytes())...) 1449 } 1450 return nil 1451 } 1452 1453 func (p *PostProcessorParser) Write(out *bytes.Buffer) { 1454 if len(p.out) > 0 { 1455 out.Write(p.out) 1456 } 1457 } 1458 1459 func fixQuoting(old string) string { 1460 // This regex captures golang template functions that use escaped quotes: 1461 // {{ env \"myvar\" }} 1462 // {{ split `some-string` \"-\" 0 }} 1463 re := regexp.MustCompile(`{{\s*\w*(\s*(\\".*\\")\s*)+\w*\s*}}`) 1464 1465 body := re.ReplaceAllFunc([]byte(old), func(s []byte) []byte { 1466 // Get the capture group 1467 group := re.ReplaceAllString(string(s), `$1`) 1468 1469 unquoted, err := strconv.Unquote(fmt.Sprintf("\"%s\"", group)) 1470 if err != nil { 1471 return s 1472 } 1473 return []byte(strings.Replace(string(s), group, unquoted, 1)) 1474 1475 }) 1476 1477 return string(body) 1478 } 1479 1480 func knownPluginComponent(component string) string { 1481 for prefix := range knownPlugins { 1482 if strings.HasPrefix(component, prefix) { 1483 return prefix 1484 } 1485 } 1486 return unknownPluginName 1487 }