Merge pull request #9468 from hashicorp/azr-ft-hcl2-inspect

[hcl2] inspect command
This commit is contained in:
Megan Marsh 2020-06-23 13:48:54 -07:00 committed by GitHub
commit 7eda9eacc6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 389 additions and 146 deletions

View File

@ -121,3 +121,12 @@ type ValidateArgs struct {
MetaArgs MetaArgs
SyntaxOnly bool SyntaxOnly bool
} }
func (va *InspectArgs) AddFlagSets(flags *flag.FlagSet) {
va.MetaArgs.AddFlagSets(flags)
}
// InspectArgs represents a parsed cli line for a `packer inspect`
type InspectArgs struct {
MetaArgs
}

View File

@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func Test_piping(t *testing.T) { func Test_console(t *testing.T) {
tc := []struct { tc := []struct {
piped string piped string

View File

@ -85,6 +85,8 @@ func TestHelperProcess(*testing.T) {
switch cmd { switch cmd {
case "console": case "console":
os.Exit((&ConsoleCommand{Meta: commandMeta()}).Run(args)) os.Exit((&ConsoleCommand{Meta: commandMeta()}).Run(args))
case "inspect":
os.Exit((&InspectCommand{Meta: commandMeta()}).Run(args))
default: default:
fmt.Fprintf(os.Stderr, "Unknown command %q\n", cmd) fmt.Fprintf(os.Stderr, "Unknown command %q\n", cmd)
os.Exit(2) os.Exit(2)

View File

@ -1,12 +1,10 @@
package command package command
import ( import (
"fmt" "context"
"sort"
"strings" "strings"
"github.com/hashicorp/packer/template" "github.com/hashicorp/packer/packer"
"github.com/posener/complete" "github.com/posener/complete"
) )
@ -15,142 +13,40 @@ type InspectCommand struct {
} }
func (c *InspectCommand) Run(args []string) int { func (c *InspectCommand) Run(args []string) int {
flags := c.Meta.FlagSet("inspect", FlagSetNone) ctx := context.Background()
cfg, ret := c.ParseArgs(args)
if ret != 0 {
return ret
}
return c.RunContext(ctx, cfg)
}
func (c *InspectCommand) ParseArgs(args []string) (*InspectArgs, int) {
var cfg InspectArgs
flags := c.Meta.FlagSet("inspect", FlagSetVars)
flags.Usage = func() { c.Ui.Say(c.Help()) } flags.Usage = func() { c.Ui.Say(c.Help()) }
cfg.AddFlagSets(flags)
if err := flags.Parse(args); err != nil { if err := flags.Parse(args); err != nil {
return 1 return &cfg, 1
} }
args = flags.Args() args = flags.Args()
if len(args) != 1 { if len(args) == 1 {
flags.Usage() cfg.Path = args[0]
return 1 }
return &cfg, 0
} }
// Parse the template func (c *InspectCommand) RunContext(ctx context.Context, cla *InspectArgs) int {
tpl, err := template.ParseFile(args[0]) packerStarter, ret := c.GetConfig(&cla.MetaArgs)
if err != nil { if ret != 0 {
c.Ui.Error(fmt.Sprintf("Failed to parse template: %s", err)) return ret
return 1
} }
return packerStarter.InspectConfig(packer.InspectConfigOptions{
// Convenience... Ui: c.Ui,
ui := c.Ui })
// Description
if tpl.Description != "" {
ui.Say("Description:\n")
ui.Say(tpl.Description + "\n")
}
// Variables
if len(tpl.Variables) == 0 {
ui.Say("Variables:\n")
ui.Say(" <No variables>")
} else {
requiredHeader := false
for k, v := range tpl.Variables {
for _, sensitive := range tpl.SensitiveVariables {
if ok := strings.Compare(sensitive.Default, v.Default); ok == 0 {
v.Default = "<sensitive>"
}
}
if v.Required {
if !requiredHeader {
requiredHeader = true
ui.Say("Required variables:\n")
}
ui.Machine("template-variable", k, v.Default, "1")
ui.Say(" " + k)
}
}
if requiredHeader {
ui.Say("")
}
ui.Say("Optional variables and their defaults:\n")
keys := make([]string, 0, len(tpl.Variables))
max := 0
for k := range tpl.Variables {
keys = append(keys, k)
if len(k) > max {
max = len(k)
}
}
sort.Strings(keys)
for _, k := range keys {
v := tpl.Variables[k]
if v.Required {
continue
}
for _, sensitive := range tpl.SensitiveVariables {
if ok := strings.Compare(sensitive.Default, v.Default); ok == 0 {
v.Default = "<sensitive>"
}
}
padding := strings.Repeat(" ", max-len(k))
output := fmt.Sprintf(" %s%s = %s", k, padding, v.Default)
ui.Machine("template-variable", k, v.Default, "0")
ui.Say(output)
}
}
ui.Say("")
// Builders
ui.Say("Builders:\n")
if len(tpl.Builders) == 0 {
ui.Say(" <No builders>")
} else {
keys := make([]string, 0, len(tpl.Builders))
max := 0
for k := range tpl.Builders {
keys = append(keys, k)
if len(k) > max {
max = len(k)
}
}
sort.Strings(keys)
for _, k := range keys {
v := tpl.Builders[k]
padding := strings.Repeat(" ", max-len(k))
output := fmt.Sprintf(" %s%s", k, padding)
if v.Name != v.Type {
output = fmt.Sprintf("%s (%s)", output, v.Type)
}
ui.Machine("template-builder", k, v.Type)
ui.Say(output)
}
}
ui.Say("")
// Provisioners
ui.Say("Provisioners:\n")
if len(tpl.Provisioners) == 0 {
ui.Say(" <No provisioners>")
} else {
for _, v := range tpl.Provisioners {
ui.Machine("template-provisioner", v.Type)
ui.Say(fmt.Sprintf(" %s", v.Type))
}
}
ui.Say("\nNote: If your build names contain user variables or template\n" +
"functions such as 'timestamp', these are processed at build time,\n" +
"and therefore only show in their raw form here.")
return 0
} }
func (*InspectCommand) Help() string { func (*InspectCommand) Help() string {

106
command/inspect_test.go Normal file
View File

@ -0,0 +1,106 @@
package command
import (
"fmt"
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
)
func Test_commands(t *testing.T) {
tc := []struct {
command []string
env []string
expected string
}{
{[]string{"inspect", "-var=fruit=banana", filepath.Join(testFixture("var-arg"), "fruit_builder.pkr.hcl")}, nil, `Packer Inspect: HCL2 mode
> input-variables:
var.fruit: "banana" [debug: {Type:cty.String,CmdValue:banana,VarfileValue:null,EnvValue:null,DefaultValue:null}]
> local-variables:
local.fruit: "banana"
> builds:
> <unnamed build 0>:
sources:
null.builder
provisioners:
shell-local
post-processors:
<no post-processor>
`},
{[]string{"inspect", "-var=fruit=peach", filepath.Join(testFixture("hcl"), "inspect", "fruit_string.pkr.hcl")}, nil, `Packer Inspect: HCL2 mode
> input-variables:
var.fruit: "peach" [debug: {Type:cty.String,CmdValue:peach,VarfileValue:null,EnvValue:null,DefaultValue:banana}]
> local-variables:
> builds:
`},
{[]string{"inspect", "-var=fruit=peach", filepath.Join(testFixture("hcl"), "inspect")}, nil, `Packer Inspect: HCL2 mode
> input-variables:
var.fruit: "peach" [debug: {Type:cty.String,CmdValue:peach,VarfileValue:null,EnvValue:null,DefaultValue:banana}]
> local-variables:
> builds:
> aws_example_builder:
> Description: The builder of clouds !!
Use it at will.
sources:
amazon-ebs.example-1
amazon-ebs.example-2
provisioners:
shell
post-processors:
manifest
`},
}
for _, tc := range tc {
t.Run(fmt.Sprintf("packer %s", tc.command), func(t *testing.T) {
p := helperCommand(t, tc.command...)
p.Env = append(p.Env, tc.env...)
bs, err := p.Output()
if err != nil {
t.Fatalf("%v: %s", err, bs)
}
actual := string(bs)
if diff := cmp.Diff(tc.expected, actual); diff != "" {
t.Fatalf("unexpected ouput %s", diff)
}
})
}
}

View File

@ -0,0 +1,29 @@
build {
name = "aws_example_builder"
description = <<EOF
The builder of clouds !!
Use it at will.
EOF
sources = [
"source.amazon-ebs.example-1",
// this one is not defined but we don't want to error there, we just
// would like to show what sources are being referenced.
"source.amazon-ebs.example-2",
]
provisioner "shell" {
files = [
"bins/install-this.sh",
"bins/install-that.sh",
"bins/conf-this.sh",
]
}
post-processor "manifest" {
}
}

View File

@ -0,0 +1,5 @@
variable "fruit" {
type = string
default = "banana"
}

View File

@ -38,6 +38,10 @@ type BuildBlock struct {
// Name is a string representing the named build to show in the logs // Name is a string representing the named build to show in the logs
Name string Name string
// A description of what this build does, it could be used in a inspect
// call for example.
Description string
// Sources is the list of sources that we want to start in this build block. // Sources is the list of sources that we want to start in this build block.
Sources []SourceRef Sources []SourceRef
@ -61,6 +65,7 @@ func (p *Parser) decodeBuildConfig(block *hcl.Block) (*BuildBlock, hcl.Diagnosti
var b struct { var b struct {
Name string `hcl:"name,optional"` Name string `hcl:"name,optional"`
Description string `hcl:"description,optional"`
FromSources []string `hcl:"sources,optional"` FromSources []string `hcl:"sources,optional"`
Config hcl.Body `hcl:",remain"` Config hcl.Body `hcl:",remain"`
} }
@ -70,6 +75,7 @@ func (p *Parser) decodeBuildConfig(block *hcl.Block) (*BuildBlock, hcl.Diagnosti
} }
build.Name = b.Name build.Name = b.Name
build.Description = b.Description
for _, buildFrom := range b.FromSources { for _, buildFrom := range b.FromSources {
ref := sourceRefFromString(buildFrom) ref := sourceRefFromString(buildFrom)

View File

@ -439,6 +439,13 @@ func (p *PackerConfig) EvaluateExpression(line string) (out string, exit bool, d
case line == "help": case line == "help":
return PackerConsoleHelp, false, nil return PackerConsoleHelp, false, nil
case line == "variables": case line == "variables":
return p.printVariables(), false, nil
default:
return p.handleEval(line)
}
}
func (p *PackerConfig) printVariables() string {
out := &strings.Builder{} out := &strings.Builder{}
out.WriteString("> input-variables:\n\n") out.WriteString("> input-variables:\n\n")
for _, v := range p.InputVariables { for _, v := range p.InputVariables {
@ -450,11 +457,52 @@ func (p *PackerConfig) EvaluateExpression(line string) (out string, exit bool, d
val, _ := v.Value() val, _ := v.Value()
fmt.Fprintf(out, "local.%s: %q\n", v.Name, PrintableCtyValue(val)) fmt.Fprintf(out, "local.%s: %q\n", v.Name, PrintableCtyValue(val))
} }
return out.String()
return out.String(), false, nil
default:
return p.handleEval(line)
} }
func (p *PackerConfig) printBuilds() string {
out := &strings.Builder{}
out.WriteString("> builds:\n")
for i, build := range p.Builds {
name := build.Name
if name == "" {
name = fmt.Sprintf("<unnamed build %d>", i)
}
fmt.Fprintf(out, "\n > %s:\n", name)
if build.Description != "" {
fmt.Fprintf(out, "\n > Description: %s\n", build.Description)
}
fmt.Fprintf(out, "\n sources:\n")
if len(build.Sources) == 0 {
fmt.Fprintf(out, "\n <no source>\n")
}
for _, source := range build.Sources {
fmt.Fprintf(out, "\n %s\n", source)
}
fmt.Fprintf(out, "\n provisioners:\n\n")
if len(build.ProvisionerBlocks) == 0 {
fmt.Fprintf(out, " <no provisioner>\n")
}
for _, prov := range build.ProvisionerBlocks {
str := prov.PType
if prov.PName != "" {
str = strings.Join([]string{prov.PType, prov.PName}, ".")
}
fmt.Fprintf(out, " %s\n", str)
}
fmt.Fprintf(out, "\n post-processors:\n\n")
if len(build.PostProcessors) == 0 {
fmt.Fprintf(out, " <no post-processor>\n")
}
for _, pp := range build.PostProcessors {
str := pp.PType
if pp.PName != "" {
str = strings.Join([]string{pp.PType, pp.PName}, ".")
}
fmt.Fprintf(out, " %s\n", str)
}
}
return out.String()
} }
func (p *PackerConfig) handleEval(line string) (out string, exit bool, diags hcl.Diagnostics) { func (p *PackerConfig) handleEval(line string) (out string, exit bool, diags hcl.Diagnostics) {
@ -479,3 +527,12 @@ func (p *PackerConfig) FixConfig(_ packer.FixConfigOptions) (diags hcl.Diagnosti
// No Fixers exist for HCL2 configs so there is nothing to do here for now. // No Fixers exist for HCL2 configs so there is nothing to do here for now.
return return
} }
func (p *PackerConfig) InspectConfig(opts packer.InspectConfigOptions) int {
ui := opts.Ui
ui.Say("Packer Inspect: HCL2 mode\n")
ui.Say(p.printVariables())
ui.Say(p.printBuilds())
return 0
}

View File

@ -424,6 +424,129 @@ func (c *Core) EvaluateExpression(line string) (string, bool, hcl.Diagnostics) {
} }
} }
func (c *Core) InspectConfig(opts InspectConfigOptions) int {
// Convenience...
ui := opts.Ui
tpl := c.Template
ui.Say("Packer Inspect: JSON mode")
// Description
if tpl.Description != "" {
ui.Say("Description:\n")
ui.Say(tpl.Description + "\n")
}
// Variables
if len(tpl.Variables) == 0 {
ui.Say("Variables:\n")
ui.Say(" <No variables>")
} else {
requiredHeader := false
for k, v := range tpl.Variables {
for _, sensitive := range tpl.SensitiveVariables {
if ok := strings.Compare(sensitive.Default, v.Default); ok == 0 {
v.Default = "<sensitive>"
}
}
if v.Required {
if !requiredHeader {
requiredHeader = true
ui.Say("Required variables:\n")
}
ui.Machine("template-variable", k, v.Default, "1")
ui.Say(" " + k)
}
}
if requiredHeader {
ui.Say("")
}
ui.Say("Optional variables and their defaults:\n")
keys := make([]string, 0, len(tpl.Variables))
max := 0
for k := range tpl.Variables {
keys = append(keys, k)
if len(k) > max {
max = len(k)
}
}
sort.Strings(keys)
for _, k := range keys {
v := tpl.Variables[k]
if v.Required {
continue
}
for _, sensitive := range tpl.SensitiveVariables {
if ok := strings.Compare(sensitive.Default, v.Default); ok == 0 {
v.Default = "<sensitive>"
}
}
padding := strings.Repeat(" ", max-len(k))
output := fmt.Sprintf(" %s%s = %s", k, padding, v.Default)
ui.Machine("template-variable", k, v.Default, "0")
ui.Say(output)
}
}
ui.Say("")
// Builders
ui.Say("Builders:\n")
if len(tpl.Builders) == 0 {
ui.Say(" <No builders>")
} else {
keys := make([]string, 0, len(tpl.Builders))
max := 0
for k := range tpl.Builders {
keys = append(keys, k)
if len(k) > max {
max = len(k)
}
}
sort.Strings(keys)
for _, k := range keys {
v := tpl.Builders[k]
padding := strings.Repeat(" ", max-len(k))
output := fmt.Sprintf(" %s%s", k, padding)
if v.Name != v.Type {
output = fmt.Sprintf("%s (%s)", output, v.Type)
}
ui.Machine("template-builder", k, v.Type)
ui.Say(output)
}
}
ui.Say("")
// Provisioners
ui.Say("Provisioners:\n")
if len(tpl.Provisioners) == 0 {
ui.Say(" <No provisioners>")
} else {
for _, v := range tpl.Provisioners {
ui.Machine("template-provisioner", v.Type)
ui.Say(fmt.Sprintf(" %s", v.Type))
}
}
ui.Say("\nNote: If your build names contain user variables or template\n" +
"functions such as 'timestamp', these are processed at build time,\n" +
"and therefore only show in their raw form here.")
return 0
}
func (c *Core) FixConfig(opts FixConfigOptions) hcl.Diagnostics { func (c *Core) FixConfig(opts FixConfigOptions) hcl.Diagnostics {
var diags hcl.Diagnostics var diags hcl.Diagnostics

View File

@ -29,6 +29,7 @@ type Handler interface {
Evaluator Evaluator
BuildGetter BuildGetter
ConfigFixer ConfigFixer
ConfigInspector
} }
//go:generate enumer -type FixConfigMode //go:generate enumer -type FixConfigMode
@ -52,3 +53,12 @@ type ConfigFixer interface {
// FixConfig will output the config in a fixed manner. // FixConfig will output the config in a fixed manner.
FixConfig(FixConfigOptions) hcl.Diagnostics FixConfig(FixConfigOptions) hcl.Diagnostics
} }
type InspectConfigOptions struct {
Ui
}
type ConfigInspector interface {
// Inspect will output self inspection for a configuration
InspectConfig(InspectConfigOptions) (ret int)
}