Skip to content

Commit 56ab28a

Browse files
ibrahimyprndeloof
authored andcommitted
compose: recreate container when mounted image digest changes
Until now, mustRecreate logic only checked for divergence in TypeVolume mounts but ignored TypeImage mounts. This inconsistency caused containers to erroneously retain stale images even after the source image was rebuilt. This commit updates ensureImagesExists to resolve image volume sources to their digests using the official reference package. This enables ServiceHash (config hash) to naturally detect underlying image digest changes, triggering recreation via the standard convergence logic. An E2E test case is added to verify this behavior. Fixes #13547 Signed-off-by: ibrahim yapar <74625807+ibrahimypr@users.noreply.github.com>
1 parent e7d870a commit 56ab28a

File tree

4 files changed

+109
-0
lines changed

4 files changed

+109
-0
lines changed

pkg/compose/build.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,37 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
158158
if ok {
159159
service.CustomLabels.Add(api.ImageDigestLabel, img.ID)
160160
}
161+
162+
resolveImageVolumes(&service, images, project.Name)
163+
161164
project.Services[name] = service
162165
}
163166
return nil
164167
}
165168

169+
func resolveImageVolumes(service *types.ServiceConfig, images map[string]api.ImageSummary, projectName string) {
170+
for i, vol := range service.Volumes {
171+
if vol.Type == types.VolumeTypeImage {
172+
imgName := vol.Source
173+
if _, ok := images[vol.Source]; !ok {
174+
// check if source is another service in the project
175+
imgName = api.GetImageNameOrDefault(types.ServiceConfig{Name: vol.Source}, projectName)
176+
// If we still can't find it, it might be an external image that wasn't pulled yet or doesn't exist
177+
if _, ok := images[imgName]; !ok {
178+
continue
179+
}
180+
}
181+
if img, ok := images[imgName]; ok {
182+
// Use Image ID directly as source.
183+
// Using name@digest format (via reference.WithDigest) fails for local-only images
184+
// that don't have RepoDigests (e.g. built locally in CI).
185+
// Image ID (sha256:...) is always valid and ensures ServiceHash changes on rebuild.
186+
service.Volumes[i].Source = img.ID
187+
}
188+
}
189+
}
190+
}
191+
166192
func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]api.ImageSummary, error) {
167193
imageNames := utils.Set[string]{}
168194
for _, s := range project.Services {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# syntax=docker/dockerfile:1
2+
#
3+
# Copyright 2020 Docker Compose CLI authors
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
FROM alpine
19+
WORKDIR /app
20+
ARG CONTENT=initial
21+
RUN echo "$CONTENT" > /app/content.txt
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
services:
2+
source:
3+
build:
4+
context: .
5+
dockerfile: Dockerfile
6+
image: image-volume-source
7+
8+
consumer:
9+
image: alpine
10+
depends_on:
11+
- source
12+
command: ["cat", "/data/content.txt"]
13+
volumes:
14+
- type: image
15+
source: image-volume-source
16+
target: /data
17+
image:
18+
subpath: app

pkg/e2e/volumes_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,47 @@ func TestImageVolume(t *testing.T) {
190190
out := res.Combined()
191191
assert.Check(t, strings.Contains(out, "index.html"))
192192
}
193+
194+
func TestImageVolumeRecreateOnRebuild(t *testing.T) {
195+
c := NewCLI(t)
196+
const projectName = "compose-e2e-image-volume-recreate"
197+
t.Cleanup(func() {
198+
c.cleanupWithDown(t, projectName)
199+
c.RunDockerOrExitError(t, "rmi", "-f", "image-volume-source")
200+
})
201+
202+
version := c.RunDockerCmd(t, "version", "-f", "{{.Server.Version}}")
203+
major, _, found := strings.Cut(version.Combined(), ".")
204+
assert.Assert(t, found)
205+
if major == "26" || major == "27" {
206+
t.Skip("Skipping test due to docker version < 28")
207+
}
208+
209+
// First build and run with initial content
210+
c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
211+
"--project-name", projectName, "build", "--build-arg", "CONTENT=foo")
212+
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
213+
"--project-name", projectName, "up", "-d")
214+
assert.Check(t, !strings.Contains(res.Combined(), "error"))
215+
216+
// Check initial content
217+
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
218+
"--project-name", projectName, "logs", "consumer")
219+
assert.Check(t, strings.Contains(res.Combined(), "foo"), "Expected 'foo' in output, got: %s", res.Combined())
220+
221+
// Rebuild source image with different content
222+
c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
223+
"--project-name", projectName, "build", "--build-arg", "CONTENT=bar")
224+
225+
// Run up again - consumer should be recreated because source image changed
226+
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
227+
"--project-name", projectName, "up", "-d")
228+
// The consumer container should be recreated
229+
assert.Check(t, strings.Contains(res.Combined(), "Recreate") || strings.Contains(res.Combined(), "Created"),
230+
"Expected container to be recreated, got: %s", res.Combined())
231+
232+
// Check updated content
233+
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
234+
"--project-name", projectName, "logs", "consumer")
235+
assert.Check(t, strings.Contains(res.Combined(), "bar"), "Expected 'bar' in output after rebuild, got: %s", res.Combined())
236+
}

0 commit comments

Comments
 (0)