diff --git a/copy/copy.go b/copy/copy.go index 29660b6b25..7b3f36aeb3 100644 --- a/copy/copy.go +++ b/copy/copy.go @@ -8,13 +8,13 @@ import ( "io/ioutil" "os" "reflect" - "runtime" "strings" "sync" "time" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/image" + platform "github.com/containers/image/v5/internal/pkg/platform" "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/pkg/blobinfocache" "github.com/containers/image/v5/pkg/compression" @@ -703,21 +703,22 @@ func checkImageDestinationForCurrentRuntime(ctx context.Context, sys *types.Syst if err != nil { return errors.Wrapf(err, "Error parsing image configuration") } - - wantedOS := runtime.GOOS - if sys != nil && sys.OSChoice != "" { - wantedOS = sys.OSChoice - } - if wantedOS != c.OS { - return fmt.Errorf("Image operating system mismatch: image uses %q, expecting %q", c.OS, wantedOS) - } - - wantedArch := runtime.GOARCH - if sys != nil && sys.ArchitectureChoice != "" { - wantedArch = sys.ArchitectureChoice + wantedPlatforms, err := platform.WantedPlatforms(sys) + if err != nil { + return errors.Wrapf(err, "error getting current platform information %#v", sys) } - if wantedArch != c.Architecture { - return fmt.Errorf("Image architecture mismatch: image uses %q, expecting %q", c.Architecture, wantedArch) + for _, wantedPlatform := range wantedPlatforms { + if wantedPlatform.OS != c.OS { + return fmt.Errorf("Image operating system mismatch: image uses %q, expecting %q", c.OS, wantedPlatform.OS) + } + if wantedPlatform.Architecture != c.Architecture { + return fmt.Errorf("Image architecture mismatch: image uses %q, expecting %q", c.Architecture, wantedPlatform.Architecture) + } + /* + // TODO Waiting for https://github.com/opencontainers/image-spec/pull/777 + if wantedPlatform.Variant != "" && c.Variant != "" && wantedPlatform.Variant != c.Variant { + return fmt.Errorf("Image variant mismatch: image uses %q, expecting %q", c.Variant, wantedPlatform.Variant) + }*/ } } return nil diff --git a/image/fixtures/schema2list.json b/image/fixtures/schema2list.json index 844215b29b..398b746cbd 100644 --- a/image/fixtures/schema2list.json +++ b/image/fixtures/schema2list.json @@ -31,16 +31,6 @@ "variant": "v6" } }, - { - "mediaType": "application/vnd.docker.distribution.manifest.v2+json", - "size": 527, - "digest": "sha256:a8fe0549cac196f439de3bf2b57af14f7cd4e59915ccd524428f588628a4ef31", - "platform": { - "architecture": "arm", - "os": "linux", - "variant": "v7" - } - }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 527, diff --git a/internal/pkg/platform/platform_matcher.go b/internal/pkg/platform/platform_matcher.go new file mode 100644 index 0000000000..55925cc908 --- /dev/null +++ b/internal/pkg/platform/platform_matcher.go @@ -0,0 +1,184 @@ +package platform + +// Largely based on +// https://github.com/moby/moby/blob/bc846d2e8fe5538220e0c31e9d0e8446f6fbc022/distribution/cpuinfo_unix.go +// https://github.com/containerd/containerd/blob/726dcaea50883e51b2ec6db13caff0e7936b711d/platforms/cpuinfo.go + +import ( + "bufio" + "fmt" + "os" + "runtime" + "strings" + + "github.com/containers/image/v5/types" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +// For Linux, the kernel has already detected the ABI, ISA and Features. +// So we don't need to access the ARM registers to detect platform information +// by ourselves. We can just parse these information from /proc/cpuinfo +func getCPUInfo(pattern string) (info string, err error) { + if runtime.GOOS != "linux" { + return "", fmt.Errorf("getCPUInfo for OS %s not implemented", runtime.GOOS) + } + + cpuinfo, err := os.Open("/proc/cpuinfo") + if err != nil { + return "", err + } + defer cpuinfo.Close() + + // Start to Parse the Cpuinfo line by line. For SMP SoC, we parse + // the first core is enough. + scanner := bufio.NewScanner(cpuinfo) + for scanner.Scan() { + newline := scanner.Text() + list := strings.Split(newline, ":") + + if len(list) > 1 && strings.EqualFold(strings.TrimSpace(list[0]), pattern) { + return strings.TrimSpace(list[1]), nil + } + } + + // Check whether the scanner encountered errors + err = scanner.Err() + if err != nil { + return "", err + } + + return "", fmt.Errorf("getCPUInfo for pattern: %s not found", pattern) +} + +func getCPUVariantWindows() string { + // Windows only supports v7 for ARM32 and v8 for ARM64 and so we can use + // runtime.GOARCH to determine the variants + var variant string + switch runtime.GOARCH { + case "arm64": + variant = "v8" + case "arm": + variant = "v7" + default: + variant = "" + } + + return variant +} + +func getCPUVariantArm() string { + variant, err := getCPUInfo("Cpu architecture") + if err != nil { + return "" + } + // TODO handle RPi Zero mismatch (https://github.com/moby/moby/pull/36121#issuecomment-398328286) + + switch strings.ToLower(variant) { + case "8", "aarch64": + variant = "v8" + case "7", "7m", "?(12)", "?(13)", "?(14)", "?(15)", "?(16)", "?(17)": + variant = "v7" + case "6", "6tej": + variant = "v6" + case "5", "5t", "5te", "5tej": + variant = "v5" + case "4", "4t": + variant = "v4" + case "3": + variant = "v3" + default: + variant = "" + } + + return variant +} + +func getCPUVariant(os string, arch string) string { + if os == "windows" { + return getCPUVariantWindows() + } + if arch == "arm" || arch == "arm64" { + return getCPUVariantArm() + } + return "" +} + +var compatibility = map[string][]string{ + "arm": {"v7", "v6", "v5"}, + "arm64": {"v8"}, +} + +// Returns all compatible platforms with the platform specifics possibly overriden by user, +// the most compatible platform is first. +// If some option (arch, os, variant) is not present, a value from current platform is detected. +func WantedPlatforms(ctx *types.SystemContext) ([]imgspecv1.Platform, error) { + wantedArch := runtime.GOARCH + if ctx != nil && ctx.ArchitectureChoice != "" { + wantedArch = ctx.ArchitectureChoice + } + wantedOS := runtime.GOOS + if ctx != nil && ctx.OSChoice != "" { + wantedOS = ctx.OSChoice + } + + wantedVariant := getCPUVariant(runtime.GOOS, runtime.GOARCH) + if ctx != nil && ctx.VariantChoice != "" { + wantedVariant = ctx.VariantChoice + } + + var wantedPlatforms []imgspecv1.Platform + if wantedVariant != "" && compatibility[wantedArch] != nil { + wantedPlatforms = make([]imgspecv1.Platform, 0, len(compatibility[wantedArch])) + wantedIndex := -1 + for i, v := range compatibility[wantedArch] { + if wantedVariant == v { + wantedIndex = i + break + } + } + // user wants a variant which we know nothing about - not even compatibility + if wantedIndex == -1 { + wantedPlatforms = []imgspecv1.Platform{ + { + OS: wantedOS, + Architecture: wantedArch, + Variant: wantedVariant, + }, + } + } else { + for i := wantedIndex; i < len(compatibility[wantedArch]); i++ { + v := compatibility[wantedArch][i] + wantedPlatforms = append(wantedPlatforms, imgspecv1.Platform{ + OS: wantedOS, + Architecture: wantedArch, + Variant: v, + }) + } + } + } else { + wantedPlatforms = []imgspecv1.Platform{ + { + OS: wantedOS, + Architecture: wantedArch, + Variant: wantedVariant, + }, + } + } + + return wantedPlatforms, nil +} + +func MatchesPlatform(image imgspecv1.Platform, wanted imgspecv1.Platform) bool { + if image.Architecture != wanted.Architecture { + return false + } + if image.OS != wanted.OS { + return false + } + + if wanted.Variant == "" || image.Variant == wanted.Variant { + return true + } + + return false +} diff --git a/internal/pkg/platform/platform_matcher_test.go b/internal/pkg/platform/platform_matcher_test.go new file mode 100644 index 0000000000..658a9ba56b --- /dev/null +++ b/internal/pkg/platform/platform_matcher_test.go @@ -0,0 +1,46 @@ +package platform + +import ( + "testing" + + "github.com/containers/image/v5/types" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" +) + +func TestWantedPlatformsCompatibility(t *testing.T) { + ctx := &types.SystemContext{ + ArchitectureChoice: "arm", + OSChoice: "linux", + VariantChoice: "v6", + } + platforms, err := WantedPlatforms(ctx) + assert.Nil(t, err) + assert.Equal(t, len(platforms), 2) + assert.Equal(t, platforms[0], imgspecv1.Platform{ + OS: ctx.OSChoice, + Architecture: ctx.ArchitectureChoice, + Variant: "v6", + }) + assert.Equal(t, platforms[1], imgspecv1.Platform{ + OS: ctx.OSChoice, + Architecture: ctx.ArchitectureChoice, + Variant: "v5", + }) +} + +func TestWantedPlatformsCustom(t *testing.T) { + ctx := &types.SystemContext{ + ArchitectureChoice: "armel", + OSChoice: "freeBSD", + VariantChoice: "custom", + } + platforms, err := WantedPlatforms(ctx) + assert.Nil(t, err) + assert.Equal(t, len(platforms), 1) + assert.Equal(t, platforms[0], imgspecv1.Platform{ + OS: ctx.OSChoice, + Architecture: ctx.ArchitectureChoice, + Variant: ctx.VariantChoice, + }) +} diff --git a/manifest/docker_schema2_list.go b/manifest/docker_schema2_list.go index 453976c487..59612f64b7 100644 --- a/manifest/docker_schema2_list.go +++ b/manifest/docker_schema2_list.go @@ -3,8 +3,8 @@ package manifest import ( "encoding/json" "fmt" - "runtime" + platform "github.com/containers/image/v5/internal/pkg/platform" "github.com/containers/image/v5/types" "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -92,21 +92,25 @@ func (list *Schema2List) UpdateInstances(updates []ListUpdate) error { // ChooseInstance parses blob as a schema2 manifest list, and returns the digest // of the image which is appropriate for the current environment. func (list *Schema2List) ChooseInstance(ctx *types.SystemContext) (digest.Digest, error) { - wantedArch := runtime.GOARCH - if ctx != nil && ctx.ArchitectureChoice != "" { - wantedArch = ctx.ArchitectureChoice - } - wantedOS := runtime.GOOS - if ctx != nil && ctx.OSChoice != "" { - wantedOS = ctx.OSChoice + wantedPlatforms, err := platform.WantedPlatforms(ctx) + if err != nil { + return "", errors.Wrapf(err, "error getting platform information %#v", ctx) } - - for _, d := range list.Manifests { - if d.Platform.Architecture == wantedArch && d.Platform.OS == wantedOS { - return d.Digest, nil + for _, wantedPlatform := range wantedPlatforms { + for _, d := range list.Manifests { + imagePlatform := imgspecv1.Platform{ + Architecture: d.Platform.Architecture, + OS: d.Platform.OS, + OSVersion: d.Platform.OSVersion, + OSFeatures: dupStringSlice(d.Platform.OSFeatures), + Variant: d.Platform.Variant, + } + if platform.MatchesPlatform(imagePlatform, wantedPlatform) { + return d.Digest, nil + } } } - return "", fmt.Errorf("no image found in manifest list for architecture %s, OS %s", wantedArch, wantedOS) + return "", fmt.Errorf("no image found in manifest list for architecture %s, variant %s, OS %s", wantedPlatforms[0].Architecture, wantedPlatforms[0].Variant, wantedPlatforms[0].OS) } // Serialize returns the list in a blob format. diff --git a/manifest/list_test.go b/manifest/list_test.go index 58bcb60b77..93866b1093 100644 --- a/manifest/list_test.go +++ b/manifest/list_test.go @@ -75,9 +75,8 @@ func TestChooseInstance(t *testing.T) { matchedInstances: map[string]digest.Digest{ "amd64": "sha256:030fcb92e1487b18c974784dcc110a93147c9fc402188370fbfd17efabffc6af", "s390x": "sha256:e5aa1b0a24620228b75382997a0977f609b3ca3a95533dafdef84c74cc8df642", - // There are several "arm" images with different variants; - // the current code returns the first match. NOTE: This is NOT an API promise. - "arm": "sha256:9142d97ef280a7953cf1a85716de49a24cc1dd62776352afad67e635331ff77a", + "arm": "sha256:b5dbad4bdb4444d919294afe49a095c23e86782f98cdf0aa286198ddb814b50b", + "arm64": "sha256:dc472a59fb006797aa2a6bfb54cc9c57959bb0a6d11fadaa608df8c16dea39cf", }, unmatchedInstances: []string{ "unmatched", @@ -101,10 +100,14 @@ func TestChooseInstance(t *testing.T) { require.NoError(t, err) // Match found for arch, expected := range manifestList.matchedInstances { - digest, err := list.ChooseInstance(&types.SystemContext{ + ctx := &types.SystemContext{ ArchitectureChoice: arch, OSChoice: "linux", - }) + } + if arch == "arm" { + ctx.VariantChoice = "v7" + } + digest, err := list.ChooseInstance(ctx) require.NoError(t, err, arch) assert.Equal(t, expected, digest) } diff --git a/manifest/oci_index.go b/manifest/oci_index.go index 816503ce5e..932be51218 100644 --- a/manifest/oci_index.go +++ b/manifest/oci_index.go @@ -5,6 +5,7 @@ import ( "fmt" "runtime" + platform "github.com/containers/image/v5/internal/pkg/platform" "github.com/containers/image/v5/types" "github.com/opencontainers/go-digest" imgspec "github.com/opencontainers/image-spec/specs-go" @@ -75,26 +76,31 @@ func (index *OCI1Index) UpdateInstances(updates []ListUpdate) error { // ChooseInstance parses blob as an oci v1 manifest index, and returns the digest // of the image which is appropriate for the current environment. func (index *OCI1Index) ChooseInstance(ctx *types.SystemContext) (digest.Digest, error) { - wantedArch := runtime.GOARCH - if ctx != nil && ctx.ArchitectureChoice != "" { - wantedArch = ctx.ArchitectureChoice - } - wantedOS := runtime.GOOS - if ctx != nil && ctx.OSChoice != "" { - wantedOS = ctx.OSChoice + wantedPlatforms, err := platform.WantedPlatforms(ctx) + if err != nil { + return "", errors.Wrapf(err, "error getting platform information %#v", ctx) } - - for _, d := range index.Manifests { - if d.Platform != nil && d.Platform.Architecture == wantedArch && d.Platform.OS == wantedOS { - return d.Digest, nil + for _, wantedPlatform := range wantedPlatforms { + for _, d := range index.Manifests { + imagePlatform := imgspecv1.Platform{ + Architecture: d.Platform.Architecture, + OS: d.Platform.OS, + OSVersion: d.Platform.OSVersion, + OSFeatures: dupStringSlice(d.Platform.OSFeatures), + Variant: d.Platform.Variant, + } + if platform.MatchesPlatform(imagePlatform, wantedPlatform) { + return d.Digest, nil + } } } + for _, d := range index.Manifests { if d.Platform == nil { return d.Digest, nil } } - return "", fmt.Errorf("no image found in image index for architecture %s, OS %s", wantedArch, wantedOS) + return "", fmt.Errorf("no image found in manifest list for architecture %s, variant %s, OS %s", wantedPlatforms[0].Architecture, wantedPlatforms[0].Variant, wantedPlatforms[0].OS) } // Serialize returns the index in a blob format. diff --git a/types/types.go b/types/types.go index ba249ca25d..ed26a25f23 100644 --- a/types/types.go +++ b/types/types.go @@ -510,6 +510,8 @@ type SystemContext struct { ArchitectureChoice string // If not "", overrides the use of platform.GOOS when choosing an image or verifying OS match. OSChoice string + // If not "", overrides the use of detected ARM platform variant when choosing an image or verifying variant match. + VariantChoice string // If not "", overrides the system's default directory containing a blob info cache. BlobInfoCacheDir string // Additional tags when creating or copying a docker-archive.