Skip to content

Commit e301c95

Browse files
authored
Merge pull request #59 from singularityhub/add/background
adding support for background
2 parents d4f71bb + c045e87 commit e301c95

File tree

6 files changed

+102
-34
lines changed

6 files changed

+102
-34
lines changed

docs/spec/spec-2.0.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,29 @@ And if you want args or options, you can again add them:
248248
- "env-file=myvars.env"
249249
```
250250

251-
The run and exec sections are separate to allow you to run both, or either without
251+
As of version 0.1.17, you can also ask the run command to be placed in the background.
252+
Here is an example that starts a notebook and then still is able to execute a start adn run command:
253+
254+
```yaml
255+
version: "2.0"
256+
instances:
257+
jupyter:
258+
image: docker://umids/jupyterlab
259+
volumes:
260+
- ./work:/usr/local/share/jupyter/lab/settings/
261+
ports:
262+
- 8888:8888
263+
run:
264+
background: true
265+
second:
266+
build:
267+
context: ./second
268+
run: []
269+
depends_on:
270+
- jupyter
271+
```
272+
273+
This full example is provided under [singularity-compose-examples](https://github.com/singularityhub/singularity-compose-examples/tree/master/v2.0/jupyterlab). Finally, note that the run and exec sections are separate to allow you to run both, or either without
252274
the other.
253275

254276
## Instance

scompose/config/schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def validate_config(filepath):
6565
"type": "object",
6666
"properties": {
6767
"args": {"type": ["string", "array"]},
68+
"background": {"type": "boolean"},
6869
"options": string_list,
6970
},
7071
}

scompose/logger/progress.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
ETA_SMA_WINDOW = 9
2626

2727

28-
class ProgressBar(object):
28+
class ProgressBar:
2929
def __enter__(self):
3030
return self
3131

scompose/project/instance.py

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
import os
1717
import platform
1818
import re
19+
import time
1920

2021

21-
class Instance(object):
22-
"""A section of a singularity-compose.yml, typically includes an image
22+
class Instance:
23+
"""
24+
A section of a singularity-compose.yml, typically includes an image
2325
name, volumes, build directory, and any ports or environment variables
2426
relevant to the instance.
2527
@@ -85,6 +87,16 @@ def get_replica_name(self):
8587
def uri(self):
8688
return "instance://%s" % self.get_replica_name()
8789

90+
@property
91+
def run_background(self):
92+
"""
93+
Determine if the process should be run in the background.
94+
"""
95+
run = self.params.get("run", {}) or {}
96+
if isinstance(run, list):
97+
return False
98+
return run.get("background") or False
99+
88100
def set_context(self, params):
89101
"""set and validate parameters from the singularity-compose.yml,
90102
including build (context and recipe). We don't pull or create
@@ -127,12 +139,15 @@ def set_context(self, params):
127139
# Volumes and Ports
128140

129141
def set_volumes(self, params):
130-
"""set volumes from the recipe"""
142+
"""
143+
Set volumes from the recipe
144+
"""
131145
self.volumes = params.get("volumes", [])
132146
self._volumes_from = params.get("volumes_from", [])
133147

134148
def set_volumes_from(self, instances):
135-
"""volumes from is called after all instances are read in, and
149+
"""
150+
Volumes from is called after all instances are read in, and
136151
then volumes can be mapped (and shared) with both containers.
137152
with Docker, this is done with isolation, but for Singularity
138153
we will try sharing a bind on the host.
@@ -149,7 +164,9 @@ def set_volumes_from(self, instances):
149164
self.volumes.append(volume)
150165

151166
def set_network(self, params):
152-
"""set network from the recipe to be used"""
167+
"""
168+
Set network from the recipe to be used
169+
"""
153170
self.network = params.get("network", {})
154171

155172
# if not specified, set the default value for the property
@@ -188,13 +205,15 @@ def set_run(self, params):
188205
self.run_opts = self._get_command_opts(run_group.get("options", []))
189206

190207
def _get_command_opts(self, group):
191-
"""Given a string of arguments or options, parse into a list with
208+
"""
209+
Given a string of arguments or options, parse into a list with
192210
proper flags added.
193211
"""
194212
return ["--%s" % opt if len(opt) > 1 else "-%s" % opt for opt in group]
195213

196214
def _get_network_commands(self, ip_address=None):
197-
"""take a list of ports, return the list of --network-args to
215+
"""
216+
Take a list of ports, return the list of --network-args to
198217
ensure they are bound correctly.
199218
"""
200219
ports = ["--net"]
@@ -216,7 +235,9 @@ def _get_network_commands(self, ip_address=None):
216235
return ports
217236

218237
def _get_bind_commands(self):
219-
"""take a list of volumes, and return the bind commands for Singularity"""
238+
"""
239+
Take a list of volumes, and return the bind commands for Singularity
240+
"""
220241
binds = []
221242
for volume in self.volumes:
222243
src, dest = volume.split(":")
@@ -237,7 +258,8 @@ def _get_bind_commands(self):
237258
return binds
238259

239260
def run_post(self):
240-
"""run post create commands. Can be added to an instance definition
261+
"""
262+
Run post create commands. Can be added to an instance definition
241263
either to run a command directly, or execute a script. The path
242264
is assumed to be on the host.
243265
@@ -272,7 +294,8 @@ def run_post(self):
272294
# Image
273295

274296
def get_image(self):
275-
"""get the associated instance image name, to be built if it doesn't
297+
"""
298+
Get the associated instance image name, to be built if it doesn't
276299
exit. It can either be defined at the config from self.image, or
277300
ultimately generated via a pull from a uri.
278301
"""
@@ -294,7 +317,8 @@ def get_image(self):
294317
# Build
295318

296319
def build(self, working_dir):
297-
"""build an image if called for based on having a recipe and context.
320+
"""
321+
Build an image if called for based on having a recipe and context.
298322
Otherwise, pull a container uri to the instance workspace.
299323
"""
300324
sif_binary = self.get_image()
@@ -363,7 +387,8 @@ def build(self, working_dir):
363387
bot.exit("neither image and build defined for %s" % self.name)
364388

365389
def get_build_options(self):
366-
"""'get build options will parse through params, and return build
390+
"""
391+
Get build options will parse through params, and return build
367392
options (if they exist)
368393
"""
369394
options = []
@@ -390,7 +415,8 @@ def get_build_options(self):
390415

391416
# State
392417
def exists(self):
393-
"""return boolean if an instance exists. We do this by way of listing
418+
"""
419+
Return boolean if an instance exists. We do this by way of listing
394420
instances, and so the calling user is important.
395421
"""
396422
instances = [x.name for x in self.client.instances(quiet=True, sudo=self.sudo)]
@@ -404,7 +430,8 @@ def get(self):
404430
break
405431

406432
def stop(self, timeout=None):
407-
"""delete the instance, if it exists. Singularity doesn't have delete
433+
"""
434+
Delete the instance, if it exists. Singularity doesn't have delete
408435
or remove commands, everything is a stop.
409436
"""
410437
if self.instance:
@@ -415,7 +442,8 @@ def stop(self, timeout=None):
415442
# Networking
416443

417444
def get_address(self):
418-
"""get the bridge address of an image. If it's busybox, we can't use
445+
"""
446+
Get the bridge address of an image. If it's busybox, we can't use
419447
hostname -I.
420448
"""
421449
ip_address = None
@@ -453,7 +481,9 @@ def get_address(self):
453481
# Logs
454482

455483
def clear_logs(self):
456-
"""delete logs for an instance, if they exist."""
484+
"""
485+
Delete logs for an instance, if they exist.
486+
"""
457487
log_folder = self._get_log_folder()
458488

459489
for ext in ["out", "err"]:
@@ -473,7 +503,9 @@ def clear_logs(self):
473503
pass
474504

475505
def _get_log_folder(self):
476-
"""get a log folder that includes a user, home, and host"""
506+
"""
507+
Get a log folder that includes a user, home, and host
508+
"""
477509
home = get_userhome()
478510
user = os.path.basename(home)
479511

@@ -486,7 +518,9 @@ def _get_log_folder(self):
486518
return os.path.join(home, ".singularity", "instances", "logs", hostname, user)
487519

488520
def logs(self, tail=0):
489-
"""show logs for an instance"""
521+
"""
522+
Show logs for an instance
523+
"""
490524

491525
log_folder = self._get_log_folder()
492526

@@ -516,7 +550,8 @@ def logs(self, tail=0):
516550
# Create and Delete
517551

518552
def up(self, working_dir, ip_address=None, writable_tmpfs=False):
519-
"""up is the same as create, but like Docker, we build / pull instances
553+
"""
554+
Up is the same as create, but like Docker, we build / pull instances
520555
first.
521556
"""
522557
image = self.get_image() or ""
@@ -527,7 +562,9 @@ def up(self, working_dir, ip_address=None, writable_tmpfs=False):
527562
self.create(writable_tmpfs=writable_tmpfs, ip_address=ip_address)
528563

529564
def create(self, ip_address=None, sudo=False, writable_tmpfs=False):
530-
"""create an instance, if it doesn't exist."""
565+
"""
566+
Create an instance, if it doesn't exist.
567+
"""
531568
image = self.get_image()
532569

533570
# Case 1: No build context or image defined
@@ -605,19 +642,25 @@ def create(self, ip_address=None, sudo=False, writable_tmpfs=False):
605642
# If the user has run defined, finish with the run
606643
if "run" in self.params:
607644

645+
run_args = self.run_args or ""
646+
608647
# Show the command to the user
609648
commands = "%s %s %s" % (
610649
" ".join(self.run_opts),
611650
self.uri,
612-
self.run_args or "",
651+
run_args,
613652
)
614-
bot.debug("singularity run %s" % commands)
615653

616-
for line in self.client.run(
617-
image=self.instance,
618-
args=self.run_args,
619-
sudo=self.sudo,
620-
stream=True,
621-
options=self.run_opts,
654+
bot.debug("singularity run %s" % commands)
655+
for line in (
656+
self.client.run(
657+
image=self.instance,
658+
args=run_args,
659+
sudo=self.sudo,
660+
stream=True,
661+
options=self.run_opts,
662+
background=self.run_background,
663+
)
664+
or []
622665
):
623-
print(line)
666+
print(line.strip("\n"))

scompose/project/project.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
from copy import deepcopy
2323

2424

25-
class Project(object):
26-
"""A compose project is a group of containers read in from a config file."""
25+
class Project:
26+
"""
27+
A compose project is a group of containers read in from a config file.
28+
"""
2729

2830
config = None
2931
instances = {}

scompose/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323

2424
INSTALL_REQUIRES = (
25-
("spython", {"min_version": "0.1.1"}),
25+
("spython", {"min_version": "0.2.11"}),
2626
("pyaml", {"min_version": "5.1.1"}),
2727
)
2828

0 commit comments

Comments
 (0)