aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/podman/images.go267
-rw-r--r--libpod/runtime_img.go178
-rw-r--r--pkg/inspect/inspect.go15
-rw-r--r--test/e2e/images_test.go57
4 files changed, 357 insertions, 160 deletions
diff --git a/cmd/podman/images.go b/cmd/podman/images.go
index 3b41204f1..2dcd743cf 100644
--- a/cmd/podman/images.go
+++ b/cmd/podman/images.go
@@ -1,18 +1,15 @@
package main
import (
- "fmt"
"reflect"
"strings"
"time"
- "github.com/containers/storage"
"github.com/docker/go-units"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/projectatomic/libpod/cmd/podman/formats"
"github.com/projectatomic/libpod/libpod"
- "github.com/projectatomic/libpod/libpod/common"
"github.com/projectatomic/libpod/pkg/inspect"
"github.com/urfave/cli"
)
@@ -31,15 +28,16 @@ type imagesJSONParams struct {
Name []string `json:"names"`
Digest digest.Digest `json:"digest"`
Created time.Time `json:"created"`
- Size int64 `json:"size"`
+ Size *uint64 `json:"size"`
}
type imagesOptions struct {
- quiet bool
- noHeading bool
- noTrunc bool
- digests bool
- format string
+ quiet bool
+ noHeading bool
+ noTrunc bool
+ digests bool
+ format string
+ outputformat string
}
var (
@@ -64,7 +62,7 @@ var (
Name: "format",
Usage: "Change the output format to JSON or a Go template",
},
- cli.StringFlag{
+ cli.StringSliceFlag{
Name: "filter, f",
Usage: "filter output based on conditions provided (default [])",
},
@@ -92,38 +90,32 @@ func imagesCmd(c *cli.Context) error {
return errors.Wrapf(err, "Could not get runtime")
}
defer runtime.Shutdown(false)
-
- format := genImagesFormat(c.String("format"), c.Bool("quiet"), c.Bool("noheading"), c.Bool("digests"))
-
- opts := imagesOptions{
- quiet: c.Bool("quiet"),
- noHeading: c.Bool("noheading"),
- noTrunc: c.Bool("no-trunc"),
- digests: c.Bool("digests"),
- format: format,
- }
-
+ var filterFuncs []libpod.ImageResultFilter
var imageInput string
if len(c.Args()) == 1 {
imageInput = c.Args().Get(0)
}
+
if len(c.Args()) > 1 {
return errors.New("'podman images' requires at most 1 argument")
}
- params, err := runtime.ParseImageFilter(imageInput, c.String("filter"))
- if err != nil {
- return errors.Wrapf(err, "error parsing filter")
+ if len(c.StringSlice("filter")) > 0 || len(strings.TrimSpace(imageInput)) != 0 {
+ filterFuncs, err = CreateFilterFuncs(runtime, c, imageInput)
+ if err != nil {
+ return err
+ }
}
- // generate the different filters
- labelFilter := generateImagesFilter(params, "label")
- beforeImageFilter := generateImagesFilter(params, "before-image")
- sinceImageFilter := generateImagesFilter(params, "since-image")
- danglingFilter := generateImagesFilter(params, "dangling")
- referenceFilter := generateImagesFilter(params, "reference")
- imageInputFilter := generateImagesFilter(params, "image-input")
+ opts := imagesOptions{
+ quiet: c.Bool("quiet"),
+ noHeading: c.Bool("noheading"),
+ noTrunc: c.Bool("no-trunc"),
+ digests: c.Bool("digests"),
+ format: c.String("format"),
+ }
+ opts.outputformat = opts.setOutputFormat()
/*
podman does not implement --all for images
@@ -131,29 +123,36 @@ func imagesCmd(c *cli.Context) error {
children to the image once built. until buildah supports caching builds,
it will not generate these intermediate images.
*/
-
- images, err := runtime.GetImages(params, labelFilter, beforeImageFilter, sinceImageFilter, danglingFilter, referenceFilter, imageInputFilter)
+ images, err := runtime.GetImageResults()
if err != nil {
- return errors.Wrapf(err, "could not get list of images matching filter")
+ return errors.Wrapf(err, "unable to get images")
+ }
+
+ var filteredImages []inspect.ImageResult
+ // filter the images
+ if len(c.StringSlice("filter")) > 0 || len(strings.TrimSpace(imageInput)) != 0 {
+ filteredImages = libpod.FilterImages(images, filterFuncs)
+ } else {
+ filteredImages = images
}
- return generateImagesOutput(runtime, images, opts)
+ return generateImagesOutput(runtime, filteredImages, opts)
}
-func genImagesFormat(format string, quiet, noHeading, digests bool) string {
- if format != "" {
+func (i imagesOptions) setOutputFormat() string {
+ if i.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 strings.Replace(i.format, `\t`, "\t", -1)
}
- if quiet {
+ if i.quiet {
return formats.IDString
}
- format = "table {{.Repository}}\t{{.Tag}}\t"
- if noHeading {
+ format := "table {{.Repository}}\t{{.Tag}}\t"
+ if i.noHeading {
format = "{{.Repository}}\t{{.Tag}}\t"
}
- if digests {
+ if i.digests {
format += "{{.Digest}}\t"
}
format += "{{.ID}}\t{{.Created}}\t{{.Size}}\t"
@@ -174,60 +173,22 @@ func imagesToGeneric(templParams []imagesTemplateParams, JSONParams []imagesJSON
return
}
-// generate the header based on the template provided
-func (i *imagesTemplateParams) headerMap() map[string]string {
- v := reflect.Indirect(reflect.ValueOf(i))
- values := make(map[string]string)
-
- for i := 0; i < v.NumField(); i++ {
- key := v.Type().Field(i).Name
- value := key
- if value == "ID" {
- value = "Image" + value
- }
- values[key] = strings.ToUpper(splitCamelCase(value))
- }
- return values
-}
-
// getImagesTemplateOutput returns the images information to be printed in human readable format
-func getImagesTemplateOutput(runtime *libpod.Runtime, images []*storage.Image, opts imagesOptions) (imagesOutput []imagesTemplateParams) {
- var (
- lastID string
- )
+func getImagesTemplateOutput(runtime *libpod.Runtime, images []inspect.ImageResult, opts imagesOptions) (imagesOutput []imagesTemplateParams) {
for _, img := range images {
- if opts.quiet && lastID == img.ID {
- continue // quiet should not show the same ID multiple times
- }
createdTime := img.Created
imageID := "sha256:" + img.ID
if !opts.noTrunc {
imageID = shortID(img.ID)
}
-
- repository := "<none>"
- tag := "<none>"
- if len(img.Names) > 0 {
- arr := strings.Split(img.Names[0], ":")
- repository = arr[0]
- if len(arr) == 2 {
- tag = arr[1]
- }
- }
-
- imgData, _ := runtime.GetImageInspectInfo(*img)
- if imgData != nil {
- createdTime = *imgData.Created
- }
-
params := imagesTemplateParams{
- Repository: repository,
- Tag: tag,
+ Repository: img.Repository,
+ Tag: img.Tag,
ID: imageID,
- Digest: imgData.Digest,
+ Digest: img.Digest,
Created: units.HumanDuration(time.Since((createdTime))) + " ago",
- Size: units.HumanSizeWithPrecision(float64(imgData.Size), 3),
+ Size: units.HumanSizeWithPrecision(float64(*img.Size), 3),
}
imagesOutput = append(imagesOutput, params)
}
@@ -235,21 +196,14 @@ func getImagesTemplateOutput(runtime *libpod.Runtime, images []*storage.Image, o
}
// getImagesJSONOutput returns the images information in its raw form
-func getImagesJSONOutput(runtime *libpod.Runtime, images []*storage.Image) (imagesOutput []imagesJSONParams) {
+func getImagesJSONOutput(runtime *libpod.Runtime, images []inspect.ImageResult) (imagesOutput []imagesJSONParams) {
for _, img := range images {
- createdTime := img.Created
-
- imgData, _ := runtime.GetImageInspectInfo(*img)
- if imgData != nil {
- createdTime = *imgData.Created
- }
-
params := imagesJSONParams{
ID: img.ID,
- Name: img.Names,
- Digest: imgData.Digest,
- Created: createdTime,
- Size: imgData.Size,
+ Name: img.RepoTags,
+ Digest: img.Digest,
+ Created: img.Created,
+ Size: img.Size,
}
imagesOutput = append(imagesOutput, params)
}
@@ -257,11 +211,11 @@ func getImagesJSONOutput(runtime *libpod.Runtime, images []*storage.Image) (imag
}
// generateImagesOutput generates the images based on the format provided
-func generateImagesOutput(runtime *libpod.Runtime, images []*storage.Image, opts imagesOptions) error {
+
+func generateImagesOutput(runtime *libpod.Runtime, images []inspect.ImageResult, opts imagesOptions) error {
if len(images) == 0 {
return nil
}
-
var out formats.Writer
switch opts.format {
@@ -270,77 +224,70 @@ func generateImagesOutput(runtime *libpod.Runtime, images []*storage.Image, opts
out = formats.JSONStructArray{Output: imagesToGeneric([]imagesTemplateParams{}, imagesOutput)}
default:
imagesOutput := getImagesTemplateOutput(runtime, images, opts)
- out = formats.StdoutTemplateArray{Output: imagesToGeneric(imagesOutput, []imagesJSONParams{}), Template: opts.format, Fields: imagesOutput[0].headerMap()}
-
+ out = formats.StdoutTemplateArray{Output: imagesToGeneric(imagesOutput, []imagesJSONParams{}), Template: opts.outputformat, Fields: imagesOutput[0].HeaderMap()}
}
-
return formats.Writer(out).Out()
}
-// generateImagesFilter returns an ImageFilter based on filterType
-// to add more filters, define a new case and write what the ImageFilter function should do
-func generateImagesFilter(params *libpod.ImageFilterParams, filterType string) libpod.ImageFilter {
- switch filterType {
- case "label":
- return func(image *storage.Image, info *inspect.ImageData) bool {
- if params == nil || params.Label == "" {
- return true
- }
+// HeaderMap produces a generic map of "headers" based on a line
+// of output
+func (i *imagesTemplateParams) HeaderMap() map[string]string {
+ v := reflect.Indirect(reflect.ValueOf(i))
+ values := make(map[string]string)
- pair := strings.SplitN(params.Label, "=", 2)
- if val, ok := info.Labels[pair[0]]; ok {
- if len(pair) == 2 && val == pair[1] {
- return true
- }
- if len(pair) == 1 {
- return true
- }
- }
- return false
- }
- case "before-image":
- return func(image *storage.Image, info *inspect.ImageData) bool {
- if params == nil || params.BeforeImage.IsZero() {
- return true
- }
- return info.Created.Before(params.BeforeImage)
- }
- case "since-image":
- return func(image *storage.Image, info *inspect.ImageData) bool {
- if params == nil || params.SinceImage.IsZero() {
- return true
- }
- return info.Created.After(params.SinceImage)
+ for i := 0; i < v.NumField(); i++ {
+ key := v.Type().Field(i).Name
+ value := key
+ if value == "ID" {
+ value = "Image" + value
}
- case "dangling":
- return func(image *storage.Image, info *inspect.ImageData) bool {
- if params == nil || params.Dangling == "" {
- return true
- }
- if common.IsFalse(params.Dangling) && params.ImageName != "<none>" {
- return true
+ values[key] = strings.ToUpper(splitCamelCase(value))
+ }
+ return values
+}
+
+// CreateFilterFuncs returns an array of filter functions based on the user inputs
+// and is later used to filter images for output
+func CreateFilterFuncs(r *libpod.Runtime, c *cli.Context, userInput string) ([]libpod.ImageResultFilter, error) {
+ var filterFuncs []libpod.ImageResultFilter
+ for _, filter := range c.StringSlice("filter") {
+ splitFilter := strings.Split(filter, "=")
+ switch splitFilter[0] {
+ case "before":
+ before := r.NewImage(splitFilter[1])
+ _, beforeID, _ := before.GetLocalImageName()
+
+ if before.LocalName == "" {
+ return nil, errors.Errorf("unable to find image % in local stores", splitFilter[1])
}
- if common.IsTrue(params.Dangling) && params.ImageName == "<none>" {
- return true
+ img, err := r.GetImage(beforeID)
+ if err != nil {
+ return nil, err
}
- return false
- }
- case "reference":
- return func(image *storage.Image, info *inspect.ImageData) bool {
- if params == nil || params.ReferencePattern == "" {
- return true
+ filterFuncs = append(filterFuncs, libpod.ImageCreatedBefore(img.Created))
+ case "after":
+ after := r.NewImage(splitFilter[1])
+ _, afterID, _ := after.GetLocalImageName()
+
+ if after.LocalName == "" {
+ return nil, errors.Errorf("unable to find image % in local stores", splitFilter[1])
}
- return libpod.MatchesReference(params.ImageName, params.ReferencePattern)
- }
- case "image-input":
- return func(image *storage.Image, info *inspect.ImageData) bool {
- if params == nil || params.ImageInput == "" {
- return true
+ img, err := r.GetImage(afterID)
+ if err != nil {
+ return nil, err
}
- return libpod.MatchesReference(params.ImageName, params.ImageInput)
+ filterFuncs = append(filterFuncs, libpod.ImageCreatedAfter(img.Created))
+ case "dangling":
+ filterFuncs = append(filterFuncs, libpod.ImageDangling())
+ case "label":
+ labelFilter := strings.Join(splitFilter[1:], "=")
+ filterFuncs = append(filterFuncs, libpod.ImageLabel(labelFilter))
+ default:
+ return nil, errors.Errorf("invalid filter %s ", splitFilter[0])
}
- default:
- fmt.Println("invalid filter type", filterType)
- return nil
}
+ if len(strings.TrimSpace(userInput)) != 0 {
+ filterFuncs = append(filterFuncs, libpod.OutputImageFilter(userInput))
+ }
+ return filterFuncs, nil
}
diff --git a/libpod/runtime_img.go b/libpod/runtime_img.go
index 76687351d..cc6275494 100644
--- a/libpod/runtime_img.go
+++ b/libpod/runtime_img.go
@@ -108,6 +108,9 @@ type imageDecomposeStruct struct {
transport string
}
+// ImageResultFilter is a mock function for image filtering
+type ImageResultFilter func(inspect.ImageResult) bool
+
func (k *Image) assembleFqName() string {
return fmt.Sprintf("%s/%s:%s", k.Registry, k.ImageName, k.Tag)
}
@@ -1262,3 +1265,178 @@ func getPolicyContext(ctx *types.SystemContext) (*signature.PolicyContext, error
}
return policyContext, nil
}
+
+// sizer knows its size.
+type sizer interface {
+ Size() (int64, error)
+}
+
+func imageSize(img types.ImageSource) *uint64 {
+ if s, ok := img.(sizer); ok {
+ if sum, err := s.Size(); err == nil {
+ usum := uint64(sum)
+ return &usum
+ }
+ }
+ return nil
+}
+
+func reposToMap(repotags []string) map[string]string {
+ // map format is repo -> tag
+ repos := make(map[string]string)
+ for _, repo := range repotags {
+ var repository, tag string
+ if len(repo) > 0 {
+ li := strings.LastIndex(repo, ":")
+ repository = repo[0:li]
+ tag = repo[li+1:]
+ }
+ repos[repository] = tag
+ }
+ if len(repos) == 0 {
+ repos["<none>"] = "<none"
+ }
+ return repos
+}
+
+// GetImageResults gets the images for podman images and returns them as
+// an array of ImageResults
+func (r *Runtime) GetImageResults() ([]inspect.ImageResult, error) {
+ var results []inspect.ImageResult
+
+ images, err := r.store.Images()
+ if err != nil {
+ return nil, err
+ }
+ for _, image := range images {
+ storeRef, err := is.Transport.ParseStoreReference(r.store, image.ID)
+ if err != nil {
+ return nil, err
+ }
+ systemContext := &types.SystemContext{}
+ img, err := storeRef.NewImageSource(systemContext)
+ if err != nil {
+ return nil, err
+ }
+ ic, err := storeRef.NewImage(&types.SystemContext{})
+ if err != nil {
+ return nil, err
+ }
+ imgInspect, err := ic.Inspect()
+ if err != nil {
+ return nil, err
+ }
+ dangling := false
+ if len(image.Names) == 0 {
+ dangling = true
+ }
+
+ for repo, tag := range reposToMap(image.Names) {
+ results = append(results, inspect.ImageResult{
+ ID: image.ID,
+ Repository: repo,
+ RepoTags: image.Names,
+ Tag: tag,
+ Size: imageSize(img),
+ Digest: image.Digest,
+ Created: image.Created,
+ Labels: imgInspect.Labels,
+ Dangling: dangling,
+ })
+ }
+
+ }
+ return results, nil
+}
+
+// ImageCreatedBefore allows you to filter on images created before
+// the given time.Time
+func ImageCreatedBefore(createTime time.Time) ImageResultFilter {
+ return func(i inspect.ImageResult) bool {
+ if i.Created.Before(createTime) {
+ return true
+ }
+ return false
+ }
+}
+
+// ImageCreatedAfter allows you to filter on images created after
+// the given time.Time
+func ImageCreatedAfter(createTime time.Time) ImageResultFilter {
+ return func(i inspect.ImageResult) bool {
+ if i.Created.After(createTime) {
+ return true
+ }
+ return false
+ }
+}
+
+// ImageDangling allows you to filter images for dangling images
+func ImageDangling() ImageResultFilter {
+ return func(i inspect.ImageResult) bool {
+ if i.Dangling {
+ return true
+ }
+ return false
+ }
+}
+
+// ImageLabel allows you to filter by images labels key and/or value
+func ImageLabel(labelfilter string) ImageResultFilter {
+ // We need to handle both label=key and label=key=value
+ return func(i inspect.ImageResult) bool {
+ var value string
+ splitFilter := strings.Split(labelfilter, "=")
+ key := splitFilter[0]
+ if len(splitFilter) > 1 {
+ value = splitFilter[1]
+ }
+ for labelKey, labelValue := range i.Labels {
+ // handles label=key
+ if key == labelKey && len(strings.TrimSpace(value)) == 0 {
+ return true
+ }
+ //handles label=key=value
+ if key == labelKey && value == labelValue {
+ return true
+ }
+ }
+ return false
+ }
+}
+
+// OutputImageFilter allows you to filter by an a specific image name
+func OutputImageFilter(name string) ImageResultFilter {
+ return func(i inspect.ImageResult) bool {
+ li := strings.LastIndex(name, ":")
+ var repository, tag string
+ if li < 0 {
+ repository = name
+ } else {
+ repository = name[0:li]
+ tag = name[li+1:]
+ }
+ if repository == i.Repository && len(strings.TrimSpace(tag)) == 0 {
+ return true
+ }
+ if repository == i.Repository && tag == i.Tag {
+ return true
+ }
+ return false
+ }
+}
+
+// FilterImages filters images using a set of predefined fitler funcs
+func FilterImages(images []inspect.ImageResult, filters []ImageResultFilter) []inspect.ImageResult {
+ var filteredImages []inspect.ImageResult
+ for _, image := range images {
+ include := true
+ for _, filter := range filters {
+ include = include && filter(image)
+ }
+ if include {
+ filteredImages = append(filteredImages, image)
+ }
+ }
+ return filteredImages
+}
diff --git a/pkg/inspect/inspect.go b/pkg/inspect/inspect.go
index e523d4c4d..806692425 100644
--- a/pkg/inspect/inspect.go
+++ b/pkg/inspect/inspect.go
@@ -202,3 +202,18 @@ type NetworkSettings struct {
IPv6Gateway string `json:"IPv6Gateway"`
MacAddress string `json:"MacAddress"`
}
+
+// ImageResult is used for podman images for collection and output
+type ImageResult struct {
+ Tag string
+ Repository string
+ RepoDigests []string
+ RepoTags []string
+ ID string
+ Digest digest.Digest
+ ConfigDigest digest.Digest
+ Created time.Time
+ Size *uint64
+ Labels map[string]string
+ Dangling bool
+}
diff --git a/test/e2e/images_test.go b/test/e2e/images_test.go
index 4c7c93e4b..ecc0f2415 100644
--- a/test/e2e/images_test.go
+++ b/test/e2e/images_test.go
@@ -37,6 +37,15 @@ var _ = Describe("Podman images", func() {
Expect(session.LineInOuputStartsWith("docker.io/library/busybox")).To(BeTrue())
})
+ It("podman images with digests", func() {
+ session := podmanTest.Podman([]string{"images", "--digests"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(len(session.OutputToStringArray())).To(BeNumerically(">", 2))
+ Expect(session.LineInOuputStartsWith("docker.io/library/alpine")).To(BeTrue())
+ Expect(session.LineInOuputStartsWith("docker.io/library/busybox")).To(BeTrue())
+ })
+
It("podman images in JSON format", func() {
session := podmanTest.Podman([]string{"images", "--format=json"})
session.WaitWithDefaultTimeout()
@@ -44,10 +53,58 @@ var _ = Describe("Podman images", func() {
Expect(session.IsJSONOutputValid()).To(BeTrue())
})
+ It("podman images in GO template format", func() {
+ session := podmanTest.Podman([]string{"images", "--format={{.ID}}"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ })
+
It("podman images with short options", func() {
session := podmanTest.Podman([]string{"images", "-qn"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
Expect(len(session.OutputToStringArray())).To(BeNumerically(">", 1))
})
+
+ It("podman images filter by image name", func() {
+ session := podmanTest.Podman([]string{"images", "-q", ALPINE})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ Expect(len(session.OutputToStringArray())).To(Equal(1))
+ })
+
+ It("podman images filter before image", func() {
+ dockerfile := `FROM docker.io/library/alpine:latest
+`
+ podmanTest.BuildImage(dockerfile, "foobar.com/before:latest")
+ result := podmanTest.Podman([]string{"images", "-q", "-f", "before=foobar.com/before:latest"})
+ result.WaitWithDefaultTimeout()
+ Expect(result.ExitCode()).To(Equal(0))
+ Expect(len(result.OutputToStringArray())).To(Equal(2))
+ })
+
+ It("podman images filter after image", func() {
+ rmi := podmanTest.Podman([]string{"rmi", "busybox"})
+ rmi.WaitWithDefaultTimeout()
+ Expect(rmi.ExitCode()).To(Equal(0))
+
+ dockerfile := `FROM docker.io/library/alpine:latest
+`
+ podmanTest.BuildImage(dockerfile, "foobar.com/before:latest")
+ result := podmanTest.Podman([]string{"images", "-q", "-f", "after=docker.io/library/alpine:latest"})
+ result.WaitWithDefaultTimeout()
+ Expect(result.ExitCode()).To(Equal(0))
+ Expect(len(result.OutputToStringArray())).To(Equal(1))
+ })
+
+ It("podman images filter dangling", func() {
+ dockerfile := `FROM docker.io/library/alpine:latest
+`
+ podmanTest.BuildImage(dockerfile, "foobar.com/before:latest")
+ podmanTest.BuildImage(dockerfile, "foobar.com/before:latest")
+ result := podmanTest.Podman([]string{"images", "-q", "-f", "dangling=true"})
+ result.WaitWithDefaultTimeout()
+ Expect(result.ExitCode()).To(Equal(0))
+ Expect(len(result.OutputToStringArray())).To(Equal(1))
+ })
})