mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
daemon: Best-effot container OS to ImagePlatform migration
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
@@ -78,6 +78,14 @@ func (daemon *Daemon) load(id string) (*container.Container, error) {
|
||||
}
|
||||
selinux.ReserveLabel(ctr.ProcessLabel)
|
||||
|
||||
if ctr.ImagePlatform.Architecture == "" {
|
||||
migration := daemonPlatformReader{
|
||||
imageService: daemon.imageService,
|
||||
content: daemon.containerdClient.ContentStore(),
|
||||
}
|
||||
migrateContainerOS(context.TODO(), migration, ctr)
|
||||
}
|
||||
|
||||
if ctr.ID != id {
|
||||
return ctr, fmt.Errorf("Container %s is stored at %s", ctr.ID, id)
|
||||
}
|
||||
|
||||
137
daemon/migration.go
Normal file
137
daemon/migration.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/containerd/containerd/content"
|
||||
"github.com/containerd/log"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/docker/docker/api/types/backend"
|
||||
"github.com/docker/docker/container"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/internal/multierror"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func migrateContainerOS(ctx context.Context,
|
||||
migration platformReader,
|
||||
ctr *container.Container,
|
||||
) {
|
||||
deduced, err := deduceContainerPlatform(ctx, migration, ctr)
|
||||
if err != nil {
|
||||
log.G(ctx).WithFields(log.Fields{
|
||||
"container": ctr.ID,
|
||||
"error": err,
|
||||
}).Warn("failed to deduce the container architecture")
|
||||
ctr.ImagePlatform.OS = ctr.OS //nolint:staticcheck // ignore SA1019
|
||||
return
|
||||
}
|
||||
|
||||
ctr.ImagePlatform = deduced
|
||||
}
|
||||
|
||||
type platformReader interface {
|
||||
ReadPlatformFromConfigByImageManifest(ctx context.Context, desc ocispec.Descriptor) (ocispec.Platform, error)
|
||||
ReadPlatformFromImage(ctx context.Context, id image.ID) (ocispec.Platform, error)
|
||||
}
|
||||
|
||||
// deduceContainerPlatform tries to deduce `ctr`'s platform.
|
||||
// If both `ctr.OS` and `ctr.ImageManifest` are empty, assume the image comes
|
||||
// from a pre-OS times and use the host platform to match the behavior of
|
||||
// [container.FromDisk].
|
||||
// Otherwise:
|
||||
// - `ctr.ImageManifest.Platform` is used, if it exists and is not empty.
|
||||
// - The platform from the manifest's config is used, if `ctr.ImageManifest` exists
|
||||
// and we're able to load its config from the content store.
|
||||
// - The platform found by loading the image from the image service by ID (using
|
||||
// `ctr.ImageID`) is used – this looks for the best *present* matching manifest in
|
||||
// the store.
|
||||
func deduceContainerPlatform(
|
||||
ctx context.Context,
|
||||
migration platformReader,
|
||||
ctr *container.Container,
|
||||
) (ocispec.Platform, error) {
|
||||
if ctr.OS == "" && ctr.ImageManifest == nil { //nolint:staticcheck // ignore SA1019 because we are testing deprecated field migration
|
||||
return platforms.DefaultSpec(), nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
isValidPlatform := func(p ocispec.Platform) bool {
|
||||
return p.OS != "" && p.Architecture != ""
|
||||
}
|
||||
|
||||
if ctr.ImageManifest != nil {
|
||||
if ctr.ImageManifest.Platform != nil {
|
||||
return *ctr.ImageManifest.Platform, nil
|
||||
}
|
||||
|
||||
if ctr.ImageManifest != nil {
|
||||
p, err := migration.ReadPlatformFromConfigByImageManifest(ctx, *ctr.ImageManifest)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
if isValidPlatform(p) {
|
||||
return p, nil
|
||||
}
|
||||
errs = append(errs, errors.New("malformed image config obtained by ImageManifestDescriptor"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ctr.ImageID != "" {
|
||||
p, err := migration.ReadPlatformFromImage(ctx, ctr.ImageID)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
if isValidPlatform(p) {
|
||||
return p, nil
|
||||
}
|
||||
errs = append(errs, errors.New("malformed image config obtained by image id"))
|
||||
}
|
||||
}
|
||||
|
||||
return ocispec.Platform{}, errors.Wrap(multierror.Join(errs...), "cannot deduce the container platform")
|
||||
}
|
||||
|
||||
type daemonPlatformReader struct {
|
||||
imageService ImageService
|
||||
content content.Provider
|
||||
}
|
||||
|
||||
func (r daemonPlatformReader) ReadPlatformFromConfigByImageManifest(
|
||||
ctx context.Context,
|
||||
desc ocispec.Descriptor,
|
||||
) (ocispec.Platform, error) {
|
||||
b, err := content.ReadBlob(ctx, r.content, desc)
|
||||
if err != nil {
|
||||
return ocispec.Platform{}, err
|
||||
}
|
||||
|
||||
var mfst ocispec.Manifest
|
||||
if err := json.Unmarshal(b, &mfst); err != nil {
|
||||
return ocispec.Platform{}, err
|
||||
}
|
||||
|
||||
b, err = content.ReadBlob(ctx, r.content, mfst.Config)
|
||||
if err != nil {
|
||||
return ocispec.Platform{}, err
|
||||
}
|
||||
|
||||
var plat ocispec.Platform
|
||||
if err := json.Unmarshal(b, &plat); err != nil {
|
||||
return ocispec.Platform{}, err
|
||||
}
|
||||
|
||||
return plat, nil
|
||||
}
|
||||
|
||||
func (r daemonPlatformReader) ReadPlatformFromImage(ctx context.Context, id image.ID) (ocispec.Platform, error) {
|
||||
img, err := r.imageService.GetImage(ctx, id.String(), backend.GetImageOpts{})
|
||||
if err != nil {
|
||||
return ocispec.Platform{}, err
|
||||
}
|
||||
|
||||
return img.Platform(), nil
|
||||
}
|
||||
173
daemon/migration_test.go
Normal file
173
daemon/migration_test.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/docker/docker/container"
|
||||
"github.com/docker/docker/image"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
type mockPlatformReader struct{}
|
||||
|
||||
func (m mockPlatformReader) ReadPlatformFromImage(ctx context.Context, id image.ID) (ocispec.Platform, error) {
|
||||
switch id {
|
||||
case "multiplatform":
|
||||
// This image has multiple platforms, but GetImage will prefer the first one
|
||||
// because the ID points to the full image index, not a specific platform.
|
||||
return platforms.DefaultSpec(), nil
|
||||
case "linux/arm64/v8":
|
||||
return ocispec.Platform{
|
||||
OS: "linux",
|
||||
Architecture: "arm64",
|
||||
Variant: "v8",
|
||||
}, nil
|
||||
case "linux/amd64":
|
||||
return ocispec.Platform{
|
||||
OS: "linux",
|
||||
Architecture: "amd64",
|
||||
}, nil
|
||||
case "windows/amd64":
|
||||
return ocispec.Platform{
|
||||
OS: "windows",
|
||||
Architecture: "amd64",
|
||||
}, nil
|
||||
default:
|
||||
return ocispec.Platform{}, errors.New("image not found")
|
||||
}
|
||||
}
|
||||
|
||||
func (m mockPlatformReader) ReadPlatformFromConfigByImageManifest(ctx context.Context, desc ocispec.Descriptor) (ocispec.Platform, error) {
|
||||
return m.ReadPlatformFromImage(ctx, image.ID(desc.Digest))
|
||||
}
|
||||
|
||||
//nolint:staticcheck // ignore SA1019 because we are testing deprecated field migration
|
||||
func TestContainerMigrateOS(t *testing.T) {
|
||||
type Container = container.Container
|
||||
|
||||
var mock mockPlatformReader
|
||||
|
||||
// ImageManifest is nil for containers created with graphdrivers image store
|
||||
var graphdrivers *ocispec.Descriptor = nil
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
ctr Container
|
||||
expected ocispec.Platform
|
||||
}{
|
||||
{
|
||||
name: "gd pre-OS container",
|
||||
ctr: Container{
|
||||
ImageManifest: graphdrivers,
|
||||
OS: "",
|
||||
},
|
||||
expected: platforms.DefaultSpec(),
|
||||
},
|
||||
{
|
||||
name: "gd with linux arm64 image",
|
||||
ctr: Container{
|
||||
ImageManifest: graphdrivers,
|
||||
ImageID: "linux/arm64/v8",
|
||||
OS: "linux",
|
||||
},
|
||||
expected: ocispec.Platform{
|
||||
OS: "linux",
|
||||
Architecture: "arm64",
|
||||
Variant: "v8",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gd with windows image",
|
||||
ctr: Container{
|
||||
ImageManifest: graphdrivers,
|
||||
ImageID: "windows/amd64",
|
||||
OS: "windows",
|
||||
},
|
||||
expected: ocispec.Platform{
|
||||
OS: "windows",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gd with an image thats no longer available",
|
||||
ctr: Container{
|
||||
ImageManifest: graphdrivers,
|
||||
ImageID: "notfound",
|
||||
OS: "linux",
|
||||
},
|
||||
expected: platforms.Platform{
|
||||
OS: "linux",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "c8d with linux arm64 image",
|
||||
ctr: Container{
|
||||
ImageManifest: &ocispec.Descriptor{
|
||||
Digest: "linux/arm64/v8",
|
||||
},
|
||||
OS: "linux",
|
||||
ImageID: "linux/arm64/v8",
|
||||
},
|
||||
expected: ocispec.Platform{
|
||||
OS: "linux",
|
||||
Architecture: "arm64",
|
||||
Variant: "v8",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "c8d with an image thats no longer available",
|
||||
ctr: Container{
|
||||
ImageManifest: &ocispec.Descriptor{
|
||||
Digest: "notfound",
|
||||
},
|
||||
OS: "linux",
|
||||
ImageID: "notfound",
|
||||
},
|
||||
expected: platforms.Platform{
|
||||
OS: "linux",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "c8d with ImageManifest that is no longer available",
|
||||
ctr: Container{
|
||||
ImageManifest: &ocispec.Descriptor{
|
||||
Digest: "notfound",
|
||||
},
|
||||
OS: "linux",
|
||||
ImageID: "multiplatform",
|
||||
},
|
||||
// Note: This might produce unexpected results, because if the platform-specific manifest
|
||||
// is not available, and the ImageID points to a multi-platform image, then GetImage will
|
||||
// return any available platform with host platform being the priority.
|
||||
// So it will just use whatever platform is returned by GetImage (docker image inspect).
|
||||
expected: platforms.DefaultSpec(),
|
||||
},
|
||||
{
|
||||
name: "ImageManifest has priority over ImageID migration",
|
||||
ctr: Container{
|
||||
ImageManifest: &ocispec.Descriptor{
|
||||
Digest: "linux/arm64/v8",
|
||||
},
|
||||
OS: "linux",
|
||||
ImageID: "linux/amd64",
|
||||
},
|
||||
expected: ocispec.Platform{
|
||||
OS: "linux",
|
||||
Architecture: "arm64",
|
||||
Variant: "v8",
|
||||
},
|
||||
},
|
||||
} {
|
||||
ctr := tc.ctr
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
migrateContainerOS(context.Background(), mock, &ctr)
|
||||
|
||||
assert.DeepEqual(t, tc.expected, ctr.ImagePlatform)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user