Files

80 lines
2.9 KiB
Go

package docker
import (
"context"
"fmt"
"strings"
"github.com/containerd/errdefs"
"github.com/distribution/reference"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
"github.com/psviderski/uncloud/pkg/api"
)
// RemoveImage removes an image while protecting names used by containers, even when force is requested.
func (s *Service) RemoveImage(ctx context.Context, id string, opts image.RemoveOptions) ([]image.DeleteResponse, error) {
if err := s.checkImageRemovalConflicts(ctx, id); err != nil {
return nil, err
}
// Docker enforces its own conflicts, including protection of a used image's final reference.
return s.Client.ImageRemove(ctx, id, opts)
}
// checkImageRemovalConflicts rejects an image ID used by a running or stopped container, or a name used by a
// container while it still resolves to that container's image. A digest request also checks the repository tags
// Docker would remove with it. Unused aliases are allowed.
func (s *Service) checkImageRemovalConflicts(ctx context.Context, id string) error {
img, err := s.Client.ImageInspect(ctx, id)
if err != nil {
return err
}
// Match Docker's resolved ID before parsing names, since a short ID also parses as a repository name.
isID := strings.HasPrefix(strings.TrimPrefix(img.ID, "sha256:"), strings.TrimPrefix(id, "sha256:"))
var named reference.Named
if !isID {
named, err = reference.ParseDockerRef(id)
if err != nil {
return errdefs.ErrInvalidArgument.WithMessage(err.Error())
}
}
_, isDigest := named.(reference.Canonical)
containers, err := s.Client.ContainerList(ctx, container.ListOptions{
All: true, Filters: filters.NewArgs(filters.Arg("ancestor", img.ID)),
})
if err != nil {
return fmt.Errorf("list containers using image: %w", err)
}
for _, ctr := range containers {
// The ancestor filter can include descendants. A retagged name must also be checked against its current ID.
if ctr.ImageID != img.ID {
continue
}
usedRef, err := reference.ParseDockerRef(ctr.Image)
// Familiar names make "alpine:latest" and "docker.io/library/alpine:latest" identify the same tag.
removesUsedName := err == nil && named != nil && reference.FamiliarString(usedRef) == reference.FamiliarString(named)
if isDigest && err == nil && usedRef.Name() == named.Name() {
// Removing a digest also removes this repository's tags pointing at the same image.
removesUsedName = true
}
if isID || removesUsedName {
ctrID, state := stringid.TruncateID(ctr.ID), "stopped"
if serviceName := ctr.Labels[api.LabelServiceName]; serviceName != "" {
ctrID = serviceName + "/" + ctrID
}
if ctr.State == container.StateRunning || ctr.State == container.StatePaused {
state = container.StateRunning
}
return errdefs.ErrConflict.WithMessage(
fmt.Sprintf("image '%s' is used by container '%s' (%s)", id, ctrID, state))
}
}
return nil
}