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:
parent
7c01caaac2
commit
fa2e6ee0bf
|
@ -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/".
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue