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
3 changes: 3 additions & 0 deletions AI_AGENT_DISCLOSURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*This contribution was prepared by an AI agent acting on a human's behalf. The human submitter may not have independently reviewed or tested the change.*

2026-08-13
19 changes: 17 additions & 2 deletions pkg/compose/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,11 +286,26 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser
}
s.events.On(newEvent(resource, api.Done, api.StatusPulled))

inspected, err := s.apiClient().ImageInspect(ctx, service.Image)
return s.pulledImageDigest(ctx, service.Image)
}

// pulledImageDigest resolves a pulled image through the same runnable-content
// digest path used to discover images that are already local.
func (s *composeService) pulledImageDigest(ctx context.Context, imageName string) (string, error) {
// Use the same runnable-content digest as the local-image discovery path.
// With the containerd image store, inspect.ID can identify the top-level
// index while getImageSummaries selects the platform image manifest. Mixing
// those digest types makes a container created immediately after a pull look
// stale on the next `up`.
summaries, err := s.getImageSummaries(ctx, []string{imageName})
if err != nil {
return "", err
}
return inspected.ID, nil
summary, ok := summaries[imageName]
if !ok {
return "", fmt.Errorf("unable to inspect image '%s' after pull", imageName)
}
return summary.ID, nil
}

// ImageDigestResolver creates a func able to resolve image digest from a docker ref,
Expand Down
55 changes: 55 additions & 0 deletions pkg/compose/pull_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,70 @@
package compose

import (
"context"
"io"
"iter"
"sort"
"strings"
"testing"

"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/config/configfile"
imagetypes "github.com/moby/moby/api/types/image"
"github.com/moby/moby/api/types/jsonstream"
"github.com/moby/moby/client"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"

"github.com/docker/compose/v5/pkg/api"
)

type testImagePullResponse struct {
io.ReadCloser
}

func (r testImagePullResponse) JSONMessages(context.Context) iter.Seq2[jsonstream.Message, error] {
return func(yield func(jsonstream.Message, error) bool) {}
}

func (r testImagePullResponse) Wait(context.Context) error {
return nil
}

func TestPullServiceImageUsesRunnableContentDigest(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
dockerAPI, cli := prepareMocks(mockCtrl)
cli.EXPECT().ConfigFile().Return(configfile.New(""))
tested, err := NewComposeService(cli)
assert.NilError(t, err)

dockerAPI.EXPECT().ImagePull(gomock.Any(), "foo:1", gomock.Any()).Return(testImagePullResponse{
ReadCloser: io.NopCloser(strings.NewReader("")),
}, nil)
dockerAPI.EXPECT().Ping(gomock.Any(), client.PingOptions{NegotiateAPIVersion: true}).
Return(client.PingResult{APIVersion: "1.48"}, nil)
dockerAPI.EXPECT().ClientVersion().Return("1.48")
dockerAPI.EXPECT().ImageInspect(gomock.Any(), "foo:1", gomock.Any()).Return(client.ImageInspectResult{
InspectResponse: imagetypes.InspectResponse{
ID: "sha256:index",
Manifests: []imagetypes.ManifestSummary{{
ID: "sha256:image",
Kind: imagetypes.ManifestKindImage,
Available: true,
ImageData: &imagetypes.ImageProperties{
Platform: specs.Platform{OS: "linux", Architecture: "amd64"},
},
}},
},
}, nil)

id, err := tested.(*composeService).pullServiceImage(t.Context(), types.ServiceConfig{Image: "foo:1"}, true, "")
assert.NilError(t, err)
assert.Equal(t, id, "sha256:image")
}

// scheduledHookImages runs addPreStartHookPulls and returns the hook image
// references it scheduled for pull, sorted for deterministic assertions.
func scheduledHookImages(t *testing.T, project *types.Project, present map[string]api.ImageSummary) []string {
Expand Down