V2 podman image prune

* Fixed header for `podman image ls`
* Implemented prune `all` flag, preserved filter method for backwards
  capability
* Updated binding tests

Signed-off-by: Jhon Honce <jhonce@redhat.com>
This commit is contained in:
Jhon Honce
2020-03-26 17:16:59 -07:00
parent 1710eca4e9
commit 581dd312af
10 changed files with 144 additions and 33 deletions

View File

@ -15,7 +15,7 @@ var (
Args: listCmd.Args, Args: listCmd.Args,
Short: listCmd.Short, Short: listCmd.Short,
Long: listCmd.Long, Long: listCmd.Long,
PreRunE: listCmd.PreRunE, PreRunE: preRunE,
RunE: listCmd.RunE, RunE: listCmd.RunE,
Example: strings.Replace(listCmd.Example, "podman image list", "podman images", -1), Example: strings.Replace(listCmd.Example, "podman image list", "podman images", -1),
} }

View File

@ -212,7 +212,7 @@ func imageListFormat(flags listFlagType) (string, string) {
row += "\t{{.Digest}}" row += "\t{{.Digest}}"
} }
hdr += "\tID" hdr += "\tIMAGE ID"
if flags.noTrunc { if flags.noTrunc {
row += "\tsha256:{{.ID}}" row += "\tsha256:{{.ID}}"
} else { } else {

View File

@ -0,0 +1,86 @@
package images
import (
"bufio"
"fmt"
"os"
"strings"
"github.com/containers/libpod/cmd/podmanV2/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
}

View File

@ -64,6 +64,7 @@ func PruneImages(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value("runtime").(*libpod.Runtime) runtime := r.Context().Value("runtime").(*libpod.Runtime)
query := struct { query := struct {
All bool
Filters map[string][]string `schema:"filters"` Filters map[string][]string `schema:"filters"`
}{ }{
// This is where you can override the golang default value for one of fields // This is where you can override the golang default value for one of fields
@ -80,7 +81,7 @@ func PruneImages(w http.ResponseWriter, r *http.Request) {
filters = append(filters, fmt.Sprintf("%s=%s", k, val)) filters = append(filters, fmt.Sprintf("%s=%s", k, val))
} }
} }
pruneCids, err := runtime.ImageRuntime().PruneImages(r.Context(), false, filters) pruneCids, err := runtime.ImageRuntime().PruneImages(r.Context(), query.All, filters)
if err != nil { if err != nil {
utils.InternalServerError(w, err) utils.InternalServerError(w, err)
return return

View File

@ -119,12 +119,12 @@ func GetImages(w http.ResponseWriter, r *http.Request) {
func PruneImages(w http.ResponseWriter, r *http.Request) { func PruneImages(w http.ResponseWriter, r *http.Request) {
var ( var (
all bool
err error err error
) )
runtime := r.Context().Value("runtime").(*libpod.Runtime) runtime := r.Context().Value("runtime").(*libpod.Runtime)
decoder := r.Context().Value("decoder").(*schema.Decoder) decoder := r.Context().Value("decoder").(*schema.Decoder)
query := struct { query := struct {
All bool `schema:"all"`
Filters map[string][]string `schema:"filters"` Filters map[string][]string `schema:"filters"`
}{ }{
// override any golang type defaults // override any golang type defaults
@ -140,7 +140,7 @@ func PruneImages(w http.ResponseWriter, r *http.Request) {
if _, found := r.URL.Query()["filters"]; found { if _, found := r.URL.Query()["filters"]; found {
dangling := query.Filters["all"] dangling := query.Filters["all"]
if len(dangling) > 0 { if len(dangling) > 0 {
all, err = strconv.ParseBool(query.Filters["all"][0]) query.All, err = strconv.ParseBool(query.Filters["all"][0])
if err != nil { if err != nil {
utils.InternalServerError(w, err) utils.InternalServerError(w, err)
return return
@ -152,7 +152,8 @@ func PruneImages(w http.ResponseWriter, r *http.Request) {
libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0])) libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0]))
} }
} }
cids, err := runtime.ImageRuntime().PruneImages(r.Context(), all, libpodFilters)
cids, err := runtime.ImageRuntime().PruneImages(r.Context(), query.All, libpodFilters)
if err != nil { if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err)
return return

View File

@ -154,7 +154,7 @@ func Export(ctx context.Context, nameOrID string, w io.Writer, format *string, c
// Prune removes unused images from local storage. The optional filters can be used to further // Prune removes unused images from local storage. The optional filters can be used to further
// define which images should be pruned. // define which images should be pruned.
func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { func Prune(ctx context.Context, all *bool, filters map[string][]string) ([]string, error) {
var ( var (
deleted []string deleted []string
) )
@ -163,6 +163,9 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) {
return nil, err return nil, err
} }
params := url.Values{} params := url.Values{}
if all != nil {
params.Set("all", strconv.FormatBool(*all))
}
if filters != nil { if filters != nil {
stringFilter, err := bindings.FiltersToString(filters) stringFilter, err := bindings.FiltersToString(filters)
if err != nil { if err != nil {
@ -174,7 +177,7 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) {
if err != nil { if err != nil {
return deleted, err return deleted, err
} }
return deleted, response.Process(nil) return deleted, response.Process(&deleted)
} }
// Tag adds an additional name to locally-stored image. Both the tag and repo parameters are required. // Tag adds an additional name to locally-stored image. Both the tag and repo parameters are required.

View File

@ -16,22 +16,22 @@ import (
var _ = Describe("Podman images", func() { var _ = Describe("Podman images", func() {
var ( var (
//tempdir string // tempdir string
//err error // err error
//podmanTest *PodmanTestIntegration // podmanTest *PodmanTestIntegration
bt *bindingTest bt *bindingTest
s *gexec.Session s *gexec.Session
err error err error
) )
BeforeEach(func() { BeforeEach(func() {
//tempdir, err = CreateTempDirInTempDir() // tempdir, err = CreateTempDirInTempDir()
//if err != nil { // if err != nil {
// os.Exit(1) // os.Exit(1)
//} // }
//podmanTest = PodmanTestCreate(tempdir) // podmanTest = PodmanTestCreate(tempdir)
//podmanTest.Setup() // podmanTest.Setup()
//podmanTest.SeedImages() // podmanTest.SeedImages()
bt = newBindingTest() bt = newBindingTest()
bt.RestoreImagesFromCache() bt.RestoreImagesFromCache()
s = bt.startAPIService() s = bt.startAPIService()
@ -41,12 +41,13 @@ var _ = Describe("Podman images", func() {
}) })
AfterEach(func() { AfterEach(func() {
//podmanTest.Cleanup() // podmanTest.Cleanup()
//f := CurrentGinkgoTestDescription() // f := CurrentGinkgoTestDescription()
//processTestResult(f) // processTestResult(f)
s.Kill() s.Kill()
bt.cleanup() bt.cleanup()
}) })
It("inspect image", func() { It("inspect image", func() {
// Inspect invalid image be 404 // Inspect invalid image be 404
_, err = images.GetImage(bt.conn, "foobar5000", nil) _, err = images.GetImage(bt.conn, "foobar5000", nil)
@ -71,7 +72,7 @@ var _ = Describe("Podman images", func() {
Expect(err).To(BeNil()) Expect(err).To(BeNil())
// TODO it looks like the images API alwaays returns size regardless // TODO it looks like the images API alwaays returns size regardless
// of bool or not. What should we do ? // of bool or not. What should we do ?
//Expect(data.Size).To(BeZero()) // Expect(data.Size).To(BeZero())
// Enabling the size parameter should result in size being populated // Enabling the size parameter should result in size being populated
data, err = images.GetImage(bt.conn, alpine.name, &bindings.PTrue) data, err = images.GetImage(bt.conn, alpine.name, &bindings.PTrue)
@ -142,7 +143,7 @@ var _ = Describe("Podman images", func() {
err = images.Tag(bt.conn, alpine.shortName, "demo", alpine.shortName) err = images.Tag(bt.conn, alpine.shortName, "demo", alpine.shortName)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
//Validates if name updates when the image is retagged. // Validates if name updates when the image is retagged.
_, err := images.GetImage(bt.conn, "alpine:demo", nil) _, err := images.GetImage(bt.conn, "alpine:demo", nil)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
@ -165,7 +166,7 @@ var _ = Describe("Podman images", func() {
Expect(err).To(BeNil()) Expect(err).To(BeNil())
Expect(len(imageSummary)).To(Equal(3)) Expect(len(imageSummary)).To(Equal(3))
//Validate the image names. // Validate the image names.
var names []string var names []string
for _, i := range imageSummary { for _, i := range imageSummary {
names = append(names, i.RepoTags...) names = append(names, i.RepoTags...)
@ -289,6 +290,7 @@ var _ = Describe("Podman images", func() {
Expect(data.Comment).To(Equal(testMessage)) Expect(data.Comment).To(Equal(testMessage))
}) })
It("History Image", func() { It("History Image", func() {
// a bogus name should return a 404 // a bogus name should return a 404
_, err := images.History(bt.conn, "foobar") _, err := images.History(bt.conn, "foobar")
@ -343,4 +345,12 @@ var _ = Describe("Podman images", func() {
Expect(len(imgs)).To(BeNumerically(">=", 1)) Expect(len(imgs)).To(BeNumerically(">=", 1))
}) })
It("Prune images", func() {
trueBoxed := true
results, err := images.Prune(bt.conn, &trueBoxed, nil)
Expect(err).NotTo(HaveOccurred())
Expect(len(results)).To(BeNumerically(">", 0))
Expect(results).To(ContainElement("docker.io/library/alpine:latest"))
})
}) })

View File

@ -119,7 +119,7 @@ type ImageInspectOptions struct {
type ImageListOptions struct { type ImageListOptions struct {
All bool `json:"all" schema:"all"` All bool `json:"all" schema:"all"`
Filter []string `json:",omitempty"` Filter []string `json:"Filter,omitempty"`
Filters url.Values `json:"filters" schema:"filters"` Filters url.Values `json:"filters" schema:"filters"`
} }
@ -128,8 +128,9 @@ type ImageListOptions struct {
// } // }
type ImagePruneOptions struct { type ImagePruneOptions struct {
All bool All bool `json:"all" schema:"all"`
Filter ImageFilter Filter []string `json:"filter" schema:"filter"`
Filters url.Values `json:"filters" schema:"filters"`
} }
type ImagePruneReport struct { type ImagePruneReport struct {

View File

@ -88,13 +88,18 @@ func (ir *ImageEngine) deleteImage(ctx context.Context, img *libpodImage.Image,
} }
func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOptions) (*entities.ImagePruneReport, error) { func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOptions) (*entities.ImagePruneReport, error) {
results, err := ir.Libpod.ImageRuntime().PruneImages(ctx, opts.All, []string{}) results, err := ir.Libpod.ImageRuntime().PruneImages(ctx, opts.All, opts.Filter)
if err != nil { if err != nil {
return nil, err return nil, err
} }
report := entities.ImagePruneReport{} report := entities.ImagePruneReport{
copy(report.Report.Id, results) Report: entities.Report{
Id: results,
Err: nil,
},
Size: 0,
}
return &report, nil return &report, nil
} }

View File

@ -2,7 +2,6 @@ package tunnel
import ( import (
"context" "context"
"net/url"
images "github.com/containers/libpod/pkg/bindings/images" images "github.com/containers/libpod/pkg/bindings/images"
"github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/domain/entities"
@ -72,12 +71,17 @@ func (ir *ImageEngine) History(ctx context.Context, nameOrId string, opts entiti
} }
func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOptions) (*entities.ImagePruneReport, error) { func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOptions) (*entities.ImagePruneReport, error) {
results, err := images.Prune(ir.ClientCxt, url.Values{}) results, err := images.Prune(ir.ClientCxt, &opts.All, opts.Filters)
if err != nil { if err != nil {
return nil, err return nil, err
} }
report := entities.ImagePruneReport{} report := entities.ImagePruneReport{
copy(report.Report.Id, results) Report: entities.Report{
Id: results,
Err: nil,
},
Size: 0,
}
return &report, nil return &report, nil
} }