diff options
Diffstat (limited to 'cmd')
-rw-r--r-- | cmd/podman/common/createparse.go | 2 | ||||
-rw-r--r-- | cmd/podman/common/specgen.go | 2 | ||||
-rw-r--r-- | cmd/podman/containers/diff.go | 9 | ||||
-rw-r--r-- | cmd/podman/containers/ps.go | 2 | ||||
-rw-r--r-- | cmd/podman/images/list.go | 74 | ||||
-rw-r--r-- | cmd/podman/parse/json.go | 3 | ||||
-rw-r--r-- | cmd/podman/parse/json_test.go | 25 | ||||
-rw-r--r-- | cmd/podman/report/format.go | 68 | ||||
-rw-r--r-- | cmd/podman/report/format_test.go | 35 | ||||
-rw-r--r-- | cmd/podman/root.go | 137 |
10 files changed, 241 insertions, 116 deletions
diff --git a/cmd/podman/common/createparse.go b/cmd/podman/common/createparse.go index 059f9050f..09ee5aa0c 100644 --- a/cmd/podman/common/createparse.go +++ b/cmd/podman/common/createparse.go @@ -10,7 +10,7 @@ import ( func (c *ContainerCLIOpts) validate() error { var () if c.Rm && c.Restart != "" && c.Restart != "no" { - return errors.Errorf("the --rm option conflicts with --restart") + return errors.Errorf(`the --rm option conflicts with --restart, when the restartPolicy is not "" and "no"`) } if _, err := util.ValidatePullType(c.Pull); err != nil { diff --git a/cmd/podman/common/specgen.go b/cmd/podman/common/specgen.go index e7b88eb3f..84ae70b6a 100644 --- a/cmd/podman/common/specgen.go +++ b/cmd/podman/common/specgen.go @@ -233,7 +233,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string // validate flags as needed if err := c.validate(); err != nil { - return nil + return err } s.User = c.User diff --git a/cmd/podman/containers/diff.go b/cmd/podman/containers/diff.go index 227c13f4c..a3ca6edf9 100644 --- a/cmd/podman/containers/diff.go +++ b/cmd/podman/containers/diff.go @@ -1,6 +1,7 @@ package containers import ( + "github.com/containers/podman/v2/cmd/podman/parse" "github.com/containers/podman/v2/cmd/podman/registry" "github.com/containers/podman/v2/cmd/podman/report" "github.com/containers/podman/v2/cmd/podman/validate" @@ -52,11 +53,11 @@ func diff(cmd *cobra.Command, args []string) error { return err } - switch diffOpts.Format { - case "": - return report.ChangesToTable(results) - case "json": + switch { + case parse.MatchesJSONFormat(diffOpts.Format): return report.ChangesToJSON(results) + case diffOpts.Format == "": + return report.ChangesToTable(results) default: return errors.New("only supported value for '--format' is 'json'") } diff --git a/cmd/podman/containers/ps.go b/cmd/podman/containers/ps.go index a78b35c08..c4c8b60f3 100644 --- a/cmd/podman/containers/ps.go +++ b/cmd/podman/containers/ps.go @@ -240,7 +240,7 @@ func ps(cmd *cobra.Command, args []string) error { func createPsOut() (string, string) { var row string if listOpts.Namespace { - headers := "CONTAINER ID\tNAMES\tPID\tCGROUPNS\tIPC\tMNT\tNET\tPIDN\tUSERNS\tUTS\n" + headers := "CONTAINER ID\tNAMES\tPID\tCGROUPNS\tIPC\tMNT\tNET\tPIDNS\tUSERNS\tUTS\n" row := "{{.ID}}\t{{.Names}}\t{{.Pid}}\t{{.Namespaces.Cgroup}}\t{{.Namespaces.IPC}}\t{{.Namespaces.MNT}}\t{{.Namespaces.NET}}\t{{.Namespaces.PIDNS}}\t{{.Namespaces.User}}\t{{.Namespaces.UTS}}\n" return headers, row } diff --git a/cmd/podman/images/list.go b/cmd/podman/images/list.go index ffb341fc4..239da9d28 100644 --- a/cmd/podman/images/list.go +++ b/cmd/podman/images/list.go @@ -11,7 +11,9 @@ import ( "unicode" "github.com/containers/image/v5/docker/reference" + "github.com/containers/podman/v2/cmd/podman/parse" "github.com/containers/podman/v2/cmd/podman/registry" + "github.com/containers/podman/v2/cmd/podman/report" "github.com/containers/podman/v2/pkg/domain/entities" "github.com/docker/go-units" "github.com/pkg/errors" @@ -106,9 +108,12 @@ func images(cmd *cobra.Command, args []string) error { switch { case listFlag.quiet: return writeID(imgs) - case cmd.Flag("format").Changed && listFlag.format == "json": + case parse.MatchesJSONFormat(listFlag.format): return writeJSON(imgs) default: + if cmd.Flag("format").Changed { + listFlag.noHeading = true // V1 compatibility + } return writeTemplate(imgs) } } @@ -156,25 +161,29 @@ func writeJSON(images []imageReporter) error { } func writeTemplate(imgs []imageReporter) error { - var ( - hdr, row string - ) - if len(listFlag.format) < 1 { - hdr, row = imageListFormat(listFlag) + hdrs := report.Headers(imageReporter{}, map[string]string{ + "ID": "IMAGE ID", + "ReadOnly": "R/O", + }) + + var row string + if listFlag.format == "" { + row = lsFormatFromFlags(listFlag) } else { - row = listFlag.format - if !strings.HasSuffix(row, "\n") { - row += "\n" - } + row = report.NormalizeFormat(listFlag.format) } - format := hdr + "{{range . }}" + row + "{{end}}" - tmpl, err := template.New("list").Parse(format) - if err != nil { - return err - } - tmpl = template.Must(tmpl, nil) + + format := "{{range . }}" + row + "{{end}}" + tmpl := template.Must(template.New("list").Parse(format)) w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) defer w.Flush() + + if !listFlag.noHeading { + if err := tmpl.Execute(w, hdrs); err != nil { + return err + } + } + return tmpl.Execute(w, imgs) } @@ -276,40 +285,27 @@ func sortFunc(key string, data []imageReporter) func(i, j int) bool { } } -func imageListFormat(flags listFlagType) (string, string) { - // Defaults - hdr := "REPOSITORY\tTAG" - row := "{{.Repository}}\t{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}" +func lsFormatFromFlags(flags listFlagType) string { + row := []string{ + "{{if .Repository}}{{.Repository}}{{else}}<none>{{end}}", + "{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}", + } if flags.digests { - hdr += "\tDIGEST" - row += "\t{{.Digest}}" + row = append(row, "{{.Digest}}") } - hdr += "\tIMAGE ID" - row += "\t{{.ID}}" - - hdr += "\tCREATED\tSIZE" - row += "\t{{.Created}}\t{{.Size}}" + row = append(row, "{{.ID}}", "{{.Created}}", "{{.Size}}") if flags.history { - hdr += "\tHISTORY" - row += "\t{{if .History}}{{.History}}{{else}}<none>{{end}}" + row = append(row, "{{if .History}}{{.History}}{{else}}<none>{{end}}") } if flags.readOnly { - hdr += "\tReadOnly" - row += "\t{{.ReadOnly}}" - } - - if flags.noHeading { - hdr = "" - } else { - hdr += "\n" + row = append(row, "{{.ReadOnly}}") } - row += "\n" - return hdr, row + return strings.Join(row, "\t") + "\n" } type imageReporter struct { diff --git a/cmd/podman/parse/json.go b/cmd/podman/parse/json.go index 95a6633b8..40ac415db 100644 --- a/cmd/podman/parse/json.go +++ b/cmd/podman/parse/json.go @@ -2,8 +2,9 @@ package parse import "regexp" -var jsonFormatRegex = regexp.MustCompile(`^(\s*json\s*|\s*{{\s*json\s*\.\s*}}\s*)$`) +var jsonFormatRegex = regexp.MustCompile(`^\s*(json|{{\s*json\s*( \.)?\s*}})\s*$`) +// MatchesJSONFormat test CLI --format string to be a JSON request func MatchesJSONFormat(s string) bool { return jsonFormatRegex.Match([]byte(s)) } diff --git a/cmd/podman/parse/json_test.go b/cmd/podman/parse/json_test.go index 5cad185fd..ec3b5664b 100644 --- a/cmd/podman/parse/json_test.go +++ b/cmd/podman/parse/json_test.go @@ -1,6 +1,8 @@ package parse import ( + "fmt" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -13,18 +15,31 @@ func TestMatchesJSONFormat(t *testing.T) { }{ {"json", true}, {" json", true}, - {"json ", true}, + {" json ", true}, {" json ", true}, + {"{{json}}", true}, + {"{{json }}", true}, {"{{json .}}", true}, {"{{ json .}}", true}, - {"{{json . }}", true}, - {" {{ json . }} ", true}, - {"{{json }}", false}, - {"{{json .", false}, + {"{{ json . }}", true}, + {" {{ json . }} ", true}, + {"{{ json .", false}, {"json . }}", false}, + {"{{.ID }} json .", false}, + {"json .", false}, + {"{{json.}}", false}, } for _, tt := range tests { assert.Equal(t, tt.expected, MatchesJSONFormat(tt.input)) } + + for _, tc := range tests { + tc := tc + label := "MatchesJSONFormat/" + strings.ReplaceAll(tc.input, " ", "_") + t.Run(label, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.expected, MatchesJSONFormat(tc.input), fmt.Sprintf("Scanning %q failed", tc.input)) + }) + } } diff --git a/cmd/podman/report/format.go b/cmd/podman/report/format.go new file mode 100644 index 000000000..32d92bec5 --- /dev/null +++ b/cmd/podman/report/format.go @@ -0,0 +1,68 @@ +package report + +import ( + "reflect" + "strings" +) + +// tableReplacer will remove 'table ' prefix and clean up tabs +var tableReplacer = strings.NewReplacer( + "table ", "", + `\t`, "\t", + `\n`, "\n", + " ", "\t", +) + +// escapedReplacer will clean up escaped characters from CLI +var escapedReplacer = strings.NewReplacer( + `\t`, "\t", + `\n`, "\n", +) + +// NormalizeFormat reads given go template format provided by CLI and munges it into what we need +func NormalizeFormat(format string) string { + f := format + // two replacers used so we only remove the prefix keyword `table` + if strings.HasPrefix(f, "table ") { + f = tableReplacer.Replace(f) + } else { + f = escapedReplacer.Replace(format) + } + + if !strings.HasSuffix(f, "\n") { + f += "\n" + } + + return f +} + +// Headers queries the interface for field names +func Headers(object interface{}, overrides map[string]string) []map[string]string { + value := reflect.ValueOf(object) + if value.Kind() == reflect.Ptr { + value = value.Elem() + } + + // Column header will be field name upper-cased. + headers := make(map[string]string, value.NumField()) + for i := 0; i < value.Type().NumField(); i++ { + field := value.Type().Field(i) + // Recurse to find field names from promoted structs + if field.Type.Kind() == reflect.Struct && field.Anonymous { + h := Headers(reflect.New(field.Type).Interface(), nil) + for k, v := range h[0] { + headers[k] = v + } + continue + } + headers[field.Name] = strings.ToUpper(field.Name) + } + + if len(overrides) > 0 { + // Override column header as provided + for k, v := range overrides { + headers[k] = strings.ToUpper(v) + } + } + return []map[string]string{headers} +} diff --git a/cmd/podman/report/format_test.go b/cmd/podman/report/format_test.go new file mode 100644 index 000000000..7dd62e899 --- /dev/null +++ b/cmd/podman/report/format_test.go @@ -0,0 +1,35 @@ +package report + +import ( + "strings" + "testing" +) + +func TestNormalizeFormat(t *testing.T) { + cases := []struct { + format string + expected string + }{ + {"table {{.ID}}", "{{.ID}}\n"}, + {"table {{.ID}} {{.C}}", "{{.ID}}\t{{.C}}\n"}, + {"{{.ID}}", "{{.ID}}\n"}, + {"{{.ID}}\n", "{{.ID}}\n"}, + {"{{.ID}} {{.C}}", "{{.ID}} {{.C}}\n"}, + {"\t{{.ID}}", "\t{{.ID}}\n"}, + {`\t` + "{{.ID}}", "\t{{.ID}}\n"}, + {"table {{.ID}}\t{{.C}}", "{{.ID}}\t{{.C}}\n"}, + {"{{.ID}} table {{.C}}", "{{.ID}} table {{.C}}\n"}, + } + for _, tc := range cases { + tc := tc + + label := strings.ReplaceAll(tc.format, " ", "<sp>") + t.Run("NormalizeFormat/"+label, func(t *testing.T) { + t.Parallel() + actual := NormalizeFormat(tc.format) + if actual != tc.expected { + t.Errorf("Expected %q, actual %q", tc.expected, actual) + } + }) + } +} diff --git a/cmd/podman/root.go b/cmd/podman/root.go index 6424ec12e..1e73f7540 100644 --- a/cmd/podman/root.go +++ b/cmd/podman/root.go @@ -154,34 +154,35 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error { } } - if cmd.Flag("cpu-profile").Changed { - f, err := os.Create(cfg.CPUProfile) - if err != nil { - return errors.Wrapf(err, "unable to create cpu profiling file %s", - cfg.CPUProfile) - } - if err := pprof.StartCPUProfile(f); err != nil { - return err + if !registry.IsRemote() { + if cmd.Flag("cpu-profile").Changed { + f, err := os.Create(cfg.CPUProfile) + if err != nil { + return errors.Wrapf(err, "unable to create cpu profiling file %s", + cfg.CPUProfile) + } + if err := pprof.StartCPUProfile(f); err != nil { + return err + } } - } - if cmd.Flag("trace").Changed { - tracer, closer := tracing.Init("podman") - opentracing.SetGlobalTracer(tracer) - cfg.SpanCloser = closer + if cmd.Flag("trace").Changed { + tracer, closer := tracing.Init("podman") + opentracing.SetGlobalTracer(tracer) + cfg.SpanCloser = closer - cfg.Span = tracer.StartSpan("before-context") - cfg.SpanCtx = opentracing.ContextWithSpan(registry.Context(), cfg.Span) - opentracing.StartSpanFromContext(cfg.SpanCtx, cmd.Name()) - } + cfg.Span = tracer.StartSpan("before-context") + cfg.SpanCtx = opentracing.ContextWithSpan(registry.Context(), cfg.Span) + opentracing.StartSpanFromContext(cfg.SpanCtx, cmd.Name()) + } - if cfg.MaxWorks <= 0 { - return errors.Errorf("maximum workers must be set to a positive number (got %d)", cfg.MaxWorks) - } - if err := parallel.SetMaxThreads(uint(cfg.MaxWorks)); err != nil { - return err + if cfg.MaxWorks <= 0 { + return errors.Errorf("maximum workers must be set to a positive number (got %d)", cfg.MaxWorks) + } + if err := parallel.SetMaxThreads(uint(cfg.MaxWorks)); err != nil { + return err + } } - // Setup Rootless environment, IFF: // 1) in ABI mode // 2) running as non-root @@ -206,12 +207,14 @@ func persistentPostRunE(cmd *cobra.Command, args []string) error { } cfg := registry.PodmanConfig() - if cmd.Flag("cpu-profile").Changed { - pprof.StopCPUProfile() - } - if cmd.Flag("trace").Changed { - cfg.Span.Finish() - cfg.SpanCloser.Close() + if !registry.IsRemote() { + if cmd.Flag("cpu-profile").Changed { + pprof.StopCPUProfile() + } + if cmd.Flag("trace").Changed { + cfg.Span.Finish() + cfg.SpanCloser.Close() + } } registry.ImageEngine().Shutdown(registry.Context()) @@ -249,51 +252,57 @@ func rootFlags(cmd *cobra.Command, opts *entities.PodmanConfig) { srv, uri, ident := resolveDestination() lFlags := cmd.Flags() - lFlags.BoolVarP(&opts.Remote, "remote", "r", false, "Access remote Podman service (default false)") lFlags.StringVarP(&opts.Engine.ActiveService, "connection", "c", srv, "Connection to use for remote Podman service") lFlags.StringVar(&opts.URI, "url", uri, "URL to access Podman service (CONTAINER_HOST)") lFlags.StringVar(&opts.Identity, "identity", ident, "path to SSH identity file, (CONTAINER_SSHKEY)") + lFlags.BoolVarP(&opts.Remote, "remote", "r", false, "Access remote Podman service (default false)") pFlags := cmd.PersistentFlags() - pFlags.StringVar(&cfg.Engine.CgroupManager, "cgroup-manager", cfg.Engine.CgroupManager, "Cgroup manager to use (\"cgroupfs\"|\"systemd\")") - pFlags.StringVar(&opts.CPUProfile, "cpu-profile", "", "Path for the cpu profiling results") - pFlags.StringVar(&opts.ConmonPath, "conmon", "", "Path of the conmon binary") - pFlags.StringVar(&cfg.Engine.NetworkCmdPath, "network-cmd-path", cfg.Engine.NetworkCmdPath, "Path to the command for configuring the network") - pFlags.StringVar(&cfg.Network.NetworkConfigDir, "cni-config-dir", cfg.Network.NetworkConfigDir, "Path of the configuration directory for CNI networks") - pFlags.StringVar(&cfg.Containers.DefaultMountsFile, "default-mounts-file", cfg.Containers.DefaultMountsFile, "Path to default mounts file") - pFlags.StringVar(&cfg.Engine.EventsLogger, "events-backend", cfg.Engine.EventsLogger, `Events backend to use ("file"|"journald"|"none")`) - pFlags.StringSliceVar(&cfg.Engine.HooksDir, "hooks-dir", cfg.Engine.HooksDir, "Set the OCI hooks directory path (may be set multiple times)") - pFlags.IntVar(&opts.MaxWorks, "max-workers", (runtime.NumCPU()*3)+1, "The maximum number of workers for parallel operations") - pFlags.StringVar(&cfg.Engine.Namespace, "namespace", cfg.Engine.Namespace, "Set the libpod namespace, used to create separate views of the containers and pods on the system") - pFlags.StringVar(&cfg.Engine.StaticDir, "root", "", "Path to the root directory in which data, including images, is stored") - pFlags.StringVar(&opts.RegistriesConf, "registries-conf", "", "Path to a registries.conf to use for image processing") - pFlags.StringVar(&opts.Runroot, "runroot", "", "Path to the 'run directory' where all state information is stored") - pFlags.StringVar(&opts.RuntimePath, "runtime", "", "Path to the OCI-compatible binary used to run containers, default is /usr/bin/runc") - // -s is deprecated due to conflict with -s on subcommands - pFlags.StringVar(&opts.StorageDriver, "storage-driver", "", "Select which storage driver is used to manage storage of images and containers (default is overlay)") - pFlags.StringArrayVar(&opts.StorageOpts, "storage-opt", []string{}, "Used to pass an option to the storage driver") - - pFlags.StringVar(&opts.Engine.TmpDir, "tmpdir", "", "Path to the tmp directory for libpod state content.\n\nNote: use the environment variable 'TMPDIR' to change the temporary storage location for container images, '/var/tmp'.\n") - pFlags.BoolVar(&opts.Trace, "trace", false, "Enable opentracing output (default false)") - + if registry.IsRemote() { + if err := lFlags.MarkHidden("remote"); err != nil { + logrus.Warnf("unable to mark --remote flag as hidden: %s", err.Error()) + } + opts.Remote = true + } else { + pFlags.StringVar(&cfg.Engine.CgroupManager, "cgroup-manager", cfg.Engine.CgroupManager, "Cgroup manager to use (\"cgroupfs\"|\"systemd\")") + pFlags.StringVar(&opts.CPUProfile, "cpu-profile", "", "Path for the cpu profiling results") + pFlags.StringVar(&opts.ConmonPath, "conmon", "", "Path of the conmon binary") + pFlags.StringVar(&cfg.Engine.NetworkCmdPath, "network-cmd-path", cfg.Engine.NetworkCmdPath, "Path to the command for configuring the network") + pFlags.StringVar(&cfg.Network.NetworkConfigDir, "cni-config-dir", cfg.Network.NetworkConfigDir, "Path of the configuration directory for CNI networks") + pFlags.StringVar(&cfg.Containers.DefaultMountsFile, "default-mounts-file", cfg.Containers.DefaultMountsFile, "Path to default mounts file") + pFlags.StringVar(&cfg.Engine.EventsLogger, "events-backend", cfg.Engine.EventsLogger, `Events backend to use ("file"|"journald"|"none")`) + pFlags.StringSliceVar(&cfg.Engine.HooksDir, "hooks-dir", cfg.Engine.HooksDir, "Set the OCI hooks directory path (may be set multiple times)") + pFlags.IntVar(&opts.MaxWorks, "max-workers", (runtime.NumCPU()*3)+1, "The maximum number of workers for parallel operations") + pFlags.StringVar(&cfg.Engine.Namespace, "namespace", cfg.Engine.Namespace, "Set the libpod namespace, used to create separate views of the containers and pods on the system") + pFlags.StringVar(&cfg.Engine.StaticDir, "root", "", "Path to the root directory in which data, including images, is stored") + pFlags.StringVar(&opts.RegistriesConf, "registries-conf", "", "Path to a registries.conf to use for image processing") + pFlags.StringVar(&opts.Runroot, "runroot", "", "Path to the 'run directory' where all state information is stored") + pFlags.StringVar(&opts.RuntimePath, "runtime", "", "Path to the OCI-compatible binary used to run containers, default is /usr/bin/runc") + // -s is deprecated due to conflict with -s on subcommands + pFlags.StringVar(&opts.StorageDriver, "storage-driver", "", "Select which storage driver is used to manage storage of images and containers (default is overlay)") + pFlags.StringArrayVar(&opts.StorageOpts, "storage-opt", []string{}, "Used to pass an option to the storage driver") + + pFlags.StringVar(&opts.Engine.TmpDir, "tmpdir", "", "Path to the tmp directory for libpod state content.\n\nNote: use the environment variable 'TMPDIR' to change the temporary storage location for container images, '/var/tmp'.\n") + pFlags.BoolVar(&opts.Trace, "trace", false, "Enable opentracing output (default false)") + + // Hide these flags for both ABI and Tunneling + for _, f := range []string{ + "cpu-profile", + "default-mounts-file", + "max-workers", + "registries-conf", + "trace", + } { + if err := pFlags.MarkHidden(f); err != nil { + logrus.Warnf("unable to mark %s flag as hidden: %s", f, err.Error()) + } + } + } // Override default --help information of `--help` global flag var dummyHelp bool pFlags.BoolVar(&dummyHelp, "help", false, "Help for podman") pFlags.StringVar(&logLevel, "log-level", logLevel, fmt.Sprintf("Log messages above specified level (%s)", strings.Join(logLevels, ", "))) - // Hide these flags for both ABI and Tunneling - for _, f := range []string{ - "cpu-profile", - "default-mounts-file", - "max-workers", - "registries-conf", - "trace", - } { - if err := pFlags.MarkHidden(f); err != nil { - logrus.Warnf("unable to mark %s flag as hidden: %s", f, err.Error()) - } - } - // Only create these flags for ABI connections if !registry.IsRemote() { pFlags.StringArrayVar(&opts.RuntimeFlags, "runtime-flag", []string{}, "add global flags for the container runtime") |