mirror of
https://github.com/apple/container.git
synced 2026-09-15 20:25:46 +00:00
- Discussion topic #1336. - This change migrates away from using `UserDefaults`, instead providing a TOML configuration mechanism for user configurable settings. All existing system property settings keys are supported in the new configuration file. However, users will have to migrate any settings they have configured in the `UserDefaults` into TOML for these settings to take effect. - Breaking changes: * `container system property get` is removed in favor of users directly utilizing `container system property list --format toml | jq<>`. * `container system property set` is removed since the TOML configuration is effectively immutable during the lifetime of the `container` daemon. Uses can edit the TOML they have in their home directory, however no changes will take effect until the daemon is restarted via `container system stop && container system start` * `container system property list --format table` is removed as generating tabular format is non-trivial and the new TOML format is intended to be human readable
305 lines
12 KiB
Swift
305 lines
12 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025-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 ContainerAPIClient
|
|
import ContainerizationExtras
|
|
import Foundation
|
|
import Testing
|
|
|
|
/// Tests that need total control over environment to avoid conflicts.
|
|
@Suite(.serialSuites, .serialized)
|
|
class TestCLINoParallelCases: CLITest {
|
|
func getTestName() -> String {
|
|
Test.current!.name.trimmingCharacters(in: ["(", ")"]).lowercased()
|
|
}
|
|
|
|
func getLowercasedTestName() -> String {
|
|
getTestName().lowercased()
|
|
}
|
|
|
|
@Test func testImageSingleConcurrentDownload() throws {
|
|
// removing this image during parallel tests breaks stuff!
|
|
_ = try? run(arguments: ["image", "rm", alpine])
|
|
defer { _ = try? run(arguments: ["image", "rm", "--all"]) }
|
|
do {
|
|
try doPull(imageName: alpine, args: ["--max-concurrent-downloads", "1"])
|
|
let imagePresent = try isImagePresent(targetImage: alpine)
|
|
#expect(imagePresent, "Expected image to be pulled with maxConcurrentDownloads=1")
|
|
} catch {
|
|
Issue.record("failed to pull image with maxConcurrentDownloads flag: \(error)")
|
|
return
|
|
}
|
|
}
|
|
|
|
@Test func testImageManyConcurrentDownloads() throws {
|
|
// removing this image during parallel tests breaks stuff!
|
|
_ = try? run(arguments: ["image", "rm", alpine])
|
|
defer { _ = try? run(arguments: ["image", "rm", "--all"]) }
|
|
do {
|
|
try doPull(imageName: alpine, args: ["--max-concurrent-downloads", "64"])
|
|
let imagePresent = try isImagePresent(targetImage: alpine)
|
|
#expect(imagePresent, "Expected image to be pulled with maxConcurrentDownloads=64")
|
|
} catch {
|
|
Issue.record("failed to pull image with maxConcurrentDownloads flag: \(error)")
|
|
return
|
|
}
|
|
}
|
|
|
|
@Test func testImagePruneNoImages() throws {
|
|
// Prune with no images should succeed
|
|
_ = try? run(arguments: ["image", "rm", "--all"])
|
|
let (_, output, error, status) = try run(arguments: ["image", "prune"])
|
|
if status != 0 {
|
|
throw CLIError.executionFailed("image prune failed: \(error)")
|
|
}
|
|
|
|
#expect(output.contains("Zero KB"), "should show no space reclaimed")
|
|
}
|
|
|
|
@Test func testImagePruneUnusedImages() throws {
|
|
// 1. Pull the images
|
|
_ = try? run(arguments: ["image", "rm", "--all"])
|
|
defer { _ = try? run(arguments: ["image", "rm", "--all"]) }
|
|
try doPull(imageName: alpine)
|
|
try doPull(imageName: busybox)
|
|
|
|
// 2. Verify the images are present
|
|
let alpinePresent = try isImagePresent(targetImage: alpine)
|
|
#expect(alpinePresent, "expected to see image \(alpine) pulled")
|
|
let busyBoxPresent = try isImagePresent(targetImage: busybox)
|
|
#expect(busyBoxPresent, "expected to see image \(busybox) pulled")
|
|
|
|
// 3. Prune with the -a flag should remove all unused images
|
|
let (_, output, error, status) = try run(arguments: ["image", "prune", "-a"])
|
|
if status != 0 {
|
|
throw CLIError.executionFailed("image prune failed: \(error)")
|
|
}
|
|
#expect(output.contains(alpine), "should prune alpine image")
|
|
#expect(output.contains(busybox), "should prune busybox image")
|
|
|
|
// 4. Verify the images are gone
|
|
let alpineRemoved = try !isImagePresent(targetImage: alpine)
|
|
#expect(alpineRemoved, "expected image \(alpine) to be removed")
|
|
let busyboxRemoved = try !isImagePresent(targetImage: busybox)
|
|
#expect(busyboxRemoved, "expected image \(busybox) to be removed")
|
|
}
|
|
|
|
@Test func testImagePruneDanglingImages() throws {
|
|
let name = getTestName()
|
|
let containerName = "\(name)_container"
|
|
|
|
// 1. Pull the images
|
|
_ = try? run(arguments: ["image", "rm", "--all"])
|
|
defer { _ = try? run(arguments: ["image", "rm", "--all"]) }
|
|
_ = try? run(arguments: ["rm", "--all", "--force"])
|
|
defer { _ = try? run(arguments: ["rm", "--all", "--force"]) }
|
|
try doPull(imageName: alpine)
|
|
try doPull(imageName: busybox)
|
|
|
|
// 2. Verify the images are present
|
|
let alpinePresent = try isImagePresent(targetImage: alpine)
|
|
#expect(alpinePresent, "expected to see image \(alpine) pulled")
|
|
let busyBoxPresent = try isImagePresent(targetImage: busybox)
|
|
#expect(busyBoxPresent, "expected to see image \(busybox) pulled")
|
|
|
|
// 3. Create a running container based on alpine
|
|
try doLongRun(
|
|
name: containerName,
|
|
image: alpine
|
|
)
|
|
try waitForContainerRunning(containerName)
|
|
|
|
// 4. Prune should only remove the dangling image
|
|
let (_, output, error, status) = try run(arguments: ["image", "prune", "-a"])
|
|
if status != 0 {
|
|
throw CLIError.executionFailed("image prune failed: \(error)")
|
|
}
|
|
#expect(output.contains(busybox), "should prune busybox image")
|
|
|
|
// 5. Verify the busybox image is gone
|
|
let busyboxRemoved = try !isImagePresent(targetImage: busybox)
|
|
#expect(busyboxRemoved, "expected image \(busybox) to be removed")
|
|
|
|
// 6. Verify the alpine image still exists
|
|
let alpineStillPresent = try isImagePresent(targetImage: alpine)
|
|
#expect(alpineStillPresent, "expected image \(alpine) to remain")
|
|
}
|
|
|
|
@available(macOS 26, *)
|
|
@Test func testNetworkPruneNoNetworks() throws {
|
|
// Ensure the testnetworkcreateanduse network is deleted
|
|
// Clean up is necessary for testing prune with no networks
|
|
doNetworkDeleteIfExists(name: "testnetworkcreateanduse")
|
|
|
|
// Prune with no networks should succeed
|
|
let (_, _, _, statusBefore) = try run(arguments: ["network", "list", "--quiet"])
|
|
#expect(statusBefore == 0)
|
|
let (_, output, error, status) = try run(arguments: ["network", "prune"])
|
|
if status != 0 {
|
|
throw CLIError.executionFailed("network prune failed: \(error)")
|
|
}
|
|
|
|
#expect(output.isEmpty, "should show no networks pruned")
|
|
}
|
|
|
|
@available(macOS 26, *)
|
|
@Test func testNetworkPruneUnusedNetworks() throws {
|
|
let name = getTestName()
|
|
let network1 = "\(name)_1"
|
|
let network2 = "\(name)_2"
|
|
|
|
// Clean up any existing resources from previous runs
|
|
doNetworkDeleteIfExists(name: network1)
|
|
doNetworkDeleteIfExists(name: network2)
|
|
|
|
defer {
|
|
doNetworkDeleteIfExists(name: network1)
|
|
doNetworkDeleteIfExists(name: network2)
|
|
}
|
|
|
|
try doNetworkCreate(name: network1)
|
|
try doNetworkCreate(name: network2)
|
|
|
|
// Verify networks are created
|
|
let (_, listBefore, _, statusBefore) = try run(arguments: ["network", "list", "--quiet"])
|
|
#expect(statusBefore == 0)
|
|
#expect(listBefore.contains(network1))
|
|
#expect(listBefore.contains(network2))
|
|
|
|
// Prune should remove both
|
|
let (_, output, error, status) = try run(arguments: ["network", "prune"])
|
|
if status != 0 {
|
|
throw CLIError.executionFailed("network prune failed: \(error)")
|
|
}
|
|
|
|
#expect(output.contains(network1), "should prune network1")
|
|
#expect(output.contains(network2), "should prune network2")
|
|
|
|
// Verify networks are gone
|
|
let (_, listAfter, _, statusAfter) = try run(arguments: ["network", "list", "--quiet"])
|
|
#expect(statusAfter == 0)
|
|
#expect(!listAfter.contains(network1), "network1 should be pruned")
|
|
#expect(!listAfter.contains(network2), "network2 should be pruned")
|
|
}
|
|
|
|
@available(macOS 26, *)
|
|
@Test func testNetworkPruneSkipsNetworksInUse() throws {
|
|
let name = getTestName()
|
|
let containerName = "\(name)_c1"
|
|
let networkInUse = "\(name)_inuse"
|
|
let networkUnused = "\(name)_unused"
|
|
|
|
// Clean up any existing resources from previous runs
|
|
try? doStop(name: containerName)
|
|
try? doRemove(name: containerName)
|
|
doNetworkDeleteIfExists(name: networkInUse)
|
|
doNetworkDeleteIfExists(name: networkUnused)
|
|
|
|
defer {
|
|
try? doStop(name: containerName)
|
|
try? doRemove(name: containerName)
|
|
doNetworkDeleteIfExists(name: networkInUse)
|
|
doNetworkDeleteIfExists(name: networkUnused)
|
|
}
|
|
|
|
try doNetworkCreate(name: networkInUse)
|
|
try doNetworkCreate(name: networkUnused)
|
|
|
|
// Verify networks are created
|
|
let (_, listBefore, _, statusBefore) = try run(arguments: ["network", "list", "--quiet"])
|
|
#expect(statusBefore == 0)
|
|
#expect(listBefore.contains(networkInUse))
|
|
#expect(listBefore.contains(networkUnused))
|
|
|
|
// Creation of container with network connection
|
|
let port = UInt16.random(in: 50000..<60000)
|
|
try doLongRun(
|
|
name: containerName,
|
|
image: "docker.io/library/python:alpine",
|
|
args: ["--network", networkInUse],
|
|
containerArgs: ["python3", "-m", "http.server", "--bind", "0.0.0.0", "\(port)"]
|
|
)
|
|
try waitForContainerRunning(containerName)
|
|
let container = try inspectContainer(containerName)
|
|
#expect(container.networks.count > 0)
|
|
|
|
// Prune should only remove the unused network
|
|
let (_, _, error, status) = try run(arguments: ["network", "prune"])
|
|
if status != 0 {
|
|
throw CLIError.executionFailed("network prune failed: \(error)")
|
|
}
|
|
|
|
// Verify in-use network still exists
|
|
let (_, listAfter, _, statusAfter) = try run(arguments: ["network", "list", "--quiet"])
|
|
#expect(statusAfter == 0)
|
|
#expect(listAfter.contains(networkInUse), "network in use should NOT be pruned")
|
|
#expect(!listAfter.contains(networkUnused), "unused network should be pruned")
|
|
}
|
|
|
|
@available(macOS 26, *)
|
|
@Test func testNetworkPruneSkipsNetworkAttachedToStoppedContainer() async throws {
|
|
let name = getTestName()
|
|
let containerName = "\(name)_c1"
|
|
let networkName = "\(name)"
|
|
|
|
// Clean up any existing resources from previous runs
|
|
try? doStop(name: containerName)
|
|
try? doRemove(name: containerName)
|
|
doNetworkDeleteIfExists(name: networkName)
|
|
|
|
defer {
|
|
try? doStop(name: containerName)
|
|
try? doRemove(name: containerName)
|
|
doNetworkDeleteIfExists(name: networkName)
|
|
}
|
|
|
|
try doNetworkCreate(name: networkName)
|
|
|
|
// Creation of container with network connection
|
|
let port = UInt16.random(in: 50000..<60000)
|
|
try doLongRun(
|
|
name: containerName,
|
|
image: "docker.io/library/python:alpine",
|
|
args: ["--network", networkName],
|
|
containerArgs: ["python3", "-m", "http.server", "--bind", "0.0.0.0", "\(port)"]
|
|
)
|
|
try await Task.sleep(for: .seconds(1))
|
|
|
|
// Prune should NOT remove the network (container exists, even if stopped)
|
|
let (_, _, error, status) = try run(arguments: ["network", "prune"])
|
|
if status != 0 {
|
|
throw CLIError.executionFailed("network prune failed: \(error)")
|
|
}
|
|
|
|
let (_, listAfter, _, statusAfter) = try run(arguments: ["network", "list", "--quiet"])
|
|
#expect(statusAfter == 0)
|
|
#expect(listAfter.contains(networkName), "network attached to stopped container should NOT be pruned")
|
|
|
|
try? doStop(name: containerName)
|
|
try? doRemove(name: containerName)
|
|
|
|
let (_, _, error2, status2) = try run(arguments: ["network", "prune"])
|
|
if status2 != 0 {
|
|
throw CLIError.executionFailed("network prune failed: \(error2)")
|
|
}
|
|
|
|
// Verify network is gone
|
|
let (_, listFinal, _, statusFinal) = try run(arguments: ["network", "list", "--quiet"])
|
|
#expect(statusFinal == 0)
|
|
#expect(!listFinal.contains(networkName), "network should be pruned after container is deleted")
|
|
}
|
|
}
|