Skip to content

Commit c48ed09

Browse files
Fix: Environment variables can be duplicated on run (#1218)
## Type of Change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Documentation update ## Motivation and Context As reported in [1212](#1212), environment variables defined in a Dockerfile (ENV) were not being overridden when passed via -e at runtime, resulting in duplicate entries in the container's environment. **Root cause:** `Parser.allEnv()` simply merged image, env-file, and user-provided environment variables without deduplication. Since `getenv()` returns the first match, the image default took precedence over the user override. **Fix:** `allEnv()` now deduplicates by key, with later sources overriding earlier ones ## Testing - [x] Tested locally - [x] Added/updated tests - [ ] Added/updated docs
1 parent a409c0f commit c48ed09

2 files changed

Lines changed: 47 additions & 5 deletions

File tree

Sources/Services/ContainerAPIService/Client/Parser.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,20 @@ public struct Parser {
9898
}
9999

100100
public static func allEnv(imageEnvs: [String], envFiles: [String], envs: [String]) throws -> [String] {
101-
var output: [String] = []
102-
output.append(contentsOf: Parser.env(envList: imageEnvs))
101+
var combined: [String] = []
102+
combined.append(contentsOf: Parser.env(envList: imageEnvs))
103103
for envFile in envFiles {
104104
let content = try Parser.envFile(path: envFile)
105-
output.append(contentsOf: content)
105+
combined.append(contentsOf: content)
106106
}
107-
output.append(contentsOf: Parser.env(envList: envs))
108-
return output
107+
combined.append(contentsOf: Parser.env(envList: envs))
108+
109+
let deduped = combined.reduce(into: [String: String]()) { map, entry in
110+
let key = String(entry.split(separator: "=", maxSplits: 1).first ?? Substring(entry))
111+
map[key] = entry
112+
}
113+
114+
return deduped.map { $0.value }
109115
}
110116

111117
public static func envFile(path: String) throws -> [String] {

Tests/ContainerAPIClientTests/ParserTest.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,42 @@ struct ParserTest {
553553
#expect(result == ["EMPTY="])
554554
}
555555

556+
@Test
557+
func testAllEnvUserOverridesImage() throws {
558+
let result = try Parser.allEnv(
559+
imageEnvs: ["FOO=fromimage", "BAR=kept"],
560+
envFiles: [],
561+
envs: ["FOO=fromuser"]
562+
)
563+
#expect(Set(result) == Set(["FOO=fromuser", "BAR=kept"]))
564+
}
565+
566+
@Test
567+
func testAllEnvFileOverridesImage() throws {
568+
let tmpFile = try tmpFileWithContent("FOO=fromfile\n")
569+
defer { try? FileManager.default.removeItem(at: tmpFile) }
570+
571+
let result = try Parser.allEnv(
572+
imageEnvs: ["FOO=fromimage", "BAR=kept"],
573+
envFiles: [tmpFile.path],
574+
envs: []
575+
)
576+
#expect(Set(result) == Set(["FOO=fromfile", "BAR=kept"]))
577+
}
578+
579+
@Test
580+
func testAllEnvUserOverridesFileOverridesImage() throws {
581+
let tmpFile = try tmpFileWithContent("FOO=fromfile\nBAZ=fromfile\n")
582+
defer { try? FileManager.default.removeItem(at: tmpFile) }
583+
584+
let result = try Parser.allEnv(
585+
imageEnvs: ["FOO=fromimage", "BAR=fromimage"],
586+
envFiles: [tmpFile.path],
587+
envs: ["FOO=fromuser"]
588+
)
589+
#expect(Set(result) == Set(["FOO=fromuser", "BAR=fromimage", "BAZ=fromfile"]))
590+
}
591+
556592
private func tmpFileWithContent(_ content: String) throws -> URL {
557593
let tempDir = FileManager.default.temporaryDirectory
558594
let tempFile = tempDir.appendingPathComponent("envfile-test-\(UUID().uuidString)")

0 commit comments

Comments
 (0)