packer console for HCL2 (#9359)

This commit is contained in:
Adrien Delorme 2020-06-05 17:23:54 +02:00 committed by GitHub
parent 26e099023b
commit bac9c74447
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 807 additions and 153 deletions

View File

@ -58,14 +58,13 @@ func (c *BuildCommand) ParseArgs(args []string) (*BuildArgs, int) {
return &cfg, 0 return &cfg, 0
} }
func (m *Meta) GetConfigFromHCL(cla *MetaArgs) (packer.BuildGetter, int) { func (m *Meta) GetConfigFromHCL(cla *MetaArgs) (*hcl2template.PackerConfig, int) {
parser := &hcl2template.Parser{ parser := &hcl2template.Parser{
Parser: hclparse.NewParser(), Parser: hclparse.NewParser(),
BuilderSchemas: m.CoreConfig.Components.BuilderStore, BuilderSchemas: m.CoreConfig.Components.BuilderStore,
ProvisionersSchemas: m.CoreConfig.Components.ProvisionerStore, ProvisionersSchemas: m.CoreConfig.Components.ProvisionerStore,
PostProcessorsSchemas: m.CoreConfig.Components.PostProcessorStore, PostProcessorsSchemas: m.CoreConfig.Components.PostProcessorStore,
} }
cfg, diags := parser.Parse(cla.Path, cla.VarFiles, cla.Vars) cfg, diags := parser.Parse(cla.Path, cla.VarFiles, cla.Vars)
return cfg, writeDiags(m.Ui, parser.Files(), diags) return cfg, writeDiags(m.Ui, parser.Files(), diags)
} }
@ -88,15 +87,15 @@ func writeDiags(ui packer.Ui, files map[string]*hcl.File, diags hcl.Diagnostics)
return 0 return 0
} }
func (m *Meta) GetConfig(cla *MetaArgs) (packer.BuildGetter, int) { func (m *Meta) GetConfig(cla *MetaArgs) (packer.Handler, int) {
cfgType, err := ConfigType(cla.Path) cfgType, err := cla.GetConfigType()
if err != nil { if err != nil {
m.Ui.Error(fmt.Sprintf("could not tell config type: %s", err)) m.Ui.Error(fmt.Sprintf("%q: %s", cla.Path, err))
return nil, 1 return nil, 1
} }
switch cfgType { switch cfgType {
case "hcl": case ConfigTypeHCL2:
// TODO(azr): allow to pass a slice of files here. // TODO(azr): allow to pass a slice of files here.
return m.GetConfigFromHCL(cla) return m.GetConfigFromHCL(cla)
default: default:
@ -110,9 +109,18 @@ func (m *Meta) GetConfig(cla *MetaArgs) (packer.BuildGetter, int) {
} }
} }
func (m *Meta) GetConfigFromJSON(cla *MetaArgs) (packer.BuildGetter, int) { func (m *Meta) GetConfigFromJSON(cla *MetaArgs) (*packer.Core, int) {
// Parse the template // Parse the template
tpl, err := template.ParseFile(cla.Path) var tpl *template.Template
var err error
if cla.Path == "" {
// here cla validation passed so this means we want a default builder
// and we probably are in the console command
tpl, err = template.Parse(TiniestBuilder)
} else {
tpl, err = template.ParseFile(cla.Path)
}
if err != nil { if err != nil {
m.Ui.Error(fmt.Sprintf("Failed to parse template: %s", err)) m.Ui.Error(fmt.Sprintf("Failed to parse template: %s", err))
return nil, 1 return nil, 1

View File

@ -2,7 +2,6 @@ package command
import ( import (
"flag" "flag"
"fmt"
"strings" "strings"
"github.com/hashicorp/packer/helper/enumflag" "github.com/hashicorp/packer/helper/enumflag"
@ -10,32 +9,44 @@ import (
sliceflag "github.com/hashicorp/packer/helper/flag-slice" sliceflag "github.com/hashicorp/packer/helper/flag-slice"
) )
//go:generate enumer -type configType -trimprefix ConfigType -transform snake
type configType int
const (
ConfigTypeJSON configType = iota // default config type
ConfigTypeHCL2
)
func (c *configType) Set(value string) error {
v, err := configTypeString(value)
if err == nil {
*c = v
}
return err
}
// ConfigType tells what type of config we should use, it can return values // ConfigType tells what type of config we should use, it can return values
// like "hcl" or "json". // like "hcl" or "json".
// Make sure Args was correctly set before. // Make sure Args was correctly set before.
func ConfigType(args ...string) (string, error) { func (ma *MetaArgs) GetConfigType() (configType, error) {
switch len(args) { if ma.Path == "" {
// TODO(azr): in the future, I want to allow passing multiple arguments to return ma.ConfigType, nil
// merge HCL confs together; but this will probably need an RFC first.
case 1:
name := args[0]
if name == "-" {
// TODO(azr): To allow piping HCL2 confs (when args is "-"), we probably
// will need to add a setting that says "this is an HCL config".
return "json", nil
}
if strings.HasSuffix(name, ".pkr.hcl") ||
strings.HasSuffix(name, ".pkr.json") {
return "hcl", nil
}
isDir, err := isDir(name)
if isDir {
return "hcl", err
}
return "json", err
default:
return "", fmt.Errorf("packer only takes one argument: %q", args)
} }
name := ma.Path
if name == "-" {
// TODO(azr): To allow piping HCL2 confs (when args is "-"), we probably
// will need to add a setting that says "this is an HCL config".
return ma.ConfigType, nil
}
if strings.HasSuffix(name, ".pkr.hcl") ||
strings.HasSuffix(name, ".pkr.json") {
return ConfigTypeHCL2, nil
}
isDir, err := isDir(name)
if isDir {
return ConfigTypeHCL2, err
}
return ma.ConfigType, err
} }
// NewMetaArgs parses cli args and put possible values // NewMetaArgs parses cli args and put possible values
@ -44,14 +55,19 @@ func (ma *MetaArgs) AddFlagSets(fs *flag.FlagSet) {
fs.Var((*sliceflag.StringFlag)(&ma.Except), "except", "") fs.Var((*sliceflag.StringFlag)(&ma.Except), "except", "")
fs.Var((*kvflag.Flag)(&ma.Vars), "var", "") fs.Var((*kvflag.Flag)(&ma.Vars), "var", "")
fs.Var((*kvflag.StringSlice)(&ma.VarFiles), "var-file", "") fs.Var((*kvflag.StringSlice)(&ma.VarFiles), "var-file", "")
fs.Var(&ma.ConfigType, "config-type", "set to 'hcl2' to run in hcl2 mode when no file is passed.")
} }
// MetaArgs defines commonalities between all comands // MetaArgs defines commonalities between all comands
type MetaArgs struct { type MetaArgs struct {
// TODO(azr): in the future, I want to allow passing multiple path to
// merge HCL confs together; but this will probably need an RFC first.
Path string Path string
Only, Except []string Only, Except []string
Vars map[string]string Vars map[string]string
VarFiles []string VarFiles []string
// set to "hcl2" to force hcl2 mode
ConfigType configType
} }
func (ba *BuildArgs) AddFlagSets(flags *flag.FlagSet) { func (ba *BuildArgs) AddFlagSets(flags *flag.FlagSet) {
@ -78,7 +94,9 @@ type BuildArgs struct {
} }
// ConsoleArgs represents a parsed cli line for a `packer console` // ConsoleArgs represents a parsed cli line for a `packer console`
type ConsoleArgs struct{ MetaArgs } type ConsoleArgs struct {
MetaArgs
}
func (fa *FixArgs) AddFlagSets(flags *flag.FlagSet) { func (fa *FixArgs) AddFlagSets(flags *flag.FlagSet) {
flags.BoolVar(&fa.Validate, "validate", true, "") flags.BoolVar(&fa.Validate, "validate", true, "")

View File

@ -0,0 +1,50 @@
// Code generated by "enumer -type configType -trimprefix ConfigType -transform snake"; DO NOT EDIT.
//
package command
import (
"fmt"
)
const _configTypeName = "jsonhcl2"
var _configTypeIndex = [...]uint8{0, 4, 8}
func (i configType) String() string {
if i < 0 || i >= configType(len(_configTypeIndex)-1) {
return fmt.Sprintf("configType(%d)", i)
}
return _configTypeName[_configTypeIndex[i]:_configTypeIndex[i+1]]
}
var _configTypeValues = []configType{0, 1}
var _configTypeNameToValueMap = map[string]configType{
_configTypeName[0:4]: 0,
_configTypeName[4:8]: 1,
}
// configTypeString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func configTypeString(s string) (configType, error) {
if val, ok := _configTypeNameToValueMap[s]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to configType values", s)
}
// configTypeValues returns all values of the enum
func configTypeValues() []configType {
return _configTypeValues
}
// IsAconfigType returns "true" if the value is listed in the enum definition. "false" otherwise
func (i configType) IsAconfigType() bool {
for _, v := range _configTypeValues {
if i == v {
return true
}
}
return false
}

View File

@ -3,7 +3,6 @@ package command
import ( import (
"bufio" "bufio"
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"strings" "strings"
@ -12,19 +11,17 @@ import (
"github.com/hashicorp/packer/helper/wrappedreadline" "github.com/hashicorp/packer/helper/wrappedreadline"
"github.com/hashicorp/packer/helper/wrappedstreams" "github.com/hashicorp/packer/helper/wrappedstreams"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template"
"github.com/hashicorp/packer/template/interpolate"
"github.com/posener/complete" "github.com/posener/complete"
) )
const TiniestBuilder = `{ var TiniestBuilder = strings.NewReader(`{
"builders": [ "builders": [
{ {
"type":"null", "type":"null",
"communicator": "none" "communicator": "none"
} }
] ]
}` }`)
type ConsoleCommand struct { type ConsoleCommand struct {
Meta Meta
@ -51,49 +48,24 @@ func (c *ConsoleCommand) ParseArgs(args []string) (*ConsoleArgs, int) {
} }
args = flags.Args() args = flags.Args()
if len(args) == 1 {
cfg.Path = args[0]
}
return &cfg, 0 return &cfg, 0
} }
func (c *ConsoleCommand) RunContext(ctx context.Context, cla *ConsoleArgs) int { func (c *ConsoleCommand) RunContext(ctx context.Context, cla *ConsoleArgs) int {
packerStarter, ret := c.GetConfig(&cla.MetaArgs)
var templ *template.Template if ret != 0 {
if cla.Path == "" { return ret
// If user has not defined a builder, create a tiny null placeholder
// builder so that we can properly initialize the core
tpl, err := template.Parse(strings.NewReader(TiniestBuilder))
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to generate placeholder template: %s", err))
return 1
}
templ = tpl
} else {
// Parse the provided template
tpl, err := template.ParseFile(cla.Path)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to parse template: %s", err))
return 1
}
templ = tpl
}
// Get the core
core, err := c.Meta.Core(templ, &cla.MetaArgs)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
// IO Loop
session := &REPLSession{
Core: core,
} }
// Determine if stdin is a pipe. If so, we evaluate directly. // Determine if stdin is a pipe. If so, we evaluate directly.
if c.StdinPiped() { if c.StdinPiped() {
return c.modePiped(session) return c.modePiped(packerStarter)
} }
return c.modeInteractive(session) return c.modeInteractive(packerStarter)
} }
func (*ConsoleCommand) Help() string { func (*ConsoleCommand) Help() string {
@ -128,13 +100,14 @@ func (*ConsoleCommand) AutocompleteFlags() complete.Flags {
} }
} }
func (c *ConsoleCommand) modePiped(session *REPLSession) int { func (c *ConsoleCommand) modePiped(cfg packer.Evaluator) int {
var lastResult string var lastResult string
scanner := bufio.NewScanner(wrappedstreams.Stdin()) scanner := bufio.NewScanner(wrappedstreams.Stdin())
ret := 0
for scanner.Scan() { for scanner.Scan() {
result, err := session.Handle(strings.TrimSpace(scanner.Text())) result, _, diags := cfg.EvaluateExpression(strings.TrimSpace(scanner.Text()))
if err != nil { if len(diags) > 0 {
return 0 ret = writeDiags(c.Ui, nil, diags)
} }
// Store the last result // Store the last result
lastResult = result lastResult = result
@ -142,10 +115,11 @@ func (c *ConsoleCommand) modePiped(session *REPLSession) int {
// Output the final result // Output the final result
c.Ui.Message(lastResult) c.Ui.Message(lastResult)
return 0 return ret
} }
func (c *ConsoleCommand) modeInteractive(session *REPLSession) int { // Setup the UI so we can output directly to stdout func (c *ConsoleCommand) modeInteractive(cfg packer.Evaluator) int {
// Setup the UI so we can output directly to stdout
l, err := readline.NewEx(wrappedreadline.Override(&readline.Config{ l, err := readline.NewEx(wrappedreadline.Override(&readline.Config{
Prompt: "> ", Prompt: "> ",
InterruptPrompt: "^C", InterruptPrompt: "^C",
@ -170,76 +144,16 @@ func (c *ConsoleCommand) modeInteractive(session *REPLSession) int { // Setup th
} else if err == io.EOF { } else if err == io.EOF {
break break
} }
out, err := session.Handle(line) out, exit, diags := cfg.EvaluateExpression(line)
if err == ErrSessionExit { ret := writeDiags(c.Ui, nil, diags)
break if exit {
return ret
} }
if err != nil {
c.Ui.Error(err.Error())
continue
}
c.Ui.Say(out) c.Ui.Say(out)
if exit {
return ret
}
} }
return 0 return 0
} }
// ErrSessionExit is a special error result that should be checked for
// from Handle to signal a graceful exit.
var ErrSessionExit = errors.New("Session exit")
// Session represents the state for a single Read-Evaluate-Print-Loop (REPL) session.
type REPLSession struct {
// Core is used for constructing interpolations based off packer templates
Core *packer.Core
}
// Handle a single line of input from the REPL.
//
// The return value is the output and the error to show.
func (s *REPLSession) Handle(line string) (string, error) {
switch {
case strings.TrimSpace(line) == "":
return "", nil
case strings.TrimSpace(line) == "exit":
return "", ErrSessionExit
case strings.TrimSpace(line) == "help":
return s.handleHelp()
case strings.TrimSpace(line) == "variables":
return s.handleVariables()
default:
return s.handleEval(line)
}
}
func (s *REPLSession) handleEval(line string) (string, error) {
ctx := s.Core.Context()
rendered, err := interpolate.Render(line, ctx)
if err != nil {
return "", fmt.Errorf("Error interpolating: %s", err)
}
return rendered, nil
}
func (s *REPLSession) handleVariables() (string, error) {
varsstring := "\n"
for k, v := range s.Core.Context().UserVariables {
varsstring += fmt.Sprintf("%s: %+v,\n", k, v)
}
return varsstring, nil
}
func (s *REPLSession) handleHelp() (string, error) {
text := `
The Packer console allows you to experiment with Packer interpolations.
You may access variables in the Packer config you called the console with.
Type in the interpolation to test and hit <enter> to see the result.
To exit the console, type "exit" and hit <enter>, or use Control-C.
`
return strings.TrimSpace(text), nil
}

42
command/console_test.go Normal file
View File

@ -0,0 +1,42 @@
package command
import (
"fmt"
"path/filepath"
"strings"
"testing"
"github.com/hashicorp/packer/hcl2template"
"github.com/hashicorp/packer/packer"
"github.com/stretchr/testify/assert"
)
func Test_piping(t *testing.T) {
tc := []struct {
piped string
command []string
env []string
expected string
}{
{"help", []string{"console"}, nil, packer.ConsoleHelp + "\n"},
{"help", []string{"console", "--config-type=hcl2"}, nil, hcl2template.PackerConsoleHelp + "\n"},
{"var.fruit", []string{"console", filepath.Join(testFixture("var-arg"), "fruit_builder.pkr.hcl")}, []string{"PKR_VAR_fruit=potato"}, "potato\n"},
{"upper(var.fruit)", []string{"console", filepath.Join(testFixture("var-arg"), "fruit_builder.pkr.hcl")}, []string{"PKR_VAR_fruit=potato"}, "POTATO\n"},
{"1 + 5", []string{"console", "--config-type=hcl2"}, nil, "6\n"},
{"var.images", []string{"console", filepath.Join(testFixture("var-arg"), "map.pkr.hcl")}, nil, "{\n" + ` "key" = "value"` + "\n}\n"},
}
for _, tc := range tc {
t.Run(fmt.Sprintf("echo %q | packer %s", tc.piped, tc.command), func(t *testing.T) {
p := helperCommand(t, tc.command...)
p.Stdin = strings.NewReader(tc.piped)
p.Env = append(p.Env, tc.env...)
bs, err := p.Output()
if err != nil {
t.Fatalf("%v: %s", err, bs)
}
assert.Equal(t, tc.expected, string(bs))
})
}
}

127
command/exec_test.go Normal file
View File

@ -0,0 +1,127 @@
package command
import (
"context"
"fmt"
"os"
"os/exec"
"runtime"
"testing"
"github.com/hashicorp/packer/builder/file"
"github.com/hashicorp/packer/builder/null"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/post-processor/manifest"
shell_local_pp "github.com/hashicorp/packer/post-processor/shell-local"
filep "github.com/hashicorp/packer/provisioner/file"
"github.com/hashicorp/packer/provisioner/shell"
shell_local "github.com/hashicorp/packer/provisioner/shell-local"
"github.com/hashicorp/packer/version"
)
// HasExec reports whether the current system can start new processes
// using os.StartProcess or (more commonly) exec.Command.
func HasExec() bool {
switch runtime.GOOS {
case "js":
return false
case "darwin":
if runtime.GOARCH == "arm64" {
return false
}
}
return true
}
// MustHaveExec checks that the current system can start new processes
// using os.StartProcess or (more commonly) exec.Command.
// If not, MustHaveExec calls t.Skip with an explanation.
func MustHaveExec(t testing.TB) {
if !HasExec() {
t.Skipf("skipping test: cannot exec subprocess on %s/%s", runtime.GOOS, runtime.GOARCH)
}
}
func helperCommandContext(t *testing.T, ctx context.Context, s ...string) (cmd *exec.Cmd) {
MustHaveExec(t)
cs := []string{"-test.run=TestHelperProcess", "--"}
cs = append(cs, s...)
if ctx != nil {
cmd = exec.CommandContext(ctx, os.Args[0], cs...)
} else {
cmd = exec.Command(os.Args[0], cs...)
}
cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1")
return cmd
}
func helperCommand(t *testing.T, s ...string) *exec.Cmd {
return helperCommandContext(t, nil, s...)
}
// TestHelperProcess isn't a real test. It's used as a helper process
// for TestParameterRun.
func TestHelperProcess(*testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
defer os.Exit(0)
args := os.Args
for len(args) > 0 {
if args[0] == "--" {
args = args[1:]
break
}
args = args[1:]
}
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "No command\n")
os.Exit(2)
}
cmd, args := args[0], args[1:]
switch cmd {
case "console":
os.Exit((&ConsoleCommand{Meta: commandMeta()}).Run(args))
default:
fmt.Fprintf(os.Stderr, "Unknown command %q\n", cmd)
os.Exit(2)
}
}
func commandMeta() Meta {
basicUi := &packer.BasicUi{
Reader: os.Stdin,
Writer: os.Stdout,
ErrorWriter: os.Stdout,
}
CommandMeta := Meta{
CoreConfig: &packer.CoreConfig{
Components: getBareComponentFinder(),
Version: version.Version,
},
Ui: basicUi,
}
return CommandMeta
}
func getBareComponentFinder() packer.ComponentFinder {
return packer.ComponentFinder{
BuilderStore: packer.MapOfBuilder{
"file": func() (packer.Builder, error) { return &file.Builder{}, nil },
"null": func() (packer.Builder, error) { return &null.Builder{}, nil },
},
ProvisionerStore: packer.MapOfProvisioner{
"shell-local": func() (packer.Provisioner, error) { return &shell_local.Provisioner{}, nil },
"shell": func() (packer.Provisioner, error) { return &shell.Provisioner{}, nil },
"file": func() (packer.Provisioner, error) { return &filep.Provisioner{}, nil },
},
PostProcessorStore: packer.MapOfPostProcessor{
"shell-local": func() (packer.PostProcessor, error) { return &shell_local_pp.PostProcessor{}, nil },
"manifest": func() (packer.PostProcessor, error) { return &manifest.PostProcessor{}, nil },
},
}
}

View File

@ -0,0 +1,7 @@
variable "images" {
type = map(string)
default = {
key = "value"
}
}

View File

@ -66,7 +66,7 @@ func (p *Parser) Parse(filename string, varFiles []string, argVars map[string]st
var diags hcl.Diagnostics var diags hcl.Diagnostics
// parse config files // parse config files
{ if filename != "" {
hclFiles, jsonFiles, moreDiags := GetHCL2Files(filename, hcl2FileExt, hcl2JsonFileExt) hclFiles, jsonFiles, moreDiags := GetHCL2Files(filename, hcl2FileExt, hcl2JsonFileExt)
diags = append(diags, moreDiags...) diags = append(diags, moreDiags...)
if len(hclFiles)+len(jsonFiles) == 0 { if len(hclFiles)+len(jsonFiles) == 0 {

106
hcl2template/repl/format.go Normal file
View File

@ -0,0 +1,106 @@
package repl
import (
"bufio"
"bytes"
"fmt"
"sort"
"strconv"
"strings"
)
// FormatResult formats the given result value for human-readable output.
//
// The value must currently be a string, list, map, and any nested values
// with those same types.
func FormatResult(value interface{}) string {
return formatResult(value, false)
}
func formatResult(value interface{}, nested bool) string {
if value == nil {
return "null"
}
switch output := value.(type) {
case string:
if nested {
return fmt.Sprintf("%q", output)
}
return output
case int:
return strconv.Itoa(output)
case float64:
return fmt.Sprintf("%g", output)
case bool:
switch {
case output == true:
return "true"
default:
return "false"
}
case []interface{}:
return formatListResult(output)
case map[string]interface{}:
return formatMapResult(output)
default:
return "<unknown-type>"
}
}
func formatListResult(value []interface{}) string {
var outputBuf bytes.Buffer
outputBuf.WriteString("[")
if len(value) > 0 {
outputBuf.WriteString("\n")
}
for _, v := range value {
raw := formatResult(v, true)
outputBuf.WriteString(indent(raw))
outputBuf.WriteString(",\n")
}
outputBuf.WriteString("]")
return outputBuf.String()
}
func formatMapResult(value map[string]interface{}) string {
ks := make([]string, 0, len(value))
for k := range value {
ks = append(ks, k)
}
sort.Strings(ks)
var outputBuf bytes.Buffer
outputBuf.WriteString("{")
if len(value) > 0 {
outputBuf.WriteString("\n")
}
for _, k := range ks {
v := value[k]
rawK := formatResult(k, true)
rawV := formatResult(v, true)
outputBuf.WriteString(indent(fmt.Sprintf("%s = %s", rawK, rawV)))
outputBuf.WriteString("\n")
}
outputBuf.WriteString("}")
return outputBuf.String()
}
func indent(value string) string {
var outputBuf bytes.Buffer
s := bufio.NewScanner(strings.NewReader(value))
newline := false
for s.Scan() {
if newline {
outputBuf.WriteByte('\n')
}
outputBuf.WriteString(" " + s.Text())
newline = true
}
return outputBuf.String()
}

View File

@ -0,0 +1,4 @@
// Package repl provides the structs and functions necessary to run REPL for
// HCL2. The REPL allows experimentation of HCL2 interpolations without having
// to run a HCL2 configuration.
package repl

View File

@ -0,0 +1,87 @@
package hcl2shim
import (
"fmt"
"math/big"
"github.com/zclconf/go-cty/cty"
)
// UnknownVariableValue is a sentinel value that can be used
// to denote that the value of a variable is unknown at this time.
// RawConfig uses this information to build up data about
// unknown keys.
const UnknownVariableValue = "74D93920-ED26-11E3-AC10-0800200C9A66"
// ConfigValueFromHCL2 converts a value from HCL2 (really, from the cty dynamic
// types library that HCL2 uses) to a value type that matches what would've
// been produced from the HCL-based interpolator for an equivalent structure.
//
// This function will transform a cty null value into a Go nil value, which
// isn't a possible outcome of the HCL/HIL-based decoder and so callers may
// need to detect and reject any null values.
func ConfigValueFromHCL2(v cty.Value) interface{} {
if !v.IsKnown() {
return UnknownVariableValue
}
if v.IsNull() {
return nil
}
switch v.Type() {
case cty.Bool:
return v.True() // like HCL.BOOL
case cty.String:
return v.AsString() // like HCL token.STRING or token.HEREDOC
case cty.Number:
// We can't match HCL _exactly_ here because it distinguishes between
// int and float values, but we'll get as close as we can by using
// an int if the number is exactly representable, and a float if not.
// The conversion to float will force precision to that of a float64,
// which is potentially losing information from the specific number
// given, but no worse than what HCL would've done in its own conversion
// to float.
f := v.AsBigFloat()
if i, acc := f.Int64(); acc == big.Exact {
// if we're on a 32-bit system and the number is too big for 32-bit
// int then we'll fall through here and use a float64.
const MaxInt = int(^uint(0) >> 1)
const MinInt = -MaxInt - 1
if i <= int64(MaxInt) && i >= int64(MinInt) {
return int(i) // Like HCL token.NUMBER
}
}
f64, _ := f.Float64()
return f64 // like HCL token.FLOAT
}
if v.Type().IsListType() || v.Type().IsSetType() || v.Type().IsTupleType() {
l := make([]interface{}, 0, v.LengthInt())
it := v.ElementIterator()
for it.Next() {
_, ev := it.Element()
l = append(l, ConfigValueFromHCL2(ev))
}
return l
}
if v.Type().IsMapType() || v.Type().IsObjectType() {
l := make(map[string]interface{})
it := v.ElementIterator()
for it.Next() {
ek, ev := it.Element()
cv := ConfigValueFromHCL2(ev)
if cv != nil {
l[ek.AsString()] = cv
}
}
return l
}
// If we fall out here then we have some weird type that we haven't
// accounted for. This should never happen unless the caller is using
// capsule types, and we don't currently have any such types defined.
panic(fmt.Errorf("can't convert %#v to config value", v))
}

View File

@ -0,0 +1,96 @@
package hcl2shim
import (
"fmt"
"reflect"
"testing"
"github.com/zclconf/go-cty/cty"
)
func TestConfigValueFromHCL2(t *testing.T) {
tests := []struct {
Input cty.Value
Want interface{}
}{
{
cty.True,
true,
},
{
cty.False,
false,
},
{
cty.NumberIntVal(12),
int(12),
},
{
cty.NumberFloatVal(12.5),
float64(12.5),
},
{
cty.StringVal("hello world"),
"hello world",
},
{
cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("Ermintrude"),
"age": cty.NumberIntVal(19),
"address": cty.ObjectVal(map[string]cty.Value{
"street": cty.ListVal([]cty.Value{cty.StringVal("421 Shoreham Loop")}),
"city": cty.StringVal("Fridgewater"),
"state": cty.StringVal("MA"),
"zip": cty.StringVal("91037"),
}),
}),
map[string]interface{}{
"name": "Ermintrude",
"age": int(19),
"address": map[string]interface{}{
"street": []interface{}{"421 Shoreham Loop"},
"city": "Fridgewater",
"state": "MA",
"zip": "91037",
},
},
},
{
cty.MapVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
"bar": cty.StringVal("baz"),
}),
map[string]interface{}{
"foo": "bar",
"bar": "baz",
},
},
{
cty.TupleVal([]cty.Value{
cty.StringVal("foo"),
cty.True,
}),
[]interface{}{
"foo",
true,
},
},
{
cty.NullVal(cty.String),
nil,
},
{
cty.UnknownVal(cty.String),
UnknownVariableValue,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) {
got := ConfigValueFromHCL2(test.Input)
if !reflect.DeepEqual(got, test.Want) {
t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, got, test.Want)
}
})
}
}

View File

@ -2,8 +2,10 @@ package hcl2template
import ( import (
"fmt" "fmt"
"strings"
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/packer/helper/common" "github.com/hashicorp/packer/helper/common"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
@ -12,8 +14,6 @@ import (
// PackerConfig represents a loaded Packer HCL config. It will contain // PackerConfig represents a loaded Packer HCL config. It will contain
// references to all possible blocks of the allowed configuration. // references to all possible blocks of the allowed configuration.
type PackerConfig struct { type PackerConfig struct {
// parser *Parser
// Directory where the config files are defined // Directory where the config files are defined
Basedir string Basedir string
@ -386,3 +386,67 @@ func (cfg *PackerConfig) GetBuilds(opts packer.GetBuildsOptions) ([]packer.Build
} }
return res, diags return res, diags
} }
var PackerConsoleHelp = strings.TrimSpace(`
Packer console HCL2 Mode.
The Packer console allows you to experiment with Packer interpolations.
You may access variables and functions in the Packer config you called the
console with.
Type in the interpolation to test and hit <enter> to see the result.
"upper(var.foo.id)" would evaluate to the ID of "foo" and uppercase is, if it
exists in your config file.
"variables" will dump all available variables and their values.
To exit the console, type "exit" and hit <enter>, or use Control-C.
/!\ It is not possible to use go templating interpolation like "{{timestamp}}"
with in HCL2 mode.
`)
func (p *PackerConfig) EvaluateExpression(line string) (out string, exit bool, diags hcl.Diagnostics) {
switch {
case line == "":
return "", false, nil
case line == "exit":
return "", true, nil
case line == "help":
return PackerConsoleHelp, false, nil
case line == "variables":
out := &strings.Builder{}
out.WriteString("> input-variables:\n\n")
for _, v := range p.InputVariables {
val, _ := v.Value()
fmt.Fprintf(out, "var.%s: %q [debug: %#v]\n", v.Name, PrintableCtyValue(val), v)
}
out.WriteString("\n> local-variables:\n\n")
for _, v := range p.LocalVariables {
val, _ := v.Value()
fmt.Fprintf(out, "local.%s: %q\n", v.Name, PrintableCtyValue(val))
}
return out.String(), false, nil
default:
return p.handleEval(line)
}
}
func (p *PackerConfig) handleEval(line string) (out string, exit bool, diags hcl.Diagnostics) {
// Parse the given line as an expression
expr, parseDiags := hclsyntax.ParseExpression([]byte(line), "<console-input>", hcl.Pos{Line: 1, Column: 1})
diags = append(diags, parseDiags...)
if parseDiags.HasErrors() {
return "", false, diags
}
val, valueDiags := expr.Value(p.EvalContext(nil))
diags = append(diags, valueDiags...)
if valueDiags.HasErrors() {
return "", false, diags
}
return PrintableCtyValue(val), false, diags
}

View File

@ -51,8 +51,13 @@ type Variable struct {
} }
func (v *Variable) GoString() string { func (v *Variable) GoString() string {
return fmt.Sprintf("{Type:%q,CmdValue:%q,VarfileValue:%q,EnvValue:%q,DefaultValue:%q}", return fmt.Sprintf("{Type:%s,CmdValue:%s,VarfileValue:%s,EnvValue:%s,DefaultValue:%s}",
v.Type.GoString(), v.CmdValue.GoString(), v.VarfileValue.GoString(), v.EnvValue.GoString(), v.DefaultValue.GoString()) v.Type.GoString(),
PrintableCtyValue(v.CmdValue),
PrintableCtyValue(v.VarfileValue),
PrintableCtyValue(v.EnvValue),
PrintableCtyValue(v.DefaultValue),
)
} }
func (v *Variable) Value() (cty.Value, *hcl.Diagnostic) { func (v *Variable) Value() (cty.Value, *hcl.Diagnostic) {

View File

@ -9,6 +9,9 @@ import (
"github.com/gobwas/glob" "github.com/gobwas/glob"
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/packer/hcl2template/repl"
hcl2shim "github.com/hashicorp/packer/hcl2template/shim"
"github.com/zclconf/go-cty/cty"
) )
func warningErrorsToDiags(block *hcl.Block, warnings []string, err error) hcl.Diagnostics { func warningErrorsToDiags(block *hcl.Block, warnings []string, err error) hcl.Diagnostics {
@ -47,6 +50,9 @@ func isDir(name string) (bool, error) {
// returned. Otherwise if filename references a file and filename matches one // returned. Otherwise if filename references a file and filename matches one
// of the suffixes it is returned in the according slice. // of the suffixes it is returned in the according slice.
func GetHCL2Files(filename, hclSuffix, jsonSuffix string) (hclFiles, jsonFiles []string, diags hcl.Diagnostics) { func GetHCL2Files(filename, hclSuffix, jsonSuffix string) (hclFiles, jsonFiles []string, diags hcl.Diagnostics) {
if filename == "" {
return
}
isDir, err := isDir(filename) isDir, err := isDir(filename)
if err != nil { if err != nil {
diags = append(diags, &hcl.Diagnostic{ diags = append(diags, &hcl.Diagnostic{
@ -109,3 +115,12 @@ func convertFilterOption(patterns []string, optionName string) ([]glob.Glob, hcl
return globs, diags return globs, diags
} }
func PrintableCtyValue(v cty.Value) string {
if !v.IsWhollyKnown() {
return "<unknown>"
}
gval := hcl2shim.ConfigValueFromHCL2(v)
str := repl.FormatResult(gval)
return str
}

View File

@ -374,6 +374,52 @@ func (c *Core) Context() *interpolate.Context {
} }
} }
var ConsoleHelp = strings.TrimSpace(`
Packer console JSON Mode.
The Packer console allows you to experiment with Packer interpolations.
You may access variables in the Packer config you called the console with.
Type in the interpolation to test and hit <enter> to see the result.
"variables" will dump all available variables and their values.
"{{timestamp}}" will output the timestamp, for example "1559855090".
To exit the console, type "exit" and hit <enter>, or use Control-C.
/!\ If you would like to start console in hcl2 mode without a config you can
use the --config-type=hcl2 option.
`)
func (c *Core) EvaluateExpression(line string) (string, bool, hcl.Diagnostics) {
switch {
case line == "":
return "", false, nil
case line == "exit":
return "", true, nil
case line == "help":
return ConsoleHelp, false, nil
case line == "variables":
varsstring := "\n"
for k, v := range c.Context().UserVariables {
varsstring += fmt.Sprintf("%s: %+v,\n", k, v)
}
return varsstring, false, nil
default:
ctx := c.Context()
rendered, err := interpolate.Render(line, ctx)
var diags hcl.Diagnostics
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Summary: "Interpolation error",
Detail: err.Error(),
})
}
return rendered, false, diags
}
}
// validate does a full validation of the template. // validate does a full validation of the template.
// //
// This will automatically call template.validate() in addition to doing // This will automatically call template.validate() in addition to doing

View File

@ -17,6 +17,19 @@ type BuildGetter interface {
GetBuilds(GetBuildsOptions) ([]Build, hcl.Diagnostics) GetBuilds(GetBuildsOptions) ([]Build, hcl.Diagnostics)
} }
type Evaluator interface {
// EvaluateExpression is meant to be used in the `packer console` command.
// It parses the input string and returns what needs to be displayed. In
// case of an error the error should be displayed.
EvaluateExpression(expr string) (output string, exit bool, diags hcl.Diagnostics)
}
// The packer.Handler handles all Packer things.
type Handler interface {
Evaluator
BuildGetter
}
//go:generate enumer -type FixConfigMode //go:generate enumer -type FixConfigMode
type FixConfigMode int type FixConfigMode int

View File

@ -16,9 +16,13 @@ console with, or provide variables when you call console using the -var or
~> **Note:** `console` is available from version 1.4.2 and above. ~> **Note:** `console` is available from version 1.4.2 and above.
Type in the interpolation to test and hit \<enter\> to see the result. ~> **Note:** For HCL2 `console` is available from version 1.6.0 and above, use
`packer console --config-type=hcl2` to try it without a config file. Go
templating ( or `{{..}}` calls ) will not work in HCL2 mode.
To exit the console, type "exit" and hit \<enter\>, or use Control-C. Type in the interpolation to test and hit `<enter>` to see the result.
To exit the console, type "exit" and hit `<enter>`, or use Control-C.
```shell-session ```shell-session
$ packer console my_template.json $ packer console my_template.json
@ -45,7 +49,7 @@ help output, which can be seen via `packer console -h`.
- `variables` - prints a list of all variables read into the console from the - `variables` - prints a list of all variables read into the console from the
`-var` option, `-var-files` option, and template. `-var` option, `-var-files` option, and template.
## Usage Examples ## Usage Examples - repl session ( JSON )
Let's say you launch a console using a Packer template `example_template.json`: Let's say you launch a console using a Packer template `example_template.json`:
@ -69,14 +73,14 @@ myvar:
```shell-session ```shell-session
> {{user `myvar`}} > {{user `myvar`}}
> asdfasdf asdfasdf
``` ```
From there you can test more complicated interpolations: From there you can test more complicated interpolations:
```shell-session ```shell-session
> {{user `myvar`}}-{{timestamp}} > {{user `myvar`}}-{{timestamp}}
> asdfasdf-1559854396 asdfasdf-1559854396
``` ```
And when you're done using the console, just type "exit" or CTRL-C And when you're done using the console, just type "exit" or CTRL-C
@ -96,6 +100,8 @@ If you don't have specific variables or var files you want to test, and just
want to experiment with a particular template engine, you can do so by simply want to experiment with a particular template engine, you can do so by simply
calling `packer console` without a template file. calling `packer console` without a template file.
## Usage Examples - piped commands ( JSON )
If you'd like to just see a specific single interpolation without launching If you'd like to just see a specific single interpolation without launching
the REPL, you can do so by echoing and piping the string into the console the REPL, you can do so by echoing and piping the string into the console
command: command:
@ -104,3 +110,49 @@ command:
$ echo {{timestamp}} | packer console $ echo {{timestamp}} | packer console
1559855090 1559855090
``` ```
## Usage Examples - repl session ( HCL2 )
~> **Note:** For HCL2 `console` is available from version 1.6.0 and above, use
`packer console --config-type=hcl2` to try it without a config file. Go
templating ( or `{{..}}` calls ) will not work in HCL2 mode.
Without a config file, `packer console` can be used to experiment with the
expression syntax and [built-in functions](/docs/from-1.5/functions).
### Starting
To start a session on a folder containing HCL2 config files, run:
```shell-session
packer console folder/
```
Because `folder/` is a folder Packer will start in HCL2 mode, you can also
directly pass an HCL2 formatted config file:
```shell-session
packer console file.pkr.hcl
```
Because the file is suffixed with `.pkr.hcl` Packer will start in HCL2 mode.
When you just want to play arround without a config file you can set the
`--config-type=hcl2` option and Packer will start in HCL2 mode:
```shell-session
packer console --config-type=hcl2
```
### Scripting
The `packer console` command can be used in non-interactive scripts by piping
newline-separated commands to it. Only the output from the final command is
printed unless an error occurs earlier.
For example:
```shell-session
$ echo "1 + 5" | terraform console
6
```