diff --git a/Sources/ComposeEngine/ComposeOrchestrator.swift b/Sources/ComposeEngine/ComposeOrchestrator.swift index 4f5931b..12f4a88 100644 --- a/Sources/ComposeEngine/ComposeOrchestrator.swift +++ b/Sources/ComposeEngine/ComposeOrchestrator.swift @@ -87,9 +87,14 @@ public struct ComposeOrchestrator: Sendable { return "\(sanitize(project))-\(sanitize(service))-\(replica)" } - /// How many replicas a service runs: the `--scale` value when given, else `scale:`, else 1. + /// How many replicas a service runs. The `--scale` value wins, then a + /// non-default `scale:`, then `deploy.replicas`, then 1. public static func replicaCount(for serviceName: String, in project: ComposeProject, scales: [String: Int]) -> Int { - max(1, scales[serviceName] ?? project.services[serviceName]?.scale ?? 1) + let service = project.services[serviceName] + if let scaled = scales[serviceName] { return max(1, scaled) } + if let scale = service?.scale, scale != 1 { return max(1, scale) } + if let replicas = service?.deploy?.replicas { return max(1, replicas) } + return 1 } /// The image reference for a service: its `image:` when present, otherwise diff --git a/Sources/ComposeEngine/Models/ComposeModels.swift b/Sources/ComposeEngine/Models/ComposeModels.swift index 45f2a37..5d8deaf 100644 --- a/Sources/ComposeEngine/Models/ComposeModels.swift +++ b/Sources/ComposeEngine/Models/ComposeModels.swift @@ -798,18 +798,23 @@ public struct HealthCheckConfig: Sendable, Codable, Equatable { /// The deploy section (only resource limits are honored). public struct DeployConfig: Sendable, Codable, Equatable { public var resources: DeployResources? + /// Number of containers to run for the service (used when `scale:` is not set). + public var replicas: Int? - public init(resources: DeployResources? = nil) { + public init(resources: DeployResources? = nil, replicas: Int? = nil) { self.resources = resources + self.replicas = replicas } enum CodingKeys: String, CodingKey { case resources + case replicas } public init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) resources = try c.decodeIfPresent(DeployResources.self, forKey: .resources) + replicas = try c.decodeIfPresent(Int.self, forKey: .replicas) } } diff --git a/Tests/ComposeEngineTests/ComposeMultiFileTests.swift b/Tests/ComposeEngineTests/ComposeMultiFileTests.swift index 302315e..d33c766 100644 --- a/Tests/ComposeEngineTests/ComposeMultiFileTests.swift +++ b/Tests/ComposeEngineTests/ComposeMultiFileTests.swift @@ -94,3 +94,29 @@ final class ComposeReplicaTests: XCTestCase { XCTAssertEqual(ComposeOrchestrator.replicaCount(for: "db", in: project, scales: ["db": 0]), 1) } } + +final class ComposeDeployReplicasTests: XCTestCase { + func testDeployReplicasUsedWhenScaleIsDefault() throws { + let project = try ComposeParser().parse(yaml: """ + services: + web: + image: nginx + deploy: + replicas: 3 + """) + XCTAssertEqual(ComposeOrchestrator.replicaCount(for: "web", in: project, scales: [:]), 3) + XCTAssertEqual(ComposeOrchestrator.replicaCount(for: "web", in: project, scales: ["web": 2]), 2) + } + + func testExplicitScaleBeatsDeployReplicas() throws { + let project = try ComposeParser().parse(yaml: """ + services: + web: + image: nginx + scale: 4 + deploy: + replicas: 3 + """) + XCTAssertEqual(ComposeOrchestrator.replicaCount(for: "web", in: project, scales: [:]), 4) + } +}