summaryrefslogtreecommitdiff
path: root/libpod/image
diff options
context:
space:
mode:
Diffstat (limited to 'libpod/image')
-rw-r--r--libpod/image/filters.go176
-rw-r--r--libpod/image/image.go163
-rw-r--r--libpod/image/prune.go79
-rw-r--r--libpod/image/pull.go10
4 files changed, 335 insertions, 93 deletions
diff --git a/libpod/image/filters.go b/libpod/image/filters.go
new file mode 100644
index 000000000..d545f1bfc
--- /dev/null
+++ b/libpod/image/filters.go
@@ -0,0 +1,176 @@
+package image
+
+import (
+ "context"
+ "fmt"
+ "github.com/pkg/errors"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/containers/libpod/pkg/inspect"
+ "github.com/sirupsen/logrus"
+)
+
+// ResultFilter is a mock function for image filtering
+type ResultFilter func(*Image) bool
+
+// Filter is a function to determine whether an image is included in
+// command output. Images to be outputted are tested using the function. A true
+// return will include the image, a false return will exclude it.
+type Filter func(*Image, *inspect.ImageData) bool
+
+// CreatedBeforeFilter allows you to filter on images created before
+// the given time.Time
+func CreatedBeforeFilter(createTime time.Time) ResultFilter {
+ return func(i *Image) bool {
+ return i.Created().Before(createTime)
+ }
+}
+
+// CreatedAfterFilter allows you to filter on images created after
+// the given time.Time
+func CreatedAfterFilter(createTime time.Time) ResultFilter {
+ return func(i *Image) bool {
+ return i.Created().After(createTime)
+ }
+}
+
+// DanglingFilter allows you to filter images for dangling images
+func DanglingFilter(danglingImages bool) ResultFilter {
+ return func(i *Image) bool {
+ if danglingImages {
+ return i.Dangling()
+ }
+ return !i.Dangling()
+ }
+}
+
+// ReadOnlyFilter allows you to filter images based on read/only and read/write
+func ReadOnlyFilter(readOnly bool) ResultFilter {
+ return func(i *Image) bool {
+ if readOnly {
+ return i.IsReadOnly()
+ }
+ return !i.IsReadOnly()
+ }
+}
+
+// LabelFilter allows you to filter by images labels key and/or value
+func LabelFilter(ctx context.Context, labelfilter string) ResultFilter {
+ // We need to handle both label=key and label=key=value
+ return func(i *Image) bool {
+ var value string
+ splitFilter := strings.Split(labelfilter, "=")
+ key := splitFilter[0]
+ if len(splitFilter) > 1 {
+ value = splitFilter[1]
+ }
+ labels, err := i.Labels(ctx)
+ if err != nil {
+ return false
+ }
+ if len(strings.TrimSpace(labels[key])) > 0 && len(strings.TrimSpace(value)) == 0 {
+ return true
+ }
+ return labels[key] == value
+ }
+}
+
+// ReferenceFilter allows you to filter by image name
+// Replacing all '/' with '|' so that filepath.Match() can work
+// '|' character is not valid in image name, so this is safe
+func ReferenceFilter(ctx context.Context, referenceFilter string) ResultFilter {
+ filter := fmt.Sprintf("*%s*", referenceFilter)
+ filter = strings.Replace(filter, "/", "|", -1)
+ return func(i *Image) bool {
+ if len(referenceFilter) < 1 {
+ return true
+ }
+ for _, name := range i.Names() {
+ newName := strings.Replace(name, "/", "|", -1)
+ match, err := filepath.Match(filter, newName)
+ if err != nil {
+ logrus.Errorf("failed to match %s and %s, %q", name, referenceFilter, err)
+ }
+ if match {
+ return true
+ }
+ }
+ return false
+ }
+}
+
+// OutputImageFilter allows you to filter by an a specific image name
+func OutputImageFilter(userImage *Image) ResultFilter {
+ return func(i *Image) bool {
+ return userImage.ID() == i.ID()
+ }
+}
+
+// FilterImages filters images using a set of predefined filter funcs
+func FilterImages(images []*Image, filters []ResultFilter) []*Image {
+ var filteredImages []*Image
+ for _, image := range images {
+ include := true
+ for _, filter := range filters {
+ include = include && filter(image)
+ }
+ if include {
+ filteredImages = append(filteredImages, image)
+ }
+ }
+ return filteredImages
+}
+
+// createFilterFuncs returns an array of filter functions based on the user inputs
+// and is later used to filter images for output
+func (ir *Runtime) createFilterFuncs(filters []string, img *Image) ([]ResultFilter, error) {
+ var filterFuncs []ResultFilter
+ ctx := context.Background()
+ for _, filter := range filters {
+ splitFilter := strings.Split(filter, "=")
+ if len(splitFilter) < 2 {
+ return nil, errors.Errorf("invalid filter syntax %s", filter)
+ }
+ switch splitFilter[0] {
+ case "before":
+ before, err := ir.NewFromLocal(splitFilter[1])
+ if err != nil {
+ return nil, errors.Wrapf(err, "unable to find image %s in local stores", splitFilter[1])
+ }
+ filterFuncs = append(filterFuncs, CreatedBeforeFilter(before.Created()))
+ case "after":
+ after, err := ir.NewFromLocal(splitFilter[1])
+ if err != nil {
+ return nil, errors.Wrapf(err, "unable to find image %s in local stores", splitFilter[1])
+ }
+ filterFuncs = append(filterFuncs, CreatedAfterFilter(after.Created()))
+ case "readonly":
+ readonly, err := strconv.ParseBool(splitFilter[1])
+ if err != nil {
+ return nil, errors.Wrapf(err, "invalid filter readonly=%s", splitFilter[1])
+ }
+ filterFuncs = append(filterFuncs, ReadOnlyFilter(readonly))
+ case "dangling":
+ danglingImages, err := strconv.ParseBool(splitFilter[1])
+ if err != nil {
+ return nil, errors.Wrapf(err, "invalid filter dangling=%s", splitFilter[1])
+ }
+ filterFuncs = append(filterFuncs, DanglingFilter(danglingImages))
+ case "label":
+ labelFilter := strings.Join(splitFilter[1:], "=")
+ filterFuncs = append(filterFuncs, LabelFilter(ctx, labelFilter))
+ case "reference":
+ referenceFilter := strings.Join(splitFilter[1:], "=")
+ filterFuncs = append(filterFuncs, ReferenceFilter(ctx, referenceFilter))
+ default:
+ return nil, errors.Errorf("invalid filter %s ", splitFilter[0])
+ }
+ }
+ if img != nil {
+ filterFuncs = append(filterFuncs, OutputImageFilter(img))
+ }
+ return filterFuncs, nil
+}
diff --git a/libpod/image/image.go b/libpod/image/image.go
index c912ac2ca..c8583a1c5 100644
--- a/libpod/image/image.go
+++ b/libpod/image/image.go
@@ -74,6 +74,11 @@ type InfoImage struct {
Layers []LayerInfo
}
+// ImageFilter is a function to determine whether a image is included
+// in command output. Images to be outputted are tested using the function.
+// A true return will include the image, a false return will exclude it.
+type ImageFilter func(*Image) bool //nolint
+
// ErrRepoTagNotFound is the error returned when the image id given doesn't match a rep tag in store
var ErrRepoTagNotFound = stderrors.New("unable to match user input to any specific repotag")
@@ -211,6 +216,19 @@ func (ir *Runtime) Shutdown(force bool) error {
return err
}
+// GetImagesWithFilters gets images with a series of filters applied
+func (ir *Runtime) GetImagesWithFilters(filters []string) ([]*Image, error) {
+ filterFuncs, err := ir.createFilterFuncs(filters, nil)
+ if err != nil {
+ return nil, err
+ }
+ images, err := ir.GetImages()
+ if err != nil {
+ return nil, err
+ }
+ return FilterImages(images, filterFuncs), nil
+}
+
func (i *Image) reloadImage() error {
newImage, err := i.imageruntime.getImage(i.ID())
if err != nil {
@@ -330,6 +348,21 @@ func (i *Image) Names() []string {
return i.image.Names
}
+// NamesHistory returns a string array of names previously associated with the
+// image, which may be a mixture of tags and digests
+func (i *Image) NamesHistory() []string {
+ if len(i.image.Names) > 0 && len(i.image.NamesHistory) > 0 &&
+ // We compare the latest (time-referenced) tags for equality and skip
+ // it in the history if they match to not display them twice. We have
+ // to compare like this, because `i.image.Names` (latest last) gets
+ // appended on retag, whereas `i.image.NamesHistory` gets prepended
+ // (latest first)
+ i.image.Names[len(i.image.Names)-1] == i.image.NamesHistory[0] {
+ return i.image.NamesHistory[1:]
+ }
+ return i.image.NamesHistory
+}
+
// RepoTags returns a string array of repotags associated with the image
func (i *Image) RepoTags() ([]string, error) {
var repoTags []string
@@ -765,109 +798,65 @@ func (i *Image) History(ctx context.Context) ([]*History, error) {
return nil, err
}
- // Use our layers list to find images that use any of them (or no
- // layer, since every base layer is derived from an empty layer) as its
- // topmost layer.
- interestingLayers := make(map[string]bool)
- var layer *storage.Layer
- if i.TopLayer() != "" {
- if layer, err = i.imageruntime.store.Layer(i.TopLayer()); err != nil {
- return nil, err
- }
+ // Build a mapping from top-layer to image ID.
+ images, err := i.imageruntime.GetImages()
+ if err != nil {
+ return nil, err
}
- interestingLayers[""] = true
- for layer != nil {
- interestingLayers[layer.ID] = true
- if layer.Parent == "" {
- break
+ topLayerMap := make(map[string]string)
+ for _, image := range images {
+ if _, exists := topLayerMap[image.TopLayer()]; !exists {
+ topLayerMap[image.TopLayer()] = image.ID()
}
- layer, err = i.imageruntime.store.Layer(layer.Parent)
+ }
+
+ var allHistory []*History
+ var layer *storage.Layer
+
+ // Check if we have an actual top layer to prevent lookup errors.
+ if i.TopLayer() != "" {
+ layer, err = i.imageruntime.store.Layer(i.TopLayer())
if err != nil {
return nil, err
}
}
- // Get the IDs of the images that share some of our layers. Hopefully
- // this step means that we'll be able to avoid reading the
- // configuration of every single image in local storage later on.
- images, err := i.imageruntime.GetImages()
- if err != nil {
- return nil, errors.Wrapf(err, "error getting images from store")
- }
- interestingImages := make([]*Image, 0, len(images))
- for i := range images {
- if interestingLayers[images[i].TopLayer()] {
- interestingImages = append(interestingImages, images[i])
- }
- }
+ // Iterate in reverse order over the history entries, and lookup the
+ // corresponding image ID, size and get the next later if needed.
+ numHistories := len(oci.History) - 1
+ for x := numHistories; x >= 0; x-- {
+ var size int64
- // Build a list of image IDs that correspond to our history entries.
- historyImages := make([]*Image, len(oci.History))
- if len(oci.History) > 0 {
- // The starting image shares its whole history with itself.
- historyImages[len(historyImages)-1] = i
- for i := range interestingImages {
- image, err := images[i].ociv1Image(ctx)
- if err != nil {
- return nil, errors.Wrapf(err, "error getting image configuration for image %q", images[i].ID())
- }
- // If the candidate has a longer history or no history
- // at all, then it doesn't share the portion of our
- // history that we're interested in matching with other
- // images.
- if len(image.History) == 0 || len(image.History) > len(historyImages) {
- continue
- }
- // If we don't include all of the layers that the
- // candidate image does (i.e., our rootfs didn't look
- // like its rootfs at any point), then it can't be part
- // of our history.
- if len(image.RootFS.DiffIDs) > len(oci.RootFS.DiffIDs) {
- continue
- }
- candidateLayersAreUsed := true
- for i := range image.RootFS.DiffIDs {
- if image.RootFS.DiffIDs[i] != oci.RootFS.DiffIDs[i] {
- candidateLayersAreUsed = false
- break
- }
- }
- if !candidateLayersAreUsed {
- continue
+ id := "<missing>"
+ if x == numHistories {
+ id = i.ID()
+ } else if layer != nil {
+ if !oci.History[x].EmptyLayer {
+ size = layer.UncompressedSize
}
- // If the candidate's entire history is an initial
- // portion of our history, then we're based on it,
- // either directly or indirectly.
- sharedHistory := historiesMatch(oci.History, image.History)
- if sharedHistory == len(image.History) {
- historyImages[sharedHistory-1] = images[i]
+ if imageID, exists := topLayerMap[layer.ID]; exists {
+ id = imageID
+ // Delete the entry to avoid reusing it for following history items.
+ delete(topLayerMap, layer.ID)
}
}
- }
-
- var (
- size int64
- sizeCount = 1
- allHistory []*History
- )
- for i := len(oci.History) - 1; i >= 0; i-- {
- imageID := "<missing>"
- if historyImages[i] != nil {
- imageID = historyImages[i].ID()
- }
- if !oci.History[i].EmptyLayer {
- size = img.LayerInfos()[len(img.LayerInfos())-sizeCount].Size
- sizeCount++
- }
allHistory = append(allHistory, &History{
- ID: imageID,
- Created: oci.History[i].Created,
- CreatedBy: oci.History[i].CreatedBy,
+ ID: id,
+ Created: oci.History[x].Created,
+ CreatedBy: oci.History[x].CreatedBy,
Size: size,
- Comment: oci.History[i].Comment,
+ Comment: oci.History[x].Comment,
})
+
+ if layer != nil && layer.Parent != "" && !oci.History[x].EmptyLayer {
+ layer, err = i.imageruntime.store.Layer(layer.Parent)
+ if err != nil {
+ return nil, err
+ }
+ }
}
+
return allHistory, nil
}
diff --git a/libpod/image/prune.go b/libpod/image/prune.go
index 006cbdf22..f5be8ed50 100644
--- a/libpod/image/prune.go
+++ b/libpod/image/prune.go
@@ -2,23 +2,78 @@ package image
import (
"context"
+ "strings"
+ "time"
"github.com/containers/libpod/libpod/events"
+ "github.com/containers/libpod/pkg/timetype"
"github.com/containers/storage"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
+func generatePruneFilterFuncs(filter, filterValue string) (ImageFilter, error) {
+ switch filter {
+ case "label":
+ var filterArray = strings.SplitN(filterValue, "=", 2)
+ var filterKey = filterArray[0]
+ if len(filterArray) > 1 {
+ filterValue = filterArray[1]
+ } else {
+ filterValue = ""
+ }
+ return func(i *Image) bool {
+ labels, err := i.Labels(context.Background())
+ if err != nil {
+ return false
+ }
+ for labelKey, labelValue := range labels {
+ if labelKey == filterKey && ("" == filterValue || labelValue == filterValue) {
+ return true
+ }
+ }
+ return false
+ }, nil
+
+ case "until":
+ ts, err := timetype.GetTimestamp(filterValue, time.Now())
+ if err != nil {
+ return nil, err
+ }
+ seconds, nanoseconds, err := timetype.ParseTimestamps(ts, 0)
+ if err != nil {
+ return nil, err
+ }
+ until := time.Unix(seconds, nanoseconds)
+ return func(i *Image) bool {
+ if !until.IsZero() && i.Created().After((until)) {
+ return true
+ }
+ return false
+ }, nil
+
+ }
+ return nil, nil
+}
+
// GetPruneImages returns a slice of images that have no names/unused
-func (ir *Runtime) GetPruneImages(all bool) ([]*Image, error) {
+func (ir *Runtime) GetPruneImages(all bool, filterFuncs []ImageFilter) ([]*Image, error) {
var (
pruneImages []*Image
)
+
allImages, err := ir.GetRWImages()
if err != nil {
return nil, err
}
for _, i := range allImages {
+ // filter the images based on this.
+ for _, filterFunc := range filterFuncs {
+ if !filterFunc(i) {
+ continue
+ }
+ }
+
if len(i.Names()) == 0 {
pruneImages = append(pruneImages, i)
continue
@@ -38,9 +93,25 @@ func (ir *Runtime) GetPruneImages(all bool) ([]*Image, error) {
// PruneImages prunes dangling and optionally all unused images from the local
// image store
-func (ir *Runtime) PruneImages(ctx context.Context, all bool) ([]string, error) {
- var prunedCids []string
- pruneImages, err := ir.GetPruneImages(all)
+func (ir *Runtime) PruneImages(ctx context.Context, all bool, filter []string) ([]string, error) {
+ var (
+ prunedCids []string
+ filterFuncs []ImageFilter
+ )
+ for _, f := range filter {
+ filterSplit := strings.SplitN(f, "=", 2)
+ if len(filterSplit) < 2 {
+ return nil, errors.Errorf("filter input must be in the form of filter=value: %s is invalid", f)
+ }
+
+ generatedFunc, err := generatePruneFilterFuncs(filterSplit[0], filterSplit[1])
+ if err != nil {
+ return nil, errors.Wrapf(err, "invalid filter")
+ }
+ filterFuncs = append(filterFuncs, generatedFunc)
+ }
+
+ pruneImages, err := ir.GetPruneImages(all, filterFuncs)
if err != nil {
return nil, errors.Wrap(err, "unable to get images to prune")
}
diff --git a/libpod/image/pull.go b/libpod/image/pull.go
index 7f5dc33b9..326a23f4c 100644
--- a/libpod/image/pull.go
+++ b/libpod/image/pull.go
@@ -230,7 +230,12 @@ func (ir *Runtime) pullImageFromHeuristicSource(ctx context.Context, inputName s
sc.BlobInfoCacheDir = filepath.Join(ir.store.GraphRoot(), "cache")
srcRef, err := alltransports.ParseImageName(inputName)
if err != nil {
- // could be trying to pull from registry with short name
+ // We might be pulling with an unqualified image reference in which case
+ // we need to make sure that we're not using any other transport.
+ srcTransport := alltransports.TransportFromImageName(inputName)
+ if srcTransport != nil && srcTransport.Name() != DockerTransport {
+ return nil, err
+ }
goal, err = ir.pullGoalFromPossiblyUnqualifiedName(inputName)
if err != nil {
return nil, errors.Wrap(err, "error getting default registries to try")
@@ -325,7 +330,7 @@ func (ir *Runtime) doPullImage(ctx context.Context, sc *types.SystemContext, goa
if goal.usedSearchRegistries && len(goal.searchedRegistries) == 0 {
return nil, errors.Errorf("image name provided is a short name and no search registries are defined in the registries config file.")
}
- // If the image passed in was fully-qualified, we will have 1 refpair. Bc the image is fq'd, we dont need to yap about registries.
+ // If the image passed in was fully-qualified, we will have 1 refpair. Bc the image is fq'd, we don't need to yap about registries.
if !goal.usedSearchRegistries {
if pullErrors != nil && len(pullErrors.Errors) > 0 { // this should always be true
return nil, errors.Wrap(pullErrors.Errors[0], "unable to pull image")
@@ -347,6 +352,7 @@ func (ir *Runtime) pullGoalFromPossiblyUnqualifiedName(inputName string) (*pullG
if err != nil {
return nil, err
}
+
if decomposedImage.hasRegistry {
srcRef, err := docker.ParseReference("//" + inputName)
if err != nil {