diff --git a/cmd/podman/containers/create.go b/cmd/podman/containers/create.go index a285d7d12c1..7a1181eb21e 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,11 +353,13 @@ 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 } } diff --git a/cmd/podman/images/pull.go b/cmd/podman/images/pull.go index 4f91fd555d9..1ffae75b1ae 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,9 @@ 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) } } diff --git a/docs/source/markdown/options/platform.md b/docs/source/markdown/options/platform.md index ff1f86ded06..8c7905a705d 100644 --- a/docs/source/markdown/options/platform.md +++ b/docs/source/markdown/options/platform.md @@ -7,3 +7,8 @@ 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 **platform** setting in **containers.conf**(5). +3. 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..7288aea2f5e 100644 --- a/docs/source/markdown/podman-build.1.md.in +++ b/docs/source/markdown/podman-build.1.md.in @@ -319,6 +319,11 @@ 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 **platform** setting in **containers.conf**(5). +3. 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 9a73e5b01cb..7835ac3c1e6 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/docker/docker/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(rtc.Engine.Platform) + 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 564d53dbb01..d9af2b54d1d 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,37 @@ 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 { + rtc, err := runtime.GetConfig() + confPlatform := "" + if err == nil { + confPlatform = rtc.Engine.Platform + } + var pErr error + platOS, platArch, platVariant, pErr = util.DefaultPlatform(confPlatform) + 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 +332,26 @@ 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 { + rtc, err := runtime.GetConfig() + confPlatform := "" + if err == nil { + confPlatform = rtc.Engine.Platform + } + var pErr error + pullOptions.OS, pullOptions.Architecture, pullOptions.Variant, pErr = util.DefaultPlatform(confPlatform) + 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..2cb4f1be169 100644 --- a/pkg/api/handlers/compat/images_build.go +++ b/pkg/api/handlers/compat/images_build.go @@ -776,20 +776,39 @@ 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, then containers.conf platform. + runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) + confPlatform := "" + if rtc, err := runtime.GetConfigNoCopy(); err == nil { + confPlatform = rtc.Engine.Platform + } + defOS, defArch, defVariant, pErr := util.DefaultPlatform(confPlatform) + if pErr != nil { + return nil, cleanup, utils.GetBadRequestError("platform", confPlatform, 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 11b031e0fa0..9fb8201bdad 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" @@ -75,6 +76,23 @@ 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, then containers.conf platform. + if query.Arch == "" && query.OS == "" && query.Variant == "" { + confPlatform := "" + if rtc, err := runtime.GetConfig(); err == nil { + confPlatform = rtc.Engine.Platform + } + defOS, defArch, defVariant, pErr := util.DefaultPlatform(confPlatform) + 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 cacb4b90c73..2e851deddc1 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,22 @@ 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 + // containers.conf [engine] platform (resolved server-side). + if options.Arch == "" && options.OS == "" && options.Variant == "" { + rtc, err := ir.Libpod.GetConfigNoCopy() + if err != nil { + return nil, err + } + defOS, defArch, defVariant, err := util.DefaultPlatform(rtc.Engine.Platform) + 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 +529,22 @@ 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 + // containers.conf [engine] platform (resolved server-side). + if options.OS == "" && options.Architecture == "" && options.Variant == "" { + rtc, err := ir.Libpod.GetConfigNoCopy() + if err != nil { + return nil, err + } + defOS, defArch, defVariant, err := util.DefaultPlatform(rtc.Engine.Platform) + 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 +614,24 @@ 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 + // containers.conf [engine] platform (resolved server-side). + if len(opts.Platforms) == 0 { + rtc, err := ir.Libpod.GetConfigNoCopy() + if err != nil { + return nil, err + } + defOS, defArch, defVariant, err := util.DefaultPlatform(rtc.Engine.Platform) + 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 56ffbddce15..f2de4c5ea49 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" @@ -143,6 +144,28 @@ func ParseRegistryCreds(creds string) (*types.DockerAuthConfig, error) { }, nil } +// DefaultPlatform returns the default platform (platOS, arch, variant) from +// the CONTAINER_DEFAULT_PLATFORM environment variable or the given +// containers.conf Engine.Platform value. The confPlatform parameter should +// be the caller's config.Engine.Platform string. If neither source provides +// a value, empty strings and a nil error are returned. The platform string +// is always parsed through buildah's parse.Platform() so that validation and +// normalisation are consistent across all call sites. +func DefaultPlatform(confPlatform string) (platOS, arch, variant string, err error) { + platform, ok := os.LookupEnv("CONTAINER_DEFAULT_PLATFORM") + if !ok || platform == "" { + platform = confPlatform + } + 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..4ab8ef34dd2 100644 --- a/test/system/010-images.bats +++ b/test/system/010-images.bats @@ -450,5 +450,49 @@ EOF wait } +# bats test_tags=ci:parallel +@test "podman pull - containers.conf default platform" { + skip_if_remote "remote does not support CONTAINERS_CONF_OVERRIDE" + + # Get the host architecture so we can set it explicitly via containers.conf + # and verify it's being used (the pull should succeed with matching arch). + run_podman info --format '{{.Host.Arch}}' + host_arch="$output" + + containersconf=$PODMAN_TMPDIR/containers.conf + cat >$containersconf <$containersconf <$containersconf <