Skip to content

Commit 77c8836

Browse files
authored
Merge pull request #551 from Sahil-u07/improve-docker-compose
feat: improve docker-compose generation with restart, depends_on, network, and ZMQ mode
2 parents 61c70fb + a30127e commit 77c8836

4 files changed

Lines changed: 127 additions & 16 deletions

File tree

concore_cli/cli.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,16 @@ def init(name, template, interactive):
7575
is_flag=True,
7676
help="Generate docker-compose.yml in output directory (docker type only)",
7777
)
78-
def build(workflow_file, source, output, type, auto_build, compose):
78+
@click.option(
79+
"--zmq",
80+
is_flag=True,
81+
help="Configure compose for ZMQ networking mode (requires --compose)",
82+
)
83+
def build(workflow_file, source, output, type, auto_build, compose, zmq):
7984
"""Compile a concore workflow into executable scripts"""
85+
if zmq and not compose:
86+
console.print("[red]Error:[/red] --zmq requires --compose")
87+
sys.exit(1)
8088
try:
8189
build_workflow(
8290
workflow_file,
@@ -86,6 +94,7 @@ def build(workflow_file, source, output, type, auto_build, compose):
8694
auto_build,
8795
console,
8896
compose=compose,
97+
zmq_mode=zmq,
8998
)
9099
except Exception as e:
91100
console.print(f"[red]Error:[/red] {str(e)}")

concore_cli/commands/build.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,15 @@ def _parse_docker_run_line(line):
7575
}
7676

7777

78-
def _write_docker_compose(output_path):
78+
def _write_docker_compose(output_path, console, zmq_mode=False):
7979
run_script = output_path / "run"
8080
if not run_script.exists():
81+
console.print(
82+
f"[yellow]Warning:[/yellow] No docker run script found in {output_path}."
83+
)
84+
console.print(
85+
"[dim]Tip: run concore build --type docker first, then use --compose[/dim]"
86+
)
8187
return None
8288

8389
services = []
@@ -89,15 +95,10 @@ def _write_docker_compose(output_path):
8995
if not services:
9096
return None
9197

92-
compose_lines = [
93-
"networks:",
94-
" concore-net:",
95-
" driver: bridge",
96-
"",
97-
"services:",
98-
]
98+
compose_lines = ["services:"]
9999

100100
named_volumes = set()
101+
previous_service_name = None
101102
for index, service in enumerate(services, start=1):
102103
service_name = re.sub(r"[^A-Za-z0-9_.-]", "-", service["container_name"]).strip(
103104
"-."
@@ -107,14 +108,11 @@ def _write_docker_compose(output_path):
107108
elif not service_name[0].isalpha():
108109
service_name = f"service-{service_name}"
109110

110-
compose_lines.append(f" {_yaml_quote(service_name)}:")
111+
compose_lines.append(f" {service_name}:")
111112
compose_lines.append(f" image: {_yaml_quote(service['image'])}")
112113
compose_lines.append(
113114
f" container_name: {_yaml_quote(service['container_name'])}"
114115
)
115-
compose_lines.append(" restart: on-failure")
116-
compose_lines.append(" networks:")
117-
compose_lines.append(" - concore-net")
118116

119117
if service["volumes"]:
120118
compose_lines.append(" volumes:")
@@ -124,12 +122,28 @@ def _write_docker_compose(output_path):
124122
if re.match(r"^[a-zA-Z0-9_-]+$", part1):
125123
named_volumes.add(part1)
126124

125+
compose_lines.append(" restart: on-failure")
126+
if zmq_mode:
127+
compose_lines.append(" environment:")
128+
compose_lines.append(" - CONCORE_TRANSPORT=zmq")
129+
if index > 1 and previous_service_name:
130+
compose_lines.append(" depends_on:")
131+
compose_lines.append(f" - {previous_service_name}")
132+
compose_lines.append(" networks:")
133+
compose_lines.append(" - concore_net")
134+
previous_service_name = service_name
135+
127136
if named_volumes:
128137
compose_lines.append("")
129138
compose_lines.append("volumes:")
130139
for v in sorted(named_volumes):
131140
compose_lines.append(f" {v}:")
132141

142+
compose_lines.append("")
143+
compose_lines.append("networks:")
144+
compose_lines.append(" concore_net:")
145+
compose_lines.append(" driver: bridge")
146+
133147
compose_lines.append("")
134148
compose_path = output_path / "docker-compose.yml"
135149
compose_path.write_text("\n".join(compose_lines), encoding="utf-8")
@@ -144,6 +158,7 @@ def build_workflow(
144158
auto_build,
145159
console,
146160
compose=False,
161+
zmq_mode=False,
147162
):
148163
workflow_path = Path(workflow_file).resolve()
149164
source_path = Path(source).resolve()
@@ -238,7 +253,9 @@ def build_workflow(
238253
)
239254

240255
if compose:
241-
compose_path = _write_docker_compose(output_path)
256+
compose_path = _write_docker_compose(
257+
output_path, console, zmq_mode=zmq_mode
258+
)
242259
if compose_path is not None:
243260
console.print(
244261
f"[green]✓[/green] Compose file written to [cyan]{compose_path}[/cyan]"

tests/test_cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,8 +384,8 @@ def test_build_command_docker_compose_single_node(self):
384384
self.assertIn("container_name: 'N1'", compose_content)
385385
self.assertIn("image: 'docker-script'", compose_content)
386386
self.assertIn("networks:", compose_content)
387-
self.assertIn("concore-net:", compose_content)
388-
self.assertIn("- concore-net", compose_content)
387+
self.assertIn("concore_net:", compose_content)
388+
self.assertIn("- concore_net", compose_content)
389389
self.assertIn("restart: on-failure", compose_content)
390390

391391
metadata = json.loads(Path("out/STUDY.json").read_text())

tests/test_compose_generation.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from concore_cli.commands.build import _write_docker_compose
2+
from rich.console import Console
3+
from pathlib import Path
4+
5+
6+
def _fake_run_script(output_dir, services):
7+
lines = [
8+
f"docker run --name {s['name']} -v /study:/study {s['image']} &"
9+
for s in services
10+
]
11+
(Path(output_dir) / "run").write_text("\n".join(lines))
12+
13+
14+
def test_compose_has_restart_policy(tmp_path):
15+
_fake_run_script(tmp_path, [{"name": "node1", "image": "concore/py"}])
16+
path = _write_docker_compose(tmp_path, Console(quiet=True))
17+
assert path is not None
18+
content = path.read_text()
19+
assert "restart: on-failure" in content
20+
21+
22+
def test_compose_has_network_section(tmp_path):
23+
_fake_run_script(tmp_path, [{"name": "node1", "image": "concore/py"}])
24+
path = _write_docker_compose(tmp_path, Console(quiet=True))
25+
content = path.read_text()
26+
assert "concore_net" in content
27+
assert "networks:" in content
28+
29+
30+
def test_compose_depends_on_second_service(tmp_path):
31+
_fake_run_script(
32+
tmp_path,
33+
[
34+
{"name": "controller", "image": "concore/py"},
35+
{"name": "plant", "image": "concore/cpp"},
36+
],
37+
)
38+
path = _write_docker_compose(tmp_path, Console(quiet=True))
39+
content = path.read_text()
40+
assert "depends_on" in content
41+
assert "controller" in content
42+
43+
44+
def test_compose_first_service_has_no_depends_on(tmp_path):
45+
_fake_run_script(
46+
tmp_path,
47+
[
48+
{"name": "controller", "image": "concore/py"},
49+
{"name": "plant", "image": "concore/cpp"},
50+
],
51+
)
52+
path = _write_docker_compose(tmp_path, Console(quiet=True))
53+
lines = path.read_text().splitlines()
54+
controller_idx = next(i for i, line in enumerate(lines) if "controller:" in line)
55+
plant_idx = next(i for i, line in enumerate(lines) if "plant:" in line)
56+
section = lines[controller_idx:plant_idx]
57+
assert not any("depends_on" in line for line in section)
58+
59+
60+
def test_zmq_mode_adds_env(tmp_path):
61+
_fake_run_script(tmp_path, [{"name": "node1", "image": "concore/py"}])
62+
path = _write_docker_compose(tmp_path, Console(quiet=True), zmq_mode=True)
63+
content = path.read_text()
64+
assert "CONCORE_TRANSPORT=zmq" in content
65+
66+
67+
def test_no_zmq_env_in_default_mode(tmp_path):
68+
_fake_run_script(tmp_path, [{"name": "node1", "image": "concore/py"}])
69+
path = _write_docker_compose(tmp_path, Console(quiet=True), zmq_mode=False)
70+
content = path.read_text()
71+
assert "CONCORE_TRANSPORT" not in content
72+
73+
74+
def test_missing_run_script_returns_none(tmp_path):
75+
result = _write_docker_compose(tmp_path, Console(quiet=True))
76+
assert result is None
77+
78+
79+
def test_zmq_without_compose_errors():
80+
from click.testing import CliRunner
81+
from concore_cli.cli import cli
82+
83+
runner = CliRunner()
84+
result = runner.invoke(cli, ["build", "wf.graphml", "--zmq"])
85+
assert result.exit_code != 0

0 commit comments

Comments
 (0)