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 package libimage
import ( import (
"runtime"
"strings" "strings"
"github.com/containerd/containerd/platforms"
"github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/docker/reference"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "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 // NormalizeName normalizes the provided name according to the conventions by
// Podman and Buildah. If tag and digest are missing, the "latest" tag will be // 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/". // used. If it's a short name, it will be prefixed with "localhost/".

View File

@ -4,6 +4,9 @@ import (
"context" "context"
"fmt" "fmt"
"runtime" "runtime"
"github.com/containerd/containerd/platforms"
"github.com/sirupsen/logrus"
) )
// PlatformPolicy controls the behavior of image-platform matching. // PlatformPolicy controls the behavior of image-platform matching.
@ -16,11 +19,42 @@ const (
PlatformPolicyWarn PlatformPolicyWarn
) )
func toPlatformString(architecture, os, variant string) string { // NormalizePlatform normalizes (according to the OCI spec) the specified os,
if variant == "" { // arch and variant. If left empty, the individual item will not be normalized.
return fmt.Sprintf("%s/%s", os, architecture) 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. // 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 // * 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) // * 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.) // * 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) { func (i *Image) matchesPlatform(ctx context.Context, os, arch, 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
}
inspectInfo, err := i.inspectInfo(ctx) inspectInfo, err := i.inspectInfo(ctx)
if err != nil { if err != nil {
return nil, customPlatform, fmt.Errorf("inspecting image: %w", err) return nil, false, fmt.Errorf("inspecting image: %w", err)
} }
matches := true customPlatform := len(os)+len(arch)+len(variant) != 0
switch {
case architecture != inspectInfo.Architecture: expected, err := platforms.Parse(toPlatformString(os, arch, variant))
matches = false if err != nil {
case os != inspectInfo.Os: return nil, false, fmt.Errorf("parsing host platform: %v", err)
matches = false }
case variant != "" && variant != inspectInfo.Variant: fromImage, err := platforms.Parse(toPlatformString(inspectInfo.Os, inspectInfo.Architecture, inspectInfo.Variant))
matches = false 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 return nil, customPlatform, nil
} }
imagePlatform := toPlatformString(inspectInfo.Architecture, inspectInfo.Os, inspectInfo.Variant) return fmt.Errorf("image platform (%s) does not match the expected platform (%s)", fromImage, expected), customPlatform, nil
expectedPlatform := toPlatformString(architecture, os, variant)
return fmt.Errorf("image platform (%s) does not match the expected platform (%s)", imagePlatform, expectedPlatform), customPlatform, nil
} }

View File

@ -1,6 +1,8 @@
package libimage package libimage
import ( import (
"fmt"
"runtime"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -8,13 +10,16 @@ import (
func TestToPlatformString(t *testing.T) { func TestToPlatformString(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
arch, os, variant, expected string os, arch, variant, expected string
}{ }{
{"a", "b", "", "b/a"}, {"a", "b", "", "a/b"},
{"a", "b", "c", "b/a/c"}, {"a", "", "", fmt.Sprintf("a/%s", runtime.GOARCH)},
{"", "", "c", "//c"}, // callers are responsible for the input {"", "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) platform := toPlatformString(test.os, test.arch, test.variant)
require.Equal(t, platform, test.expected) 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 // Note that we can ignore the 2nd return value here. Some
// images may ship with "wrong" platform, but we already warn // images may ship with "wrong" platform, but we already warn
// about it. Throwing an error is not (yet) the plan. // 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 { if err != nil {
return nil, fmt.Errorf("checking platform of image %s: %w", name, err) 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 // Ignore the (fatal) error since the image may be corrupted, which
// will bubble up at other places. During lookup, we just return it as // will bubble up at other places. During lookup, we just return it as
// is. // 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 { if customPlatform {
logrus.Debugf("%v", matchError) logrus.Debugf("%v", matchError)
// Return nil if the user clearly requested a custom // Return nil if the user clearly requested a custom