mirror of
https://github.com/docker/compose.git
synced 2026-02-09 01:59:22 +08:00
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>
This commit is contained in:
committed by
Nicolas De loof
parent
e7d870a106
commit
56ab28aef3
@@ -158,11 +158,37 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
|
||||
if ok {
|
||||
service.CustomLabels.Add(api.ImageDigestLabel, img.ID)
|
||||
}
|
||||
|
||||
resolveImageVolumes(&service, images, project.Name)
|
||||
|
||||
project.Services[name] = service
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveImageVolumes(service *types.ServiceConfig, images map[string]api.ImageSummary, projectName 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]api.ImageSummary, error) {
|
||||
imageNames := utils.Set[string]{}
|
||||
for _, s := range project.Services {
|
||||
|
||||
21
pkg/e2e/fixtures/image-volume-recreate/Dockerfile
Normal file
21
pkg/e2e/fixtures/image-volume-recreate/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
#
|
||||
# Copyright 2020 Docker Compose CLI authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
FROM alpine
|
||||
WORKDIR /app
|
||||
ARG CONTENT=initial
|
||||
RUN echo "$CONTENT" > /app/content.txt
|
||||
18
pkg/e2e/fixtures/image-volume-recreate/compose.yaml
Normal file
18
pkg/e2e/fixtures/image-volume-recreate/compose.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
services:
|
||||
source:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: image-volume-source
|
||||
|
||||
consumer:
|
||||
image: alpine
|
||||
depends_on:
|
||||
- source
|
||||
command: ["cat", "/data/content.txt"]
|
||||
volumes:
|
||||
- type: image
|
||||
source: image-volume-source
|
||||
target: /data
|
||||
image:
|
||||
subpath: app
|
||||
@@ -190,3 +190,47 @@ func TestImageVolume(t *testing.T) {
|
||||
out := res.Combined()
|
||||
assert.Check(t, strings.Contains(out, "index.html"))
|
||||
}
|
||||
|
||||
func TestImageVolumeRecreateOnRebuild(t *testing.T) {
|
||||
c := NewCLI(t)
|
||||
const projectName = "compose-e2e-image-volume-recreate"
|
||||
t.Cleanup(func() {
|
||||
c.cleanupWithDown(t, projectName)
|
||||
c.RunDockerOrExitError(t, "rmi", "-f", "image-volume-source")
|
||||
})
|
||||
|
||||
version := c.RunDockerCmd(t, "version", "-f", "{{.Server.Version}}")
|
||||
major, _, found := strings.Cut(version.Combined(), ".")
|
||||
assert.Assert(t, found)
|
||||
if major == "26" || major == "27" {
|
||||
t.Skip("Skipping test due to docker version < 28")
|
||||
}
|
||||
|
||||
// First build and run with initial content
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
|
||||
"--project-name", projectName, "build", "--build-arg", "CONTENT=foo")
|
||||
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
|
||||
"--project-name", projectName, "up", "-d")
|
||||
assert.Check(t, !strings.Contains(res.Combined(), "error"))
|
||||
|
||||
// Check initial content
|
||||
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
|
||||
"--project-name", projectName, "logs", "consumer")
|
||||
assert.Check(t, strings.Contains(res.Combined(), "foo"), "Expected 'foo' in output, got: %s", res.Combined())
|
||||
|
||||
// Rebuild source image with different content
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
|
||||
"--project-name", projectName, "build", "--build-arg", "CONTENT=bar")
|
||||
|
||||
// Run up again - consumer should be recreated because source image changed
|
||||
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
|
||||
"--project-name", projectName, "up", "-d")
|
||||
// The consumer container should be recreated
|
||||
assert.Check(t, strings.Contains(res.Combined(), "Recreate") || strings.Contains(res.Combined(), "Created"),
|
||||
"Expected container to be recreated, got: %s", res.Combined())
|
||||
|
||||
// Check updated content
|
||||
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
|
||||
"--project-name", projectName, "logs", "consumer")
|
||||
assert.Check(t, strings.Contains(res.Combined(), "bar"), "Expected 'bar' in output after rebuild, got: %s", res.Combined())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user