Skip to content

Commit a81ec7f

Browse files
Add deploy control group (#56)
* fix singularity check command. If validation fails exception was being thrown instead of printing error message * add support for instance replicas * fix test_no_circular_dependency test * reuse same sif image for replicas Signed-off-by: Paulo Miguel Almeida <paulo.miguel.almeida.rodenas@gmail.com>
1 parent 642b850 commit a81ec7f

File tree

9 files changed

+85
-38
lines changed

9 files changed

+85
-38
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and **Merged pull requests**. Critical items to know are:
1414
The versions coincide with releases on pypi.
1515

1616
## [0.1.x](https://github.com/singularityhub/singularity-compose/tree/master) (0.1.x)
17+
- add support for instance replicas (0.1.17)
1718
- fix check command validation (0.1.16)
1819
- fix a bug triggered when using startoptions in conjunction with network=false (0.1.15)
1920
- bind volumes can handle tilde expansion (0.1.14)

docs/spec/spec-2.0.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,22 @@ for `--fakeroot`:
168168

169169
You could also add "args" here within the start group to provide arguments to the start script.
170170

171+
## Deploy Group
172+
173+
### Replicas
174+
175+
By default `singularity-compose` will launch a single replica of each instance listed in the compose file.
176+
If your use-case requires multiple instances of the exact same configuration, you can use `deploy->replicas`
177+
option.
178+
179+
The example below will run 2 container instances with the same instance configuration.
180+
181+
```yaml
182+
instance:
183+
...
184+
deploy:
185+
replicas: 2
186+
```
171187

172188
## Environment
173189

scompose/config/schema.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ def validate_config(filepath):
9292
],
9393
}
9494

95+
instance_deploy = {
96+
"type": "object",
97+
"properties": {
98+
"replicas": {"type": "number", "minimum": 1},
99+
},
100+
}
101+
95102
# A single instance
96103
instance = {
97104
"type": "object",
@@ -107,6 +114,7 @@ def validate_config(filepath):
107114
"exec": instance_exec,
108115
"run": {"oneOf": [instance_run, {"type": "array"}]},
109116
"post": instance_post,
117+
"deploy": instance_deploy,
110118
},
111119
}
112120

scompose/project/instance.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class Instance(object):
3131
params: all of the parameters defined in the configuration.
3232
"""
3333

34-
def __init__(self, name, working_dir, sudo=False, params=None):
34+
def __init__(self, name, replica_number, working_dir, sudo=False, params=None):
3535

3636
if not params:
3737
params = {}
@@ -41,6 +41,7 @@ def __init__(self, name, working_dir, sudo=False, params=None):
4141
self.instance = None
4242
self.sudo = sudo
4343
self.set_name(name, params)
44+
self.replica_number = replica_number
4445

4546
# Start includes networking args and command
4647
self.set_start(params)
@@ -61,7 +62,7 @@ def __init__(self, name, working_dir, sudo=False, params=None):
6162
self.get()
6263

6364
def __str__(self):
64-
return "(instance:%s)" % self.name
65+
return "(instance:%s)" % self.get_replica_name()
6566

6667
def __repr__(self):
6768
return self.__str__()
@@ -77,9 +78,12 @@ def set_name(self, name, params):
7778
"""
7879
self.name = params.get("name", name)
7980

81+
def get_replica_name(self):
82+
return f"{self.name}{self.replica_number}"
83+
8084
@property
8185
def uri(self):
82-
return "instance://%s" % self.name
86+
return "instance://%s" % self.get_replica_name()
8387

8488
def set_context(self, params):
8589
"""set and validate parameters from the singularity-compose.yml,
@@ -385,24 +389,23 @@ def get_build_options(self):
385389
return options
386390

387391
# State
388-
389392
def exists(self):
390393
"""return boolean if an instance exists. We do this by way of listing
391394
instances, and so the calling user is important.
392395
"""
393396
instances = [x.name for x in self.client.instances(quiet=True, sudo=self.sudo)]
394-
return self.name in instances
397+
return self.get_replica_name() in instances
395398

396399
def get(self):
397400
"""If an instance exists, add to self.instance"""
398401
for instance in self.client.instances(quiet=True, sudo=self.sudo):
399-
if instance.name == self.name:
402+
if instance.name == self.get_replica_name():
400403
self.instance = instance
401404
break
402405

403406
def stop(self, timeout=None):
404407
"""delete the instance, if it exists. Singularity doesn't have delete
405-
or remove commands, everyting is a stop.
408+
or remove commands, everything is a stop.
406409
"""
407410
if self.instance:
408411
bot.info("Stopping %s" % self)
@@ -454,7 +457,9 @@ def clear_logs(self):
454457
log_folder = self._get_log_folder()
455458

456459
for ext in ["out", "err"]:
457-
logfile = os.path.join(log_folder, "%s.%s" % (self.name, ext.lower()))
460+
logfile = os.path.join(
461+
log_folder, "%s.%s" % (self.get_replica_name(), ext.lower())
462+
)
458463

459464
# Use Try/catch to account for not existing.
460465
try:
@@ -486,7 +491,9 @@ def logs(self, tail=0):
486491
log_folder = self._get_log_folder()
487492

488493
for ext in ["OUT", "ERR"]:
489-
logfile = os.path.join(log_folder, "%s.%s" % (self.name, ext.lower()))
494+
logfile = os.path.join(
495+
log_folder, "%s.%s" % (self.get_replica_name(), ext.lower())
496+
)
490497

491498
# Use Try/catch to account for not existing.
492499
try:
@@ -497,7 +504,9 @@ def logs(self, tail=0):
497504
# If the user only wants to see certain number
498505
if tail > 0:
499506
result = "\n".join(result.split("\n")[-tail:])
500-
bot.custom(prefix=self.name, message=ext, color="CYAN")
507+
bot.custom(
508+
prefix=self.get_replica_name(), message=ext, color="CYAN"
509+
)
501510
print(result)
502511
bot.newline()
503512

@@ -534,7 +543,7 @@ def create(self, ip_address=None, sudo=False, writable_tmpfs=False):
534543
# Finally, create the instance
535544
if not self.exists():
536545

537-
bot.info("Creating %s" % self.name)
546+
bot.info("Creating %s" % self.get_replica_name())
538547

539548
# Command options
540549
options = []
@@ -550,18 +559,23 @@ def create(self, ip_address=None, sudo=False, writable_tmpfs=False):
550559
options += self.start_opts
551560

552561
# Hostname
553-
options += ["--hostname", self.name]
562+
options += ["--hostname", self.get_replica_name()]
554563

555564
# Writable Temporary Directory
556565
if writable_tmpfs:
557566
options += ["--writable-tmpfs"]
558567

559568
# Show the command to the user
560-
commands = "%s %s %s %s" % (" ".join(options), image, self.name, self.args)
569+
commands = "%s %s %s %s" % (
570+
" ".join(options),
571+
image,
572+
self.get_replica_name(),
573+
self.args,
574+
)
561575
bot.debug("singularity instance start %s" % commands)
562576

563577
self.instance = self.client.instance(
564-
name=self.name,
578+
name=self.get_replica_name(),
565579
sudo=self.sudo,
566580
options=options,
567581
image=image,

scompose/project/project.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import os
2020
import re
2121
import subprocess
22+
from copy import deepcopy
2223

2324

2425
class Project(object):
@@ -193,14 +194,20 @@ def parse(self):
193194
# Create each instance object
194195
for name in self.config.get("instances", []):
195196
params = self.config["instances"][name]
196-
197-
# Validates params
198-
self.instances[name] = Instance(
199-
name=name,
200-
params=params,
201-
sudo=self.sudo,
202-
working_dir=self.working_dir,
203-
)
197+
replicas = params.get("deploy", {"replicas": 1})["replicas"]
198+
199+
# 1-indexed to mimic docker-compose behaviour
200+
for idx in range(1, replicas + 1):
201+
tmp_inst = Instance(
202+
name=name,
203+
replica_number=idx,
204+
# deepcopy is required otherwise changes to one replica would reflect on
205+
# others since they point to the same memory reference
206+
params=deepcopy(params),
207+
sudo=self.sudo,
208+
working_dir=self.working_dir,
209+
)
210+
self.instances[tmp_inst.get_replica_name()] = tmp_inst
204211

205212
self.instances = self._sort_instances(self.instances)
206213

@@ -223,8 +230,9 @@ def _sort_instances(self, instances):
223230
index = sorted_instances.index(instance)
224231

225232
for dep in depends_on:
226-
if not dep in sorted_instances:
227-
sorted_instances.insert(index, dep)
233+
for inst in instances.values():
234+
if dep == inst.name:
235+
sorted_instances.insert(index, inst.get_replica_name())
228236

229237
return {k: self.instances[k] for k in sorted_instances}
230238

@@ -251,7 +259,7 @@ def get_ip_lookup(self, names, bridge="10.22.0.0/16"):
251259
next(host_iter)
252260

253261
# If an instance is already running, we want to include it
254-
all_names = set(self.config["instances"].keys())
262+
all_names = set(self.get_instance_names())
255263
skip_addresses = [x["ip"] for name, x in self.running.items() if x["ip"]]
256264

257265
# Only use addresses not currently in use
@@ -500,7 +508,7 @@ def _create(
500508
names = names or self.get_instance_names()
501509

502510
# Keep track of created instances to determine if we have circular dependency structure
503-
created = []
511+
created = set()
504512
circular_dep = False
505513

506514
# Generate ip addresses for each
@@ -529,10 +537,10 @@ def _create(
529537
create_func(
530538
working_dir=self.working_dir,
531539
writable_tmpfs=writable_tmpfs,
532-
ip_address=lookup[instance.name],
540+
ip_address=lookup[instance.get_replica_name()],
533541
)
534542

535-
created.append(instance.name)
543+
created.add(instance.name)
536544

537545
# Run post create commands
538546
instance.run_post()

scompose/tests/test_client.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,17 @@ def test_commands(tmp_path):
5151
sleep(10)
5252

5353
print("Testing logs")
54-
project.logs(["httpd"], tail=20)
54+
project.logs(["httpd1"], tail=20)
5555

5656
print("Clearing logs")
57-
project.clear_logs(["httpd"])
58-
project.logs(["httpd"], tail=20)
57+
project.clear_logs(["httpd1"])
58+
project.logs(["httpd1"], tail=20)
5959

6060
print("Testing ps")
6161
project.ps()
6262

6363
print("Testing exec")
64-
project.execute("httpd", ["echo", "MarsBar"])
64+
project.execute("httpd1", ["echo", "MarsBar"])
6565

6666
# Ensure running
6767
print(requests.get("http://127.0.0.1").status_code)
@@ -70,6 +70,6 @@ def test_commands(tmp_path):
7070
project.down()
7171

7272
print("Testing ip lookup")
73-
lookup = project.get_ip_lookup(["httpd"])
74-
assert "httpd" in lookup
75-
assert lookup["httpd"] == "10.22.0.2"
73+
lookup = project.get_ip_lookup(["httpd1"])
74+
assert "httpd1" in lookup
75+
assert lookup["httpd1"] == "10.22.0.2"

scompose/tests/test_command_args.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,4 @@ def test_command_args(tmp_path):
5656
project.down()
5757

5858
log = bot.get_logs()
59-
assert "echo arg0 arg1 arg2" in log
59+
assert "arg0 arg1 arg2" in log

scompose/tests/test_depends_on.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def test_no_circular_dependency(tmp_path):
8989

9090
# Test depends_on DAG order
9191
keys = list(project.instances.keys())
92-
assert keys == ["first", "second", "third"]
92+
assert keys == ["first1", "second1", "third1"]
9393

9494
print("Testing up")
9595
project.up()

scompose/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
99
"""
1010

11-
__version__ = "0.1.16"
11+
__version__ = "0.1.17"
1212
AUTHOR = "Vanessa Sochat"
1313
AUTHOR_EMAIL = "vsoch@users.noreply.github.com"
1414
NAME = "singularity-compose"

0 commit comments

Comments
 (0)