mirror of
https://github.com/docker/compose.git
synced 2026-10-12 02:13:08 +00:00
docs+test: disambiguate content digests from distribution digests
Compose manipulates two digest kinds that must never be conflated: the platform-specific content digest (localContentDigest) is ONLY an identity to compare a running container with a fresh build/pull, while pinning image references in a reproducible compose model (publish / config --resolve-image-digests) must keep the registry descriptor digest — the multi-platform index — through ImageDigestResolver, or the published file would be bound to the platform of whoever resolved it. Document the distinction on both producers and lock it with a test that fails if the resolver ever goes through a local image inspect. Signed-off-by: Nicolas De Loof <[email protected]>
This commit is contained in:
committed by
Guillaume Lours
parent
a2e7fe6590
commit
b32c5b9a94
@@ -264,6 +264,13 @@ func (s *composeService) canonicalBuiltDigest(ctx context.Context, imageRef, pla
|
||||
// pre-manifest behavior. Every image identity compose records for staleness
|
||||
// comparison must be computed through here, whatever the image's provenance
|
||||
// (pulled, built, already local), so any two runs produce comparable values.
|
||||
//
|
||||
// The converse also holds: this platform-specific digest is ONLY an identity
|
||||
// for staleness comparison. Pinning image references in a reproducible
|
||||
// compose model (publish / config --resolve-image-digests) must keep using
|
||||
// the registry descriptor digest — the multi-platform index — through
|
||||
// ImageDigestResolver, or the published file would be bound to the platform
|
||||
// of whoever resolved it.
|
||||
func localContentDigest(inspect client.ImageInspectResult, platform string) (string, bool, error) {
|
||||
var matcher platforms.Matcher = platforms.Default()
|
||||
pinned := platform != ""
|
||||
|
||||
+11
-1
@@ -335,7 +335,17 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImageDigestResolver creates a func able to resolve image digest from a docker ref,
|
||||
// ImageDigestResolver creates a func able to resolve image digest from a
|
||||
// docker ref, for pinning image references in a reproducible compose model
|
||||
// (`compose publish` / `config --resolve-image-digests`).
|
||||
//
|
||||
// It deliberately returns the registry descriptor digest — the multi-platform
|
||||
// index digest for multi-arch images — via DistributionInspect: a published
|
||||
// compose file must stay deployable on any platform. This is NOT the same
|
||||
// digest kind as localContentDigest, which selects the platform-specific
|
||||
// runnable manifest to compare a running container with a fresh build/pull;
|
||||
// never funnel this resolution through the local content-digest producer, and
|
||||
// never pin a published reference with a per-platform digest.
|
||||
func ImageDigestResolver(ctx context.Context, file *configfile.ConfigFile, apiClient client.APIClient) func(named reference.Named) (digest.Digest, error) {
|
||||
return func(named reference.Named) (digest.Digest, error) {
|
||||
auth, err := encodedAuth(named, file)
|
||||
|
||||
@@ -25,13 +25,19 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/moby/moby/api/types/image"
|
||||
"github.com/moby/moby/api/types/jsonstream"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"go.uber.org/mock/gomock"
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/mocks"
|
||||
)
|
||||
|
||||
// scheduledHookImages runs addPreStartHookPulls and returns the hook image
|
||||
@@ -294,3 +300,31 @@ func TestShouldPullImageProvider(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, !pull, "provider service without image has nothing to pull")
|
||||
}
|
||||
|
||||
// TestImageDigestResolverUsesDistributionDigest locks the distinction between
|
||||
// the two digest kinds compose manipulates: pinning a reference for a
|
||||
// reproducible compose model (publish / config --resolve-image-digests) must
|
||||
// use the registry descriptor digest — the multi-platform index — and never
|
||||
// the local platform-specific content digest used for staleness comparison.
|
||||
// The mock declares no ImageInspect expectation, so any attempt to resolve
|
||||
// through the local content-digest producer fails the test.
|
||||
func TestImageDigestResolverUsesDistributionDigest(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
mockAPI := mocks.NewMockAPIClient(mockCtrl)
|
||||
|
||||
const indexDigest = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
mockAPI.EXPECT().
|
||||
DistributionInspect(gomock.Any(), "docker.io/library/foo:1", gomock.Any()).
|
||||
Return(client.DistributionInspectResult{
|
||||
DistributionInspect: registrytypes.DistributionInspect{
|
||||
Descriptor: ocispec.Descriptor{Digest: digest.Digest(indexDigest)},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
named, err := reference.ParseDockerRef("foo:1")
|
||||
assert.NilError(t, err)
|
||||
resolved, err := ImageDigestResolver(t.Context(), &configfile.ConfigFile{}, mockAPI)(named)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, resolved.String(), indexDigest)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user