diff --git a/cmd/podman/common/build.go b/cmd/podman/common/build.go index bd79b8b960..d51358707c 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 8f8aebb82d..00eb81c695 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 8ff7492d7f..647ee54df0 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 ff1f86ded0..7a24e9d87b 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 e4905a138d..e6a56bd41d 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 1fb92b212f..cf6da24be9 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 1cdffdf5eb..3ab18b2e2d 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 ad9e419f0a..2ed36f851c 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 c18339a282..f33bb83d5c 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 55c84458b1..b36ae6075a 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 99d09ea7d3..b404fce560 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 ef25f345d7..5cec68d419 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