diff --git a/Sources/Services/ContainerAPIService/Client/Parser.swift b/Sources/Services/ContainerAPIService/Client/Parser.swift index ef209df5c..272469284 100644 --- a/Sources/Services/ContainerAPIService/Client/Parser.swift +++ b/Sources/Services/ContainerAPIService/Client/Parser.swift @@ -245,7 +245,9 @@ public struct Parser { if label.isEmpty { throw ContainerizationError(.invalidArgument, message: "label cannot be an empty string") } - let parts = label.split(separator: "=", maxSplits: 2) + // Split on the first `=` only, so a value may itself contain `=` + // (e.g. `--label 'rule=Host(`ex.com`)'`), matching `docker run --label`. + let parts = label.split(separator: "=", maxSplits: 1) switch parts.count { case 1: result[String(parts[0])] = "" diff --git a/Tests/ContainerAPIClientTests/ParserTest.swift b/Tests/ContainerAPIClientTests/ParserTest.swift index 0dcc6f7cf..f05049dd0 100644 --- a/Tests/ContainerAPIClientTests/ParserTest.swift +++ b/Tests/ContainerAPIClientTests/ParserTest.swift @@ -1348,6 +1348,22 @@ struct ParserTest { #expect(result["key99"] == "value99") } + @Test("label value may contain '=' (split on first '=' only)") + func testLabelValueContainingEquals() throws { + let result = try Parser.labels([ + "config=key=value", + "traefik.http.routers.x.rule=Host(`ex.com`)", + "plain=value", + "novalue", + "emptyvalue=", + ]) + #expect(result["config"] == "key=value") + #expect(result["traefik.http.routers.x.rule"] == "Host(`ex.com`)") + #expect(result["plain"] == "value") + #expect(result["novalue"] == "") + #expect(result["emptyvalue"] == "") + } + @Test("resolve with large input preserves all entries") func testParseKeyValuePairsLargeInput() { let pairs = (0..<100).map { "key\($0)=value\($0)" }