Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pkg/api/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 28 additions & 16 deletions pkg/compose/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package compose
import (
"context"
"fmt"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -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, ","))
}
}

Expand Down
14 changes: 8 additions & 6 deletions pkg/compose/observed_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions pkg/compose/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down