packer console for HCL2 (#9359)
This commit is contained in:
parent
26e099023b
commit
bac9c74447
|
@ -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
|
||||||
|
|
|
@ -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 := ma.Path
|
||||||
name := args[0]
|
|
||||||
if name == "-" {
|
if name == "-" {
|
||||||
// TODO(azr): To allow piping HCL2 confs (when args is "-"), we probably
|
// 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".
|
// will need to add a setting that says "this is an HCL config".
|
||||||
return "json", nil
|
return ma.ConfigType, nil
|
||||||
}
|
}
|
||||||
if strings.HasSuffix(name, ".pkr.hcl") ||
|
if strings.HasSuffix(name, ".pkr.hcl") ||
|
||||||
strings.HasSuffix(name, ".pkr.json") {
|
strings.HasSuffix(name, ".pkr.json") {
|
||||||
return "hcl", nil
|
return ConfigTypeHCL2, nil
|
||||||
}
|
}
|
||||||
isDir, err := isDir(name)
|
isDir, err := isDir(name)
|
||||||
if isDir {
|
if isDir {
|
||||||
return "hcl", err
|
return ConfigTypeHCL2, err
|
||||||
}
|
|
||||||
return "json", err
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("packer only takes one argument: %q", args)
|
|
||||||
}
|
}
|
||||||
|
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, "")
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
variable "images" {
|
||||||
|
type = map(string)
|
||||||
|
default = {
|
||||||
|
key = "value"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -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
|
|
@ -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))
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
```
|
Loading…
Reference in New Issue