image prune: support removing external containers

Support removing external containers (e.g., build containers) during
image prune.

Fixes: #11472
Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
This commit is contained in:
Valentin Rothberg
2021-09-24 14:39:36 +02:00
parent 340166876e
commit a9a54eefab
26 changed files with 208 additions and 70 deletions

View File

@ -304,7 +304,7 @@ func (r *Runtime) newCopier(options *CopyOptions) (*copier, error) {
defaultContainerConfig, err := config.Default()
if err != nil {
logrus.Warnf("failed to get container config for copy options: %v", err)
logrus.Warnf("Failed to get container config for copy options: %v", err)
} else {
c.imageCopyOptions.MaxParallelDownloads = defaultContainerConfig.Engine.ImageParallelCopies
}

View File

@ -47,11 +47,11 @@ func filterImages(images []*Image, filters []filterFunc) ([]*Image, error) {
// compileImageFilters creates `filterFunc`s for the specified filters. The
// required format is `key=value` with the following supported keys:
// after, since, before, containers, dangling, id, label, readonly, reference, intermediate
func (r *Runtime) compileImageFilters(ctx context.Context, filters []string) ([]filterFunc, error) {
logrus.Tracef("Parsing image filters %s", filters)
func (r *Runtime) compileImageFilters(ctx context.Context, options *ListImagesOptions) ([]filterFunc, error) {
logrus.Tracef("Parsing image filters %s", options.Filters)
filterFuncs := []filterFunc{}
for _, filter := range filters {
for _, filter := range options.Filters {
var key, value string
split := strings.SplitN(filter, "=", 2)
if len(split) != 2 {
@ -77,11 +77,16 @@ func (r *Runtime) compileImageFilters(ctx context.Context, filters []string) ([]
filterFuncs = append(filterFuncs, filterBefore(img.Created()))
case "containers":
containers, err := strconv.ParseBool(value)
if err != nil {
return nil, errors.Wrapf(err, "non-boolean value %q for dangling filter", value)
switch value {
case "false", "true":
case "external":
if options.IsExternalContainerFunc == nil {
return nil, fmt.Errorf("libimage error: external containers filter without callback")
}
default:
return nil, fmt.Errorf("unsupported value %q for containers filter", value)
}
filterFuncs = append(filterFuncs, filterContainers(containers))
filterFuncs = append(filterFuncs, filterContainers(value, options.IsExternalContainerFunc))
case "dangling":
dangling, err := strconv.ParseBool(value)
@ -190,13 +195,28 @@ func filterReadOnly(value bool) filterFunc {
}
// filterContainers creates a container filter for matching the specified value.
func filterContainers(value bool) filterFunc {
func filterContainers(value string, fn IsExternalContainerFunc) filterFunc {
return func(img *Image) (bool, error) {
ctrs, err := img.Containers()
if err != nil {
return false, err
}
return (len(ctrs) > 0) == value, nil
if value != "external" {
boolValue := value == "true"
return (len(ctrs) > 0) == boolValue, nil
}
// Check whether all associated containers are external ones.
for _, c := range ctrs {
isExternal, err := fn(c)
if err != nil {
return false, fmt.Errorf("checking if %s is an external container in filter: %w", c, err)
}
if !isExternal {
return isExternal, nil
}
}
return true, nil
}
}

View File

@ -2,6 +2,7 @@ package libimage
import (
"context"
"fmt"
"path/filepath"
"sort"
"strings"
@ -51,7 +52,7 @@ func (i *Image) reload() error {
logrus.Tracef("Reloading image %s", i.ID())
img, err := i.runtime.store.Image(i.ID())
if err != nil {
return errors.Wrap(err, "error reloading image")
return errors.Wrap(err, "reloading image")
}
i.storageImage = img
i.cached.imageSource = nil
@ -232,11 +233,15 @@ func (i *Image) Containers() ([]string, error) {
}
// removeContainers removes all containers using the image.
func (i *Image) removeContainers(fn RemoveContainerFunc) error {
// Execute the custom removal func if specified.
if fn != nil {
func (i *Image) removeContainers(options *RemoveImagesOptions) error {
if !options.Force && !options.ExternalContainers {
// Nothing to do.
return nil
}
if options.Force && options.RemoveContainerFunc != nil {
logrus.Debugf("Removing containers of image %s with custom removal function", i.ID())
if err := fn(i.ID()); err != nil {
if err := options.RemoveContainerFunc(i.ID()); err != nil {
return err
}
}
@ -246,6 +251,19 @@ func (i *Image) removeContainers(fn RemoveContainerFunc) error {
return err
}
if !options.Force && options.ExternalContainers {
// All containers must be external ones.
for _, cID := range containers {
isExternal, err := options.IsExternalContainerFunc(cID)
if err != nil {
return fmt.Errorf("checking if %s is an external container: %w", cID, err)
}
if !isExternal {
return fmt.Errorf("cannot remove container %s: not an external container", cID)
}
}
}
logrus.Debugf("Removing containers of image %s from the local containers storage", i.ID())
var multiE error
for _, cID := range containers {
@ -392,11 +410,9 @@ func (i *Image) removeRecursive(ctx context.Context, rmMap map[string]*RemoveIma
return processedIDs, nil
}
// Perform the actual removal. First, remove containers if needed.
if options.Force {
if err := i.removeContainers(options.RemoveContainerFunc); err != nil {
return processedIDs, err
}
// Perform the container removal, if needed.
if err := i.removeContainers(options); err != nil {
return processedIDs, err
}
// Podman/Docker compat: we only report an image as removed if it has
@ -406,7 +422,7 @@ func (i *Image) removeRecursive(ctx context.Context, rmMap map[string]*RemoveIma
if err != nil {
// We must be tolerant toward corrupted images.
// See containers/podman commit fd9dd7065d44.
logrus.Warnf("error determining if an image is a parent: %v, ignoring the error", err)
logrus.Warnf("Failed to determine if an image is a parent: %v, ignoring the error", err)
hasChildren = false
}
@ -416,7 +432,7 @@ func (i *Image) removeRecursive(ctx context.Context, rmMap map[string]*RemoveIma
if err != nil {
// We must be tolerant toward corrupted images.
// See containers/podman commit fd9dd7065d44.
logrus.Warnf("error determining parent of image: %v, ignoring the error", err)
logrus.Warnf("Failed to determine parent of image: %v, ignoring the error", err)
parent = nil
}
@ -440,7 +456,7 @@ func (i *Image) removeRecursive(ctx context.Context, rmMap map[string]*RemoveIma
if err != nil {
// See Podman commit fd9dd7065d44: we need to
// be tolerant toward corrupted images.
logrus.Warnf("error determining if an image is a parent: %v, ignoring the error", err)
logrus.Warnf("Failed to determine if an image is a parent: %v, ignoring the error", err)
danglingParent = false
}
if !danglingParent {
@ -462,7 +478,7 @@ func (i *Image) Tag(name string) error {
ref, err := NormalizeName(name)
if err != nil {
return errors.Wrapf(err, "error normalizing name %q", name)
return errors.Wrapf(err, "normalizing name %q", name)
}
if _, isDigested := ref.(reference.Digested); isDigested {
@ -499,7 +515,7 @@ func (i *Image) Untag(name string) error {
ref, err := NormalizeName(name)
if err != nil {
return errors.Wrapf(err, "error normalizing name %q", name)
return errors.Wrapf(err, "normalizing name %q", name)
}
// FIXME: this is breaking Podman CI but must be re-enabled once
@ -885,12 +901,12 @@ func getImageID(ctx context.Context, src types.ImageReference, sys *types.System
}
defer func() {
if err := newImg.Close(); err != nil {
logrus.Errorf("failed to close image: %q", err)
logrus.Errorf("Failed to close image: %q", err)
}
}()
imageDigest := newImg.ConfigInfo().Digest
if err = imageDigest.Validate(); err != nil {
return "", errors.Wrapf(err, "error getting config info")
return "", errors.Wrapf(err, "getting config info")
}
return "@" + imageDigest.Encoded(), nil
}

View File

@ -125,19 +125,19 @@ func (l *list) SaveToImage(store storage.Store, imageID string, names []string,
if err != nil {
if created {
if _, err2 := store.DeleteImage(img.ID, true); err2 != nil {
logrus.Errorf("error deleting image %q after failing to save manifest for it", img.ID)
logrus.Errorf("Deleting image %q after failing to save manifest for it", img.ID)
}
}
return "", errors.Wrapf(err, "error saving manifest list to image %q", imageID)
return "", errors.Wrapf(err, "saving manifest list to image %q", imageID)
}
err = store.SetImageBigData(imageID, instancesData, instancesBytes, nil)
if err != nil {
if created {
if _, err2 := store.DeleteImage(img.ID, true); err2 != nil {
logrus.Errorf("error deleting image %q after failing to save instance locations for it", img.ID)
logrus.Errorf("Deleting image %q after failing to save instance locations for it", img.ID)
}
}
return "", errors.Wrapf(err, "error saving instance list to image %q", imageID)
return "", errors.Wrapf(err, "saving instance list to image %q", imageID)
}
return imageID, nil
}
@ -200,7 +200,7 @@ func (l *list) Push(ctx context.Context, dest types.ImageReference, options Push
}
defer func() {
if err2 := policyContext.Destroy(); err2 != nil {
logrus.Errorf("error destroying signature policy context: %v", err2)
logrus.Errorf("Destroying signature policy context: %v", err2)
}
}()

View File

@ -2,6 +2,7 @@ package libimage
import (
"context"
"fmt"
"os"
"strings"
@ -306,7 +307,7 @@ func (r *Runtime) lookupImageInLocalStorage(name, candidate string, options *Loo
if errors.Cause(err) == os.ErrNotExist {
// We must be tolerant toward corrupted images.
// See containers/podman commit fd9dd7065d44.
logrus.Warnf("error determining if an image is a manifest list: %v, ignoring the error", err)
logrus.Warnf("Failed to determine if an image is a manifest list: %v, ignoring the error", err)
return image, nil
}
return nil, err
@ -484,10 +485,16 @@ func (r *Runtime) imageReferenceMatchesContext(ref types.ImageReference, options
return true, nil
}
// IsExternalContainerFunc allows for checking whether the specified container
// is an external one. The definition of an external container can be set by
// callers.
type IsExternalContainerFunc func(containerID string) (bool, error)
// ListImagesOptions allow for customizing listing images.
type ListImagesOptions struct {
// Filters to filter the listed images. Supported filters are
// * after,before,since=image
// * containers=true,false,external
// * dangling=true,false
// * intermediate=true,false (useful for pruning images)
// * id=id
@ -495,6 +502,11 @@ type ListImagesOptions struct {
// * readonly=true,false
// * reference=name[:tag] (wildcards allowed)
Filters []string
// IsExternalContainerFunc allows for checking whether the specified
// container is an external one (when containers=external filter is
// used). The definition of an external container can be set by
// callers.
IsExternalContainerFunc IsExternalContainerFunc
}
// ListImages lists images in the local container storage. If names are
@ -525,7 +537,7 @@ func (r *Runtime) ListImages(ctx context.Context, names []string, options *ListI
var filters []filterFunc
if len(options.Filters) > 0 {
compiledFilters, err := r.compileImageFilters(ctx, options.Filters)
compiledFilters, err := r.compileImageFilters(ctx, options)
if err != nil {
return nil, err
}
@ -550,8 +562,17 @@ type RemoveImagesOptions struct {
// containers using a specific image. By default, all containers in
// the local containers storage will be removed (if Force is set).
RemoveContainerFunc RemoveContainerFunc
// IsExternalContainerFunc allows for checking whether the specified
// container is an external one (when containers=external filter is
// used). The definition of an external container can be set by
// callers.
IsExternalContainerFunc IsExternalContainerFunc
// Remove external containers even when Force is false. Requires
// IsExternalContainerFunc to be specified.
ExternalContainers bool
// Filters to filter the removed images. Supported filters are
// * after,before,since=image
// * containers=true,false,external
// * dangling=true,false
// * intermediate=true,false (useful for pruning images)
// * id=id
@ -581,6 +602,10 @@ func (r *Runtime) RemoveImages(ctx context.Context, names []string, options *Rem
options = &RemoveImagesOptions{}
}
if options.ExternalContainers && options.IsExternalContainerFunc == nil {
return nil, []error{fmt.Errorf("libimage error: cannot remove external containers without callback")}
}
// The logic here may require some explanation. Image removal is
// surprisingly complex since it is recursive (intermediate parents are
// removed) and since multiple items in `names` may resolve to the
@ -635,7 +660,11 @@ func (r *Runtime) RemoveImages(ctx context.Context, names []string, options *Rem
}
default:
filteredImages, err := r.ListImages(ctx, nil, &ListImagesOptions{Filters: options.Filters})
options := &ListImagesOptions{
IsExternalContainerFunc: options.IsExternalContainerFunc,
Filters: options.Filters,
}
filteredImages, err := r.ListImages(ctx, nil, options)
if err != nil {
appendError(err)
return nil, rmErrors

View File

@ -97,22 +97,22 @@ func InstallDefault(name string) error {
}
if err := cmd.Start(); err != nil {
if pipeErr := pipe.Close(); pipeErr != nil {
logrus.Errorf("unable to close AppArmor pipe: %q", pipeErr)
logrus.Errorf("Unable to close AppArmor pipe: %q", pipeErr)
}
return errors.Wrapf(err, "start %s command", apparmorParserPath)
}
if err := p.generateDefault(apparmorParserPath, pipe); err != nil {
if pipeErr := pipe.Close(); pipeErr != nil {
logrus.Errorf("unable to close AppArmor pipe: %q", pipeErr)
logrus.Errorf("Unable to close AppArmor pipe: %q", pipeErr)
}
if cmdErr := cmd.Wait(); cmdErr != nil {
logrus.Errorf("unable to wait for AppArmor command: %q", cmdErr)
logrus.Errorf("Unable to wait for AppArmor command: %q", cmdErr)
}
return errors.Wrap(err, "generate default profile into pipe")
}
if pipeErr := pipe.Close(); pipeErr != nil {
logrus.Errorf("unable to close AppArmor pipe: %q", pipeErr)
logrus.Errorf("Unable to close AppArmor pipe: %q", pipeErr)
}
return errors.Wrap(cmd.Wait(), "wait for AppArmor command")
@ -252,7 +252,7 @@ func CheckProfileAndLoadDefault(name string) (string, error) {
if name != "" {
return "", errors.Wrapf(ErrApparmorRootless, "cannot load AppArmor profile %q", name)
} else {
logrus.Debug("skipping loading default AppArmor profile (rootless mode)")
logrus.Debug("Skipping loading default AppArmor profile (rootless mode)")
return "", nil
}
}
@ -292,7 +292,7 @@ func CheckProfileAndLoadDefault(name string) (string, error) {
if err != nil {
return "", errors.Wrapf(err, "install profile %s", name)
}
logrus.Infof("successfully loaded AppAmor profile %q", name)
logrus.Infof("Successfully loaded AppAmor profile %q", name)
} else {
logrus.Infof("AppAmor profile %q is already loaded", name)
}

View File

@ -30,7 +30,7 @@ func RetryIfNecessary(ctx context.Context, operation func() error, retryOptions
if retryOptions.Delay != 0 {
delay = retryOptions.Delay
}
logrus.Warnf("failed, retrying in %s ... (%d/%d). Error: %v", delay, attempt+1, retryOptions.MaxRetry, err)
logrus.Warnf("Failed, retrying in %s ... (%d/%d). Error: %v", delay, attempt+1, retryOptions.MaxRetry, err)
select {
case <-time.After(delay):
break

View File

@ -114,13 +114,13 @@ func getMounts(filePath string) []string {
file, err := os.Open(filePath)
if err != nil {
// This is expected on most systems
logrus.Debugf("file %q not found, skipping...", filePath)
logrus.Debugf("File %q not found, skipping...", filePath)
return nil
}
defer file.Close()
scanner := bufio.NewScanner(file)
if err = scanner.Err(); err != nil {
logrus.Errorf("error reading file %q, %v skipping...", filePath, err)
logrus.Errorf("Reading file %q, %v skipping...", filePath, err)
return nil
}
var mounts []string
@ -128,7 +128,7 @@ func getMounts(filePath string) []string {
if strings.HasPrefix(strings.TrimSpace(scanner.Text()), "/") {
mounts = append(mounts, scanner.Text())
} else {
logrus.Debugf("skipping unrecognized mount in %v: %q",
logrus.Debugf("Skipping unrecognized mount in %v: %q",
filePath, scanner.Text())
}
}
@ -176,7 +176,7 @@ func MountsWithUIDGID(mountLabel, containerWorkingDir, mountFile, mountPoint str
if _, err := os.Stat(file); err == nil {
mounts, err := addSubscriptionsFromMountsFile(file, mountLabel, containerWorkingDir, uid, gid)
if err != nil {
logrus.Warnf("error mounting subscriptions, skipping entry in %s: %v", file, err)
logrus.Warnf("Failed to mount subscriptions, skipping entry in %s: %v", file, err)
}
subscriptionMounts = mounts
break
@ -192,7 +192,7 @@ func MountsWithUIDGID(mountLabel, containerWorkingDir, mountFile, mountPoint str
switch {
case err == nil:
if err := addFIPSModeSubscription(&subscriptionMounts, containerWorkingDir, mountPoint, mountLabel, uid, gid); err != nil {
logrus.Errorf("error adding FIPS mode subscription to container: %v", err)
logrus.Errorf("Adding FIPS mode subscription to container: %v", err)
}
case os.IsNotExist(err):
logrus.Debug("/etc/system-fips does not exist on host, not mounting FIPS mode subscription")

View File

@ -83,12 +83,12 @@ func (s *supplementedImageReference) NewImageSource(ctx context.Context, sys *ty
if iss != nil {
// The composite source has been created. Use its Close method.
if err2 := iss.Close(); err2 != nil {
logrus.Errorf("error opening image: %v", err2)
logrus.Errorf("Opening image: %v", err2)
}
} else if top != nil {
// The composite source has not been created, but the top was already opened. Close it.
if err2 := top.Close(); err2 != nil {
logrus.Errorf("error opening image: %v", err2)
logrus.Errorf("Closing image: %v", err2)
}
}
}

View File

@ -237,7 +237,7 @@ func checkCgroupPids(cgMounts map[string]string, quiet bool) cgroupPids {
_, ok := cgMounts["pids"]
if !ok {
if !quiet {
logrus.Warn("unable to find pids cgroup in mounts")
logrus.Warn("Unable to find pids cgroup in mounts")
}
return cgroupPids{}
}

View File

@ -1,4 +1,4 @@
package version
// Version is the version of the build.
const Version = "0.46.0"
const Version = "0.46.1-dev"