//===----------------------------------------------------------------------===// // Copyright © 2026 Apple Inc. and the Containerization 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. //===----------------------------------------------------------------------===// #if os(macOS) import Containerization import ContainerizationArchive import ContainerizationError import ContainerizationExtras import Foundation import Testing @testable import Containerization private struct NilGatewayInterface: Interface { let ipv4Address: CIDRv4 let ipv4Gateway: IPv4Address? = nil let macAddress: MACAddress? = nil init() { self.ipv4Address = try! CIDRv4("192.168.64.2/24") } } private struct NilGatewayNetwork: Network { mutating func createInterface(_ id: String) throws -> Interface? { NilGatewayInterface() } mutating func releaseInterface(_ id: String) throws {} } @Suite struct ContainerManagerTests { @Test func testCreateThrowsWhenGatewayMissing() async throws { let fm = FileManager.default let root = fm.uniqueTemporaryDirectory(create: true) defer { try? fm.removeItem(at: root) } let kernelPath = root.appendingPathComponent("vmlinux") fm.createFile(atPath: kernelPath.path, contents: Data(), attributes: nil) let initfsPath = root.appendingPathComponent("initfs.ext4") fm.createFile(atPath: initfsPath.path, contents: Data(), attributes: nil) let kernel = Kernel(path: kernelPath, platform: .linuxArm) let initfs = Mount.block(format: "ext4", source: initfsPath.path, destination: "/") var manager = try ContainerManager( kernel: kernel, initfs: initfs, root: root, network: NilGatewayNetwork() ) let tempDir = fm.uniqueTemporaryDirectory() defer { try? fm.removeItem(at: tempDir) } let tarPath = Foundation.Bundle.module.url(forResource: "scratch", withExtension: "tar")! let reader = try ArchiveReader(format: .pax, filter: .none, file: tarPath) let rejectedPaths = try reader.extractContents(to: tempDir) #expect(rejectedPaths.isEmpty) let images = try await manager.imageStore.load(from: tempDir) let image = images.first! let rootfsPath = root.appendingPathComponent("rootfs.ext4") fm.createFile(atPath: rootfsPath.path, contents: Data(), attributes: nil) let rootfs = Mount.block(format: "ext4", source: rootfsPath.path, destination: "/") do { _ = try await manager.create("test-nil-gateway", image: image, rootfs: rootfs) { _ in } #expect(Bool(false), "expected invalidState error for missing ipv4 gateway") } catch let error as ContainerizationError { #expect(error.code == .invalidState) #expect(error.message.contains("missing ipv4 gateway")) } catch { #expect(Bool(false), "unexpected error: \(error)") } } @Test func testNetworkingFalseSkipsInterfaceCreation() async throws { let fm = FileManager.default let root = fm.uniqueTemporaryDirectory(create: true) defer { try? fm.removeItem(at: root) } let kernelPath = root.appendingPathComponent("vmlinux") fm.createFile(atPath: kernelPath.path, contents: Data(), attributes: nil) let initfsPath = root.appendingPathComponent("initfs.ext4") fm.createFile(atPath: initfsPath.path, contents: Data(), attributes: nil) let kernel = Kernel(path: kernelPath, platform: .linuxArm) let initfs = Mount.block(format: "ext4", source: initfsPath.path, destination: "/") // Use NilGatewayNetwork — with networking: true this would throw invalidState, // but with networking: false the network's createInterface() is never called. var manager = try ContainerManager( kernel: kernel, initfs: initfs, root: root, network: NilGatewayNetwork() ) let tempDir = fm.uniqueTemporaryDirectory() defer { try? fm.removeItem(at: tempDir) } let tarPath = Foundation.Bundle.module.url(forResource: "scratch", withExtension: "tar")! let reader = try ArchiveReader(format: .pax, filter: .none, file: tarPath) let rejectedPaths = try reader.extractContents(to: tempDir) #expect(rejectedPaths.isEmpty) let images = try await manager.imageStore.load(from: tempDir) let image = images.first! let rootfsPath = root.appendingPathComponent("rootfs.ext4") fm.createFile(atPath: rootfsPath.path, contents: Data(), attributes: nil) let rootfs = Mount.block(format: "ext4", source: rootfsPath.path, destination: "/") // With networking: false, NilGatewayNetwork.createInterface() is never called, // so we should not get the "missing ipv4 gateway" error. // The container creation will fail for other reasons (dummy VMM), but the // configuration closure should see empty interfaces. var closureWasCalled = false do { _ = try await manager.create( "test-no-networking", image: image, rootfs: rootfs, networking: false ) { config in closureWasCalled = true #expect(config.interfaces.isEmpty) #expect(config.dns == nil) } } catch { // Container creation may fail due to dummy kernel/VMM — that's expected. // The key assertion is in the configuration closure above. let description = String(describing: error) #expect(!description.contains("missing ipv4 gateway")) } #expect(closureWasCalled, "configuration closure must be invoked to validate interfaces") } } #endif