From 9222c8e68b456218dd9bb81f4b3ea9889845eccf Mon Sep 17 00:00:00 2001 From: Sara Sandoval Date: Tue, 2 Dec 2025 19:16:45 -0600 Subject: [PATCH 01/15] sara push 1 --- Sources/ContainerClient/Flags.swift | 8 ++ Sources/ContainerClient/Parser.swift | 10 ++- Sources/ContainerClient/Utility.swift | 21 +++++ Sources/ContainerCommands/BuildCommand.swift | 19 ++++- .../Builder/BuilderStart.swift | 84 +++++++++++++++++-- .../System/Property/PropertySet.swift | 6 ++ .../ContainerPersistence/DefaultsStore.swift | 16 ++++ 7 files changed, 155 insertions(+), 9 deletions(-) diff --git a/Sources/ContainerClient/Flags.swift b/Sources/ContainerClient/Flags.swift index 939a5504..20897508 100644 --- a/Sources/ContainerClient/Flags.swift +++ b/Sources/ContainerClient/Flags.swift @@ -74,6 +74,14 @@ public struct Flags { help: "Amount of memory (1MiByte granularity), with optional K, M, G, T, or P suffix" ) public var memory: String? + + // sara + @Option( + name: .shortAndLong, + help: "Disk capacity / storage size for the container (e.g. 512GB, 1TB)" + ) + public var storage: String? +// sara done } public struct Registry: ParsableArguments { diff --git a/Sources/ContainerClient/Parser.swift b/Sources/ContainerClient/Parser.swift index 4415b933..bbf6f04b 100644 --- a/Sources/ContainerClient/Parser.swift +++ b/Sources/ContainerClient/Parser.swift @@ -84,7 +84,8 @@ public struct Parser { try .init(from: platform) } - public static func resources(cpus: Int64?, memory: String?) throws -> ContainerConfiguration.Resources { + // sara added storage below + public static func resources(cpus: Int64?, memory: String?, storage: String?) throws -> ContainerConfiguration.Resources { var resource = ContainerConfiguration.Resources() if let cpus { resource.cpus = Int(cpus) @@ -92,6 +93,13 @@ public struct Parser { if let memory { resource.memoryInBytes = try Parser.memoryString(memory).mib() } + // sara start + if let storage { + // parse "512GB", "1TB", etc. into bytes + let storageInMiB = try Parser.memoryString(storage) + resource.storage = UInt64(storageInMiB.mib()) + } + // sara done return resource } diff --git a/Sources/ContainerClient/Utility.swift b/Sources/ContainerClient/Utility.swift index 16f43398..053f7461 100644 --- a/Sources/ContainerClient/Utility.swift +++ b/Sources/ContainerClient/Utility.swift @@ -165,9 +165,30 @@ public struct Utility { var config = ContainerConfiguration(id: id, image: description, process: pc) config.platform = requestedPlatform + // sara + let effectiveStorage: String? = { + if let storage = resource.storage { + return storage + } + if let defaultStorage: String = DefaultsStore.getOptional(key: .defaultContainerStorage) { + return defaultStorage + } + return nil + }() + + config.resources = try Parser.resources( + cpus: resource.cpus, + memory: resource.memory, + storage: effectiveStorage + ) + // sara + config.resources = try Parser.resources( cpus: resource.cpus, memory: resource.memory + // sara + storage: resource.storage + // done ) let tmpfs = try Parser.tmpfsMounts(management.tmpFs) diff --git a/Sources/ContainerCommands/BuildCommand.swift b/Sources/ContainerCommands/BuildCommand.swift index d0f2afc3..25c7d778 100644 --- a/Sources/ContainerCommands/BuildCommand.swift +++ b/Sources/ContainerCommands/BuildCommand.swift @@ -81,6 +81,14 @@ extension Application { ) var memory: String = "2048MB" + // sara here + @Option( + name: .long, + help: "Disk capacity for the builder container (e.g. 512GB, 1TB)" + ) + var storage: String? + // sara done + @Flag(name: .long, help: "Do not use cache") var noCache: Bool = false @@ -140,12 +148,14 @@ extension Application { progress.set(description: "Dialing builder") - let builder: Builder? = try await withThrowingTaskGroup(of: Builder.self) { [vsockPort, cpus, memory] group in + // sara here added storage below + let builder: Builder? = try await withThrowingTaskGroup(of: Builder.self) { [vsockPort, cpus, memory, storage] group in defer { group.cancelAll() } - - group.addTask { [vsockPort, cpus, memory] in + + // sara here added stoagre below + group.addTask { [vsockPort, cpus, memory, storage] in while true { do { let container = try await ClientContainer.get(id: "buildkit") @@ -166,6 +176,9 @@ extension Application { try await BuilderStart.start( cpus: cpus, memory: memory, + // sara here + storage: storage, + // sara done progressUpdate: progress.handler ) diff --git a/Sources/ContainerCommands/Builder/BuilderStart.swift b/Sources/ContainerCommands/Builder/BuilderStart.swift index 18e145d6..8d7f3d7b 100644 --- a/Sources/ContainerCommands/Builder/BuilderStart.swift +++ b/Sources/ContainerCommands/Builder/BuilderStart.swift @@ -44,6 +44,14 @@ extension Application { ) var memory: String = "2048MB" + // sara + @Option( + name: .long, + help: "Disk capacity for the builder container (e.g. 512GB, 1TB)" + ) + var storage: String? + // sara + @OptionGroup var global: Flags.Global @@ -60,11 +68,13 @@ extension Application { progress.finish() } progress.start() - try await Self.start(cpus: self.cpus, memory: self.memory, progressUpdate: progress.handler) + // sara storage below + try await Self.start(cpus: self.cpus, memory: self.memory, storage: self.storage, progressUpdate: progress.handler) progress.finish() } - static func start(cpus: Int64?, memory: String?, progressUpdate: @escaping ProgressUpdateHandler) async throws { + // sara storage below + static func start(cpus: Int64?, memory: String?, storage: String?, progressUpdate: @escaping ProgressUpdateHandler) async throws { await progressUpdate([ .setDescription("Fetching BuildKit image"), .setItemsName("blobs"), @@ -88,6 +98,22 @@ extension Application { let builderPlatform = ContainerizationOCI.Platform(arch: "arm64", os: "linux", variant: "v8") + // sara and karen + // Decide which storage string to use for the builder: + // 1. CLI flag if provided + // 2. Default from config (you'll add .defaultBuilderStorage in DefaultsStore) + // 3. Otherwise, nil (keep existing behavior) + let effectiveStorage: String? = { + if let storage { + return storage + } + if let defaultStorage: String = DefaultsStore.getOptional(key: .defaultBuilderStorage) { + return defaultStorage + } + return nil + }() + // done + let existingContainer = try? await ClientContainer.get(id: "buildkit") if let existingContainer { let existingImage = existingContainer.configuration.image.reference @@ -103,6 +129,7 @@ extension Application { } return false }() + /* before let memChanged = try { if let memory { let memoryInBytes = try Parser.resources(cpus: nil, memory: memory).memoryInBytes @@ -111,11 +138,36 @@ extension Application { } } return false + }() */ + + // sara and karen + let memChanged = try { + if let memory { + let memoryInMiB = try Parser.memoryString(memory) + let memoryInBytes = UInt64(memoryInMiB.mib()) + return existingResources.memoryInBytes != memoryInBytes + } + return false }() + // done + + // sara and karen + let storageChanged = try { + if let effectiveStorage { + let storageMiB = try Parser.memoryString(effectiveStorage) + let storageBytes = UInt64(storageMiB.mib()) + // existingResources.storage is UInt64? + return existingResources.storage != storageBytes + } + return false + }() + // done + switch existingContainer.status { case .running: - guard imageChanged || cpuChanged || memChanged else { + // sara added storage changed below + guard imageChanged || cpuChanged || memChanged || storageChanged else { // If image, mem and cpu are the same, continue using the existing builder return } @@ -125,7 +177,8 @@ extension Application { case .stopped: // If the builder is stopped and matches our requirements, start it // Otherwise, delete it and create a new one - guard imageChanged || cpuChanged || memChanged else { + // sara storage changed below + guard imageChanged || cpuChanged || memChanged || storageChanged else { try await existingContainer.startBuildKit(progressUpdate, nil) return } @@ -182,10 +235,31 @@ extension Application { user: .id(uid: 0, gid: 0) ) + // before sara + // let resources = try Parser.resources( + // cpus: cpus, + // memory: memory + // ) + + // sara changes + // Decide which storage value to use: + // 1. CLI flag if provided + // 2. Config default from DefaultsStore + // 3. Fallback (e.g. nil or hard-coded legacy default) + let effectiveStorage: String? = { + if let storage { return storage } + if let defaultStorage: String = DefaultsStore.get(key: .defaultBuilderStorage) { + return defaultStorage + } + return nil // or "512GB" if you want an explicit legacy default here + }() + let resources = try Parser.resources( cpus: cpus, - memory: memory + memory: memory, + storage: effectiveStorage // ← new argument after you extend Parser.resources ) + // sara done var config = ContainerConfiguration(id: id, image: imageDesc, process: processConfig) config.resources = resources diff --git a/Sources/ContainerCommands/System/Property/PropertySet.swift b/Sources/ContainerCommands/System/Property/PropertySet.swift index dc01d84d..03e2d345 100644 --- a/Sources/ContainerCommands/System/Property/PropertySet.swift +++ b/Sources/ContainerCommands/System/Property/PropertySet.swift @@ -74,6 +74,12 @@ extension Application { throw ContainerizationError(.invalidArgument, message: "invalid CIDRv4 address: \(value)") } DefaultsStore.set(value: value, key: key) + // sara + case .defaultBuilderStorage, .defaultContainerStorage: + // validate capacity string (e.g. 512GB, 1TB, 2048MB) + _ = try Parser.memoryString(value) + DefaultsStore.set(value: value, key: key) + // done } } } diff --git a/Sources/ContainerPersistence/DefaultsStore.swift b/Sources/ContainerPersistence/DefaultsStore.swift index 45281469..7b8cf7a1 100644 --- a/Sources/ContainerPersistence/DefaultsStore.swift +++ b/Sources/ContainerPersistence/DefaultsStore.swift @@ -26,6 +26,8 @@ public enum DefaultsStore { case buildRosetta = "build.rosetta" case defaultDNSDomain = "dns.domain" case defaultBuilderImage = "image.builder" + case defaultBuilderStorage = "builder.storage" // sara + case defaultContainerStorage = "rootfs.capacity" // sara case defaultInitImage = "image.init" case defaultKernelBinaryPath = "kernel.binaryPath" case defaultKernelURL = "kernel.url" @@ -69,6 +71,8 @@ public enum DefaultsStore { let allKeys: [(Self.Keys, (Self.Keys) -> Any?)] = [ (.buildRosetta, { Self.getBool(key: $0) }), (.defaultBuilderImage, { Self.get(key: $0) }), + (.defaultBuilderStorage, { Self.getOptional(key: $0) }), // sara + (.defaultContainerStorage, { Self.getOptional(key: $0) }), // sara (.defaultInitImage, { Self.get(key: $0) }), (.defaultKernelBinaryPath, { Self.get(key: $0) }), (.defaultKernelURL, { Self.get(key: $0) }), @@ -124,6 +128,10 @@ extension DefaultsStore.Keys { return "If defined, the local DNS domain to use for containers with unqualified names." case .defaultBuilderImage: return "The image reference for the utility container that `container build` uses." + case .defaultBuilderStorage: + return "Default disk capacity for the builder container (e.g. 512GB, 1TB)." // sara + case .defaultContainerStorage: + return "Default disk capacity for native containers (rootfs.capacity, e.g. 512GB, 1TB)." // sara case .defaultInitImage: return "The image reference for the default initial filesystem image." case .defaultKernelBinaryPath: @@ -145,6 +153,10 @@ extension DefaultsStore.Keys { return String.self case .defaultBuilderImage: return String.self + case .defaultBuilderStorage: // sara + return String.self + case .defaultContainerStorage: // sara + return String.self case .defaultInitImage: return String.self case .defaultKernelBinaryPath: @@ -168,6 +180,10 @@ extension DefaultsStore.Keys { case .defaultBuilderImage: let tag = String(cString: get_container_builder_shim_version()) return "ghcr.io/apple/container-builder-shim/builder:\(tag)" + case .defaultBuilderStorage: + return "" // no built-in default; nil means "use legacy 512GB behavior" // sara + case .defaultContainerStorage: + return "" // same idea: only set if user configures it // sara case .defaultInitImage: let tag = String(cString: get_swift_containerization_version()) guard tag != "latest" else { From 30db2673ceb712fc01d7e1bb515210e77de4a712 Mon Sep 17 00:00:00 2001 From: karenheckel Date: Tue, 2 Dec 2025 21:22:34 -0600 Subject: [PATCH 02/15] simplify storage capacity descriptions in options --- Sources/ContainerClient/Flags.swift | 2 +- Sources/ContainerClient/Parser.swift | 1 - Sources/ContainerClient/Utility.swift | 8 ---- Sources/ContainerCommands/BuildCommand.swift | 2 +- .../Builder/BuilderStart.swift | 42 +++++-------------- .../System/Property/PropertySet.swift | 1 - .../ContainerPersistence/DefaultsStore.swift | 4 +- 7 files changed, 15 insertions(+), 45 deletions(-) diff --git a/Sources/ContainerClient/Flags.swift b/Sources/ContainerClient/Flags.swift index 20897508..e08c4cb4 100644 --- a/Sources/ContainerClient/Flags.swift +++ b/Sources/ContainerClient/Flags.swift @@ -78,7 +78,7 @@ public struct Flags { // sara @Option( name: .shortAndLong, - help: "Disk capacity / storage size for the container (e.g. 512GB, 1TB)" + help: "Disk capacity / storage size for the container" ) public var storage: String? // sara done diff --git a/Sources/ContainerClient/Parser.swift b/Sources/ContainerClient/Parser.swift index bbf6f04b..f19ea248 100644 --- a/Sources/ContainerClient/Parser.swift +++ b/Sources/ContainerClient/Parser.swift @@ -95,7 +95,6 @@ public struct Parser { } // sara start if let storage { - // parse "512GB", "1TB", etc. into bytes let storageInMiB = try Parser.memoryString(storage) resource.storage = UInt64(storageInMiB.mib()) } diff --git a/Sources/ContainerClient/Utility.swift b/Sources/ContainerClient/Utility.swift index 053f7461..69d423b3 100644 --- a/Sources/ContainerClient/Utility.swift +++ b/Sources/ContainerClient/Utility.swift @@ -183,14 +183,6 @@ public struct Utility { ) // sara - config.resources = try Parser.resources( - cpus: resource.cpus, - memory: resource.memory - // sara - storage: resource.storage - // done - ) - let tmpfs = try Parser.tmpfsMounts(management.tmpFs) let volumesOrFs = try Parser.volumes(management.volumes) let mountsOrFs = try Parser.mounts(management.mounts) diff --git a/Sources/ContainerCommands/BuildCommand.swift b/Sources/ContainerCommands/BuildCommand.swift index 25c7d778..992b6dfb 100644 --- a/Sources/ContainerCommands/BuildCommand.swift +++ b/Sources/ContainerCommands/BuildCommand.swift @@ -84,7 +84,7 @@ extension Application { // sara here @Option( name: .long, - help: "Disk capacity for the builder container (e.g. 512GB, 1TB)" + help: "Disk capacity for the builder container" ) var storage: String? // sara done diff --git a/Sources/ContainerCommands/Builder/BuilderStart.swift b/Sources/ContainerCommands/Builder/BuilderStart.swift index 8d7f3d7b..413fc58e 100644 --- a/Sources/ContainerCommands/Builder/BuilderStart.swift +++ b/Sources/ContainerCommands/Builder/BuilderStart.swift @@ -47,7 +47,7 @@ extension Application { // sara @Option( name: .long, - help: "Disk capacity for the builder container (e.g. 512GB, 1TB)" + help: "Disk capacity for the builder container" ) var storage: String? // sara @@ -152,15 +152,15 @@ extension Application { // done // sara and karen - let storageChanged = try { - if let effectiveStorage { - let storageMiB = try Parser.memoryString(effectiveStorage) - let storageBytes = UInt64(storageMiB.mib()) - // existingResources.storage is UInt64? - return existingResources.storage != storageBytes - } - return false - }() + let storageChanged = try { + if let effectiveStorage { + let storageInMiB = try Parser.memoryString(effectiveStorage) + let storageInBytes = UInt64(storageInMiB.mib()) + // existingResources.storage is UInt64? + return existingResources.storage != storageInBytes + } + return false + }() // done @@ -235,31 +235,11 @@ extension Application { user: .id(uid: 0, gid: 0) ) - // before sara - // let resources = try Parser.resources( - // cpus: cpus, - // memory: memory - // ) - - // sara changes - // Decide which storage value to use: - // 1. CLI flag if provided - // 2. Config default from DefaultsStore - // 3. Fallback (e.g. nil or hard-coded legacy default) - let effectiveStorage: String? = { - if let storage { return storage } - if let defaultStorage: String = DefaultsStore.get(key: .defaultBuilderStorage) { - return defaultStorage - } - return nil // or "512GB" if you want an explicit legacy default here - }() - let resources = try Parser.resources( cpus: cpus, memory: memory, - storage: effectiveStorage // ← new argument after you extend Parser.resources + storage: effectiveStorage // sara change here ) - // sara done var config = ContainerConfiguration(id: id, image: imageDesc, process: processConfig) config.resources = resources diff --git a/Sources/ContainerCommands/System/Property/PropertySet.swift b/Sources/ContainerCommands/System/Property/PropertySet.swift index 03e2d345..b444a7ec 100644 --- a/Sources/ContainerCommands/System/Property/PropertySet.swift +++ b/Sources/ContainerCommands/System/Property/PropertySet.swift @@ -76,7 +76,6 @@ extension Application { DefaultsStore.set(value: value, key: key) // sara case .defaultBuilderStorage, .defaultContainerStorage: - // validate capacity string (e.g. 512GB, 1TB, 2048MB) _ = try Parser.memoryString(value) DefaultsStore.set(value: value, key: key) // done diff --git a/Sources/ContainerPersistence/DefaultsStore.swift b/Sources/ContainerPersistence/DefaultsStore.swift index 7b8cf7a1..ae092b1f 100644 --- a/Sources/ContainerPersistence/DefaultsStore.swift +++ b/Sources/ContainerPersistence/DefaultsStore.swift @@ -129,9 +129,9 @@ extension DefaultsStore.Keys { case .defaultBuilderImage: return "The image reference for the utility container that `container build` uses." case .defaultBuilderStorage: - return "Default disk capacity for the builder container (e.g. 512GB, 1TB)." // sara + return "Default disk capacity for the builder container." // sara case .defaultContainerStorage: - return "Default disk capacity for native containers (rootfs.capacity, e.g. 512GB, 1TB)." // sara + return "Default disk capacity for native containers." // sara case .defaultInitImage: return "The image reference for the default initial filesystem image." case .defaultKernelBinaryPath: From d58586415628f56c647f75c4bb0735aa6a5a35c1 Mon Sep 17 00:00:00 2001 From: karenheckel Date: Tue, 2 Dec 2025 22:39:29 -0600 Subject: [PATCH 03/15] Add host storage validation for requested storage capacity --- Sources/ContainerClient/Parser.swift | 27 +++++++++++++++++++ Sources/ContainerClient/Utility.swift | 6 ++++- .../Builder/BuilderStart.swift | 6 +++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Sources/ContainerClient/Parser.swift b/Sources/ContainerClient/Parser.swift index f19ea248..aaabfafd 100644 --- a/Sources/ContainerClient/Parser.swift +++ b/Sources/ContainerClient/Parser.swift @@ -882,3 +882,30 @@ public struct Parser { } } } + +// sara and karen +extension Parser { + /// Validates that the host has enough disk space for the requested storage. + public static func validateHostStorage(bytes: UInt64) throws { + let fileURL = URL(fileURLWithPath: "/") + do { + let values = try fileURL.resourceValues(forKeys: [.volumeAvailableCapacityKey]) + if let available = values.volumeAvailableCapacity { + if UInt64(available) < bytes { + let availableStr = ByteCountFormatter.string(fromByteCount: Int64(available), countStyle: .file) + let requestedStr = ByteCountFormatter.string(fromByteCount: Int64(bytes), countStyle: .file) + + throw ContainerizationError( + .invalidArgument, + message: "requested storage (\(requestedStr)) exceeds available host capacity (\(availableStr))" + ) + } + } + } catch let error as ContainerizationError { + throw error + } catch { + throw ContainerizationError(.unknown, message: "failed to validate host storage: \(error.localizedDescription)") + } + } +} +// end diff --git a/Sources/ContainerClient/Utility.swift b/Sources/ContainerClient/Utility.swift index 69d423b3..cefebfae 100644 --- a/Sources/ContainerClient/Utility.swift +++ b/Sources/ContainerClient/Utility.swift @@ -181,7 +181,11 @@ public struct Utility { memory: resource.memory, storage: effectiveStorage ) - // sara + + if let storageBytes = config.resources.storage { + try Parser.validateHostStorage(bytes: storageBytes) + } + // end sara & karen let tmpfs = try Parser.tmpfsMounts(management.tmpFs) let volumesOrFs = try Parser.volumes(management.volumes) diff --git a/Sources/ContainerCommands/Builder/BuilderStart.swift b/Sources/ContainerCommands/Builder/BuilderStart.swift index 413fc58e..f3b9331a 100644 --- a/Sources/ContainerCommands/Builder/BuilderStart.swift +++ b/Sources/ContainerCommands/Builder/BuilderStart.swift @@ -241,6 +241,12 @@ extension Application { storage: effectiveStorage // sara change here ) + // karen and sara + if let storageBytes = resources.storage { + try Parser.validateHostStorage(bytes: storageBytes) + } + // end + var config = ContainerConfiguration(id: id, image: imageDesc, process: processConfig) config.resources = resources config.mounts = [ From 758d2d9eeca0ab52b74568842cf21d62d186553e Mon Sep 17 00:00:00 2001 From: karenheckel Date: Thu, 4 Dec 2025 17:16:58 -0600 Subject: [PATCH 04/15] Clean up storage related sections --- Sources/ContainerClient/Flags.swift | 2 -- Sources/ContainerClient/Parser.swift | 11 ++----- Sources/ContainerClient/Utility.swift | 2 -- Sources/ContainerCommands/BuildCommand.swift | 8 +---- .../Builder/BuilderStart.swift | 33 ++----------------- .../System/Property/PropertySet.swift | 2 -- .../ContainerPersistence/DefaultsStore.swift | 20 +++++------ 7 files changed, 16 insertions(+), 62 deletions(-) diff --git a/Sources/ContainerClient/Flags.swift b/Sources/ContainerClient/Flags.swift index e08c4cb4..074315b5 100644 --- a/Sources/ContainerClient/Flags.swift +++ b/Sources/ContainerClient/Flags.swift @@ -75,13 +75,11 @@ public struct Flags { ) public var memory: String? - // sara @Option( name: .shortAndLong, help: "Disk capacity / storage size for the container" ) public var storage: String? -// sara done } public struct Registry: ParsableArguments { diff --git a/Sources/ContainerClient/Parser.swift b/Sources/ContainerClient/Parser.swift index aaabfafd..f05e74de 100644 --- a/Sources/ContainerClient/Parser.swift +++ b/Sources/ContainerClient/Parser.swift @@ -84,7 +84,6 @@ public struct Parser { try .init(from: platform) } - // sara added storage below public static func resources(cpus: Int64?, memory: String?, storage: String?) throws -> ContainerConfiguration.Resources { var resource = ContainerConfiguration.Resources() if let cpus { @@ -93,12 +92,10 @@ public struct Parser { if let memory { resource.memoryInBytes = try Parser.memoryString(memory).mib() } - // sara start if let storage { let storageInMiB = try Parser.memoryString(storage) - resource.storage = UInt64(storageInMiB.mib()) + resource.storage = UInt64(storageInMiB.mib()) } - // sara done return resource } @@ -883,7 +880,6 @@ public struct Parser { } } -// sara and karen extension Parser { /// Validates that the host has enough disk space for the requested storage. public static func validateHostStorage(bytes: UInt64) throws { @@ -894,9 +890,9 @@ extension Parser { if UInt64(available) < bytes { let availableStr = ByteCountFormatter.string(fromByteCount: Int64(available), countStyle: .file) let requestedStr = ByteCountFormatter.string(fromByteCount: Int64(bytes), countStyle: .file) - + throw ContainerizationError( - .invalidArgument, + .invalidArgument, message: "requested storage (\(requestedStr)) exceeds available host capacity (\(availableStr))" ) } @@ -908,4 +904,3 @@ extension Parser { } } } -// end diff --git a/Sources/ContainerClient/Utility.swift b/Sources/ContainerClient/Utility.swift index cefebfae..8a36c0b6 100644 --- a/Sources/ContainerClient/Utility.swift +++ b/Sources/ContainerClient/Utility.swift @@ -165,7 +165,6 @@ public struct Utility { var config = ContainerConfiguration(id: id, image: description, process: pc) config.platform = requestedPlatform - // sara let effectiveStorage: String? = { if let storage = resource.storage { return storage @@ -185,7 +184,6 @@ public struct Utility { if let storageBytes = config.resources.storage { try Parser.validateHostStorage(bytes: storageBytes) } - // end sara & karen let tmpfs = try Parser.tmpfsMounts(management.tmpFs) let volumesOrFs = try Parser.volumes(management.volumes) diff --git a/Sources/ContainerCommands/BuildCommand.swift b/Sources/ContainerCommands/BuildCommand.swift index 992b6dfb..babc86fc 100644 --- a/Sources/ContainerCommands/BuildCommand.swift +++ b/Sources/ContainerCommands/BuildCommand.swift @@ -81,13 +81,11 @@ extension Application { ) var memory: String = "2048MB" - // sara here @Option( name: .long, help: "Disk capacity for the builder container" ) var storage: String? - // sara done @Flag(name: .long, help: "Do not use cache") var noCache: Bool = false @@ -148,13 +146,11 @@ extension Application { progress.set(description: "Dialing builder") - // sara here added storage below let builder: Builder? = try await withThrowingTaskGroup(of: Builder.self) { [vsockPort, cpus, memory, storage] group in defer { group.cancelAll() } - - // sara here added stoagre below + group.addTask { [vsockPort, cpus, memory, storage] in while true { do { @@ -176,9 +172,7 @@ extension Application { try await BuilderStart.start( cpus: cpus, memory: memory, - // sara here storage: storage, - // sara done progressUpdate: progress.handler ) diff --git a/Sources/ContainerCommands/Builder/BuilderStart.swift b/Sources/ContainerCommands/Builder/BuilderStart.swift index f3b9331a..1aa67efc 100644 --- a/Sources/ContainerCommands/Builder/BuilderStart.swift +++ b/Sources/ContainerCommands/Builder/BuilderStart.swift @@ -44,13 +44,11 @@ extension Application { ) var memory: String = "2048MB" - // sara @Option( name: .long, help: "Disk capacity for the builder container" ) var storage: String? - // sara @OptionGroup var global: Flags.Global @@ -68,12 +66,10 @@ extension Application { progress.finish() } progress.start() - // sara storage below try await Self.start(cpus: self.cpus, memory: self.memory, storage: self.storage, progressUpdate: progress.handler) progress.finish() } - // sara storage below static func start(cpus: Int64?, memory: String?, storage: String?, progressUpdate: @escaping ProgressUpdateHandler) async throws { await progressUpdate([ .setDescription("Fetching BuildKit image"), @@ -98,11 +94,7 @@ extension Application { let builderPlatform = ContainerizationOCI.Platform(arch: "arm64", os: "linux", variant: "v8") - // sara and karen - // Decide which storage string to use for the builder: - // 1. CLI flag if provided - // 2. Default from config (you'll add .defaultBuilderStorage in DefaultsStore) - // 3. Otherwise, nil (keep existing behavior) + // Decide which storage string to use for the builder let effectiveStorage: String? = { if let storage { return storage @@ -112,7 +104,6 @@ extension Application { } return nil }() - // done let existingContainer = try? await ClientContainer.get(id: "buildkit") if let existingContainer { @@ -129,18 +120,7 @@ extension Application { } return false }() - /* before - let memChanged = try { - if let memory { - let memoryInBytes = try Parser.resources(cpus: nil, memory: memory).memoryInBytes - if existingResources.memoryInBytes != memoryInBytes { - return true - } - } - return false - }() */ - // sara and karen let memChanged = try { if let memory { let memoryInMiB = try Parser.memoryString(memory) @@ -149,24 +129,18 @@ extension Application { } return false }() - // done - // sara and karen let storageChanged = try { if let effectiveStorage { let storageInMiB = try Parser.memoryString(effectiveStorage) let storageInBytes = UInt64(storageInMiB.mib()) - // existingResources.storage is UInt64? return existingResources.storage != storageInBytes } return false }() - // done - switch existingContainer.status { case .running: - // sara added storage changed below guard imageChanged || cpuChanged || memChanged || storageChanged else { // If image, mem and cpu are the same, continue using the existing builder return @@ -177,7 +151,6 @@ extension Application { case .stopped: // If the builder is stopped and matches our requirements, start it // Otherwise, delete it and create a new one - // sara storage changed below guard imageChanged || cpuChanged || memChanged || storageChanged else { try await existingContainer.startBuildKit(progressUpdate, nil) return @@ -238,14 +211,12 @@ extension Application { let resources = try Parser.resources( cpus: cpus, memory: memory, - storage: effectiveStorage // sara change here + storage: effectiveStorage ) - // karen and sara if let storageBytes = resources.storage { try Parser.validateHostStorage(bytes: storageBytes) } - // end var config = ContainerConfiguration(id: id, image: imageDesc, process: processConfig) config.resources = resources diff --git a/Sources/ContainerCommands/System/Property/PropertySet.swift b/Sources/ContainerCommands/System/Property/PropertySet.swift index b444a7ec..462af7a6 100644 --- a/Sources/ContainerCommands/System/Property/PropertySet.swift +++ b/Sources/ContainerCommands/System/Property/PropertySet.swift @@ -74,11 +74,9 @@ extension Application { throw ContainerizationError(.invalidArgument, message: "invalid CIDRv4 address: \(value)") } DefaultsStore.set(value: value, key: key) - // sara case .defaultBuilderStorage, .defaultContainerStorage: _ = try Parser.memoryString(value) DefaultsStore.set(value: value, key: key) - // done } } } diff --git a/Sources/ContainerPersistence/DefaultsStore.swift b/Sources/ContainerPersistence/DefaultsStore.swift index ae092b1f..66cb6585 100644 --- a/Sources/ContainerPersistence/DefaultsStore.swift +++ b/Sources/ContainerPersistence/DefaultsStore.swift @@ -26,8 +26,8 @@ public enum DefaultsStore { case buildRosetta = "build.rosetta" case defaultDNSDomain = "dns.domain" case defaultBuilderImage = "image.builder" - case defaultBuilderStorage = "builder.storage" // sara - case defaultContainerStorage = "rootfs.capacity" // sara + case defaultBuilderStorage = "builder.storage" + case defaultContainerStorage = "rootfs.capacity" case defaultInitImage = "image.init" case defaultKernelBinaryPath = "kernel.binaryPath" case defaultKernelURL = "kernel.url" @@ -71,8 +71,8 @@ public enum DefaultsStore { let allKeys: [(Self.Keys, (Self.Keys) -> Any?)] = [ (.buildRosetta, { Self.getBool(key: $0) }), (.defaultBuilderImage, { Self.get(key: $0) }), - (.defaultBuilderStorage, { Self.getOptional(key: $0) }), // sara - (.defaultContainerStorage, { Self.getOptional(key: $0) }), // sara + (.defaultBuilderStorage, { Self.getOptional(key: $0) }), + (.defaultContainerStorage, { Self.getOptional(key: $0) }), (.defaultInitImage, { Self.get(key: $0) }), (.defaultKernelBinaryPath, { Self.get(key: $0) }), (.defaultKernelURL, { Self.get(key: $0) }), @@ -129,9 +129,9 @@ extension DefaultsStore.Keys { case .defaultBuilderImage: return "The image reference for the utility container that `container build` uses." case .defaultBuilderStorage: - return "Default disk capacity for the builder container." // sara + return "Default disk capacity for the builder container." case .defaultContainerStorage: - return "Default disk capacity for native containers." // sara + return "Default disk capacity for native containers." case .defaultInitImage: return "The image reference for the default initial filesystem image." case .defaultKernelBinaryPath: @@ -153,9 +153,9 @@ extension DefaultsStore.Keys { return String.self case .defaultBuilderImage: return String.self - case .defaultBuilderStorage: // sara + case .defaultBuilderStorage: return String.self - case .defaultContainerStorage: // sara + case .defaultContainerStorage: return String.self case .defaultInitImage: return String.self @@ -181,9 +181,9 @@ extension DefaultsStore.Keys { let tag = String(cString: get_container_builder_shim_version()) return "ghcr.io/apple/container-builder-shim/builder:\(tag)" case .defaultBuilderStorage: - return "" // no built-in default; nil means "use legacy 512GB behavior" // sara + return "" case .defaultContainerStorage: - return "" // same idea: only set if user configures it // sara + return "" case .defaultInitImage: let tag = String(cString: get_swift_containerization_version()) guard tag != "latest" else { From 646254c22bbea7212c91e0ed5c801a0ee056a517 Mon Sep 17 00:00:00 2001 From: karenheckel Date: Thu, 4 Dec 2025 17:28:27 -0600 Subject: [PATCH 05/15] Adding signature --- Sources/ContainerClient/Utility.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ContainerClient/Utility.swift b/Sources/ContainerClient/Utility.swift index 8a36c0b6..8d3424fb 100644 --- a/Sources/ContainerClient/Utility.swift +++ b/Sources/ContainerClient/Utility.swift @@ -381,3 +381,4 @@ public struct Utility { return volume } } + From 3e70f07ddfee6a50126a38a749aca628d27f526e Mon Sep 17 00:00:00 2001 From: karenheckel Date: Thu, 4 Dec 2025 17:31:46 -0600 Subject: [PATCH 06/15] Running formatter --- Sources/ContainerClient/Utility.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ContainerClient/Utility.swift b/Sources/ContainerClient/Utility.swift index 8d3424fb..8a36c0b6 100644 --- a/Sources/ContainerClient/Utility.swift +++ b/Sources/ContainerClient/Utility.swift @@ -381,4 +381,3 @@ public struct Utility { return volume } } - From d86e5df98da46ebdac7843fe804f80945b88e5df Mon Sep 17 00:00:00 2001 From: Sara Sandoval Date: Sun, 7 Dec 2025 16:32:51 -0600 Subject: [PATCH 07/15] fixed "validate it separately when we read" --- .../Builder/BuilderStart.swift | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Sources/ContainerCommands/Builder/BuilderStart.swift b/Sources/ContainerCommands/Builder/BuilderStart.swift index 1aa67efc..1cdf5572 100644 --- a/Sources/ContainerCommands/Builder/BuilderStart.swift +++ b/Sources/ContainerCommands/Builder/BuilderStart.swift @@ -94,16 +94,18 @@ extension Application { let builderPlatform = ContainerizationOCI.Platform(arch: "arm64", os: "linux", variant: "v8") + // sara comment fix // Decide which storage string to use for the builder - let effectiveStorage: String? = { - if let storage { - return storage - } - if let defaultStorage: String = DefaultsStore.getOptional(key: .defaultBuilderStorage) { - return defaultStorage - } - return nil - }() + let effectiveStorage: String? + if let storage { + effectiveStorage = storage + } else if let defaultStorage: String = DefaultsStore.getOptional(key: .defaultBuilderStorage) { + _ = try Parser.memoryString(defaultStorage) + effectiveStorage = defaultStorage + } else { + effectiveStorage = nil + } + // sara done let existingContainer = try? await ClientContainer.get(id: "buildkit") if let existingContainer { From ee6964dca543ee1dd610fff26492d467cb9750da Mon Sep 17 00:00:00 2001 From: Sara Sandoval Date: Sun, 7 Dec 2025 16:49:11 -0600 Subject: [PATCH 08/15] fixes "separately validate the cases of storage provided through the command vs provided through the user default" --- Sources/ContainerClient/Utility.swift | 31 ++++++++++++++----- .../Builder/BuilderStart.swift | 17 +++++++++- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/Sources/ContainerClient/Utility.swift b/Sources/ContainerClient/Utility.swift index 829ee2b5..384275e8 100644 --- a/Sources/ContainerClient/Utility.swift +++ b/Sources/ContainerClient/Utility.swift @@ -168,21 +168,38 @@ public struct Utility { var config = ContainerConfiguration(id: id, image: description, process: pc) config.platform = requestedPlatform - let effectiveStorage: String? = { - if let storage = resource.storage { - return storage + // sara + let effectiveStorage: String? + if let storage = resource.storage { + do { + _ = try Parser.memoryString(storage) + } catch { + throw ContainerizationError( + .invalidArgument, + message: "invalid storage value '\(storage)' for --storage" + ) } - if let defaultStorage: String = DefaultsStore.getOptional(key: .defaultContainerStorage) { - return defaultStorage + effectiveStorage = storage + } else if let defaultStorage: String = DefaultsStore.getOptional(key: .defaultContainerStorage) { + do { + _ = try Parser.memoryString(defaultStorage) + } catch { + throw ContainerizationError( + .invalidArgument, + message: "invalid default container storage value '\(defaultStorage)'; update it with `container property set defaultContainerStorage`" + ) } - return nil - }() + effectiveStorage = defaultStorage + } else { + effectiveStorage = nil + } config.resources = try Parser.resources( cpus: resource.cpus, memory: resource.memory, storage: effectiveStorage ) + // sara done if let storageBytes = config.resources.storage { try Parser.validateHostStorage(bytes: storageBytes) diff --git a/Sources/ContainerCommands/Builder/BuilderStart.swift b/Sources/ContainerCommands/Builder/BuilderStart.swift index 1cdf5572..17ce060c 100644 --- a/Sources/ContainerCommands/Builder/BuilderStart.swift +++ b/Sources/ContainerCommands/Builder/BuilderStart.swift @@ -98,9 +98,24 @@ extension Application { // Decide which storage string to use for the builder let effectiveStorage: String? if let storage { + do { + _ = try Parser.memoryString(storage) + } catch { + throw ContainerizationError( + .invalidArgument, + message: "invalid storage value '\(storage)' for --storage" + ) + } effectiveStorage = storage } else if let defaultStorage: String = DefaultsStore.getOptional(key: .defaultBuilderStorage) { - _ = try Parser.memoryString(defaultStorage) + do { + _ = try Parser.memoryString(defaultStorage) + } catch { + throw ContainerizationError( + .invalidArgument, + message: "invalid default builder storage value '\(defaultStorage)'; update it with `container property set defaultBuilderStorage`" + ) + } effectiveStorage = defaultStorage } else { effectiveStorage = nil From 0658f2e8c4532f56517168910dfed3efb83f64de Mon Sep 17 00:00:00 2001 From: karenheckel Date: Sun, 7 Dec 2025 16:55:21 -0600 Subject: [PATCH 09/15] Remove host storage validation logic --- Sources/ContainerClient/Parser.swift | 25 ------------------- Sources/ContainerClient/Utility.swift | 6 ----- .../Builder/BuilderStart.swift | 6 ----- 3 files changed, 37 deletions(-) diff --git a/Sources/ContainerClient/Parser.swift b/Sources/ContainerClient/Parser.swift index f05e74de..03f6b451 100644 --- a/Sources/ContainerClient/Parser.swift +++ b/Sources/ContainerClient/Parser.swift @@ -879,28 +879,3 @@ public struct Parser { } } } - -extension Parser { - /// Validates that the host has enough disk space for the requested storage. - public static func validateHostStorage(bytes: UInt64) throws { - let fileURL = URL(fileURLWithPath: "/") - do { - let values = try fileURL.resourceValues(forKeys: [.volumeAvailableCapacityKey]) - if let available = values.volumeAvailableCapacity { - if UInt64(available) < bytes { - let availableStr = ByteCountFormatter.string(fromByteCount: Int64(available), countStyle: .file) - let requestedStr = ByteCountFormatter.string(fromByteCount: Int64(bytes), countStyle: .file) - - throw ContainerizationError( - .invalidArgument, - message: "requested storage (\(requestedStr)) exceeds available host capacity (\(availableStr))" - ) - } - } - } catch let error as ContainerizationError { - throw error - } catch { - throw ContainerizationError(.unknown, message: "failed to validate host storage: \(error.localizedDescription)") - } - } -} diff --git a/Sources/ContainerClient/Utility.swift b/Sources/ContainerClient/Utility.swift index 384275e8..644680c3 100644 --- a/Sources/ContainerClient/Utility.swift +++ b/Sources/ContainerClient/Utility.swift @@ -168,7 +168,6 @@ public struct Utility { var config = ContainerConfiguration(id: id, image: description, process: pc) config.platform = requestedPlatform - // sara let effectiveStorage: String? if let storage = resource.storage { do { @@ -199,11 +198,6 @@ public struct Utility { memory: resource.memory, storage: effectiveStorage ) - // sara done - - if let storageBytes = config.resources.storage { - try Parser.validateHostStorage(bytes: storageBytes) - } let tmpfs = try Parser.tmpfsMounts(management.tmpFs) let volumesOrFs = try Parser.volumes(management.volumes) diff --git a/Sources/ContainerCommands/Builder/BuilderStart.swift b/Sources/ContainerCommands/Builder/BuilderStart.swift index 17ce060c..dc42e1be 100644 --- a/Sources/ContainerCommands/Builder/BuilderStart.swift +++ b/Sources/ContainerCommands/Builder/BuilderStart.swift @@ -94,7 +94,6 @@ extension Application { let builderPlatform = ContainerizationOCI.Platform(arch: "arm64", os: "linux", variant: "v8") - // sara comment fix // Decide which storage string to use for the builder let effectiveStorage: String? if let storage { @@ -120,7 +119,6 @@ extension Application { } else { effectiveStorage = nil } - // sara done let existingContainer = try? await ClientContainer.get(id: "buildkit") if let existingContainer { @@ -231,10 +229,6 @@ extension Application { storage: effectiveStorage ) - if let storageBytes = resources.storage { - try Parser.validateHostStorage(bytes: storageBytes) - } - var config = ContainerConfiguration(id: id, image: imageDesc, process: processConfig) config.resources = resources config.mounts = [ From 4246b502c9533deff97d603a79aaf335a939dcbc Mon Sep 17 00:00:00 2001 From: karenheckel Date: Sun, 7 Dec 2025 17:52:13 -0600 Subject: [PATCH 10/15] Add CLI tests for a regular container and the builder --- .../Build/CLIBuilderLifecycleTest.swift | 46 +++++++++++++++++++ .../Subcommands/Run/TestCLIRunOptions.swift | 36 +++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/Tests/CLITests/Subcommands/Build/CLIBuilderLifecycleTest.swift b/Tests/CLITests/Subcommands/Build/CLIBuilderLifecycleTest.swift index 22d064b3..bb258ce2 100644 --- a/Tests/CLITests/Subcommands/Build/CLIBuilderLifecycleTest.swift +++ b/Tests/CLITests/Subcommands/Build/CLIBuilderLifecycleTest.swift @@ -16,6 +16,7 @@ import Foundation import Testing +import ContainerClient extension TestCLIBuildBase { class CLIBuilderLifecycleTest: TestCLIBuildBase { @@ -33,5 +34,50 @@ extension TestCLIBuildBase { #expect(status == "stopped", "BuildKit container is not stopped") } } + + @Test func testBuilderStorageFlag() throws { + do { + let requestedStorage = "4096MB" + let expectedMiB = Int(try Parser.memoryString(requestedStorage)) + + try? builderStop() + try? builderDelete(force: true) + + let (_, _, err, status) = try run(arguments: [ + "builder", + "start", + "--storage", requestedStorage, + ]) + try #require(status == 0, "builder start failed: \(err)") + + try waitForBuilderRunning() + + defer { + try? builderStop() + try? builderDelete(force: true) + } + + let buildkitName = "buildkit" + var output = try doExec( + name: buildkitName, + cmd: ["sh", "-c", "df -m / | tail -1 | tr -s ' ' | cut -d' ' -f2"] + ) + output = output.trimmingCharacters(in: .whitespacesAndNewlines) + + guard let reportedMiB = Int(output) else { + Issue.record("expected integer df output, got '\(output)'") + return + } + + let tolerance = expectedMiB / 10 + #expect( + abs(reportedMiB - expectedMiB) <= tolerance, + "expected root filesystem size ≈ \(expectedMiB) MiB for storage \(requestedStorage), got \(reportedMiB) MiB" + ) + } catch { + Issue.record("failed to verify builder storage: \(error)") + return + } + } } } diff --git a/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift b/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift index 500b88f8..ea4ed85e 100644 --- a/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift +++ b/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift @@ -576,6 +576,42 @@ class TestCLIRunCommand: CLITest { } } + @Test func testRunCommandStorage() throws { + do { + let name = getTestName() + let requestedStorage = "2048MB" + let expectedMiB = Int(try Parser.memoryString(requestedStorage)) + + try doLongRun(name: name, args: ["--storage", requestedStorage]) + defer { + try? doStop(name: name) + } + + var output = try doExec( + name: name, + cmd: ["sh", "-c", "df -m / | tail -1 | tr -s ' ' | cut -d' ' -f2"] + ) + output = output.trimmingCharacters(in: .whitespacesAndNewlines) + + guard let reportedMiB = Int(output) else { + Issue.record("expected integer df output, got '\(output)'") + return + } + + let tolerance = expectedMiB / 10 + #expect( + abs(reportedMiB - expectedMiB) <= tolerance, + "expected root filesystem size ≈ \(expectedMiB) MiB for storage \(requestedStorage), got \(reportedMiB) MiB" + ) + + try doStop(name: name) + } catch { + Issue.record("failed to run container \(error)") + return + } + + } + func getDefaultDomain() throws -> String? { let (_, output, err, status) = try run(arguments: ["system", "property", "get", "dns.domain"]) try #require(status == 0, "default DNS domain retrieval returned status \(status): \(err)") From 27d6445be8e2d4d9d0453377037f5c7c89f2d7a7 Mon Sep 17 00:00:00 2001 From: Sara Sandoval Date: Sun, 7 Dec 2025 18:09:51 -0600 Subject: [PATCH 11/15] fixed naming issue --- Sources/ContainerPersistence/DefaultsStore.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ContainerPersistence/DefaultsStore.swift b/Sources/ContainerPersistence/DefaultsStore.swift index 66cb6585..f2ed71ce 100644 --- a/Sources/ContainerPersistence/DefaultsStore.swift +++ b/Sources/ContainerPersistence/DefaultsStore.swift @@ -27,7 +27,7 @@ public enum DefaultsStore { case defaultDNSDomain = "dns.domain" case defaultBuilderImage = "image.builder" case defaultBuilderStorage = "builder.storage" - case defaultContainerStorage = "rootfs.capacity" + case defaultContainerStorage = "container.storage" case defaultInitImage = "image.init" case defaultKernelBinaryPath = "kernel.binaryPath" case defaultKernelURL = "kernel.url" From fafe865c3194abc71d9d4d3cb0f2bc80062a82d0 Mon Sep 17 00:00:00 2001 From: karenheckel Date: Tue, 9 Dec 2025 17:49:11 -0600 Subject: [PATCH 12/15] Running formatter --- Tests/CLITests/Subcommands/Build/CLIBuilderLifecycleTest.swift | 2 +- Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/CLITests/Subcommands/Build/CLIBuilderLifecycleTest.swift b/Tests/CLITests/Subcommands/Build/CLIBuilderLifecycleTest.swift index bb258ce2..74d6df72 100644 --- a/Tests/CLITests/Subcommands/Build/CLIBuilderLifecycleTest.swift +++ b/Tests/CLITests/Subcommands/Build/CLIBuilderLifecycleTest.swift @@ -14,9 +14,9 @@ // limitations under the License. //===----------------------------------------------------------------------===// +import ContainerClient import Foundation import Testing -import ContainerClient extension TestCLIBuildBase { class CLIBuilderLifecycleTest: TestCLIBuildBase { diff --git a/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift b/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift index a24bfe03..d98ec0df 100644 --- a/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift +++ b/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift @@ -596,7 +596,7 @@ class TestCLIRunCommand: CLITest { output = output.trimmingCharacters(in: .whitespacesAndNewlines) guard let reportedMiB = Int(output) else { - Issue.record("expected integer df output, got '\(output)'") + Issue.record("expected integer df output, got '\(output)'") return } From 59ae309714b6b83884a0de108663d8382476173e Mon Sep 17 00:00:00 2001 From: karenheckel Date: Wed, 10 Dec 2025 14:57:11 -0600 Subject: [PATCH 13/15] Fixing unit mismatches --- Sources/ContainerClient/Parser.swift | 2 +- Sources/ContainerCommands/Builder/BuilderStart.swift | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/ContainerClient/Parser.swift b/Sources/ContainerClient/Parser.swift index 574ba37e..7182f30d 100644 --- a/Sources/ContainerClient/Parser.swift +++ b/Sources/ContainerClient/Parser.swift @@ -94,7 +94,7 @@ public struct Parser { } if let storage { let storageInMiB = try Parser.memoryString(storage) - resource.storage = UInt64(storageInMiB.mib()) + resource.storage = UInt64(storageInMiB) } return resource } diff --git a/Sources/ContainerCommands/Builder/BuilderStart.swift b/Sources/ContainerCommands/Builder/BuilderStart.swift index dc42e1be..85dc5ede 100644 --- a/Sources/ContainerCommands/Builder/BuilderStart.swift +++ b/Sources/ContainerCommands/Builder/BuilderStart.swift @@ -148,10 +148,10 @@ extension Application { let storageChanged = try { if let effectiveStorage { let storageInMiB = try Parser.memoryString(effectiveStorage) - let storageInBytes = UInt64(storageInMiB.mib()) + let storageInBytes = UInt64(storageInMiB) return existingResources.storage != storageInBytes } - return false + return existingResources.storage != 0 }() switch existingContainer.status { @@ -230,6 +230,7 @@ extension Application { ) var config = ContainerConfiguration(id: id, image: imageDesc, process: processConfig) + config.platform = builderPlatform config.resources = resources config.mounts = [ .init( From 64a25cf1bbc8c775d5e6525077b6b82e9d6aeece Mon Sep 17 00:00:00 2001 From: karenheckel Date: Wed, 10 Dec 2025 18:23:54 -0600 Subject: [PATCH 14/15] Update test cases to confirm CLI work is correct --- Sources/ContainerClient/Parser.swift | 2 +- .../Builder/BuilderStart.swift | 2 +- .../Build/CLIBuilderLifecycleTest.swift | 19 +++++++---------- .../Subcommands/Run/TestCLIRunCommand.swift | 21 +++++++++---------- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/Sources/ContainerClient/Parser.swift b/Sources/ContainerClient/Parser.swift index 7182f30d..574ba37e 100644 --- a/Sources/ContainerClient/Parser.swift +++ b/Sources/ContainerClient/Parser.swift @@ -94,7 +94,7 @@ public struct Parser { } if let storage { let storageInMiB = try Parser.memoryString(storage) - resource.storage = UInt64(storageInMiB) + resource.storage = UInt64(storageInMiB.mib()) } return resource } diff --git a/Sources/ContainerCommands/Builder/BuilderStart.swift b/Sources/ContainerCommands/Builder/BuilderStart.swift index 85dc5ede..7e15d297 100644 --- a/Sources/ContainerCommands/Builder/BuilderStart.swift +++ b/Sources/ContainerCommands/Builder/BuilderStart.swift @@ -148,7 +148,7 @@ extension Application { let storageChanged = try { if let effectiveStorage { let storageInMiB = try Parser.memoryString(effectiveStorage) - let storageInBytes = UInt64(storageInMiB) + let storageInBytes = UInt64(storageInMiB.mib()) return existingResources.storage != storageInBytes } return existingResources.storage != 0 diff --git a/Tests/CLITests/Subcommands/Build/CLIBuilderLifecycleTest.swift b/Tests/CLITests/Subcommands/Build/CLIBuilderLifecycleTest.swift index 74d6df72..a30d97b5 100644 --- a/Tests/CLITests/Subcommands/Build/CLIBuilderLifecycleTest.swift +++ b/Tests/CLITests/Subcommands/Build/CLIBuilderLifecycleTest.swift @@ -35,10 +35,11 @@ extension TestCLIBuildBase { } } - @Test func testBuilderStorageFlag() throws { + @Test func testBuilderStorageFlag() async throws { do { let requestedStorage = "4096MB" let expectedMiB = Int(try Parser.memoryString(requestedStorage)) + let expectedBytes = UInt64(expectedMiB.mib()) try? builderStop() try? builderDelete(force: true) @@ -58,21 +59,17 @@ extension TestCLIBuildBase { } let buildkitName = "buildkit" - var output = try doExec( - name: buildkitName, - cmd: ["sh", "-c", "df -m / | tail -1 | tr -s ' ' | cut -d' ' -f2"] - ) - output = output.trimmingCharacters(in: .whitespacesAndNewlines) + let buildkit = try await ClientContainer.get(id: buildkitName) + let resources = buildkit.configuration.resources - guard let reportedMiB = Int(output) else { - Issue.record("expected integer df output, got '\(output)'") + guard let storageBytes = resources.storage else { + Issue.record("expected builder resources.storage to be set for --storage \(requestedStorage)") return } - let tolerance = expectedMiB / 10 #expect( - abs(reportedMiB - expectedMiB) <= tolerance, - "expected root filesystem size ≈ \(expectedMiB) MiB for storage \(requestedStorage), got \(reportedMiB) MiB" + storageBytes == expectedBytes, + "expected builder storage \(expectedBytes) bytes for \(requestedStorage), got \(storageBytes) bytes" ) } catch { Issue.record("failed to verify builder storage: \(error)") diff --git a/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift b/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift index d98ec0df..bbca3a3e 100644 --- a/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift +++ b/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift @@ -578,32 +578,31 @@ class TestCLIRunCommand: CLITest { } } - @Test func testRunCommandStorage() throws { + @Test + func testRunCommandStorage() async throws { do { let name = getTestName() let requestedStorage = "2048MB" let expectedMiB = Int(try Parser.memoryString(requestedStorage)) + let expectedBytes = UInt64(expectedMiB.mib()) try doLongRun(name: name, args: ["--storage", requestedStorage]) defer { try? doStop(name: name) } - var output = try doExec( - name: name, - cmd: ["sh", "-c", "df -m / | tail -1 | tr -s ' ' | cut -d' ' -f2"] - ) - output = output.trimmingCharacters(in: .whitespacesAndNewlines) + // Inspect configuration via the client instead of df + let container = try await ClientContainer.get(id: name) + let resources = container.configuration.resources - guard let reportedMiB = Int(output) else { - Issue.record("expected integer df output, got '\(output)'") + guard let storageBytes = resources.storage else { + Issue.record("expected container resources.storage to be set for --storage \(requestedStorage)") return } - let tolerance = expectedMiB / 10 #expect( - abs(reportedMiB - expectedMiB) <= tolerance, - "expected root filesystem size ≈ \(expectedMiB) MiB for storage \(requestedStorage), got \(reportedMiB) MiB" + storageBytes == expectedBytes, + "expected container storage \(expectedBytes) bytes for \(requestedStorage), got \(storageBytes) bytes" ) try doStop(name: name) From 5aaad530cc018ce7e89a1d37da2152d145d852ee Mon Sep 17 00:00:00 2001 From: karenheckel Date: Thu, 11 Dec 2025 18:54:39 -0600 Subject: [PATCH 15/15] Add storage configuration to container --- Sources/Services/ContainerSandboxService/SandboxService.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Services/ContainerSandboxService/SandboxService.swift b/Sources/Services/ContainerSandboxService/SandboxService.swift index 546fcdb9..1973c316 100644 --- a/Sources/Services/ContainerSandboxService/SandboxService.swift +++ b/Sources/Services/ContainerSandboxService/SandboxService.swift @@ -805,6 +805,7 @@ public actor SandboxService { ) throws { czConfig.cpus = config.resources.cpus czConfig.memoryInBytes = config.resources.memoryInBytes + czConfig.storageInBytes = config.resources.storage czConfig.sysctl = config.sysctls.reduce(into: [String: String]()) { $0[$1.key] = $1.value }