From c3be82ddefbb87be94b4dcfc24746a844834b4b7 Mon Sep 17 00:00:00 2001 From: Trevor Burnham Date: Wed, 8 Apr 2026 16:03:43 -0400 Subject: [PATCH] Support default platform via containers.conf and CONTAINER_DEFAULT_PLATFORM Signed-off-by: Trevor Burnham --- cmd/podman/common/build.go | 17 ++++++ cmd/podman/containers/create.go | 22 ++++++-- cmd/podman/images/pull.go | 23 +++++--- docs/source/markdown/options/platform.md | 4 ++ docs/source/markdown/podman-build.1.md.in | 4 ++ pkg/api/handlers/compat/containers_create.go | 17 +++++- pkg/api/handlers/compat/images.go | 56 ++++++++++++++------ pkg/api/handlers/compat/images_build.go | 40 +++++++++----- pkg/api/handlers/libpod/images_pull.go | 14 +++++ pkg/domain/infra/abi/images.go | 39 ++++++++++++++ pkg/util/utils.go | 19 +++++++ test/system/010-images.bats | 19 +++++++ 12 files changed, 232 insertions(+), 42 deletions(-) diff --git a/cmd/podman/common/build.go b/cmd/podman/common/build.go index bd79b8b9605..d51358707cd 100644 --- a/cmd/podman/common/build.go +++ b/cmd/podman/common/build.go @@ -23,6 +23,7 @@ import ( "github.com/containers/podman/v6/cmd/podman/utils" "github.com/containers/podman/v6/pkg/domain/entities" "github.com/containers/podman/v6/pkg/env" + podmanUtil "github.com/containers/podman/v6/pkg/util" "github.com/openshift/imagebuilder" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -473,6 +474,22 @@ func buildFlagsWrapperToOptions(c *cobra.Command, contextDir string, flags *Buil return nil, err } + // If no explicit --platform, --os, --arch, or --variant was given, + // fall back to CONTAINER_DEFAULT_PLATFORM env var so that + // podman-remote transmits the platform to the server. + if !c.Flag("platform").Changed && !c.Flag("os").Changed && + !c.Flag("arch").Changed && !c.Flag("variant").Changed { + defOS, defArch, defVariant, pErr := podmanUtil.DefaultPlatform() + if pErr != nil { + return nil, pErr + } + if defOS != "" || defArch != "" || defVariant != "" { + platforms = []struct{ OS, Arch, Variant string }{ + {OS: defOS, Arch: defArch, Variant: defVariant}, + } + } + } + decConfig, err := getDecryptConfig(flags.DecryptionKeys) if err != nil { return nil, fmt.Errorf("unable to obtain decrypt config: %w", err) diff --git a/cmd/podman/containers/create.go b/cmd/podman/containers/create.go index 8f8aebb82d5..00eb81c6959 100644 --- a/cmd/podman/containers/create.go +++ b/cmd/podman/containers/create.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/containers/buildah/pkg/cli" + "github.com/containers/buildah/pkg/parse" "github.com/containers/podman/v6/cmd/podman/common" "github.com/containers/podman/v6/cmd/podman/registry" "github.com/containers/podman/v6/cmd/podman/utils" @@ -352,12 +353,25 @@ func pullImage(cmd *cobra.Command, imageName string, cliVals *entities.Container if cliVals.Arch != "" || cliVals.OS != "" { return "", errors.New("--platform option can not be specified with --arch or --os") } - OS, Arch, hasArch := strings.Cut(cliVals.Platform, "/") - cliVals.OS = OS - if hasArch { - cliVals.Arch = Arch + pOS, pArch, pVariant, pErr := parse.Platform(cliVals.Platform) + if pErr != nil { + return "", fmt.Errorf("parsing platform %q: %w", cliVals.Platform, pErr) } + cliVals.OS = pOS + cliVals.Arch = pArch + cliVals.Variant = pVariant } + } else { + // No explicit --platform, --os, or --arch was given; fall back to + // CONTAINER_DEFAULT_PLATFORM env var so that podman-remote + // transmits the platform to the server. + defOS, defArch, defVariant, pErr := util.DefaultPlatform() + if pErr != nil { + return "", pErr + } + cliVals.OS = defOS + cliVals.Arch = defArch + cliVals.Variant = defVariant } skipTLSVerify := types.OptionalBoolUndefined diff --git a/cmd/podman/images/pull.go b/cmd/podman/images/pull.go index 8ff7492d7fd..647ee54df0a 100644 --- a/cmd/podman/images/pull.go +++ b/cmd/podman/images/pull.go @@ -4,9 +4,9 @@ import ( "errors" "fmt" "os" - "strings" "github.com/containers/buildah/pkg/cli" + "github.com/containers/buildah/pkg/parse" "github.com/containers/podman/v6/cmd/podman/common" "github.com/containers/podman/v6/cmd/podman/registry" "github.com/containers/podman/v6/cmd/podman/utils" @@ -188,13 +188,20 @@ func imagePull(cmd *cobra.Command, args []string) error { return errors.New("--platform option can not be specified with --arch or --os") } - specs := strings.Split(platform, "/") - pullOptions.OS = specs[0] // may be empty - if len(specs) > 1 { - pullOptions.Arch = specs[1] - if len(specs) > 2 { - pullOptions.Variant = specs[2] - } + pullOptions.OS, pullOptions.Arch, pullOptions.Variant, err = parse.Platform(platform) + if err != nil { + return fmt.Errorf("parsing platform %q: %w", platform, err) + } + } + + // If no explicit --platform, --os, --arch, or --variant was given, + // fall back to CONTAINER_DEFAULT_PLATFORM env var. This must be + // resolved on the client side so that podman-remote transmits the + // platform to the server. + if platform == "" && pullOptions.Arch == "" && pullOptions.OS == "" && pullOptions.Variant == "" { + pullOptions.OS, pullOptions.Arch, pullOptions.Variant, err = util.DefaultPlatform() + if err != nil { + return err } } diff --git a/docs/source/markdown/options/platform.md b/docs/source/markdown/options/platform.md index ff1f86ded06..7a24e9d87b5 100644 --- a/docs/source/markdown/options/platform.md +++ b/docs/source/markdown/options/platform.md @@ -7,3 +7,7 @@ Specify the platform for selecting the image. (Conflicts with --arch and --os) The `--platform` option can be used to override the current architecture and operating system. Unless overridden, subsequent lookups of the same image in the local storage matches this platform, regardless of the host. + +If not specified, the default platform is resolved in the following order: +1. The **CONTAINER_DEFAULT_PLATFORM** environment variable. +2. The host's native OS/architecture. diff --git a/docs/source/markdown/podman-build.1.md.in b/docs/source/markdown/podman-build.1.md.in index e4905a138d7..e6a56bd41d0 100644 --- a/docs/source/markdown/podman-build.1.md.in +++ b/docs/source/markdown/podman-build.1.md.in @@ -319,6 +319,10 @@ architecture of the host (for example `linux/arm`). Unless overridden, subsequent lookups of the same image in the local storage matches this platform, regardless of the host. +If not specified, the default platform is resolved in the following order: +1. The **CONTAINER_DEFAULT_PLATFORM** environment variable. +2. The host's native OS/architecture. + If `--platform` is set, then the values of the `--arch`, `--os`, and `--variant` options is overridden. diff --git a/pkg/api/handlers/compat/containers_create.go b/pkg/api/handlers/compat/containers_create.go index 1fb92b212f2..cf6da24be98 100644 --- a/pkg/api/handlers/compat/containers_create.go +++ b/pkg/api/handlers/compat/containers_create.go @@ -24,6 +24,7 @@ import ( "github.com/containers/podman/v6/pkg/rootless" "github.com/containers/podman/v6/pkg/specgen" "github.com/containers/podman/v6/pkg/specgenutil" + "github.com/containers/podman/v6/pkg/util" "github.com/moby/moby/api/types/mount" "go.podman.io/common/libimage" "go.podman.io/common/libnetwork/types" @@ -74,13 +75,25 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { body.Config.Image = imageName lookupImageOptions := libimage.LookupImageOptions{} - if query.Platform != "" { + // If no platform was specified in the query, fall back to + // CONTAINER_DEFAULT_PLATFORM env var, then containers.conf platform. + platform := query.Platform + if platform != "" { var err error - lookupImageOptions.OS, lookupImageOptions.Architecture, lookupImageOptions.Variant, err = parse.Platform(query.Platform) + lookupImageOptions.OS, lookupImageOptions.Architecture, lookupImageOptions.Variant, err = parse.Platform(platform) if err != nil { utils.Error(w, http.StatusBadRequest, fmt.Errorf("parsing platform: %w", err)) return } + } else { + defOS, defArch, defVariant, err := util.DefaultPlatform() + if err != nil { + utils.Error(w, http.StatusBadRequest, err) + return + } + lookupImageOptions.OS = defOS + lookupImageOptions.Architecture = defArch + lookupImageOptions.Variant = defVariant } newImage, resolvedName, err := runtime.LibimageRuntime().LookupImage(body.Config.Image, &lookupImageOptions) if err != nil { diff --git a/pkg/api/handlers/compat/images.go b/pkg/api/handlers/compat/images.go index 1cdffdf5eb0..3ab18b2e2d5 100644 --- a/pkg/api/handlers/compat/images.go +++ b/pkg/api/handlers/compat/images.go @@ -12,6 +12,7 @@ import ( "time" "github.com/containers/buildah" + "github.com/containers/buildah/pkg/parse" "github.com/containers/podman/v6/libpod" "github.com/containers/podman/v6/pkg/api/handlers" "github.com/containers/podman/v6/pkg/api/handlers/utils" @@ -222,16 +223,32 @@ func CreateImageFromSrc(w http.ResponseWriter, r *http.Request) { reference = possiblyNormalizedName } - platformSpecs := strings.Split(query.Platform, "/") - opts := entities.ImageImportOptions{ - Source: source, - Changes: query.Changes, - Message: query.Message, - Reference: reference, - OS: platformSpecs[0], + // If no platform was specified in the query, fall back to + // CONTAINER_DEFAULT_PLATFORM env var, then containers.conf platform. + var platOS, platArch, platVariant string + if query.Platform != "" { + var pErr error + platOS, platArch, platVariant, pErr = parse.Platform(query.Platform) + if pErr != nil { + utils.Error(w, http.StatusBadRequest, fmt.Errorf("parsing platform: %w", pErr)) + return + } + } else { + var pErr error + platOS, platArch, platVariant, pErr = util.DefaultPlatform() + if pErr != nil { + utils.Error(w, http.StatusBadRequest, pErr) + return + } } - if len(platformSpecs) > 1 { - opts.Architecture = platformSpecs[1] + opts := entities.ImageImportOptions{ + Source: source, + Changes: query.Changes, + Message: query.Message, + Reference: reference, + OS: platOS, + Architecture: platArch, + Variant: platVariant, } imageEngine := abi.ImageEngine{Libpod: runtime} @@ -310,12 +327,21 @@ func CreateImageFromImage(w http.ResponseWriter, r *http.Request) { } // Handle the platform. - platformSpecs := strings.Split(query.Platform, "/") - pullOptions.OS = platformSpecs[0] // may be empty - if len(platformSpecs) > 1 { - pullOptions.Architecture = platformSpecs[1] - if len(platformSpecs) > 2 { - pullOptions.Variant = platformSpecs[2] + // If no platform was specified in the query, fall back to + // CONTAINER_DEFAULT_PLATFORM env var, then containers.conf platform. + if query.Platform != "" { + var pErr error + pullOptions.OS, pullOptions.Architecture, pullOptions.Variant, pErr = parse.Platform(query.Platform) + if pErr != nil { + utils.Error(w, http.StatusBadRequest, fmt.Errorf("parsing platform: %w", pErr)) + return + } + } else { + var pErr error + pullOptions.OS, pullOptions.Architecture, pullOptions.Variant, pErr = util.DefaultPlatform() + if pErr != nil { + utils.Error(w, http.StatusBadRequest, pErr) + return } } diff --git a/pkg/api/handlers/compat/images_build.go b/pkg/api/handlers/compat/images_build.go index ad9e419f0ae..2ed36f851cc 100644 --- a/pkg/api/handlers/compat/images_build.go +++ b/pkg/api/handlers/compat/images_build.go @@ -776,20 +776,34 @@ func createBuildOptions(query *BuildQuery, buildCtx *BuildContext, queryValues u // Process platforms platforms := query.Platform - if len(platforms) == 1 { - // Docker API uses comma separated platform arg so match this here - platforms = strings.Split(query.Platform[0], ",") - } - for _, platformSpec := range platforms { - os, arch, variant, err := parse.Platform(platformSpec) - if err != nil { - return nil, cleanup, utils.GetBadRequestError("platform", platformSpec, err) + if len(platforms) == 0 || (len(platforms) == 1 && platforms[0] == "") { + // No explicit platform specified; fall back to + // CONTAINER_DEFAULT_PLATFORM env var. + defOS, defArch, defVariant, pErr := util.DefaultPlatform() + if pErr != nil { + return nil, cleanup, utils.GetBadRequestError("platform", "", pErr) + } + if defOS != "" || defArch != "" || defVariant != "" { + buildOptions.Platforms = append(buildOptions.Platforms, struct{ OS, Arch, Variant string }{ + OS: defOS, Arch: defArch, Variant: defVariant, + }) + } + } else { + if len(platforms) == 1 { + // Docker API uses comma separated platform arg so match this here + platforms = strings.Split(platforms[0], ",") + } + for _, platformSpec := range platforms { + os, arch, variant, err := parse.Platform(platformSpec) + if err != nil { + return nil, cleanup, utils.GetBadRequestError("platform", platformSpec, err) + } + buildOptions.Platforms = append(buildOptions.Platforms, struct{ OS, Arch, Variant string }{ + OS: os, + Arch: arch, + Variant: variant, + }) } - buildOptions.Platforms = append(buildOptions.Platforms, struct{ OS, Arch, Variant string }{ - OS: os, - Arch: arch, - Variant: variant, - }) } // Process source policy diff --git a/pkg/api/handlers/libpod/images_pull.go b/pkg/api/handlers/libpod/images_pull.go index c18339a2825..f33bb83d5c2 100644 --- a/pkg/api/handlers/libpod/images_pull.go +++ b/pkg/api/handlers/libpod/images_pull.go @@ -16,6 +16,7 @@ import ( "github.com/containers/podman/v6/pkg/auth" "github.com/containers/podman/v6/pkg/channel" "github.com/containers/podman/v6/pkg/domain/entities" + "github.com/containers/podman/v6/pkg/util" "github.com/gorilla/schema" "github.com/sirupsen/logrus" "go.podman.io/common/libimage" @@ -89,6 +90,19 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) { pullOptions.OS = query.OS pullOptions.Variant = query.Variant + // If no explicit platform fields were given, fall back to + // CONTAINER_DEFAULT_PLATFORM env var. + if query.Arch == "" && query.OS == "" && query.Variant == "" { + defOS, defArch, defVariant, pErr := util.DefaultPlatform() + if pErr != nil { + utils.Error(w, http.StatusBadRequest, pErr) + return + } + pullOptions.OS = defOS + pullOptions.Architecture = defArch + pullOptions.Variant = defVariant + } + if _, found := r.URL.Query()["tlsVerify"]; found { pullOptions.InsecureSkipTLSVerify = types.NewOptionalBool(!query.TLSVerify) } diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 55c84458b13..b36ae6075a5 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -27,6 +27,7 @@ import ( domainUtils "github.com/containers/podman/v6/pkg/domain/utils" "github.com/containers/podman/v6/pkg/errorhandling" "github.com/containers/podman/v6/pkg/rootless" + "github.com/containers/podman/v6/pkg/util" "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" @@ -282,6 +283,18 @@ func (ir *ImageEngine) Unmount(ctx context.Context, nameOrIDs []string, options } func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, options entities.ImagePullOptions) (*entities.ImagePullReport, error) { + // If no explicit platform was requested, fall back to + // CONTAINER_DEFAULT_PLATFORM env var. + if options.Arch == "" && options.OS == "" && options.Variant == "" { + defOS, defArch, defVariant, err := util.DefaultPlatform() + if err != nil { + return nil, err + } + options.OS = defOS + options.Arch = defArch + options.Variant = defVariant + } + pullOptions := &libimage.PullOptions{AllTags: options.AllTags} pullOptions.AuthFilePath = options.Authfile pullOptions.CertDirPath = options.CertDir @@ -512,6 +525,18 @@ func (ir *ImageEngine) Save(ctx context.Context, nameOrID string, tags []string, } func (ir *ImageEngine) Import(ctx context.Context, options entities.ImageImportOptions) (*entities.ImageImportReport, error) { + // If no explicit platform was requested, fall back to + // CONTAINER_DEFAULT_PLATFORM env var. + if options.OS == "" && options.Architecture == "" && options.Variant == "" { + defOS, defArch, defVariant, err := util.DefaultPlatform() + if err != nil { + return nil, err + } + options.OS = defOS + options.Architecture = defArch + options.Variant = defVariant + } + importOptions := &libimage.ImportOptions{} importOptions.Changes = options.Changes importOptions.CommitMessage = options.Message @@ -581,6 +606,20 @@ func (ir *ImageEngine) Config(_ context.Context) (*config.Config, error) { } func (ir *ImageEngine) Build(ctx context.Context, containerFiles []string, opts entities.BuildOptions) (*entities.BuildReport, error) { + // If no explicit platform was requested, fall back to + // CONTAINER_DEFAULT_PLATFORM env var. + if len(opts.Platforms) == 0 { + defOS, defArch, defVariant, err := util.DefaultPlatform() + if err != nil { + return nil, err + } + if defOS != "" || defArch != "" || defVariant != "" { + opts.Platforms = append(opts.Platforms, struct{ OS, Arch, Variant string }{ + OS: defOS, Arch: defArch, Variant: defVariant, + }) + } + } + id, _, err := ir.Libpod.Build(ctx, opts.BuildOptions, containerFiles...) if err != nil { return nil, err diff --git a/pkg/util/utils.go b/pkg/util/utils.go index 99d09ea7d38..b404fce5601 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -16,6 +16,7 @@ import ( "syscall" "time" + "github.com/containers/buildah/pkg/parse" "github.com/containers/podman/v6/libpod/define" "github.com/containers/podman/v6/pkg/namespaces" "github.com/containers/podman/v6/pkg/rootless" @@ -147,6 +148,24 @@ func ParseRegistryCreds(creds string) (*types.DockerAuthConfig, error) { }, nil } +// DefaultPlatform returns the default platform (platOS, arch, variant) from +// the CONTAINER_DEFAULT_PLATFORM environment variable. If the variable is +// unset or empty, empty strings and a nil error are returned (meaning the +// host's native platform should be used). The platform string is always +// parsed through buildah's parse.Platform() so that validation and +// normalisation are consistent across all call sites. +func DefaultPlatform() (platOS, arch, variant string, err error) { + platform := os.Getenv("CONTAINER_DEFAULT_PLATFORM") + if platform == "" { + return "", "", "", nil + } + platOS, arch, variant, err = parse.Platform(platform) + if err != nil { + return "", "", "", fmt.Errorf("parsing default platform %q: %w", platform, err) + } + return platOS, arch, variant, nil +} + // StringMatchRegexSlice determines if a given string matches one of the given regexes, returns bool func StringMatchRegexSlice(s string, re []string) bool { for _, r := range re { diff --git a/test/system/010-images.bats b/test/system/010-images.bats index ef25f345d7c..5cec68d4197 100644 --- a/test/system/010-images.bats +++ b/test/system/010-images.bats @@ -450,5 +450,24 @@ EOF wait } +# bats test_tags=ci:parallel +@test "podman pull - CONTAINER_DEFAULT_PLATFORM" { + # Get the host architecture so we can verify the env var is being used. + run_podman info --format '{{.Host.Arch}}' + host_arch="$output" + + # CONTAINER_DEFAULT_PLATFORM set to the host arch — pull should succeed. + CONTAINER_DEFAULT_PLATFORM="linux/${host_arch}" \ + run_podman pull -q --policy=always $IMAGE + CONTAINER_DEFAULT_PLATFORM="linux/${host_arch}" \ + run_podman inspect --format '{{.Architecture}}' $IMAGE + is "$output" "$host_arch" "CONTAINER_DEFAULT_PLATFORM sets image arch" + + # Invalid platform value should produce an error. + CONTAINER_DEFAULT_PLATFORM="not/a/valid/platform/string" \ + run_podman 125 pull $IMAGE + assert "$output" =~ "parsing default platform" "invalid CONTAINER_DEFAULT_PLATFORM" +} + # vim: filetype=sh