From 911789a5cc6839f9ffcc8ded99d0e307ed6b3008 Mon Sep 17 00:00:00 2001 From: DonsWayo Date: Wed, 17 Jun 2026 12:29:13 +0200 Subject: [PATCH 1/2] feat(up): stamp com.docker.compose project/service labels on containers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Containers created by `up` carry no labels, so tools that want to group a stack must guess from the `-` name prefix — which mis-groups unrelated containers that merely share a prefix. Stamp the de-facto-standard Docker Compose labels at creation: com.docker.compose.project= com.docker.compose.service= so any tool can group reliably via `container inspect`. No behavior change otherwise; down/ps continue to work by name. Verified on macOS 26 + container 1.0.0: after `up`, `container inspect -` shows both labels; `down` tears the stack down unchanged. --- Sources/Container-Compose/Commands/ComposeUp.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index 97a16f9..08026ea 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -449,6 +449,13 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { runCommandArgs.append("--name") runCommandArgs.append(containerName) + // Stamp Docker-Compose-compatible project/service labels so external tools (GUIs, + // dashboards) can group a stack's containers reliably by label, instead of guessing + // from the `-` name prefix (which mis-groups unrelated containers + // that merely share a prefix). + runCommandArgs.append(contentsOf: ["--label", "com.docker.compose.project=\(projectName)"]) + runCommandArgs.append(contentsOf: ["--label", "com.docker.compose.service=\(serviceName)"]) + // REMOVED: Restart policy is not supported by `container run` // if let restart = service.restart { // runCommandArgs.append("--restart") From ebefa65d007d5461a3a6df9502e30a92ee4e04b9 Mon Sep 17 00:00:00 2001 From: Cyb3rDudu <49803228+Cyb3rDudu@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:04:00 +0700 Subject: [PATCH 2/2] feat(up): generalize container labels + add label test Generalize the com.docker.compose.* label stamping introduced in this PR: - Model Service.labels: [String: String]? (property, CodingKeys, decoder, memberwise init), matching the existing Network/Volume pattern, so the Compose `labels:` directive on a service is no longer silently ignored. - In configService, pass user labels through as `--label key=value`, then stamp com.docker.compose.project/service on top so they take precedence over any user value for the same key. Keys are emitted sorted for a deterministic `container run` argv. Add a dynamic test (testUpStampsComposeLabels) plus dockerComposeYaml10 that asserts the compose labels are stamped, user labels pass through, and the stamped service label overrides a colliding user-defined value. swift build + swift build --build-tests both clean. --- .../Codable Structs/Service.swift | 11 +++++- .../Commands/ComposeUp.swift | 18 ++++++--- .../ComposeUpTests.swift | 37 +++++++++++++++++++ .../TestHelpers/DockerComposeYamlFiles.swift | 16 ++++++++ 4 files changed, 75 insertions(+), 7 deletions(-) diff --git a/Sources/Container-Compose/Codable Structs/Service.swift b/Sources/Container-Compose/Codable Structs/Service.swift index 8e84cb0..21392d8 100644 --- a/Sources/Container-Compose/Codable Structs/Service.swift +++ b/Sources/Container-Compose/Codable Structs/Service.swift @@ -65,6 +65,12 @@ public struct Service: Codable, Hashable { /// Explicit name for the container instance public let container_name: String? + /// User-defined labels applied to the container (e.g. `{ "foo": "bar" }`). + /// Passed through as `--label key=value`; the `com.docker.compose.project` and + /// `com.docker.compose.service` labels are additionally stamped by `ComposeUp` + /// and take precedence over any user value for those keys. + public let labels: [String: String]? + /// List of networks the service will connect to public let networks: [String]? @@ -104,7 +110,7 @@ public struct Service: Codable, Hashable { // Defines custom coding keys to map YAML keys to Swift properties enum CodingKeys: String, CodingKey { case image, build, deploy, restart, healthcheck, volumes, environment, env_file, ports, command, depends_on, user, - container_name, networks, hostname, entrypoint, privileged, read_only, working_dir, configs, secrets, stdin_open, tty, platform + container_name, labels, networks, hostname, entrypoint, privileged, read_only, working_dir, configs, secrets, stdin_open, tty, platform } /// Public memberwise initializer for testing @@ -122,6 +128,7 @@ public struct Service: Codable, Hashable { depends_on: [String]? = nil, user: String? = nil, container_name: String? = nil, + labels: [String: String]? = nil, networks: [String]? = nil, hostname: String? = nil, entrypoint: [String]? = nil, @@ -148,6 +155,7 @@ public struct Service: Codable, Hashable { self.depends_on = depends_on self.user = user self.container_name = container_name + self.labels = labels self.networks = networks self.hostname = hostname self.entrypoint = entrypoint @@ -218,6 +226,7 @@ public struct Service: Codable, Hashable { user = try container.decodeIfPresent(String.self, forKey: .user) container_name = try container.decodeIfPresent(String.self, forKey: .container_name) + labels = try container.decodeIfPresent([String: String].self, forKey: .labels) networks = try container.decodeIfPresent([String].self, forKey: .networks) hostname = try container.decodeIfPresent(String.self, forKey: .hostname) diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index 08026ea..f3a3359 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -449,12 +449,18 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { runCommandArgs.append("--name") runCommandArgs.append(containerName) - // Stamp Docker-Compose-compatible project/service labels so external tools (GUIs, - // dashboards) can group a stack's containers reliably by label, instead of guessing - // from the `-` name prefix (which mis-groups unrelated containers - // that merely share a prefix). - runCommandArgs.append(contentsOf: ["--label", "com.docker.compose.project=\(projectName)"]) - runCommandArgs.append(contentsOf: ["--label", "com.docker.compose.service=\(serviceName)"]) + // Apply any user-defined labels, then stamp Docker-Compose-compatible project/service + // labels so external tools (GUIs, dashboards) can group a stack's containers reliably + // by label instead of guessing from the `-` name prefix (which + // mis-groups unrelated containers that merely share a prefix). The compose labels are + // set last so they take precedence over a user value for the same key; keys are sorted + // for a deterministic `container run` argv. + var labels = service.labels ?? [:] + labels["com.docker.compose.project"] = projectName + labels["com.docker.compose.service"] = serviceName + for key in labels.keys.sorted() { + runCommandArgs.append(contentsOf: ["--label", "\(key)=\(labels[key] ?? "")"]) + } // REMOVED: Restart policy is not supported by `container run` // if let restart = service.restart { diff --git a/Tests/Container-Compose-DynamicTests/ComposeUpTests.swift b/Tests/Container-Compose-DynamicTests/ComposeUpTests.swift index 2d5b987..6de494b 100644 --- a/Tests/Container-Compose-DynamicTests/ComposeUpTests.swift +++ b/Tests/Container-Compose-DynamicTests/ComposeUpTests.swift @@ -375,6 +375,43 @@ struct ComposeUpTests { try? await stopInstance(location: upProject.base) } + @Test("up stamps compose project/service labels and passes user labels through") + func testUpStampsComposeLabels() async throws { + let explicitName = "container-compose-test-\(makeContainerName())" + let yaml = DockerComposeYamlFiles.dockerComposeYaml10(containerName: explicitName) + let project = try DockerComposeYamlFiles.copyYamlToTemporaryLocation(yaml: yaml) + + var composeUp = try ComposeUp.parse([ + "-d", "--cwd", project.base.path(percentEncoded: false), + ]) + try await composeUp.run() + + let container = try await ContainerClient().get(id: explicitName) + let labels = container.configuration.labels + + // Compose labels are stamped with the resolved project/service name. + #expect( + labels["com.docker.compose.project"] == project.name, + "Expected com.docker.compose.project='\(project.name)', got '\(labels["com.docker.compose.project"] ?? "nil")'" + ) + #expect( + labels["com.docker.compose.service"] == "web", + "Expected com.docker.compose.service='web', got '\(labels["com.docker.compose.service"] ?? "nil")'" + ) + // User-defined labels pass through to the container. + #expect( + labels["app"] == "container-compose-tests", + "Expected user label app='container-compose-tests', got '\(labels["app"] ?? "nil")'" + ) + // The stamped compose label wins over a user value for the same key. + #expect( + labels["com.docker.compose.service"] != "should-be-overridden", + "com.docker.compose.service should be overridden by the stamped value" + ) + + try? await stopInstance(location: project.base) + } + enum Errors: Error { case containerNotFound } diff --git a/Tests/TestHelpers/DockerComposeYamlFiles.swift b/Tests/TestHelpers/DockerComposeYamlFiles.swift index f5e28bc..8ca96e0 100644 --- a/Tests/TestHelpers/DockerComposeYamlFiles.swift +++ b/Tests/TestHelpers/DockerComposeYamlFiles.swift @@ -259,6 +259,22 @@ public struct DockerComposeYamlFiles { """ } + /// A single service with an explicit `container_name` plus user-defined `labels`, + /// one of which (`com.docker.compose.service`) collides with a stamped label so the + /// precedence behaviour can be exercised. + public static func dockerComposeYaml10(containerName: String) -> String { + return """ + version: '3.8' + services: + web: + image: nginx:alpine + container_name: \(containerName) + labels: + app: container-compose-tests + com.docker.compose.service: should-be-overridden + """ + } + /// Represents a temporary Docker Compose project copied to a temporary location for testing. public struct TemporaryProject { /// The URL of the temporary docker-compose.yaml file.