Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions Sources/ContainerCommands/Image/ImagePull.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import ContainerAPIClient
import ContainerPersistence
import ContainerPlugin
import Containerization
import ContainerizationError
import ContainerizationOCI
import Foundation
import Logging
import TerminalProgress

extension Application {
Expand Down Expand Up @@ -50,7 +53,7 @@ extension Application {
var os: String?

@Option(
help: "Limit the pull to the specified platform (format: os/arch[/variant], takes precedence over --os and --arch) [environment: CONTAINER_DEFAULT_PLATFORM]"
help: "Limit the pull to a platform (os/arch[/variant]; 'all' unpacks every platform; takes precedence over --os and --arch) [environment: CONTAINER_DEFAULT_PLATFORM]"
)
var platform: String?

Expand All @@ -70,7 +73,7 @@ extension Application {

public func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try await ClientHealthCheck.loadContainerSystemConfig()
let p = try DefaultPlatform.resolve(platform: platform, os: os, arch: arch, log: log)
let selection = try Self.resolvePlatforms(platform: platform, os: os, arch: arch, log: log)

let scheme = try RequestScheme(registry.scheme)

Expand All @@ -94,17 +97,49 @@ extension Application {
let taskManager = ProgressTaskCoordinator()
let fetchTask = await taskManager.startTask()
let image = try await ClientImage.pull(
reference: processedReference, platform: p, scheme: scheme, containerSystemConfig: containerSystemConfig,
reference: processedReference, platform: selection.fetch, scheme: scheme, containerSystemConfig: containerSystemConfig,
progressUpdate: ProgressTaskCoordinator.handler(for: fetchTask, from: progress.handler),
maxConcurrentDownloads: self.imageFetchFlags.maxConcurrentDownloads
)

progress.set(description: "Unpacking image")
progress.set(itemsName: "entries")
let unpackTask = await taskManager.startTask()
try await image.unpack(platform: p, progressUpdate: ProgressTaskCoordinator.handler(for: unpackTask, from: progress.handler))
try await image.unpack(platform: selection.unpack, progressUpdate: ProgressTaskCoordinator.handler(for: unpackTask, from: progress.handler))
await taskManager.finish()
progress.finish()
}

struct PlatformSelection {
let fetch: ContainerizationOCI.Platform?
let unpack: ContainerizationOCI.Platform?
}

/// Resolves the platforms used for fetching and unpacking.
///
/// `--platform all` fetches every platform and unpacks every platform,
/// restoring the previous behavior. Otherwise the fetch uses the
/// requested platform (or all platforms when none is requested, so
/// blobs remain available offline), and unpacking defaults to the host
/// platform to avoid one snapshot per architecture.
static func resolvePlatforms(
platform: String?,
os: String?,
arch: String?,
environment: [String: String] = ProcessInfo.processInfo.environment,
log: Logger? = nil
) throws -> PlatformSelection {
if platform?.lowercased() == "all" {
guard os == nil, arch == nil else {
throw ContainerizationError(
.invalidArgument,
message: "--platform all cannot be combined with --os or --arch"
)
}
return PlatformSelection(fetch: nil, unpack: nil)
}
let resolved = try DefaultPlatform.resolve(platform: platform, os: os, arch: arch, environment: environment, log: log)
return PlatformSelection(fetch: resolved, unpack: resolved ?? .current)
}
}
}
123 changes: 123 additions & 0 deletions Tests/ContainerCommandsTests/ImagePullPlatformTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//

import ContainerizationError
import ContainerizationOCI
import Testing

@testable import ContainerCommands

struct ImagePullPlatformTests {
@Test func noFlagsFetchesAllAndUnpacksHost() throws {
let selection = try Application.ImagePull.resolvePlatforms(
platform: nil,
os: nil,
arch: nil,
environment: [:]
)
#expect(selection.fetch == nil)
#expect(selection.unpack?.os == Platform.current.os)
#expect(selection.unpack?.architecture == Platform.current.architecture)
}

@Test func allFetchesAndUnpacksEveryPlatform() throws {
let selection = try Application.ImagePull.resolvePlatforms(
platform: "all",
os: nil,
arch: nil,
environment: [:]
)
#expect(selection.fetch == nil)
#expect(selection.unpack == nil)
}

@Test func allIsCaseInsensitive() throws {
let selection = try Application.ImagePull.resolvePlatforms(
platform: "ALL",
os: nil,
arch: nil,
environment: [:]
)
#expect(selection.fetch == nil)
#expect(selection.unpack == nil)
}

@Test func allRejectsOSOrArchOverrides() {
#expect {
_ = try Application.ImagePull.resolvePlatforms(
platform: "all",
os: "linux",
arch: nil,
environment: [:]
)
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("--platform all")
}
#expect {
_ = try Application.ImagePull.resolvePlatforms(
platform: "all",
os: nil,
arch: "arm64",
environment: [:]
)
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("--platform all")
}
}

@Test func explicitPlatformIsUsedForFetchAndUnpack() throws {
let selection = try Application.ImagePull.resolvePlatforms(
platform: "linux/amd64",
os: nil,
arch: nil,
environment: [:]
)
#expect(selection.fetch?.os == "linux")
#expect(selection.fetch?.architecture == "amd64")
#expect(selection.unpack?.os == "linux")
#expect(selection.unpack?.architecture == "amd64")
}

@Test func environmentPlatformIsUsedForFetchAndUnpack() throws {
let selection = try Application.ImagePull.resolvePlatforms(
platform: nil,
os: nil,
arch: nil,
environment: ["CONTAINER_DEFAULT_PLATFORM": "linux/amd64"]
)
#expect(selection.fetch?.architecture == "amd64")
#expect(selection.unpack?.architecture == "amd64")
}

@Test func osAndArchFlagsAreUsedForFetchAndUnpack() throws {
let selection = try Application.ImagePull.resolvePlatforms(
platform: nil,
os: "linux",
arch: "arm64",
environment: [:]
)
#expect(selection.fetch?.os == "linux")
#expect(selection.fetch?.architecture == "arm64")
#expect(selection.unpack?.os == "linux")
#expect(selection.unpack?.architecture == "arm64")
}
}
2 changes: 1 addition & 1 deletion docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ container image pull [--scheme <scheme>] [--progress <type>] [--max-concurrent-d
* `--max-concurrent-downloads <max-concurrent-downloads>`: Maximum number of concurrent downloads (default: 3)
* `-a, --arch <arch>`: Limit the pull to the specified architecture
* `--os <os>`: Limit the pull to the specified OS
* `--platform <platform>`: Limit the pull to the specified platform (format: os/arch[/variant], takes precedence over --os and --arch)
* `--platform <platform>`: Limit the pull to the specified platform (format: os/arch[/variant], takes precedence over --os and --arch). Use `--platform all` to unpack every platform in the image. By default only the host platform is unpacked, to avoid storing a snapshot for each architecture; other platforms are unpacked on demand when run.

### `container image push`

Expand Down