mirror of
https://github.com/psviderski/uncloud.git
synced 2026-11-10 20:54:17 +00:00
feat(images): implement RemoveImage gRPC API with extra protection for used images
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user