mirror of
https://github.com/psviderski/uncloud.git
synced 2026-11-10 12:44:16 +00:00
80 lines
2.9 KiB
Go
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
|
|
}
|