Signed-off-by: Derek McGowan <derek@mcg.dev>
| ... | ... |
@@ -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" |
| ... | ... |
@@ -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" |
| ... | ... |
@@ -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" |
| ... | ... |
@@ -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 | 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 |
-} |
| 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 |
-} |