Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 20 additions & 15 deletions copy/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
"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"
Expand Down Expand Up @@ -715,21 +715,26 @@ 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 {
logrus.Infof("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)
}

options := newOrderedSet()
match := false
for _, wantedPlatform := range wantedPlatforms {
// Waiting for https://github.com/opencontainers/image-spec/pull/777 :
// This currently can’t use image.MatchesPlatform because we don’t know what to use
// for image.Variant.
if wantedPlatform.OS == c.OS && wantedPlatform.Architecture == c.Architecture {
match = true
break
}
options.append(fmt.Sprintf("%s+%s", wantedPlatform.OS, wantedPlatform.Architecture))
}
if wantedArch != c.Architecture {
logrus.Infof("Image architecture mismatch: image uses %q, expecting %q", c.Architecture, wantedArch)
if !match {
logrus.Infof("Image operating system mismatch: image uses OS %q+architecture %q, expecting one of %q",
c.OS, c.Architecture, strings.Join(options.list, ", "))
}
}
return nil
Expand Down
2 changes: 1 addition & 1 deletion copy/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
// Include v2s1 signed but not v2s1 unsigned, because docker/distribution requires a signature even if the unsigned MIME type is used.
var preferredManifestMIMETypes = []string{manifest.DockerV2Schema2MediaType, manifest.DockerV2Schema1SignedMediaType}

// orderedSet is a list of strings (MIME types in our case), with each string appearing at most once.
// orderedSet is a list of strings (MIME types or platform descriptors in our case), with each string appearing at most once.
type orderedSet struct {
list []string
included map[string]struct{}
Expand Down
10 changes: 0 additions & 10 deletions image/fixtures/schema2list.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
196 changes: 196 additions & 0 deletions internal/pkg/platform/platform_matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package platform

// Largely based on
// https://github.com/moby/moby/blob/bc846d2e8fe5538220e0c31e9d0e8446f6fbc022/distribution/cpuinfo_unix.go
// Copyright 2012-2017 Docker, Inc.
//
// https://github.com/containerd/containerd/blob/726dcaea50883e51b2ec6db13caff0e7936b711d/platforms/cpuinfo.go
// Copyright The containerd Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

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
}
46 changes: 46 additions & 0 deletions internal/pkg/platform/platform_matcher_test.go
Original file line number Diff line number Diff line change
@@ -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,
})
}
30 changes: 17 additions & 13 deletions manifest/docker_schema2_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -89,21 +89,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.
Expand Down
Loading