mirror of
https://github.com/psviderski/uncloud.git
synced 2026-11-10 12:44:16 +00:00
feat(images): implement RemoveImage gRPC API with extra protection for used images
This commit is contained in:
@@ -42,6 +42,8 @@ type DNSClient interface {
|
||||
type ImageClient interface {
|
||||
InspectImage(ctx context.Context, id string) ([]MachineImage, error)
|
||||
InspectRemoteImage(ctx context.Context, id string) ([]MachineRemoteImage, error)
|
||||
ListImages(ctx context.Context, filter ImageFilter) ([]MachineImages, error)
|
||||
RemoveImage(ctx context.Context, image string, opts RemoveImageOptions) ([]MachineImageRemoval, error)
|
||||
}
|
||||
|
||||
type LogsClient interface {
|
||||
|
||||
+9
-19
@@ -32,26 +32,16 @@ type ImageFilter struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// ImageReferences returns the image tag and named digest references, sorted and without duplicates.
|
||||
// References use familiar names such as "alpine:latest" and "alpine@sha256:...".
|
||||
// Bare digests and invalid references are excluded. It returns nil if there are no named references.
|
||||
func ImageReferences(img image.Summary) []string {
|
||||
var refs []string
|
||||
for _, names := range [][]string{img.RepoTags, img.RepoDigests} {
|
||||
for _, name := range names {
|
||||
ref, err := reference.ParseAnyReference(name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// RepoDigests can contain bare digests. Keep only references with a repository name.
|
||||
if named, ok := ref.(reference.Named); ok {
|
||||
refs = append(refs, reference.FamiliarString(named))
|
||||
}
|
||||
}
|
||||
}
|
||||
// RemoveImageOptions defines options for removing an image from cluster machines.
|
||||
type RemoveImageOptions struct {
|
||||
// Machines selects machines to remove the image from (names or IDs). If empty, all cluster machines are selected.
|
||||
Machines []string
|
||||
}
|
||||
|
||||
slices.Sort(refs)
|
||||
return slices.Compact(refs)
|
||||
// MachineImageRemoval contains the outcome of removing an image on one machine.
|
||||
type MachineImageRemoval struct {
|
||||
Metadata *pb.Metadata
|
||||
Results []image.DeleteResponse
|
||||
}
|
||||
|
||||
// MachineRemoteImage represents an image in a remote registry fetched by a particular machine.
|
||||
|
||||
@@ -23,9 +23,11 @@ import (
|
||||
"github.com/docker/docker/api/types/image"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/go-connections/nat"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/psviderski/uncloud/api/pb"
|
||||
cliprogress "github.com/psviderski/uncloud/internal/cli/progress"
|
||||
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||
"github.com/psviderski/uncloud/internal/docker"
|
||||
"github.com/psviderski/uncloud/internal/machine/constants"
|
||||
@@ -34,6 +36,8 @@ import (
|
||||
"github.com/psviderski/uncloud/internal/secret"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
netproxy "golang.org/x/net/proxy"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is the container image used to run socat proxy containers.
|
||||
@@ -104,6 +108,80 @@ func (cli *Client) ListImages(ctx context.Context, filter api.ImageFilter) ([]ap
|
||||
return machineImages, nil
|
||||
}
|
||||
|
||||
// RemoveImage removes one exact image name or ID on the selected machines with protection of used references.
|
||||
// Results include the outcome of removing an image on each machine.
|
||||
//
|
||||
// An absent image on some machines is normal if removal succeeds on at least one machine. If every selected machine
|
||||
// reports NotFound, the returned error is api.ErrNotFound. Other machine failures are combined, with successful
|
||||
// results still returned.
|
||||
func (cli *Client) RemoveImage(
|
||||
ctx context.Context, imageNameOrID string, opts api.RemoveImageOptions,
|
||||
) ([]api.MachineImageRemoval, error) {
|
||||
if strings.TrimSpace(imageNameOrID) == "" {
|
||||
return nil, errors.New("image name or ID is required")
|
||||
}
|
||||
|
||||
resp, err := cli.Docker.GRPCClient.RemoveImage(ProxyMachinesContext(ctx, opts.Machines),
|
||||
&pb.RemoveImageRequest{Id: imageNameOrID, PruneChildren: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pw := progress.ContextWriter(ctx)
|
||||
var results []api.MachineImageRemoval
|
||||
var errs []error
|
||||
notFoundCount := 0
|
||||
for _, msg := range resp.Messages {
|
||||
result := api.MachineImageRemoval{Metadata: msg.Metadata}
|
||||
for _, removal := range msg.Results {
|
||||
result.Results = append(result.Results, image.DeleteResponse{
|
||||
Deleted: removal.Deleted,
|
||||
Untagged: removal.Untagged,
|
||||
})
|
||||
}
|
||||
results = append(results, result)
|
||||
|
||||
eventID := cliprogress.ImageEventID(imageNameOrID, msg.Metadata.MachineName)
|
||||
if err = result.Metadata.Err(); err != nil {
|
||||
event := progress.NewEvent(eventID, progress.Done, "Not found")
|
||||
if status.Code(err) == codes.NotFound {
|
||||
notFoundCount++
|
||||
} else {
|
||||
errs = append(errs, fmt.Errorf("machine '%s': %w", msg.Metadata.MachineName, err))
|
||||
event.Status = progress.Error
|
||||
event.StatusText = status.Convert(err).Message()
|
||||
}
|
||||
pw.Event(event)
|
||||
continue
|
||||
}
|
||||
|
||||
// An unnamed image can return Deleted without Untagged. Use the requested name or ID as its row fallback.
|
||||
deleted := false
|
||||
for _, removal := range result.Results {
|
||||
if removal.Untagged != "" {
|
||||
eventID = cliprogress.ImageEventID(removal.Untagged, msg.Metadata.MachineName)
|
||||
pw.Event(progress.NewEvent(eventID, progress.Done, "Untagged"))
|
||||
}
|
||||
if removal.Deleted != "" && !deleted {
|
||||
// Docker reports the deletion of the selected image before any pruned parents or released layers.
|
||||
// Only that first deletion updates the last untagged row marking the image as Removed.
|
||||
deleted = true
|
||||
pw.Event(progress.NewEvent(eventID, progress.Done,
|
||||
fmt.Sprintf("Removed (%s)", stringid.TruncateID(removal.Deleted))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err = errors.Join(errs...); err != nil {
|
||||
return results, err
|
||||
}
|
||||
if notFoundCount == len(results) {
|
||||
return results, api.ErrNotFound
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// TODO: move PushImageOptions to api package and include PushImage in ImageClient interface.
|
||||
type PushImageOptions struct {
|
||||
// AllMachines pushes the image to all machines in the cluster. Takes precedence over Machines field.
|
||||
|
||||
Reference in New Issue
Block a user