libimage: normalize platforms correctly

Use containerd's platform package for platform checks. While the OCI
image spec requires the platform values to conform with GOOS and GOARCH
definitions of Go' runtime package, the values of uname are used by
convention.  Supporting these values silences annoying false-positive
warnings.

Fixes: #containers/podman/issues/14669
Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
This commit is contained in:
Valentin Rothberg 2022-06-27 13:50:59 +02:00
parent 7c01caaac2
commit fa2e6ee0bf
5 changed files with 64 additions and 73 deletions

View File

@ -1,51 +1,13 @@
package libimage
import (
"runtime"
"strings"
"github.com/containerd/containerd/platforms"
"github.com/containers/image/v5/docker/reference"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// NormalizePlatform normalizes (according to the OCI spec) the specified os,
// arch and variant. If left empty, the individual item will not be normalized.
func NormalizePlatform(rawOS, rawArch, rawVariant string) (os, arch, variant string) {
os, arch, variant = rawOS, rawArch, rawVariant
if os == "" {
os = runtime.GOOS
}
if arch == "" {
arch = runtime.GOARCH
}
rawPlatform := os + "/" + arch
if variant != "" {
rawPlatform += "/" + variant
}
normalizedPlatform, err := platforms.Parse(rawPlatform)
if err != nil {
logrus.Debugf("Error normalizing platform: %v", err)
return rawOS, rawArch, rawVariant
}
logrus.Debugf("Normalized platform %s to %s", rawPlatform, normalizedPlatform)
os = rawOS
if rawOS != "" {
os = normalizedPlatform.OS
}
arch = rawArch
if rawArch != "" {
arch = normalizedPlatform.Architecture
}
variant = rawVariant
if rawVariant != "" {
variant = normalizedPlatform.Variant
}
return os, arch, variant
}
// NormalizeName normalizes the provided name according to the conventions by
// Podman and Buildah. If tag and digest are missing, the "latest" tag will be
// used. If it's a short name, it will be prefixed with "localhost/".

View File

@ -4,6 +4,9 @@ import (
"context"
"fmt"
"runtime"
"github.com/containerd/containerd/platforms"
"github.com/sirupsen/logrus"
)
// PlatformPolicy controls the behavior of image-platform matching.
@ -16,11 +19,42 @@ const (
PlatformPolicyWarn
)
func toPlatformString(architecture, os, variant string) string {
if variant == "" {
return fmt.Sprintf("%s/%s", os, architecture)
// NormalizePlatform normalizes (according to the OCI spec) the specified os,
// arch and variant. If left empty, the individual item will not be normalized.
func NormalizePlatform(rawOS, rawArch, rawVariant string) (os, arch, variant string) {
rawPlatform := toPlatformString(rawOS, rawArch, rawVariant)
normalizedPlatform, err := platforms.Parse(rawPlatform)
if err != nil {
logrus.Debugf("Error normalizing platform: %v", err)
return rawOS, rawArch, rawVariant
}
return fmt.Sprintf("%s/%s/%s", os, architecture, variant)
logrus.Debugf("Normalized platform %s to %s", rawPlatform, normalizedPlatform)
os = rawOS
if rawOS != "" {
os = normalizedPlatform.OS
}
arch = rawArch
if rawArch != "" {
arch = normalizedPlatform.Architecture
}
variant = rawVariant
if rawVariant != "" {
variant = normalizedPlatform.Variant
}
return os, arch, variant
}
func toPlatformString(os, arch, variant string) string {
if os == "" {
os = runtime.GOOS
}
if arch == "" {
arch = runtime.GOARCH
}
if variant == "" {
return fmt.Sprintf("%s/%s", os, arch)
}
return fmt.Sprintf("%s/%s/%s", os, arch, variant)
}
// Checks whether the image matches the specified platform.
@ -28,36 +62,26 @@ func toPlatformString(architecture, os, variant string) string {
// * 1) a matching error that can be used for logging (or returning) what does not match
// * 2) a bool indicating whether architecture, os or variant were set (some callers need that to decide whether they need to throw an error)
// * 3) a fatal error that occurred prior to check for matches (e.g., storage errors etc.)
func (i *Image) matchesPlatform(ctx context.Context, architecture, os, variant string) (error, bool, error) {
customPlatform := len(architecture)+len(os)+len(variant) != 0
if len(architecture) == 0 {
architecture = runtime.GOARCH
}
if len(os) == 0 {
os = runtime.GOOS
}
func (i *Image) matchesPlatform(ctx context.Context, os, arch, variant string) (error, bool, error) {
inspectInfo, err := i.inspectInfo(ctx)
if err != nil {
return nil, customPlatform, fmt.Errorf("inspecting image: %w", err)
return nil, false, fmt.Errorf("inspecting image: %w", err)
}
matches := true
switch {
case architecture != inspectInfo.Architecture:
matches = false
case os != inspectInfo.Os:
matches = false
case variant != "" && variant != inspectInfo.Variant:
matches = false
customPlatform := len(os)+len(arch)+len(variant) != 0
expected, err := platforms.Parse(toPlatformString(os, arch, variant))
if err != nil {
return nil, false, fmt.Errorf("parsing host platform: %v", err)
}
fromImage, err := platforms.Parse(toPlatformString(inspectInfo.Os, inspectInfo.Architecture, inspectInfo.Variant))
if err != nil {
return nil, false, fmt.Errorf("parsing image platform: %v", err)
}
if matches {
if platforms.NewMatcher(expected).Match(fromImage) {
return nil, customPlatform, nil
}
imagePlatform := toPlatformString(inspectInfo.Architecture, inspectInfo.Os, inspectInfo.Variant)
expectedPlatform := toPlatformString(architecture, os, variant)
return fmt.Errorf("image platform (%s) does not match the expected platform (%s)", imagePlatform, expectedPlatform), customPlatform, nil
return fmt.Errorf("image platform (%s) does not match the expected platform (%s)", fromImage, expected), customPlatform, nil
}

View File

@ -1,6 +1,8 @@
package libimage
import (
"fmt"
"runtime"
"testing"
"github.com/stretchr/testify/require"
@ -8,13 +10,16 @@ import (
func TestToPlatformString(t *testing.T) {
for _, test := range []struct {
arch, os, variant, expected string
os, arch, variant, expected string
}{
{"a", "b", "", "b/a"},
{"a", "b", "c", "b/a/c"},
{"", "", "c", "//c"}, // callers are responsible for the input
{"a", "b", "", "a/b"},
{"a", "", "", fmt.Sprintf("a/%s", runtime.GOARCH)},
{"", "b", "", fmt.Sprintf("%s/b", runtime.GOOS)},
{"a", "b", "c", "a/b/c"},
{"", "", "", fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)},
{"", "", "c", fmt.Sprintf("%s/%s/c", runtime.GOOS, runtime.GOARCH)},
} {
platform := toPlatformString(test.arch, test.os, test.variant)
require.Equal(t, platform, test.expected)
platform := toPlatformString(test.os, test.arch, test.variant)
require.Equal(t, test.expected, platform)
}
}

View File

@ -169,7 +169,7 @@ func (r *Runtime) Pull(ctx context.Context, name string, pullPolicy config.PullP
// Note that we can ignore the 2nd return value here. Some
// images may ship with "wrong" platform, but we already warn
// about it. Throwing an error is not (yet) the plan.
matchError, _, err := image.matchesPlatform(ctx, options.Architecture, options.OS, options.Variant)
matchError, _, err := image.matchesPlatform(ctx, options.OS, options.Architecture, options.Variant)
if err != nil {
return nil, fmt.Errorf("checking platform of image %s: %w", name, err)
}

View File

@ -396,7 +396,7 @@ func (r *Runtime) lookupImageInLocalStorage(name, candidate string, options *Loo
// Ignore the (fatal) error since the image may be corrupted, which
// will bubble up at other places. During lookup, we just return it as
// is.
if matchError, customPlatform, _ := image.matchesPlatform(context.Background(), options.Architecture, options.OS, options.Variant); matchError != nil {
if matchError, customPlatform, _ := image.matchesPlatform(context.Background(), options.OS, options.Architecture, options.Variant); matchError != nil {
if customPlatform {
logrus.Debugf("%v", matchError)
// Return nil if the user clearly requested a custom