From 294aba38df49893952749e01c8d044b486c23bf5 Mon Sep 17 00:00:00 2001 From: cstea Date: Thu, 19 Feb 2026 18:30:31 -0600 Subject: [PATCH] feat: Support for --cpus and --memory resource options --- .../Commands/ComposeUp.swift | 8 +++++ .../ComposeUpTests.swift | 33 +++++++++++++++++++ .../DockerComposeParsingTests.swift | 21 ++++++++++++ 3 files changed, 62 insertions(+) diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index 1f1ee46..d274ff9 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -489,6 +489,14 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { runCommandArgs.append("--read-only") } + // Add resource limits + if let cpus = service.deploy?.resources?.limits?.cpus { + runCommandArgs.append(contentsOf: ["--cpus", cpus]) + } + if let memory = service.deploy?.resources?.limits?.memory { + runCommandArgs.append(contentsOf: ["--memory", memory]) + } + // Handle service-level configs (note: still only parsing/logging, not attaching) if let serviceConfigs = service.configs { print( diff --git a/Tests/Container-Compose-DynamicTests/ComposeUpTests.swift b/Tests/Container-Compose-DynamicTests/ComposeUpTests.swift index d8df8d3..779b789 100644 --- a/Tests/Container-Compose-DynamicTests/ComposeUpTests.swift +++ b/Tests/Container-Compose-DynamicTests/ComposeUpTests.swift @@ -234,6 +234,39 @@ struct ComposeUpTests { #expect(webContainer.status == .running) #expect(dbContainer.status == .running) } + + @Test("Test container created with non-default CPU and memory limits") + func testCpuAndMemoryLimits() async throws { + let yaml = """ + version: "3.8" + services: + app: + image: nginx:alpine + deploy: + resources: + limits: + cpus: "1" + memory: "512MB" + """ + + let tempLocation = URL.temporaryDirectory.appending(path: "Container-Compose_Tests_\(UUID().uuidString)/docker-compose.yaml") + try? FileManager.default.createDirectory(at: tempLocation.deletingLastPathComponent(), withIntermediateDirectories: true) + try yaml.write(to: tempLocation, atomically: false, encoding: .utf8) + let folderName = tempLocation.deletingLastPathComponent().lastPathComponent + + var composeUp = try ComposeUp.parse(["-d", "--cwd", tempLocation.deletingLastPathComponent().path(percentEncoded: false)]) + try await composeUp.run() + + let containers = try await ClientContainer.list() + .filter { $0.configuration.id.contains(folderName) } + + guard let appContainer = containers.first(where: { $0.configuration.id == "\(folderName)-app" }) else { + throw Errors.containerNotFound + } + + #expect(appContainer.configuration.resources.cpus == 1) + #expect(appContainer.configuration.resources.memoryInBytes == 512.mib()) + } enum Errors: Error { case containerNotFound diff --git a/Tests/Container-Compose-StaticTests/DockerComposeParsingTests.swift b/Tests/Container-Compose-StaticTests/DockerComposeParsingTests.swift index 2a237b3..2749fe7 100644 --- a/Tests/Container-Compose-StaticTests/DockerComposeParsingTests.swift +++ b/Tests/Container-Compose-StaticTests/DockerComposeParsingTests.swift @@ -378,6 +378,27 @@ struct DockerComposeParsingTests { #expect(compose.services["app"]??.platform == "linux/amd64") } + + @Test("Parse deploy resources limits (cpus and memory)") + func parseComposeWithDeployResources() throws { + let yaml = """ + version: '3.8' + services: + app: + image: alpine:latest + deploy: + resources: + limits: + cpus: "0.5" + memory: "512M" + """ + + let decoder = YAMLDecoder() + let compose = try decoder.decode(DockerCompose.self, from: yaml) + + #expect(compose.services["app"]??.deploy?.resources?.limits?.cpus == "0.5") + #expect(compose.services["app"]??.deploy?.resources?.limits?.memory == "512M") + } @Test("Service must have image or build - should fail without either") func serviceRequiresImageOrBuild() throws {