diff options
Diffstat (limited to 'cmd/podman/images')
-rw-r--r-- | cmd/podman/images/diff.go | 62 | ||||
-rw-r--r-- | cmd/podman/images/exists.go | 40 | ||||
-rw-r--r-- | cmd/podman/images/history.go | 152 | ||||
-rw-r--r-- | cmd/podman/images/image.go | 25 | ||||
-rw-r--r-- | cmd/podman/images/images.go | 30 | ||||
-rw-r--r-- | cmd/podman/images/import.go | 84 | ||||
-rw-r--r-- | cmd/podman/images/inspect.go | 108 | ||||
-rw-r--r-- | cmd/podman/images/list.go | 273 | ||||
-rw-r--r-- | cmd/podman/images/load.go | 94 | ||||
-rw-r--r-- | cmd/podman/images/prune.go | 86 | ||||
-rw-r--r-- | cmd/podman/images/pull.go | 118 | ||||
-rw-r--r-- | cmd/podman/images/push.go | 120 | ||||
-rw-r--r-- | cmd/podman/images/rm.go | 71 | ||||
-rw-r--r-- | cmd/podman/images/rmi.go | 28 | ||||
-rw-r--r-- | cmd/podman/images/save.go | 86 | ||||
-rw-r--r-- | cmd/podman/images/search.go | 156 | ||||
-rw-r--r-- | cmd/podman/images/tag.go | 32 | ||||
-rw-r--r-- | cmd/podman/images/untag.go | 31 |
18 files changed, 1596 insertions, 0 deletions
diff --git a/cmd/podman/images/diff.go b/cmd/podman/images/diff.go new file mode 100644 index 000000000..dd98dc4d6 --- /dev/null +++ b/cmd/podman/images/diff.go @@ -0,0 +1,62 @@ +package images + +import ( + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/report" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + // podman container _inspect_ + diffCmd = &cobra.Command{ + Use: "diff [flags] CONTAINER", + Args: registry.IdOrLatestArgs, + Short: "Inspect changes on image's file systems", + Long: `Displays changes on a image's filesystem. The image will be compared to its parent layer.`, + RunE: diff, + Example: `podman image diff myImage + podman image diff --format json redis:alpine`, + } + diffOpts *entities.DiffOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: diffCmd, + Parent: imageCmd, + }) + + diffOpts = &entities.DiffOptions{} + flags := diffCmd.Flags() + flags.BoolVar(&diffOpts.Archive, "archive", true, "Save the diff as a tar archive") + _ = flags.MarkHidden("archive") + flags.StringVar(&diffOpts.Format, "format", "", "Change the output format") +} + +func diff(cmd *cobra.Command, args []string) error { + if len(args) == 0 && !diffOpts.Latest { + return errors.New("image must be specified: podman image diff [options [...]] ID-NAME") + } + + results, err := registry.ImageEngine().Diff(registry.GetContext(), args[0], entities.DiffOptions{}) + if err != nil { + return err + } + + switch diffOpts.Format { + case "": + return report.ChangesToTable(results) + case "json": + return report.ChangesToJSON(results) + default: + return errors.New("only supported value for '--format' is 'json'") + } +} + +func Diff(cmd *cobra.Command, args []string, options entities.DiffOptions) error { + diffOpts = &options + return diff(cmd, args) +} diff --git a/cmd/podman/images/exists.go b/cmd/podman/images/exists.go new file mode 100644 index 000000000..0bb288b96 --- /dev/null +++ b/cmd/podman/images/exists.go @@ -0,0 +1,40 @@ +package images + +import ( + "os" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + existsCmd = &cobra.Command{ + Use: "exists IMAGE", + Short: "Check if an image exists in local storage", + Long: `If the named image exists in local storage, podman image exists exits with 0, otherwise the exit code will be 1.`, + Args: cobra.ExactArgs(1), + RunE: exists, + Example: `podman image exists ID + podman image exists IMAGE && podman pull IMAGE`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: existsCmd, + Parent: imageCmd, + }) +} + +func exists(cmd *cobra.Command, args []string) error { + found, err := registry.ImageEngine().Exists(registry.GetContext(), args[0]) + if err != nil { + return err + } + if !found.Value { + os.Exit(1) + } + return nil +} diff --git a/cmd/podman/images/history.go b/cmd/podman/images/history.go new file mode 100644 index 000000000..c92072bff --- /dev/null +++ b/cmd/podman/images/history.go @@ -0,0 +1,152 @@ +package images + +import ( + "context" + "fmt" + "os" + "strings" + "text/tabwriter" + "text/template" + "time" + "unicode" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/docker/go-units" + jsoniter "github.com/json-iterator/go" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + long = `Displays the history of an image. + + The information can be printed out in an easy to read, or user specified format, and can be truncated.` + + // podman _history_ + historyCmd = &cobra.Command{ + Use: "history [flags] IMAGE", + Short: "Show history of a specified image", + Long: long, + Example: "podman history quay.io/fedora/fedora", + Args: cobra.ExactArgs(1), + RunE: history, + } + + opts = struct { + human bool + noTrunc bool + quiet bool + format string + }{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: historyCmd, + }) + + flags := historyCmd.Flags() + flags.StringVar(&opts.format, "format", "", "Change the output to JSON or a Go template") + flags.BoolVarP(&opts.human, "human", "H", true, "Display sizes and dates in human readable format") + flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate the output") + flags.BoolVar(&opts.noTrunc, "notruncate", false, "Do not truncate the output") + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Display the numeric IDs only") +} + +func history(cmd *cobra.Command, args []string) error { + results, err := registry.ImageEngine().History(context.Background(), args[0], entities.ImageHistoryOptions{}) + if err != nil { + return err + } + + if opts.format == "json" { + var err error + if len(results.Layers) == 0 { + _, err = fmt.Fprintf(os.Stdout, "[]\n") + } else { + // ah-hoc change to "Created": type and format + type layer struct { + entities.ImageHistoryLayer + Created string `json:"Created"` + } + + layers := make([]layer, len(results.Layers)) + for i, l := range results.Layers { + layers[i].ImageHistoryLayer = l + layers[i].Created = l.Created.Format(time.RFC3339) + } + json := jsoniter.ConfigCompatibleWithStandardLibrary + enc := json.NewEncoder(os.Stdout) + err = enc.Encode(layers) + } + return err + } + var hr []historyreporter + for _, l := range results.Layers { + hr = append(hr, historyreporter{l}) + } + // Defaults + hdr := "ID\tCREATED\tCREATED BY\tSIZE\tCOMMENT\n" + row := "{{.ID}}\t{{.Created}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}\n" + + if len(opts.format) > 0 { + hdr = "" + row = opts.format + if !strings.HasSuffix(opts.format, "\n") { + row += "\n" + } + } else { + switch { + case opts.human: + row = "{{.ID}}\t{{.Created}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}\n" + case opts.noTrunc: + row = "{{.ID}}\t{{.Created}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}\n" + case opts.quiet: + hdr = "" + row = "{{.ID}}\n" + } + } + format := hdr + "{{range . }}" + row + "{{end}}" + + tmpl := template.Must(template.New("report").Parse(format)) + w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) + err = tmpl.Execute(w, hr) + if err != nil { + fmt.Fprintln(os.Stderr, errors.Wrapf(err, "Failed to print report")) + } + w.Flush() + return nil +} + +type historyreporter struct { + entities.ImageHistoryLayer +} + +func (h historyreporter) Created() string { + if opts.human { + return units.HumanDuration(time.Since(h.ImageHistoryLayer.Created)) + " ago" + } + return h.ImageHistoryLayer.Created.Format(time.RFC3339) +} + +func (h historyreporter) Size() string { + s := units.HumanSizeWithPrecision(float64(h.ImageHistoryLayer.Size), 3) + i := strings.LastIndexFunc(s, unicode.IsNumber) + return s[:i+1] + " " + s[i+1:] +} + +func (h historyreporter) CreatedBy() string { + if len(h.ImageHistoryLayer.CreatedBy) > 45 { + return h.ImageHistoryLayer.CreatedBy[:45-3] + "..." + } + return h.ImageHistoryLayer.CreatedBy +} + +func (h historyreporter) ID() string { + if !opts.noTrunc && len(h.ImageHistoryLayer.ID) >= 12 { + return h.ImageHistoryLayer.ID[0:12] + } + return h.ImageHistoryLayer.ID +} diff --git a/cmd/podman/images/image.go b/cmd/podman/images/image.go new file mode 100644 index 000000000..37e46ab9e --- /dev/null +++ b/cmd/podman/images/image.go @@ -0,0 +1,25 @@ +package images + +import ( + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // Command: podman _image_ + imageCmd = &cobra.Command{ + Use: "image", + Short: "Manage images", + Long: "Manage images", + TraverseChildren: true, + RunE: registry.SubCommandExists, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: imageCmd, + }) +} diff --git a/cmd/podman/images/images.go b/cmd/podman/images/images.go new file mode 100644 index 000000000..fd3ede26a --- /dev/null +++ b/cmd/podman/images/images.go @@ -0,0 +1,30 @@ +package images + +import ( + "strings" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // podman _images_ Alias for podman image _list_ + imagesCmd = &cobra.Command{ + Use: strings.Replace(listCmd.Use, "list", "images", 1), + Args: listCmd.Args, + Short: listCmd.Short, + Long: listCmd.Long, + RunE: listCmd.RunE, + Example: strings.Replace(listCmd.Example, "podman image list", "podman images", -1), + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: imagesCmd, + }) + + imageListFlagSet(imagesCmd.Flags()) +} diff --git a/cmd/podman/images/import.go b/cmd/podman/images/import.go new file mode 100644 index 000000000..1c0568762 --- /dev/null +++ b/cmd/podman/images/import.go @@ -0,0 +1,84 @@ +package images + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podman/parse" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + importDescription = `Create a container image from the contents of the specified tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz). + + Note remote tar balls can be specified, via web address. + Optionally tag the image. You can specify the instructions using the --change option.` + importCommand = &cobra.Command{ + Use: "import [flags] PATH [REFERENCE]", + Short: "Import a tarball to create a filesystem image", + Long: importDescription, + RunE: importCon, + Example: `podman import http://example.com/ctr.tar url-image + cat ctr.tar | podman -q import --message "importing the ctr.tar tarball" - image-imported + cat ctr.tar | podman import -`, + } +) + +var ( + importOpts entities.ImageImportOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: importCommand, + }) + + flags := importCommand.Flags() + flags.StringArrayVarP(&importOpts.Changes, "change", "c", []string{}, "Apply the following possible instructions to the created image (default []): CMD | ENTRYPOINT | ENV | EXPOSE | LABEL | STOPSIGNAL | USER | VOLUME | WORKDIR") + flags.StringVarP(&importOpts.Message, "message", "m", "", "Set commit message for imported image") + flags.BoolVarP(&importOpts.Quiet, "quiet", "q", false, "Suppress output") +} + +func importCon(cmd *cobra.Command, args []string) error { + var ( + source string + reference string + ) + switch len(args) { + case 0: + return errors.Errorf("need to give the path to the tarball, or must specify a tarball of '-' for stdin") + case 1: + source = args[0] + case 2: + source = args[0] + // TODO when save is merged, we need to process reference + // like it is done in there or we end up with docker.io prepends + // instead of the localhost ones + reference = args[1] + default: + return errors.Errorf("too many arguments. Usage TARBALL [REFERENCE]") + } + errFileName := parse.ValidateFileName(source) + errURL := parse.ValidURL(source) + if errURL == nil { + importOpts.SourceIsURL = true + } + if errFileName != nil && errURL != nil { + return multierror.Append(errFileName, errURL) + } + + importOpts.Source = source + importOpts.Reference = reference + + response, err := registry.ImageEngine().Import(context.Background(), importOpts) + if err != nil { + return err + } + fmt.Println(response.Id) + return nil +} diff --git a/cmd/podman/images/inspect.go b/cmd/podman/images/inspect.go new file mode 100644 index 000000000..4482ceee5 --- /dev/null +++ b/cmd/podman/images/inspect.go @@ -0,0 +1,108 @@ +package images + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "text/tabwriter" + "text/template" + + "github.com/containers/buildah/pkg/formats" + "github.com/containers/libpod/cmd/podman/common" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + // Command: podman image _inspect_ + inspectCmd = &cobra.Command{ + Use: "inspect [flags] IMAGE", + Short: "Display the configuration of an image", + Long: `Displays the low-level information on an image identified by name or ID.`, + RunE: inspect, + Example: `podman image inspect alpine`, + } + inspectOpts *entities.InspectOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: inspectCmd, + Parent: imageCmd, + }) + inspectOpts = common.AddInspectFlagSet(inspectCmd) +} + +func inspect(cmd *cobra.Command, args []string) error { + latestContainer := inspectOpts.Latest + + if len(args) == 0 && !latestContainer { + return errors.Errorf("container or image name must be specified: podman inspect [options [...]] name") + } + + if len(args) > 0 && latestContainer { + return errors.Errorf("you cannot provide additional arguments with --latest") + } + + results, err := registry.ImageEngine().Inspect(context.Background(), args, *inspectOpts) + if err != nil { + return err + } + + if len(results.Images) > 0 { + if inspectOpts.Format == "" { + buf, err := json.MarshalIndent(results.Images, "", " ") + if err != nil { + return err + } + fmt.Println(string(buf)) + + for id, e := range results.Errors { + fmt.Fprintf(os.Stderr, "%s: %s\n", id, e.Error()) + } + return nil + } + row := inspectFormat(inspectOpts.Format) + format := "{{range . }}" + row + "{{end}}" + tmpl, err := template.New("inspect").Parse(format) + if err != nil { + return err + } + + w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) + defer func() { _ = w.Flush() }() + err = tmpl.Execute(w, results.Images) + if err != nil { + return err + } + } + + for id, e := range results.Errors { + fmt.Fprintf(os.Stderr, "%s: %s\n", id, e.Error()) + } + return nil +} + +func inspectFormat(row string) string { + r := strings.NewReplacer("{{.Id}}", formats.IDString, + ".Src", ".Source", + ".Dst", ".Destination", + ".ImageID", ".Image", + ) + row = r.Replace(row) + + if !strings.HasSuffix(row, "\n") { + row += "\n" + } + return row +} + +func Inspect(cmd *cobra.Command, args []string, options *entities.InspectOptions) error { + inspectOpts = options + return inspect(cmd, args) +} diff --git a/cmd/podman/images/list.go b/cmd/podman/images/list.go new file mode 100644 index 000000000..366dfc4ba --- /dev/null +++ b/cmd/podman/images/list.go @@ -0,0 +1,273 @@ +package images + +import ( + "errors" + "fmt" + "os" + "sort" + "strings" + "text/tabwriter" + "text/template" + "time" + "unicode" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/docker/go-units" + jsoniter "github.com/json-iterator/go" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +type listFlagType struct { + format string + history bool + noHeading bool + noTrunc bool + quiet bool + sort string + readOnly bool + digests bool +} + +var ( + // Command: podman image _list_ + listCmd = &cobra.Command{ + Use: "list [flag] [IMAGE]", + Aliases: []string{"ls"}, + Args: cobra.MaximumNArgs(1), + Short: "List images in local storage", + Long: "Lists images previously pulled to the system or created on the system.", + RunE: images, + Example: `podman image list --format json + podman image list --sort repository --format "table {{.ID}} {{.Repository}} {{.Tag}}" + podman image list --filter dangling=true`, + } + + // Options to pull data + listOptions = entities.ImageListOptions{} + + // Options for presenting data + listFlag = listFlagType{} + + sortFields = entities.NewStringSet( + "created", + "id", + "repository", + "size", + "tag") +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: listCmd, + Parent: imageCmd, + }) + imageListFlagSet(listCmd.Flags()) +} + +func imageListFlagSet(flags *pflag.FlagSet) { + flags.BoolVarP(&listOptions.All, "all", "a", false, "Show all images (default hides intermediate images)") + flags.StringSliceVarP(&listOptions.Filter, "filter", "f", []string{}, "Filter output based on conditions provided (default [])") + flags.StringVar(&listFlag.format, "format", "", "Change the output format to JSON or a Go template") + flags.BoolVar(&listFlag.digests, "digests", false, "Show digests") + flags.BoolVarP(&listFlag.noHeading, "noheading", "n", false, "Do not print column headings") + flags.BoolVar(&listFlag.noTrunc, "no-trunc", false, "Do not truncate output") + flags.BoolVar(&listFlag.noTrunc, "notruncate", false, "Do not truncate output") + flags.BoolVarP(&listFlag.quiet, "quiet", "q", false, "Display only image IDs") + flags.StringVar(&listFlag.sort, "sort", "created", "Sort by "+sortFields.String()) + flags.BoolVarP(&listFlag.history, "history", "", false, "Display the image name history") +} + +func images(cmd *cobra.Command, args []string) error { + if len(listOptions.Filter) > 0 && len(args) > 0 { + return errors.New("cannot specify an image and a filter(s)") + } + + if len(listOptions.Filter) < 1 && len(args) > 0 { + listOptions.Filter = append(listOptions.Filter, "reference="+args[0]) + } + + if cmd.Flag("sort").Changed && !sortFields.Contains(listFlag.sort) { + return fmt.Errorf("\"%s\" is not a valid field for sorting. Choose from: %s", + listFlag.sort, sortFields.String()) + } + + summaries, err := registry.ImageEngine().List(registry.GetContext(), listOptions) + if err != nil { + return err + } + + imageS := summaries + sort.Slice(imageS, sortFunc(listFlag.sort, imageS)) + + if cmd.Flag("format").Changed && listFlag.format == "json" { + return writeJSON(imageS) + } else { + return writeTemplate(imageS, err) + } +} + +func writeJSON(imageS []*entities.ImageSummary) error { + type image struct { + entities.ImageSummary + Created string + } + + imgs := make([]image, 0, len(imageS)) + for _, e := range imageS { + var h image + h.ImageSummary = *e + h.Created = time.Unix(e.Created, 0).Format(time.RFC3339) + h.RepoTags = nil + + imgs = append(imgs, h) + } + + json := jsoniter.ConfigCompatibleWithStandardLibrary + enc := json.NewEncoder(os.Stdout) + return enc.Encode(imgs) +} + +func writeTemplate(imageS []*entities.ImageSummary, err error) error { + var ( + hdr, row string + ) + imgs := make([]imageReporter, 0, len(imageS)) + for _, e := range imageS { + for _, tag := range e.RepoTags { + var h imageReporter + h.ImageSummary = *e + h.Repository, h.Tag = tokenRepoTag(tag) + imgs = append(imgs, h) + } + if e.IsReadOnly() { + listFlag.readOnly = true + } + } + if len(listFlag.format) < 1 { + hdr, row = imageListFormat(listFlag) + } else { + row = listFlag.format + if !strings.HasSuffix(row, "\n") { + row += "\n" + } + } + format := hdr + "{{range . }}" + row + "{{end}}" + tmpl := template.Must(template.New("list").Parse(format)) + w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) + defer w.Flush() + return tmpl.Execute(w, imgs) +} + +func tokenRepoTag(tag string) (string, string) { + tokens := strings.SplitN(tag, ":", 2) + switch len(tokens) { + case 0: + return tag, "" + case 1: + return tokens[0], "" + case 2: + return tokens[0], tokens[1] + default: + return "<N/A>", "" + } +} + +func sortFunc(key string, data []*entities.ImageSummary) func(i, j int) bool { + switch key { + case "id": + return func(i, j int) bool { + return data[i].ID < data[j].ID + } + case "repository": + return func(i, j int) bool { + return data[i].RepoTags[0] < data[j].RepoTags[0] + } + case "size": + return func(i, j int) bool { + return data[i].Size < data[j].Size + } + case "tag": + return func(i, j int) bool { + return data[i].RepoTags[0] < data[j].RepoTags[0] + } + default: + // case "created": + return func(i, j int) bool { + return data[i].Created >= data[j].Created + } + } +} + +func imageListFormat(flags listFlagType) (string, string) { + if flags.quiet { + return "", "{{.ID}}\n" + } + + // Defaults + hdr := "REPOSITORY\tTAG" + row := "{{.Repository}}\t{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}" + + if flags.digests { + hdr += "\tDIGEST" + row += "\t{{.Digest}}" + } + + hdr += "\tIMAGE ID" + if flags.noTrunc { + row += "\tsha256:{{.ID}}" + } else { + row += "\t{{.ID}}" + } + + hdr += "\tCREATED\tSIZE" + row += "\t{{.Created}}\t{{.Size}}" + + if flags.history { + hdr += "\tHISTORY" + row += "\t{{if .History}}{{.History}}{{else}}<none>{{end}}" + } + + if flags.readOnly { + hdr += "\tReadOnly" + row += "\t{{.ReadOnly}}" + } + + if flags.noHeading { + hdr = "" + } else { + hdr += "\n" + } + + row += "\n" + return hdr, row +} + +type imageReporter struct { + Repository string `json:"repository,omitempty"` + Tag string `json:"tag,omitempty"` + entities.ImageSummary +} + +func (i imageReporter) ID() string { + if !listFlag.noTrunc && len(i.ImageSummary.ID) >= 12 { + return i.ImageSummary.ID[0:12] + } + return i.ImageSummary.ID +} + +func (i imageReporter) Created() string { + return units.HumanDuration(time.Since(time.Unix(i.ImageSummary.Created, 0))) + " ago" +} + +func (i imageReporter) Size() string { + s := units.HumanSizeWithPrecision(float64(i.ImageSummary.Size), 3) + j := strings.LastIndexFunc(s, unicode.IsNumber) + return s[:j+1] + " " + s[j+1:] +} + +func (i imageReporter) History() string { + return strings.Join(i.ImageSummary.History, ", ") +} diff --git a/cmd/podman/images/load.go b/cmd/podman/images/load.go new file mode 100644 index 000000000..23c657b59 --- /dev/null +++ b/cmd/podman/images/load.go @@ -0,0 +1,94 @@ +package images + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "os" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/libpod/cmd/podman/parse" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/util" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" +) + +var ( + loadDescription = "Loads an image from a locally stored archive (tar file) into container storage." + loadCommand = &cobra.Command{ + Use: "load [flags] [NAME[:TAG]]", + Short: "Load an image from container archive", + Long: loadDescription, + RunE: load, + Args: cobra.MaximumNArgs(1), + } +) + +var ( + loadOpts entities.ImageLoadOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: loadCommand, + }) + + flags := loadCommand.Flags() + flags.StringVarP(&loadOpts.Input, "input", "i", "", "Read from specified archive file (default: stdin)") + flags.BoolVarP(&loadOpts.Quiet, "quiet", "q", false, "Suppress the output") + flags.StringVar(&loadOpts.SignaturePolicy, "signature-policy", "", "Pathname of signature policy file") + if registry.IsRemote() { + _ = flags.MarkHidden("signature-policy") + } + +} + +func load(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + ref, err := reference.Parse(args[0]) + if err != nil { + return err + } + if t, ok := ref.(reference.Tagged); ok { + loadOpts.Tag = t.Tag() + } else { + loadOpts.Tag = "latest" + } + if r, ok := ref.(reference.Named); ok { + fmt.Println(r.Name()) + loadOpts.Name = r.Name() + } + } + if len(loadOpts.Input) > 0 { + if err := parse.ValidateFileName(loadOpts.Input); err != nil { + return err + } + } else { + if terminal.IsTerminal(int(os.Stdin.Fd())) { + return errors.Errorf("cannot read from terminal. Use command-line redirection or the --input flag.") + } + outFile, err := ioutil.TempFile(util.Tmpdir(), "podman") + if err != nil { + return errors.Errorf("error creating file %v", err) + } + defer os.Remove(outFile.Name()) + defer outFile.Close() + + _, err = io.Copy(outFile, os.Stdin) + if err != nil { + return errors.Errorf("error copying file %v", err) + } + loadOpts.Input = outFile.Name() + } + response, err := registry.ImageEngine().Load(context.Background(), loadOpts) + if err != nil { + return err + } + fmt.Println("Loaded image: " + response.Name) + return nil +} diff --git a/cmd/podman/images/prune.go b/cmd/podman/images/prune.go new file mode 100644 index 000000000..b90d889be --- /dev/null +++ b/cmd/podman/images/prune.go @@ -0,0 +1,86 @@ +package images + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + pruneDescription = `Removes all unnamed images from local storage. + + If an image is not being used by a container, it will be removed from the system.` + pruneCmd = &cobra.Command{ + Use: "prune", + Args: cobra.NoArgs, + Short: "Remove unused images", + Long: pruneDescription, + RunE: prune, + Example: `podman image prune`, + } + + pruneOpts = entities.ImagePruneOptions{} + force bool + filter = []string{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: pruneCmd, + Parent: imageCmd, + }) + + flags := pruneCmd.Flags() + flags.BoolVarP(&pruneOpts.All, "all", "a", false, "Remove all unused images, not just dangling ones") + flags.BoolVarP(&force, "force", "f", false, "Do not prompt for confirmation") + flags.StringArrayVar(&filter, "filter", []string{}, "Provide filter values (e.g. 'label=<key>=<value>')") + +} + +func prune(cmd *cobra.Command, args []string) error { + if !force { + reader := bufio.NewReader(os.Stdin) + fmt.Printf(` +WARNING! This will remove all dangling images. +Are you sure you want to continue? [y/N] `) + answer, err := reader.ReadString('\n') + if err != nil { + return errors.Wrapf(err, "error reading input") + } + if strings.ToLower(answer)[0] != 'y' { + return nil + } + } + + // TODO Remove once filter refactor is finished and url.Values rules :) + for _, f := range filter { + t := strings.SplitN(f, "=", 2) + pruneOpts.Filters.Add(t[0], t[1]) + } + + results, err := registry.ImageEngine().Prune(registry.GetContext(), pruneOpts) + if err != nil { + return err + } + + for _, i := range results.Report.Id { + fmt.Println(i) + } + + for _, e := range results.Report.Err { + fmt.Fprint(os.Stderr, e.Error()+"\n") + } + + if results.Size > 0 { + fmt.Fprintf(os.Stdout, "Size: %d\n", results.Size) + } + + return nil +} diff --git a/cmd/podman/images/pull.go b/cmd/podman/images/pull.go new file mode 100644 index 000000000..fb107d00c --- /dev/null +++ b/cmd/podman/images/pull.go @@ -0,0 +1,118 @@ +package images + +import ( + "fmt" + + buildahcli "github.com/containers/buildah/pkg/cli" + "github.com/containers/image/v5/types" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// pullOptionsWrapper wraps entities.ImagePullOptions and prevents leaking +// CLI-only fields into the API types. +type pullOptionsWrapper struct { + entities.ImagePullOptions + TLSVerifyCLI bool // CLI only +} + +var ( + pullOptions = pullOptionsWrapper{} + pullDescription = `Pulls an image from a registry and stores it locally. + + An image can be pulled by tag or digest. If a tag is not specified, the image with the 'latest' tag is pulled.` + + // Command: podman pull + pullCmd = &cobra.Command{ + Use: "pull [flags] IMAGE", + Args: cobra.ExactArgs(1), + Short: "Pull an image from a registry", + Long: pullDescription, + RunE: imagePull, + Example: `podman pull imageName + podman pull fedora:latest`, + } + + // Command: podman image pull + // It's basically a clone of `pullCmd` with the exception of being a + // child of the images command. + imagesPullCmd = &cobra.Command{ + Use: pullCmd.Use, + Short: pullCmd.Short, + Long: pullCmd.Long, + RunE: pullCmd.RunE, + Example: `podman image pull imageName + podman image pull fedora:latest`, + } +) + +func init() { + // pull + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: pullCmd, + }) + + flags := pullCmd.Flags() + pullFlags(flags) + + // images pull + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: imagesPullCmd, + Parent: imageCmd, + }) + + imagesPullFlags := imagesPullCmd.Flags() + pullFlags(imagesPullFlags) +} + +// pullFlags set the flags for the pull command. +func pullFlags(flags *pflag.FlagSet) { + flags.BoolVar(&pullOptions.AllTags, "all-tags", false, "All tagged images in the repository will be pulled") + flags.StringVar(&pullOptions.Authfile, "authfile", buildahcli.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") + flags.StringVar(&pullOptions.CertDir, "cert-dir", "", "`Pathname` of a directory containing TLS certificates and keys") + flags.StringVar(&pullOptions.Credentials, "creds", "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry") + flags.StringVar(&pullOptions.OverrideArch, "override-arch", "", "Use `ARCH` instead of the architecture of the machine for choosing images") + flags.StringVar(&pullOptions.OverrideOS, "override-os", "", "Use `OS` instead of the running OS for choosing images") + flags.BoolVarP(&pullOptions.Quiet, "quiet", "q", false, "Suppress output information when pulling images") + flags.StringVar(&pullOptions.SignaturePolicy, "signature-policy", "", "`Pathname` of signature policy file (not usually used)") + flags.BoolVar(&pullOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries") + + if registry.IsRemote() { + _ = flags.MarkHidden("authfile") + _ = flags.MarkHidden("cert-dir") + _ = flags.MarkHidden("signature-policy") + _ = flags.MarkHidden("tls-verify") + } +} + +// imagePull is implement the command for pulling images. +func imagePull(cmd *cobra.Command, args []string) error { + pullOptsAPI := pullOptions.ImagePullOptions + // TLS verification in c/image is controlled via a `types.OptionalBool` + // which allows for distinguishing among set-true, set-false, unspecified + // which is important to implement a sane way of dealing with defaults of + // boolean CLI flags. + if cmd.Flags().Changed("tls-verify") { + pullOptsAPI.TLSVerify = types.NewOptionalBool(pullOptions.TLSVerifyCLI) + } + + // Let's do all the remaining Yoga in the API to prevent us from + // scattering logic across (too) many parts of the code. + pullReport, err := registry.ImageEngine().Pull(registry.GetContext(), args[0], pullOptsAPI) + if err != nil { + return err + } + + if len(pullReport.Images) > 1 { + fmt.Println("Pulled Images:") + } + for _, img := range pullReport.Images { + fmt.Println(img) + } + + return nil +} diff --git a/cmd/podman/images/push.go b/cmd/podman/images/push.go new file mode 100644 index 000000000..f12a5ac86 --- /dev/null +++ b/cmd/podman/images/push.go @@ -0,0 +1,120 @@ +package images + +import ( + buildahcli "github.com/containers/buildah/pkg/cli" + "github.com/containers/image/v5/types" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// pushOptionsWrapper wraps entities.ImagepushOptions and prevents leaking +// CLI-only fields into the API types. +type pushOptionsWrapper struct { + entities.ImagePushOptions + TLSVerifyCLI bool // CLI only +} + +var ( + pushOptions = pushOptionsWrapper{} + pushDescription = `Pushes a source image to a specified destination. + + The Image "DESTINATION" uses a "transport":"details" format. See podman-push(1) section "DESTINATION" for the expected format.` + + // Command: podman push + pushCmd = &cobra.Command{ + Use: "push [flags] SOURCE DESTINATION", + Short: "Push an image to a specified destination", + Long: pushDescription, + RunE: imagePush, + Example: `podman push imageID docker://registry.example.com/repository:tag + podman push imageID oci-archive:/path/to/layout:image:tag`, + } + + // Command: podman image push + // It's basically a clone of `pushCmd` with the exception of being a + // child of the images command. + imagePushCmd = &cobra.Command{ + Use: pushCmd.Use, + Short: pushCmd.Short, + Long: pushCmd.Long, + RunE: pushCmd.RunE, + Example: `podman image push imageID docker://registry.example.com/repository:tag + podman image push imageID oci-archive:/path/to/layout:image:tag`, + } +) + +func init() { + // push + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: pushCmd, + }) + + flags := pushCmd.Flags() + pushFlags(flags) + + // images push + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: imagePushCmd, + Parent: imageCmd, + }) + + pushFlags(imagePushCmd.Flags()) +} + +// pushFlags set the flags for the push command. +func pushFlags(flags *pflag.FlagSet) { + flags.StringVar(&pushOptions.Authfile, "authfile", buildahcli.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") + flags.StringVar(&pushOptions.CertDir, "cert-dir", "", "Path to a directory containing TLS certificates and keys") + flags.BoolVar(&pushOptions.Compress, "compress", false, "Compress tarball image layers when pushing to a directory using the 'dir' transport. (default is same compression type as source)") + flags.StringVar(&pushOptions.Credentials, "creds", "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry") + flags.StringVar(&pushOptions.DigestFile, "digestfile", "", "Write the digest of the pushed image to the specified file") + flags.StringVarP(&pushOptions.Format, "format", "f", "", "Manifest type (oci, v2s1, or v2s2) to use when pushing an image using the 'dir' transport (default is manifest type of source)") + flags.BoolVarP(&pushOptions.Quiet, "quiet", "q", false, "Suppress output information when pushing images") + flags.BoolVar(&pushOptions.RemoveSignatures, "remove-signatures", false, "Discard any pre-existing signatures in the image") + flags.StringVar(&pushOptions.SignaturePolicy, "signature-policy", "", "Path to a signature-policy file") + flags.StringVar(&pushOptions.SignBy, "sign-by", "", "Add a signature at the destination using the specified key") + flags.BoolVar(&pushOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries") + + if registry.IsRemote() { + _ = flags.MarkHidden("authfile") + _ = flags.MarkHidden("cert-dir") + _ = flags.MarkHidden("compress") + _ = flags.MarkHidden("quiet") + _ = flags.MarkHidden("signature-policy") + _ = flags.MarkHidden("tls-verify") + } +} + +// imagePush is implement the command for pushing images. +func imagePush(cmd *cobra.Command, args []string) error { + var source, destination string + switch len(args) { + case 1: + source = args[0] + case 2: + source = args[0] + destination = args[1] + case 0: + fallthrough + default: + return errors.New("push requires at least one image name, or optionally a second to specify a different destination") + } + + pushOptsAPI := pushOptions.ImagePushOptions + // TLS verification in c/image is controlled via a `types.OptionalBool` + // which allows for distinguishing among set-true, set-false, unspecified + // which is important to implement a sane way of dealing with defaults of + // boolean CLI flags. + if cmd.Flags().Changed("tls-verify") { + pushOptsAPI.TLSVerify = types.NewOptionalBool(pushOptions.TLSVerifyCLI) + } + + // Let's do all the remaining Yoga in the API to prevent us from scattering + // logic across (too) many parts of the code. + return registry.ImageEngine().Push(registry.GetContext(), source, destination, pushOptsAPI) +} diff --git a/cmd/podman/images/rm.go b/cmd/podman/images/rm.go new file mode 100644 index 000000000..135fda387 --- /dev/null +++ b/cmd/podman/images/rm.go @@ -0,0 +1,71 @@ +package images + +import ( + "fmt" + "os" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var ( + rmDescription = "Removes one or more previously pulled or locally created images." + rmCmd = &cobra.Command{ + Use: "rm [flags] IMAGE [IMAGE...]", + Short: "Removes one or more images from local storage", + Long: rmDescription, + RunE: rm, + Example: `podman image rm imageID + podman image rm --force alpine + podman image rm c4dfb1609ee2 93fd78260bd1 c0ed59d05ff7`, + } + + imageOpts = entities.ImageDeleteOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: rmCmd, + Parent: imageCmd, + }) + + imageRemoveFlagSet(rmCmd.Flags()) +} + +func imageRemoveFlagSet(flags *pflag.FlagSet) { + flags.BoolVarP(&imageOpts.All, "all", "a", false, "Remove all images") + flags.BoolVarP(&imageOpts.Force, "force", "f", false, "Force Removal of the image") +} +func rm(cmd *cobra.Command, args []string) error { + + if len(args) < 1 && !imageOpts.All { + return errors.Errorf("image name or ID must be specified") + } + if len(args) > 0 && imageOpts.All { + return errors.Errorf("when using the --all switch, you may not pass any images names or IDs") + } + report, err := registry.ImageEngine().Delete(registry.GetContext(), args, imageOpts) + if err != nil { + switch { + case report != nil && report.ImageNotFound != nil: + fmt.Fprintln(os.Stderr, err.Error()) + registry.SetExitCode(2) + case report != nil && report.ImageInUse != nil: + fmt.Fprintln(os.Stderr, err.Error()) + default: + return err + } + } + + for _, u := range report.Untagged { + fmt.Println("Untagged: " + u) + } + for _, d := range report.Deleted { + fmt.Println("Deleted: " + d) + } + return nil +} diff --git a/cmd/podman/images/rmi.go b/cmd/podman/images/rmi.go new file mode 100644 index 000000000..8e1759ef4 --- /dev/null +++ b/cmd/podman/images/rmi.go @@ -0,0 +1,28 @@ +package images + +import ( + "strings" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + rmiCmd = &cobra.Command{ + Use: strings.Replace(rmCmd.Use, "rm ", "rmi ", 1), + Args: rmCmd.Args, + Short: rmCmd.Short, + Long: rmCmd.Long, + RunE: rmCmd.RunE, + Example: strings.Replace(rmCmd.Example, "podman image rm", "podman rmi", -1), + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: rmiCmd, + }) + imageRemoveFlagSet(rmiCmd.Flags()) +} diff --git a/cmd/podman/images/save.go b/cmd/podman/images/save.go new file mode 100644 index 000000000..8f7832074 --- /dev/null +++ b/cmd/podman/images/save.go @@ -0,0 +1,86 @@ +package images + +import ( + "context" + "os" + "strings" + + "github.com/containers/libpod/libpod/define" + + "github.com/containers/libpod/cmd/podman/parse" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/util" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" +) + +var validFormats = []string{define.OCIManifestDir, define.OCIArchive, define.V2s2ManifestDir, define.V2s2Archive} + +var ( + saveDescription = `Save an image to docker-archive or oci-archive on the local machine. Default is docker-archive.` + + saveCommand = &cobra.Command{ + Use: "save [flags] IMAGE", + Short: "Save image to an archive", + Long: saveDescription, + RunE: save, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.Errorf("need at least 1 argument") + } + format, err := cmd.Flags().GetString("format") + if err != nil { + return err + } + if !util.StringInSlice(format, validFormats) { + return errors.Errorf("format value must be one of %s", strings.Join(validFormats, " ")) + } + return nil + }, + Example: `podman save --quiet -o myimage.tar imageID + podman save --format docker-dir -o ubuntu-dir ubuntu + podman save > alpine-all.tar alpine:latest`, + } +) + +var ( + saveOpts entities.ImageSaveOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: saveCommand, + }) + flags := saveCommand.Flags() + flags.BoolVar(&saveOpts.Compress, "compress", false, "Compress tarball image layers when saving to a directory using the 'dir' transport. (default is same compression type as source)") + flags.StringVar(&saveOpts.Format, "format", define.V2s2Archive, "Save image to oci-archive, oci-dir (directory with oci manifest type), docker-archive, docker-dir (directory with v2s2 manifest type)") + flags.StringVarP(&saveOpts.Output, "output", "o", "", "Write to a specified file (default: stdout, which must be redirected)") + flags.BoolVarP(&saveOpts.Quiet, "quiet", "q", false, "Suppress the output") + +} + +func save(cmd *cobra.Command, args []string) error { + var ( + tags []string + ) + if cmd.Flag("compress").Changed && (saveOpts.Format != define.OCIManifestDir && saveOpts.Format != define.V2s2ManifestDir && saveOpts.Format == "") { + return errors.Errorf("--compress can only be set when --format is either 'oci-dir' or 'docker-dir'") + } + if len(saveOpts.Output) == 0 { + fi := os.Stdout + if terminal.IsTerminal(int(fi.Fd())) { + return errors.Errorf("refusing to save to terminal. Use -o flag or redirect") + } + saveOpts.Output = "/dev/stdout" + } + if err := parse.ValidateFileName(saveOpts.Output); err != nil { + return err + } + if len(args) > 1 { + tags = args[1:] + } + return registry.ImageEngine().Save(context.Background(), args[0], tags, saveOpts) +} diff --git a/cmd/podman/images/search.go b/cmd/podman/images/search.go new file mode 100644 index 000000000..fdad94d45 --- /dev/null +++ b/cmd/podman/images/search.go @@ -0,0 +1,156 @@ +package images + +import ( + "reflect" + "strings" + + buildahcli "github.com/containers/buildah/pkg/cli" + "github.com/containers/buildah/pkg/formats" + "github.com/containers/image/v5/types" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/util/camelcase" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// searchOptionsWrapper wraps entities.ImagePullOptions and prevents leaking +// CLI-only fields into the API types. +type searchOptionsWrapper struct { + entities.ImageSearchOptions + // CLI only flags + TLSVerifyCLI bool // Used to convert to an optional bool later + Format string // For go templating +} + +var ( + searchOptions = searchOptionsWrapper{} + searchDescription = `Search registries for a given image. Can search all the default registries or a specific registry. + + Users can limit the number of results, and filter the output based on certain conditions.` + + // Command: podman search + searchCmd = &cobra.Command{ + Use: "search [flags] TERM", + Short: "Search registry for image", + Long: searchDescription, + RunE: imageSearch, + Args: cobra.ExactArgs(1), + Annotations: map[string]string{ + registry.ParentNSRequired: "", + }, + Example: `podman search --filter=is-official --limit 3 alpine + podman search registry.fedoraproject.org/ # only works with v2 registries + podman search --format "table {{.Index}} {{.Name}}" registry.fedoraproject.org/fedora`, + } + + // Command: podman image search + imageSearchCmd = &cobra.Command{ + Use: searchCmd.Use, + Short: searchCmd.Short, + Long: searchCmd.Long, + RunE: searchCmd.RunE, + Args: searchCmd.Args, + Example: `podman image search --filter=is-official --limit 3 alpine + podman image search registry.fedoraproject.org/ # only works with v2 registries + podman image search --format "table {{.Index}} {{.Name}}" registry.fedoraproject.org/fedora`, + } +) + +func init() { + // search + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: searchCmd, + }) + + flags := searchCmd.Flags() + searchFlags(flags) + + // images search + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: imageSearchCmd, + Parent: imageCmd, + }) + + imageSearchFlags := imageSearchCmd.Flags() + searchFlags(imageSearchFlags) +} + +// searchFlags set the flags for the pull command. +func searchFlags(flags *pflag.FlagSet) { + flags.StringSliceVarP(&searchOptions.Filters, "filter", "f", []string{}, "Filter output based on conditions provided (default [])") + flags.StringVar(&searchOptions.Format, "format", "", "Change the output format to a Go template") + flags.IntVar(&searchOptions.Limit, "limit", 0, "Limit the number of results") + flags.BoolVar(&searchOptions.NoTrunc, "no-trunc", false, "Do not truncate the output") + flags.StringVar(&searchOptions.Authfile, "authfile", buildahcli.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") + flags.BoolVar(&searchOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries") + if registry.IsRemote() { + _ = flags.MarkHidden("authfile") + _ = flags.MarkHidden("tls-verify") + } +} + +// imageSearch implements the command for searching images. +func imageSearch(cmd *cobra.Command, args []string) error { + searchTerm := "" + switch len(args) { + case 1: + searchTerm = args[0] + default: + return errors.Errorf("search requires exactly one argument") + } + + sarchOptsAPI := searchOptions.ImageSearchOptions + // TLS verification in c/image is controlled via a `types.OptionalBool` + // which allows for distinguishing among set-true, set-false, unspecified + // which is important to implement a sane way of dealing with defaults of + // boolean CLI flags. + if cmd.Flags().Changed("tls-verify") { + sarchOptsAPI.TLSVerify = types.NewOptionalBool(pullOptions.TLSVerifyCLI) + } + + searchReport, err := registry.ImageEngine().Search(registry.GetContext(), searchTerm, sarchOptsAPI) + if err != nil { + return err + } + + format := genSearchFormat(searchOptions.Format) + if len(searchReport) == 0 { + return nil + } + out := formats.StdoutTemplateArray{Output: searchToGeneric(searchReport), Template: format, Fields: searchHeaderMap()} + return out.Out() +} + +// searchHeaderMap returns the headers of a SearchResult. +func searchHeaderMap() map[string]string { + s := new(entities.ImageSearchReport) + v := reflect.Indirect(reflect.ValueOf(s)) + values := make(map[string]string, v.NumField()) + + for i := 0; i < v.NumField(); i++ { + key := v.Type().Field(i).Name + value := key + values[key] = strings.ToUpper(strings.Join(camelcase.Split(value), " ")) + } + return values +} + +func genSearchFormat(format string) string { + if format != "" { + // "\t" from the command line is not being recognized as a tab + // replacing the string "\t" to a tab character if the user passes in "\t" + return strings.Replace(format, `\t`, "\t", -1) + } + return "table {{.Index}}\t{{.Name}}\t{{.Description}}\t{{.Stars}}\t{{.Official}}\t{{.Automated}}\t" +} + +func searchToGeneric(params []entities.ImageSearchReport) (genericParams []interface{}) { + for _, v := range params { + genericParams = append(genericParams, interface{}(v)) + } + return genericParams +} diff --git a/cmd/podman/images/tag.go b/cmd/podman/images/tag.go new file mode 100644 index 000000000..411313a9b --- /dev/null +++ b/cmd/podman/images/tag.go @@ -0,0 +1,32 @@ +package images + +import ( + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + tagDescription = "Adds one or more additional names to locally-stored image." + tagCommand = &cobra.Command{ + Use: "tag [flags] IMAGE TARGET_NAME [TARGET_NAME...]", + Short: "Add an additional name to a local image", + Long: tagDescription, + RunE: tag, + Args: cobra.MinimumNArgs(2), + Example: `podman tag 0e3bbc2 fedora:latest + podman tag imageID:latest myNewImage:newTag + podman tag httpd myregistryhost:5000/fedora/httpd:v2`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: tagCommand, + }) +} + +func tag(cmd *cobra.Command, args []string) error { + return registry.ImageEngine().Tag(registry.GetContext(), args[0], args[1:], entities.ImageTagOptions{}) +} diff --git a/cmd/podman/images/untag.go b/cmd/podman/images/untag.go new file mode 100644 index 000000000..3218844b7 --- /dev/null +++ b/cmd/podman/images/untag.go @@ -0,0 +1,31 @@ +package images + +import ( + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + untagCommand = &cobra.Command{ + Use: "untag [flags] IMAGE [NAME...]", + Short: "Remove a name from a local image", + Long: "Removes one or more names from a locally-stored image.", + RunE: untag, + Args: cobra.MinimumNArgs(1), + Example: `podman untag 0e3bbc2 + podman untag imageID:latest otherImageName:latest + podman untag httpd myregistryhost:5000/fedora/httpd:v2`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: untagCommand, + }) +} + +func untag(cmd *cobra.Command, args []string) error { + return registry.ImageEngine().Untag(registry.GetContext(), args[0], args[1:], entities.ImageUntagOptions{}) +} |