Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Sources/ComposeEngine/ComposeOrchestrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion Sources/ComposeEngine/Models/ComposeModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
26 changes: 26 additions & 0 deletions Tests/ComposeEngineTests/ComposeMultiFileTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading