Merge pull request #15673 from Luap99/template

Fix go template parsing with "\n" in it
This commit is contained in:
OpenShift Merge Robot
2022-09-13 20:26:24 +02:00
committed by GitHub
14 changed files with 287 additions and 168 deletions

View File

@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/containers/common/pkg/auth"
"github.com/containers/common/pkg/completion"
@ -104,15 +103,15 @@ func reportsToOutput(allReports []*entities.AutoUpdateReport) []autoUpdateOutput
}
func writeTemplate(allReports []*entities.AutoUpdateReport, inputFormat string) error {
var format string
var printHeader bool
rpt := report.New(os.Stdout, "auto-update")
defer rpt.Flush()
output := reportsToOutput(allReports)
var err error
switch inputFormat {
case "":
rows := []string{"{{.Unit}}", "{{.Container}}", "{{.Image}}", "{{.Policy}}", "{{.Updated}}"}
format = "{{range . }}" + strings.Join(rows, "\t") + "\n{{end -}}"
printHeader = true
format := "{{range . }}\t{{.Unit}}\t{{.Container}}\t{{.Image}}\t{{.Policy}}\t{{.Updated}}\n{{end -}}"
rpt, err = rpt.Parse(report.OriginPodman, format)
case "json":
prettyJSON, err := json.MarshalIndent(output, "", " ")
if err != nil {
@ -121,26 +120,17 @@ func writeTemplate(allReports []*entities.AutoUpdateReport, inputFormat string)
fmt.Println(string(prettyJSON))
return nil
default:
format = "{{range . }}" + inputFormat + "\n{{end -}}"
rpt, err = rpt.Parse(report.OriginUser, inputFormat)
}
tmpl, err := report.NewTemplate("auto-update").Parse(format)
if err != nil {
return err
}
w, err := report.NewWriterDefault(os.Stdout)
if err != nil {
return err
}
defer w.Flush()
if printHeader {
if rpt.RenderHeaders {
headers := report.Headers(autoUpdateOutput{}, nil)
if err := tmpl.Execute(w, headers); err != nil {
if err := rpt.Execute(headers); err != nil {
return err
}
}
return tmpl.Execute(w, output)
return rpt.Execute(output)
}

View File

@ -8,7 +8,6 @@ import (
"os"
"regexp"
"strings"
"text/template"
"github.com/containers/common/pkg/completion"
"github.com/containers/common/pkg/report"
@ -16,7 +15,6 @@ import (
"github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/cmd/podman/validate"
"github.com/containers/podman/v4/pkg/domain/entities"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@ -176,13 +174,18 @@ func (i *inspector) inspect(namesOrIDs []string) error {
}
default:
// Landing here implies user has given a custom --format
row := inspectNormalize(i.options.Format, tmpType)
row = report.NormalizeFormat(row)
row = report.EnforceRange(row)
err = printTmpl(tmpType, row, data)
var rpt *report.Formatter
format := inspectNormalize(i.options.Format, i.options.Type)
rpt, err = report.New(os.Stdout, "inspect").Parse(report.OriginUser, format)
if err != nil {
return err
}
defer rpt.Flush()
err = rpt.Execute(data)
}
if err != nil {
logrus.Errorf("Printing inspect output: %v", err)
errs = append(errs, fmt.Errorf("printing inspect output: %w", err))
}
if len(errs) > 0 {
@ -205,22 +208,6 @@ func printJSON(data interface{}) error {
return enc.Encode(data)
}
func printTmpl(typ, row string, data []interface{}) error {
// We cannot use c/common/reports here, too many levels of interface{}
t, err := template.New(typ + " inspect").Funcs(template.FuncMap(report.DefaultFuncs)).Parse(row)
if err != nil {
return err
}
w, err := report.NewWriterDefault(os.Stdout)
if err != nil {
return err
}
err = t.Execute(w, data)
w.Flush()
return err
}
func (i *inspector) inspectAll(ctx context.Context, namesOrIDs []string) ([]interface{}, []error, error) {
var data []interface{}
allErrs := []error{}

View File

@ -5,7 +5,6 @@ package machine
import (
"fmt"
"html/template"
"os"
"runtime"
@ -75,13 +74,16 @@ func info(cmd *cobra.Command, args []string) error {
}
fmt.Println(string(b))
case cmd.Flags().Changed("format"):
tmpl := template.New(cmd.Name()).Funcs(template.FuncMap(report.DefaultFuncs))
inFormat = report.NormalizeFormat(inFormat)
tmpl, err := tmpl.Parse(inFormat)
rpt := report.New(os.Stdout, cmd.Name())
defer rpt.Flush()
// Use OriginUnknown so it does not add an extra range since it
// will only be called for a single element and not a slice.
rpt, err = rpt.Parse(report.OriginUnknown, inFormat)
if err != nil {
return err
}
return tmpl.Execute(os.Stdout, info)
return rpt.Execute(info)
default:
b, err := yaml.Marshal(info)
if err != nil {

View File

@ -11,7 +11,6 @@ import (
"github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/cmd/podman/utils"
"github.com/containers/podman/v4/pkg/machine"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@ -66,28 +65,23 @@ func inspect(cmd *cobra.Command, args []string) error {
}
vms = append(vms, *ii)
}
switch {
case cmd.Flag("format").Changed:
row := report.NormalizeFormat(inspectFlag.format)
row = report.EnforceRange(row)
rpt := report.New(os.Stdout, cmd.Name())
defer rpt.Flush()
tmpl, err := report.NewTemplate("Machine inspect").Parse(row)
rpt, err := rpt.Parse(report.OriginUser, inspectFlag.format)
if err != nil {
return err
}
w, err := report.NewWriterDefault(os.Stdout)
if err != nil {
return err
if err := rpt.Execute(vms); err != nil {
errs = append(errs, err)
}
if err := tmpl.Execute(w, vms); err != nil {
logrus.Error(err)
}
w.Flush()
default:
if err := printJSON(vms); err != nil {
logrus.Error(err)
errs = append(errs, err)
}
}
return errs.PrintErrors()

View File

@ -53,7 +53,7 @@ func init() {
flags := lsCmd.Flags()
formatFlagName := "format"
flags.StringVar(&listFlag.format, formatFlagName, "{{.Name}}\t{{.VMType}}\t{{.Created}}\t{{.LastUp}}\t{{.CPUs}}\t{{.Memory}}\t{{.DiskSize}}\n", "Format volume output using JSON or a Go template")
flags.StringVar(&listFlag.format, formatFlagName, "{{range .}}{{.Name}}\t{{.VMType}}\t{{.Created}}\t{{.LastUp}}\t{{.CPUs}}\t{{.Memory}}\t{{.DiskSize}}\n{{end -}}", "Format volume output using JSON or a Go template")
_ = lsCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&entities.ListReporter{}))
flags.BoolVar(&listFlag.noHeading, "noheading", false, "Do not print headers")
flags.BoolVarP(&listFlag.quiet, "quiet", "q", false, "Show only machine names")
@ -66,10 +66,6 @@ func list(cmd *cobra.Command, args []string) error {
err error
)
if listFlag.quiet {
listFlag.format = "{{.Name}}\n"
}
provider := GetSystemDefaultProvider()
listResponse, err = provider.List(opts)
if err != nil {
@ -115,37 +111,29 @@ func outputTemplate(cmd *cobra.Command, responses []*entities.ListReporter) erro
"Memory": "MEMORY",
"DiskSize": "DISK SIZE",
})
printHeader := !listFlag.noHeading
if listFlag.quiet {
printHeader = false
}
var row string
rpt := report.New(os.Stdout, cmd.Name())
defer rpt.Flush()
var err error
switch {
case cmd.Flags().Changed("format"):
row = cmd.Flag("format").Value.String()
printHeader = report.HasTable(row)
row = report.NormalizeFormat(row)
case cmd.Flag("format").Changed:
rpt, err = rpt.Parse(report.OriginUser, listFlag.format)
case listFlag.quiet:
rpt, err = rpt.Parse(report.OriginUser, "{{.Name}}\n")
default:
row = cmd.Flag("format").Value.String()
rpt, err = rpt.Parse(report.OriginPodman, listFlag.format)
}
format := report.EnforceRange(row)
tmpl, err := report.NewTemplate("list").Parse(format)
if err != nil {
return err
}
w, err := report.NewWriterDefault(os.Stdout)
if err != nil {
return err
}
defer w.Flush()
if printHeader {
if err := tmpl.Execute(w, headers); err != nil {
if rpt.RenderHeaders && !listFlag.noHeading {
if err := rpt.Execute(headers); err != nil {
return fmt.Errorf("failed to write report column headers: %w", err)
}
}
return tmpl.Execute(w, responses)
return rpt.Execute(responses)
}
func strTime(t time.Time) string {

View File

@ -122,36 +122,27 @@ func templateOut(cmd *cobra.Command, responses []types.Network) error {
"ID": "network id",
})
renderHeaders := report.HasTable(networkListOptions.Format)
var row string
rpt := report.New(os.Stdout, cmd.Name())
defer rpt.Flush()
var err error
switch {
case cmd.Flags().Changed("format"):
row = report.NormalizeFormat(networkListOptions.Format)
case cmd.Flag("format").Changed:
rpt, err = rpt.Parse(report.OriginUser, networkListOptions.Format)
default:
// 'podman network ls' equivalent to 'podman network ls --format="table {{.ID}} {{.Name}} {{.Version}} {{.Plugins}}" '
row = "{{.ID}}\t{{.Name}}\t{{.Driver}}\n"
renderHeaders = true
rpt, err = rpt.Parse(report.OriginPodman, "{{range .}}{{.ID}}\t{{.Name}}\t{{.Driver}}\n{{end -}}")
}
format := report.EnforceRange(row)
tmpl, err := report.NewTemplate("list").Parse(format)
if err != nil {
return err
}
w, err := report.NewWriterDefault(os.Stdout)
if err != nil {
return err
}
defer w.Flush()
noHeading, _ := cmd.Flags().GetBool("noheading")
if !noHeading && renderHeaders {
if err := tmpl.Execute(w, headers); err != nil {
return err
if rpt.RenderHeaders && !noHeading {
if err := rpt.Execute(headers); err != nil {
return fmt.Errorf("failed to write report column headers: %w", err)
}
}
return tmpl.Execute(w, nlprs)
return rpt.Execute(nlprs)
}
// ListPrintReports returns the network list report

View File

@ -47,20 +47,15 @@ func inspect(cmd *cobra.Command, args []string) error {
}
if cmd.Flags().Changed("format") {
row := report.NormalizeFormat(format)
formatted := report.EnforceRange(row)
rpt := report.New(os.Stdout, cmd.Name())
defer rpt.Flush()
tmpl, err := report.NewTemplate("inspect").Parse(formatted)
rpt, err := rpt.Parse(report.OriginUser, format)
if err != nil {
return err
}
w, err := report.NewWriterDefault(os.Stdout)
if err != nil {
return err
}
defer w.Flush()
if err := tmpl.Execute(w, inspected); err != nil {
if err := rpt.Execute(inspected); err != nil {
return err
}
} else {

View File

@ -46,7 +46,7 @@ func init() {
flags := lsCmd.Flags()
formatFlagName := "format"
flags.StringVar(&listFlag.format, formatFlagName, "{{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.CreatedAt}}\t{{.UpdatedAt}}\t\n", "Format volume output using Go template")
flags.StringVar(&listFlag.format, formatFlagName, "{{range .}}{{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.CreatedAt}}\t{{.UpdatedAt}}\n{{end -}}", "Format volume output using Go template")
_ = lsCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&entities.SecretInfoReport{}))
filterFlagName := "filter"
@ -105,31 +105,25 @@ func outputTemplate(cmd *cobra.Command, responses []*entities.SecretListReport)
"UpdatedAt": "UPDATED",
})
row := cmd.Flag("format").Value.String()
if cmd.Flags().Changed("format") {
row = report.NormalizeFormat(row)
}
format := report.EnforceRange(row)
rpt := report.New(os.Stdout, cmd.Name())
defer rpt.Flush()
tmpl, err := report.NewTemplate("list").Parse(format)
var err error
switch {
case cmd.Flag("format").Changed:
rpt, err = rpt.Parse(report.OriginUser, listFlag.format)
default:
rpt, err = rpt.Parse(report.OriginPodman, listFlag.format)
}
if err != nil {
return err
}
w, err := report.NewWriterDefault(os.Stdout)
if err != nil {
return err
}
defer w.Flush()
if cmd.Flags().Changed("format") && !report.HasTable(listFlag.format) {
listFlag.noHeading = true
}
if !listFlag.noHeading {
if err := tmpl.Execute(w, headers); err != nil {
noHeading, _ := cmd.Flags().GetBool("noheading")
if rpt.RenderHeaders && !noHeading {
if err := rpt.Execute(headers); err != nil {
return fmt.Errorf("failed to write report column headers: %w", err)
}
}
return tmpl.Execute(w, responses)
return rpt.Execute(responses)
}

View File

@ -3,7 +3,6 @@ package system
import (
"fmt"
"os"
"text/template"
"github.com/containers/common/pkg/completion"
"github.com/containers/common/pkg/report"
@ -86,14 +85,16 @@ func info(cmd *cobra.Command, args []string) error {
}
fmt.Println(string(b))
case cmd.Flags().Changed("format"):
// Cannot use report.New() as it enforces {{range .}} for OriginUser templates
tmpl := template.New(cmd.Name()).Funcs(template.FuncMap(report.DefaultFuncs))
inFormat = report.NormalizeFormat(inFormat)
tmpl, err := tmpl.Parse(inFormat)
rpt := report.New(os.Stdout, cmd.Name())
defer rpt.Flush()
// Use OriginUnknown so it does not add an extra range since it
// will only be called for a single element and not a slice.
rpt, err = rpt.Parse(report.OriginUnknown, inFormat)
if err != nil {
return err
}
return tmpl.Execute(os.Stdout, info)
return rpt.Execute(info)
default:
b, err := yaml.Marshal(info)
if err != nil {

View File

@ -4,7 +4,6 @@ import (
"fmt"
"os"
"strings"
"text/template"
"github.com/containers/common/pkg/completion"
"github.com/containers/common/pkg/report"
@ -12,6 +11,7 @@ import (
"github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/cmd/podman/validate"
"github.com/containers/podman/v4/pkg/domain/entities"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@ -53,22 +53,25 @@ func version(cmd *cobra.Command, args []string) error {
}
if cmd.Flag("format").Changed {
// Cannot use report.New() as it enforces {{range .}} for OriginUser templates
tmpl := template.New(cmd.Name()).Funcs(template.FuncMap(report.DefaultFuncs))
rpt := report.New(os.Stdout, cmd.Name())
defer rpt.Flush()
versionFormat = report.NormalizeFormat(versionFormat)
tmpl, err := tmpl.Parse(versionFormat)
// Use OriginUnknown so it does not add an extra range since it
// will only be called for a single element and not a slice.
rpt, err = rpt.Parse(report.OriginUnknown, versionFormat)
if err != nil {
return err
}
if err := tmpl.Execute(os.Stdout, versions); err != nil {
if err := rpt.Execute(versions); err != nil {
// only log at debug since we fall back to the client only template
logrus.Debugf("Failed to execute template: %v", err)
// On Failure, assume user is using older version of podman version --format and check client
versionFormat = strings.ReplaceAll(versionFormat, ".Server.", ".")
tmpl, err := tmpl.Parse(versionFormat)
rpt, err := rpt.Parse(report.OriginUnknown, versionFormat)
if err != nil {
return err
}
if err := tmpl.Execute(os.Stdout, versions.Client); err != nil {
if err := rpt.Execute(versions.Client); err != nil {
return err
}
}

View File

@ -55,7 +55,7 @@ func init() {
_ = lsCommand.RegisterFlagCompletionFunc(filterFlagName, common.AutocompleteVolumeFilters)
formatFlagName := "format"
flags.StringVar(&cliOpts.Format, formatFlagName, "{{.Driver}}\t{{.Name}}\n", "Format volume output using Go template")
flags.StringVar(&cliOpts.Format, formatFlagName, "{{range .}}{{.Driver}}\t{{.Name}}\n{{end -}}", "Format volume output using Go template")
_ = lsCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&entities.VolumeListReport{}))
flags.Bool("noheading", false, "Do not print headers")
@ -95,34 +95,28 @@ func outputTemplate(cmd *cobra.Command, responses []*entities.VolumeListReport)
"Name": "VOLUME NAME",
})
var row string
rpt := report.New(os.Stdout, cmd.Name())
defer rpt.Flush()
var err error
switch {
case cmd.Flag("format").Changed:
rpt, err = rpt.Parse(report.OriginUser, cliOpts.Format)
case cliOpts.Quiet:
row = "{{.Name}}\n"
case cmd.Flags().Changed("format"):
row = report.NormalizeFormat(cliOpts.Format)
rpt, err = rpt.Parse(report.OriginUser, "{{.Name}}\n")
default:
row = cmd.Flag("format").Value.String()
rpt, err = rpt.Parse(report.OriginPodman, cliOpts.Format)
}
format := report.EnforceRange(row)
tmpl, err := report.NewTemplate("list").Parse(format)
if err != nil {
return err
}
w, err := report.NewWriterDefault(os.Stdout)
if err != nil {
return err
}
defer w.Flush()
if !(noHeading || cliOpts.Quiet || cmd.Flag("format").Changed) {
if err := tmpl.Execute(w, headers); err != nil {
if (rpt.RenderHeaders) && !noHeading {
if err := rpt.Execute(headers); err != nil {
return fmt.Errorf("failed to write report column headers: %w", err)
}
}
return tmpl.Execute(w, responses)
return rpt.Execute(responses)
}
func outputJSON(vols []*entities.VolumeListReport) error {

View File

@ -75,5 +75,13 @@ var _ = Describe("podman machine stop", func() {
Expect(err).To(BeNil())
Expect(inspectSession).To(Exit(0))
Expect(inspectSession.Bytes()).To(ContainSubstring(name))
// check invalid template returns error
inspect = new(inspectMachine)
inspect = inspect.withFormat("{{.Abcde}}")
inspectSession, err = mb.setName(name).setCmd(inspect).run()
Expect(err).To(BeNil())
Expect(inspectSession).To(Exit(125))
Expect(inspectSession.errorToString()).To(ContainSubstring("can't evaluate field Abcde in type machine.InspectInfo"))
})
})

View File

@ -75,7 +75,10 @@ var _ = Describe("Podman volume ls", func() {
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))
Expect(session.OutputToStringArray()).To(HaveLen(1), session.OutputToString())
arr := session.OutputToStringArray()
Expect(arr).To(HaveLen(2))
Expect(arr[0]).To(ContainSubstring("NAME"))
Expect(arr[1]).To(ContainSubstring("myvol"))
})
It("podman ls volume with --filter flag", func() {

169
test/system/610-format.bats Normal file
View File

@ -0,0 +1,169 @@
#!/usr/bin/env bats -*- bats -*-
#
# PR #15673: For all commands that accept --format '{{.GoTemplate}}',
# invoke with --format '{{"\n"}}' and make sure they don't choke.
#
load helpers
function teardown() {
# In case test fails: standard teardown does not wipe machines or secrets
run_podman '?' machine rm -f mymachine
run_podman '?' secret rm mysecret
basic_teardown
}
# Most commands can't just be run with --format; they need an argument or
# option. This table defines what those are.
#
# FIXME: once you've finished fixing them all, remove the SKIPs (just
# remove the entire lines, except for pod-inspect, just remove the SKIP
# but leave "mypod")
extra_args_table="
history | $IMAGE
image history | $IMAGE
image inspect | $IMAGE
container inspect | mycontainer
machine inspect | mymachine
volume inspect | -a
secret inspect | mysecret
network inspect | podman
ps | -a
image search | sdfsdf
search | sdfsdf
pod inspect | mypod
container stats | --no-stream
pod stats | --no-stream
stats | --no-stream
events | --stream=false --events-backend=file
"
# Main test loop. Recursively runs 'podman [subcommand] help', looks for:
# > '[command]', which indicates, recurse; or
# > '--format', in which case we
# > check autocompletion, look for Go templates, in which case we
# > run the command with --format '{{"\n"}}' and make sure it passes
function check_subcommand() {
for cmd in $(_podman_commands "$@"); do
# Special case: 'podman machine' can't be run as root. No override.
if [[ "$cmd" = "machine" ]]; then
if ! is_rootless; then
unset extra_args["podman machine inspect"]
continue
fi
fi
# Human-readable podman command string, with multiple spaces collapsed
command_string="podman $* $cmd"
command_string=${command_string// / } # 'podman x' -> 'podman x'
# Run --help, decide if this is a subcommand with subcommands
run_podman "$@" $cmd --help
local full_help="$output"
# The line immediately after 'Usage:' gives us a 1-line synopsis
usage=$(echo "$full_help" | grep -A1 '^Usage:' | tail -1)
assert "$usage" != "" "podman $cmd: no Usage message found"
# Strip off the leading command string; we no longer need it
usage=$(sed -e "s/^ $command_string \?//" <<<"$usage")
# If usage ends in '[command]', recurse into subcommands
if expr "$usage" : '\[command\]' >/dev/null; then
# (except for 'podman help', which is a special case)
if [[ $cmd != "help" ]]; then
check_subcommand "$@" $cmd
fi
continue
fi
# Not a subcommand-subcommand. Look for --format option
if [[ ! "$output" =~ "--format" ]]; then
continue
fi
# Have --format. Make sure it's a Go-template option, not like --push
run_podman __completeNoDesc "$@" "$cmd" --format '{{.'
if [[ ! "$output" =~ \{\{\.[A-Z] ]]; then
continue
fi
# Got one.
dprint "$command_string has --format"
# Whatever is needed to make a runnable command
local extra=${extra_args[$command_string]}
if [[ -n "$extra" ]]; then
# Cross off our list
unset extra_args["$command_string"]
fi
# This is what does the work. We run with '?' so we can offer
# better error messages than just "exited with error status".
run_podman '?' "$@" "$cmd" $extra --format '{{"\n"}}'
# Output must always be empty.
#
# - If you see "unterminated quoted string" here, there's a
# regression, and you need to fix --format (see PR #15673)
#
# - If you see any other error, it probably means that someone
# added a new podman subcommand that supports --format but
# needs some sort of option or argument to actually run.
# See 'extra_args_table' at the top of this script.
#
assert "$output" = "" "$command_string --format '{{\"\n\"}}'"
# *Now* check exit status. This should never, ever, ever trigger!
# If it does, it means the podman command failed without an err msg!
assert "$status" = "0" \
"$command_string --format '{{\"\n\"}}' failed with no output!"
done
}
# Test entry point
@test "check Go template formatting" {
skip_if_remote
if is_ubuntu; then
skip 'ubuntu VMs do not have qemu (exec: "qemu-system-x86_64": executable file not found in $PATH)'
fi
# Convert the table at top to an associative array, keyed on subcommand
declare -A extra_args
while read subcommand extra; do
extra_args["podman $subcommand"]=$extra
done < <(parse_table "$extra_args_table")
# Setup: some commands need a container, pod, machine, or secret
run_podman run -d --name mycontainer $IMAGE top
run_podman pod create mypod
run_podman secret create mysecret /etc/hosts
if is_rootless; then
run_podman machine init --image-path=/dev/null mymachine
fi
# Run the test
check_subcommand
# Clean up
run_podman pod rm mypod
run_podman rmi $(pause_image)
run_podman rm -f -t0 mycontainer
run_podman secret rm mysecret
if is_rootless; then
run_podman machine rm -f mymachine
fi
# Make sure there are no leftover commands in our table - this would
# indicate a typo in the table, or a flaw in our logic such that
# we're not actually recursing.
local leftovers="${!extra_args[@]}"
assert "$leftovers" = "" "Did not find (or test) subcommands:"
}
# vim: filetype=sh