Skip to content
Merged
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: 1 addition & 2 deletions lib/images/credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -203,7 +202,7 @@ func TestRecoverInterruptedCredentialedPullFailsForFreshRetry(t *testing.T) {
assert.Equal(t, ErrBorrowedCredentialsExpired.Error(), *stored.Error)
assert.Zero(t, m.queue.QueueLength())

data, err := os.ReadFile(filepath.Join(p.ImageDigestDir(repository, strings.TrimPrefix(digest, "sha256:")), "metadata.json"))
data, err := os.ReadFile(metadataPath(p, repository, strings.TrimPrefix(digest, "sha256:")))
require.NoError(t, err)
assert.NotContains(t, string(data), "password")
}
25 changes: 19 additions & 6 deletions lib/images/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,12 @@ func TestCreateImage(t *testing.T) {
require.NoError(t, err)
require.NotEqual(t, 0, linkStat.Mode()&os.ModeSymlink, "should be a symlink")

// Verify symlink points to digest directory
// Verify symlink resolves to the expected digest in either layout.
linkTarget, err := os.Readlink(linkPath)
require.NoError(t, err)
require.Equal(t, digestHex, linkTarget, "symlink should point to digest")
resolvedDigest, err := resolveTag(paths.New(dataDir), ref.Repository(), ref.Tag())
require.NoError(t, err)
require.Equal(t, digestHex, resolvedDigest, "symlink should resolve to the image digest")
t.Logf("Tag symlink: %s -> %s", linkPath, linkTarget)
}

Expand Down Expand Up @@ -213,6 +215,13 @@ func TestTagImage(t *testing.T) {
aliased, err := mgr.TagImage(ctx, digestRef, "docker.io/library/busybox:stable")
require.NoError(t, err)
require.Equal(t, "docker.io/library/busybox:stable", aliased.Name)
digestHex := strings.TrimPrefix(got.Digest, "sha256:")
require.FileExists(t, p.ImageContentPath(digestHex))
require.FileExists(t, p.ImageContentMetadata(digestHex))
_, err = os.Lstat(p.ImageRepositoryTagSymlink("docker.io/library/busybox", "stable"))
require.NoError(t, err)
_, err = os.Stat(p.ImageDigestDir(repo, digestHex))
require.ErrorIs(t, err, os.ErrNotExist)
resolvedAlias, err := mgr.GetImage(ctx, "docker.io/library/busybox:stable")
require.NoError(t, err)
require.Equal(t, "docker.io/library/busybox:stable", resolvedAlias.Name)
Expand All @@ -224,6 +233,13 @@ func TestTagImage(t *testing.T) {
require.Equal(t, "docker.io/library/busybox:stable", resolvedAlias.Name)
require.Equal(t, got.Digest, resolvedAlias.Digest)

require.NoError(t, mgr.DeleteImage(ctx, "docker.io/library/busybox:stable"))
_, err = os.Stat(p.ImageContentDir(digestHex))
require.NoError(t, err)
require.NoError(t, mgr.DeleteImage(ctx, digestRef))
_, err = os.Stat(p.ImageContentDir(digestHex))
require.ErrorIs(t, err, os.ErrNotExist)

_, err = mgr.TagImage(ctx, digestRef, repo+"@sha256:029a752048e32e843bd6defe3841186fb8d19a28dae8ec287f433bb9d6d1ad85")
require.ErrorIs(t, err, ErrInvalidName)
}
Expand All @@ -243,11 +259,8 @@ func TestDigestOnlyContentSurvivesAliasDeletion(t *testing.T) {
CreatedAt: time.Now().UTC(),
}))
require.NoError(t, os.WriteFile(p.ImageContentPath(digest), []byte("rootfs"), 0o644))
linkPath := p.ImageRepositoryTagSymlink("registry.example.com/myapp", "v1")
require.NoError(t, os.MkdirAll(filepath.Dir(linkPath), 0o755))
target, err := filepath.Rel(filepath.Dir(linkPath), p.ImageContentDir(digest))
_, err = mgr.TagImage(context.Background(), repository+"@sha256:"+digest, "registry.example.com/myapp:v1")
require.NoError(t, err)
require.NoError(t, os.Symlink(target, linkPath))

require.NoError(t, mgr.DeleteImage(context.Background(), "registry.example.com/myapp:v1"))
_, err = mgr.GetImage(context.Background(), repository+"@sha256:"+digest)
Expand Down
31 changes: 22 additions & 9 deletions lib/images/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,9 @@ func (m *imageMetadata) toImage() *Image {
return img
}

// contentLayoutEnabled is intentionally disabled in the compatibility layer.
// Existing content-layout images remain writable; the child PR enables the
// layout for new images after these readers are deployed.
const contentLayoutEnabled = false
Comment thread
cursor[bot] marked this conversation as resolved.
// The compatibility readers are in the parent PR; this child enables new
// content-layout writes after those readers are deployed.
const contentLayoutEnabled = true

// legacyDigestDir returns the directory used by the original image layout.
func legacyDigestDir(p *paths.Paths, repository, digestHex string) string {
Expand All @@ -106,9 +105,19 @@ func contentDigestPath(p *paths.Paths, digestHex string) string {
return p.ImageContentPath(digestHex)
}

// digestPath returns the path to the rootfs disk file for a digest, preferring
// the new content-addressed layout and falling back to the legacy layout.
func legacyImageExists(p *paths.Paths, repository, digestHex string) bool {
_, metadataErr := os.Stat(p.ImageMetadata(repository, digestHex))
_, diskErr := os.Stat(p.ImageDigestPath(repository, digestHex))
return metadataErr == nil && diskErr == nil
}

// digestPath returns the path to the rootfs disk file for a digest. A complete
// legacy image takes precedence when both layouts contain the digest so a
// pending or failed content record from another repository cannot shadow it.
func digestPath(p *paths.Paths, repository, digestHex string) string {
if legacyImageExists(p, repository, digestHex) {
return p.ImageDigestPath(repository, digestHex)
}
Comment thread
cursor[bot] marked this conversation as resolved.
contentPath := p.ImageContentPath(digestHex)
if _, err := os.Stat(contentPath); err == nil {
return contentPath
Expand All @@ -130,9 +139,13 @@ func GetDiskPath(p *paths.Paths, imageName string, digest string) (string, error
return digestPath(p, ref.Repository(), digestHex), nil
}

// metadataPath returns the path to metadata.json for a digest, preferring the
// new content-addressed layout and falling back to the legacy layout.
// metadataPath returns the path to metadata.json for a digest. A complete
// legacy image takes precedence when both layouts contain the digest so a
// pending or failed content record from another repository cannot shadow it.
func metadataPath(p *paths.Paths, repository, digestHex string) string {
if legacyImageExists(p, repository, digestHex) {
return p.ImageMetadata(repository, digestHex)
}
contentPath := p.ImageContentMetadata(digestHex)
if _, err := os.Stat(contentPath); err == nil {
return contentPath
Expand Down Expand Up @@ -300,7 +313,7 @@ func cloneReadyImageLegacy(p *paths.Paths, sourceRepository, targetRepository, d
}

func cloneReadyImage(p *paths.Paths, sourceRepository, targetRepository, digestHex string, sourceMeta *imageMetadata, targetName string) error {
if !contentLayoutEnabled {
if !contentLayoutEnabled && usesLegacyLayout(p, sourceRepository, digestHex) {
return cloneReadyImageLegacy(p, sourceRepository, targetRepository, digestHex, sourceMeta, targetName)
}
return cloneReadyImageContent(p, sourceRepository, targetRepository, digestHex, sourceMeta, targetName)
Expand Down
40 changes: 40 additions & 0 deletions lib/images/storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,46 @@ import (
"github.com/stretchr/testify/require"
)

func TestLegacyImageIsNotShadowedByContentMetadata(t *testing.T) {
p := paths.New(t.TempDir())
repository := "docker.io/library/alpine"
digest := "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
legacyMeta := &imageMetadata{
Name: repository + ":latest",
Digest: "sha256:" + digest,
Status: StatusReady,
}
legacyDir := p.ImageDigestDir(repository, digest)
require.NoError(t, os.MkdirAll(legacyDir, 0o755))
require.NoError(t, writeMetadataFile(p.ImageMetadata(repository, digest), legacyMeta))
require.NoError(t, os.WriteFile(p.ImageDigestPath(repository, digest), []byte("legacy rootfs"), 0o644))
require.NoError(t, writeMetadataFile(p.ImageContentMetadata(digest), &imageMetadata{
Name: "docker.io/library/busybox:latest",
Digest: "sha256:" + digest,
Status: StatusFailed,
}))

meta, err := readMetadata(p, repository, digest)
require.NoError(t, err)
require.Equal(t, StatusReady, meta.Status)
require.Equal(t, p.ImageDigestPath(repository, digest), digestPath(p, repository, digest))
}

func TestWriteMetadataUsesContentWhenLegacyDirectoryIsEmpty(t *testing.T) {
p := paths.New(t.TempDir())
repository := "docker.io/library/alpine"
digest := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
require.NoError(t, os.MkdirAll(p.ImageDigestDir(repository, digest), 0o755))
require.NoError(t, writeMetadata(p, repository, digest, &imageMetadata{
Name: repository + ":latest",
Digest: "sha256:" + digest,
Status: StatusPending,
}))
require.FileExists(t, p.ImageContentMetadata(digest))
_, err := os.Stat(p.ImageMetadata(repository, digest))
require.ErrorIs(t, err, os.ErrNotExist)
}

func TestListAllMetadataContentLayout(t *testing.T) {
p := paths.New(t.TempDir())
digest := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
Expand Down
9 changes: 5 additions & 4 deletions lib/scopes/scopes.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,11 @@ var RouteScopes = map[string]Scope{
"POST /resources/memory/reclaim": ResourceWrite,

// Images
"GET /images": ImageRead,
"POST /images": ImageWrite,
"DELETE /images/{name}": ImageDelete,
"GET /images/{name}": ImageRead,
"GET /images": ImageRead,
"POST /images": ImageWrite,
"POST /images/{name}/tag": ImageWrite,
"DELETE /images/{name}": ImageDelete,
"GET /images/{name}": ImageRead,

// Pushes (outbound image pushes to remote registries)
"POST /pushes": ImageWrite,
Expand Down
Loading