From 0f44ff1d3bc3dab84dd51b34233cbf51c5f6848d Mon Sep 17 00:00:00 2001 From: Ryan Cole Date: Sun, 23 Jul 2017 21:38:03 -0400 Subject: [PATCH] move functions supporting images command to libkpod/image Signed-off-by: Ryan Cole --- cmd/kpod/common.go | 19 --- cmd/kpod/images.go | 242 +++++---------------------------------- cmd/kpod/push.go | 2 +- cmd/kpod/rmi.go | 5 +- libkpod/common/common.go | 19 +++ libkpod/image/image.go | 229 ++++++++++++++++++++++++++++++++++++ 6 files changed, 281 insertions(+), 235 deletions(-) diff --git a/cmd/kpod/common.go b/cmd/kpod/common.go index 0e82322d..894c78fd 100644 --- a/cmd/kpod/common.go +++ b/cmd/kpod/common.go @@ -1,12 +1,8 @@ package main import ( - "strings" - is "github.com/containers/image/storage" - "github.com/containers/image/types" "github.com/containers/storage" - "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -35,18 +31,3 @@ func getStore(c *cli.Context) (storage.Store, error) { is.Transport.SetStore(store) return store, nil } - -func parseRegistryCreds(creds string) (*types.DockerAuthConfig, error) { - if creds == "" { - return nil, errors.New("no credentials supplied") - } - if strings.Index(creds, ":") < 0 { - return nil, errors.New("user name supplied, but no password supplied") - } - v := strings.SplitN(creds, ":", 2) - cfg := &types.DockerAuthConfig{ - Username: v[0], - Password: v[1], - } - return cfg, nil -} diff --git a/cmd/kpod/images.go b/cmd/kpod/images.go index 231182a0..22943043 100644 --- a/cmd/kpod/images.go +++ b/cmd/kpod/images.go @@ -3,13 +3,11 @@ package main import ( "fmt" "os" - "strings" "text/template" - is "github.com/containers/image/storage" "github.com/containers/storage" - "github.com/kubernetes-incubator/cri-o/libkpod/common" "github.com/kubernetes-incubator/cri-o/libkpod/image" + libkpodimage "github.com/kubernetes-incubator/cri-o/libkpod/image" "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -22,15 +20,6 @@ type imageOutputParams struct { Size string } -type filterParams struct { - dangling string - label string - beforeImage string // Images are sorted by date, so we can just output until we see the image - sinceImage string // Images are sorted by date, so we can just output until we don't see the image - seenImage bool // Hence this boolean - referencePattern string -} - var ( imagesFlags = []cli.Flag{ cli.BoolFlag{ @@ -106,14 +95,9 @@ func imagesCmd(c *cli.Context) error { return errors.New("'buildah images' requires at most 1 argument") } - images, err := store.Images() - if err != nil { - return errors.Wrapf(err, "error reading images") - } - - var params *filterParams + var params *libkpodimage.FilterParams if c.IsSet("filter") { - params, err = parseFilter(images, c.String("filter")) + params, err = libkpodimage.ParseFilter(store, c.String("filter")) if err != nil { return errors.Wrapf(err, "error parsing filter") } @@ -121,60 +105,15 @@ func imagesCmd(c *cli.Context) error { params = nil } - if len(images) > 0 && !noheading && !quiet && !hasTemplate { + imageList, err := libkpodimage.GetImagesMatchingFilter(store, params, name) + if err != nil { + return errors.Wrapf(err, "could not get list of images matching filter") + } + if len(imageList) > 0 && !noheading && !quiet && !hasTemplate { outputHeader(truncate, digests) } - return outputImages(images, formatString, store, params, name, hasTemplate, truncate, digests, quiet) -} - -func parseFilter(images []storage.Image, filter string) (*filterParams, error) { - params := new(filterParams) - filterStrings := strings.Split(filter, ",") - for _, param := range filterStrings { - pair := strings.SplitN(param, "=", 2) - switch strings.TrimSpace(pair[0]) { - case "dangling": - if common.IsValidBool(pair[1]) { - params.dangling = pair[1] - } else { - return nil, fmt.Errorf("invalid filter: '%s=[%s]'", pair[0], pair[1]) - } - case "label": - params.label = pair[1] - case "before": - if imageExists(images, pair[1]) { - params.beforeImage = pair[1] - } else { - return nil, fmt.Errorf("no such id: %s", pair[0]) - } - case "since": - if imageExists(images, pair[1]) { - params.sinceImage = pair[1] - } else { - return nil, fmt.Errorf("no such id: %s``", pair[0]) - } - case "reference": - params.referencePattern = pair[1] - default: - return nil, fmt.Errorf("invalid filter: '%s'", pair[0]) - } - } - return params, nil -} - -func imageExists(images []storage.Image, ref string) bool { - for _, image := range images { - if matchesID(image.ID, ref) { - return true - } - for _, name := range image.Names { - if matchesReference(name, ref) { - return true - } - } - } - return false + return outputImages(store, imageList, formatString, hasTemplate, truncate, digests, quiet) } func outputHeader(truncate, digests bool) { @@ -191,7 +130,7 @@ func outputHeader(truncate, digests bool) { fmt.Printf("%-22s %s\n", "CREATED AT", "SIZE") } -func outputImages(images []storage.Image, format string, store storage.Store, filters *filterParams, argName string, hasTemplate, truncate, digests, quiet bool) error { +func outputImages(store storage.Store, images []storage.Image, format string, hasTemplate, truncate, digests, quiet bool) error { for _, img := range images { imageMetadata, err := image.ParseMetadata(img) if err != nil { @@ -202,156 +141,33 @@ func outputImages(images []storage.Image, format string, store storage.Store, fi if len(imageMetadata.Blobs) > 0 { digest = string(imageMetadata.Blobs[0].Digest) } - size, _ := image.Size(store, img) + size, _ := libkpodimage.Size(store, img) - names := []string{""} - if len(img.Names) > 0 { - names = img.Names - } else { - // images without names should be printed with "" as the image name - names = append(names, "") + if quiet { + fmt.Printf("%-64s\n", img.ID) + // We only want to print each id once + break } - for _, name := range names { - if !matchesFilter(img, store, name, filters) || !matchesReference(name, argName) { - continue - } - if quiet { - fmt.Printf("%-64s\n", img.ID) - // We only want to print each id once - break - } - params := imageOutputParams{ - ID: img.ID, - Name: name, - Digest: digest, - CreatedAt: createdTime, - Size: formattedSize(size), - } - if hasTemplate { - err = outputUsingTemplate(format, params) - if err != nil { - return err - } - continue - } - - outputUsingFormatString(truncate, digests, params) + params := imageOutputParams{ + ID: img.ID, + Name: img.Names[0], + Digest: digest, + CreatedAt: createdTime, + Size: libkpodimage.FormattedSize(size), } + if hasTemplate { + err = outputUsingTemplate(format, params) + if err != nil { + return err + } + continue + } + outputUsingFormatString(truncate, digests, params) } return nil } -func matchesFilter(image storage.Image, store storage.Store, name string, params *filterParams) bool { - if params == nil { - return true - } - if params.dangling != "" && !matchesDangling(name, params.dangling) { - return false - } else if params.label != "" && !matchesLabel(image, store, params.label) { - return false - } else if params.beforeImage != "" && !matchesBeforeImage(image, name, params) { - return false - } else if params.sinceImage != "" && !matchesSinceImage(image, name, params) { - return false - } else if params.referencePattern != "" && !matchesReference(name, params.referencePattern) { - return false - } - return true -} - -func matchesDangling(name string, dangling string) bool { - if common.IsFalse(dangling) && name != "" { - return true - } else if common.IsTrue(dangling) && name == "" { - return true - } - return false -} - -func matchesLabel(image storage.Image, store storage.Store, label string) bool { - storeRef, err := is.Transport.ParseStoreReference(store, "@"+image.ID) - if err != nil { - - } - img, err := storeRef.NewImage(nil) - if err != nil { - return false - } - info, err := img.Inspect() - if err != nil { - return false - } - - pair := strings.SplitN(label, "=", 2) - for key, value := range info.Labels { - if key == pair[0] { - if len(pair) == 2 { - if value == pair[1] { - return true - } - } else { - return false - } - } - } - return false -} - -// Returns true if the image was created since the filter image. Returns -// false otherwise -func matchesBeforeImage(image storage.Image, name string, params *filterParams) bool { - if params.seenImage { - return false - } - if matchesReference(name, params.beforeImage) || matchesID(image.ID, params.beforeImage) { - params.seenImage = true - return false - } - return true -} - -// Returns true if the image was created since the filter image. Returns -// false otherwise -func matchesSinceImage(image storage.Image, name string, params *filterParams) bool { - if params.seenImage { - return true - } - if matchesReference(name, params.sinceImage) || matchesID(image.ID, params.sinceImage) { - params.seenImage = true - } - return false -} - -func matchesID(id, argID string) bool { - return strings.HasPrefix(argID, id) -} - -func matchesReference(name, argName string) bool { - if argName == "" { - return true - } - splitName := strings.Split(name, ":") - // If the arg contains a tag, we handle it differently than if it does not - if strings.Contains(argName, ":") { - splitArg := strings.Split(argName, ":") - return strings.HasSuffix(splitName[0], splitArg[0]) && (splitName[1] == splitArg[1]) - } - return strings.HasSuffix(splitName[0], argName) -} - -func formattedSize(size int64) string { - suffixes := [5]string{"B", "KB", "MB", "GB", "TB"} - - count := 0 - formattedSize := float64(size) - for formattedSize >= 1024 && count < 4 { - formattedSize /= 1024 - count++ - } - return fmt.Sprintf("%.4g %s", formattedSize, suffixes[count]) -} - func outputUsingTemplate(format string, params imageOutputParams) error { tmpl, err := template.New("image").Parse(format) if err != nil { diff --git a/cmd/kpod/push.go b/cmd/kpod/push.go index 938c2cc4..32b7cd43 100644 --- a/cmd/kpod/push.go +++ b/cmd/kpod/push.go @@ -86,7 +86,7 @@ func pushCmd(c *cli.Context) error { signBy := c.String("sign-by") if registryCredsString != "" { - creds, err := parseRegistryCreds(registryCredsString) + creds, err := common.ParseRegistryCreds(registryCredsString) if err != nil { return err } diff --git a/cmd/kpod/rmi.go b/cmd/kpod/rmi.go index e1128da8..9bab10bb 100644 --- a/cmd/kpod/rmi.go +++ b/cmd/kpod/rmi.go @@ -8,6 +8,7 @@ import ( "github.com/containers/image/transports/alltransports" "github.com/containers/image/types" "github.com/containers/storage" + libkpodimage "github.com/kubernetes-incubator/cri-o/libkpod/image" "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -67,7 +68,7 @@ func rmiCmd(c *cli.Context) error { } } // If the user supplied an ID, we cannot delete the image if it is referred to by multiple tags - if matchesID(image.ID, id) { + if libkpodimage.MatchesID(image.ID, id) { if len(image.Names) > 1 && !force { return fmt.Errorf("unable to delete %s (must force) - image is referred to in multiple tags", image.ID) } @@ -130,7 +131,7 @@ func untagImage(imgArg string, image *storage.Image, store storage.Store) (strin newNames := []string{} removedName := "" for _, name := range image.Names { - if matchesReference(name, imgArg) { + if libkpodimage.MatchesReference(name, imgArg) { removedName = name continue } diff --git a/libkpod/common/common.go b/libkpod/common/common.go index 6da87ae4..bf847f4f 100644 --- a/libkpod/common/common.go +++ b/libkpod/common/common.go @@ -1,7 +1,9 @@ package common import ( + "errors" "io" + "strings" cp "github.com/containers/image/copy" "github.com/containers/image/signature" @@ -68,3 +70,20 @@ func GetPolicyContext(path string) (*signature.PolicyContext, error) { } return signature.NewPolicyContext(policy) } + +// ParseRegistryCreds takes a credentials string in the form USERNAME:PASSWORD +// and returns a DockerAuthConfig +func ParseRegistryCreds(creds string) (*types.DockerAuthConfig, error) { + if creds == "" { + return nil, errors.New("no credentials supplied") + } + if strings.Index(creds, ":") < 0 { + return nil, errors.New("user name supplied, but no password supplied") + } + v := strings.SplitN(creds, ":", 2) + cfg := &types.DockerAuthConfig{ + Username: v[0], + Password: v[1], + } + return cfg, nil +} diff --git a/libkpod/image/image.go b/libkpod/image/image.go index c82fd4c7..0db7bae3 100644 --- a/libkpod/image/image.go +++ b/libkpod/image/image.go @@ -1,11 +1,189 @@ package image import ( + "fmt" + "strings" + "time" + is "github.com/containers/image/storage" "github.com/containers/storage" + "github.com/kubernetes-incubator/cri-o/libkpod/common" "github.com/pkg/errors" ) +// FilterParams contains the filter options that may be given when outputting images +type FilterParams struct { + dangling string + label string + beforeImage time.Time + sinceImage time.Time + referencePattern string +} + +// ParseFilter takes a set of images and a filter string as input, and returns the +func ParseFilter(store storage.Store, filter string) (*FilterParams, error) { + images, err := store.Images() + if err != nil { + return nil, err + } + params := new(FilterParams) + filterStrings := strings.Split(filter, ",") + for _, param := range filterStrings { + pair := strings.SplitN(param, "=", 2) + switch strings.TrimSpace(pair[0]) { + case "dangling": + if common.IsValidBool(pair[1]) { + params.dangling = pair[1] + } else { + return nil, fmt.Errorf("invalid filter: '%s=[%s]'", pair[0], pair[1]) + } + case "label": + params.label = pair[1] + case "before": + if img, err := findImageInSlice(images, pair[1]); err == nil { + params.beforeImage, _ = getCreatedTime(img) + } else { + return nil, fmt.Errorf("no such id: %s", pair[0]) + } + case "since": + if img, err := findImageInSlice(images, pair[1]); err == nil { + params.sinceImage, _ = getCreatedTime(img) + } else { + return nil, fmt.Errorf("no such id: %s``", pair[0]) + } + case "reference": + params.referencePattern = pair[1] + default: + return nil, fmt.Errorf("invalid filter: '%s'", pair[0]) + } + } + return params, nil +} + +func matchesFilter(store storage.Store, image storage.Image, name string, params *FilterParams) bool { + if params == nil { + return true + } + if params.dangling != "" && !matchesDangling(name, params.dangling) { + return false + } else if params.label != "" && !matchesLabel(image, store, params.label) { + return false + } else if !params.beforeImage.IsZero() && !matchesBeforeImage(image, name, params) { + return false + } else if !params.sinceImage.IsZero() && !matchesSinceImage(image, name, params) { + return false + } else if params.referencePattern != "" && !MatchesReference(name, params.referencePattern) { + return false + } + return true +} + +func matchesDangling(name string, dangling string) bool { + if common.IsFalse(dangling) && name != "" { + return true + } else if common.IsTrue(dangling) && name == "" { + return true + } + return false +} + +func matchesLabel(image storage.Image, store storage.Store, label string) bool { + storeRef, err := is.Transport.ParseStoreReference(store, "@"+image.ID) + if err != nil { + + } + img, err := storeRef.NewImage(nil) + if err != nil { + return false + } + info, err := img.Inspect() + if err != nil { + return false + } + + pair := strings.SplitN(label, "=", 2) + for key, value := range info.Labels { + if key == pair[0] { + if len(pair) == 2 { + if value == pair[1] { + return true + } + } else { + return false + } + } + } + return false +} + +// Returns true if the image was created since the filter image. Returns +// false otherwise +func matchesBeforeImage(image storage.Image, name string, params *FilterParams) bool { + if params.beforeImage.IsZero() { + return true + } + createdTime, err := getCreatedTime(image) + if err != nil { + return false + } + if createdTime.Before(params.beforeImage) { + return true + } + return false +} + +// Returns true if the image was created since the filter image. Returns +// false otherwise +func matchesSinceImage(image storage.Image, name string, params *FilterParams) bool { + if params.sinceImage.IsZero() { + return true + } + createdTime, err := getCreatedTime(image) + if err != nil { + return false + } + if createdTime.After(params.beforeImage) { + return true + } + return false +} + +// MatchesID returns true if argID is a full or partial match for id +func MatchesID(id, argID string) bool { + return strings.HasPrefix(id, argID) +} + +// MatchesReference returns true if argName is a full or partial match for name +// Partial matches will register only if they match the most specific part of the name available +// For example, take the image docker.io/library/redis:latest +// redis, library,redis, docker.io/library/redis, redis:latest, etc. will match +// But redis:alpine, ry/redis, library, and io/library/redis will not +func MatchesReference(name, argName string) bool { + if argName == "" { + return true + } + splitName := strings.Split(name, ":") + // If the arg contains a tag, we handle it differently than if it does not + if strings.Contains(argName, ":") { + splitArg := strings.Split(argName, ":") + return strings.HasSuffix(splitName[0], splitArg[0]) && (splitName[1] == splitArg[1]) + } + return strings.HasSuffix(splitName[0], argName) +} + +// FormattedSize returns a human-readable formatted size for the image +func FormattedSize(size int64) string { + suffixes := [5]string{"B", "KB", "MB", "GB", "TB"} + + count := 0 + formattedSize := float64(size) + for formattedSize >= 1024 && count < 4 { + formattedSize /= 1024 + count++ + } + return fmt.Sprintf("%.4g %s", formattedSize, suffixes[count]) +} + // FindImage searches for an image with a matching the given name or ID in the given store func FindImage(store storage.Store, image string) (*storage.Image, error) { var img *storage.Image @@ -26,6 +204,20 @@ func FindImage(store storage.Store, image string) (*storage.Image, error) { return img, nil } +func findImageInSlice(images []storage.Image, ref string) (storage.Image, error) { + for _, image := range images { + if MatchesID(image.ID, ref) { + return image, nil + } + for _, name := range image.Names { + if MatchesReference(name, ref) { + return image, nil + } + } + } + return storage.Image{}, errors.New("could not find image") +} + // Size returns the size of the image in the given store func Size(store storage.Store, img storage.Image) (int64, error) { is.Transport.SetStore(store) @@ -55,3 +247,40 @@ func GetTopLayerID(img storage.Image) (string, error) { // Return the first layer associated with the given digest return metadata.Layers[digest][0], nil } + +func getCreatedTime(image storage.Image) (time.Time, error) { + metadata, err := ParseMetadata(image) + if err != nil { + return time.Time{}, err + } + return metadata.CreatedTime, nil +} + +// GetImagesMatchingFilter returns a slice of all images in the store that match the provided FilterParams. +// Images with more than one name matching the filter will be in the slice once for each name +func GetImagesMatchingFilter(store storage.Store, filter *FilterParams, argName string) ([]storage.Image, error) { + images, err := store.Images() + filteredImages := []storage.Image{} + if err != nil { + return nil, err + } + if filter == nil { + return images, nil + } + for _, image := range images { + names := []string{} + if len(image.Names) > 0 { + names = image.Names + } else { + names = append(names, "") + } + for _, name := range names { + if matchesFilter(store, image, name, filter) || MatchesReference(name, argName) { + newImage := image + newImage.Names = []string{name} + filteredImages = append(filteredImages, newImage) + } + } + } + return filteredImages, nil +}