mirror of
https://github.com/docker/compose.git
synced 2026-10-12 02:13:08 +00:00
fix(build): resolve image volumes to a mountable name, not a manifest digest
With the containerd image store, ImageSummary.ID holds the digest of the platform-specific manifest so ServiceHash stays stable across attested rebuilds (see contentDigest). resolveImageVolumes reused that same value as the `type: image` mount Source, but the daemon only resolves a mount Source by name/tag or top-level image ID, not by manifest digest — so `compose up` failed with "No such image" whenever the volume's source image was already present locally (always for a built image; on a second run for a pulled one). Keep Source as the resolved image name, and track the digest separately via a new com.docker.compose.image-volume-digest label so mustRecreate can still detect a rebuilt/updated source image independently of Source. Fixes #14005 Signed-off-by: Ricardo Branco <[email protected]> Signed-off-by: Guillaume Lours <[email protected]>
This commit is contained in:
committed by
Guillaume Lours
parent
d4fe2aead5
commit
1615bf352f
@@ -47,6 +47,11 @@ const (
|
||||
SlugLabel = "com.docker.compose.slug"
|
||||
// ImageDigestLabel stores digest of the container image used to run service
|
||||
ImageDigestLabel = "com.docker.compose.image"
|
||||
// ImageVolumeDigestLabel stores the content digest of each `type: image`
|
||||
// volume's source image, as "target=digest" pairs joined by ",", so
|
||||
// mustRecreate can detect a rebuilt/updated source image independently of
|
||||
// the mount Source (which must stay a resolvable name, not a digest).
|
||||
ImageVolumeDigestLabel = "com.docker.compose.image-volume-digest"
|
||||
// DependenciesLabel stores service dependencies
|
||||
DependenciesLabel = "com.docker.compose.depends_on"
|
||||
// VersionLabel stores the compose tool version used to build/run application
|
||||
|
||||
+28
-16
@@ -19,6 +19,7 @@ package compose
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -167,25 +168,36 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
|
||||
}
|
||||
|
||||
func resolveImageVolumes(service *types.ServiceConfig, images map[string]api.ImageSummary, projectName string) {
|
||||
var digests []string
|
||||
for i, vol := range service.Volumes {
|
||||
if vol.Type == types.VolumeTypeImage {
|
||||
imgName := vol.Source
|
||||
if _, ok := images[vol.Source]; !ok {
|
||||
// check if source is another service in the project
|
||||
imgName = api.GetImageNameOrDefault(types.ServiceConfig{Name: vol.Source}, projectName)
|
||||
// If we still can't find it, it might be an external image that wasn't pulled yet or doesn't exist
|
||||
if _, ok := images[imgName]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if img, ok := images[imgName]; ok {
|
||||
// Use Image ID directly as source.
|
||||
// Using name@digest format (via reference.WithDigest) fails for local-only images
|
||||
// that don't have RepoDigests (e.g. built locally in CI).
|
||||
// Image ID (sha256:...) is always valid and ensures ServiceHash changes on rebuild.
|
||||
service.Volumes[i].Source = img.ID
|
||||
if vol.Type != types.VolumeTypeImage {
|
||||
continue
|
||||
}
|
||||
imgName := vol.Source
|
||||
if _, ok := images[vol.Source]; !ok {
|
||||
// check if source is another service in the project
|
||||
imgName = api.GetImageNameOrDefault(types.ServiceConfig{Name: vol.Source}, projectName)
|
||||
// If we still can't find it, it might be an external image that wasn't pulled yet or doesn't exist
|
||||
if _, ok := images[imgName]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
img, ok := images[imgName]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// The daemon only resolves a `type=image` mount Source that is a name/tag
|
||||
// or a top-level image ID, not a per-platform manifest digest (which is
|
||||
// what ImageSummary.ID holds to stay stable across attested rebuilds, see
|
||||
// contentDigest()). Keep Source as the resolved name so mounting always
|
||||
// works, and track the digest separately so mustRecreate can still detect
|
||||
// a changed source image.
|
||||
service.Volumes[i].Source = imgName
|
||||
digests = append(digests, vol.Target+"="+img.ID)
|
||||
}
|
||||
if len(digests) > 0 {
|
||||
sort.Strings(digests)
|
||||
service.CustomLabels.Add(api.ImageVolumeDigestLabel, strings.Join(digests, ","))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,12 +103,13 @@ func (s *ObservedState) selectVolume(key, desiredName string) (ObservedVolume, [
|
||||
// ObservedContainer holds the relevant state extracted from a running or stopped
|
||||
// container, with label values pre-parsed for efficient comparison.
|
||||
type ObservedContainer struct {
|
||||
ID string
|
||||
Name string
|
||||
State container.ContainerState // "running", "exited", "created", "restarting", etc.
|
||||
ConfigHash string // label com.docker.compose.config-hash
|
||||
ImageDigest string // label com.docker.compose.image
|
||||
Number int // label com.docker.compose.container-number
|
||||
ID string
|
||||
Name string
|
||||
State container.ContainerState // "running", "exited", "created", "restarting", etc.
|
||||
ConfigHash string // label com.docker.compose.config-hash
|
||||
ImageDigest string // label com.docker.compose.image
|
||||
ImageVolumeDigest string // label com.docker.compose.image-volume-digest
|
||||
Number int // label com.docker.compose.container-number
|
||||
|
||||
// ConnectedNetworks maps network IDs found in the container's network
|
||||
// settings. Key is the network name as seen by Docker, value is the
|
||||
@@ -333,6 +334,7 @@ func toObservedContainer(c container.Summary) ObservedContainer {
|
||||
State: c.State,
|
||||
ConfigHash: c.Labels[api.ConfigHashLabel],
|
||||
ImageDigest: c.Labels[api.ImageDigestLabel],
|
||||
ImageVolumeDigest: c.Labels[api.ImageVolumeDigestLabel],
|
||||
Number: number,
|
||||
ConnectedNetworks: networks,
|
||||
Summary: c,
|
||||
|
||||
@@ -770,6 +770,9 @@ func (r *reconciler) mustRecreate(expected types.ServiceConfig, expectedHash str
|
||||
if oc.ImageDigest != expected.CustomLabels[api.ImageDigestLabel] {
|
||||
return true
|
||||
}
|
||||
if oc.ImageVolumeDigest != expected.CustomLabels[api.ImageVolumeDigestLabel] {
|
||||
return true
|
||||
}
|
||||
if oc.State == container.StateRunning && r.hasNetworkMismatch(expected, oc) {
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user