Browse code

Move image to daemon/internal/image

Signed-off-by: Derek McGowan <derek@mcg.dev>

Derek McGowan authored on 2025/07/25 04:11:02
Showing 104 changed files
... ...
@@ -9,7 +9,7 @@ import (
9 9
 	"github.com/docker/docker/daemon/builder"
10 10
 	daemonevents "github.com/docker/docker/daemon/events"
11 11
 	buildkit "github.com/docker/docker/daemon/internal/builder-next"
12
-	"github.com/docker/docker/image"
12
+	"github.com/docker/docker/daemon/internal/image"
13 13
 	"github.com/docker/docker/pkg/stringid"
14 14
 	"github.com/moby/moby/api/types/backend"
15 15
 	"github.com/moby/moby/api/types/build"
... ...
@@ -6,7 +6,7 @@ import (
6 6
 	"io"
7 7
 
8 8
 	"github.com/distribution/reference"
9
-	"github.com/docker/docker/image"
9
+	"github.com/docker/docker/daemon/internal/image"
10 10
 	"github.com/pkg/errors"
11 11
 )
12 12
 
... ...
@@ -8,8 +8,8 @@ import (
8 8
 	"context"
9 9
 	"io"
10 10
 
11
+	"github.com/docker/docker/daemon/internal/image"
11 12
 	"github.com/docker/docker/daemon/internal/layer"
12
-	"github.com/docker/docker/image"
13 13
 	"github.com/moby/moby/api/types/backend"
14 14
 	"github.com/moby/moby/api/types/container"
15 15
 	"github.com/opencontainers/go-digest"
... ...
@@ -17,8 +17,8 @@ import (
17 17
 
18 18
 	"github.com/containerd/platforms"
19 19
 	"github.com/docker/docker/daemon/builder"
20
+	"github.com/docker/docker/daemon/internal/image"
20 21
 	"github.com/docker/docker/errdefs"
21
-	"github.com/docker/docker/image"
22 22
 	"github.com/docker/docker/pkg/jsonmessage"
23 23
 	"github.com/docker/go-connections/nat"
24 24
 	"github.com/moby/buildkit/frontend/dockerfile/instructions"
... ...
@@ -9,7 +9,7 @@ import (
9 9
 	"testing"
10 10
 
11 11
 	"github.com/docker/docker/daemon/builder"
12
-	"github.com/docker/docker/image"
12
+	"github.com/docker/docker/daemon/internal/image"
13 13
 	"github.com/docker/docker/oci"
14 14
 	"github.com/docker/go-connections/nat"
15 15
 	"github.com/moby/buildkit/frontend/dockerfile/instructions"
... ...
@@ -26,8 +26,8 @@ import (
26 26
 	"strings"
27 27
 
28 28
 	"github.com/docker/docker/daemon/builder"
29
+	"github.com/docker/docker/daemon/internal/image"
29 30
 	"github.com/docker/docker/errdefs"
30
-	"github.com/docker/docker/image"
31 31
 	"github.com/docker/docker/oci"
32 32
 	"github.com/moby/buildkit/frontend/dockerfile/instructions"
33 33
 	"github.com/moby/buildkit/frontend/dockerfile/shell"
... ...
@@ -7,7 +7,7 @@ import (
7 7
 	"github.com/containerd/log"
8 8
 	"github.com/containerd/platforms"
9 9
 	"github.com/docker/docker/daemon/builder"
10
-	dockerimage "github.com/docker/docker/image"
10
+	dockerimage "github.com/docker/docker/daemon/internal/image"
11 11
 	"github.com/moby/moby/api/types/backend"
12 12
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
13 13
 	"github.com/pkg/errors"
... ...
@@ -8,7 +8,7 @@ import (
8 8
 
9 9
 	"github.com/containerd/platforms"
10 10
 	"github.com/docker/docker/daemon/builder"
11
-	"github.com/docker/docker/image"
11
+	"github.com/docker/docker/daemon/internal/image"
12 12
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
13 13
 	"gotest.tools/v3/assert"
14 14
 )
... ...
@@ -13,8 +13,8 @@ import (
13 13
 	"github.com/containerd/log"
14 14
 	"github.com/containerd/platforms"
15 15
 	"github.com/docker/docker/daemon/builder"
16
+	"github.com/docker/docker/daemon/internal/image"
16 17
 	networkSettings "github.com/docker/docker/daemon/network"
17
-	"github.com/docker/docker/image"
18 18
 	"github.com/docker/docker/pkg/stringid"
19 19
 	"github.com/docker/go-connections/nat"
20 20
 	"github.com/moby/go-archive"
... ...
@@ -9,8 +9,8 @@ import (
9 9
 
10 10
 	"github.com/docker/docker/daemon/builder"
11 11
 	"github.com/docker/docker/daemon/builder/remotecontext"
12
+	"github.com/docker/docker/daemon/internal/image"
12 13
 	"github.com/docker/docker/daemon/internal/layer"
13
-	"github.com/docker/docker/image"
14 14
 	"github.com/docker/go-connections/nat"
15 15
 	"github.com/moby/go-archive"
16 16
 	"github.com/moby/moby/api/types/backend"
... ...
@@ -7,8 +7,8 @@ import (
7 7
 	"runtime"
8 8
 
9 9
 	"github.com/docker/docker/daemon/builder"
10
+	"github.com/docker/docker/daemon/internal/image"
10 11
 	"github.com/docker/docker/daemon/internal/layer"
11
-	"github.com/docker/docker/image"
12 12
 	"github.com/moby/moby/api/types/backend"
13 13
 	"github.com/moby/moby/api/types/container"
14 14
 	"github.com/opencontainers/go-digest"
... ...
@@ -8,13 +8,13 @@ import (
8 8
 	"github.com/distribution/reference"
9 9
 	"github.com/docker/distribution"
10 10
 	clustertypes "github.com/docker/docker/daemon/cluster/provider"
11
+	"github.com/docker/docker/daemon/internal/image"
11 12
 	"github.com/docker/docker/daemon/libnetwork"
12 13
 	"github.com/docker/docker/daemon/libnetwork/cluster"
13 14
 	networktypes "github.com/docker/docker/daemon/libnetwork/types"
14 15
 	networkSettings "github.com/docker/docker/daemon/network"
15 16
 	"github.com/docker/docker/daemon/pkg/plugin"
16 17
 	volumeopts "github.com/docker/docker/daemon/volume/service/opts"
17
-	"github.com/docker/docker/image"
18 18
 	"github.com/moby/moby/api/types/backend"
19 19
 	"github.com/moby/moby/api/types/container"
20 20
 	"github.com/moby/moby/api/types/events"
... ...
@@ -17,11 +17,11 @@ import (
17 17
 	"github.com/containerd/log"
18 18
 	"github.com/docker/docker/daemon/config"
19 19
 	"github.com/docker/docker/daemon/container"
20
+	"github.com/docker/docker/daemon/internal/image"
20 21
 	"github.com/docker/docker/daemon/network"
21 22
 	"github.com/docker/docker/daemon/pkg/opts"
22 23
 	volumemounts "github.com/docker/docker/daemon/volume/mounts"
23 24
 	"github.com/docker/docker/errdefs"
24
-	"github.com/docker/docker/image"
25 25
 	"github.com/docker/docker/oci/caps"
26 26
 	"github.com/docker/go-connections/nat"
27 27
 	containertypes "github.com/moby/moby/api/types/container"
... ...
@@ -17,6 +17,7 @@ import (
17 17
 	cerrdefs "github.com/containerd/errdefs"
18 18
 	"github.com/containerd/log"
19 19
 	"github.com/containerd/platforms"
20
+	"github.com/docker/docker/daemon/internal/image"
20 21
 	libcontainerdtypes "github.com/docker/docker/daemon/internal/libcontainerd/types"
21 22
 	"github.com/docker/docker/daemon/internal/restartmanager"
22 23
 	"github.com/docker/docker/daemon/internal/stream"
... ...
@@ -28,7 +29,6 @@ import (
28 28
 	"github.com/docker/docker/daemon/volume"
29 29
 	volumemounts "github.com/docker/docker/daemon/volume/mounts"
30 30
 	"github.com/docker/docker/errdefs"
31
-	"github.com/docker/docker/image"
32 31
 	"github.com/docker/docker/oci"
33 32
 	"github.com/docker/go-units"
34 33
 	containertypes "github.com/moby/moby/api/types/container"
... ...
@@ -10,10 +10,10 @@ import (
10 10
 	cerrdefs "github.com/containerd/errdefs"
11 11
 	"github.com/containerd/log"
12 12
 	"github.com/docker/docker/daemon/builder"
13
+	"github.com/docker/docker/daemon/internal/image"
14
+	"github.com/docker/docker/daemon/internal/image/cache"
13 15
 	"github.com/docker/docker/daemon/internal/layer"
14 16
 	"github.com/docker/docker/errdefs"
15
-	"github.com/docker/docker/image"
16
-	"github.com/docker/docker/image/cache"
17 17
 	"github.com/docker/docker/internal/multierror"
18 18
 	"github.com/moby/moby/api/types/backend"
19 19
 	"github.com/moby/moby/api/types/container"
... ...
@@ -13,8 +13,8 @@ import (
13 13
 	"github.com/containerd/platforms"
14 14
 	"github.com/distribution/reference"
15 15
 	"github.com/docker/docker/daemon/images"
16
+	"github.com/docker/docker/daemon/internal/image"
16 17
 	"github.com/docker/docker/errdefs"
17
-	"github.com/docker/docker/image"
18 18
 	imagespec "github.com/moby/docker-image-spec/specs-go/v1"
19 19
 	"github.com/moby/moby/api/types/backend"
20 20
 	"github.com/opencontainers/go-digest"
... ...
@@ -22,9 +22,9 @@ import (
22 22
 	"github.com/containerd/platforms"
23 23
 	"github.com/distribution/reference"
24 24
 	"github.com/docker/docker/daemon/builder"
25
+	"github.com/docker/docker/daemon/internal/image"
25 26
 	"github.com/docker/docker/daemon/internal/layer"
26 27
 	"github.com/docker/docker/errdefs"
27
-	"github.com/docker/docker/image"
28 28
 	"github.com/docker/docker/pkg/progress"
29 29
 	"github.com/docker/docker/pkg/streamformatter"
30 30
 	"github.com/docker/docker/pkg/stringid"
... ...
@@ -4,8 +4,8 @@ import (
4 4
 	"context"
5 5
 
6 6
 	c8dimages "github.com/containerd/containerd/v2/core/images"
7
+	"github.com/docker/docker/daemon/internal/image"
7 8
 	"github.com/docker/docker/errdefs"
8
-	"github.com/docker/docker/image"
9 9
 	"github.com/opencontainers/go-digest"
10 10
 	"github.com/pkg/errors"
11 11
 )
... ...
@@ -16,7 +16,7 @@ import (
16 16
 	"github.com/containerd/containerd/v2/core/snapshots"
17 17
 	cerrdefs "github.com/containerd/errdefs"
18 18
 	"github.com/containerd/log"
19
-	"github.com/docker/docker/image"
19
+	"github.com/docker/docker/daemon/internal/image"
20 20
 	imagespec "github.com/moby/docker-image-spec/specs-go/v1"
21 21
 	"github.com/moby/go-archive"
22 22
 	"github.com/moby/moby/api/types/backend"
... ...
@@ -16,8 +16,8 @@ import (
16 16
 	"github.com/distribution/reference"
17 17
 	"github.com/docker/docker/daemon/container"
18 18
 	dimages "github.com/docker/docker/daemon/images"
19
+	"github.com/docker/docker/daemon/internal/image"
19 20
 	"github.com/docker/docker/daemon/internal/metrics"
20
-	"github.com/docker/docker/image"
21 21
 	"github.com/docker/docker/pkg/stringid"
22 22
 	"github.com/moby/moby/api/types/events"
23 23
 	imagetypes "github.com/moby/moby/api/types/image"
... ...
@@ -16,8 +16,8 @@ import (
16 16
 	"github.com/containerd/platforms"
17 17
 	"github.com/distribution/reference"
18 18
 	"github.com/docker/docker/daemon/builder/dockerfile"
19
+	"github.com/docker/docker/daemon/internal/image"
19 20
 	"github.com/docker/docker/errdefs"
20
-	"github.com/docker/docker/image"
21 21
 	"github.com/docker/docker/pkg/pools"
22 22
 	"github.com/google/uuid"
23 23
 	imagespec "github.com/moby/docker-image-spec/specs-go/v1"
... ...
@@ -12,10 +12,10 @@ import (
12 12
 	cerrdefs "github.com/containerd/errdefs"
13 13
 	"github.com/containerd/log"
14 14
 	"github.com/docker/docker/daemon/container"
15
+	"github.com/docker/docker/daemon/internal/image"
15 16
 	"github.com/docker/docker/daemon/internal/layer"
16 17
 	"github.com/docker/docker/daemon/snapshotter"
17 18
 	"github.com/docker/docker/errdefs"
18
-	"github.com/docker/docker/image"
19 19
 	"github.com/opencontainers/image-spec/identity"
20 20
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
21 21
 	"github.com/pkg/errors"
... ...
@@ -8,8 +8,8 @@ import (
8 8
 	cerrdefs "github.com/containerd/errdefs"
9 9
 	"github.com/containerd/log"
10 10
 	"github.com/distribution/reference"
11
+	"github.com/docker/docker/daemon/internal/image"
11 12
 	"github.com/docker/docker/errdefs"
12
-	"github.com/docker/docker/image"
13 13
 	"github.com/moby/moby/api/types/events"
14 14
 	"github.com/pkg/errors"
15 15
 )
... ...
@@ -6,8 +6,8 @@ package containerd
6 6
 import (
7 7
 	"slices"
8 8
 
9
+	"github.com/docker/docker/daemon/internal/image"
9 10
 	"github.com/docker/docker/dockerversion"
10
-	"github.com/docker/docker/image"
11 11
 	"github.com/docker/go-connections/nat"
12 12
 	imagespec "github.com/moby/docker-image-spec/specs-go/v1"
13 13
 	"github.com/moby/moby/api/types/container"
... ...
@@ -4,8 +4,8 @@ package containerd
4 4
 
5 5
 import (
6 6
 	"github.com/docker/docker/daemon/container"
7
+	"github.com/docker/docker/daemon/internal/image"
7 8
 	"github.com/docker/docker/errdefs"
8
-	"github.com/docker/docker/image"
9 9
 	"github.com/pkg/errors"
10 10
 )
11 11
 
... ...
@@ -5,7 +5,7 @@ import (
5 5
 	"fmt"
6 6
 
7 7
 	"github.com/docker/docker/daemon/container"
8
-	"github.com/docker/docker/image"
8
+	"github.com/docker/docker/daemon/internal/image"
9 9
 	"github.com/pkg/errors"
10 10
 )
11 11
 
... ...
@@ -17,9 +17,9 @@ import (
17 17
 	"github.com/docker/docker/daemon/config"
18 18
 	"github.com/docker/docker/daemon/container"
19 19
 	"github.com/docker/docker/daemon/images"
20
+	"github.com/docker/docker/daemon/internal/image"
20 21
 	"github.com/docker/docker/daemon/internal/metrics"
21 22
 	"github.com/docker/docker/errdefs"
22
-	"github.com/docker/docker/image"
23 23
 	"github.com/docker/docker/internal/multierror"
24 24
 	"github.com/docker/docker/internal/otelutil"
25 25
 	"github.com/docker/docker/runconfig"
... ...
@@ -41,6 +41,7 @@ import (
41 41
 	"github.com/docker/docker/daemon/images"
42 42
 	"github.com/docker/docker/daemon/internal/distribution"
43 43
 	dmetadata "github.com/docker/docker/daemon/internal/distribution/metadata"
44
+	"github.com/docker/docker/daemon/internal/image"
44 45
 	"github.com/docker/docker/daemon/internal/layer"
45 46
 	libcontainerdtypes "github.com/docker/docker/daemon/internal/libcontainerd/types"
46 47
 	"github.com/docker/docker/daemon/internal/metrics"
... ...
@@ -57,7 +58,6 @@ import (
57 57
 	"github.com/docker/docker/daemon/stats"
58 58
 	volumesservice "github.com/docker/docker/daemon/volume/service"
59 59
 	"github.com/docker/docker/dockerversion"
60
-	"github.com/docker/docker/image"
61 60
 	"github.com/docker/docker/pkg/authorization"
62 61
 	"github.com/docker/docker/pkg/fileutils"
63 62
 	"github.com/docker/docker/pkg/idtools"
... ...
@@ -8,8 +8,8 @@ import (
8 8
 	"github.com/docker/docker/daemon/builder"
9 9
 	"github.com/docker/docker/daemon/container"
10 10
 	"github.com/docker/docker/daemon/images"
11
+	"github.com/docker/docker/daemon/internal/image"
11 12
 	"github.com/docker/docker/daemon/internal/layer"
12
-	"github.com/docker/docker/image"
13 13
 	"github.com/moby/go-archive"
14 14
 	"github.com/moby/moby/api/types/backend"
15 15
 	"github.com/moby/moby/api/types/events"
... ...
@@ -7,9 +7,9 @@ import (
7 7
 
8 8
 	"github.com/containerd/log"
9 9
 	"github.com/docker/docker/daemon/builder"
10
+	"github.com/docker/docker/daemon/internal/image"
11
+	"github.com/docker/docker/daemon/internal/image/cache"
10 12
 	"github.com/docker/docker/daemon/internal/layer"
11
-	"github.com/docker/docker/image"
12
-	"github.com/docker/docker/image/cache"
13 13
 	"github.com/moby/moby/api/types/backend"
14 14
 )
15 15
 
... ...
@@ -13,8 +13,8 @@ import (
13 13
 	"github.com/containerd/log"
14 14
 	"github.com/containerd/platforms"
15 15
 	"github.com/distribution/reference"
16
+	"github.com/docker/docker/daemon/internal/image"
16 17
 	"github.com/docker/docker/errdefs"
17
-	"github.com/docker/docker/image"
18 18
 	"github.com/moby/moby/api/types/backend"
19 19
 	"github.com/opencontainers/go-digest"
20 20
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
... ...
@@ -10,8 +10,8 @@ import (
10 10
 	"github.com/containerd/platforms"
11 11
 	"github.com/distribution/reference"
12 12
 	"github.com/docker/docker/daemon/builder"
13
+	"github.com/docker/docker/daemon/internal/image"
13 14
 	"github.com/docker/docker/daemon/internal/layer"
14
-	"github.com/docker/docker/image"
15 15
 	"github.com/docker/docker/pkg/progress"
16 16
 	"github.com/docker/docker/pkg/streamformatter"
17 17
 	"github.com/docker/docker/pkg/stringid"
... ...
@@ -5,8 +5,8 @@ import (
5 5
 	"encoding/json"
6 6
 	"io"
7 7
 
8
+	"github.com/docker/docker/daemon/internal/image"
8 9
 	"github.com/docker/docker/daemon/internal/layer"
9
-	"github.com/docker/docker/image"
10 10
 	"github.com/docker/docker/pkg/ioutils"
11 11
 	"github.com/moby/moby/api/types/backend"
12 12
 	"github.com/moby/moby/api/types/events"
... ...
@@ -8,9 +8,9 @@ import (
8 8
 
9 9
 	"github.com/distribution/reference"
10 10
 	"github.com/docker/docker/daemon/container"
11
+	"github.com/docker/docker/daemon/internal/image"
11 12
 	"github.com/docker/docker/daemon/internal/metrics"
12 13
 	"github.com/docker/docker/errdefs"
13
-	"github.com/docker/docker/image"
14 14
 	"github.com/docker/docker/pkg/stringid"
15 15
 	"github.com/moby/moby/api/types/backend"
16 16
 	"github.com/moby/moby/api/types/events"
... ...
@@ -4,8 +4,8 @@ import (
4 4
 	"context"
5 5
 	"io"
6 6
 
7
+	"github.com/docker/docker/daemon/internal/image/tarexport"
7 8
 	"github.com/docker/docker/errdefs"
8
-	"github.com/docker/docker/image/tarexport"
9 9
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
10 10
 	"github.com/pkg/errors"
11 11
 )
... ...
@@ -9,10 +9,10 @@ import (
9 9
 	"github.com/containerd/platforms"
10 10
 	"github.com/distribution/reference"
11 11
 	"github.com/docker/docker/daemon/builder/dockerfile"
12
+	"github.com/docker/docker/daemon/internal/image"
12 13
 	"github.com/docker/docker/daemon/internal/layer"
13 14
 	"github.com/docker/docker/dockerversion"
14 15
 	"github.com/docker/docker/errdefs"
15
-	"github.com/docker/docker/image"
16 16
 	"github.com/moby/go-archive/compression"
17 17
 	"github.com/moby/moby/api/types/container"
18 18
 	"github.com/moby/moby/api/types/events"
... ...
@@ -5,8 +5,8 @@ import (
5 5
 	"time"
6 6
 
7 7
 	"github.com/distribution/reference"
8
+	"github.com/docker/docker/daemon/internal/image"
8 9
 	"github.com/docker/docker/daemon/internal/layer"
9
-	"github.com/docker/docker/image"
10 10
 	"github.com/moby/moby/api/types/backend"
11 11
 	imagetypes "github.com/moby/moby/api/types/image"
12 12
 	"github.com/moby/moby/api/types/storage"
... ...
@@ -9,8 +9,8 @@ import (
9 9
 
10 10
 	"github.com/distribution/reference"
11 11
 	"github.com/docker/docker/daemon/container"
12
+	"github.com/docker/docker/daemon/internal/image"
12 13
 	"github.com/docker/docker/daemon/internal/layer"
13
-	"github.com/docker/docker/image"
14 14
 	"github.com/moby/moby/api/types/backend"
15 15
 	imagetypes "github.com/moby/moby/api/types/image"
16 16
 	timetypes "github.com/moby/moby/api/types/time"
... ...
@@ -8,9 +8,9 @@ import (
8 8
 	cerrdefs "github.com/containerd/errdefs"
9 9
 	"github.com/containerd/log"
10 10
 	"github.com/distribution/reference"
11
+	"github.com/docker/docker/daemon/internal/image"
11 12
 	"github.com/docker/docker/daemon/internal/layer"
12 13
 	"github.com/docker/docker/errdefs"
13
-	"github.com/docker/docker/image"
14 14
 	"github.com/moby/moby/api/types/events"
15 15
 	"github.com/moby/moby/api/types/filters"
16 16
 	imagetypes "github.com/moby/moby/api/types/image"
... ...
@@ -5,8 +5,8 @@ import (
5 5
 	"fmt"
6 6
 	"time"
7 7
 
8
+	"github.com/docker/docker/daemon/internal/image"
8 9
 	"github.com/docker/docker/daemon/internal/layer"
9
-	"github.com/docker/docker/image"
10 10
 	"github.com/pkg/errors"
11 11
 )
12 12
 
... ...
@@ -4,7 +4,7 @@ import (
4 4
 	"context"
5 5
 
6 6
 	"github.com/distribution/reference"
7
-	"github.com/docker/docker/image"
7
+	"github.com/docker/docker/daemon/internal/image"
8 8
 	"github.com/moby/moby/api/types/events"
9 9
 )
10 10
 
... ...
@@ -7,7 +7,7 @@ import (
7 7
 
8 8
 	"github.com/containerd/log"
9 9
 	"github.com/docker/docker/daemon/container"
10
-	"github.com/docker/docker/image"
10
+	"github.com/docker/docker/daemon/internal/image"
11 11
 )
12 12
 
13 13
 // GetLayerFolders returns the layer folders from an image RootFS
... ...
@@ -4,8 +4,8 @@ import (
4 4
 	"context"
5 5
 
6 6
 	"github.com/docker/docker/daemon/container"
7
+	"github.com/docker/docker/daemon/internal/image"
7 8
 	"github.com/docker/docker/daemon/internal/layer"
8
-	"github.com/docker/docker/image"
9 9
 	"github.com/pkg/errors"
10 10
 )
11 11
 
... ...
@@ -14,8 +14,8 @@ import (
14 14
 	"github.com/docker/docker/daemon/internal/distribution"
15 15
 	"github.com/docker/docker/daemon/internal/distribution/metadata"
16 16
 	"github.com/docker/docker/daemon/internal/distribution/xfer"
17
+	"github.com/docker/docker/daemon/internal/image"
17 18
 	"github.com/docker/docker/daemon/internal/layer"
18
-	"github.com/docker/docker/image"
19 19
 	refstore "github.com/docker/docker/reference"
20 20
 	"github.com/opencontainers/go-digest"
21 21
 	"github.com/pkg/errors"
... ...
@@ -10,8 +10,8 @@ import (
10 10
 	cerrdefs "github.com/containerd/errdefs"
11 11
 	"github.com/containerd/log"
12 12
 	"github.com/docker/docker/daemon/internal/distribution"
13
+	"github.com/docker/docker/daemon/internal/image"
13 14
 	"github.com/docker/docker/daemon/internal/layer"
14
-	"github.com/docker/docker/image"
15 15
 	"github.com/opencontainers/go-digest"
16 16
 	"github.com/pkg/errors"
17 17
 )
... ...
@@ -12,7 +12,7 @@ import (
12 12
 	"github.com/containerd/containerd/v2/pkg/namespaces"
13 13
 	"github.com/containerd/containerd/v2/plugins/content/local"
14 14
 	cerrdefs "github.com/containerd/errdefs"
15
-	"github.com/docker/docker/image"
15
+	"github.com/docker/docker/daemon/internal/image"
16 16
 	"github.com/opencontainers/go-digest"
17 17
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
18 18
 	bolt "go.etcd.io/bbolt"
... ...
@@ -28,8 +28,8 @@ import (
28 28
 	dimages "github.com/docker/docker/daemon/images"
29 29
 	"github.com/docker/docker/daemon/internal/distribution/metadata"
30 30
 	"github.com/docker/docker/daemon/internal/distribution/xfer"
31
+	"github.com/docker/docker/daemon/internal/image"
31 32
 	"github.com/docker/docker/daemon/internal/layer"
32
-	"github.com/docker/docker/image"
33 33
 	pkgprogress "github.com/docker/docker/pkg/progress"
34 34
 	refstore "github.com/docker/docker/reference"
35 35
 	"github.com/moby/buildkit/cache"
... ...
@@ -9,7 +9,7 @@ import (
9 9
 	c8dimages "github.com/containerd/containerd/v2/core/images"
10 10
 	"github.com/containerd/containerd/v2/core/remotes/docker"
11 11
 	"github.com/distribution/reference"
12
-	imagestore "github.com/docker/docker/image"
12
+	imagestore "github.com/docker/docker/daemon/internal/image"
13 13
 	refstore "github.com/docker/docker/reference"
14 14
 	"github.com/moby/buildkit/cache/remotecache"
15 15
 	registryremotecache "github.com/moby/buildkit/cache/remotecache/registry"
... ...
@@ -14,8 +14,8 @@ import (
14 14
 	"github.com/containerd/containerd/v2/core/leases"
15 15
 	"github.com/containerd/log"
16 16
 	"github.com/distribution/reference"
17
+	"github.com/docker/docker/daemon/internal/image"
17 18
 	"github.com/docker/docker/daemon/internal/layer"
18
-	"github.com/docker/docker/image"
19 19
 	"github.com/moby/buildkit/exporter"
20 20
 	"github.com/moby/buildkit/exporter/containerimage"
21 21
 	"github.com/moby/buildkit/exporter/containerimage/exptypes"
... ...
@@ -3,8 +3,8 @@ package imagerefchecker
3 3
 import (
4 4
 	"sync"
5 5
 
6
+	"github.com/docker/docker/daemon/internal/image"
6 7
 	"github.com/docker/docker/daemon/internal/layer"
7
-	"github.com/docker/docker/image"
8 8
 	"github.com/moby/buildkit/cache"
9 9
 	"github.com/opencontainers/go-digest"
10 10
 )
... ...
@@ -11,8 +11,8 @@ import (
11 11
 	"github.com/docker/distribution/manifest/schema2"
12 12
 	"github.com/docker/docker/daemon/internal/distribution/metadata"
13 13
 	"github.com/docker/docker/daemon/internal/distribution/xfer"
14
+	"github.com/docker/docker/daemon/internal/image"
14 15
 	"github.com/docker/docker/daemon/internal/layer"
15
-	"github.com/docker/docker/image"
16 16
 	"github.com/docker/docker/pkg/progress"
17 17
 	refstore "github.com/docker/docker/reference"
18 18
 	registrypkg "github.com/docker/docker/registry"
... ...
@@ -19,8 +19,8 @@ import (
19 19
 	"github.com/docker/distribution/registry/client/transport"
20 20
 	"github.com/docker/docker/daemon/internal/distribution/metadata"
21 21
 	"github.com/docker/docker/daemon/internal/distribution/xfer"
22
+	"github.com/docker/docker/daemon/internal/image"
22 23
 	"github.com/docker/docker/daemon/internal/layer"
23
-	"github.com/docker/docker/image"
24 24
 	"github.com/docker/docker/pkg/ioutils"
25 25
 	"github.com/docker/docker/pkg/progress"
26 26
 	"github.com/docker/docker/pkg/stringid"
... ...
@@ -11,7 +11,7 @@ import (
11 11
 	"testing"
12 12
 
13 13
 	"github.com/distribution/reference"
14
-	"github.com/docker/docker/image"
14
+	"github.com/docker/docker/daemon/internal/image"
15 15
 	"github.com/docker/docker/registry"
16 16
 	registrytypes "github.com/moby/moby/api/types/registry"
17 17
 	"github.com/opencontainers/go-digest"
... ...
@@ -17,7 +17,7 @@ import (
17 17
 	"github.com/docker/distribution/manifest/manifestlist"
18 18
 	"github.com/docker/distribution/manifest/schema2"
19 19
 	"github.com/docker/distribution/registry/client/transport"
20
-	"github.com/docker/docker/image"
20
+	"github.com/docker/docker/daemon/internal/image"
21 21
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
22 22
 )
23 23
 
... ...
@@ -9,8 +9,8 @@ import (
9 9
 
10 10
 	"github.com/containerd/log"
11 11
 	"github.com/docker/distribution"
12
+	"github.com/docker/docker/daemon/internal/image"
12 13
 	"github.com/docker/docker/daemon/internal/layer"
13
-	"github.com/docker/docker/image"
14 14
 	"github.com/docker/docker/pkg/ioutils"
15 15
 	"github.com/docker/docker/pkg/progress"
16 16
 	"github.com/moby/go-archive/compression"
17 17
new file mode 100644
... ...
@@ -0,0 +1,276 @@
0
+package cache
1
+
2
+import (
3
+	"context"
4
+	"fmt"
5
+	"reflect"
6
+	"strings"
7
+
8
+	"github.com/containerd/log"
9
+	"github.com/docker/docker/daemon/builder"
10
+	"github.com/docker/docker/daemon/internal/image"
11
+	"github.com/docker/docker/daemon/internal/layer"
12
+	"github.com/docker/docker/dockerversion"
13
+	containertypes "github.com/moby/moby/api/types/container"
14
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
15
+	"github.com/pkg/errors"
16
+)
17
+
18
+type ImageCacheStore interface {
19
+	Get(image.ID) (*image.Image, error)
20
+	GetByRef(ctx context.Context, refOrId string) (*image.Image, error)
21
+	SetParent(target, parent image.ID) error
22
+	GetParent(target image.ID) (image.ID, error)
23
+	Create(parent *image.Image, image image.Image, extraLayer layer.DiffID) (image.ID, error)
24
+	IsBuiltLocally(id image.ID) (bool, error)
25
+	Children(id image.ID) []image.ID
26
+}
27
+
28
+func New(ctx context.Context, store ImageCacheStore, cacheFrom []string) (builder.ImageCache, error) {
29
+	local := &LocalImageCache{store: store}
30
+	if len(cacheFrom) == 0 {
31
+		return local, nil
32
+	}
33
+
34
+	cache := &ImageCache{
35
+		store:           store,
36
+		localImageCache: local,
37
+	}
38
+
39
+	for _, ref := range cacheFrom {
40
+		img, err := store.GetByRef(ctx, ref)
41
+		if err != nil {
42
+			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
43
+				return nil, err
44
+			}
45
+			log.G(ctx).Warnf("Could not look up %s for cache resolution, skipping: %+v", ref, err)
46
+			continue
47
+		}
48
+		cache.Populate(img)
49
+	}
50
+
51
+	return cache, nil
52
+}
53
+
54
+// LocalImageCache is cache based on parent chain.
55
+type LocalImageCache struct {
56
+	store ImageCacheStore
57
+}
58
+
59
+// GetCache returns the image id found in the cache
60
+func (lic *LocalImageCache) GetCache(imgID string, config *containertypes.Config, platform ocispec.Platform) (string, error) {
61
+	return getImageIDAndError(getLocalCachedImage(lic.store, image.ID(imgID), config, platform))
62
+}
63
+
64
+// ImageCache is cache based on history objects. Requires initial set of images.
65
+type ImageCache struct {
66
+	sources         []*image.Image
67
+	store           ImageCacheStore
68
+	localImageCache *LocalImageCache
69
+}
70
+
71
+// Populate adds an image to the cache (to be queried later)
72
+func (ic *ImageCache) Populate(image *image.Image) {
73
+	ic.sources = append(ic.sources, image)
74
+}
75
+
76
+// GetCache returns the image id found in the cache
77
+func (ic *ImageCache) GetCache(parentID string, cfg *containertypes.Config, platform ocispec.Platform) (string, error) {
78
+	imgID, err := ic.localImageCache.GetCache(parentID, cfg, platform)
79
+	if err != nil {
80
+		return "", err
81
+	}
82
+	if imgID != "" {
83
+		for _, s := range ic.sources {
84
+			if ic.isParent(s.ID(), image.ID(imgID)) {
85
+				return imgID, nil
86
+			}
87
+		}
88
+	}
89
+
90
+	var parent *image.Image
91
+	lenHistory := 0
92
+	if parentID != "" {
93
+		parent, err = ic.store.Get(image.ID(parentID))
94
+		if err != nil {
95
+			return "", errors.Wrapf(err, "unable to find image %v", parentID)
96
+		}
97
+		lenHistory = len(parent.History)
98
+	}
99
+
100
+	for _, target := range ic.sources {
101
+		if !isValidParent(target, parent) || !isValidConfig(cfg, target.History[lenHistory]) {
102
+			continue
103
+		}
104
+
105
+		if len(target.History)-1 == lenHistory { // last
106
+			if parent != nil {
107
+				if err := ic.store.SetParent(target.ID(), parent.ID()); err != nil {
108
+					return "", errors.Wrapf(err, "failed to set parent for %v to %v", target.ID(), parent.ID())
109
+				}
110
+			}
111
+			return target.ID().String(), nil
112
+		}
113
+
114
+		imgID, err := ic.restoreCachedImage(parent, target, cfg)
115
+		if err != nil {
116
+			return "", errors.Wrapf(err, "failed to restore cached image from %q to %v", parentID, target.ID())
117
+		}
118
+
119
+		ic.sources = []*image.Image{target} // avoid jumping to different target, tuned for safety atm
120
+		return imgID.String(), nil
121
+	}
122
+
123
+	return "", nil
124
+}
125
+
126
+func (ic *ImageCache) restoreCachedImage(parent, target *image.Image, cfg *containertypes.Config) (image.ID, error) {
127
+	var history []image.History
128
+	rootFS := image.NewRootFS()
129
+	lenHistory := 0
130
+	if parent != nil {
131
+		history = parent.History
132
+		rootFS = parent.RootFS
133
+		lenHistory = len(parent.History)
134
+	}
135
+	history = append(history, target.History[lenHistory])
136
+	layer := getLayerForHistoryIndex(target, lenHistory)
137
+	if layer != "" {
138
+		rootFS.Append(layer)
139
+	}
140
+
141
+	restoredImg := image.Image{
142
+		V1Image: image.V1Image{
143
+			DockerVersion: dockerversion.Version,
144
+			Config:        cfg,
145
+			Architecture:  target.Architecture,
146
+			OS:            target.OS,
147
+			Author:        target.Author,
148
+			Created:       history[len(history)-1].Created,
149
+		},
150
+		RootFS:     rootFS,
151
+		History:    history,
152
+		OSFeatures: target.OSFeatures,
153
+		OSVersion:  target.OSVersion,
154
+	}
155
+
156
+	imgID, err := ic.store.Create(parent, restoredImg, layer)
157
+	if err != nil {
158
+		return "", errors.Wrap(err, "failed to create cache image")
159
+	}
160
+
161
+	return imgID, nil
162
+}
163
+
164
+func (ic *ImageCache) isParent(imgID, parentID image.ID) bool {
165
+	nextParent, err := ic.store.GetParent(imgID)
166
+	if err != nil {
167
+		return false
168
+	}
169
+	if nextParent == parentID {
170
+		return true
171
+	}
172
+	return ic.isParent(nextParent, parentID)
173
+}
174
+
175
+func getLayerForHistoryIndex(image *image.Image, index int) layer.DiffID {
176
+	layerIndex := 0
177
+	for i, h := range image.History {
178
+		if i == index {
179
+			if h.EmptyLayer {
180
+				return ""
181
+			}
182
+			break
183
+		}
184
+		if !h.EmptyLayer {
185
+			layerIndex++
186
+		}
187
+	}
188
+	return image.RootFS.DiffIDs[layerIndex] // validate?
189
+}
190
+
191
+func isValidConfig(cfg *containertypes.Config, h image.History) bool {
192
+	// todo: make this format better than join that loses data
193
+	return strings.Join(cfg.Cmd, " ") == h.CreatedBy
194
+}
195
+
196
+func isValidParent(img, parent *image.Image) bool {
197
+	if len(img.History) == 0 {
198
+		return false
199
+	}
200
+	if parent == nil || len(parent.History) == 0 && len(parent.RootFS.DiffIDs) == 0 {
201
+		return true
202
+	}
203
+	if len(parent.History) >= len(img.History) {
204
+		return false
205
+	}
206
+	if len(parent.RootFS.DiffIDs) > len(img.RootFS.DiffIDs) {
207
+		return false
208
+	}
209
+
210
+	for i, h := range parent.History {
211
+		if !reflect.DeepEqual(h, img.History[i]) {
212
+			return false
213
+		}
214
+	}
215
+	for i, d := range parent.RootFS.DiffIDs {
216
+		if d != img.RootFS.DiffIDs[i] {
217
+			return false
218
+		}
219
+	}
220
+	return true
221
+}
222
+
223
+func getImageIDAndError(img *image.Image, err error) (string, error) {
224
+	if img == nil || err != nil {
225
+		return "", err
226
+	}
227
+	return img.ID().String(), nil
228
+}
229
+
230
+// getLocalCachedImage returns the most recent created image that is a child
231
+// of the image with imgID, that had the same config when it was
232
+// created. nil is returned if a child cannot be found. An error is
233
+// returned if the parent image cannot be found.
234
+func getLocalCachedImage(imageStore ImageCacheStore, parentID image.ID, config *containertypes.Config, platform ocispec.Platform) (*image.Image, error) {
235
+	if config == nil {
236
+		return nil, nil
237
+	}
238
+
239
+	var match *image.Image
240
+	for _, id := range imageStore.Children(parentID) {
241
+		img, err := imageStore.Get(id)
242
+		if err != nil {
243
+			return nil, fmt.Errorf("unable to find image %q", id)
244
+		}
245
+
246
+		builtLocally, err := imageStore.IsBuiltLocally(id)
247
+		if err != nil {
248
+			log.G(context.TODO()).WithFields(log.Fields{
249
+				"error": err,
250
+				"id":    id,
251
+			}).Warn("failed to check if image was built locally")
252
+			continue
253
+		}
254
+		if !builtLocally {
255
+			continue
256
+		}
257
+
258
+		imgPlatform := img.Platform()
259
+		// Discard old linux/amd64 images with empty platform.
260
+		if imgPlatform.OS == "" && imgPlatform.Architecture == "" {
261
+			continue
262
+		}
263
+		if !comparePlatform(platform, imgPlatform) {
264
+			continue
265
+		}
266
+
267
+		if compare(&img.ContainerConfig, config) {
268
+			// check for the most up to date match
269
+			if img.Created != nil && (match == nil || match.Created.Before(*img.Created)) {
270
+				match = img
271
+			}
272
+		}
273
+	}
274
+	return match, nil
275
+}
0 276
new file mode 100644
... ...
@@ -0,0 +1,182 @@
0
+package cache
1
+
2
+import (
3
+	"strings"
4
+
5
+	"github.com/containerd/platforms"
6
+	"github.com/moby/moby/api/types/container"
7
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
8
+)
9
+
10
+func comparePlatform(builderPlatform, imagePlatform ocispec.Platform) bool {
11
+	// On Windows, only check the Major and Minor versions.
12
+	// The Build and Revision compatibility depends on whether `process` or
13
+	// `hyperv` isolation used.
14
+	//
15
+	// Fixes https://github.com/moby/moby/issues/47307
16
+	if builderPlatform.OS == "windows" && imagePlatform.OS == builderPlatform.OS {
17
+		// OSVersion format is:
18
+		// Major.Minor.Build.Revision
19
+		builderParts := strings.Split(builderPlatform.OSVersion, ".")
20
+		imageParts := strings.Split(imagePlatform.OSVersion, ".")
21
+
22
+		if len(builderParts) >= 3 && len(imageParts) >= 3 {
23
+			// Keep only Major & Minor.
24
+			builderParts[0] = imageParts[0]
25
+			builderParts[1] = imageParts[1]
26
+			imagePlatform.OSVersion = strings.Join(builderParts, ".")
27
+		}
28
+	}
29
+
30
+	return platforms.Only(builderPlatform).Match(imagePlatform)
31
+}
32
+
33
+// compare two Config struct. Do not container-specific fields:
34
+// - Image
35
+// - Hostname
36
+// - Domainname
37
+// - MacAddress
38
+func compare(a, b *container.Config) bool {
39
+	if a == nil || b == nil {
40
+		return false
41
+	}
42
+
43
+	if len(a.Env) != len(b.Env) {
44
+		return false
45
+	}
46
+	if len(a.Cmd) != len(b.Cmd) {
47
+		return false
48
+	}
49
+	if len(a.Entrypoint) != len(b.Entrypoint) {
50
+		return false
51
+	}
52
+	if len(a.Shell) != len(b.Shell) {
53
+		return false
54
+	}
55
+	if len(a.ExposedPorts) != len(b.ExposedPorts) {
56
+		return false
57
+	}
58
+	if len(a.Volumes) != len(b.Volumes) {
59
+		return false
60
+	}
61
+	if len(a.Labels) != len(b.Labels) {
62
+		return false
63
+	}
64
+	if len(a.OnBuild) != len(b.OnBuild) {
65
+		return false
66
+	}
67
+
68
+	for i := 0; i < len(a.Env); i++ {
69
+		if a.Env[i] != b.Env[i] {
70
+			return false
71
+		}
72
+	}
73
+	for i := 0; i < len(a.OnBuild); i++ {
74
+		if a.OnBuild[i] != b.OnBuild[i] {
75
+			return false
76
+		}
77
+	}
78
+	for i := 0; i < len(a.Cmd); i++ {
79
+		if a.Cmd[i] != b.Cmd[i] {
80
+			return false
81
+		}
82
+	}
83
+	for i := 0; i < len(a.Entrypoint); i++ {
84
+		if a.Entrypoint[i] != b.Entrypoint[i] {
85
+			return false
86
+		}
87
+	}
88
+	for i := 0; i < len(a.Shell); i++ {
89
+		if a.Shell[i] != b.Shell[i] {
90
+			return false
91
+		}
92
+	}
93
+	for k := range a.ExposedPorts {
94
+		if _, exists := b.ExposedPorts[k]; !exists {
95
+			return false
96
+		}
97
+	}
98
+	for key := range a.Volumes {
99
+		if _, exists := b.Volumes[key]; !exists {
100
+			return false
101
+		}
102
+	}
103
+	for k, v := range a.Labels {
104
+		if v != b.Labels[k] {
105
+			return false
106
+		}
107
+	}
108
+
109
+	if a.AttachStdin != b.AttachStdin {
110
+		return false
111
+	}
112
+	if a.AttachStdout != b.AttachStdout {
113
+		return false
114
+	}
115
+	if a.AttachStderr != b.AttachStderr {
116
+		return false
117
+	}
118
+	if a.NetworkDisabled != b.NetworkDisabled {
119
+		return false
120
+	}
121
+	if a.Tty != b.Tty {
122
+		return false
123
+	}
124
+	if a.OpenStdin != b.OpenStdin {
125
+		return false
126
+	}
127
+	if a.StdinOnce != b.StdinOnce {
128
+		return false
129
+	}
130
+	if a.ArgsEscaped != b.ArgsEscaped {
131
+		return false
132
+	}
133
+	if a.User != b.User {
134
+		return false
135
+	}
136
+	if a.WorkingDir != b.WorkingDir {
137
+		return false
138
+	}
139
+	if a.StopSignal != b.StopSignal {
140
+		return false
141
+	}
142
+
143
+	if (a.StopTimeout == nil) != (b.StopTimeout == nil) {
144
+		return false
145
+	}
146
+	if a.StopTimeout != nil && b.StopTimeout != nil {
147
+		if *a.StopTimeout != *b.StopTimeout {
148
+			return false
149
+		}
150
+	}
151
+	if (a.Healthcheck == nil) != (b.Healthcheck == nil) {
152
+		return false
153
+	}
154
+	if a.Healthcheck != nil && b.Healthcheck != nil {
155
+		if a.Healthcheck.Interval != b.Healthcheck.Interval {
156
+			return false
157
+		}
158
+		if a.Healthcheck.StartInterval != b.Healthcheck.StartInterval {
159
+			return false
160
+		}
161
+		if a.Healthcheck.StartPeriod != b.Healthcheck.StartPeriod {
162
+			return false
163
+		}
164
+		if a.Healthcheck.Timeout != b.Healthcheck.Timeout {
165
+			return false
166
+		}
167
+		if a.Healthcheck.Retries != b.Healthcheck.Retries {
168
+			return false
169
+		}
170
+		if len(a.Healthcheck.Test) != len(b.Healthcheck.Test) {
171
+			return false
172
+		}
173
+		for i := 0; i < len(a.Healthcheck.Test); i++ {
174
+			if a.Healthcheck.Test[i] != b.Healthcheck.Test[i] {
175
+				return false
176
+			}
177
+		}
178
+	}
179
+
180
+	return true
181
+}
0 182
new file mode 100644
... ...
@@ -0,0 +1,204 @@
0
+package cache
1
+
2
+import (
3
+	"runtime"
4
+	"testing"
5
+
6
+	"github.com/docker/go-connections/nat"
7
+	"github.com/moby/moby/api/types/container"
8
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
9
+	"gotest.tools/v3/assert"
10
+	is "gotest.tools/v3/assert/cmp"
11
+)
12
+
13
+// Just to make life easier
14
+func newPortNoError(proto, port string) nat.Port {
15
+	p, _ := nat.NewPort(proto, port)
16
+	return p
17
+}
18
+
19
+func TestCompare(t *testing.T) {
20
+	ports1 := make(nat.PortSet)
21
+	ports1[newPortNoError("tcp", "1111")] = struct{}{}
22
+	ports1[newPortNoError("tcp", "2222")] = struct{}{}
23
+	ports2 := make(nat.PortSet)
24
+	ports2[newPortNoError("tcp", "3333")] = struct{}{}
25
+	ports2[newPortNoError("tcp", "4444")] = struct{}{}
26
+	ports3 := make(nat.PortSet)
27
+	ports3[newPortNoError("tcp", "1111")] = struct{}{}
28
+	ports3[newPortNoError("tcp", "2222")] = struct{}{}
29
+	ports3[newPortNoError("tcp", "5555")] = struct{}{}
30
+	volumes1 := make(map[string]struct{})
31
+	volumes1["/test1"] = struct{}{}
32
+	volumes2 := make(map[string]struct{})
33
+	volumes2["/test2"] = struct{}{}
34
+	volumes3 := make(map[string]struct{})
35
+	volumes3["/test1"] = struct{}{}
36
+	volumes3["/test3"] = struct{}{}
37
+	envs1 := []string{"ENV1=value1", "ENV2=value2"}
38
+	envs2 := []string{"ENV1=value1", "ENV3=value3"}
39
+	entrypoint1 := []string{"/bin/sh", "-c"}
40
+	entrypoint2 := []string{"/bin/sh", "-d"}
41
+	entrypoint3 := []string{"/bin/sh", "-c", "echo"}
42
+	cmd1 := []string{"/bin/sh", "-c"}
43
+	cmd2 := []string{"/bin/sh", "-d"}
44
+	cmd3 := []string{"/bin/sh", "-c", "echo"}
45
+	labels1 := map[string]string{"LABEL1": "value1", "LABEL2": "value2"}
46
+	labels2 := map[string]string{"LABEL1": "value1", "LABEL2": "value3"}
47
+	labels3 := map[string]string{"LABEL1": "value1", "LABEL2": "value2", "LABEL3": "value3"}
48
+
49
+	sameConfigs := map[*container.Config]*container.Config{
50
+		// Empty config
51
+		{}: {},
52
+		// Does not compare hostname, domainname & image
53
+		{
54
+			Hostname:   "host1",
55
+			Domainname: "domain1",
56
+			Image:      "image1",
57
+			User:       "user",
58
+		}: {
59
+			Hostname:   "host2",
60
+			Domainname: "domain2",
61
+			Image:      "image2",
62
+			User:       "user",
63
+		},
64
+		// only OpenStdin
65
+		{OpenStdin: false}: {OpenStdin: false},
66
+		// only env
67
+		{Env: envs1}: {Env: envs1},
68
+		// only cmd
69
+		{Cmd: cmd1}: {Cmd: cmd1},
70
+		// only labels
71
+		{Labels: labels1}: {Labels: labels1},
72
+		// only exposedPorts
73
+		{ExposedPorts: ports1}: {ExposedPorts: ports1},
74
+		// only entrypoints
75
+		{Entrypoint: entrypoint1}: {Entrypoint: entrypoint1},
76
+		// only volumes
77
+		{Volumes: volumes1}: {Volumes: volumes1},
78
+	}
79
+	differentConfigs := map[*container.Config]*container.Config{
80
+		nil: nil,
81
+		{
82
+			Hostname:   "host1",
83
+			Domainname: "domain1",
84
+			Image:      "image1",
85
+			User:       "user1",
86
+		}: {
87
+			Hostname:   "host1",
88
+			Domainname: "domain1",
89
+			Image:      "image1",
90
+			User:       "user2",
91
+		},
92
+		// only OpenStdin
93
+		{OpenStdin: false}: {OpenStdin: true},
94
+		{OpenStdin: true}:  {OpenStdin: false},
95
+		// only env
96
+		{Env: envs1}: {Env: envs2},
97
+		// only cmd
98
+		{Cmd: cmd1}: {Cmd: cmd2},
99
+		// not the same number of parts
100
+		{Cmd: cmd1}: {Cmd: cmd3},
101
+		// only labels
102
+		{Labels: labels1}: {Labels: labels2},
103
+		// not the same number of labels
104
+		{Labels: labels1}: {Labels: labels3},
105
+		// only exposedPorts
106
+		{ExposedPorts: ports1}: {ExposedPorts: ports2},
107
+		// not the same number of ports
108
+		{ExposedPorts: ports1}: {ExposedPorts: ports3},
109
+		// only entrypoints
110
+		{Entrypoint: entrypoint1}: {Entrypoint: entrypoint2},
111
+		// not the same number of parts
112
+		{Entrypoint: entrypoint1}: {Entrypoint: entrypoint3},
113
+		// only volumes
114
+		{Volumes: volumes1}: {Volumes: volumes2},
115
+		// not the same number of labels
116
+		{Volumes: volumes1}: {Volumes: volumes3},
117
+	}
118
+	for config1, config2 := range sameConfigs {
119
+		if !compare(config1, config2) {
120
+			t.Fatalf("Compare should be true for [%v] and [%v]", config1, config2)
121
+		}
122
+	}
123
+	for config1, config2 := range differentConfigs {
124
+		if compare(config1, config2) {
125
+			t.Fatalf("Compare should be false for [%v] and [%v]", config1, config2)
126
+		}
127
+	}
128
+}
129
+
130
+func TestPlatformCompare(t *testing.T) {
131
+	for _, tc := range []struct {
132
+		name     string
133
+		builder  ocispec.Platform
134
+		image    ocispec.Platform
135
+		expected bool
136
+	}{
137
+		{
138
+			name:     "same os and arch",
139
+			builder:  ocispec.Platform{Architecture: "amd64", OS: runtime.GOOS},
140
+			image:    ocispec.Platform{Architecture: "amd64", OS: runtime.GOOS},
141
+			expected: true,
142
+		},
143
+		{
144
+			name:     "same os different arch",
145
+			builder:  ocispec.Platform{Architecture: "amd64", OS: runtime.GOOS},
146
+			image:    ocispec.Platform{Architecture: "arm64", OS: runtime.GOOS},
147
+			expected: false,
148
+		},
149
+		{
150
+			name:     "same os smaller host variant",
151
+			builder:  ocispec.Platform{Variant: "v7", Architecture: "arm", OS: runtime.GOOS},
152
+			image:    ocispec.Platform{Variant: "v8", Architecture: "arm", OS: runtime.GOOS},
153
+			expected: false,
154
+		},
155
+		{
156
+			name:     "same os higher host variant",
157
+			builder:  ocispec.Platform{Variant: "v8", Architecture: "arm", OS: runtime.GOOS},
158
+			image:    ocispec.Platform{Variant: "v7", Architecture: "arm", OS: runtime.GOOS},
159
+			expected: true,
160
+		},
161
+		{
162
+			// Test for https://github.com/moby/moby/issues/47307
163
+			name:     "different build and revision",
164
+			builder:  ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.22621"},
165
+			image:    ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.17763.5329"},
166
+			expected: true,
167
+		},
168
+		{
169
+			name:     "different revision",
170
+			builder:  ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.17763.1234"},
171
+			image:    ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.17763.5329"},
172
+			expected: true,
173
+		},
174
+		{
175
+			name:     "different major",
176
+			builder:  ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "11.0.17763.5329"},
177
+			image:    ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.17763.5329"},
178
+			expected: false,
179
+		},
180
+		{
181
+			name:     "different minor same osver",
182
+			builder:  ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.17763.5329"},
183
+			image:    ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.1.17763.5329"},
184
+			expected: false,
185
+		},
186
+		{
187
+			name:     "different arch same osver",
188
+			builder:  ocispec.Platform{Architecture: "arm64", OS: "windows", OSVersion: "10.0.17763.5329"},
189
+			image:    ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.17763.5329"},
190
+			expected: false,
191
+		},
192
+	} {
193
+		// OSVersion comparison is only performed by containerd platform
194
+		// matcher if built on Windows.
195
+		if (tc.image.OSVersion != "" || tc.builder.OSVersion != "") && runtime.GOOS != "windows" {
196
+			continue
197
+		}
198
+
199
+		t.Run(tc.name, func(t *testing.T) {
200
+			assert.Check(t, is.Equal(comparePlatform(tc.builder, tc.image), tc.expected))
201
+		})
202
+	}
203
+}
0 204
new file mode 100644
... ...
@@ -0,0 +1,175 @@
0
+package image
1
+
2
+import (
3
+	"context"
4
+	"fmt"
5
+	"os"
6
+	"path/filepath"
7
+	"sync"
8
+
9
+	"github.com/containerd/log"
10
+	"github.com/moby/sys/atomicwriter"
11
+	"github.com/opencontainers/go-digest"
12
+	"github.com/pkg/errors"
13
+)
14
+
15
+// DigestWalkFunc is function called by StoreBackend.Walk
16
+type DigestWalkFunc func(id digest.Digest) error
17
+
18
+// StoreBackend provides interface for image.Store persistence
19
+type StoreBackend interface {
20
+	Walk(f DigestWalkFunc) error
21
+	Get(id digest.Digest) ([]byte, error)
22
+	Set(data []byte) (digest.Digest, error)
23
+	Delete(id digest.Digest) error
24
+	SetMetadata(id digest.Digest, key string, data []byte) error
25
+	GetMetadata(id digest.Digest, key string) ([]byte, error)
26
+	DeleteMetadata(id digest.Digest, key string) error
27
+}
28
+
29
+// fs implements StoreBackend using the filesystem.
30
+type fs struct {
31
+	sync.RWMutex
32
+	root string
33
+}
34
+
35
+const (
36
+	contentDirName  = "content"
37
+	metadataDirName = "metadata"
38
+)
39
+
40
+// NewFSStoreBackend returns new filesystem based backend for image.Store
41
+func NewFSStoreBackend(root string) (StoreBackend, error) {
42
+	return newFSStore(root)
43
+}
44
+
45
+func newFSStore(root string) (*fs, error) {
46
+	s := &fs{
47
+		root: root,
48
+	}
49
+	if err := os.MkdirAll(filepath.Join(root, contentDirName, string(digest.Canonical)), 0o700); err != nil {
50
+		return nil, errors.Wrap(err, "failed to create storage backend")
51
+	}
52
+	if err := os.MkdirAll(filepath.Join(root, metadataDirName, string(digest.Canonical)), 0o700); err != nil {
53
+		return nil, errors.Wrap(err, "failed to create storage backend")
54
+	}
55
+	return s, nil
56
+}
57
+
58
+func (s *fs) contentFile(dgst digest.Digest) string {
59
+	return filepath.Join(s.root, contentDirName, string(dgst.Algorithm()), dgst.Encoded())
60
+}
61
+
62
+func (s *fs) metadataDir(dgst digest.Digest) string {
63
+	return filepath.Join(s.root, metadataDirName, string(dgst.Algorithm()), dgst.Encoded())
64
+}
65
+
66
+// Walk calls the supplied callback for each image ID in the storage backend.
67
+func (s *fs) Walk(f DigestWalkFunc) error {
68
+	// Only Canonical digest (sha256) is currently supported
69
+	s.RLock()
70
+	dir, err := os.ReadDir(filepath.Join(s.root, contentDirName, string(digest.Canonical)))
71
+	s.RUnlock()
72
+	if err != nil {
73
+		return err
74
+	}
75
+	for _, v := range dir {
76
+		dgst := digest.NewDigestFromEncoded(digest.Canonical, v.Name())
77
+		if err := dgst.Validate(); err != nil {
78
+			log.G(context.TODO()).Debugf("skipping invalid digest %s: %s", dgst, err)
79
+			continue
80
+		}
81
+		if err := f(dgst); err != nil {
82
+			return err
83
+		}
84
+	}
85
+	return nil
86
+}
87
+
88
+// Get returns the content stored under a given digest.
89
+func (s *fs) Get(dgst digest.Digest) ([]byte, error) {
90
+	s.RLock()
91
+	defer s.RUnlock()
92
+
93
+	return s.get(dgst)
94
+}
95
+
96
+func (s *fs) get(dgst digest.Digest) ([]byte, error) {
97
+	content, err := os.ReadFile(s.contentFile(dgst))
98
+	if err != nil {
99
+		return nil, errors.Wrapf(err, "failed to get digest %s", dgst)
100
+	}
101
+
102
+	// todo: maybe optional
103
+	if digest.FromBytes(content) != dgst {
104
+		return nil, fmt.Errorf("failed to verify: %v", dgst)
105
+	}
106
+
107
+	return content, nil
108
+}
109
+
110
+// Set stores content by checksum.
111
+func (s *fs) Set(data []byte) (digest.Digest, error) {
112
+	s.Lock()
113
+	defer s.Unlock()
114
+
115
+	if len(data) == 0 {
116
+		return "", errors.New("invalid empty data")
117
+	}
118
+
119
+	dgst := digest.FromBytes(data)
120
+	if err := atomicwriter.WriteFile(s.contentFile(dgst), data, 0o600); err != nil {
121
+		return "", errors.Wrap(err, "failed to write digest data")
122
+	}
123
+
124
+	return dgst, nil
125
+}
126
+
127
+// Delete removes content and metadata files associated with the digest.
128
+func (s *fs) Delete(dgst digest.Digest) error {
129
+	s.Lock()
130
+	defer s.Unlock()
131
+
132
+	if err := os.RemoveAll(s.metadataDir(dgst)); err != nil {
133
+		return err
134
+	}
135
+	return os.Remove(s.contentFile(dgst))
136
+}
137
+
138
+// SetMetadata sets metadata for a given ID. It fails if there's no base file.
139
+func (s *fs) SetMetadata(dgst digest.Digest, key string, data []byte) error {
140
+	s.Lock()
141
+	defer s.Unlock()
142
+	if _, err := s.get(dgst); err != nil {
143
+		return err
144
+	}
145
+
146
+	baseDir := s.metadataDir(dgst)
147
+	if err := os.MkdirAll(baseDir, 0o700); err != nil {
148
+		return err
149
+	}
150
+	return atomicwriter.WriteFile(filepath.Join(baseDir, key), data, 0o600)
151
+}
152
+
153
+// GetMetadata returns metadata for a given digest.
154
+func (s *fs) GetMetadata(dgst digest.Digest, key string) ([]byte, error) {
155
+	s.RLock()
156
+	defer s.RUnlock()
157
+
158
+	if _, err := s.get(dgst); err != nil {
159
+		return nil, err
160
+	}
161
+	bytes, err := os.ReadFile(filepath.Join(s.metadataDir(dgst), key))
162
+	if err != nil {
163
+		return nil, errors.Wrap(err, "failed to read metadata")
164
+	}
165
+	return bytes, nil
166
+}
167
+
168
+// DeleteMetadata removes the metadata associated with a digest.
169
+func (s *fs) DeleteMetadata(dgst digest.Digest, key string) error {
170
+	s.Lock()
171
+	defer s.Unlock()
172
+
173
+	return os.RemoveAll(filepath.Join(s.metadataDir(dgst), key))
174
+}
0 175
new file mode 100644
... ...
@@ -0,0 +1,257 @@
0
+package image
1
+
2
+import (
3
+	"crypto/rand"
4
+	"crypto/sha256"
5
+	"encoding/hex"
6
+	"errors"
7
+	"os"
8
+	"path/filepath"
9
+	"testing"
10
+
11
+	"github.com/opencontainers/go-digest"
12
+	"gotest.tools/v3/assert"
13
+	is "gotest.tools/v3/assert/cmp"
14
+)
15
+
16
+func defaultFSStoreBackend(t *testing.T) StoreBackend {
17
+	t.Helper()
18
+	fsBackend, err := NewFSStoreBackend(t.TempDir())
19
+	assert.Check(t, err)
20
+	return fsBackend
21
+}
22
+
23
+func TestFSGetInvalidData(t *testing.T) {
24
+	rootDir := t.TempDir()
25
+	fsStore, err := NewFSStoreBackend(rootDir)
26
+	assert.Check(t, err)
27
+
28
+	dgst, err := fsStore.Set([]byte("foobar"))
29
+	assert.Check(t, err)
30
+
31
+	err = os.WriteFile(filepath.Join(rootDir, contentDirName, string(dgst.Algorithm()), dgst.Encoded()), []byte("foobar2"), 0o600)
32
+	assert.Check(t, err)
33
+
34
+	_, err = fsStore.Get(dgst)
35
+	assert.Check(t, is.ErrorContains(err, "failed to verify"))
36
+}
37
+
38
+func TestFSInvalidSet(t *testing.T) {
39
+	rootDir := t.TempDir()
40
+	fsStore, err := NewFSStoreBackend(rootDir)
41
+	assert.Check(t, err)
42
+
43
+	id := digest.FromBytes([]byte("foobar"))
44
+	err = os.Mkdir(filepath.Join(rootDir, contentDirName, string(id.Algorithm()), id.Encoded()), 0o700)
45
+	assert.Check(t, err)
46
+
47
+	_, err = fsStore.Set([]byte("foobar"))
48
+	assert.Check(t, is.ErrorContains(err, "failed to write digest data"))
49
+}
50
+
51
+func TestFSInvalidRoot(t *testing.T) {
52
+	tmpdir := t.TempDir()
53
+
54
+	tcases := []struct {
55
+		root, invalidFile string
56
+	}{
57
+		{"root", "root"},
58
+		{"root", "root/content"},
59
+		{"root", "root/metadata"},
60
+	}
61
+
62
+	for _, tc := range tcases {
63
+		root := filepath.Join(tmpdir, tc.root)
64
+		filePath := filepath.Join(tmpdir, tc.invalidFile)
65
+		err := os.MkdirAll(filepath.Dir(filePath), 0o700)
66
+		assert.Check(t, err)
67
+
68
+		f, err := os.Create(filePath)
69
+		assert.Check(t, err)
70
+		f.Close()
71
+
72
+		_, err = NewFSStoreBackend(root)
73
+		assert.Check(t, is.ErrorContains(err, "failed to create storage backend"))
74
+
75
+		os.RemoveAll(root)
76
+	}
77
+}
78
+
79
+func TestFSMetadataGetSet(t *testing.T) {
80
+	fsStore := defaultFSStoreBackend(t)
81
+
82
+	id, err := fsStore.Set([]byte("foo"))
83
+	assert.Check(t, err)
84
+
85
+	id2, err := fsStore.Set([]byte("bar"))
86
+	assert.Check(t, err)
87
+
88
+	tcases := []struct {
89
+		id    digest.Digest
90
+		key   string
91
+		value []byte
92
+	}{
93
+		{id, "tkey", []byte("tval1")},
94
+		{id, "tkey2", []byte("tval2")},
95
+		{id2, "tkey", []byte("tval3")},
96
+	}
97
+
98
+	for _, tc := range tcases {
99
+		err = fsStore.SetMetadata(tc.id, tc.key, tc.value)
100
+		assert.Check(t, err)
101
+
102
+		actual, err := fsStore.GetMetadata(tc.id, tc.key)
103
+		assert.Check(t, err)
104
+
105
+		assert.Check(t, is.DeepEqual(tc.value, actual))
106
+	}
107
+
108
+	_, err = fsStore.GetMetadata(id2, "tkey2")
109
+	assert.Check(t, is.ErrorContains(err, "failed to read metadata"))
110
+
111
+	id3 := digest.FromBytes([]byte("baz"))
112
+	err = fsStore.SetMetadata(id3, "tkey", []byte("tval"))
113
+	assert.Check(t, is.ErrorContains(err, "failed to get digest"))
114
+
115
+	_, err = fsStore.GetMetadata(id3, "tkey")
116
+	assert.Check(t, is.ErrorContains(err, "failed to get digest"))
117
+}
118
+
119
+func TestFSInvalidWalker(t *testing.T) {
120
+	rootDir := t.TempDir()
121
+	fsStore, err := NewFSStoreBackend(rootDir)
122
+	assert.Check(t, err)
123
+
124
+	fooID, err := fsStore.Set([]byte("foo"))
125
+	assert.Check(t, err)
126
+
127
+	err = os.WriteFile(filepath.Join(rootDir, contentDirName, "sha256/foobar"), []byte("foobar"), 0o600)
128
+	assert.Check(t, err)
129
+
130
+	n := 0
131
+	err = fsStore.Walk(func(id digest.Digest) error {
132
+		assert.Check(t, is.Equal(fooID, id))
133
+		n++
134
+		return nil
135
+	})
136
+	assert.Check(t, err)
137
+	assert.Check(t, is.Equal(1, n))
138
+}
139
+
140
+func TestFSGetSet(t *testing.T) {
141
+	fsStore := defaultFSStoreBackend(t)
142
+
143
+	type tcase struct {
144
+		input    []byte
145
+		expected digest.Digest
146
+	}
147
+	tcases := []tcase{
148
+		{[]byte("foobar"), digest.Digest("sha256:c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2")},
149
+	}
150
+
151
+	randomInput := make([]byte, 8*1024)
152
+	_, err := rand.Read(randomInput)
153
+	assert.Check(t, err)
154
+
155
+	// skipping use of digest pkg because it is used by the implementation
156
+	h := sha256.New()
157
+	_, err = h.Write(randomInput)
158
+	assert.Check(t, err)
159
+
160
+	tcases = append(tcases, tcase{
161
+		input:    randomInput,
162
+		expected: digest.Digest("sha256:" + hex.EncodeToString(h.Sum(nil))),
163
+	})
164
+
165
+	for _, tc := range tcases {
166
+		id, err := fsStore.Set(tc.input)
167
+		assert.Check(t, err)
168
+		assert.Check(t, is.Equal(tc.expected, id))
169
+	}
170
+
171
+	for _, tc := range tcases {
172
+		data, err := fsStore.Get(tc.expected)
173
+		assert.Check(t, err)
174
+		assert.Check(t, is.DeepEqual(tc.input, data))
175
+	}
176
+}
177
+
178
+func TestFSGetUnsetKey(t *testing.T) {
179
+	fsStore := defaultFSStoreBackend(t)
180
+
181
+	for _, key := range []digest.Digest{"foobar:abc", "sha256:abc", "sha256:c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2a"} {
182
+		_, err := fsStore.Get(key)
183
+		assert.Check(t, is.ErrorContains(err, "failed to get digest"))
184
+	}
185
+}
186
+
187
+func TestFSGetEmptyData(t *testing.T) {
188
+	fsStore := defaultFSStoreBackend(t)
189
+
190
+	for _, emptyData := range [][]byte{nil, {}} {
191
+		_, err := fsStore.Set(emptyData)
192
+		assert.Check(t, is.ErrorContains(err, "invalid empty data"))
193
+	}
194
+}
195
+
196
+func TestFSDelete(t *testing.T) {
197
+	fsStore := defaultFSStoreBackend(t)
198
+
199
+	id, err := fsStore.Set([]byte("foo"))
200
+	assert.Check(t, err)
201
+
202
+	id2, err := fsStore.Set([]byte("bar"))
203
+	assert.Check(t, err)
204
+
205
+	err = fsStore.Delete(id)
206
+	assert.Check(t, err)
207
+
208
+	_, err = fsStore.Get(id)
209
+	assert.Check(t, is.ErrorContains(err, "failed to get digest"))
210
+
211
+	_, err = fsStore.Get(id2)
212
+	assert.Check(t, err)
213
+
214
+	err = fsStore.Delete(id2)
215
+	assert.Check(t, err)
216
+
217
+	_, err = fsStore.Get(id2)
218
+	assert.Check(t, is.ErrorContains(err, "failed to get digest"))
219
+}
220
+
221
+func TestFSWalker(t *testing.T) {
222
+	fsStore := defaultFSStoreBackend(t)
223
+
224
+	id, err := fsStore.Set([]byte("foo"))
225
+	assert.Check(t, err)
226
+
227
+	id2, err := fsStore.Set([]byte("bar"))
228
+	assert.Check(t, err)
229
+
230
+	tcases := make(map[digest.Digest]struct{})
231
+	tcases[id] = struct{}{}
232
+	tcases[id2] = struct{}{}
233
+	n := 0
234
+	err = fsStore.Walk(func(id digest.Digest) error {
235
+		delete(tcases, id)
236
+		n++
237
+		return nil
238
+	})
239
+	assert.Check(t, err)
240
+	assert.Check(t, is.Equal(2, n))
241
+	assert.Check(t, is.Len(tcases, 0))
242
+}
243
+
244
+func TestFSWalkerStopOnError(t *testing.T) {
245
+	fsStore := defaultFSStoreBackend(t)
246
+
247
+	id, err := fsStore.Set([]byte("foo"))
248
+	assert.Check(t, err)
249
+
250
+	tcases := make(map[digest.Digest]struct{})
251
+	tcases[id] = struct{}{}
252
+	err = fsStore.Walk(func(id digest.Digest) error {
253
+		return errors.New("what")
254
+	})
255
+	assert.Check(t, is.ErrorContains(err, "what"))
256
+}
0 257
new file mode 100644
... ...
@@ -0,0 +1,302 @@
0
+package image
1
+
2
+import (
3
+	"context"
4
+	"encoding/json"
5
+	"errors"
6
+	"io"
7
+	"runtime"
8
+	"strings"
9
+	"time"
10
+
11
+	"github.com/docker/docker/daemon/internal/layer"
12
+	"github.com/docker/docker/dockerversion"
13
+	"github.com/moby/moby/api/types/container"
14
+	"github.com/opencontainers/go-digest"
15
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
16
+)
17
+
18
+// ID is the content-addressable ID of an image.
19
+type ID digest.Digest
20
+
21
+func (id ID) String() string {
22
+	return id.Digest().String()
23
+}
24
+
25
+// Digest converts ID into a digest
26
+func (id ID) Digest() digest.Digest {
27
+	return digest.Digest(id)
28
+}
29
+
30
+// V1Image stores the V1 image configuration.
31
+type V1Image struct {
32
+	// ID is a unique 64 character identifier of the image
33
+	ID string `json:"id,omitempty"`
34
+
35
+	// Parent is the ID of the parent image.
36
+	//
37
+	// Depending on how the image was created, this field may be empty and
38
+	// is only set for images that were built/created locally. This field
39
+	// is empty if the image was pulled from an image registry.
40
+	Parent string `json:"parent,omitempty"`
41
+
42
+	// Comment is an optional message that can be set when committing or
43
+	// importing the image.
44
+	Comment string `json:"comment,omitempty"`
45
+
46
+	// Created is the timestamp at which the image was created
47
+	Created *time.Time `json:"created"`
48
+
49
+	// Container is the ID of the container that was used to create the image.
50
+	//
51
+	// Depending on how the image was created, this field may be empty.
52
+	Container string `json:"container,omitempty"`
53
+
54
+	// ContainerConfig is the configuration of the container that was committed
55
+	// into the image.
56
+	ContainerConfig container.Config `json:"container_config,omitempty"`
57
+
58
+	// DockerVersion is the version of Docker that was used to build the image.
59
+	//
60
+	// Depending on how the image was created, this field may be empty.
61
+	DockerVersion string `json:"docker_version,omitempty"`
62
+
63
+	// Author is the name of the author that was specified when committing the
64
+	// image, or as specified through MAINTAINER (deprecated) in the Dockerfile.
65
+	Author string `json:"author,omitempty"`
66
+
67
+	// Config is the configuration of the container received from the client.
68
+	Config *container.Config `json:"config,omitempty"`
69
+
70
+	// Architecture is the hardware CPU architecture that the image runs on.
71
+	Architecture string `json:"architecture,omitempty"`
72
+
73
+	// Variant is the CPU architecture variant (presently ARM-only).
74
+	Variant string `json:"variant,omitempty"`
75
+
76
+	// OS is the Operating System the image is built to run on.
77
+	OS string `json:"os,omitempty"`
78
+
79
+	// Size is the total size of the image including all layers it is composed of.
80
+	Size int64 `json:",omitempty"`
81
+}
82
+
83
+// Image stores the image configuration
84
+type Image struct {
85
+	V1Image
86
+
87
+	// Parent is the ID of the parent image.
88
+	//
89
+	// Depending on how the image was created, this field may be empty and
90
+	// is only set for images that were built/created locally. This field
91
+	// is empty if the image was pulled from an image registry.
92
+	Parent ID `json:"parent,omitempty"` //nolint:govet
93
+
94
+	// RootFS contains information about the image's RootFS, including the
95
+	// layer IDs.
96
+	RootFS  *RootFS   `json:"rootfs,omitempty"`
97
+	History []History `json:"history,omitempty"`
98
+
99
+	// OsVersion is the version of the Operating System the image is built to
100
+	// run on (especially for Windows).
101
+	OSVersion  string   `json:"os.version,omitempty"`
102
+	OSFeatures []string `json:"os.features,omitempty"`
103
+
104
+	// rawJSON caches the immutable JSON associated with this image.
105
+	rawJSON []byte
106
+
107
+	// computedID is the ID computed from the hash of the image config.
108
+	// Not to be confused with the legacy V1 ID in V1Image.
109
+	computedID ID
110
+
111
+	// Details holds additional details about image
112
+	Details *Details `json:"-"`
113
+}
114
+
115
+// Details provides additional image data
116
+type Details struct {
117
+	// ManifestDescriptor is the descriptor of the platform-specific manifest
118
+	// chosen by the [GetImage] call that returned this image.
119
+	// The exact descriptor depends on the [GetImageOpts.Platform] field
120
+	// passed to [GetImage] and the content availability.
121
+	// This is only set by the containerd image service.
122
+	ManifestDescriptor *ocispec.Descriptor
123
+}
124
+
125
+// RawJSON returns the immutable JSON associated with the image.
126
+func (img *Image) RawJSON() []byte {
127
+	return img.rawJSON
128
+}
129
+
130
+// ID returns the image's content-addressable ID.
131
+func (img *Image) ID() ID {
132
+	return img.computedID
133
+}
134
+
135
+// ImageID stringifies ID.
136
+func (img *Image) ImageID() string {
137
+	return img.ID().String()
138
+}
139
+
140
+// RunConfig returns the image's container config.
141
+func (img *Image) RunConfig() *container.Config {
142
+	return img.Config
143
+}
144
+
145
+// BaseImgArch returns the image's architecture. If not populated, defaults to the host runtime arch.
146
+func (img *Image) BaseImgArch() string {
147
+	arch := img.Architecture
148
+	if arch == "" {
149
+		arch = runtime.GOARCH
150
+	}
151
+	return arch
152
+}
153
+
154
+// BaseImgVariant returns the image's variant, whether populated or not.
155
+// This avoids creating an inconsistency where the stored image variant
156
+// is "greater than" (i.e. v8 vs v6) the actual image variant.
157
+func (img *Image) BaseImgVariant() string {
158
+	return img.Variant
159
+}
160
+
161
+// OperatingSystem returns the image's operating system. If not populated, defaults to the host runtime OS.
162
+func (img *Image) OperatingSystem() string {
163
+	os := img.OS
164
+	if os == "" {
165
+		os = runtime.GOOS
166
+	}
167
+	return os
168
+}
169
+
170
+// Platform generates an OCI platform from the image
171
+func (img *Image) Platform() ocispec.Platform {
172
+	return ocispec.Platform{
173
+		Architecture: img.Architecture,
174
+		OS:           img.OS,
175
+		OSVersion:    img.OSVersion,
176
+		OSFeatures:   img.OSFeatures,
177
+		Variant:      img.Variant,
178
+	}
179
+}
180
+
181
+// MarshalJSON serializes the image to JSON. It sorts the top-level keys so
182
+// that JSON that's been manipulated by a push/pull cycle with a legacy
183
+// registry won't end up with a different key order.
184
+func (img *Image) MarshalJSON() ([]byte, error) {
185
+	type MarshalImage Image
186
+
187
+	pass1, err := json.Marshal(MarshalImage(*img))
188
+	if err != nil {
189
+		return nil, err
190
+	}
191
+
192
+	var c map[string]*json.RawMessage
193
+	if err := json.Unmarshal(pass1, &c); err != nil {
194
+		return nil, err
195
+	}
196
+	return json.Marshal(c)
197
+}
198
+
199
+// ChildConfig is the configuration to apply to an Image to create a new
200
+// Child image. Other properties of the image are copied from the parent.
201
+type ChildConfig struct {
202
+	ContainerID     string
203
+	Author          string
204
+	Comment         string
205
+	DiffID          layer.DiffID
206
+	ContainerConfig *container.Config
207
+	Config          *container.Config
208
+}
209
+
210
+// NewImage creates a new image with the given ID
211
+func NewImage(id ID) *Image {
212
+	return &Image{
213
+		computedID: id,
214
+	}
215
+}
216
+
217
+// NewChildImage creates a new Image as a child of this image.
218
+func NewChildImage(img *Image, child ChildConfig, os string) *Image {
219
+	isEmptyLayer := layer.IsEmpty(child.DiffID)
220
+	var rootFS *RootFS
221
+	if img.RootFS != nil {
222
+		rootFS = img.RootFS.Clone()
223
+	} else {
224
+		rootFS = NewRootFS()
225
+	}
226
+
227
+	if !isEmptyLayer {
228
+		rootFS.Append(child.DiffID)
229
+	}
230
+	imgHistory := NewHistory(
231
+		child.Author,
232
+		child.Comment,
233
+		strings.Join(child.ContainerConfig.Cmd, " "),
234
+		isEmptyLayer)
235
+
236
+	return &Image{
237
+		V1Image: V1Image{
238
+			DockerVersion:   dockerversion.Version,
239
+			Config:          child.Config,
240
+			Architecture:    img.BaseImgArch(),
241
+			Variant:         img.BaseImgVariant(),
242
+			OS:              os,
243
+			Container:       child.ContainerID,
244
+			ContainerConfig: *child.ContainerConfig,
245
+			Author:          child.Author,
246
+			Created:         imgHistory.Created,
247
+		},
248
+		RootFS:     rootFS,
249
+		History:    append(img.History, imgHistory),
250
+		OSFeatures: img.OSFeatures,
251
+		OSVersion:  img.OSVersion,
252
+	}
253
+}
254
+
255
+// Clone clones an image and changes ID.
256
+func Clone(base *Image, id ID) *Image {
257
+	img := *base
258
+	img.RootFS = img.RootFS.Clone()
259
+	img.V1Image.ID = id.String()
260
+	img.computedID = id
261
+	return &img
262
+}
263
+
264
+// History stores build commands that were used to create an image
265
+type History = ocispec.History
266
+
267
+// NewHistory creates a new history struct from arguments, and sets the created
268
+// time to the current time in UTC
269
+func NewHistory(author, comment, createdBy string, isEmptyLayer bool) History {
270
+	now := time.Now().UTC()
271
+	return History{
272
+		Author:     author,
273
+		Created:    &now,
274
+		CreatedBy:  createdBy,
275
+		Comment:    comment,
276
+		EmptyLayer: isEmptyLayer,
277
+	}
278
+}
279
+
280
+// Exporter provides interface for loading and saving images
281
+type Exporter interface {
282
+	Load(context.Context, io.ReadCloser, io.Writer, bool) error
283
+	// TODO: Load(net.Context, io.ReadCloser, <- chan StatusMessage) error
284
+	Save(context.Context, []string, io.Writer) error
285
+}
286
+
287
+// NewFromJSON creates an Image configuration from json.
288
+func NewFromJSON(src []byte) (*Image, error) {
289
+	img := &Image{}
290
+
291
+	if err := json.Unmarshal(src, img); err != nil {
292
+		return nil, err
293
+	}
294
+	if img.RootFS == nil {
295
+		return nil, errors.New("invalid image JSON, no RootFS key")
296
+	}
297
+
298
+	img.rawJSON = src
299
+
300
+	return img, nil
301
+}
0 302
new file mode 100644
... ...
@@ -0,0 +1,18 @@
0
+package image
1
+
2
+import (
3
+	"errors"
4
+	"runtime"
5
+	"strings"
6
+
7
+	"github.com/docker/docker/errdefs"
8
+)
9
+
10
+// CheckOS checks if the given OS matches the host's platform, and
11
+// returns an error otherwise.
12
+func CheckOS(os string) error {
13
+	if !strings.EqualFold(runtime.GOOS, os) {
14
+		return errdefs.InvalidParameter(errors.New("operating system is not supported"))
15
+	}
16
+	return nil
17
+}
0 18
new file mode 100644
... ...
@@ -0,0 +1,125 @@
0
+package image
1
+
2
+import (
3
+	"encoding/json"
4
+	"runtime"
5
+	"sort"
6
+	"strings"
7
+	"testing"
8
+
9
+	"github.com/docker/docker/daemon/internal/layer"
10
+	"github.com/google/go-cmp/cmp"
11
+	"github.com/moby/moby/api/types/container"
12
+	"gotest.tools/v3/assert"
13
+	is "gotest.tools/v3/assert/cmp"
14
+)
15
+
16
+const sampleImageJSON = `{
17
+	"architecture": "amd64",
18
+	"os": "linux",
19
+	"config": {},
20
+	"rootfs": {
21
+		"type": "layers",
22
+		"diff_ids": []
23
+	}
24
+}`
25
+
26
+func TestNewFromJSON(t *testing.T) {
27
+	img, err := NewFromJSON([]byte(sampleImageJSON))
28
+	assert.NilError(t, err)
29
+	assert.Check(t, is.Equal(sampleImageJSON, string(img.RawJSON())))
30
+}
31
+
32
+func TestNewFromJSONWithInvalidJSON(t *testing.T) {
33
+	_, err := NewFromJSON([]byte("{}"))
34
+	assert.Check(t, is.Error(err, "invalid image JSON, no RootFS key"))
35
+}
36
+
37
+func TestMarshalKeyOrder(t *testing.T) {
38
+	b, err := json.Marshal(&Image{
39
+		V1Image: V1Image{
40
+			Comment:      "a",
41
+			Author:       "b",
42
+			Architecture: "c",
43
+		},
44
+	})
45
+	assert.Check(t, err)
46
+
47
+	expectedOrder := []string{"architecture", "author", "comment"}
48
+	var indexes []int
49
+	for _, k := range expectedOrder {
50
+		indexes = append(indexes, strings.Index(string(b), k))
51
+	}
52
+
53
+	if !sort.IntsAreSorted(indexes) {
54
+		t.Fatal("invalid key order in JSON: ", string(b))
55
+	}
56
+}
57
+
58
+func TestImage(t *testing.T) {
59
+	cid := "50a16564e727"
60
+	config := &container.Config{
61
+		Hostname:   "hostname",
62
+		Domainname: "domain",
63
+		User:       "root",
64
+	}
65
+	os := runtime.GOOS
66
+
67
+	img := &Image{
68
+		V1Image: V1Image{
69
+			Config: config,
70
+		},
71
+		computedID: ID(cid),
72
+	}
73
+
74
+	assert.Check(t, is.Equal(cid, img.ImageID()))
75
+	assert.Check(t, is.Equal(cid, img.ID().String()))
76
+	assert.Check(t, is.Equal(os, img.OperatingSystem()))
77
+	assert.Check(t, is.DeepEqual(config, img.RunConfig()))
78
+}
79
+
80
+func TestImageOSNotEmpty(t *testing.T) {
81
+	os := "os"
82
+	img := &Image{
83
+		V1Image: V1Image{
84
+			OS: os,
85
+		},
86
+		OSVersion: "osversion",
87
+	}
88
+	assert.Check(t, is.Equal(os, img.OperatingSystem()))
89
+}
90
+
91
+func TestNewChildImageFromImageWithRootFS(t *testing.T) {
92
+	rootFS := NewRootFS()
93
+	rootFS.Append("ba5e")
94
+	parent := &Image{
95
+		RootFS: rootFS,
96
+		History: []History{
97
+			NewHistory("a", "c", "r", false),
98
+		},
99
+	}
100
+	childConfig := ChildConfig{
101
+		DiffID:  layer.DiffID("abcdef"),
102
+		Author:  "author",
103
+		Comment: "comment",
104
+		ContainerConfig: &container.Config{
105
+			Cmd: []string{"echo", "foo"},
106
+		},
107
+		Config: &container.Config{},
108
+	}
109
+
110
+	newImage := NewChildImage(parent, childConfig, "platform")
111
+	expectedDiffIDs := []layer.DiffID{"ba5e", "abcdef"}
112
+	assert.Check(t, is.DeepEqual(expectedDiffIDs, newImage.RootFS.DiffIDs))
113
+	assert.Check(t, is.Equal(childConfig.Author, newImage.Author))
114
+	assert.Check(t, is.DeepEqual(childConfig.Config, newImage.Config))
115
+	assert.Check(t, is.DeepEqual(*childConfig.ContainerConfig, newImage.ContainerConfig))
116
+	assert.Check(t, is.Equal("platform", newImage.OS))
117
+	assert.Check(t, is.DeepEqual(childConfig.Config, newImage.Config))
118
+
119
+	assert.Check(t, is.Len(newImage.History, 2))
120
+	assert.Check(t, is.Equal(childConfig.Comment, newImage.History[1].Comment))
121
+
122
+	assert.Check(t, !cmp.Equal(parent.RootFS.DiffIDs, newImage.RootFS.DiffIDs),
123
+		"RootFS should be copied not mutated")
124
+}
0 125
new file mode 100644
... ...
@@ -0,0 +1,45 @@
0
+// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
1
+//go:build go1.23
2
+
3
+package image
4
+
5
+import (
6
+	"slices"
7
+
8
+	"github.com/docker/docker/daemon/internal/layer"
9
+	"github.com/opencontainers/image-spec/identity"
10
+)
11
+
12
+// TypeLayers is used for RootFS.Type for filesystems organized into layers.
13
+const TypeLayers = "layers"
14
+
15
+// RootFS describes images root filesystem
16
+// This is currently a placeholder that only supports layers. In the future
17
+// this can be made into an interface that supports different implementations.
18
+type RootFS struct {
19
+	Type    string         `json:"type"`
20
+	DiffIDs []layer.DiffID `json:"diff_ids,omitempty"`
21
+}
22
+
23
+// NewRootFS returns empty RootFS struct
24
+func NewRootFS() *RootFS {
25
+	return &RootFS{Type: TypeLayers}
26
+}
27
+
28
+// Append appends a new diffID to rootfs
29
+func (r *RootFS) Append(id layer.DiffID) {
30
+	r.DiffIDs = append(r.DiffIDs, id)
31
+}
32
+
33
+// Clone returns a copy of the RootFS
34
+func (r *RootFS) Clone() *RootFS {
35
+	return &RootFS{
36
+		Type:    r.Type,
37
+		DiffIDs: slices.Clone(r.DiffIDs),
38
+	}
39
+}
40
+
41
+// ChainID returns the ChainID for the top layer in RootFS.
42
+func (r *RootFS) ChainID() layer.ChainID {
43
+	return identity.ChainID(r.DiffIDs)
44
+}
0 45
new file mode 100644
... ...
@@ -0,0 +1,4 @@
0
+# Docker Image Specification v1.
1
+
2
+This specification moved to a separate repository:
3
+https://github.com/moby/docker-image-spec
0 4
new file mode 100644
... ...
@@ -0,0 +1,4 @@
0
+# Docker Image Specification v1.3.0
1
+
2
+This specification moved to a separate repository:
3
+https://github.com/moby/docker-image-spec
0 4
new file mode 100644
... ...
@@ -0,0 +1,4 @@
0
+# Docker Image Specification v1.1.0
1
+
2
+This specification moved to a separate repository:
3
+https://github.com/moby/docker-image-spec
0 4
new file mode 100644
... ...
@@ -0,0 +1,4 @@
0
+# Docker Image Specification v1.2.0
1
+
2
+This specification moved to a separate repository:
3
+https://github.com/moby/docker-image-spec
0 4
new file mode 100644
... ...
@@ -0,0 +1,4 @@
0
+# Docker Image Specification v1.0.0
1
+
2
+This specification moved to a separate repository:
3
+https://github.com/moby/docker-image-spec
0 4
new file mode 100644
... ...
@@ -0,0 +1,367 @@
0
+package image
1
+
2
+import (
3
+	"context"
4
+	"fmt"
5
+	"os"
6
+	"sync"
7
+	"time"
8
+
9
+	"github.com/containerd/log"
10
+	"github.com/docker/docker/daemon/internal/layer"
11
+	"github.com/docker/docker/errdefs"
12
+	"github.com/opencontainers/go-digest"
13
+	"github.com/opencontainers/go-digest/digestset"
14
+	"github.com/pkg/errors"
15
+)
16
+
17
+// Store is an interface for creating and accessing images
18
+type Store interface {
19
+	Create(config []byte) (ID, error)
20
+	Get(id ID) (*Image, error)
21
+	Delete(id ID) ([]layer.Metadata, error)
22
+	Search(partialID string) (ID, error)
23
+	SetParent(id ID, parent ID) error
24
+	GetParent(id ID) (ID, error)
25
+	SetLastUpdated(id ID) error
26
+	GetLastUpdated(id ID) (time.Time, error)
27
+	SetBuiltLocally(id ID) error
28
+	IsBuiltLocally(id ID) (bool, error)
29
+	Children(id ID) []ID
30
+	Map() map[ID]*Image
31
+	Heads() map[ID]*Image
32
+	Len() int
33
+}
34
+
35
+// LayerGetReleaser is a minimal interface for getting and releasing images.
36
+type LayerGetReleaser interface {
37
+	Get(layer.ChainID) (layer.Layer, error)
38
+	Release(layer.Layer) ([]layer.Metadata, error)
39
+}
40
+
41
+type imageMeta struct {
42
+	layer    layer.Layer
43
+	children map[ID]struct{}
44
+}
45
+
46
+type store struct {
47
+	sync.RWMutex
48
+	lss       LayerGetReleaser
49
+	images    map[ID]*imageMeta
50
+	fs        StoreBackend
51
+	digestSet *digestset.Set
52
+}
53
+
54
+// NewImageStore returns new store object for given set of layer stores
55
+func NewImageStore(fs StoreBackend, lss LayerGetReleaser) (Store, error) {
56
+	is := &store{
57
+		lss:       lss,
58
+		images:    make(map[ID]*imageMeta),
59
+		fs:        fs,
60
+		digestSet: digestset.NewSet(),
61
+	}
62
+
63
+	// load all current images and retain layers
64
+	if err := is.restore(); err != nil {
65
+		return nil, err
66
+	}
67
+
68
+	return is, nil
69
+}
70
+
71
+func (is *store) restore() error {
72
+	// As the code below is run when restoring all images (which can be "many"),
73
+	// constructing the "log.G(ctx).WithFields" is deliberately not "DRY", as the
74
+	// logger is only used for error-cases, and we don't want to do allocations
75
+	// if we don't need it. The "f" type alias is here is just for convenience,
76
+	// and to make the code _slightly_ more DRY. See the discussion on GitHub;
77
+	// https://github.com/moby/moby/pull/44426#discussion_r1059519071
78
+	type f = log.Fields
79
+	err := is.fs.Walk(func(dgst digest.Digest) error {
80
+		img, err := is.Get(ID(dgst))
81
+		if err != nil {
82
+			log.G(context.TODO()).WithFields(f{"digest": dgst, "err": err}).Error("invalid image")
83
+			return nil
84
+		}
85
+		var l layer.Layer
86
+		if chainID := img.RootFS.ChainID(); chainID != "" {
87
+			if err := CheckOS(img.OperatingSystem()); err != nil {
88
+				log.G(context.TODO()).WithFields(f{"chainID": chainID, "os": img.OperatingSystem()}).Error("not restoring image with unsupported operating system")
89
+				return nil
90
+			}
91
+			l, err = is.lss.Get(chainID)
92
+			if err != nil {
93
+				if errors.Is(err, layer.ErrLayerDoesNotExist) {
94
+					log.G(context.TODO()).WithFields(f{"chainID": chainID, "os": img.OperatingSystem(), "err": err}).Error("not restoring image")
95
+					return nil
96
+				}
97
+				return err
98
+			}
99
+		}
100
+		if err := is.digestSet.Add(dgst); err != nil {
101
+			return err
102
+		}
103
+
104
+		is.images[ID(dgst)] = &imageMeta{
105
+			layer:    l,
106
+			children: make(map[ID]struct{}),
107
+		}
108
+
109
+		return nil
110
+	})
111
+	if err != nil {
112
+		return err
113
+	}
114
+
115
+	// Second pass to fill in children maps
116
+	for id := range is.images {
117
+		if parent, err := is.GetParent(id); err == nil {
118
+			if parentMeta := is.images[parent]; parentMeta != nil {
119
+				parentMeta.children[id] = struct{}{}
120
+			}
121
+		}
122
+	}
123
+
124
+	return nil
125
+}
126
+
127
+func (is *store) Create(config []byte) (ID, error) {
128
+	var img *Image
129
+	img, err := NewFromJSON(config)
130
+	if err != nil {
131
+		return "", err
132
+	}
133
+
134
+	// Must reject any config that references diffIDs from the history
135
+	// which aren't among the rootfs layers.
136
+	rootFSLayers := make(map[layer.DiffID]struct{})
137
+	for _, diffID := range img.RootFS.DiffIDs {
138
+		rootFSLayers[diffID] = struct{}{}
139
+	}
140
+
141
+	layerCounter := 0
142
+	for _, h := range img.History {
143
+		if !h.EmptyLayer {
144
+			layerCounter++
145
+		}
146
+	}
147
+	if layerCounter > len(img.RootFS.DiffIDs) {
148
+		return "", errdefs.InvalidParameter(errors.New("too many non-empty layers in History section"))
149
+	}
150
+
151
+	imageDigest, err := is.fs.Set(config)
152
+	if err != nil {
153
+		return "", errdefs.InvalidParameter(err)
154
+	}
155
+
156
+	is.Lock()
157
+	defer is.Unlock()
158
+
159
+	imageID := ID(imageDigest)
160
+	if _, exists := is.images[imageID]; exists {
161
+		return imageID, nil
162
+	}
163
+
164
+	layerID := img.RootFS.ChainID()
165
+
166
+	var l layer.Layer
167
+	if layerID != "" {
168
+		if err := CheckOS(img.OperatingSystem()); err != nil {
169
+			return "", err
170
+		}
171
+		l, err = is.lss.Get(layerID)
172
+		if err != nil {
173
+			return "", errdefs.InvalidParameter(errors.Wrapf(err, "failed to get layer %s", layerID))
174
+		}
175
+	}
176
+
177
+	is.images[imageID] = &imageMeta{
178
+		layer:    l,
179
+		children: make(map[ID]struct{}),
180
+	}
181
+
182
+	if err = is.digestSet.Add(imageDigest); err != nil {
183
+		delete(is.images, imageID)
184
+		return "", errdefs.InvalidParameter(err)
185
+	}
186
+
187
+	return imageID, nil
188
+}
189
+
190
+type imageNotFoundError string
191
+
192
+func (e imageNotFoundError) Error() string {
193
+	return "No such image: " + string(e)
194
+}
195
+
196
+func (imageNotFoundError) NotFound() {}
197
+
198
+func (is *store) Search(term string) (ID, error) {
199
+	dgst, err := is.digestSet.Lookup(term)
200
+	if err != nil {
201
+		if errors.Is(err, digestset.ErrDigestNotFound) {
202
+			err = imageNotFoundError(term)
203
+		}
204
+		return "", errors.WithStack(err)
205
+	}
206
+	return ID(dgst), nil
207
+}
208
+
209
+func (is *store) Get(id ID) (*Image, error) {
210
+	// todo: Check if image is in images
211
+	// todo: Detect manual insertions and start using them
212
+	config, err := is.fs.Get(id.Digest())
213
+	if err != nil {
214
+		return nil, errdefs.NotFound(err)
215
+	}
216
+
217
+	img, err := NewFromJSON(config)
218
+	if err != nil {
219
+		return nil, errdefs.InvalidParameter(err)
220
+	}
221
+	img.computedID = id
222
+
223
+	img.Parent, err = is.GetParent(id)
224
+	if err != nil {
225
+		img.Parent = ""
226
+	}
227
+
228
+	return img, nil
229
+}
230
+
231
+func (is *store) Delete(id ID) ([]layer.Metadata, error) {
232
+	is.Lock()
233
+	defer is.Unlock()
234
+
235
+	imgMeta := is.images[id]
236
+	if imgMeta == nil {
237
+		return nil, errdefs.NotFound(fmt.Errorf("unrecognized image ID %s", id.String()))
238
+	}
239
+	_, err := is.Get(id)
240
+	if err != nil {
241
+		return nil, errdefs.NotFound(fmt.Errorf("unrecognized image %s, %v", id.String(), err))
242
+	}
243
+	for cID := range imgMeta.children {
244
+		is.fs.DeleteMetadata(cID.Digest(), "parent")
245
+	}
246
+	if parent, err := is.GetParent(id); err == nil && is.images[parent] != nil {
247
+		delete(is.images[parent].children, id)
248
+	}
249
+
250
+	if err := is.digestSet.Remove(id.Digest()); err != nil {
251
+		log.G(context.TODO()).Errorf("error removing %s from digest set: %q", id, err)
252
+	}
253
+	delete(is.images, id)
254
+	is.fs.Delete(id.Digest())
255
+
256
+	if imgMeta.layer != nil {
257
+		return is.lss.Release(imgMeta.layer)
258
+	}
259
+	return nil, nil
260
+}
261
+
262
+func (is *store) SetParent(id, parentID ID) error {
263
+	is.Lock()
264
+	defer is.Unlock()
265
+	parentMeta := is.images[parentID]
266
+	if parentMeta == nil {
267
+		return errdefs.NotFound(fmt.Errorf("unknown parent image ID %s", parentID.String()))
268
+	}
269
+	if parent, err := is.GetParent(id); err == nil && is.images[parent] != nil {
270
+		delete(is.images[parent].children, id)
271
+	}
272
+	parentMeta.children[id] = struct{}{}
273
+	return is.fs.SetMetadata(id.Digest(), "parent", []byte(parentID))
274
+}
275
+
276
+func (is *store) GetParent(id ID) (ID, error) {
277
+	d, err := is.fs.GetMetadata(id.Digest(), "parent")
278
+	if err != nil {
279
+		return "", errdefs.NotFound(err)
280
+	}
281
+	return ID(d), nil // todo: validate?
282
+}
283
+
284
+// SetLastUpdated time for the image ID to the current time
285
+func (is *store) SetLastUpdated(id ID) error {
286
+	lastUpdated := []byte(time.Now().Format(time.RFC3339Nano))
287
+	return is.fs.SetMetadata(id.Digest(), "lastUpdated", lastUpdated)
288
+}
289
+
290
+// GetLastUpdated time for the image ID
291
+func (is *store) GetLastUpdated(id ID) (time.Time, error) {
292
+	bytes, err := is.fs.GetMetadata(id.Digest(), "lastUpdated")
293
+	if err != nil || len(bytes) == 0 {
294
+		// No lastUpdated time
295
+		return time.Time{}, nil
296
+	}
297
+	return time.Parse(time.RFC3339Nano, string(bytes))
298
+}
299
+
300
+// SetBuiltLocally sets whether image can be used as a builder cache
301
+func (is *store) SetBuiltLocally(id ID) error {
302
+	return is.fs.SetMetadata(id.Digest(), "builtLocally", []byte{1})
303
+}
304
+
305
+// IsBuiltLocally returns whether image can be used as a builder cache
306
+func (is *store) IsBuiltLocally(id ID) (bool, error) {
307
+	bytes, err := is.fs.GetMetadata(id.Digest(), "builtLocally")
308
+	if err != nil || len(bytes) == 0 {
309
+		if errors.Is(err, os.ErrNotExist) {
310
+			err = nil
311
+		}
312
+		return false, err
313
+	}
314
+	return bytes[0] == 1, nil
315
+}
316
+
317
+func (is *store) Children(id ID) []ID {
318
+	is.RLock()
319
+	defer is.RUnlock()
320
+
321
+	return is.children(id)
322
+}
323
+
324
+func (is *store) children(id ID) []ID {
325
+	var ids []ID
326
+	if is.images[id] != nil {
327
+		for id := range is.images[id].children {
328
+			ids = append(ids, id)
329
+		}
330
+	}
331
+	return ids
332
+}
333
+
334
+func (is *store) Heads() map[ID]*Image {
335
+	return is.imagesMap(false)
336
+}
337
+
338
+func (is *store) Map() map[ID]*Image {
339
+	return is.imagesMap(true)
340
+}
341
+
342
+func (is *store) imagesMap(all bool) map[ID]*Image {
343
+	is.RLock()
344
+	defer is.RUnlock()
345
+
346
+	images := make(map[ID]*Image)
347
+
348
+	for id := range is.images {
349
+		if !all && len(is.children(id)) > 0 {
350
+			continue
351
+		}
352
+		img, err := is.Get(id)
353
+		if err != nil {
354
+			log.G(context.TODO()).Errorf("invalid image access: %q, error: %q", id, err)
355
+			continue
356
+		}
357
+		images[id] = img
358
+	}
359
+	return images
360
+}
361
+
362
+func (is *store) Len() int {
363
+	is.RLock()
364
+	defer is.RUnlock()
365
+	return len(is.images)
366
+}
0 367
new file mode 100644
... ...
@@ -0,0 +1,207 @@
0
+package image
1
+
2
+import (
3
+	"fmt"
4
+	"testing"
5
+
6
+	cerrdefs "github.com/containerd/errdefs"
7
+	"github.com/docker/docker/daemon/internal/layer"
8
+	"gotest.tools/v3/assert"
9
+	is "gotest.tools/v3/assert/cmp"
10
+)
11
+
12
+func TestCreate(t *testing.T) {
13
+	imgStore := defaultImageStore(t)
14
+	_, err := imgStore.Create([]byte(`{}`))
15
+	assert.Check(t, is.Error(err, "invalid image JSON, no RootFS key"))
16
+}
17
+
18
+func TestRestore(t *testing.T) {
19
+	fsStore := defaultFSStoreBackend(t)
20
+
21
+	id1, err := fsStore.Set([]byte(`{"comment": "abc", "rootfs": {"type": "layers"}}`))
22
+	assert.NilError(t, err)
23
+
24
+	_, err = fsStore.Set([]byte(`invalid`))
25
+	assert.NilError(t, err)
26
+
27
+	id2, err := fsStore.Set([]byte(`{"comment": "def", "rootfs": {"type": "layers", "diff_ids": ["2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"]}}`))
28
+	assert.NilError(t, err)
29
+
30
+	err = fsStore.SetMetadata(id2, "parent", []byte(id1))
31
+	assert.NilError(t, err)
32
+
33
+	// This produces an error log (trying to unmarshal the "invalid" value from above, but doesn't return an error;
34
+	// ERRO[0000] invalid image                                 digest="sha256:f1234d75178d892a133a410355a5a990cf75d2f33eba25d575943d4df632f3a4" err="invalid character 'i' looking for beginning of value: invalid"
35
+	imgStore, err := NewImageStore(fsStore, &mockLayerGetReleaser{})
36
+	assert.NilError(t, err)
37
+
38
+	assert.Check(t, is.Len(imgStore.Map(), 2))
39
+
40
+	img1, err := imgStore.Get(ID(id1))
41
+	assert.NilError(t, err)
42
+	assert.Check(t, is.Equal(ID(id1), img1.computedID))
43
+	assert.Check(t, is.Equal(string(id1), img1.computedID.String()))
44
+
45
+	img2, err := imgStore.Get(ID(id2))
46
+	assert.NilError(t, err)
47
+	assert.Check(t, is.Equal("abc", img1.Comment))
48
+	assert.Check(t, is.Equal("def", img2.Comment))
49
+
50
+	_, err = imgStore.GetParent(ID(id1))
51
+	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
52
+	assert.ErrorContains(t, err, "failed to read metadata")
53
+
54
+	p, err := imgStore.GetParent(ID(id2))
55
+	assert.NilError(t, err)
56
+	assert.Check(t, is.Equal(ID(id1), p))
57
+
58
+	children := imgStore.Children(ID(id1))
59
+	assert.Check(t, is.Len(children, 1))
60
+	assert.Check(t, is.Equal(ID(id2), children[0]))
61
+	assert.Check(t, is.Len(imgStore.Heads(), 1))
62
+
63
+	sid1, err := imgStore.Search(string(id1)[:10])
64
+	assert.NilError(t, err)
65
+	assert.Check(t, is.Equal(ID(id1), sid1))
66
+
67
+	sid1, err = imgStore.Search(id1.Encoded()[:6])
68
+	assert.NilError(t, err)
69
+	assert.Check(t, is.Equal(ID(id1), sid1))
70
+
71
+	invalidPattern := id1.Encoded()[1:6]
72
+	_, err = imgStore.Search(invalidPattern)
73
+	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
74
+	assert.Check(t, is.ErrorContains(err, invalidPattern))
75
+}
76
+
77
+func TestAddDelete(t *testing.T) {
78
+	imgStore := defaultImageStore(t)
79
+
80
+	id1, err := imgStore.Create([]byte(`{"comment": "abc", "rootfs": {"type": "layers", "diff_ids": ["2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"]}}`))
81
+	assert.NilError(t, err)
82
+	assert.Check(t, is.Equal(ID("sha256:8d25a9c45df515f9d0fe8e4a6b1c64dd3b965a84790ddbcc7954bb9bc89eb993"), id1))
83
+
84
+	img, err := imgStore.Get(id1)
85
+	assert.NilError(t, err)
86
+	assert.Check(t, is.Equal("abc", img.Comment))
87
+
88
+	id2, err := imgStore.Create([]byte(`{"comment": "def", "rootfs": {"type": "layers", "diff_ids": ["2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"]}}`))
89
+	assert.NilError(t, err)
90
+
91
+	err = imgStore.SetParent(id2, id1)
92
+	assert.NilError(t, err)
93
+
94
+	pid1, err := imgStore.GetParent(id2)
95
+	assert.NilError(t, err)
96
+	assert.Check(t, is.Equal(pid1, id1))
97
+
98
+	_, err = imgStore.Delete(id1)
99
+	assert.NilError(t, err)
100
+
101
+	_, err = imgStore.Get(id1)
102
+	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
103
+	assert.ErrorContains(t, err, "failed to get digest")
104
+
105
+	_, err = imgStore.Get(id2)
106
+	assert.NilError(t, err)
107
+
108
+	_, err = imgStore.GetParent(id2)
109
+	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
110
+	assert.ErrorContains(t, err, "failed to read metadata")
111
+}
112
+
113
+func TestSearchAfterDelete(t *testing.T) {
114
+	imgStore := defaultImageStore(t)
115
+
116
+	id, err := imgStore.Create([]byte(`{"comment": "abc", "rootfs": {"type": "layers"}}`))
117
+	assert.NilError(t, err)
118
+
119
+	id1, err := imgStore.Search(string(id)[:15])
120
+	assert.NilError(t, err)
121
+	assert.Check(t, is.Equal(id1, id))
122
+
123
+	_, err = imgStore.Delete(id)
124
+	assert.NilError(t, err)
125
+
126
+	_, err = imgStore.Search(string(id)[:15])
127
+	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
128
+	assert.ErrorContains(t, err, "No such image")
129
+}
130
+
131
+func TestDeleteNotExisting(t *testing.T) {
132
+	imgStore := defaultImageStore(t)
133
+	_, err := imgStore.Delete(ID("i_dont_exists"))
134
+	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
135
+}
136
+
137
+func TestParentReset(t *testing.T) {
138
+	imgStore := defaultImageStore(t)
139
+
140
+	id, err := imgStore.Create([]byte(`{"comment": "abc1", "rootfs": {"type": "layers"}}`))
141
+	assert.NilError(t, err)
142
+
143
+	id2, err := imgStore.Create([]byte(`{"comment": "abc2", "rootfs": {"type": "layers"}}`))
144
+	assert.NilError(t, err)
145
+
146
+	id3, err := imgStore.Create([]byte(`{"comment": "abc3", "rootfs": {"type": "layers"}}`))
147
+	assert.NilError(t, err)
148
+
149
+	assert.Check(t, imgStore.SetParent(id, id2))
150
+	assert.Check(t, is.Len(imgStore.Children(id2), 1))
151
+
152
+	assert.Check(t, imgStore.SetParent(id, id3))
153
+	assert.Check(t, is.Len(imgStore.Children(id2), 0))
154
+	assert.Check(t, is.Len(imgStore.Children(id3), 1))
155
+}
156
+
157
+func defaultImageStore(t *testing.T) Store {
158
+	t.Helper()
159
+	fsBackend, err := NewFSStoreBackend(t.TempDir())
160
+	assert.Check(t, err)
161
+
162
+	imgStore, err := NewImageStore(fsBackend, &mockLayerGetReleaser{})
163
+	assert.NilError(t, err)
164
+
165
+	return imgStore
166
+}
167
+
168
+func TestGetAndSetLastUpdated(t *testing.T) {
169
+	imgStore := defaultImageStore(t)
170
+
171
+	id, err := imgStore.Create([]byte(`{"comment": "abc1", "rootfs": {"type": "layers"}}`))
172
+	assert.NilError(t, err)
173
+
174
+	updated, err := imgStore.GetLastUpdated(id)
175
+	assert.NilError(t, err)
176
+	assert.Check(t, is.Equal(updated.IsZero(), true))
177
+
178
+	assert.Check(t, imgStore.SetLastUpdated(id))
179
+
180
+	updated, err = imgStore.GetLastUpdated(id)
181
+	assert.NilError(t, err)
182
+	assert.Check(t, is.Equal(updated.IsZero(), false))
183
+}
184
+
185
+func TestStoreLen(t *testing.T) {
186
+	imgStore := defaultImageStore(t)
187
+
188
+	expected := 10
189
+	for i := range expected {
190
+		_, err := imgStore.Create([]byte(fmt.Sprintf(`{"comment": "abc%d", "rootfs": {"type": "layers"}}`, i)))
191
+		assert.NilError(t, err)
192
+	}
193
+	numImages := imgStore.Len()
194
+	assert.Equal(t, expected, numImages)
195
+	assert.Equal(t, len(imgStore.Map()), numImages)
196
+}
197
+
198
+type mockLayerGetReleaser struct{}
199
+
200
+func (ls *mockLayerGetReleaser) Get(layer.ChainID) (layer.Layer, error) {
201
+	return nil, nil
202
+}
203
+
204
+func (ls *mockLayerGetReleaser) Release(layer.Layer) ([]layer.Metadata, error) {
205
+	return nil, nil
206
+}
0 207
new file mode 100644
... ...
@@ -0,0 +1,298 @@
0
+package tarexport
1
+
2
+import (
3
+	"context"
4
+	"encoding/json"
5
+	"errors"
6
+	"fmt"
7
+	"io"
8
+	"os"
9
+	"path/filepath"
10
+	"reflect"
11
+	"runtime"
12
+
13
+	"github.com/containerd/containerd/v2/pkg/tracing"
14
+	"github.com/containerd/log"
15
+	"github.com/distribution/reference"
16
+	"github.com/docker/distribution"
17
+	"github.com/docker/docker/daemon/internal/image"
18
+	"github.com/docker/docker/daemon/internal/layer"
19
+	"github.com/docker/docker/internal/ioutils"
20
+	"github.com/docker/docker/pkg/progress"
21
+	"github.com/docker/docker/pkg/streamformatter"
22
+	"github.com/docker/docker/pkg/stringid"
23
+	"github.com/moby/go-archive/chrootarchive"
24
+	"github.com/moby/go-archive/compression"
25
+	"github.com/moby/moby/api/types/events"
26
+	"github.com/moby/sys/sequential"
27
+	"github.com/moby/sys/symlink"
28
+	"github.com/opencontainers/go-digest"
29
+)
30
+
31
+func (l *tarexporter) Load(ctx context.Context, inTar io.ReadCloser, outStream io.Writer, quiet bool) (outErr error) {
32
+	ctx, span := tracing.StartSpan(ctx, "tarexport.Load")
33
+	defer span.End()
34
+	defer func() {
35
+		span.SetStatus(outErr)
36
+	}()
37
+
38
+	var progressOutput progress.Output
39
+	if !quiet {
40
+		progressOutput = streamformatter.NewJSONProgressOutput(outStream, false)
41
+	}
42
+	outStream = streamformatter.NewStdoutWriter(outStream)
43
+
44
+	tmpDir, err := os.MkdirTemp("", "docker-import-")
45
+	if err != nil {
46
+		return err
47
+	}
48
+	defer os.RemoveAll(tmpDir)
49
+
50
+	if err := untar(ctx, inTar, tmpDir); err != nil {
51
+		return err
52
+	}
53
+
54
+	// read manifest, if no file then load in legacy mode
55
+	manifestPath, err := safePath(tmpDir, manifestFileName)
56
+	if err != nil {
57
+		return err
58
+	}
59
+	manifestFile, err := os.Open(manifestPath)
60
+	if err != nil {
61
+		if os.IsNotExist(err) {
62
+			return fmt.Errorf("invalid archive: does not contain a %s", manifestFileName)
63
+		}
64
+		return fmt.Errorf("invalid archive: failed to load %s: %w", manifestFileName, err)
65
+	}
66
+	defer manifestFile.Close()
67
+
68
+	var manifest []manifestItem
69
+	if err := json.NewDecoder(manifestFile).Decode(&manifest); err != nil {
70
+		return fmt.Errorf("invalid archive: failed to decode %s: %w", manifestFileName, err)
71
+	}
72
+
73
+	// a nil manifest usually indicates a bug, so don't just silently fail.
74
+	// if someone really needs to pass an empty manifest, they can pass [].
75
+	if manifest == nil {
76
+		return errors.New("invalid manifest, manifest cannot be null (but can be [])")
77
+	}
78
+
79
+	var parentLinks []parentLink
80
+	var imageIDsStr string
81
+	var imageRefCount int
82
+
83
+	for _, m := range manifest {
84
+		select {
85
+		case <-ctx.Done():
86
+			return ctx.Err()
87
+		default:
88
+		}
89
+		configPath, err := safePath(tmpDir, m.Config)
90
+		if err != nil {
91
+			return err
92
+		}
93
+		config, err := os.ReadFile(configPath)
94
+		if err != nil {
95
+			return err
96
+		}
97
+		img, err := image.NewFromJSON(config)
98
+		if err != nil {
99
+			return err
100
+		}
101
+		if err := image.CheckOS(img.OperatingSystem()); err != nil {
102
+			return fmt.Errorf("cannot load %s image on %s", img.OperatingSystem(), runtime.GOOS)
103
+		}
104
+		if l.platformMatcher != nil && !l.platformMatcher.Match(img.Platform()) {
105
+			continue
106
+		}
107
+		rootFS := *img.RootFS
108
+		rootFS.DiffIDs = nil
109
+
110
+		if expected, actual := len(m.Layers), len(img.RootFS.DiffIDs); expected != actual {
111
+			return fmt.Errorf("invalid manifest, layers length mismatch: expected %d, got %d", expected, actual)
112
+		}
113
+
114
+		for i, diffID := range img.RootFS.DiffIDs {
115
+			select {
116
+			case <-ctx.Done():
117
+				return ctx.Err()
118
+			default:
119
+			}
120
+			layerPath, err := safePath(tmpDir, m.Layers[i])
121
+			if err != nil {
122
+				return err
123
+			}
124
+			r := rootFS
125
+			r.Append(diffID)
126
+			newLayer, err := l.lss.Get(r.ChainID())
127
+			if err != nil {
128
+				newLayer, err = l.loadLayer(ctx, layerPath, rootFS, diffID.String(), m.LayerSources[diffID], progressOutput)
129
+				if err != nil {
130
+					return err
131
+				}
132
+			}
133
+			defer layer.ReleaseAndLog(l.lss, newLayer)
134
+			if expected, actual := diffID, newLayer.DiffID(); expected != actual {
135
+				return fmt.Errorf("invalid diffID for layer %d: expected %q, got %q", i, expected, actual)
136
+			}
137
+			rootFS.Append(diffID)
138
+		}
139
+
140
+		imgID, err := l.is.Create(config)
141
+		if err != nil {
142
+			return err
143
+		}
144
+		imageIDsStr += fmt.Sprintf("Loaded image ID: %s\n", imgID)
145
+
146
+		imageRefCount = 0
147
+		for _, repoTag := range m.RepoTags {
148
+			named, err := reference.ParseNormalizedNamed(repoTag)
149
+			if err != nil {
150
+				return err
151
+			}
152
+			ref, ok := named.(reference.NamedTagged)
153
+			if !ok {
154
+				return fmt.Errorf("invalid tag %q", repoTag)
155
+			}
156
+			l.setLoadedTag(ref, imgID.Digest(), outStream)
157
+			fmt.Fprintf(outStream, "Loaded image: %s\n", reference.FamiliarString(ref))
158
+			imageRefCount++
159
+		}
160
+
161
+		parentLinks = append(parentLinks, parentLink{imgID, m.Parent})
162
+		l.loggerImgEvent.LogImageEvent(ctx, imgID.String(), imgID.String(), events.ActionLoad)
163
+	}
164
+
165
+	for _, p := range validatedParentLinks(parentLinks) {
166
+		if p.parentID != "" {
167
+			if err := l.setParentID(p.id, p.parentID); err != nil {
168
+				return err
169
+			}
170
+		}
171
+	}
172
+
173
+	if imageRefCount == 0 {
174
+		outStream.Write([]byte(imageIDsStr))
175
+	}
176
+
177
+	return nil
178
+}
179
+
180
+func untar(ctx context.Context, inTar io.ReadCloser, tmpDir string) error {
181
+	_, trace := tracing.StartSpan(ctx, "chrootarchive.Untar")
182
+	defer trace.End()
183
+
184
+	err := chrootarchive.Untar(ioutils.NewCtxReader(ctx, inTar), tmpDir, nil)
185
+	trace.SetStatus(err)
186
+	return err
187
+}
188
+
189
+func (l *tarexporter) setParentID(id, parentID image.ID) error {
190
+	img, err := l.is.Get(id)
191
+	if err != nil {
192
+		return err
193
+	}
194
+	parent, err := l.is.Get(parentID)
195
+	if err != nil {
196
+		return err
197
+	}
198
+	if !checkValidParent(img, parent) {
199
+		return fmt.Errorf("image %v is not a valid parent for %v", parent.ID(), img.ID())
200
+	}
201
+	return l.is.SetParent(id, parentID)
202
+}
203
+
204
+func (l *tarexporter) loadLayer(ctx context.Context, filename string, rootFS image.RootFS, id string, foreignSrc distribution.Descriptor, progressOutput progress.Output) (_ layer.Layer, outErr error) {
205
+	ctx, span := tracing.StartSpan(ctx, "loadLayer")
206
+	span.SetAttributes(tracing.Attribute("image.id", id))
207
+	defer span.End()
208
+	defer func() {
209
+		span.SetStatus(outErr)
210
+	}()
211
+
212
+	// We use sequential file access to avoid depleting the standby list on Windows.
213
+	// On Linux, this equates to a regular os.Open.
214
+	rawTar, err := sequential.Open(filename)
215
+	if err != nil {
216
+		log.G(context.TODO()).Debugf("Error reading embedded tar: %v", err)
217
+		return nil, err
218
+	}
219
+	defer rawTar.Close()
220
+
221
+	var r io.Reader
222
+	if progressOutput != nil {
223
+		fileInfo, err := rawTar.Stat()
224
+		if err != nil {
225
+			log.G(context.TODO()).Debugf("Error statting file: %v", err)
226
+			return nil, err
227
+		}
228
+
229
+		r = progress.NewProgressReader(rawTar, progressOutput, fileInfo.Size(), stringid.TruncateID(id), "Loading layer")
230
+	} else {
231
+		r = rawTar
232
+	}
233
+
234
+	inflatedLayerData, err := compression.DecompressStream(ioutils.NewCtxReader(ctx, r))
235
+	if err != nil {
236
+		return nil, err
237
+	}
238
+	defer inflatedLayerData.Close()
239
+
240
+	if ds, ok := l.lss.(layer.DescribableStore); ok {
241
+		return ds.RegisterWithDescriptor(inflatedLayerData, rootFS.ChainID(), foreignSrc)
242
+	}
243
+	return l.lss.Register(inflatedLayerData, rootFS.ChainID())
244
+}
245
+
246
+func (l *tarexporter) setLoadedTag(ref reference.Named, imgID digest.Digest, outStream io.Writer) error {
247
+	if prevID, err := l.rs.Get(ref); err == nil && prevID != imgID {
248
+		fmt.Fprintf(outStream, "The image %s already exists, renaming the old one with ID %s to empty string\n", reference.FamiliarString(ref), string(prevID)) // todo: this message is wrong in case of multiple tags
249
+	}
250
+
251
+	return l.rs.AddTag(ref, imgID, true)
252
+}
253
+
254
+func safePath(base, path string) (string, error) {
255
+	return symlink.FollowSymlinkInScope(filepath.Join(base, path), base)
256
+}
257
+
258
+type parentLink struct {
259
+	id, parentID image.ID
260
+}
261
+
262
+func validatedParentLinks(pl []parentLink) (ret []parentLink) {
263
+mainloop:
264
+	for i, p := range pl {
265
+		ret = append(ret, p)
266
+		for _, p2 := range pl {
267
+			if p2.id == p.parentID && p2.id != p.id {
268
+				continue mainloop
269
+			}
270
+		}
271
+		ret[i].parentID = ""
272
+	}
273
+	return ret
274
+}
275
+
276
+func checkValidParent(img, parent *image.Image) bool {
277
+	if len(img.History) == 0 && len(parent.History) == 0 {
278
+		return true // having history is not mandatory
279
+	}
280
+	if len(img.History)-len(parent.History) != 1 {
281
+		return false
282
+	}
283
+	for i, hP := range parent.History {
284
+		hC := img.History[i]
285
+		if (hP.Created == nil) != (hC.Created == nil) {
286
+			return false
287
+		}
288
+		if hP.Created != nil && !hP.Created.Equal(*hC.Created) {
289
+			return false
290
+		}
291
+		hC.Created = hP.Created
292
+		if !reflect.DeepEqual(hP, hC) {
293
+			return false
294
+		}
295
+	}
296
+	return true
297
+}
0 298
new file mode 100644
... ...
@@ -0,0 +1,74 @@
0
+// Copyright 2009 The Go Authors. All rights reserved.
1
+// Use of this source code is governed by a BSD-style
2
+// license that can be found in the LICENSE file.
3
+
4
+// Code in this file is a modified version of go stdlib;
5
+// https://cs.opensource.google/go/go/+/refs/tags/go1.23.4:src/os/path.go;l=19-66
6
+
7
+package tarexport
8
+
9
+import (
10
+	"fmt"
11
+	"os"
12
+	"path/filepath"
13
+	"syscall"
14
+	"time"
15
+
16
+	"github.com/docker/docker/pkg/system"
17
+)
18
+
19
+// mkdirAllWithChtimes is nearly an identical copy to the [os.MkdirAll] but
20
+// tracks created directories and applies the provided mtime and atime using
21
+// [system.Chtimes].
22
+func mkdirAllWithChtimes(path string, perm os.FileMode, atime, mtime time.Time) error {
23
+	// Fast path: if we can tell whether path is a directory or file, stop with success or error.
24
+	dir, err := os.Stat(path)
25
+	if err == nil {
26
+		if dir.IsDir() {
27
+			return nil
28
+		}
29
+		return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR}
30
+	}
31
+
32
+	// Slow path: make sure parent exists and then call Mkdir for path.
33
+
34
+	// Extract the parent folder from path by first removing any trailing
35
+	// path separator and then scanning backward until finding a path
36
+	// separator or reaching the beginning of the string.
37
+	i := len(path) - 1
38
+	for i >= 0 && os.IsPathSeparator(path[i]) {
39
+		i--
40
+	}
41
+	for i >= 0 && !os.IsPathSeparator(path[i]) {
42
+		i--
43
+	}
44
+	if i < 0 {
45
+		i = 0
46
+	}
47
+
48
+	// If there is a parent directory, and it is not the volume name,
49
+	// recurse to ensure parent directory exists.
50
+	if parent := path[:i]; len(parent) > len(filepath.VolumeName(path)) {
51
+		err = mkdirAllWithChtimes(parent, perm, atime, mtime)
52
+		if err != nil {
53
+			return err
54
+		}
55
+	}
56
+
57
+	// Parent now exists; invoke Mkdir and use its result.
58
+	err = os.Mkdir(path, perm)
59
+	if err != nil {
60
+		// Handle arguments like "foo/." by
61
+		// double-checking that directory doesn't exist.
62
+		dir, err1 := os.Lstat(path)
63
+		if err1 == nil && dir.IsDir() {
64
+			return nil
65
+		}
66
+		return err
67
+	}
68
+
69
+	if err := system.Chtimes(path, atime, mtime); err != nil {
70
+		return fmt.Errorf("applying atime=%v and mtime=%v: %w", atime, mtime, err)
71
+	}
72
+	return nil
73
+}
0 74
new file mode 100644
... ...
@@ -0,0 +1,638 @@
0
+package tarexport
1
+
2
+import (
3
+	"context"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io"
7
+	"os"
8
+	"path"
9
+	"path/filepath"
10
+	"time"
11
+
12
+	c8dimages "github.com/containerd/containerd/v2/core/images"
13
+	"github.com/containerd/containerd/v2/pkg/tracing"
14
+	"github.com/containerd/log"
15
+	"github.com/containerd/platforms"
16
+	"github.com/distribution/reference"
17
+	"github.com/docker/distribution"
18
+	"github.com/docker/docker/daemon/internal/image"
19
+	v1 "github.com/docker/docker/daemon/internal/image/v1"
20
+	"github.com/docker/docker/daemon/internal/layer"
21
+	"github.com/docker/docker/internal/ioutils"
22
+	"github.com/docker/docker/pkg/system"
23
+	"github.com/moby/go-archive"
24
+	"github.com/moby/moby/api/types/events"
25
+	"github.com/moby/sys/sequential"
26
+	"github.com/opencontainers/go-digest"
27
+	"github.com/opencontainers/image-spec/specs-go"
28
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
29
+	"github.com/pkg/errors"
30
+)
31
+
32
+type imageDescriptor struct {
33
+	refs     []reference.NamedTagged
34
+	layers   []layer.DiffID
35
+	image    *image.Image
36
+	layerRef layer.Layer
37
+}
38
+
39
+type saveSession struct {
40
+	*tarexporter
41
+	outDir       string
42
+	images       map[image.ID]*imageDescriptor
43
+	savedLayers  map[layer.DiffID]distribution.Descriptor
44
+	savedConfigs map[string]struct{}
45
+}
46
+
47
+func (l *tarexporter) Save(ctx context.Context, names []string, outStream io.Writer) error {
48
+	imgDescriptors, err := l.parseNames(ctx, names)
49
+	if err != nil {
50
+		return err
51
+	}
52
+
53
+	// Release all the image top layer references
54
+	defer l.releaseLayerReferences(imgDescriptors)
55
+	return (&saveSession{tarexporter: l, images: imgDescriptors}).save(ctx, outStream)
56
+}
57
+
58
+// parseNames will parse the image names to a map which contains image.ID to *imageDescriptor.
59
+// Each imageDescriptor holds an image top layer reference named 'layerRef'. It is taken here, should be released later.
60
+func (l *tarexporter) parseNames(ctx context.Context, names []string) (desc map[image.ID]*imageDescriptor, rErr error) {
61
+	imgDescr := make(map[image.ID]*imageDescriptor)
62
+	defer func() {
63
+		if rErr != nil {
64
+			l.releaseLayerReferences(imgDescr)
65
+		}
66
+	}()
67
+
68
+	addAssoc := func(id image.ID, ref reference.Named) error {
69
+		if _, ok := imgDescr[id]; !ok {
70
+			descr := &imageDescriptor{}
71
+			if err := l.takeLayerReference(id, descr); err != nil {
72
+				return err
73
+			}
74
+			imgDescr[id] = descr
75
+		}
76
+
77
+		if ref != nil {
78
+			if _, ok := ref.(reference.Canonical); ok {
79
+				return nil
80
+			}
81
+			tagged, ok := reference.TagNameOnly(ref).(reference.NamedTagged)
82
+			if !ok {
83
+				return nil
84
+			}
85
+
86
+			for _, t := range imgDescr[id].refs {
87
+				if tagged.String() == t.String() {
88
+					return nil
89
+				}
90
+			}
91
+			imgDescr[id].refs = append(imgDescr[id].refs, tagged)
92
+		}
93
+		return nil
94
+	}
95
+
96
+	for _, name := range names {
97
+		select {
98
+		case <-ctx.Done():
99
+			return nil, ctx.Err()
100
+		default:
101
+		}
102
+
103
+		ref, err := reference.ParseAnyReference(name)
104
+		if err != nil {
105
+			return nil, err
106
+		}
107
+		namedRef, ok := ref.(reference.Named)
108
+		if !ok {
109
+			// Check if digest ID reference
110
+			if digested, ok := ref.(reference.Digested); ok {
111
+				if err := addAssoc(image.ID(digested.Digest()), nil); err != nil {
112
+					return nil, err
113
+				}
114
+				continue
115
+			}
116
+			return nil, errors.Errorf("invalid reference: %v", name)
117
+		}
118
+
119
+		if reference.FamiliarName(namedRef) == string(digest.Canonical) {
120
+			imgID, err := l.is.Search(name)
121
+			if err != nil {
122
+				return nil, err
123
+			}
124
+			if err := addAssoc(imgID, nil); err != nil {
125
+				return nil, err
126
+			}
127
+			continue
128
+		}
129
+		if reference.IsNameOnly(namedRef) {
130
+			assocs := l.rs.ReferencesByName(namedRef)
131
+			for _, assoc := range assocs {
132
+				if err := addAssoc(image.ID(assoc.ID), assoc.Ref); err != nil {
133
+					return nil, err
134
+				}
135
+			}
136
+			if len(assocs) == 0 {
137
+				imgID, err := l.is.Search(name)
138
+				if err != nil {
139
+					return nil, err
140
+				}
141
+				if err := addAssoc(imgID, nil); err != nil {
142
+					return nil, err
143
+				}
144
+			}
145
+			continue
146
+		}
147
+		id, err := l.rs.Get(namedRef)
148
+		if err != nil {
149
+			return nil, err
150
+		}
151
+		if err := addAssoc(image.ID(id), namedRef); err != nil {
152
+			return nil, err
153
+		}
154
+	}
155
+	return imgDescr, nil
156
+}
157
+
158
+// takeLayerReference will take/Get the image top layer reference
159
+func (l *tarexporter) takeLayerReference(id image.ID, imgDescr *imageDescriptor) error {
160
+	img, err := l.is.Get(id)
161
+	if err != nil {
162
+		return err
163
+	}
164
+	if err := image.CheckOS(img.OperatingSystem()); err != nil {
165
+		return fmt.Errorf("os %q is not supported", img.OperatingSystem())
166
+	}
167
+	if l.platform != nil {
168
+		if !l.platformMatcher.Match(img.Platform()) {
169
+			return errors.New("no suitable export target found for platform " + platforms.FormatAll(*l.platform))
170
+		}
171
+	}
172
+	imgDescr.image = img
173
+	topLayerID := img.RootFS.ChainID()
174
+	if topLayerID == "" {
175
+		return nil
176
+	}
177
+	topLayer, err := l.lss.Get(topLayerID)
178
+	if err != nil {
179
+		return err
180
+	}
181
+	imgDescr.layerRef = topLayer
182
+	return nil
183
+}
184
+
185
+// releaseLayerReferences will release all the image top layer references
186
+func (l *tarexporter) releaseLayerReferences(imgDescr map[image.ID]*imageDescriptor) error {
187
+	for _, descr := range imgDescr {
188
+		if descr.layerRef != nil {
189
+			l.lss.Release(descr.layerRef)
190
+		}
191
+	}
192
+	return nil
193
+}
194
+
195
+func (s *saveSession) save(ctx context.Context, outStream io.Writer) error {
196
+	s.savedConfigs = make(map[string]struct{})
197
+	s.savedLayers = make(map[layer.DiffID]distribution.Descriptor)
198
+
199
+	// get image json
200
+	tempDir, err := os.MkdirTemp("", "docker-export-")
201
+	if err != nil {
202
+		return err
203
+	}
204
+	defer os.RemoveAll(tempDir)
205
+
206
+	s.outDir = tempDir
207
+	reposLegacy := make(map[string]map[string]string)
208
+
209
+	var manifest []manifestItem
210
+	var parentLinks []parentLink
211
+
212
+	var manifestDescriptors []ocispec.Descriptor
213
+
214
+	for id, imageDescr := range s.images {
215
+		select {
216
+		case <-ctx.Done():
217
+			return ctx.Err()
218
+		default:
219
+		}
220
+
221
+		foreignSrcs, err := s.saveImage(ctx, id)
222
+		if err != nil {
223
+			return err
224
+		}
225
+
226
+		var (
227
+			repoTags []string
228
+			layers   []string
229
+			foreign  = make([]ocispec.Descriptor, 0, len(foreignSrcs))
230
+		)
231
+
232
+		// Layers in manifest must follow the actual layer order from config.
233
+		for _, l := range imageDescr.layers {
234
+			desc := foreignSrcs[l]
235
+			foreign = append(foreign, ocispec.Descriptor{
236
+				MediaType:   desc.MediaType,
237
+				Digest:      desc.Digest,
238
+				Size:        desc.Size,
239
+				URLs:        desc.URLs,
240
+				Annotations: desc.Annotations,
241
+				Platform:    desc.Platform,
242
+			})
243
+		}
244
+
245
+		data, err := json.Marshal(ocispec.Manifest{
246
+			Versioned: specs.Versioned{
247
+				SchemaVersion: 2,
248
+			},
249
+			MediaType: ocispec.MediaTypeImageManifest,
250
+			Config: ocispec.Descriptor{
251
+				MediaType: ocispec.MediaTypeImageConfig,
252
+				Digest:    digest.Digest(imageDescr.image.ID()),
253
+				Size:      int64(len(imageDescr.image.RawJSON())),
254
+			},
255
+			Layers: foreign,
256
+		})
257
+		if err != nil {
258
+			return errors.Wrap(err, "error marshaling manifest")
259
+		}
260
+		dgst := digest.FromBytes(data)
261
+
262
+		mFile := filepath.Join(s.outDir, ocispec.ImageBlobsDir, dgst.Algorithm().String(), dgst.Encoded())
263
+		if err := mkdirAllWithChtimes(filepath.Dir(mFile), 0o755, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
264
+			return errors.Wrap(err, "error creating blob directory")
265
+		}
266
+		if err := system.Chtimes(filepath.Dir(mFile), time.Unix(0, 0), time.Unix(0, 0)); err != nil {
267
+			return errors.Wrap(err, "error setting blob directory timestamps")
268
+		}
269
+		if err := os.WriteFile(mFile, data, 0o644); err != nil {
270
+			return errors.Wrap(err, "error writing oci manifest file")
271
+		}
272
+		if err := system.Chtimes(mFile, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
273
+			return errors.Wrap(err, "error setting blob directory timestamps")
274
+		}
275
+
276
+		untaggedMfstDesc := ocispec.Descriptor{
277
+			MediaType: ocispec.MediaTypeImageManifest,
278
+			Digest:    dgst,
279
+			Size:      int64(len(data)),
280
+		}
281
+		for _, ref := range imageDescr.refs {
282
+			familiarName := reference.FamiliarName(ref)
283
+			if _, ok := reposLegacy[familiarName]; !ok {
284
+				reposLegacy[familiarName] = make(map[string]string)
285
+			}
286
+			reposLegacy[familiarName][ref.Tag()] = imageDescr.layers[len(imageDescr.layers)-1].Encoded()
287
+			repoTags = append(repoTags, reference.FamiliarString(ref))
288
+
289
+			taggedManifest := untaggedMfstDesc
290
+			taggedManifest.Annotations = map[string]string{
291
+				c8dimages.AnnotationImageName: ref.String(),
292
+				ocispec.AnnotationRefName:     ref.Tag(),
293
+			}
294
+			manifestDescriptors = append(manifestDescriptors, taggedManifest)
295
+		}
296
+
297
+		// If no ref was assigned, make sure still add the image is still included in index.json.
298
+		if len(manifestDescriptors) == 0 {
299
+			manifestDescriptors = append(manifestDescriptors, untaggedMfstDesc)
300
+		}
301
+
302
+		for _, lDgst := range imageDescr.layers {
303
+			// IMPORTANT: We use path, not filepath here to ensure the layers
304
+			// in the manifest use Unix-style forward-slashes.
305
+			layers = append(layers, path.Join(ocispec.ImageBlobsDir, lDgst.Algorithm().String(), lDgst.Encoded()))
306
+		}
307
+
308
+		manifest = append(manifest, manifestItem{
309
+			Config:       path.Join(ocispec.ImageBlobsDir, id.Digest().Algorithm().String(), id.Digest().Encoded()),
310
+			RepoTags:     repoTags,
311
+			Layers:       layers,
312
+			LayerSources: foreignSrcs,
313
+		})
314
+
315
+		parentID, _ := s.is.GetParent(id)
316
+		parentLinks = append(parentLinks, parentLink{id, parentID})
317
+		s.tarexporter.loggerImgEvent.LogImageEvent(ctx, id.String(), id.String(), events.ActionSave)
318
+	}
319
+
320
+	for i, p := range validatedParentLinks(parentLinks) {
321
+		if p.parentID != "" {
322
+			manifest[i].Parent = p.parentID
323
+		}
324
+	}
325
+
326
+	if len(reposLegacy) > 0 {
327
+		reposFile := filepath.Join(tempDir, legacyRepositoriesFileName)
328
+		rf, err := os.OpenFile(reposFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
329
+		if err != nil {
330
+			return err
331
+		}
332
+
333
+		if err := json.NewEncoder(rf).Encode(reposLegacy); err != nil {
334
+			rf.Close()
335
+			return err
336
+		}
337
+
338
+		rf.Close()
339
+
340
+		if err := system.Chtimes(reposFile, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
341
+			return err
342
+		}
343
+	}
344
+
345
+	manifestPath := filepath.Join(tempDir, manifestFileName)
346
+	f, err := os.OpenFile(manifestPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
347
+	if err != nil {
348
+		return err
349
+	}
350
+
351
+	if err := json.NewEncoder(f).Encode(manifest); err != nil {
352
+		f.Close()
353
+		return err
354
+	}
355
+
356
+	f.Close()
357
+
358
+	if err := system.Chtimes(manifestPath, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
359
+		return err
360
+	}
361
+
362
+	const ociLayoutContent = `{"imageLayoutVersion": "` + ocispec.ImageLayoutVersion + `"}`
363
+	layoutPath := filepath.Join(tempDir, ocispec.ImageLayoutFile)
364
+	if err := os.WriteFile(layoutPath, []byte(ociLayoutContent), 0o644); err != nil {
365
+		return errors.Wrap(err, "error writing oci layout file")
366
+	}
367
+	if err := system.Chtimes(layoutPath, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
368
+		return errors.Wrap(err, "error setting oci layout file timestamps")
369
+	}
370
+
371
+	data, err := json.Marshal(ocispec.Index{
372
+		Versioned: specs.Versioned{
373
+			SchemaVersion: 2,
374
+		},
375
+		MediaType: ocispec.MediaTypeImageIndex,
376
+		Manifests: manifestDescriptors,
377
+	})
378
+	if err != nil {
379
+		return errors.Wrap(err, "error marshaling oci index")
380
+	}
381
+
382
+	idxFile := filepath.Join(s.outDir, ocispec.ImageIndexFile)
383
+	if err := os.WriteFile(idxFile, data, 0o644); err != nil {
384
+		return errors.Wrap(err, "error writing oci index file")
385
+	}
386
+	if err := system.Chtimes(idxFile, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
387
+		return errors.Wrap(err, "error setting oci index file timestamps")
388
+	}
389
+
390
+	return s.writeTar(ctx, tempDir, outStream)
391
+}
392
+
393
+func (s *saveSession) writeTar(ctx context.Context, tempDir string, outStream io.Writer) error {
394
+	ctx, span := tracing.StartSpan(ctx, "writeTar")
395
+	defer span.End()
396
+
397
+	fs, err := archive.Tar(tempDir, archive.Uncompressed)
398
+	if err != nil {
399
+		span.SetStatus(err)
400
+		return err
401
+	}
402
+	defer fs.Close()
403
+
404
+	_, err = ioutils.CopyCtx(ctx, outStream, fs)
405
+
406
+	span.SetStatus(err)
407
+	return err
408
+}
409
+
410
+func (s *saveSession) saveImage(ctx context.Context, id image.ID) (_ map[layer.DiffID]distribution.Descriptor, outErr error) {
411
+	ctx, span := tracing.StartSpan(ctx, "saveImage")
412
+	span.SetAttributes(tracing.Attribute("image.id", id.String()))
413
+	defer span.End()
414
+	defer func() {
415
+		span.SetStatus(outErr)
416
+	}()
417
+
418
+	img := s.images[id].image
419
+	if len(img.RootFS.DiffIDs) == 0 {
420
+		return nil, errors.New("empty export - not implemented")
421
+	}
422
+
423
+	ts := time.Unix(0, 0)
424
+	if img.Created != nil {
425
+		ts = *img.Created
426
+	}
427
+
428
+	var parent digest.Digest
429
+	var layers []layer.DiffID
430
+	var foreignSrcs map[layer.DiffID]distribution.Descriptor
431
+	for i, diffID := range img.RootFS.DiffIDs {
432
+		select {
433
+		case <-ctx.Done():
434
+			return nil, ctx.Err()
435
+		default:
436
+		}
437
+		v1ImgCreated := time.Unix(0, 0)
438
+		v1Img := image.V1Image{
439
+			// This is for backward compatibility used for
440
+			// pre v1.9 docker.
441
+			Created: &v1ImgCreated,
442
+		}
443
+		if i == len(img.RootFS.DiffIDs)-1 {
444
+			v1Img = img.V1Image
445
+		}
446
+		rootFS := *img.RootFS
447
+		rootFS.DiffIDs = rootFS.DiffIDs[:i+1]
448
+		v1ID, err := v1.CreateID(v1Img, rootFS.ChainID(), parent)
449
+		if err != nil {
450
+			return nil, err
451
+		}
452
+
453
+		v1Img.ID = v1ID.Encoded()
454
+		if parent != "" {
455
+			v1Img.Parent = parent.Encoded()
456
+		}
457
+
458
+		v1Img.OS = img.OS
459
+		src, err := s.saveConfigAndLayer(ctx, rootFS.ChainID(), v1Img, &ts)
460
+		if err != nil {
461
+			return nil, err
462
+		}
463
+
464
+		layers = append(layers, diffID)
465
+		parent = v1ID
466
+		if src.Digest != "" {
467
+			if foreignSrcs == nil {
468
+				foreignSrcs = make(map[layer.DiffID]distribution.Descriptor)
469
+			}
470
+			foreignSrcs[img.RootFS.DiffIDs[i]] = src
471
+		}
472
+	}
473
+
474
+	data := img.RawJSON()
475
+	dgst := digest.FromBytes(data)
476
+
477
+	blobDir := filepath.Join(s.outDir, ocispec.ImageBlobsDir, dgst.Algorithm().String())
478
+	if err := mkdirAllWithChtimes(blobDir, 0o755, ts, ts); err != nil {
479
+		return nil, err
480
+	}
481
+	if err := system.Chtimes(blobDir, ts, ts); err != nil {
482
+		return nil, err
483
+	}
484
+	if err := system.Chtimes(filepath.Dir(blobDir), ts, ts); err != nil {
485
+		return nil, err
486
+	}
487
+
488
+	configFile := filepath.Join(blobDir, dgst.Encoded())
489
+	if err := os.WriteFile(configFile, img.RawJSON(), 0o644); err != nil {
490
+		return nil, err
491
+	}
492
+	if err := system.Chtimes(configFile, ts, ts); err != nil {
493
+		return nil, err
494
+	}
495
+
496
+	s.images[id].layers = layers
497
+	return foreignSrcs, nil
498
+}
499
+
500
+func (s *saveSession) saveConfigAndLayer(ctx context.Context, id layer.ChainID, legacyImg image.V1Image, createdTime *time.Time) (_ distribution.Descriptor, outErr error) {
501
+	ctx, span := tracing.StartSpan(ctx, "saveConfigAndLayer")
502
+	span.SetAttributes(
503
+		tracing.Attribute("layer.id", id.String()),
504
+		tracing.Attribute("image.id", legacyImg.ID),
505
+	)
506
+	defer span.End()
507
+	defer func() {
508
+		span.SetStatus(outErr)
509
+	}()
510
+
511
+	ts := time.Unix(0, 0)
512
+	if createdTime != nil {
513
+		ts = *createdTime
514
+	}
515
+
516
+	outDir := filepath.Join(s.outDir, ocispec.ImageBlobsDir)
517
+
518
+	if _, ok := s.savedConfigs[legacyImg.ID]; !ok {
519
+		if err := s.saveConfig(legacyImg, outDir, createdTime); err != nil {
520
+			return distribution.Descriptor{}, err
521
+		}
522
+	}
523
+
524
+	// serialize filesystem
525
+	l, err := s.lss.Get(id)
526
+	if err != nil {
527
+		return distribution.Descriptor{}, err
528
+	}
529
+
530
+	lDiffID := l.DiffID()
531
+	lDgst := lDiffID
532
+	if _, ok := s.savedLayers[lDiffID]; ok {
533
+		return s.savedLayers[lDiffID], nil
534
+	}
535
+	layerPath := filepath.Join(outDir, lDiffID.Algorithm().String(), lDiffID.Encoded())
536
+	defer layer.ReleaseAndLog(s.lss, l)
537
+
538
+	if _, err = os.Stat(layerPath); err == nil {
539
+		// This is should not happen. If the layer path was already created, we should have returned early.
540
+		// Log a warning an proceed to recreate the archive.
541
+		log.G(context.TODO()).WithFields(log.Fields{
542
+			"layerPath": layerPath,
543
+			"id":        id,
544
+			"lDgst":     lDgst,
545
+		}).Warn("LayerPath already exists but the descriptor is not cached")
546
+	} else if !os.IsNotExist(err) {
547
+		return distribution.Descriptor{}, err
548
+	}
549
+
550
+	// We use sequential file access to avoid depleting the standby list on
551
+	// Windows. On Linux, this equates to a regular os.Create.
552
+	if err := mkdirAllWithChtimes(filepath.Dir(layerPath), 0o755, ts, ts); err != nil {
553
+		return distribution.Descriptor{}, errors.Wrap(err, "could not create layer dir parent")
554
+	}
555
+	tarFile, err := sequential.Create(layerPath)
556
+	if err != nil {
557
+		return distribution.Descriptor{}, errors.Wrap(err, "error creating layer file")
558
+	}
559
+	defer tarFile.Close()
560
+
561
+	arch, err := l.TarStream()
562
+	if err != nil {
563
+		return distribution.Descriptor{}, err
564
+	}
565
+	defer arch.Close()
566
+
567
+	digester := digest.Canonical.Digester()
568
+	digestedArch := io.TeeReader(arch, digester.Hash())
569
+
570
+	tarSize, err := ioutils.CopyCtx(ctx, tarFile, digestedArch)
571
+	if err != nil {
572
+		return distribution.Descriptor{}, err
573
+	}
574
+
575
+	tarDigest := digester.Digest()
576
+	if lDgst != tarDigest {
577
+		log.G(context.TODO()).WithFields(log.Fields{
578
+			"layerDigest":  lDgst,
579
+			"actualDigest": tarDigest,
580
+		}).Warn("layer digest doesn't match its tar archive digest")
581
+
582
+		lDgst = digester.Digest()
583
+		layerPath = filepath.Join(outDir, lDgst.Algorithm().String(), lDgst.Encoded())
584
+	}
585
+
586
+	for _, fname := range []string{outDir, layerPath} {
587
+		// todo: maybe save layer created timestamp?
588
+		if err := system.Chtimes(fname, ts, ts); err != nil {
589
+			return distribution.Descriptor{}, errors.Wrap(err, "could not set layer timestamp")
590
+		}
591
+	}
592
+
593
+	var desc distribution.Descriptor
594
+	if fs, ok := l.(distribution.Describable); ok {
595
+		desc = fs.Descriptor()
596
+	}
597
+
598
+	if desc.Digest == "" {
599
+		desc.Digest = tarDigest
600
+		desc.Size = tarSize
601
+	}
602
+	if desc.MediaType == "" {
603
+		desc.MediaType = ocispec.MediaTypeImageLayer
604
+	}
605
+	s.savedLayers[lDiffID] = desc
606
+
607
+	return desc, nil
608
+}
609
+
610
+func (s *saveSession) saveConfig(legacyImg image.V1Image, outDir string, createdTime *time.Time) error {
611
+	imageConfig, err := json.Marshal(legacyImg)
612
+	if err != nil {
613
+		return err
614
+	}
615
+
616
+	ts := time.Unix(0, 0)
617
+	if createdTime != nil {
618
+		ts = *createdTime
619
+	}
620
+
621
+	cfgDgst := digest.FromBytes(imageConfig)
622
+	configPath := filepath.Join(outDir, cfgDgst.Algorithm().String(), cfgDgst.Encoded())
623
+	if err := mkdirAllWithChtimes(filepath.Dir(configPath), 0o755, ts, ts); err != nil {
624
+		return errors.Wrap(err, "could not create layer dir parent")
625
+	}
626
+
627
+	if err := os.WriteFile(configPath, imageConfig, 0o644); err != nil {
628
+		return err
629
+	}
630
+
631
+	if err := system.Chtimes(configPath, ts, ts); err != nil {
632
+		return errors.Wrap(err, "could not set config timestamp")
633
+	}
634
+
635
+	s.savedConfigs[legacyImg.ID] = struct{}{}
636
+	return nil
637
+}
0 638
new file mode 100644
... ...
@@ -0,0 +1,56 @@
0
+package tarexport
1
+
2
+import (
3
+	"context"
4
+
5
+	"github.com/containerd/platforms"
6
+	"github.com/docker/distribution"
7
+	"github.com/docker/docker/daemon/internal/image"
8
+	"github.com/docker/docker/daemon/internal/layer"
9
+	refstore "github.com/docker/docker/reference"
10
+	"github.com/moby/moby/api/types/events"
11
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
12
+)
13
+
14
+const (
15
+	manifestFileName           = "manifest.json"
16
+	legacyRepositoriesFileName = "repositories"
17
+)
18
+
19
+type manifestItem struct {
20
+	Config       string
21
+	RepoTags     []string
22
+	Layers       []string
23
+	Parent       image.ID                                 `json:",omitempty"`
24
+	LayerSources map[layer.DiffID]distribution.Descriptor `json:",omitempty"`
25
+}
26
+
27
+type tarexporter struct {
28
+	is              image.Store
29
+	lss             layer.Store
30
+	rs              refstore.Store
31
+	loggerImgEvent  LogImageEvent
32
+	platform        *platforms.Platform
33
+	platformMatcher platforms.Matcher
34
+}
35
+
36
+// LogImageEvent defines interface for event generation related to image tar(load and save) operations
37
+type LogImageEvent interface {
38
+	// LogImageEvent generates an event related to an image operation
39
+	LogImageEvent(ctx context.Context, imageID, refName string, action events.Action)
40
+}
41
+
42
+// NewTarExporter returns new Exporter for tar packages
43
+func NewTarExporter(is image.Store, lss layer.Store, rs refstore.Store, loggerImgEvent LogImageEvent, platform *ocispec.Platform) image.Exporter {
44
+	l := &tarexporter{
45
+		is:             is,
46
+		lss:            lss,
47
+		rs:             rs,
48
+		loggerImgEvent: loggerImgEvent,
49
+		platform:       platform,
50
+	}
51
+	if platform != nil {
52
+		l.platformMatcher = platforms.OnlyStrict(*platform)
53
+	}
54
+	return l
55
+}
0 56
new file mode 100644
... ...
@@ -0,0 +1,48 @@
0
+package v1
1
+
2
+import (
3
+	"context"
4
+	"encoding/json"
5
+
6
+	"github.com/containerd/log"
7
+	"github.com/docker/docker/daemon/internal/image"
8
+	"github.com/docker/docker/daemon/internal/layer"
9
+	"github.com/opencontainers/go-digest"
10
+)
11
+
12
+// CreateID creates an ID from v1 image, layerID and parent ID.
13
+// Used for backwards compatibility with old clients.
14
+func CreateID(v1Image image.V1Image, layerID layer.ChainID, parent digest.Digest) (digest.Digest, error) {
15
+	v1Image.ID = ""
16
+	v1JSON, err := json.Marshal(v1Image)
17
+	if err != nil {
18
+		return "", err
19
+	}
20
+
21
+	var config map[string]*json.RawMessage
22
+	if err := json.Unmarshal(v1JSON, &config); err != nil {
23
+		return "", err
24
+	}
25
+
26
+	// FIXME: note that this is slightly incompatible with RootFS logic
27
+	config["layer_id"] = rawJSON(layerID)
28
+	if parent != "" {
29
+		config["parent"] = rawJSON(parent)
30
+	}
31
+
32
+	configJSON, err := json.Marshal(config)
33
+	if err != nil {
34
+		return "", err
35
+	}
36
+	log.G(context.TODO()).Debugf("CreateV1ID %s", configJSON)
37
+
38
+	return digest.FromBytes(configJSON), nil
39
+}
40
+
41
+func rawJSON(value interface{}) *json.RawMessage {
42
+	jsonval, err := json.Marshal(value)
43
+	if err != nil {
44
+		return nil
45
+	}
46
+	return (*json.RawMessage)(&jsonval)
47
+}
... ...
@@ -12,8 +12,8 @@ import (
12 12
 	cerrdefs "github.com/containerd/errdefs"
13 13
 	"github.com/containerd/log"
14 14
 	"github.com/docker/docker/daemon/container"
15
+	"github.com/docker/docker/daemon/internal/image"
15 16
 	"github.com/docker/docker/errdefs"
16
-	"github.com/docker/docker/image"
17 17
 	"github.com/docker/go-connections/nat"
18 18
 	"github.com/moby/moby/api/types/backend"
19 19
 	containertypes "github.com/moby/moby/api/types/container"
... ...
@@ -9,7 +9,7 @@ import (
9 9
 	"time"
10 10
 
11 11
 	"github.com/docker/docker/daemon/container"
12
-	"github.com/docker/docker/image"
12
+	"github.com/docker/docker/daemon/internal/image"
13 13
 	"github.com/google/uuid"
14 14
 	containertypes "github.com/moby/moby/api/types/container"
15 15
 	"github.com/moby/moby/api/types/filters"
... ...
@@ -8,7 +8,7 @@ import (
8 8
 	"github.com/containerd/log"
9 9
 	"github.com/containerd/platforms"
10 10
 	"github.com/docker/docker/daemon/container"
11
-	"github.com/docker/docker/image"
11
+	"github.com/docker/docker/daemon/internal/image"
12 12
 	"github.com/docker/docker/internal/multierror"
13 13
 	"github.com/moby/moby/api/types/backend"
14 14
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
... ...
@@ -7,7 +7,7 @@ import (
7 7
 
8 8
 	"github.com/containerd/platforms"
9 9
 	"github.com/docker/docker/daemon/container"
10
-	"github.com/docker/docker/image"
10
+	"github.com/docker/docker/daemon/internal/image"
11 11
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
12 12
 	"gotest.tools/v3/assert"
13 13
 )
... ...
@@ -14,8 +14,8 @@ import (
14 14
 	"github.com/containerd/log"
15 15
 	"github.com/docker/docker/daemon/config"
16 16
 	"github.com/docker/docker/daemon/container"
17
+	"github.com/docker/docker/daemon/internal/image"
17 18
 	"github.com/docker/docker/errdefs"
18
-	"github.com/docker/docker/image"
19 19
 	"github.com/docker/docker/oci"
20 20
 	"github.com/moby/moby/api/types/backend"
21 21
 	containertypes "github.com/moby/moby/api/types/container"
... ...
@@ -5,7 +5,7 @@ import (
5 5
 	"io"
6 6
 
7 7
 	"github.com/distribution/reference"
8
-	dockerimage "github.com/docker/docker/image"
8
+	dockerimage "github.com/docker/docker/daemon/internal/image"
9 9
 	"github.com/moby/moby/api/types/backend"
10 10
 	"github.com/moby/moby/api/types/filters"
11 11
 	"github.com/moby/moby/api/types/image"
... ...
@@ -13,10 +13,10 @@ import (
13 13
 	"github.com/containerd/platforms"
14 14
 	"github.com/distribution/reference"
15 15
 	"github.com/docker/docker/daemon/builder/remotecontext"
16
+	"github.com/docker/docker/daemon/internal/image"
16 17
 	"github.com/docker/docker/daemon/server/httputils"
17 18
 	"github.com/docker/docker/dockerversion"
18 19
 	"github.com/docker/docker/errdefs"
19
-	"github.com/docker/docker/image"
20 20
 	"github.com/docker/docker/pkg/ioutils"
21 21
 	"github.com/docker/docker/pkg/progress"
22 22
 	"github.com/docker/docker/pkg/streamformatter"
23 23
deleted file mode 100644
... ...
@@ -1,276 +0,0 @@
1
-package cache
2
-
3
-import (
4
-	"context"
5
-	"fmt"
6
-	"reflect"
7
-	"strings"
8
-
9
-	"github.com/containerd/log"
10
-	"github.com/docker/docker/daemon/builder"
11
-	"github.com/docker/docker/daemon/internal/layer"
12
-	"github.com/docker/docker/dockerversion"
13
-	"github.com/docker/docker/image"
14
-	containertypes "github.com/moby/moby/api/types/container"
15
-	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
16
-	"github.com/pkg/errors"
17
-)
18
-
19
-type ImageCacheStore interface {
20
-	Get(image.ID) (*image.Image, error)
21
-	GetByRef(ctx context.Context, refOrId string) (*image.Image, error)
22
-	SetParent(target, parent image.ID) error
23
-	GetParent(target image.ID) (image.ID, error)
24
-	Create(parent *image.Image, image image.Image, extraLayer layer.DiffID) (image.ID, error)
25
-	IsBuiltLocally(id image.ID) (bool, error)
26
-	Children(id image.ID) []image.ID
27
-}
28
-
29
-func New(ctx context.Context, store ImageCacheStore, cacheFrom []string) (builder.ImageCache, error) {
30
-	local := &LocalImageCache{store: store}
31
-	if len(cacheFrom) == 0 {
32
-		return local, nil
33
-	}
34
-
35
-	cache := &ImageCache{
36
-		store:           store,
37
-		localImageCache: local,
38
-	}
39
-
40
-	for _, ref := range cacheFrom {
41
-		img, err := store.GetByRef(ctx, ref)
42
-		if err != nil {
43
-			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
44
-				return nil, err
45
-			}
46
-			log.G(ctx).Warnf("Could not look up %s for cache resolution, skipping: %+v", ref, err)
47
-			continue
48
-		}
49
-		cache.Populate(img)
50
-	}
51
-
52
-	return cache, nil
53
-}
54
-
55
-// LocalImageCache is cache based on parent chain.
56
-type LocalImageCache struct {
57
-	store ImageCacheStore
58
-}
59
-
60
-// GetCache returns the image id found in the cache
61
-func (lic *LocalImageCache) GetCache(imgID string, config *containertypes.Config, platform ocispec.Platform) (string, error) {
62
-	return getImageIDAndError(getLocalCachedImage(lic.store, image.ID(imgID), config, platform))
63
-}
64
-
65
-// ImageCache is cache based on history objects. Requires initial set of images.
66
-type ImageCache struct {
67
-	sources         []*image.Image
68
-	store           ImageCacheStore
69
-	localImageCache *LocalImageCache
70
-}
71
-
72
-// Populate adds an image to the cache (to be queried later)
73
-func (ic *ImageCache) Populate(image *image.Image) {
74
-	ic.sources = append(ic.sources, image)
75
-}
76
-
77
-// GetCache returns the image id found in the cache
78
-func (ic *ImageCache) GetCache(parentID string, cfg *containertypes.Config, platform ocispec.Platform) (string, error) {
79
-	imgID, err := ic.localImageCache.GetCache(parentID, cfg, platform)
80
-	if err != nil {
81
-		return "", err
82
-	}
83
-	if imgID != "" {
84
-		for _, s := range ic.sources {
85
-			if ic.isParent(s.ID(), image.ID(imgID)) {
86
-				return imgID, nil
87
-			}
88
-		}
89
-	}
90
-
91
-	var parent *image.Image
92
-	lenHistory := 0
93
-	if parentID != "" {
94
-		parent, err = ic.store.Get(image.ID(parentID))
95
-		if err != nil {
96
-			return "", errors.Wrapf(err, "unable to find image %v", parentID)
97
-		}
98
-		lenHistory = len(parent.History)
99
-	}
100
-
101
-	for _, target := range ic.sources {
102
-		if !isValidParent(target, parent) || !isValidConfig(cfg, target.History[lenHistory]) {
103
-			continue
104
-		}
105
-
106
-		if len(target.History)-1 == lenHistory { // last
107
-			if parent != nil {
108
-				if err := ic.store.SetParent(target.ID(), parent.ID()); err != nil {
109
-					return "", errors.Wrapf(err, "failed to set parent for %v to %v", target.ID(), parent.ID())
110
-				}
111
-			}
112
-			return target.ID().String(), nil
113
-		}
114
-
115
-		imgID, err := ic.restoreCachedImage(parent, target, cfg)
116
-		if err != nil {
117
-			return "", errors.Wrapf(err, "failed to restore cached image from %q to %v", parentID, target.ID())
118
-		}
119
-
120
-		ic.sources = []*image.Image{target} // avoid jumping to different target, tuned for safety atm
121
-		return imgID.String(), nil
122
-	}
123
-
124
-	return "", nil
125
-}
126
-
127
-func (ic *ImageCache) restoreCachedImage(parent, target *image.Image, cfg *containertypes.Config) (image.ID, error) {
128
-	var history []image.History
129
-	rootFS := image.NewRootFS()
130
-	lenHistory := 0
131
-	if parent != nil {
132
-		history = parent.History
133
-		rootFS = parent.RootFS
134
-		lenHistory = len(parent.History)
135
-	}
136
-	history = append(history, target.History[lenHistory])
137
-	layer := getLayerForHistoryIndex(target, lenHistory)
138
-	if layer != "" {
139
-		rootFS.Append(layer)
140
-	}
141
-
142
-	restoredImg := image.Image{
143
-		V1Image: image.V1Image{
144
-			DockerVersion: dockerversion.Version,
145
-			Config:        cfg,
146
-			Architecture:  target.Architecture,
147
-			OS:            target.OS,
148
-			Author:        target.Author,
149
-			Created:       history[len(history)-1].Created,
150
-		},
151
-		RootFS:     rootFS,
152
-		History:    history,
153
-		OSFeatures: target.OSFeatures,
154
-		OSVersion:  target.OSVersion,
155
-	}
156
-
157
-	imgID, err := ic.store.Create(parent, restoredImg, layer)
158
-	if err != nil {
159
-		return "", errors.Wrap(err, "failed to create cache image")
160
-	}
161
-
162
-	return imgID, nil
163
-}
164
-
165
-func (ic *ImageCache) isParent(imgID, parentID image.ID) bool {
166
-	nextParent, err := ic.store.GetParent(imgID)
167
-	if err != nil {
168
-		return false
169
-	}
170
-	if nextParent == parentID {
171
-		return true
172
-	}
173
-	return ic.isParent(nextParent, parentID)
174
-}
175
-
176
-func getLayerForHistoryIndex(image *image.Image, index int) layer.DiffID {
177
-	layerIndex := 0
178
-	for i, h := range image.History {
179
-		if i == index {
180
-			if h.EmptyLayer {
181
-				return ""
182
-			}
183
-			break
184
-		}
185
-		if !h.EmptyLayer {
186
-			layerIndex++
187
-		}
188
-	}
189
-	return image.RootFS.DiffIDs[layerIndex] // validate?
190
-}
191
-
192
-func isValidConfig(cfg *containertypes.Config, h image.History) bool {
193
-	// todo: make this format better than join that loses data
194
-	return strings.Join(cfg.Cmd, " ") == h.CreatedBy
195
-}
196
-
197
-func isValidParent(img, parent *image.Image) bool {
198
-	if len(img.History) == 0 {
199
-		return false
200
-	}
201
-	if parent == nil || len(parent.History) == 0 && len(parent.RootFS.DiffIDs) == 0 {
202
-		return true
203
-	}
204
-	if len(parent.History) >= len(img.History) {
205
-		return false
206
-	}
207
-	if len(parent.RootFS.DiffIDs) > len(img.RootFS.DiffIDs) {
208
-		return false
209
-	}
210
-
211
-	for i, h := range parent.History {
212
-		if !reflect.DeepEqual(h, img.History[i]) {
213
-			return false
214
-		}
215
-	}
216
-	for i, d := range parent.RootFS.DiffIDs {
217
-		if d != img.RootFS.DiffIDs[i] {
218
-			return false
219
-		}
220
-	}
221
-	return true
222
-}
223
-
224
-func getImageIDAndError(img *image.Image, err error) (string, error) {
225
-	if img == nil || err != nil {
226
-		return "", err
227
-	}
228
-	return img.ID().String(), nil
229
-}
230
-
231
-// getLocalCachedImage returns the most recent created image that is a child
232
-// of the image with imgID, that had the same config when it was
233
-// created. nil is returned if a child cannot be found. An error is
234
-// returned if the parent image cannot be found.
235
-func getLocalCachedImage(imageStore ImageCacheStore, parentID image.ID, config *containertypes.Config, platform ocispec.Platform) (*image.Image, error) {
236
-	if config == nil {
237
-		return nil, nil
238
-	}
239
-
240
-	var match *image.Image
241
-	for _, id := range imageStore.Children(parentID) {
242
-		img, err := imageStore.Get(id)
243
-		if err != nil {
244
-			return nil, fmt.Errorf("unable to find image %q", id)
245
-		}
246
-
247
-		builtLocally, err := imageStore.IsBuiltLocally(id)
248
-		if err != nil {
249
-			log.G(context.TODO()).WithFields(log.Fields{
250
-				"error": err,
251
-				"id":    id,
252
-			}).Warn("failed to check if image was built locally")
253
-			continue
254
-		}
255
-		if !builtLocally {
256
-			continue
257
-		}
258
-
259
-		imgPlatform := img.Platform()
260
-		// Discard old linux/amd64 images with empty platform.
261
-		if imgPlatform.OS == "" && imgPlatform.Architecture == "" {
262
-			continue
263
-		}
264
-		if !comparePlatform(platform, imgPlatform) {
265
-			continue
266
-		}
267
-
268
-		if compare(&img.ContainerConfig, config) {
269
-			// check for the most up to date match
270
-			if img.Created != nil && (match == nil || match.Created.Before(*img.Created)) {
271
-				match = img
272
-			}
273
-		}
274
-	}
275
-	return match, nil
276
-}
277 1
deleted file mode 100644
... ...
@@ -1,182 +0,0 @@
1
-package cache
2
-
3
-import (
4
-	"strings"
5
-
6
-	"github.com/containerd/platforms"
7
-	"github.com/moby/moby/api/types/container"
8
-	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
9
-)
10
-
11
-func comparePlatform(builderPlatform, imagePlatform ocispec.Platform) bool {
12
-	// On Windows, only check the Major and Minor versions.
13
-	// The Build and Revision compatibility depends on whether `process` or
14
-	// `hyperv` isolation used.
15
-	//
16
-	// Fixes https://github.com/moby/moby/issues/47307
17
-	if builderPlatform.OS == "windows" && imagePlatform.OS == builderPlatform.OS {
18
-		// OSVersion format is:
19
-		// Major.Minor.Build.Revision
20
-		builderParts := strings.Split(builderPlatform.OSVersion, ".")
21
-		imageParts := strings.Split(imagePlatform.OSVersion, ".")
22
-
23
-		if len(builderParts) >= 3 && len(imageParts) >= 3 {
24
-			// Keep only Major & Minor.
25
-			builderParts[0] = imageParts[0]
26
-			builderParts[1] = imageParts[1]
27
-			imagePlatform.OSVersion = strings.Join(builderParts, ".")
28
-		}
29
-	}
30
-
31
-	return platforms.Only(builderPlatform).Match(imagePlatform)
32
-}
33
-
34
-// compare two Config struct. Do not container-specific fields:
35
-// - Image
36
-// - Hostname
37
-// - Domainname
38
-// - MacAddress
39
-func compare(a, b *container.Config) bool {
40
-	if a == nil || b == nil {
41
-		return false
42
-	}
43
-
44
-	if len(a.Env) != len(b.Env) {
45
-		return false
46
-	}
47
-	if len(a.Cmd) != len(b.Cmd) {
48
-		return false
49
-	}
50
-	if len(a.Entrypoint) != len(b.Entrypoint) {
51
-		return false
52
-	}
53
-	if len(a.Shell) != len(b.Shell) {
54
-		return false
55
-	}
56
-	if len(a.ExposedPorts) != len(b.ExposedPorts) {
57
-		return false
58
-	}
59
-	if len(a.Volumes) != len(b.Volumes) {
60
-		return false
61
-	}
62
-	if len(a.Labels) != len(b.Labels) {
63
-		return false
64
-	}
65
-	if len(a.OnBuild) != len(b.OnBuild) {
66
-		return false
67
-	}
68
-
69
-	for i := 0; i < len(a.Env); i++ {
70
-		if a.Env[i] != b.Env[i] {
71
-			return false
72
-		}
73
-	}
74
-	for i := 0; i < len(a.OnBuild); i++ {
75
-		if a.OnBuild[i] != b.OnBuild[i] {
76
-			return false
77
-		}
78
-	}
79
-	for i := 0; i < len(a.Cmd); i++ {
80
-		if a.Cmd[i] != b.Cmd[i] {
81
-			return false
82
-		}
83
-	}
84
-	for i := 0; i < len(a.Entrypoint); i++ {
85
-		if a.Entrypoint[i] != b.Entrypoint[i] {
86
-			return false
87
-		}
88
-	}
89
-	for i := 0; i < len(a.Shell); i++ {
90
-		if a.Shell[i] != b.Shell[i] {
91
-			return false
92
-		}
93
-	}
94
-	for k := range a.ExposedPorts {
95
-		if _, exists := b.ExposedPorts[k]; !exists {
96
-			return false
97
-		}
98
-	}
99
-	for key := range a.Volumes {
100
-		if _, exists := b.Volumes[key]; !exists {
101
-			return false
102
-		}
103
-	}
104
-	for k, v := range a.Labels {
105
-		if v != b.Labels[k] {
106
-			return false
107
-		}
108
-	}
109
-
110
-	if a.AttachStdin != b.AttachStdin {
111
-		return false
112
-	}
113
-	if a.AttachStdout != b.AttachStdout {
114
-		return false
115
-	}
116
-	if a.AttachStderr != b.AttachStderr {
117
-		return false
118
-	}
119
-	if a.NetworkDisabled != b.NetworkDisabled {
120
-		return false
121
-	}
122
-	if a.Tty != b.Tty {
123
-		return false
124
-	}
125
-	if a.OpenStdin != b.OpenStdin {
126
-		return false
127
-	}
128
-	if a.StdinOnce != b.StdinOnce {
129
-		return false
130
-	}
131
-	if a.ArgsEscaped != b.ArgsEscaped {
132
-		return false
133
-	}
134
-	if a.User != b.User {
135
-		return false
136
-	}
137
-	if a.WorkingDir != b.WorkingDir {
138
-		return false
139
-	}
140
-	if a.StopSignal != b.StopSignal {
141
-		return false
142
-	}
143
-
144
-	if (a.StopTimeout == nil) != (b.StopTimeout == nil) {
145
-		return false
146
-	}
147
-	if a.StopTimeout != nil && b.StopTimeout != nil {
148
-		if *a.StopTimeout != *b.StopTimeout {
149
-			return false
150
-		}
151
-	}
152
-	if (a.Healthcheck == nil) != (b.Healthcheck == nil) {
153
-		return false
154
-	}
155
-	if a.Healthcheck != nil && b.Healthcheck != nil {
156
-		if a.Healthcheck.Interval != b.Healthcheck.Interval {
157
-			return false
158
-		}
159
-		if a.Healthcheck.StartInterval != b.Healthcheck.StartInterval {
160
-			return false
161
-		}
162
-		if a.Healthcheck.StartPeriod != b.Healthcheck.StartPeriod {
163
-			return false
164
-		}
165
-		if a.Healthcheck.Timeout != b.Healthcheck.Timeout {
166
-			return false
167
-		}
168
-		if a.Healthcheck.Retries != b.Healthcheck.Retries {
169
-			return false
170
-		}
171
-		if len(a.Healthcheck.Test) != len(b.Healthcheck.Test) {
172
-			return false
173
-		}
174
-		for i := 0; i < len(a.Healthcheck.Test); i++ {
175
-			if a.Healthcheck.Test[i] != b.Healthcheck.Test[i] {
176
-				return false
177
-			}
178
-		}
179
-	}
180
-
181
-	return true
182
-}
183 1
deleted file mode 100644
... ...
@@ -1,204 +0,0 @@
1
-package cache
2
-
3
-import (
4
-	"runtime"
5
-	"testing"
6
-
7
-	"github.com/docker/go-connections/nat"
8
-	"github.com/moby/moby/api/types/container"
9
-	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
10
-	"gotest.tools/v3/assert"
11
-	is "gotest.tools/v3/assert/cmp"
12
-)
13
-
14
-// Just to make life easier
15
-func newPortNoError(proto, port string) nat.Port {
16
-	p, _ := nat.NewPort(proto, port)
17
-	return p
18
-}
19
-
20
-func TestCompare(t *testing.T) {
21
-	ports1 := make(nat.PortSet)
22
-	ports1[newPortNoError("tcp", "1111")] = struct{}{}
23
-	ports1[newPortNoError("tcp", "2222")] = struct{}{}
24
-	ports2 := make(nat.PortSet)
25
-	ports2[newPortNoError("tcp", "3333")] = struct{}{}
26
-	ports2[newPortNoError("tcp", "4444")] = struct{}{}
27
-	ports3 := make(nat.PortSet)
28
-	ports3[newPortNoError("tcp", "1111")] = struct{}{}
29
-	ports3[newPortNoError("tcp", "2222")] = struct{}{}
30
-	ports3[newPortNoError("tcp", "5555")] = struct{}{}
31
-	volumes1 := make(map[string]struct{})
32
-	volumes1["/test1"] = struct{}{}
33
-	volumes2 := make(map[string]struct{})
34
-	volumes2["/test2"] = struct{}{}
35
-	volumes3 := make(map[string]struct{})
36
-	volumes3["/test1"] = struct{}{}
37
-	volumes3["/test3"] = struct{}{}
38
-	envs1 := []string{"ENV1=value1", "ENV2=value2"}
39
-	envs2 := []string{"ENV1=value1", "ENV3=value3"}
40
-	entrypoint1 := []string{"/bin/sh", "-c"}
41
-	entrypoint2 := []string{"/bin/sh", "-d"}
42
-	entrypoint3 := []string{"/bin/sh", "-c", "echo"}
43
-	cmd1 := []string{"/bin/sh", "-c"}
44
-	cmd2 := []string{"/bin/sh", "-d"}
45
-	cmd3 := []string{"/bin/sh", "-c", "echo"}
46
-	labels1 := map[string]string{"LABEL1": "value1", "LABEL2": "value2"}
47
-	labels2 := map[string]string{"LABEL1": "value1", "LABEL2": "value3"}
48
-	labels3 := map[string]string{"LABEL1": "value1", "LABEL2": "value2", "LABEL3": "value3"}
49
-
50
-	sameConfigs := map[*container.Config]*container.Config{
51
-		// Empty config
52
-		{}: {},
53
-		// Does not compare hostname, domainname & image
54
-		{
55
-			Hostname:   "host1",
56
-			Domainname: "domain1",
57
-			Image:      "image1",
58
-			User:       "user",
59
-		}: {
60
-			Hostname:   "host2",
61
-			Domainname: "domain2",
62
-			Image:      "image2",
63
-			User:       "user",
64
-		},
65
-		// only OpenStdin
66
-		{OpenStdin: false}: {OpenStdin: false},
67
-		// only env
68
-		{Env: envs1}: {Env: envs1},
69
-		// only cmd
70
-		{Cmd: cmd1}: {Cmd: cmd1},
71
-		// only labels
72
-		{Labels: labels1}: {Labels: labels1},
73
-		// only exposedPorts
74
-		{ExposedPorts: ports1}: {ExposedPorts: ports1},
75
-		// only entrypoints
76
-		{Entrypoint: entrypoint1}: {Entrypoint: entrypoint1},
77
-		// only volumes
78
-		{Volumes: volumes1}: {Volumes: volumes1},
79
-	}
80
-	differentConfigs := map[*container.Config]*container.Config{
81
-		nil: nil,
82
-		{
83
-			Hostname:   "host1",
84
-			Domainname: "domain1",
85
-			Image:      "image1",
86
-			User:       "user1",
87
-		}: {
88
-			Hostname:   "host1",
89
-			Domainname: "domain1",
90
-			Image:      "image1",
91
-			User:       "user2",
92
-		},
93
-		// only OpenStdin
94
-		{OpenStdin: false}: {OpenStdin: true},
95
-		{OpenStdin: true}:  {OpenStdin: false},
96
-		// only env
97
-		{Env: envs1}: {Env: envs2},
98
-		// only cmd
99
-		{Cmd: cmd1}: {Cmd: cmd2},
100
-		// not the same number of parts
101
-		{Cmd: cmd1}: {Cmd: cmd3},
102
-		// only labels
103
-		{Labels: labels1}: {Labels: labels2},
104
-		// not the same number of labels
105
-		{Labels: labels1}: {Labels: labels3},
106
-		// only exposedPorts
107
-		{ExposedPorts: ports1}: {ExposedPorts: ports2},
108
-		// not the same number of ports
109
-		{ExposedPorts: ports1}: {ExposedPorts: ports3},
110
-		// only entrypoints
111
-		{Entrypoint: entrypoint1}: {Entrypoint: entrypoint2},
112
-		// not the same number of parts
113
-		{Entrypoint: entrypoint1}: {Entrypoint: entrypoint3},
114
-		// only volumes
115
-		{Volumes: volumes1}: {Volumes: volumes2},
116
-		// not the same number of labels
117
-		{Volumes: volumes1}: {Volumes: volumes3},
118
-	}
119
-	for config1, config2 := range sameConfigs {
120
-		if !compare(config1, config2) {
121
-			t.Fatalf("Compare should be true for [%v] and [%v]", config1, config2)
122
-		}
123
-	}
124
-	for config1, config2 := range differentConfigs {
125
-		if compare(config1, config2) {
126
-			t.Fatalf("Compare should be false for [%v] and [%v]", config1, config2)
127
-		}
128
-	}
129
-}
130
-
131
-func TestPlatformCompare(t *testing.T) {
132
-	for _, tc := range []struct {
133
-		name     string
134
-		builder  ocispec.Platform
135
-		image    ocispec.Platform
136
-		expected bool
137
-	}{
138
-		{
139
-			name:     "same os and arch",
140
-			builder:  ocispec.Platform{Architecture: "amd64", OS: runtime.GOOS},
141
-			image:    ocispec.Platform{Architecture: "amd64", OS: runtime.GOOS},
142
-			expected: true,
143
-		},
144
-		{
145
-			name:     "same os different arch",
146
-			builder:  ocispec.Platform{Architecture: "amd64", OS: runtime.GOOS},
147
-			image:    ocispec.Platform{Architecture: "arm64", OS: runtime.GOOS},
148
-			expected: false,
149
-		},
150
-		{
151
-			name:     "same os smaller host variant",
152
-			builder:  ocispec.Platform{Variant: "v7", Architecture: "arm", OS: runtime.GOOS},
153
-			image:    ocispec.Platform{Variant: "v8", Architecture: "arm", OS: runtime.GOOS},
154
-			expected: false,
155
-		},
156
-		{
157
-			name:     "same os higher host variant",
158
-			builder:  ocispec.Platform{Variant: "v8", Architecture: "arm", OS: runtime.GOOS},
159
-			image:    ocispec.Platform{Variant: "v7", Architecture: "arm", OS: runtime.GOOS},
160
-			expected: true,
161
-		},
162
-		{
163
-			// Test for https://github.com/moby/moby/issues/47307
164
-			name:     "different build and revision",
165
-			builder:  ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.22621"},
166
-			image:    ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.17763.5329"},
167
-			expected: true,
168
-		},
169
-		{
170
-			name:     "different revision",
171
-			builder:  ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.17763.1234"},
172
-			image:    ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.17763.5329"},
173
-			expected: true,
174
-		},
175
-		{
176
-			name:     "different major",
177
-			builder:  ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "11.0.17763.5329"},
178
-			image:    ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.17763.5329"},
179
-			expected: false,
180
-		},
181
-		{
182
-			name:     "different minor same osver",
183
-			builder:  ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.17763.5329"},
184
-			image:    ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.1.17763.5329"},
185
-			expected: false,
186
-		},
187
-		{
188
-			name:     "different arch same osver",
189
-			builder:  ocispec.Platform{Architecture: "arm64", OS: "windows", OSVersion: "10.0.17763.5329"},
190
-			image:    ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.17763.5329"},
191
-			expected: false,
192
-		},
193
-	} {
194
-		// OSVersion comparison is only performed by containerd platform
195
-		// matcher if built on Windows.
196
-		if (tc.image.OSVersion != "" || tc.builder.OSVersion != "") && runtime.GOOS != "windows" {
197
-			continue
198
-		}
199
-
200
-		t.Run(tc.name, func(t *testing.T) {
201
-			assert.Check(t, is.Equal(comparePlatform(tc.builder, tc.image), tc.expected))
202
-		})
203
-	}
204
-}
205 1
deleted file mode 100644
... ...
@@ -1,175 +0,0 @@
1
-package image
2
-
3
-import (
4
-	"context"
5
-	"fmt"
6
-	"os"
7
-	"path/filepath"
8
-	"sync"
9
-
10
-	"github.com/containerd/log"
11
-	"github.com/moby/sys/atomicwriter"
12
-	"github.com/opencontainers/go-digest"
13
-	"github.com/pkg/errors"
14
-)
15
-
16
-// DigestWalkFunc is function called by StoreBackend.Walk
17
-type DigestWalkFunc func(id digest.Digest) error
18
-
19
-// StoreBackend provides interface for image.Store persistence
20
-type StoreBackend interface {
21
-	Walk(f DigestWalkFunc) error
22
-	Get(id digest.Digest) ([]byte, error)
23
-	Set(data []byte) (digest.Digest, error)
24
-	Delete(id digest.Digest) error
25
-	SetMetadata(id digest.Digest, key string, data []byte) error
26
-	GetMetadata(id digest.Digest, key string) ([]byte, error)
27
-	DeleteMetadata(id digest.Digest, key string) error
28
-}
29
-
30
-// fs implements StoreBackend using the filesystem.
31
-type fs struct {
32
-	sync.RWMutex
33
-	root string
34
-}
35
-
36
-const (
37
-	contentDirName  = "content"
38
-	metadataDirName = "metadata"
39
-)
40
-
41
-// NewFSStoreBackend returns new filesystem based backend for image.Store
42
-func NewFSStoreBackend(root string) (StoreBackend, error) {
43
-	return newFSStore(root)
44
-}
45
-
46
-func newFSStore(root string) (*fs, error) {
47
-	s := &fs{
48
-		root: root,
49
-	}
50
-	if err := os.MkdirAll(filepath.Join(root, contentDirName, string(digest.Canonical)), 0o700); err != nil {
51
-		return nil, errors.Wrap(err, "failed to create storage backend")
52
-	}
53
-	if err := os.MkdirAll(filepath.Join(root, metadataDirName, string(digest.Canonical)), 0o700); err != nil {
54
-		return nil, errors.Wrap(err, "failed to create storage backend")
55
-	}
56
-	return s, nil
57
-}
58
-
59
-func (s *fs) contentFile(dgst digest.Digest) string {
60
-	return filepath.Join(s.root, contentDirName, string(dgst.Algorithm()), dgst.Encoded())
61
-}
62
-
63
-func (s *fs) metadataDir(dgst digest.Digest) string {
64
-	return filepath.Join(s.root, metadataDirName, string(dgst.Algorithm()), dgst.Encoded())
65
-}
66
-
67
-// Walk calls the supplied callback for each image ID in the storage backend.
68
-func (s *fs) Walk(f DigestWalkFunc) error {
69
-	// Only Canonical digest (sha256) is currently supported
70
-	s.RLock()
71
-	dir, err := os.ReadDir(filepath.Join(s.root, contentDirName, string(digest.Canonical)))
72
-	s.RUnlock()
73
-	if err != nil {
74
-		return err
75
-	}
76
-	for _, v := range dir {
77
-		dgst := digest.NewDigestFromEncoded(digest.Canonical, v.Name())
78
-		if err := dgst.Validate(); err != nil {
79
-			log.G(context.TODO()).Debugf("skipping invalid digest %s: %s", dgst, err)
80
-			continue
81
-		}
82
-		if err := f(dgst); err != nil {
83
-			return err
84
-		}
85
-	}
86
-	return nil
87
-}
88
-
89
-// Get returns the content stored under a given digest.
90
-func (s *fs) Get(dgst digest.Digest) ([]byte, error) {
91
-	s.RLock()
92
-	defer s.RUnlock()
93
-
94
-	return s.get(dgst)
95
-}
96
-
97
-func (s *fs) get(dgst digest.Digest) ([]byte, error) {
98
-	content, err := os.ReadFile(s.contentFile(dgst))
99
-	if err != nil {
100
-		return nil, errors.Wrapf(err, "failed to get digest %s", dgst)
101
-	}
102
-
103
-	// todo: maybe optional
104
-	if digest.FromBytes(content) != dgst {
105
-		return nil, fmt.Errorf("failed to verify: %v", dgst)
106
-	}
107
-
108
-	return content, nil
109
-}
110
-
111
-// Set stores content by checksum.
112
-func (s *fs) Set(data []byte) (digest.Digest, error) {
113
-	s.Lock()
114
-	defer s.Unlock()
115
-
116
-	if len(data) == 0 {
117
-		return "", errors.New("invalid empty data")
118
-	}
119
-
120
-	dgst := digest.FromBytes(data)
121
-	if err := atomicwriter.WriteFile(s.contentFile(dgst), data, 0o600); err != nil {
122
-		return "", errors.Wrap(err, "failed to write digest data")
123
-	}
124
-
125
-	return dgst, nil
126
-}
127
-
128
-// Delete removes content and metadata files associated with the digest.
129
-func (s *fs) Delete(dgst digest.Digest) error {
130
-	s.Lock()
131
-	defer s.Unlock()
132
-
133
-	if err := os.RemoveAll(s.metadataDir(dgst)); err != nil {
134
-		return err
135
-	}
136
-	return os.Remove(s.contentFile(dgst))
137
-}
138
-
139
-// SetMetadata sets metadata for a given ID. It fails if there's no base file.
140
-func (s *fs) SetMetadata(dgst digest.Digest, key string, data []byte) error {
141
-	s.Lock()
142
-	defer s.Unlock()
143
-	if _, err := s.get(dgst); err != nil {
144
-		return err
145
-	}
146
-
147
-	baseDir := s.metadataDir(dgst)
148
-	if err := os.MkdirAll(baseDir, 0o700); err != nil {
149
-		return err
150
-	}
151
-	return atomicwriter.WriteFile(filepath.Join(baseDir, key), data, 0o600)
152
-}
153
-
154
-// GetMetadata returns metadata for a given digest.
155
-func (s *fs) GetMetadata(dgst digest.Digest, key string) ([]byte, error) {
156
-	s.RLock()
157
-	defer s.RUnlock()
158
-
159
-	if _, err := s.get(dgst); err != nil {
160
-		return nil, err
161
-	}
162
-	bytes, err := os.ReadFile(filepath.Join(s.metadataDir(dgst), key))
163
-	if err != nil {
164
-		return nil, errors.Wrap(err, "failed to read metadata")
165
-	}
166
-	return bytes, nil
167
-}
168
-
169
-// DeleteMetadata removes the metadata associated with a digest.
170
-func (s *fs) DeleteMetadata(dgst digest.Digest, key string) error {
171
-	s.Lock()
172
-	defer s.Unlock()
173
-
174
-	return os.RemoveAll(filepath.Join(s.metadataDir(dgst), key))
175
-}
176 1
deleted file mode 100644
... ...
@@ -1,257 +0,0 @@
1
-package image
2
-
3
-import (
4
-	"crypto/rand"
5
-	"crypto/sha256"
6
-	"encoding/hex"
7
-	"errors"
8
-	"os"
9
-	"path/filepath"
10
-	"testing"
11
-
12
-	"github.com/opencontainers/go-digest"
13
-	"gotest.tools/v3/assert"
14
-	is "gotest.tools/v3/assert/cmp"
15
-)
16
-
17
-func defaultFSStoreBackend(t *testing.T) StoreBackend {
18
-	t.Helper()
19
-	fsBackend, err := NewFSStoreBackend(t.TempDir())
20
-	assert.Check(t, err)
21
-	return fsBackend
22
-}
23
-
24
-func TestFSGetInvalidData(t *testing.T) {
25
-	rootDir := t.TempDir()
26
-	fsStore, err := NewFSStoreBackend(rootDir)
27
-	assert.Check(t, err)
28
-
29
-	dgst, err := fsStore.Set([]byte("foobar"))
30
-	assert.Check(t, err)
31
-
32
-	err = os.WriteFile(filepath.Join(rootDir, contentDirName, string(dgst.Algorithm()), dgst.Encoded()), []byte("foobar2"), 0o600)
33
-	assert.Check(t, err)
34
-
35
-	_, err = fsStore.Get(dgst)
36
-	assert.Check(t, is.ErrorContains(err, "failed to verify"))
37
-}
38
-
39
-func TestFSInvalidSet(t *testing.T) {
40
-	rootDir := t.TempDir()
41
-	fsStore, err := NewFSStoreBackend(rootDir)
42
-	assert.Check(t, err)
43
-
44
-	id := digest.FromBytes([]byte("foobar"))
45
-	err = os.Mkdir(filepath.Join(rootDir, contentDirName, string(id.Algorithm()), id.Encoded()), 0o700)
46
-	assert.Check(t, err)
47
-
48
-	_, err = fsStore.Set([]byte("foobar"))
49
-	assert.Check(t, is.ErrorContains(err, "failed to write digest data"))
50
-}
51
-
52
-func TestFSInvalidRoot(t *testing.T) {
53
-	tmpdir := t.TempDir()
54
-
55
-	tcases := []struct {
56
-		root, invalidFile string
57
-	}{
58
-		{"root", "root"},
59
-		{"root", "root/content"},
60
-		{"root", "root/metadata"},
61
-	}
62
-
63
-	for _, tc := range tcases {
64
-		root := filepath.Join(tmpdir, tc.root)
65
-		filePath := filepath.Join(tmpdir, tc.invalidFile)
66
-		err := os.MkdirAll(filepath.Dir(filePath), 0o700)
67
-		assert.Check(t, err)
68
-
69
-		f, err := os.Create(filePath)
70
-		assert.Check(t, err)
71
-		f.Close()
72
-
73
-		_, err = NewFSStoreBackend(root)
74
-		assert.Check(t, is.ErrorContains(err, "failed to create storage backend"))
75
-
76
-		os.RemoveAll(root)
77
-	}
78
-}
79
-
80
-func TestFSMetadataGetSet(t *testing.T) {
81
-	fsStore := defaultFSStoreBackend(t)
82
-
83
-	id, err := fsStore.Set([]byte("foo"))
84
-	assert.Check(t, err)
85
-
86
-	id2, err := fsStore.Set([]byte("bar"))
87
-	assert.Check(t, err)
88
-
89
-	tcases := []struct {
90
-		id    digest.Digest
91
-		key   string
92
-		value []byte
93
-	}{
94
-		{id, "tkey", []byte("tval1")},
95
-		{id, "tkey2", []byte("tval2")},
96
-		{id2, "tkey", []byte("tval3")},
97
-	}
98
-
99
-	for _, tc := range tcases {
100
-		err = fsStore.SetMetadata(tc.id, tc.key, tc.value)
101
-		assert.Check(t, err)
102
-
103
-		actual, err := fsStore.GetMetadata(tc.id, tc.key)
104
-		assert.Check(t, err)
105
-
106
-		assert.Check(t, is.DeepEqual(tc.value, actual))
107
-	}
108
-
109
-	_, err = fsStore.GetMetadata(id2, "tkey2")
110
-	assert.Check(t, is.ErrorContains(err, "failed to read metadata"))
111
-
112
-	id3 := digest.FromBytes([]byte("baz"))
113
-	err = fsStore.SetMetadata(id3, "tkey", []byte("tval"))
114
-	assert.Check(t, is.ErrorContains(err, "failed to get digest"))
115
-
116
-	_, err = fsStore.GetMetadata(id3, "tkey")
117
-	assert.Check(t, is.ErrorContains(err, "failed to get digest"))
118
-}
119
-
120
-func TestFSInvalidWalker(t *testing.T) {
121
-	rootDir := t.TempDir()
122
-	fsStore, err := NewFSStoreBackend(rootDir)
123
-	assert.Check(t, err)
124
-
125
-	fooID, err := fsStore.Set([]byte("foo"))
126
-	assert.Check(t, err)
127
-
128
-	err = os.WriteFile(filepath.Join(rootDir, contentDirName, "sha256/foobar"), []byte("foobar"), 0o600)
129
-	assert.Check(t, err)
130
-
131
-	n := 0
132
-	err = fsStore.Walk(func(id digest.Digest) error {
133
-		assert.Check(t, is.Equal(fooID, id))
134
-		n++
135
-		return nil
136
-	})
137
-	assert.Check(t, err)
138
-	assert.Check(t, is.Equal(1, n))
139
-}
140
-
141
-func TestFSGetSet(t *testing.T) {
142
-	fsStore := defaultFSStoreBackend(t)
143
-
144
-	type tcase struct {
145
-		input    []byte
146
-		expected digest.Digest
147
-	}
148
-	tcases := []tcase{
149
-		{[]byte("foobar"), digest.Digest("sha256:c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2")},
150
-	}
151
-
152
-	randomInput := make([]byte, 8*1024)
153
-	_, err := rand.Read(randomInput)
154
-	assert.Check(t, err)
155
-
156
-	// skipping use of digest pkg because it is used by the implementation
157
-	h := sha256.New()
158
-	_, err = h.Write(randomInput)
159
-	assert.Check(t, err)
160
-
161
-	tcases = append(tcases, tcase{
162
-		input:    randomInput,
163
-		expected: digest.Digest("sha256:" + hex.EncodeToString(h.Sum(nil))),
164
-	})
165
-
166
-	for _, tc := range tcases {
167
-		id, err := fsStore.Set(tc.input)
168
-		assert.Check(t, err)
169
-		assert.Check(t, is.Equal(tc.expected, id))
170
-	}
171
-
172
-	for _, tc := range tcases {
173
-		data, err := fsStore.Get(tc.expected)
174
-		assert.Check(t, err)
175
-		assert.Check(t, is.DeepEqual(tc.input, data))
176
-	}
177
-}
178
-
179
-func TestFSGetUnsetKey(t *testing.T) {
180
-	fsStore := defaultFSStoreBackend(t)
181
-
182
-	for _, key := range []digest.Digest{"foobar:abc", "sha256:abc", "sha256:c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2a"} {
183
-		_, err := fsStore.Get(key)
184
-		assert.Check(t, is.ErrorContains(err, "failed to get digest"))
185
-	}
186
-}
187
-
188
-func TestFSGetEmptyData(t *testing.T) {
189
-	fsStore := defaultFSStoreBackend(t)
190
-
191
-	for _, emptyData := range [][]byte{nil, {}} {
192
-		_, err := fsStore.Set(emptyData)
193
-		assert.Check(t, is.ErrorContains(err, "invalid empty data"))
194
-	}
195
-}
196
-
197
-func TestFSDelete(t *testing.T) {
198
-	fsStore := defaultFSStoreBackend(t)
199
-
200
-	id, err := fsStore.Set([]byte("foo"))
201
-	assert.Check(t, err)
202
-
203
-	id2, err := fsStore.Set([]byte("bar"))
204
-	assert.Check(t, err)
205
-
206
-	err = fsStore.Delete(id)
207
-	assert.Check(t, err)
208
-
209
-	_, err = fsStore.Get(id)
210
-	assert.Check(t, is.ErrorContains(err, "failed to get digest"))
211
-
212
-	_, err = fsStore.Get(id2)
213
-	assert.Check(t, err)
214
-
215
-	err = fsStore.Delete(id2)
216
-	assert.Check(t, err)
217
-
218
-	_, err = fsStore.Get(id2)
219
-	assert.Check(t, is.ErrorContains(err, "failed to get digest"))
220
-}
221
-
222
-func TestFSWalker(t *testing.T) {
223
-	fsStore := defaultFSStoreBackend(t)
224
-
225
-	id, err := fsStore.Set([]byte("foo"))
226
-	assert.Check(t, err)
227
-
228
-	id2, err := fsStore.Set([]byte("bar"))
229
-	assert.Check(t, err)
230
-
231
-	tcases := make(map[digest.Digest]struct{})
232
-	tcases[id] = struct{}{}
233
-	tcases[id2] = struct{}{}
234
-	n := 0
235
-	err = fsStore.Walk(func(id digest.Digest) error {
236
-		delete(tcases, id)
237
-		n++
238
-		return nil
239
-	})
240
-	assert.Check(t, err)
241
-	assert.Check(t, is.Equal(2, n))
242
-	assert.Check(t, is.Len(tcases, 0))
243
-}
244
-
245
-func TestFSWalkerStopOnError(t *testing.T) {
246
-	fsStore := defaultFSStoreBackend(t)
247
-
248
-	id, err := fsStore.Set([]byte("foo"))
249
-	assert.Check(t, err)
250
-
251
-	tcases := make(map[digest.Digest]struct{})
252
-	tcases[id] = struct{}{}
253
-	err = fsStore.Walk(func(id digest.Digest) error {
254
-		return errors.New("what")
255
-	})
256
-	assert.Check(t, is.ErrorContains(err, "what"))
257
-}
258 1
deleted file mode 100644
... ...
@@ -1,302 +0,0 @@
1
-package image
2
-
3
-import (
4
-	"context"
5
-	"encoding/json"
6
-	"errors"
7
-	"io"
8
-	"runtime"
9
-	"strings"
10
-	"time"
11
-
12
-	"github.com/docker/docker/daemon/internal/layer"
13
-	"github.com/docker/docker/dockerversion"
14
-	"github.com/moby/moby/api/types/container"
15
-	"github.com/opencontainers/go-digest"
16
-	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
17
-)
18
-
19
-// ID is the content-addressable ID of an image.
20
-type ID digest.Digest
21
-
22
-func (id ID) String() string {
23
-	return id.Digest().String()
24
-}
25
-
26
-// Digest converts ID into a digest
27
-func (id ID) Digest() digest.Digest {
28
-	return digest.Digest(id)
29
-}
30
-
31
-// V1Image stores the V1 image configuration.
32
-type V1Image struct {
33
-	// ID is a unique 64 character identifier of the image
34
-	ID string `json:"id,omitempty"`
35
-
36
-	// Parent is the ID of the parent image.
37
-	//
38
-	// Depending on how the image was created, this field may be empty and
39
-	// is only set for images that were built/created locally. This field
40
-	// is empty if the image was pulled from an image registry.
41
-	Parent string `json:"parent,omitempty"`
42
-
43
-	// Comment is an optional message that can be set when committing or
44
-	// importing the image.
45
-	Comment string `json:"comment,omitempty"`
46
-
47
-	// Created is the timestamp at which the image was created
48
-	Created *time.Time `json:"created"`
49
-
50
-	// Container is the ID of the container that was used to create the image.
51
-	//
52
-	// Depending on how the image was created, this field may be empty.
53
-	Container string `json:"container,omitempty"`
54
-
55
-	// ContainerConfig is the configuration of the container that was committed
56
-	// into the image.
57
-	ContainerConfig container.Config `json:"container_config,omitempty"`
58
-
59
-	// DockerVersion is the version of Docker that was used to build the image.
60
-	//
61
-	// Depending on how the image was created, this field may be empty.
62
-	DockerVersion string `json:"docker_version,omitempty"`
63
-
64
-	// Author is the name of the author that was specified when committing the
65
-	// image, or as specified through MAINTAINER (deprecated) in the Dockerfile.
66
-	Author string `json:"author,omitempty"`
67
-
68
-	// Config is the configuration of the container received from the client.
69
-	Config *container.Config `json:"config,omitempty"`
70
-
71
-	// Architecture is the hardware CPU architecture that the image runs on.
72
-	Architecture string `json:"architecture,omitempty"`
73
-
74
-	// Variant is the CPU architecture variant (presently ARM-only).
75
-	Variant string `json:"variant,omitempty"`
76
-
77
-	// OS is the Operating System the image is built to run on.
78
-	OS string `json:"os,omitempty"`
79
-
80
-	// Size is the total size of the image including all layers it is composed of.
81
-	Size int64 `json:",omitempty"`
82
-}
83
-
84
-// Image stores the image configuration
85
-type Image struct {
86
-	V1Image
87
-
88
-	// Parent is the ID of the parent image.
89
-	//
90
-	// Depending on how the image was created, this field may be empty and
91
-	// is only set for images that were built/created locally. This field
92
-	// is empty if the image was pulled from an image registry.
93
-	Parent ID `json:"parent,omitempty"` //nolint:govet
94
-
95
-	// RootFS contains information about the image's RootFS, including the
96
-	// layer IDs.
97
-	RootFS  *RootFS   `json:"rootfs,omitempty"`
98
-	History []History `json:"history,omitempty"`
99
-
100
-	// OsVersion is the version of the Operating System the image is built to
101
-	// run on (especially for Windows).
102
-	OSVersion  string   `json:"os.version,omitempty"`
103
-	OSFeatures []string `json:"os.features,omitempty"`
104
-
105
-	// rawJSON caches the immutable JSON associated with this image.
106
-	rawJSON []byte
107
-
108
-	// computedID is the ID computed from the hash of the image config.
109
-	// Not to be confused with the legacy V1 ID in V1Image.
110
-	computedID ID
111
-
112
-	// Details holds additional details about image
113
-	Details *Details `json:"-"`
114
-}
115
-
116
-// Details provides additional image data
117
-type Details struct {
118
-	// ManifestDescriptor is the descriptor of the platform-specific manifest
119
-	// chosen by the [GetImage] call that returned this image.
120
-	// The exact descriptor depends on the [GetImageOpts.Platform] field
121
-	// passed to [GetImage] and the content availability.
122
-	// This is only set by the containerd image service.
123
-	ManifestDescriptor *ocispec.Descriptor
124
-}
125
-
126
-// RawJSON returns the immutable JSON associated with the image.
127
-func (img *Image) RawJSON() []byte {
128
-	return img.rawJSON
129
-}
130
-
131
-// ID returns the image's content-addressable ID.
132
-func (img *Image) ID() ID {
133
-	return img.computedID
134
-}
135
-
136
-// ImageID stringifies ID.
137
-func (img *Image) ImageID() string {
138
-	return img.ID().String()
139
-}
140
-
141
-// RunConfig returns the image's container config.
142
-func (img *Image) RunConfig() *container.Config {
143
-	return img.Config
144
-}
145
-
146
-// BaseImgArch returns the image's architecture. If not populated, defaults to the host runtime arch.
147
-func (img *Image) BaseImgArch() string {
148
-	arch := img.Architecture
149
-	if arch == "" {
150
-		arch = runtime.GOARCH
151
-	}
152
-	return arch
153
-}
154
-
155
-// BaseImgVariant returns the image's variant, whether populated or not.
156
-// This avoids creating an inconsistency where the stored image variant
157
-// is "greater than" (i.e. v8 vs v6) the actual image variant.
158
-func (img *Image) BaseImgVariant() string {
159
-	return img.Variant
160
-}
161
-
162
-// OperatingSystem returns the image's operating system. If not populated, defaults to the host runtime OS.
163
-func (img *Image) OperatingSystem() string {
164
-	os := img.OS
165
-	if os == "" {
166
-		os = runtime.GOOS
167
-	}
168
-	return os
169
-}
170
-
171
-// Platform generates an OCI platform from the image
172
-func (img *Image) Platform() ocispec.Platform {
173
-	return ocispec.Platform{
174
-		Architecture: img.Architecture,
175
-		OS:           img.OS,
176
-		OSVersion:    img.OSVersion,
177
-		OSFeatures:   img.OSFeatures,
178
-		Variant:      img.Variant,
179
-	}
180
-}
181
-
182
-// MarshalJSON serializes the image to JSON. It sorts the top-level keys so
183
-// that JSON that's been manipulated by a push/pull cycle with a legacy
184
-// registry won't end up with a different key order.
185
-func (img *Image) MarshalJSON() ([]byte, error) {
186
-	type MarshalImage Image
187
-
188
-	pass1, err := json.Marshal(MarshalImage(*img))
189
-	if err != nil {
190
-		return nil, err
191
-	}
192
-
193
-	var c map[string]*json.RawMessage
194
-	if err := json.Unmarshal(pass1, &c); err != nil {
195
-		return nil, err
196
-	}
197
-	return json.Marshal(c)
198
-}
199
-
200
-// ChildConfig is the configuration to apply to an Image to create a new
201
-// Child image. Other properties of the image are copied from the parent.
202
-type ChildConfig struct {
203
-	ContainerID     string
204
-	Author          string
205
-	Comment         string
206
-	DiffID          layer.DiffID
207
-	ContainerConfig *container.Config
208
-	Config          *container.Config
209
-}
210
-
211
-// NewImage creates a new image with the given ID
212
-func NewImage(id ID) *Image {
213
-	return &Image{
214
-		computedID: id,
215
-	}
216
-}
217
-
218
-// NewChildImage creates a new Image as a child of this image.
219
-func NewChildImage(img *Image, child ChildConfig, os string) *Image {
220
-	isEmptyLayer := layer.IsEmpty(child.DiffID)
221
-	var rootFS *RootFS
222
-	if img.RootFS != nil {
223
-		rootFS = img.RootFS.Clone()
224
-	} else {
225
-		rootFS = NewRootFS()
226
-	}
227
-
228
-	if !isEmptyLayer {
229
-		rootFS.Append(child.DiffID)
230
-	}
231
-	imgHistory := NewHistory(
232
-		child.Author,
233
-		child.Comment,
234
-		strings.Join(child.ContainerConfig.Cmd, " "),
235
-		isEmptyLayer)
236
-
237
-	return &Image{
238
-		V1Image: V1Image{
239
-			DockerVersion:   dockerversion.Version,
240
-			Config:          child.Config,
241
-			Architecture:    img.BaseImgArch(),
242
-			Variant:         img.BaseImgVariant(),
243
-			OS:              os,
244
-			Container:       child.ContainerID,
245
-			ContainerConfig: *child.ContainerConfig,
246
-			Author:          child.Author,
247
-			Created:         imgHistory.Created,
248
-		},
249
-		RootFS:     rootFS,
250
-		History:    append(img.History, imgHistory),
251
-		OSFeatures: img.OSFeatures,
252
-		OSVersion:  img.OSVersion,
253
-	}
254
-}
255
-
256
-// Clone clones an image and changes ID.
257
-func Clone(base *Image, id ID) *Image {
258
-	img := *base
259
-	img.RootFS = img.RootFS.Clone()
260
-	img.V1Image.ID = id.String()
261
-	img.computedID = id
262
-	return &img
263
-}
264
-
265
-// History stores build commands that were used to create an image
266
-type History = ocispec.History
267
-
268
-// NewHistory creates a new history struct from arguments, and sets the created
269
-// time to the current time in UTC
270
-func NewHistory(author, comment, createdBy string, isEmptyLayer bool) History {
271
-	now := time.Now().UTC()
272
-	return History{
273
-		Author:     author,
274
-		Created:    &now,
275
-		CreatedBy:  createdBy,
276
-		Comment:    comment,
277
-		EmptyLayer: isEmptyLayer,
278
-	}
279
-}
280
-
281
-// Exporter provides interface for loading and saving images
282
-type Exporter interface {
283
-	Load(context.Context, io.ReadCloser, io.Writer, bool) error
284
-	// TODO: Load(net.Context, io.ReadCloser, <- chan StatusMessage) error
285
-	Save(context.Context, []string, io.Writer) error
286
-}
287
-
288
-// NewFromJSON creates an Image configuration from json.
289
-func NewFromJSON(src []byte) (*Image, error) {
290
-	img := &Image{}
291
-
292
-	if err := json.Unmarshal(src, img); err != nil {
293
-		return nil, err
294
-	}
295
-	if img.RootFS == nil {
296
-		return nil, errors.New("invalid image JSON, no RootFS key")
297
-	}
298
-
299
-	img.rawJSON = src
300
-
301
-	return img, nil
302
-}
303 1
deleted file mode 100644
... ...
@@ -1,18 +0,0 @@
1
-package image
2
-
3
-import (
4
-	"errors"
5
-	"runtime"
6
-	"strings"
7
-
8
-	"github.com/docker/docker/errdefs"
9
-)
10
-
11
-// CheckOS checks if the given OS matches the host's platform, and
12
-// returns an error otherwise.
13
-func CheckOS(os string) error {
14
-	if !strings.EqualFold(runtime.GOOS, os) {
15
-		return errdefs.InvalidParameter(errors.New("operating system is not supported"))
16
-	}
17
-	return nil
18
-}
19 1
deleted file mode 100644
... ...
@@ -1,125 +0,0 @@
1
-package image
2
-
3
-import (
4
-	"encoding/json"
5
-	"runtime"
6
-	"sort"
7
-	"strings"
8
-	"testing"
9
-
10
-	"github.com/docker/docker/daemon/internal/layer"
11
-	"github.com/google/go-cmp/cmp"
12
-	"github.com/moby/moby/api/types/container"
13
-	"gotest.tools/v3/assert"
14
-	is "gotest.tools/v3/assert/cmp"
15
-)
16
-
17
-const sampleImageJSON = `{
18
-	"architecture": "amd64",
19
-	"os": "linux",
20
-	"config": {},
21
-	"rootfs": {
22
-		"type": "layers",
23
-		"diff_ids": []
24
-	}
25
-}`
26
-
27
-func TestNewFromJSON(t *testing.T) {
28
-	img, err := NewFromJSON([]byte(sampleImageJSON))
29
-	assert.NilError(t, err)
30
-	assert.Check(t, is.Equal(sampleImageJSON, string(img.RawJSON())))
31
-}
32
-
33
-func TestNewFromJSONWithInvalidJSON(t *testing.T) {
34
-	_, err := NewFromJSON([]byte("{}"))
35
-	assert.Check(t, is.Error(err, "invalid image JSON, no RootFS key"))
36
-}
37
-
38
-func TestMarshalKeyOrder(t *testing.T) {
39
-	b, err := json.Marshal(&Image{
40
-		V1Image: V1Image{
41
-			Comment:      "a",
42
-			Author:       "b",
43
-			Architecture: "c",
44
-		},
45
-	})
46
-	assert.Check(t, err)
47
-
48
-	expectedOrder := []string{"architecture", "author", "comment"}
49
-	var indexes []int
50
-	for _, k := range expectedOrder {
51
-		indexes = append(indexes, strings.Index(string(b), k))
52
-	}
53
-
54
-	if !sort.IntsAreSorted(indexes) {
55
-		t.Fatal("invalid key order in JSON: ", string(b))
56
-	}
57
-}
58
-
59
-func TestImage(t *testing.T) {
60
-	cid := "50a16564e727"
61
-	config := &container.Config{
62
-		Hostname:   "hostname",
63
-		Domainname: "domain",
64
-		User:       "root",
65
-	}
66
-	os := runtime.GOOS
67
-
68
-	img := &Image{
69
-		V1Image: V1Image{
70
-			Config: config,
71
-		},
72
-		computedID: ID(cid),
73
-	}
74
-
75
-	assert.Check(t, is.Equal(cid, img.ImageID()))
76
-	assert.Check(t, is.Equal(cid, img.ID().String()))
77
-	assert.Check(t, is.Equal(os, img.OperatingSystem()))
78
-	assert.Check(t, is.DeepEqual(config, img.RunConfig()))
79
-}
80
-
81
-func TestImageOSNotEmpty(t *testing.T) {
82
-	os := "os"
83
-	img := &Image{
84
-		V1Image: V1Image{
85
-			OS: os,
86
-		},
87
-		OSVersion: "osversion",
88
-	}
89
-	assert.Check(t, is.Equal(os, img.OperatingSystem()))
90
-}
91
-
92
-func TestNewChildImageFromImageWithRootFS(t *testing.T) {
93
-	rootFS := NewRootFS()
94
-	rootFS.Append("ba5e")
95
-	parent := &Image{
96
-		RootFS: rootFS,
97
-		History: []History{
98
-			NewHistory("a", "c", "r", false),
99
-		},
100
-	}
101
-	childConfig := ChildConfig{
102
-		DiffID:  layer.DiffID("abcdef"),
103
-		Author:  "author",
104
-		Comment: "comment",
105
-		ContainerConfig: &container.Config{
106
-			Cmd: []string{"echo", "foo"},
107
-		},
108
-		Config: &container.Config{},
109
-	}
110
-
111
-	newImage := NewChildImage(parent, childConfig, "platform")
112
-	expectedDiffIDs := []layer.DiffID{"ba5e", "abcdef"}
113
-	assert.Check(t, is.DeepEqual(expectedDiffIDs, newImage.RootFS.DiffIDs))
114
-	assert.Check(t, is.Equal(childConfig.Author, newImage.Author))
115
-	assert.Check(t, is.DeepEqual(childConfig.Config, newImage.Config))
116
-	assert.Check(t, is.DeepEqual(*childConfig.ContainerConfig, newImage.ContainerConfig))
117
-	assert.Check(t, is.Equal("platform", newImage.OS))
118
-	assert.Check(t, is.DeepEqual(childConfig.Config, newImage.Config))
119
-
120
-	assert.Check(t, is.Len(newImage.History, 2))
121
-	assert.Check(t, is.Equal(childConfig.Comment, newImage.History[1].Comment))
122
-
123
-	assert.Check(t, !cmp.Equal(parent.RootFS.DiffIDs, newImage.RootFS.DiffIDs),
124
-		"RootFS should be copied not mutated")
125
-}
126 1
deleted file mode 100644
... ...
@@ -1,45 +0,0 @@
1
-// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
2
-//go:build go1.23
3
-
4
-package image
5
-
6
-import (
7
-	"slices"
8
-
9
-	"github.com/docker/docker/daemon/internal/layer"
10
-	"github.com/opencontainers/image-spec/identity"
11
-)
12
-
13
-// TypeLayers is used for RootFS.Type for filesystems organized into layers.
14
-const TypeLayers = "layers"
15
-
16
-// RootFS describes images root filesystem
17
-// This is currently a placeholder that only supports layers. In the future
18
-// this can be made into an interface that supports different implementations.
19
-type RootFS struct {
20
-	Type    string         `json:"type"`
21
-	DiffIDs []layer.DiffID `json:"diff_ids,omitempty"`
22
-}
23
-
24
-// NewRootFS returns empty RootFS struct
25
-func NewRootFS() *RootFS {
26
-	return &RootFS{Type: TypeLayers}
27
-}
28
-
29
-// Append appends a new diffID to rootfs
30
-func (r *RootFS) Append(id layer.DiffID) {
31
-	r.DiffIDs = append(r.DiffIDs, id)
32
-}
33
-
34
-// Clone returns a copy of the RootFS
35
-func (r *RootFS) Clone() *RootFS {
36
-	return &RootFS{
37
-		Type:    r.Type,
38
-		DiffIDs: slices.Clone(r.DiffIDs),
39
-	}
40
-}
41
-
42
-// ChainID returns the ChainID for the top layer in RootFS.
43
-func (r *RootFS) ChainID() layer.ChainID {
44
-	return identity.ChainID(r.DiffIDs)
45
-}
46 1
deleted file mode 100644
... ...
@@ -1,4 +0,0 @@
1
-# Docker Image Specification v1.
2
-
3
-This specification moved to a separate repository:
4
-https://github.com/moby/docker-image-spec
5 1
deleted file mode 100644
... ...
@@ -1,4 +0,0 @@
1
-# Docker Image Specification v1.3.0
2
-
3
-This specification moved to a separate repository:
4
-https://github.com/moby/docker-image-spec
5 1
deleted file mode 100644
... ...
@@ -1,4 +0,0 @@
1
-# Docker Image Specification v1.1.0
2
-
3
-This specification moved to a separate repository:
4
-https://github.com/moby/docker-image-spec
5 1
deleted file mode 100644
... ...
@@ -1,4 +0,0 @@
1
-# Docker Image Specification v1.2.0
2
-
3
-This specification moved to a separate repository:
4
-https://github.com/moby/docker-image-spec
5 1
deleted file mode 100644
... ...
@@ -1,4 +0,0 @@
1
-# Docker Image Specification v1.0.0
2
-
3
-This specification moved to a separate repository:
4
-https://github.com/moby/docker-image-spec
5 1
deleted file mode 100644
... ...
@@ -1,367 +0,0 @@
1
-package image
2
-
3
-import (
4
-	"context"
5
-	"fmt"
6
-	"os"
7
-	"sync"
8
-	"time"
9
-
10
-	"github.com/containerd/log"
11
-	"github.com/docker/docker/daemon/internal/layer"
12
-	"github.com/docker/docker/errdefs"
13
-	"github.com/opencontainers/go-digest"
14
-	"github.com/opencontainers/go-digest/digestset"
15
-	"github.com/pkg/errors"
16
-)
17
-
18
-// Store is an interface for creating and accessing images
19
-type Store interface {
20
-	Create(config []byte) (ID, error)
21
-	Get(id ID) (*Image, error)
22
-	Delete(id ID) ([]layer.Metadata, error)
23
-	Search(partialID string) (ID, error)
24
-	SetParent(id ID, parent ID) error
25
-	GetParent(id ID) (ID, error)
26
-	SetLastUpdated(id ID) error
27
-	GetLastUpdated(id ID) (time.Time, error)
28
-	SetBuiltLocally(id ID) error
29
-	IsBuiltLocally(id ID) (bool, error)
30
-	Children(id ID) []ID
31
-	Map() map[ID]*Image
32
-	Heads() map[ID]*Image
33
-	Len() int
34
-}
35
-
36
-// LayerGetReleaser is a minimal interface for getting and releasing images.
37
-type LayerGetReleaser interface {
38
-	Get(layer.ChainID) (layer.Layer, error)
39
-	Release(layer.Layer) ([]layer.Metadata, error)
40
-}
41
-
42
-type imageMeta struct {
43
-	layer    layer.Layer
44
-	children map[ID]struct{}
45
-}
46
-
47
-type store struct {
48
-	sync.RWMutex
49
-	lss       LayerGetReleaser
50
-	images    map[ID]*imageMeta
51
-	fs        StoreBackend
52
-	digestSet *digestset.Set
53
-}
54
-
55
-// NewImageStore returns new store object for given set of layer stores
56
-func NewImageStore(fs StoreBackend, lss LayerGetReleaser) (Store, error) {
57
-	is := &store{
58
-		lss:       lss,
59
-		images:    make(map[ID]*imageMeta),
60
-		fs:        fs,
61
-		digestSet: digestset.NewSet(),
62
-	}
63
-
64
-	// load all current images and retain layers
65
-	if err := is.restore(); err != nil {
66
-		return nil, err
67
-	}
68
-
69
-	return is, nil
70
-}
71
-
72
-func (is *store) restore() error {
73
-	// As the code below is run when restoring all images (which can be "many"),
74
-	// constructing the "log.G(ctx).WithFields" is deliberately not "DRY", as the
75
-	// logger is only used for error-cases, and we don't want to do allocations
76
-	// if we don't need it. The "f" type alias is here is just for convenience,
77
-	// and to make the code _slightly_ more DRY. See the discussion on GitHub;
78
-	// https://github.com/moby/moby/pull/44426#discussion_r1059519071
79
-	type f = log.Fields
80
-	err := is.fs.Walk(func(dgst digest.Digest) error {
81
-		img, err := is.Get(ID(dgst))
82
-		if err != nil {
83
-			log.G(context.TODO()).WithFields(f{"digest": dgst, "err": err}).Error("invalid image")
84
-			return nil
85
-		}
86
-		var l layer.Layer
87
-		if chainID := img.RootFS.ChainID(); chainID != "" {
88
-			if err := CheckOS(img.OperatingSystem()); err != nil {
89
-				log.G(context.TODO()).WithFields(f{"chainID": chainID, "os": img.OperatingSystem()}).Error("not restoring image with unsupported operating system")
90
-				return nil
91
-			}
92
-			l, err = is.lss.Get(chainID)
93
-			if err != nil {
94
-				if errors.Is(err, layer.ErrLayerDoesNotExist) {
95
-					log.G(context.TODO()).WithFields(f{"chainID": chainID, "os": img.OperatingSystem(), "err": err}).Error("not restoring image")
96
-					return nil
97
-				}
98
-				return err
99
-			}
100
-		}
101
-		if err := is.digestSet.Add(dgst); err != nil {
102
-			return err
103
-		}
104
-
105
-		is.images[ID(dgst)] = &imageMeta{
106
-			layer:    l,
107
-			children: make(map[ID]struct{}),
108
-		}
109
-
110
-		return nil
111
-	})
112
-	if err != nil {
113
-		return err
114
-	}
115
-
116
-	// Second pass to fill in children maps
117
-	for id := range is.images {
118
-		if parent, err := is.GetParent(id); err == nil {
119
-			if parentMeta := is.images[parent]; parentMeta != nil {
120
-				parentMeta.children[id] = struct{}{}
121
-			}
122
-		}
123
-	}
124
-
125
-	return nil
126
-}
127
-
128
-func (is *store) Create(config []byte) (ID, error) {
129
-	var img *Image
130
-	img, err := NewFromJSON(config)
131
-	if err != nil {
132
-		return "", err
133
-	}
134
-
135
-	// Must reject any config that references diffIDs from the history
136
-	// which aren't among the rootfs layers.
137
-	rootFSLayers := make(map[layer.DiffID]struct{})
138
-	for _, diffID := range img.RootFS.DiffIDs {
139
-		rootFSLayers[diffID] = struct{}{}
140
-	}
141
-
142
-	layerCounter := 0
143
-	for _, h := range img.History {
144
-		if !h.EmptyLayer {
145
-			layerCounter++
146
-		}
147
-	}
148
-	if layerCounter > len(img.RootFS.DiffIDs) {
149
-		return "", errdefs.InvalidParameter(errors.New("too many non-empty layers in History section"))
150
-	}
151
-
152
-	imageDigest, err := is.fs.Set(config)
153
-	if err != nil {
154
-		return "", errdefs.InvalidParameter(err)
155
-	}
156
-
157
-	is.Lock()
158
-	defer is.Unlock()
159
-
160
-	imageID := ID(imageDigest)
161
-	if _, exists := is.images[imageID]; exists {
162
-		return imageID, nil
163
-	}
164
-
165
-	layerID := img.RootFS.ChainID()
166
-
167
-	var l layer.Layer
168
-	if layerID != "" {
169
-		if err := CheckOS(img.OperatingSystem()); err != nil {
170
-			return "", err
171
-		}
172
-		l, err = is.lss.Get(layerID)
173
-		if err != nil {
174
-			return "", errdefs.InvalidParameter(errors.Wrapf(err, "failed to get layer %s", layerID))
175
-		}
176
-	}
177
-
178
-	is.images[imageID] = &imageMeta{
179
-		layer:    l,
180
-		children: make(map[ID]struct{}),
181
-	}
182
-
183
-	if err = is.digestSet.Add(imageDigest); err != nil {
184
-		delete(is.images, imageID)
185
-		return "", errdefs.InvalidParameter(err)
186
-	}
187
-
188
-	return imageID, nil
189
-}
190
-
191
-type imageNotFoundError string
192
-
193
-func (e imageNotFoundError) Error() string {
194
-	return "No such image: " + string(e)
195
-}
196
-
197
-func (imageNotFoundError) NotFound() {}
198
-
199
-func (is *store) Search(term string) (ID, error) {
200
-	dgst, err := is.digestSet.Lookup(term)
201
-	if err != nil {
202
-		if errors.Is(err, digestset.ErrDigestNotFound) {
203
-			err = imageNotFoundError(term)
204
-		}
205
-		return "", errors.WithStack(err)
206
-	}
207
-	return ID(dgst), nil
208
-}
209
-
210
-func (is *store) Get(id ID) (*Image, error) {
211
-	// todo: Check if image is in images
212
-	// todo: Detect manual insertions and start using them
213
-	config, err := is.fs.Get(id.Digest())
214
-	if err != nil {
215
-		return nil, errdefs.NotFound(err)
216
-	}
217
-
218
-	img, err := NewFromJSON(config)
219
-	if err != nil {
220
-		return nil, errdefs.InvalidParameter(err)
221
-	}
222
-	img.computedID = id
223
-
224
-	img.Parent, err = is.GetParent(id)
225
-	if err != nil {
226
-		img.Parent = ""
227
-	}
228
-
229
-	return img, nil
230
-}
231
-
232
-func (is *store) Delete(id ID) ([]layer.Metadata, error) {
233
-	is.Lock()
234
-	defer is.Unlock()
235
-
236
-	imgMeta := is.images[id]
237
-	if imgMeta == nil {
238
-		return nil, errdefs.NotFound(fmt.Errorf("unrecognized image ID %s", id.String()))
239
-	}
240
-	_, err := is.Get(id)
241
-	if err != nil {
242
-		return nil, errdefs.NotFound(fmt.Errorf("unrecognized image %s, %v", id.String(), err))
243
-	}
244
-	for cID := range imgMeta.children {
245
-		is.fs.DeleteMetadata(cID.Digest(), "parent")
246
-	}
247
-	if parent, err := is.GetParent(id); err == nil && is.images[parent] != nil {
248
-		delete(is.images[parent].children, id)
249
-	}
250
-
251
-	if err := is.digestSet.Remove(id.Digest()); err != nil {
252
-		log.G(context.TODO()).Errorf("error removing %s from digest set: %q", id, err)
253
-	}
254
-	delete(is.images, id)
255
-	is.fs.Delete(id.Digest())
256
-
257
-	if imgMeta.layer != nil {
258
-		return is.lss.Release(imgMeta.layer)
259
-	}
260
-	return nil, nil
261
-}
262
-
263
-func (is *store) SetParent(id, parentID ID) error {
264
-	is.Lock()
265
-	defer is.Unlock()
266
-	parentMeta := is.images[parentID]
267
-	if parentMeta == nil {
268
-		return errdefs.NotFound(fmt.Errorf("unknown parent image ID %s", parentID.String()))
269
-	}
270
-	if parent, err := is.GetParent(id); err == nil && is.images[parent] != nil {
271
-		delete(is.images[parent].children, id)
272
-	}
273
-	parentMeta.children[id] = struct{}{}
274
-	return is.fs.SetMetadata(id.Digest(), "parent", []byte(parentID))
275
-}
276
-
277
-func (is *store) GetParent(id ID) (ID, error) {
278
-	d, err := is.fs.GetMetadata(id.Digest(), "parent")
279
-	if err != nil {
280
-		return "", errdefs.NotFound(err)
281
-	}
282
-	return ID(d), nil // todo: validate?
283
-}
284
-
285
-// SetLastUpdated time for the image ID to the current time
286
-func (is *store) SetLastUpdated(id ID) error {
287
-	lastUpdated := []byte(time.Now().Format(time.RFC3339Nano))
288
-	return is.fs.SetMetadata(id.Digest(), "lastUpdated", lastUpdated)
289
-}
290
-
291
-// GetLastUpdated time for the image ID
292
-func (is *store) GetLastUpdated(id ID) (time.Time, error) {
293
-	bytes, err := is.fs.GetMetadata(id.Digest(), "lastUpdated")
294
-	if err != nil || len(bytes) == 0 {
295
-		// No lastUpdated time
296
-		return time.Time{}, nil
297
-	}
298
-	return time.Parse(time.RFC3339Nano, string(bytes))
299
-}
300
-
301
-// SetBuiltLocally sets whether image can be used as a builder cache
302
-func (is *store) SetBuiltLocally(id ID) error {
303
-	return is.fs.SetMetadata(id.Digest(), "builtLocally", []byte{1})
304
-}
305
-
306
-// IsBuiltLocally returns whether image can be used as a builder cache
307
-func (is *store) IsBuiltLocally(id ID) (bool, error) {
308
-	bytes, err := is.fs.GetMetadata(id.Digest(), "builtLocally")
309
-	if err != nil || len(bytes) == 0 {
310
-		if errors.Is(err, os.ErrNotExist) {
311
-			err = nil
312
-		}
313
-		return false, err
314
-	}
315
-	return bytes[0] == 1, nil
316
-}
317
-
318
-func (is *store) Children(id ID) []ID {
319
-	is.RLock()
320
-	defer is.RUnlock()
321
-
322
-	return is.children(id)
323
-}
324
-
325
-func (is *store) children(id ID) []ID {
326
-	var ids []ID
327
-	if is.images[id] != nil {
328
-		for id := range is.images[id].children {
329
-			ids = append(ids, id)
330
-		}
331
-	}
332
-	return ids
333
-}
334
-
335
-func (is *store) Heads() map[ID]*Image {
336
-	return is.imagesMap(false)
337
-}
338
-
339
-func (is *store) Map() map[ID]*Image {
340
-	return is.imagesMap(true)
341
-}
342
-
343
-func (is *store) imagesMap(all bool) map[ID]*Image {
344
-	is.RLock()
345
-	defer is.RUnlock()
346
-
347
-	images := make(map[ID]*Image)
348
-
349
-	for id := range is.images {
350
-		if !all && len(is.children(id)) > 0 {
351
-			continue
352
-		}
353
-		img, err := is.Get(id)
354
-		if err != nil {
355
-			log.G(context.TODO()).Errorf("invalid image access: %q, error: %q", id, err)
356
-			continue
357
-		}
358
-		images[id] = img
359
-	}
360
-	return images
361
-}
362
-
363
-func (is *store) Len() int {
364
-	is.RLock()
365
-	defer is.RUnlock()
366
-	return len(is.images)
367
-}
368 1
deleted file mode 100644
... ...
@@ -1,207 +0,0 @@
1
-package image
2
-
3
-import (
4
-	"fmt"
5
-	"testing"
6
-
7
-	cerrdefs "github.com/containerd/errdefs"
8
-	"github.com/docker/docker/daemon/internal/layer"
9
-	"gotest.tools/v3/assert"
10
-	is "gotest.tools/v3/assert/cmp"
11
-)
12
-
13
-func TestCreate(t *testing.T) {
14
-	imgStore := defaultImageStore(t)
15
-	_, err := imgStore.Create([]byte(`{}`))
16
-	assert.Check(t, is.Error(err, "invalid image JSON, no RootFS key"))
17
-}
18
-
19
-func TestRestore(t *testing.T) {
20
-	fsStore := defaultFSStoreBackend(t)
21
-
22
-	id1, err := fsStore.Set([]byte(`{"comment": "abc", "rootfs": {"type": "layers"}}`))
23
-	assert.NilError(t, err)
24
-
25
-	_, err = fsStore.Set([]byte(`invalid`))
26
-	assert.NilError(t, err)
27
-
28
-	id2, err := fsStore.Set([]byte(`{"comment": "def", "rootfs": {"type": "layers", "diff_ids": ["2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"]}}`))
29
-	assert.NilError(t, err)
30
-
31
-	err = fsStore.SetMetadata(id2, "parent", []byte(id1))
32
-	assert.NilError(t, err)
33
-
34
-	// This produces an error log (trying to unmarshal the "invalid" value from above, but doesn't return an error;
35
-	// ERRO[0000] invalid image                                 digest="sha256:f1234d75178d892a133a410355a5a990cf75d2f33eba25d575943d4df632f3a4" err="invalid character 'i' looking for beginning of value: invalid"
36
-	imgStore, err := NewImageStore(fsStore, &mockLayerGetReleaser{})
37
-	assert.NilError(t, err)
38
-
39
-	assert.Check(t, is.Len(imgStore.Map(), 2))
40
-
41
-	img1, err := imgStore.Get(ID(id1))
42
-	assert.NilError(t, err)
43
-	assert.Check(t, is.Equal(ID(id1), img1.computedID))
44
-	assert.Check(t, is.Equal(string(id1), img1.computedID.String()))
45
-
46
-	img2, err := imgStore.Get(ID(id2))
47
-	assert.NilError(t, err)
48
-	assert.Check(t, is.Equal("abc", img1.Comment))
49
-	assert.Check(t, is.Equal("def", img2.Comment))
50
-
51
-	_, err = imgStore.GetParent(ID(id1))
52
-	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
53
-	assert.ErrorContains(t, err, "failed to read metadata")
54
-
55
-	p, err := imgStore.GetParent(ID(id2))
56
-	assert.NilError(t, err)
57
-	assert.Check(t, is.Equal(ID(id1), p))
58
-
59
-	children := imgStore.Children(ID(id1))
60
-	assert.Check(t, is.Len(children, 1))
61
-	assert.Check(t, is.Equal(ID(id2), children[0]))
62
-	assert.Check(t, is.Len(imgStore.Heads(), 1))
63
-
64
-	sid1, err := imgStore.Search(string(id1)[:10])
65
-	assert.NilError(t, err)
66
-	assert.Check(t, is.Equal(ID(id1), sid1))
67
-
68
-	sid1, err = imgStore.Search(id1.Encoded()[:6])
69
-	assert.NilError(t, err)
70
-	assert.Check(t, is.Equal(ID(id1), sid1))
71
-
72
-	invalidPattern := id1.Encoded()[1:6]
73
-	_, err = imgStore.Search(invalidPattern)
74
-	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
75
-	assert.Check(t, is.ErrorContains(err, invalidPattern))
76
-}
77
-
78
-func TestAddDelete(t *testing.T) {
79
-	imgStore := defaultImageStore(t)
80
-
81
-	id1, err := imgStore.Create([]byte(`{"comment": "abc", "rootfs": {"type": "layers", "diff_ids": ["2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"]}}`))
82
-	assert.NilError(t, err)
83
-	assert.Check(t, is.Equal(ID("sha256:8d25a9c45df515f9d0fe8e4a6b1c64dd3b965a84790ddbcc7954bb9bc89eb993"), id1))
84
-
85
-	img, err := imgStore.Get(id1)
86
-	assert.NilError(t, err)
87
-	assert.Check(t, is.Equal("abc", img.Comment))
88
-
89
-	id2, err := imgStore.Create([]byte(`{"comment": "def", "rootfs": {"type": "layers", "diff_ids": ["2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"]}}`))
90
-	assert.NilError(t, err)
91
-
92
-	err = imgStore.SetParent(id2, id1)
93
-	assert.NilError(t, err)
94
-
95
-	pid1, err := imgStore.GetParent(id2)
96
-	assert.NilError(t, err)
97
-	assert.Check(t, is.Equal(pid1, id1))
98
-
99
-	_, err = imgStore.Delete(id1)
100
-	assert.NilError(t, err)
101
-
102
-	_, err = imgStore.Get(id1)
103
-	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
104
-	assert.ErrorContains(t, err, "failed to get digest")
105
-
106
-	_, err = imgStore.Get(id2)
107
-	assert.NilError(t, err)
108
-
109
-	_, err = imgStore.GetParent(id2)
110
-	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
111
-	assert.ErrorContains(t, err, "failed to read metadata")
112
-}
113
-
114
-func TestSearchAfterDelete(t *testing.T) {
115
-	imgStore := defaultImageStore(t)
116
-
117
-	id, err := imgStore.Create([]byte(`{"comment": "abc", "rootfs": {"type": "layers"}}`))
118
-	assert.NilError(t, err)
119
-
120
-	id1, err := imgStore.Search(string(id)[:15])
121
-	assert.NilError(t, err)
122
-	assert.Check(t, is.Equal(id1, id))
123
-
124
-	_, err = imgStore.Delete(id)
125
-	assert.NilError(t, err)
126
-
127
-	_, err = imgStore.Search(string(id)[:15])
128
-	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
129
-	assert.ErrorContains(t, err, "No such image")
130
-}
131
-
132
-func TestDeleteNotExisting(t *testing.T) {
133
-	imgStore := defaultImageStore(t)
134
-	_, err := imgStore.Delete(ID("i_dont_exists"))
135
-	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
136
-}
137
-
138
-func TestParentReset(t *testing.T) {
139
-	imgStore := defaultImageStore(t)
140
-
141
-	id, err := imgStore.Create([]byte(`{"comment": "abc1", "rootfs": {"type": "layers"}}`))
142
-	assert.NilError(t, err)
143
-
144
-	id2, err := imgStore.Create([]byte(`{"comment": "abc2", "rootfs": {"type": "layers"}}`))
145
-	assert.NilError(t, err)
146
-
147
-	id3, err := imgStore.Create([]byte(`{"comment": "abc3", "rootfs": {"type": "layers"}}`))
148
-	assert.NilError(t, err)
149
-
150
-	assert.Check(t, imgStore.SetParent(id, id2))
151
-	assert.Check(t, is.Len(imgStore.Children(id2), 1))
152
-
153
-	assert.Check(t, imgStore.SetParent(id, id3))
154
-	assert.Check(t, is.Len(imgStore.Children(id2), 0))
155
-	assert.Check(t, is.Len(imgStore.Children(id3), 1))
156
-}
157
-
158
-func defaultImageStore(t *testing.T) Store {
159
-	t.Helper()
160
-	fsBackend, err := NewFSStoreBackend(t.TempDir())
161
-	assert.Check(t, err)
162
-
163
-	imgStore, err := NewImageStore(fsBackend, &mockLayerGetReleaser{})
164
-	assert.NilError(t, err)
165
-
166
-	return imgStore
167
-}
168
-
169
-func TestGetAndSetLastUpdated(t *testing.T) {
170
-	imgStore := defaultImageStore(t)
171
-
172
-	id, err := imgStore.Create([]byte(`{"comment": "abc1", "rootfs": {"type": "layers"}}`))
173
-	assert.NilError(t, err)
174
-
175
-	updated, err := imgStore.GetLastUpdated(id)
176
-	assert.NilError(t, err)
177
-	assert.Check(t, is.Equal(updated.IsZero(), true))
178
-
179
-	assert.Check(t, imgStore.SetLastUpdated(id))
180
-
181
-	updated, err = imgStore.GetLastUpdated(id)
182
-	assert.NilError(t, err)
183
-	assert.Check(t, is.Equal(updated.IsZero(), false))
184
-}
185
-
186
-func TestStoreLen(t *testing.T) {
187
-	imgStore := defaultImageStore(t)
188
-
189
-	expected := 10
190
-	for i := range expected {
191
-		_, err := imgStore.Create([]byte(fmt.Sprintf(`{"comment": "abc%d", "rootfs": {"type": "layers"}}`, i)))
192
-		assert.NilError(t, err)
193
-	}
194
-	numImages := imgStore.Len()
195
-	assert.Equal(t, expected, numImages)
196
-	assert.Equal(t, len(imgStore.Map()), numImages)
197
-}
198
-
199
-type mockLayerGetReleaser struct{}
200
-
201
-func (ls *mockLayerGetReleaser) Get(layer.ChainID) (layer.Layer, error) {
202
-	return nil, nil
203
-}
204
-
205
-func (ls *mockLayerGetReleaser) Release(layer.Layer) ([]layer.Metadata, error) {
206
-	return nil, nil
207
-}
208 1
deleted file mode 100644
... ...
@@ -1,298 +0,0 @@
1
-package tarexport
2
-
3
-import (
4
-	"context"
5
-	"encoding/json"
6
-	"errors"
7
-	"fmt"
8
-	"io"
9
-	"os"
10
-	"path/filepath"
11
-	"reflect"
12
-	"runtime"
13
-
14
-	"github.com/containerd/containerd/v2/pkg/tracing"
15
-	"github.com/containerd/log"
16
-	"github.com/distribution/reference"
17
-	"github.com/docker/distribution"
18
-	"github.com/docker/docker/daemon/internal/layer"
19
-	"github.com/docker/docker/image"
20
-	"github.com/docker/docker/internal/ioutils"
21
-	"github.com/docker/docker/pkg/progress"
22
-	"github.com/docker/docker/pkg/streamformatter"
23
-	"github.com/docker/docker/pkg/stringid"
24
-	"github.com/moby/go-archive/chrootarchive"
25
-	"github.com/moby/go-archive/compression"
26
-	"github.com/moby/moby/api/types/events"
27
-	"github.com/moby/sys/sequential"
28
-	"github.com/moby/sys/symlink"
29
-	"github.com/opencontainers/go-digest"
30
-)
31
-
32
-func (l *tarexporter) Load(ctx context.Context, inTar io.ReadCloser, outStream io.Writer, quiet bool) (outErr error) {
33
-	ctx, span := tracing.StartSpan(ctx, "tarexport.Load")
34
-	defer span.End()
35
-	defer func() {
36
-		span.SetStatus(outErr)
37
-	}()
38
-
39
-	var progressOutput progress.Output
40
-	if !quiet {
41
-		progressOutput = streamformatter.NewJSONProgressOutput(outStream, false)
42
-	}
43
-	outStream = streamformatter.NewStdoutWriter(outStream)
44
-
45
-	tmpDir, err := os.MkdirTemp("", "docker-import-")
46
-	if err != nil {
47
-		return err
48
-	}
49
-	defer os.RemoveAll(tmpDir)
50
-
51
-	if err := untar(ctx, inTar, tmpDir); err != nil {
52
-		return err
53
-	}
54
-
55
-	// read manifest, if no file then load in legacy mode
56
-	manifestPath, err := safePath(tmpDir, manifestFileName)
57
-	if err != nil {
58
-		return err
59
-	}
60
-	manifestFile, err := os.Open(manifestPath)
61
-	if err != nil {
62
-		if os.IsNotExist(err) {
63
-			return fmt.Errorf("invalid archive: does not contain a %s", manifestFileName)
64
-		}
65
-		return fmt.Errorf("invalid archive: failed to load %s: %w", manifestFileName, err)
66
-	}
67
-	defer manifestFile.Close()
68
-
69
-	var manifest []manifestItem
70
-	if err := json.NewDecoder(manifestFile).Decode(&manifest); err != nil {
71
-		return fmt.Errorf("invalid archive: failed to decode %s: %w", manifestFileName, err)
72
-	}
73
-
74
-	// a nil manifest usually indicates a bug, so don't just silently fail.
75
-	// if someone really needs to pass an empty manifest, they can pass [].
76
-	if manifest == nil {
77
-		return errors.New("invalid manifest, manifest cannot be null (but can be [])")
78
-	}
79
-
80
-	var parentLinks []parentLink
81
-	var imageIDsStr string
82
-	var imageRefCount int
83
-
84
-	for _, m := range manifest {
85
-		select {
86
-		case <-ctx.Done():
87
-			return ctx.Err()
88
-		default:
89
-		}
90
-		configPath, err := safePath(tmpDir, m.Config)
91
-		if err != nil {
92
-			return err
93
-		}
94
-		config, err := os.ReadFile(configPath)
95
-		if err != nil {
96
-			return err
97
-		}
98
-		img, err := image.NewFromJSON(config)
99
-		if err != nil {
100
-			return err
101
-		}
102
-		if err := image.CheckOS(img.OperatingSystem()); err != nil {
103
-			return fmt.Errorf("cannot load %s image on %s", img.OperatingSystem(), runtime.GOOS)
104
-		}
105
-		if l.platformMatcher != nil && !l.platformMatcher.Match(img.Platform()) {
106
-			continue
107
-		}
108
-		rootFS := *img.RootFS
109
-		rootFS.DiffIDs = nil
110
-
111
-		if expected, actual := len(m.Layers), len(img.RootFS.DiffIDs); expected != actual {
112
-			return fmt.Errorf("invalid manifest, layers length mismatch: expected %d, got %d", expected, actual)
113
-		}
114
-
115
-		for i, diffID := range img.RootFS.DiffIDs {
116
-			select {
117
-			case <-ctx.Done():
118
-				return ctx.Err()
119
-			default:
120
-			}
121
-			layerPath, err := safePath(tmpDir, m.Layers[i])
122
-			if err != nil {
123
-				return err
124
-			}
125
-			r := rootFS
126
-			r.Append(diffID)
127
-			newLayer, err := l.lss.Get(r.ChainID())
128
-			if err != nil {
129
-				newLayer, err = l.loadLayer(ctx, layerPath, rootFS, diffID.String(), m.LayerSources[diffID], progressOutput)
130
-				if err != nil {
131
-					return err
132
-				}
133
-			}
134
-			defer layer.ReleaseAndLog(l.lss, newLayer)
135
-			if expected, actual := diffID, newLayer.DiffID(); expected != actual {
136
-				return fmt.Errorf("invalid diffID for layer %d: expected %q, got %q", i, expected, actual)
137
-			}
138
-			rootFS.Append(diffID)
139
-		}
140
-
141
-		imgID, err := l.is.Create(config)
142
-		if err != nil {
143
-			return err
144
-		}
145
-		imageIDsStr += fmt.Sprintf("Loaded image ID: %s\n", imgID)
146
-
147
-		imageRefCount = 0
148
-		for _, repoTag := range m.RepoTags {
149
-			named, err := reference.ParseNormalizedNamed(repoTag)
150
-			if err != nil {
151
-				return err
152
-			}
153
-			ref, ok := named.(reference.NamedTagged)
154
-			if !ok {
155
-				return fmt.Errorf("invalid tag %q", repoTag)
156
-			}
157
-			l.setLoadedTag(ref, imgID.Digest(), outStream)
158
-			fmt.Fprintf(outStream, "Loaded image: %s\n", reference.FamiliarString(ref))
159
-			imageRefCount++
160
-		}
161
-
162
-		parentLinks = append(parentLinks, parentLink{imgID, m.Parent})
163
-		l.loggerImgEvent.LogImageEvent(ctx, imgID.String(), imgID.String(), events.ActionLoad)
164
-	}
165
-
166
-	for _, p := range validatedParentLinks(parentLinks) {
167
-		if p.parentID != "" {
168
-			if err := l.setParentID(p.id, p.parentID); err != nil {
169
-				return err
170
-			}
171
-		}
172
-	}
173
-
174
-	if imageRefCount == 0 {
175
-		outStream.Write([]byte(imageIDsStr))
176
-	}
177
-
178
-	return nil
179
-}
180
-
181
-func untar(ctx context.Context, inTar io.ReadCloser, tmpDir string) error {
182
-	_, trace := tracing.StartSpan(ctx, "chrootarchive.Untar")
183
-	defer trace.End()
184
-
185
-	err := chrootarchive.Untar(ioutils.NewCtxReader(ctx, inTar), tmpDir, nil)
186
-	trace.SetStatus(err)
187
-	return err
188
-}
189
-
190
-func (l *tarexporter) setParentID(id, parentID image.ID) error {
191
-	img, err := l.is.Get(id)
192
-	if err != nil {
193
-		return err
194
-	}
195
-	parent, err := l.is.Get(parentID)
196
-	if err != nil {
197
-		return err
198
-	}
199
-	if !checkValidParent(img, parent) {
200
-		return fmt.Errorf("image %v is not a valid parent for %v", parent.ID(), img.ID())
201
-	}
202
-	return l.is.SetParent(id, parentID)
203
-}
204
-
205
-func (l *tarexporter) loadLayer(ctx context.Context, filename string, rootFS image.RootFS, id string, foreignSrc distribution.Descriptor, progressOutput progress.Output) (_ layer.Layer, outErr error) {
206
-	ctx, span := tracing.StartSpan(ctx, "loadLayer")
207
-	span.SetAttributes(tracing.Attribute("image.id", id))
208
-	defer span.End()
209
-	defer func() {
210
-		span.SetStatus(outErr)
211
-	}()
212
-
213
-	// We use sequential file access to avoid depleting the standby list on Windows.
214
-	// On Linux, this equates to a regular os.Open.
215
-	rawTar, err := sequential.Open(filename)
216
-	if err != nil {
217
-		log.G(context.TODO()).Debugf("Error reading embedded tar: %v", err)
218
-		return nil, err
219
-	}
220
-	defer rawTar.Close()
221
-
222
-	var r io.Reader
223
-	if progressOutput != nil {
224
-		fileInfo, err := rawTar.Stat()
225
-		if err != nil {
226
-			log.G(context.TODO()).Debugf("Error statting file: %v", err)
227
-			return nil, err
228
-		}
229
-
230
-		r = progress.NewProgressReader(rawTar, progressOutput, fileInfo.Size(), stringid.TruncateID(id), "Loading layer")
231
-	} else {
232
-		r = rawTar
233
-	}
234
-
235
-	inflatedLayerData, err := compression.DecompressStream(ioutils.NewCtxReader(ctx, r))
236
-	if err != nil {
237
-		return nil, err
238
-	}
239
-	defer inflatedLayerData.Close()
240
-
241
-	if ds, ok := l.lss.(layer.DescribableStore); ok {
242
-		return ds.RegisterWithDescriptor(inflatedLayerData, rootFS.ChainID(), foreignSrc)
243
-	}
244
-	return l.lss.Register(inflatedLayerData, rootFS.ChainID())
245
-}
246
-
247
-func (l *tarexporter) setLoadedTag(ref reference.Named, imgID digest.Digest, outStream io.Writer) error {
248
-	if prevID, err := l.rs.Get(ref); err == nil && prevID != imgID {
249
-		fmt.Fprintf(outStream, "The image %s already exists, renaming the old one with ID %s to empty string\n", reference.FamiliarString(ref), string(prevID)) // todo: this message is wrong in case of multiple tags
250
-	}
251
-
252
-	return l.rs.AddTag(ref, imgID, true)
253
-}
254
-
255
-func safePath(base, path string) (string, error) {
256
-	return symlink.FollowSymlinkInScope(filepath.Join(base, path), base)
257
-}
258
-
259
-type parentLink struct {
260
-	id, parentID image.ID
261
-}
262
-
263
-func validatedParentLinks(pl []parentLink) (ret []parentLink) {
264
-mainloop:
265
-	for i, p := range pl {
266
-		ret = append(ret, p)
267
-		for _, p2 := range pl {
268
-			if p2.id == p.parentID && p2.id != p.id {
269
-				continue mainloop
270
-			}
271
-		}
272
-		ret[i].parentID = ""
273
-	}
274
-	return ret
275
-}
276
-
277
-func checkValidParent(img, parent *image.Image) bool {
278
-	if len(img.History) == 0 && len(parent.History) == 0 {
279
-		return true // having history is not mandatory
280
-	}
281
-	if len(img.History)-len(parent.History) != 1 {
282
-		return false
283
-	}
284
-	for i, hP := range parent.History {
285
-		hC := img.History[i]
286
-		if (hP.Created == nil) != (hC.Created == nil) {
287
-			return false
288
-		}
289
-		if hP.Created != nil && !hP.Created.Equal(*hC.Created) {
290
-			return false
291
-		}
292
-		hC.Created = hP.Created
293
-		if !reflect.DeepEqual(hP, hC) {
294
-			return false
295
-		}
296
-	}
297
-	return true
298
-}
299 1
deleted file mode 100644
... ...
@@ -1,74 +0,0 @@
1
-// Copyright 2009 The Go Authors. All rights reserved.
2
-// Use of this source code is governed by a BSD-style
3
-// license that can be found in the LICENSE file.
4
-
5
-// Code in this file is a modified version of go stdlib;
6
-// https://cs.opensource.google/go/go/+/refs/tags/go1.23.4:src/os/path.go;l=19-66
7
-
8
-package tarexport
9
-
10
-import (
11
-	"fmt"
12
-	"os"
13
-	"path/filepath"
14
-	"syscall"
15
-	"time"
16
-
17
-	"github.com/docker/docker/pkg/system"
18
-)
19
-
20
-// mkdirAllWithChtimes is nearly an identical copy to the [os.MkdirAll] but
21
-// tracks created directories and applies the provided mtime and atime using
22
-// [system.Chtimes].
23
-func mkdirAllWithChtimes(path string, perm os.FileMode, atime, mtime time.Time) error {
24
-	// Fast path: if we can tell whether path is a directory or file, stop with success or error.
25
-	dir, err := os.Stat(path)
26
-	if err == nil {
27
-		if dir.IsDir() {
28
-			return nil
29
-		}
30
-		return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR}
31
-	}
32
-
33
-	// Slow path: make sure parent exists and then call Mkdir for path.
34
-
35
-	// Extract the parent folder from path by first removing any trailing
36
-	// path separator and then scanning backward until finding a path
37
-	// separator or reaching the beginning of the string.
38
-	i := len(path) - 1
39
-	for i >= 0 && os.IsPathSeparator(path[i]) {
40
-		i--
41
-	}
42
-	for i >= 0 && !os.IsPathSeparator(path[i]) {
43
-		i--
44
-	}
45
-	if i < 0 {
46
-		i = 0
47
-	}
48
-
49
-	// If there is a parent directory, and it is not the volume name,
50
-	// recurse to ensure parent directory exists.
51
-	if parent := path[:i]; len(parent) > len(filepath.VolumeName(path)) {
52
-		err = mkdirAllWithChtimes(parent, perm, atime, mtime)
53
-		if err != nil {
54
-			return err
55
-		}
56
-	}
57
-
58
-	// Parent now exists; invoke Mkdir and use its result.
59
-	err = os.Mkdir(path, perm)
60
-	if err != nil {
61
-		// Handle arguments like "foo/." by
62
-		// double-checking that directory doesn't exist.
63
-		dir, err1 := os.Lstat(path)
64
-		if err1 == nil && dir.IsDir() {
65
-			return nil
66
-		}
67
-		return err
68
-	}
69
-
70
-	if err := system.Chtimes(path, atime, mtime); err != nil {
71
-		return fmt.Errorf("applying atime=%v and mtime=%v: %w", atime, mtime, err)
72
-	}
73
-	return nil
74
-}
75 1
deleted file mode 100644
... ...
@@ -1,638 +0,0 @@
1
-package tarexport
2
-
3
-import (
4
-	"context"
5
-	"encoding/json"
6
-	"fmt"
7
-	"io"
8
-	"os"
9
-	"path"
10
-	"path/filepath"
11
-	"time"
12
-
13
-	c8dimages "github.com/containerd/containerd/v2/core/images"
14
-	"github.com/containerd/containerd/v2/pkg/tracing"
15
-	"github.com/containerd/log"
16
-	"github.com/containerd/platforms"
17
-	"github.com/distribution/reference"
18
-	"github.com/docker/distribution"
19
-	"github.com/docker/docker/daemon/internal/layer"
20
-	"github.com/docker/docker/image"
21
-	v1 "github.com/docker/docker/image/v1"
22
-	"github.com/docker/docker/internal/ioutils"
23
-	"github.com/docker/docker/pkg/system"
24
-	"github.com/moby/go-archive"
25
-	"github.com/moby/moby/api/types/events"
26
-	"github.com/moby/sys/sequential"
27
-	"github.com/opencontainers/go-digest"
28
-	"github.com/opencontainers/image-spec/specs-go"
29
-	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
30
-	"github.com/pkg/errors"
31
-)
32
-
33
-type imageDescriptor struct {
34
-	refs     []reference.NamedTagged
35
-	layers   []layer.DiffID
36
-	image    *image.Image
37
-	layerRef layer.Layer
38
-}
39
-
40
-type saveSession struct {
41
-	*tarexporter
42
-	outDir       string
43
-	images       map[image.ID]*imageDescriptor
44
-	savedLayers  map[layer.DiffID]distribution.Descriptor
45
-	savedConfigs map[string]struct{}
46
-}
47
-
48
-func (l *tarexporter) Save(ctx context.Context, names []string, outStream io.Writer) error {
49
-	imgDescriptors, err := l.parseNames(ctx, names)
50
-	if err != nil {
51
-		return err
52
-	}
53
-
54
-	// Release all the image top layer references
55
-	defer l.releaseLayerReferences(imgDescriptors)
56
-	return (&saveSession{tarexporter: l, images: imgDescriptors}).save(ctx, outStream)
57
-}
58
-
59
-// parseNames will parse the image names to a map which contains image.ID to *imageDescriptor.
60
-// Each imageDescriptor holds an image top layer reference named 'layerRef'. It is taken here, should be released later.
61
-func (l *tarexporter) parseNames(ctx context.Context, names []string) (desc map[image.ID]*imageDescriptor, rErr error) {
62
-	imgDescr := make(map[image.ID]*imageDescriptor)
63
-	defer func() {
64
-		if rErr != nil {
65
-			l.releaseLayerReferences(imgDescr)
66
-		}
67
-	}()
68
-
69
-	addAssoc := func(id image.ID, ref reference.Named) error {
70
-		if _, ok := imgDescr[id]; !ok {
71
-			descr := &imageDescriptor{}
72
-			if err := l.takeLayerReference(id, descr); err != nil {
73
-				return err
74
-			}
75
-			imgDescr[id] = descr
76
-		}
77
-
78
-		if ref != nil {
79
-			if _, ok := ref.(reference.Canonical); ok {
80
-				return nil
81
-			}
82
-			tagged, ok := reference.TagNameOnly(ref).(reference.NamedTagged)
83
-			if !ok {
84
-				return nil
85
-			}
86
-
87
-			for _, t := range imgDescr[id].refs {
88
-				if tagged.String() == t.String() {
89
-					return nil
90
-				}
91
-			}
92
-			imgDescr[id].refs = append(imgDescr[id].refs, tagged)
93
-		}
94
-		return nil
95
-	}
96
-
97
-	for _, name := range names {
98
-		select {
99
-		case <-ctx.Done():
100
-			return nil, ctx.Err()
101
-		default:
102
-		}
103
-
104
-		ref, err := reference.ParseAnyReference(name)
105
-		if err != nil {
106
-			return nil, err
107
-		}
108
-		namedRef, ok := ref.(reference.Named)
109
-		if !ok {
110
-			// Check if digest ID reference
111
-			if digested, ok := ref.(reference.Digested); ok {
112
-				if err := addAssoc(image.ID(digested.Digest()), nil); err != nil {
113
-					return nil, err
114
-				}
115
-				continue
116
-			}
117
-			return nil, errors.Errorf("invalid reference: %v", name)
118
-		}
119
-
120
-		if reference.FamiliarName(namedRef) == string(digest.Canonical) {
121
-			imgID, err := l.is.Search(name)
122
-			if err != nil {
123
-				return nil, err
124
-			}
125
-			if err := addAssoc(imgID, nil); err != nil {
126
-				return nil, err
127
-			}
128
-			continue
129
-		}
130
-		if reference.IsNameOnly(namedRef) {
131
-			assocs := l.rs.ReferencesByName(namedRef)
132
-			for _, assoc := range assocs {
133
-				if err := addAssoc(image.ID(assoc.ID), assoc.Ref); err != nil {
134
-					return nil, err
135
-				}
136
-			}
137
-			if len(assocs) == 0 {
138
-				imgID, err := l.is.Search(name)
139
-				if err != nil {
140
-					return nil, err
141
-				}
142
-				if err := addAssoc(imgID, nil); err != nil {
143
-					return nil, err
144
-				}
145
-			}
146
-			continue
147
-		}
148
-		id, err := l.rs.Get(namedRef)
149
-		if err != nil {
150
-			return nil, err
151
-		}
152
-		if err := addAssoc(image.ID(id), namedRef); err != nil {
153
-			return nil, err
154
-		}
155
-	}
156
-	return imgDescr, nil
157
-}
158
-
159
-// takeLayerReference will take/Get the image top layer reference
160
-func (l *tarexporter) takeLayerReference(id image.ID, imgDescr *imageDescriptor) error {
161
-	img, err := l.is.Get(id)
162
-	if err != nil {
163
-		return err
164
-	}
165
-	if err := image.CheckOS(img.OperatingSystem()); err != nil {
166
-		return fmt.Errorf("os %q is not supported", img.OperatingSystem())
167
-	}
168
-	if l.platform != nil {
169
-		if !l.platformMatcher.Match(img.Platform()) {
170
-			return errors.New("no suitable export target found for platform " + platforms.FormatAll(*l.platform))
171
-		}
172
-	}
173
-	imgDescr.image = img
174
-	topLayerID := img.RootFS.ChainID()
175
-	if topLayerID == "" {
176
-		return nil
177
-	}
178
-	topLayer, err := l.lss.Get(topLayerID)
179
-	if err != nil {
180
-		return err
181
-	}
182
-	imgDescr.layerRef = topLayer
183
-	return nil
184
-}
185
-
186
-// releaseLayerReferences will release all the image top layer references
187
-func (l *tarexporter) releaseLayerReferences(imgDescr map[image.ID]*imageDescriptor) error {
188
-	for _, descr := range imgDescr {
189
-		if descr.layerRef != nil {
190
-			l.lss.Release(descr.layerRef)
191
-		}
192
-	}
193
-	return nil
194
-}
195
-
196
-func (s *saveSession) save(ctx context.Context, outStream io.Writer) error {
197
-	s.savedConfigs = make(map[string]struct{})
198
-	s.savedLayers = make(map[layer.DiffID]distribution.Descriptor)
199
-
200
-	// get image json
201
-	tempDir, err := os.MkdirTemp("", "docker-export-")
202
-	if err != nil {
203
-		return err
204
-	}
205
-	defer os.RemoveAll(tempDir)
206
-
207
-	s.outDir = tempDir
208
-	reposLegacy := make(map[string]map[string]string)
209
-
210
-	var manifest []manifestItem
211
-	var parentLinks []parentLink
212
-
213
-	var manifestDescriptors []ocispec.Descriptor
214
-
215
-	for id, imageDescr := range s.images {
216
-		select {
217
-		case <-ctx.Done():
218
-			return ctx.Err()
219
-		default:
220
-		}
221
-
222
-		foreignSrcs, err := s.saveImage(ctx, id)
223
-		if err != nil {
224
-			return err
225
-		}
226
-
227
-		var (
228
-			repoTags []string
229
-			layers   []string
230
-			foreign  = make([]ocispec.Descriptor, 0, len(foreignSrcs))
231
-		)
232
-
233
-		// Layers in manifest must follow the actual layer order from config.
234
-		for _, l := range imageDescr.layers {
235
-			desc := foreignSrcs[l]
236
-			foreign = append(foreign, ocispec.Descriptor{
237
-				MediaType:   desc.MediaType,
238
-				Digest:      desc.Digest,
239
-				Size:        desc.Size,
240
-				URLs:        desc.URLs,
241
-				Annotations: desc.Annotations,
242
-				Platform:    desc.Platform,
243
-			})
244
-		}
245
-
246
-		data, err := json.Marshal(ocispec.Manifest{
247
-			Versioned: specs.Versioned{
248
-				SchemaVersion: 2,
249
-			},
250
-			MediaType: ocispec.MediaTypeImageManifest,
251
-			Config: ocispec.Descriptor{
252
-				MediaType: ocispec.MediaTypeImageConfig,
253
-				Digest:    digest.Digest(imageDescr.image.ID()),
254
-				Size:      int64(len(imageDescr.image.RawJSON())),
255
-			},
256
-			Layers: foreign,
257
-		})
258
-		if err != nil {
259
-			return errors.Wrap(err, "error marshaling manifest")
260
-		}
261
-		dgst := digest.FromBytes(data)
262
-
263
-		mFile := filepath.Join(s.outDir, ocispec.ImageBlobsDir, dgst.Algorithm().String(), dgst.Encoded())
264
-		if err := mkdirAllWithChtimes(filepath.Dir(mFile), 0o755, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
265
-			return errors.Wrap(err, "error creating blob directory")
266
-		}
267
-		if err := system.Chtimes(filepath.Dir(mFile), time.Unix(0, 0), time.Unix(0, 0)); err != nil {
268
-			return errors.Wrap(err, "error setting blob directory timestamps")
269
-		}
270
-		if err := os.WriteFile(mFile, data, 0o644); err != nil {
271
-			return errors.Wrap(err, "error writing oci manifest file")
272
-		}
273
-		if err := system.Chtimes(mFile, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
274
-			return errors.Wrap(err, "error setting blob directory timestamps")
275
-		}
276
-
277
-		untaggedMfstDesc := ocispec.Descriptor{
278
-			MediaType: ocispec.MediaTypeImageManifest,
279
-			Digest:    dgst,
280
-			Size:      int64(len(data)),
281
-		}
282
-		for _, ref := range imageDescr.refs {
283
-			familiarName := reference.FamiliarName(ref)
284
-			if _, ok := reposLegacy[familiarName]; !ok {
285
-				reposLegacy[familiarName] = make(map[string]string)
286
-			}
287
-			reposLegacy[familiarName][ref.Tag()] = imageDescr.layers[len(imageDescr.layers)-1].Encoded()
288
-			repoTags = append(repoTags, reference.FamiliarString(ref))
289
-
290
-			taggedManifest := untaggedMfstDesc
291
-			taggedManifest.Annotations = map[string]string{
292
-				c8dimages.AnnotationImageName: ref.String(),
293
-				ocispec.AnnotationRefName:     ref.Tag(),
294
-			}
295
-			manifestDescriptors = append(manifestDescriptors, taggedManifest)
296
-		}
297
-
298
-		// If no ref was assigned, make sure still add the image is still included in index.json.
299
-		if len(manifestDescriptors) == 0 {
300
-			manifestDescriptors = append(manifestDescriptors, untaggedMfstDesc)
301
-		}
302
-
303
-		for _, lDgst := range imageDescr.layers {
304
-			// IMPORTANT: We use path, not filepath here to ensure the layers
305
-			// in the manifest use Unix-style forward-slashes.
306
-			layers = append(layers, path.Join(ocispec.ImageBlobsDir, lDgst.Algorithm().String(), lDgst.Encoded()))
307
-		}
308
-
309
-		manifest = append(manifest, manifestItem{
310
-			Config:       path.Join(ocispec.ImageBlobsDir, id.Digest().Algorithm().String(), id.Digest().Encoded()),
311
-			RepoTags:     repoTags,
312
-			Layers:       layers,
313
-			LayerSources: foreignSrcs,
314
-		})
315
-
316
-		parentID, _ := s.is.GetParent(id)
317
-		parentLinks = append(parentLinks, parentLink{id, parentID})
318
-		s.tarexporter.loggerImgEvent.LogImageEvent(ctx, id.String(), id.String(), events.ActionSave)
319
-	}
320
-
321
-	for i, p := range validatedParentLinks(parentLinks) {
322
-		if p.parentID != "" {
323
-			manifest[i].Parent = p.parentID
324
-		}
325
-	}
326
-
327
-	if len(reposLegacy) > 0 {
328
-		reposFile := filepath.Join(tempDir, legacyRepositoriesFileName)
329
-		rf, err := os.OpenFile(reposFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
330
-		if err != nil {
331
-			return err
332
-		}
333
-
334
-		if err := json.NewEncoder(rf).Encode(reposLegacy); err != nil {
335
-			rf.Close()
336
-			return err
337
-		}
338
-
339
-		rf.Close()
340
-
341
-		if err := system.Chtimes(reposFile, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
342
-			return err
343
-		}
344
-	}
345
-
346
-	manifestPath := filepath.Join(tempDir, manifestFileName)
347
-	f, err := os.OpenFile(manifestPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
348
-	if err != nil {
349
-		return err
350
-	}
351
-
352
-	if err := json.NewEncoder(f).Encode(manifest); err != nil {
353
-		f.Close()
354
-		return err
355
-	}
356
-
357
-	f.Close()
358
-
359
-	if err := system.Chtimes(manifestPath, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
360
-		return err
361
-	}
362
-
363
-	const ociLayoutContent = `{"imageLayoutVersion": "` + ocispec.ImageLayoutVersion + `"}`
364
-	layoutPath := filepath.Join(tempDir, ocispec.ImageLayoutFile)
365
-	if err := os.WriteFile(layoutPath, []byte(ociLayoutContent), 0o644); err != nil {
366
-		return errors.Wrap(err, "error writing oci layout file")
367
-	}
368
-	if err := system.Chtimes(layoutPath, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
369
-		return errors.Wrap(err, "error setting oci layout file timestamps")
370
-	}
371
-
372
-	data, err := json.Marshal(ocispec.Index{
373
-		Versioned: specs.Versioned{
374
-			SchemaVersion: 2,
375
-		},
376
-		MediaType: ocispec.MediaTypeImageIndex,
377
-		Manifests: manifestDescriptors,
378
-	})
379
-	if err != nil {
380
-		return errors.Wrap(err, "error marshaling oci index")
381
-	}
382
-
383
-	idxFile := filepath.Join(s.outDir, ocispec.ImageIndexFile)
384
-	if err := os.WriteFile(idxFile, data, 0o644); err != nil {
385
-		return errors.Wrap(err, "error writing oci index file")
386
-	}
387
-	if err := system.Chtimes(idxFile, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
388
-		return errors.Wrap(err, "error setting oci index file timestamps")
389
-	}
390
-
391
-	return s.writeTar(ctx, tempDir, outStream)
392
-}
393
-
394
-func (s *saveSession) writeTar(ctx context.Context, tempDir string, outStream io.Writer) error {
395
-	ctx, span := tracing.StartSpan(ctx, "writeTar")
396
-	defer span.End()
397
-
398
-	fs, err := archive.Tar(tempDir, archive.Uncompressed)
399
-	if err != nil {
400
-		span.SetStatus(err)
401
-		return err
402
-	}
403
-	defer fs.Close()
404
-
405
-	_, err = ioutils.CopyCtx(ctx, outStream, fs)
406
-
407
-	span.SetStatus(err)
408
-	return err
409
-}
410
-
411
-func (s *saveSession) saveImage(ctx context.Context, id image.ID) (_ map[layer.DiffID]distribution.Descriptor, outErr error) {
412
-	ctx, span := tracing.StartSpan(ctx, "saveImage")
413
-	span.SetAttributes(tracing.Attribute("image.id", id.String()))
414
-	defer span.End()
415
-	defer func() {
416
-		span.SetStatus(outErr)
417
-	}()
418
-
419
-	img := s.images[id].image
420
-	if len(img.RootFS.DiffIDs) == 0 {
421
-		return nil, errors.New("empty export - not implemented")
422
-	}
423
-
424
-	ts := time.Unix(0, 0)
425
-	if img.Created != nil {
426
-		ts = *img.Created
427
-	}
428
-
429
-	var parent digest.Digest
430
-	var layers []layer.DiffID
431
-	var foreignSrcs map[layer.DiffID]distribution.Descriptor
432
-	for i, diffID := range img.RootFS.DiffIDs {
433
-		select {
434
-		case <-ctx.Done():
435
-			return nil, ctx.Err()
436
-		default:
437
-		}
438
-		v1ImgCreated := time.Unix(0, 0)
439
-		v1Img := image.V1Image{
440
-			// This is for backward compatibility used for
441
-			// pre v1.9 docker.
442
-			Created: &v1ImgCreated,
443
-		}
444
-		if i == len(img.RootFS.DiffIDs)-1 {
445
-			v1Img = img.V1Image
446
-		}
447
-		rootFS := *img.RootFS
448
-		rootFS.DiffIDs = rootFS.DiffIDs[:i+1]
449
-		v1ID, err := v1.CreateID(v1Img, rootFS.ChainID(), parent)
450
-		if err != nil {
451
-			return nil, err
452
-		}
453
-
454
-		v1Img.ID = v1ID.Encoded()
455
-		if parent != "" {
456
-			v1Img.Parent = parent.Encoded()
457
-		}
458
-
459
-		v1Img.OS = img.OS
460
-		src, err := s.saveConfigAndLayer(ctx, rootFS.ChainID(), v1Img, &ts)
461
-		if err != nil {
462
-			return nil, err
463
-		}
464
-
465
-		layers = append(layers, diffID)
466
-		parent = v1ID
467
-		if src.Digest != "" {
468
-			if foreignSrcs == nil {
469
-				foreignSrcs = make(map[layer.DiffID]distribution.Descriptor)
470
-			}
471
-			foreignSrcs[img.RootFS.DiffIDs[i]] = src
472
-		}
473
-	}
474
-
475
-	data := img.RawJSON()
476
-	dgst := digest.FromBytes(data)
477
-
478
-	blobDir := filepath.Join(s.outDir, ocispec.ImageBlobsDir, dgst.Algorithm().String())
479
-	if err := mkdirAllWithChtimes(blobDir, 0o755, ts, ts); err != nil {
480
-		return nil, err
481
-	}
482
-	if err := system.Chtimes(blobDir, ts, ts); err != nil {
483
-		return nil, err
484
-	}
485
-	if err := system.Chtimes(filepath.Dir(blobDir), ts, ts); err != nil {
486
-		return nil, err
487
-	}
488
-
489
-	configFile := filepath.Join(blobDir, dgst.Encoded())
490
-	if err := os.WriteFile(configFile, img.RawJSON(), 0o644); err != nil {
491
-		return nil, err
492
-	}
493
-	if err := system.Chtimes(configFile, ts, ts); err != nil {
494
-		return nil, err
495
-	}
496
-
497
-	s.images[id].layers = layers
498
-	return foreignSrcs, nil
499
-}
500
-
501
-func (s *saveSession) saveConfigAndLayer(ctx context.Context, id layer.ChainID, legacyImg image.V1Image, createdTime *time.Time) (_ distribution.Descriptor, outErr error) {
502
-	ctx, span := tracing.StartSpan(ctx, "saveConfigAndLayer")
503
-	span.SetAttributes(
504
-		tracing.Attribute("layer.id", id.String()),
505
-		tracing.Attribute("image.id", legacyImg.ID),
506
-	)
507
-	defer span.End()
508
-	defer func() {
509
-		span.SetStatus(outErr)
510
-	}()
511
-
512
-	ts := time.Unix(0, 0)
513
-	if createdTime != nil {
514
-		ts = *createdTime
515
-	}
516
-
517
-	outDir := filepath.Join(s.outDir, ocispec.ImageBlobsDir)
518
-
519
-	if _, ok := s.savedConfigs[legacyImg.ID]; !ok {
520
-		if err := s.saveConfig(legacyImg, outDir, createdTime); err != nil {
521
-			return distribution.Descriptor{}, err
522
-		}
523
-	}
524
-
525
-	// serialize filesystem
526
-	l, err := s.lss.Get(id)
527
-	if err != nil {
528
-		return distribution.Descriptor{}, err
529
-	}
530
-
531
-	lDiffID := l.DiffID()
532
-	lDgst := lDiffID
533
-	if _, ok := s.savedLayers[lDiffID]; ok {
534
-		return s.savedLayers[lDiffID], nil
535
-	}
536
-	layerPath := filepath.Join(outDir, lDiffID.Algorithm().String(), lDiffID.Encoded())
537
-	defer layer.ReleaseAndLog(s.lss, l)
538
-
539
-	if _, err = os.Stat(layerPath); err == nil {
540
-		// This is should not happen. If the layer path was already created, we should have returned early.
541
-		// Log a warning an proceed to recreate the archive.
542
-		log.G(context.TODO()).WithFields(log.Fields{
543
-			"layerPath": layerPath,
544
-			"id":        id,
545
-			"lDgst":     lDgst,
546
-		}).Warn("LayerPath already exists but the descriptor is not cached")
547
-	} else if !os.IsNotExist(err) {
548
-		return distribution.Descriptor{}, err
549
-	}
550
-
551
-	// We use sequential file access to avoid depleting the standby list on
552
-	// Windows. On Linux, this equates to a regular os.Create.
553
-	if err := mkdirAllWithChtimes(filepath.Dir(layerPath), 0o755, ts, ts); err != nil {
554
-		return distribution.Descriptor{}, errors.Wrap(err, "could not create layer dir parent")
555
-	}
556
-	tarFile, err := sequential.Create(layerPath)
557
-	if err != nil {
558
-		return distribution.Descriptor{}, errors.Wrap(err, "error creating layer file")
559
-	}
560
-	defer tarFile.Close()
561
-
562
-	arch, err := l.TarStream()
563
-	if err != nil {
564
-		return distribution.Descriptor{}, err
565
-	}
566
-	defer arch.Close()
567
-
568
-	digester := digest.Canonical.Digester()
569
-	digestedArch := io.TeeReader(arch, digester.Hash())
570
-
571
-	tarSize, err := ioutils.CopyCtx(ctx, tarFile, digestedArch)
572
-	if err != nil {
573
-		return distribution.Descriptor{}, err
574
-	}
575
-
576
-	tarDigest := digester.Digest()
577
-	if lDgst != tarDigest {
578
-		log.G(context.TODO()).WithFields(log.Fields{
579
-			"layerDigest":  lDgst,
580
-			"actualDigest": tarDigest,
581
-		}).Warn("layer digest doesn't match its tar archive digest")
582
-
583
-		lDgst = digester.Digest()
584
-		layerPath = filepath.Join(outDir, lDgst.Algorithm().String(), lDgst.Encoded())
585
-	}
586
-
587
-	for _, fname := range []string{outDir, layerPath} {
588
-		// todo: maybe save layer created timestamp?
589
-		if err := system.Chtimes(fname, ts, ts); err != nil {
590
-			return distribution.Descriptor{}, errors.Wrap(err, "could not set layer timestamp")
591
-		}
592
-	}
593
-
594
-	var desc distribution.Descriptor
595
-	if fs, ok := l.(distribution.Describable); ok {
596
-		desc = fs.Descriptor()
597
-	}
598
-
599
-	if desc.Digest == "" {
600
-		desc.Digest = tarDigest
601
-		desc.Size = tarSize
602
-	}
603
-	if desc.MediaType == "" {
604
-		desc.MediaType = ocispec.MediaTypeImageLayer
605
-	}
606
-	s.savedLayers[lDiffID] = desc
607
-
608
-	return desc, nil
609
-}
610
-
611
-func (s *saveSession) saveConfig(legacyImg image.V1Image, outDir string, createdTime *time.Time) error {
612
-	imageConfig, err := json.Marshal(legacyImg)
613
-	if err != nil {
614
-		return err
615
-	}
616
-
617
-	ts := time.Unix(0, 0)
618
-	if createdTime != nil {
619
-		ts = *createdTime
620
-	}
621
-
622
-	cfgDgst := digest.FromBytes(imageConfig)
623
-	configPath := filepath.Join(outDir, cfgDgst.Algorithm().String(), cfgDgst.Encoded())
624
-	if err := mkdirAllWithChtimes(filepath.Dir(configPath), 0o755, ts, ts); err != nil {
625
-		return errors.Wrap(err, "could not create layer dir parent")
626
-	}
627
-
628
-	if err := os.WriteFile(configPath, imageConfig, 0o644); err != nil {
629
-		return err
630
-	}
631
-
632
-	if err := system.Chtimes(configPath, ts, ts); err != nil {
633
-		return errors.Wrap(err, "could not set config timestamp")
634
-	}
635
-
636
-	s.savedConfigs[legacyImg.ID] = struct{}{}
637
-	return nil
638
-}
639 1
deleted file mode 100644
... ...
@@ -1,56 +0,0 @@
1
-package tarexport
2
-
3
-import (
4
-	"context"
5
-
6
-	"github.com/containerd/platforms"
7
-	"github.com/docker/distribution"
8
-	"github.com/docker/docker/daemon/internal/layer"
9
-	"github.com/docker/docker/image"
10
-	refstore "github.com/docker/docker/reference"
11
-	"github.com/moby/moby/api/types/events"
12
-	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
13
-)
14
-
15
-const (
16
-	manifestFileName           = "manifest.json"
17
-	legacyRepositoriesFileName = "repositories"
18
-)
19
-
20
-type manifestItem struct {
21
-	Config       string
22
-	RepoTags     []string
23
-	Layers       []string
24
-	Parent       image.ID                                 `json:",omitempty"`
25
-	LayerSources map[layer.DiffID]distribution.Descriptor `json:",omitempty"`
26
-}
27
-
28
-type tarexporter struct {
29
-	is              image.Store
30
-	lss             layer.Store
31
-	rs              refstore.Store
32
-	loggerImgEvent  LogImageEvent
33
-	platform        *platforms.Platform
34
-	platformMatcher platforms.Matcher
35
-}
36
-
37
-// LogImageEvent defines interface for event generation related to image tar(load and save) operations
38
-type LogImageEvent interface {
39
-	// LogImageEvent generates an event related to an image operation
40
-	LogImageEvent(ctx context.Context, imageID, refName string, action events.Action)
41
-}
42
-
43
-// NewTarExporter returns new Exporter for tar packages
44
-func NewTarExporter(is image.Store, lss layer.Store, rs refstore.Store, loggerImgEvent LogImageEvent, platform *ocispec.Platform) image.Exporter {
45
-	l := &tarexporter{
46
-		is:             is,
47
-		lss:            lss,
48
-		rs:             rs,
49
-		loggerImgEvent: loggerImgEvent,
50
-		platform:       platform,
51
-	}
52
-	if platform != nil {
53
-		l.platformMatcher = platforms.OnlyStrict(*platform)
54
-	}
55
-	return l
56
-}
57 1
deleted file mode 100644
... ...
@@ -1,48 +0,0 @@
1
-package v1
2
-
3
-import (
4
-	"context"
5
-	"encoding/json"
6
-
7
-	"github.com/containerd/log"
8
-	"github.com/docker/docker/daemon/internal/layer"
9
-	"github.com/docker/docker/image"
10
-	"github.com/opencontainers/go-digest"
11
-)
12
-
13
-// CreateID creates an ID from v1 image, layerID and parent ID.
14
-// Used for backwards compatibility with old clients.
15
-func CreateID(v1Image image.V1Image, layerID layer.ChainID, parent digest.Digest) (digest.Digest, error) {
16
-	v1Image.ID = ""
17
-	v1JSON, err := json.Marshal(v1Image)
18
-	if err != nil {
19
-		return "", err
20
-	}
21
-
22
-	var config map[string]*json.RawMessage
23
-	if err := json.Unmarshal(v1JSON, &config); err != nil {
24
-		return "", err
25
-	}
26
-
27
-	// FIXME: note that this is slightly incompatible with RootFS logic
28
-	config["layer_id"] = rawJSON(layerID)
29
-	if parent != "" {
30
-		config["parent"] = rawJSON(parent)
31
-	}
32
-
33
-	configJSON, err := json.Marshal(config)
34
-	if err != nil {
35
-		return "", err
36
-	}
37
-	log.G(context.TODO()).Debugf("CreateV1ID %s", configJSON)
38
-
39
-	return digest.FromBytes(configJSON), nil
40
-}
41
-
42
-func rawJSON(value interface{}) *json.RawMessage {
43
-	jsonval, err := json.Marshal(value)
44
-	if err != nil {
45
-		return nil
46
-	}
47
-	return (*json.RawMessage)(&jsonval)
48
-}