Skip to content

Commit 289dbe9

Browse files
committed
Increased compatibility with docker-compose "depends_on" function
1 parent 294f236 commit 289dbe9

File tree

13 files changed

+791
-113
lines changed

13 files changed

+791
-113
lines changed

Plugins/container-compose/Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,11 @@ let package = Package(
6161
name: "ComposeTests",
6262
dependencies: [
6363
"ComposeCore",
64+
"ComposePlugin",
6465
.product(name: "Containerization", package: "containerization"),
6566
.product(name: "Logging", package: "swift-log"),
6667
],
6768
path: "Tests/ComposeTests"
6869
),
6970
]
70-
)
71+
)

Plugins/container-compose/Sources/CLI/ComposeCommand.swift

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -47,39 +47,47 @@ struct ComposeOptions: ParsableArguments {
4747
}
4848

4949
func getComposeFileURLs() -> [URL] {
50-
// If no files specified, use default
51-
let files = file.isEmpty ? ["docker-compose.yaml", "docker-compose.yml", "compose.yaml", "compose.yml"] : file
52-
53-
var urls: [URL] = []
5450
let currentPath = FileManager.default.currentDirectoryPath
5551

56-
for fileName in files {
57-
let url: URL
58-
if fileName.hasPrefix("/") {
59-
url = URL(fileURLWithPath: fileName)
60-
} else {
61-
url = URL(fileURLWithPath: currentPath).appendingPathComponent(fileName)
62-
}
63-
64-
// For default files, only add if they exist
65-
if file.isEmpty {
66-
if FileManager.default.fileExists(atPath: url.path) {
67-
urls.append(url)
68-
break // Use first found default file
69-
}
70-
} else {
71-
// For explicitly specified files, add them all (parser will check existence)
72-
urls.append(url)
52+
// If files were explicitly specified, return all of them (relative to cwd)
53+
if !file.isEmpty {
54+
return file.map { name in
55+
if name.hasPrefix("/") { return URL(fileURLWithPath: name) }
56+
return URL(fileURLWithPath: currentPath).appendingPathComponent(name)
7357
}
7458
}
7559

76-
// If no files found from defaults, return the first default for error message
77-
if urls.isEmpty && file.isEmpty {
78-
let defaultFile = URL(fileURLWithPath: currentPath).appendingPathComponent("docker-compose.yaml")
79-
urls.append(defaultFile)
60+
// Default behavior: detect base compose file and include matching override
61+
let candidates = [
62+
"docker-compose.yml",
63+
"docker-compose.yaml",
64+
"compose.yml",
65+
"compose.yaml",
66+
]
67+
68+
for base in candidates {
69+
let baseURL = URL(fileURLWithPath: currentPath).appendingPathComponent(base)
70+
if FileManager.default.fileExists(atPath: baseURL.path) {
71+
var urls = [baseURL]
72+
// Include override for the chosen base
73+
let overrideCandidates: [String]
74+
if base.hasPrefix("docker-compose") {
75+
overrideCandidates = ["docker-compose.override.yml", "docker-compose.override.yaml"]
76+
} else {
77+
overrideCandidates = ["compose.override.yml", "compose.override.yaml"]
78+
}
79+
for o in overrideCandidates {
80+
let oURL = URL(fileURLWithPath: currentPath).appendingPathComponent(o)
81+
if FileManager.default.fileExists(atPath: oURL.path) {
82+
urls.append(oURL)
83+
}
84+
}
85+
return urls
86+
}
8087
}
8188

82-
return urls
89+
// Nothing found: return a sensible default path for better error message downstream
90+
return [URL(fileURLWithPath: currentPath).appendingPathComponent("docker-compose.yml")]
8391
}
8492

8593
func setEnvironmentVariables() {
@@ -90,4 +98,28 @@ struct ComposeOptions: ParsableArguments {
9098
}
9199
}
92100
}
101+
102+
/// Load .env from current working directory and export vars into process env
103+
/// Compose uses .env for interpolation; we approximate by exporting to env before parsing
104+
func loadDotEnvIfPresent() {
105+
let cwd = FileManager.default.currentDirectoryPath
106+
let dotEnvURL = URL(fileURLWithPath: cwd).appendingPathComponent(".env")
107+
guard FileManager.default.fileExists(atPath: dotEnvURL.path) else { return }
108+
if let contents = try? String(contentsOf: dotEnvURL, encoding: .utf8) {
109+
for line in contents.split(whereSeparator: { $0.isNewline }) {
110+
var s = String(line).trimmingCharacters(in: .whitespaces)
111+
if s.isEmpty || s.hasPrefix("#") { continue }
112+
if s.hasPrefix("export ") { s.removeFirst("export ".count) }
113+
let parts = s.split(separator: "=", maxSplits: 1)
114+
if parts.count == 2 {
115+
let key = String(parts[0]).trimmingCharacters(in: .whitespaces)
116+
var val = String(parts[1]).trimmingCharacters(in: .whitespaces)
117+
if (val.hasPrefix("\"") && val.hasSuffix("\"")) || (val.hasPrefix("'") && val.hasSuffix("'")) {
118+
val = String(val.dropFirst().dropLast())
119+
}
120+
setenv(key, val, 1) // override to ensure deterministic interpolation in this process
121+
}
122+
}
123+
}
124+
}
93125
}

Plugins/container-compose/Sources/CLI/ComposeDown.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ struct ComposeDown: AsyncParsableCommand {
4141

4242
func run() async throws {
4343
// Set environment variables
44+
composeOptions.loadDotEnvIfPresent()
4445
composeOptions.setEnvironmentVariables()
4546

4647
// Parse compose file
@@ -73,6 +74,7 @@ struct ComposeDown: AsyncParsableCommand {
7374
try await orchestrator.down(
7475
project: project,
7576
removeVolumes: volumes,
77+
removeOrphans: removeOrphans,
7678
progressHandler: progress.handler
7779
)
7880

Plugins/container-compose/Sources/CLI/ComposeUp.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ struct ComposeUp: AsyncParsableCommand {
5353

5454
func run() async throws {
5555
// Set environment variables
56+
composeOptions.loadDotEnvIfPresent()
5657
composeOptions.setEnvironmentVariables()
5758

5859
// Parse compose files
@@ -88,13 +89,43 @@ struct ComposeUp: AsyncParsableCommand {
8889
detach: detach,
8990
forceRecreate: forceRecreate,
9091
noRecreate: noRecreate,
92+
noDeps: noDeps,
93+
removeOrphans: removeOrphans,
9194
progressHandler: progress.handler
9295
)
9396

9497
progress.finish()
9598

99+
// Call out DNS names for service discovery inside the container network
100+
if !project.services.isEmpty {
101+
print("Service DNS names:")
102+
for (name, svc) in project.services.sorted(by: { $0.key < $1.key }) {
103+
let cname = svc.containerName ?? "\(project.name)_\(name)"
104+
print("- \(name): \(cname)")
105+
}
106+
}
107+
96108
if detach {
97109
print("Started project '\(project.name)' in detached mode")
110+
} else {
111+
// Stream logs for selected services (or all if none selected), similar to docker-compose up
112+
let orchestrator = Orchestrator(log: log)
113+
let logStream = try await orchestrator.logs(
114+
project: project,
115+
services: services,
116+
follow: true,
117+
tail: nil,
118+
timestamps: false
119+
)
120+
for try await entry in logStream {
121+
let output = "[\(entry.serviceName)] \(entry.message)"
122+
switch entry.stream {
123+
case .stdout:
124+
print(output)
125+
case .stderr:
126+
FileHandle.standardError.write(Data((output + "\n").utf8))
127+
}
128+
}
98129
}
99130
}
100131
}

Plugins/container-compose/Sources/Core/Models/Project.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ public struct Service: Sendable {
4646
public let volumes: [VolumeMount]
4747
public let networks: [String]
4848
public let dependsOn: [String]
49+
public let dependsOnHealthy: [String]
50+
public let dependsOnStarted: [String]
51+
public let dependsOnCompletedSuccessfully: [String]
4952
public let healthCheck: HealthCheck?
5053
public let deploy: Deploy?
5154
public let restart: String?
@@ -67,6 +70,9 @@ public struct Service: Sendable {
6770
volumes: [VolumeMount] = [],
6871
networks: [String] = [],
6972
dependsOn: [String] = [],
73+
dependsOnHealthy: [String] = [],
74+
dependsOnStarted: [String] = [],
75+
dependsOnCompletedSuccessfully: [String] = [],
7076
healthCheck: HealthCheck? = nil,
7177
deploy: Deploy? = nil,
7278
restart: String? = nil,
@@ -85,6 +91,9 @@ public struct Service: Sendable {
8591
self.volumes = volumes
8692
self.networks = networks
8793
self.dependsOn = dependsOn
94+
self.dependsOnHealthy = dependsOnHealthy
95+
self.dependsOnStarted = dependsOnStarted
96+
self.dependsOnCompletedSuccessfully = dependsOnCompletedSuccessfully
8897
self.healthCheck = healthCheck
8998
self.deploy = deploy
9099
self.restart = restart
@@ -145,6 +154,10 @@ public struct PortMapping: Sendable {
145154
default:
146155
return nil
147156
}
157+
// Validate numeric ports
158+
guard let host = Int(self.hostPort), let container = Int(self.containerPort), host > 0, container > 0, host <= 65535, container <= 65535 else {
159+
return nil
160+
}
148161
}
149162
}
150163

@@ -283,4 +296,4 @@ public struct Volume: Sendable {
283296
self.driver = driver
284297
self.external = external
285298
}
286-
}
299+
}

0 commit comments

Comments
 (0)