mirror of
https://github.com/docker/compose.git
synced 2026-10-12 02:13:08 +00:00
Resolve pre_start hook images alongside service images
pre_start hooks run as ephemeral init containers with their own image (ServiceHook.Image), but that image was ignored by image resolution: `config --images` didn't list it, `pull` didn't fetch it, and `up` failed at runtime with "No such image" when it wasn't already present locally. Add a GetDependentImages helper that returns a service's pre_start hook images, and use it wherever service images are collected/pulled: getLocalImagesDigests, pullRequiredImages (up path), the pull command, and `config --images`. Hook images inherit the parent service pull policy. post_start/pre_stop hooks run via ExecCreate inside the service container and never use hook.Image, so they are intentionally out of scope. Digest resolution/locking (--resolve-image-digests / --lock-image-digests) is not covered: compose-go's WithImagesResolved only resolves service.Image (needs an upstream change), and the --lock-image-digests override merges pre_start by concatenation, which would duplicate hooks. Signed-off-by: Nicolas De Loof <[email protected]>
This commit is contained in:
committed by
Guillaume Lours
parent
1c7930e91a
commit
c9266e6297
@@ -636,6 +636,9 @@ func runConfigImages(ctx context.Context, dockerCli command.Cli, opts configOpti
|
||||
|
||||
for _, s := range project.Services {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), api.GetImageNameOrDefault(s, project.Name))
|
||||
for _, img := range api.GetDependentImages(s) {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), img)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -760,3 +760,17 @@ func GetImageNameOrDefault(service types.ServiceConfig, projectName string) stri
|
||||
}
|
||||
return imageName
|
||||
}
|
||||
|
||||
// GetDependentImages returns the additional images a service depends on beyond
|
||||
// its main image. Currently this is the set of pre_start hook images, which run
|
||||
// as ephemeral init containers with their own image (an empty hook image falls
|
||||
// back to the service image, which is already accounted for elsewhere).
|
||||
func GetDependentImages(service types.ServiceConfig) []string {
|
||||
var images []string
|
||||
for _, hook := range service.PreStart {
|
||||
if hook.Image != "" {
|
||||
images = append(images, hook.Image)
|
||||
}
|
||||
}
|
||||
return images
|
||||
}
|
||||
|
||||
@@ -36,3 +36,52 @@ func TestRunOptionsEnvironmentMap(t *testing.T) {
|
||||
assert.Equal(t, *env["ZOT"], "")
|
||||
assert.Check(t, env["QIX"] == nil)
|
||||
}
|
||||
|
||||
func TestGetDependentImages(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
service types.ServiceConfig
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "no hooks",
|
||||
service: types.ServiceConfig{Image: "alpine:3.20"},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "pre_start hook with explicit image",
|
||||
service: types.ServiceConfig{
|
||||
Image: "alpine:3.20",
|
||||
PreStart: []types.ServiceHook{
|
||||
{Image: "alpine:3.19", Command: types.ShellCommand{"echo", "init"}},
|
||||
},
|
||||
},
|
||||
expected: []string{"alpine:3.19"},
|
||||
},
|
||||
{
|
||||
name: "pre_start hook without image is ignored",
|
||||
service: types.ServiceConfig{
|
||||
Image: "alpine:3.20",
|
||||
PreStart: []types.ServiceHook{
|
||||
{Image: "busybox", Command: types.ShellCommand{"echo", "a"}},
|
||||
{Command: types.ShellCommand{"echo", "b"}},
|
||||
},
|
||||
},
|
||||
expected: []string{"busybox"},
|
||||
},
|
||||
{
|
||||
name: "post_start and pre_stop hooks are not collected",
|
||||
service: types.ServiceConfig{
|
||||
Image: "alpine:3.20",
|
||||
PostStart: []types.ServiceHook{{Image: "ignored:post", Command: types.ShellCommand{"echo"}}},
|
||||
PreStop: []types.ServiceHook{{Image: "ignored:stop", Command: types.ShellCommand{"echo"}}},
|
||||
},
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.DeepEqual(t, GetDependentImages(tt.service), tt.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +198,9 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
|
||||
imageNames.Add(volume.Source)
|
||||
}
|
||||
}
|
||||
for _, img := range api.GetDependentImages(s) {
|
||||
imageNames.Add(img)
|
||||
}
|
||||
}
|
||||
imgs, err := s.getImageSummaries(ctx, imageNames.Elements())
|
||||
if err != nil {
|
||||
|
||||
@@ -21,6 +21,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/moby/moby/api/types/image"
|
||||
"github.com/moby/moby/client"
|
||||
"go.uber.org/mock/gomock"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@@ -107,3 +110,32 @@ func Test_addBuildDependencies(t *testing.T) {
|
||||
slices.Sort(expected)
|
||||
assert.DeepEqual(t, services, expected)
|
||||
}
|
||||
|
||||
// TestGetLocalImagesDigests_PreStartHook ensures pre_start hook images are
|
||||
// inspected alongside the service image so they get resolved (see issue #13924).
|
||||
func TestGetLocalImagesDigests_PreStartHook(t *testing.T) {
|
||||
tested, apiClient := newPreStartTestService(t)
|
||||
|
||||
project := &types.Project{
|
||||
Name: "demo",
|
||||
Services: types.Services{
|
||||
"web": types.ServiceConfig{
|
||||
Name: "web",
|
||||
Image: "alpine:3.20",
|
||||
PreStart: []types.ServiceHook{
|
||||
{Image: "alpine:3.19", Command: types.ShellCommand{"echo", "init"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
apiClient.EXPECT().ImageInspect(gomock.Any(), "alpine:3.20").
|
||||
Return(client.ImageInspectResult{InspectResponse: image.InspectResponse{ID: "sha256:service"}}, nil)
|
||||
apiClient.EXPECT().ImageInspect(gomock.Any(), "alpine:3.19").
|
||||
Return(client.ImageInspectResult{InspectResponse: image.InspectResponse{ID: "sha256:hook"}}, nil)
|
||||
|
||||
images, err := tested.getLocalImagesDigests(t.Context(), project)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, images["alpine:3.20"].ID, "sha256:service")
|
||||
assert.Equal(t, images["alpine:3.19"].ID, "sha256:hook")
|
||||
}
|
||||
|
||||
@@ -135,6 +135,42 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
|
||||
i++
|
||||
}
|
||||
|
||||
// pre_start hook images run as ephemeral init containers with their own
|
||||
// image, so they must be pulled too. They have no pull policy of their own,
|
||||
// so we inherit the parent service's policy for skip decisions.
|
||||
for name, service := range project.Services {
|
||||
if service.PullPolicy == types.PullPolicyNever || service.PullPolicy == types.PullPolicyBuild {
|
||||
continue
|
||||
}
|
||||
for _, img := range api.GetDependentImages(service) {
|
||||
switch service.PullPolicy {
|
||||
case types.PullPolicyMissing, types.PullPolicyIfNotPresent:
|
||||
if imageAlreadyPresent(img, images) {
|
||||
s.events.On(api.Resource{
|
||||
ID: "Image " + img,
|
||||
Status: api.Done,
|
||||
Text: "Skipped",
|
||||
Details: "Image is already present locally",
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
if _, ok := imagesBeingPulled[img]; ok {
|
||||
continue
|
||||
}
|
||||
imagesBeingPulled[img] = name
|
||||
hookService := types.ServiceConfig{Name: name, Image: img}
|
||||
eg.Go(func() error {
|
||||
_, err := s.pullServiceImage(ctx, hookService, opts.Quiet, project.Environment["DOCKER_DEFAULT_PLATFORM"])
|
||||
if err != nil && !opts.IgnoreFailures {
|
||||
// fail fast: a hook image can't be built as a fallback
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
err = eg.Wait()
|
||||
|
||||
if len(mustBuild) > 0 {
|
||||
@@ -313,6 +349,16 @@ func (s *composeService) pullRequiredImages(ctx context.Context, project *types.
|
||||
}
|
||||
}
|
||||
|
||||
for i, img := range api.GetDependentImages(service) {
|
||||
if _, ok := images[img]; !ok {
|
||||
// Hack: create a fake ServiceConfig so we pull missing pre_start hook image
|
||||
n := fmt.Sprintf("%s:pre_start %d", name, i)
|
||||
needPull[n] = types.ServiceConfig{
|
||||
Name: n,
|
||||
Image: img,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(needPull) == 0 {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user