Skip to content

Commit 217c7bc

Browse files
authored
adding support to install from local container (#602)
* adding support to install from local container * add --keep-path to not relocate container this means that a user can select a registry entry (container.yaml) to use for metadata, and apply to a local container. This still requires choice of an explicit registry entry for metadata selection. Signed-off-by: vsoch <[email protected]>
1 parent d7b9a54 commit 217c7bc

File tree

8 files changed

+175
-30
lines changed

8 files changed

+175
-30
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 pip. Only major versions will be released as tags on Github.
1515

1616
## [0.0.x](https://github.com/singularityhub/singularity-hpc/tree/main) (0.0.x)
17+
- support for install using registry recipe and local image (0.1.15)
1718
- fix views .view_module modulefile and loading (0.1.14)
1819
- support for system modules, depends on, in views and editor envars (0.1.13)
1920
- Wrappers now supported for shell/exec/run container commands (0.1.12)

docs/getting_started/user-guide.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,13 +1158,47 @@ the container technology.
11581158
If you don't have any module software on your system, you can now test interacting
11591159
with the module via the :ref:`getting_started-development` instructions.
11601160

1161+
.. _getting_started-commands-install-private:
1162+
11611163

11621164
Install Private Images
11631165
----------------------
11641166

11651167
What about private containers on Docker Hub? If you have a private image, you can
11661168
simply use `Singularity remote login <https://github.com/sylabs/singularity-userdocs/blob/master/singularity_and_docker.rst#singularity-cli-remote-command>`_ before attempting the install and everything should work.
11671169

1170+
.. _getting_started-commands-install-local:
1171+
1172+
1173+
Install Local Image
1174+
-------------------
1175+
1176+
The concept of installing a local image means that you are selecting a container.yaml recipe from an existing registry,
1177+
however instead of pulling it, you are pairing it was a particular URI of a local image. As an example, let's say we have pulled a local
1178+
samtools container:
1179+
1180+
.. code-block:: console
1181+
1182+
$ singularity pull docker://quay.io/biocontainers/samtools:1.10--h2e538c0_3
1183+
1184+
We might then want to install it to the samtools namespace and using the same metadata (e.g., aliases, environment, etc.):
1185+
1186+
.. code-block:: console
1187+
1188+
$ shpc install quay.io/biocontainers/samtools:1.10--h2e538c0_3 samtools_1.2--0.sif
1189+
1190+
This is similar to an ``shpc add``, however instead of needing to write a container.yaml in a local
1191+
filesystem, you are using an existing one. The use case or assumption here is that you have a local
1192+
directory of containers that can be matched to existing shpc recipes. Finally to request using the
1193+
container path "as is" without copying anything into your container folder, add ``--keep-path``:
1194+
1195+
1196+
.. code-block:: console
1197+
$ shpc install quay.io/biocontainers/samtools:1.10--h2e538c0_3 samtools_1.2--0.sif --keep-path
1198+
1199+
This feature is supported for shpc versions 0.1.15 and up.
1200+
1201+
11681202
.. _getting_started-commands-namespace:
11691203

11701204

shpc/client/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,20 @@ def get_parser():
101101
"install_recipe",
102102
help="recipe to install\nshpc install python\nshpc install python:3.9.5-alpine",
103103
)
104+
105+
install.add_argument(
106+
"container_image",
107+
help="path to an existing container image for this software",
108+
nargs="?",
109+
)
110+
111+
install.add_argument(
112+
"--keep-path",
113+
help="if installing a local container, do not copy the container - use the provided path.",
114+
default=False,
115+
action="store_true",
116+
)
117+
104118
install.add_argument(
105119
"--no-view",
106120
dest="no_view",

shpc/client/install.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,16 @@ def main(args, parser, extra, subparser):
2525
cli.settings.update_params(args.config_params)
2626

2727
# And do the install
28-
cli.install(args.install_recipe, force=args.force)
28+
cli.install(
29+
args.install_recipe,
30+
force=args.force,
31+
container_image=args.container_image,
32+
keep_path=args.keep_path,
33+
)
2934
if cli.settings.default_view and not args.no_view:
3035
cli.view_install(
31-
cli.settings.default_view, args.install_recipe, force=args.force
36+
cli.settings.default_view,
37+
args.install_recipe,
38+
force=args.force,
39+
container_image=args.container_image,
3240
)

shpc/main/modules/base.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,9 @@ def check(self, module_name):
346346

347347
return module.check()
348348

349-
def new_module(self, name, tag=None, tag_exists=True):
349+
def new_module(
350+
self, name, tag=None, tag_exists=True, container_image=None, keep_path=False
351+
):
350352
"""
351353
Create a new module
352354
"""
@@ -366,9 +368,21 @@ def new_module(self, name, tag=None, tag_exists=True):
366368
# Pass on container and settings
367369
module.container = self.container
368370
module.settings = self.settings
371+
372+
# Do we want to use a container from the local filesystem?
373+
if container_image:
374+
module.add_local_container(container_image, keep_path=keep_path)
369375
return module
370376

371-
def install(self, name, tag=None, force=False, **kwargs):
377+
def install(
378+
self,
379+
name,
380+
tag=None,
381+
force=False,
382+
container_image=None,
383+
keep_path=False,
384+
**kwargs
385+
):
372386
"""
373387
Given a unique resource identifier, install a recipe.
374388
@@ -378,7 +392,13 @@ def install(self, name, tag=None, force=False, **kwargs):
378392
"force" is currently not used.
379393
"""
380394
# Create a new module
381-
module = self.new_module(name, tag=tag, tag_exists=True)
395+
module = self.new_module(
396+
name,
397+
tag=tag,
398+
tag_exists=True,
399+
container_image=container_image,
400+
keep_path=keep_path,
401+
)
382402

383403
# We always load overrides for an install
384404
module.load_override_file()
@@ -411,12 +431,16 @@ def install(self, name, tag=None, force=False, **kwargs):
411431
logger.info("Module %s was created." % module.tagged_name)
412432
return module.container_path
413433

414-
def view_install(self, view_name, name, tag=None, force=False):
434+
def view_install(
435+
self, view_name, name, tag=None, force=False, container_image=None
436+
):
415437
"""
416438
Install a module in a view. The module must already be installed.
417439
Set "force" to True to allow overwriting existing symlinks.
418440
"""
419-
module = self.new_module(name, tag=tag, tag_exists=True)
441+
module = self.new_module(
442+
name, tag=tag, tag_exists=True, container_image=container_image
443+
)
420444

421445
# A view is a symlink under views_base/$view/$module
422446
if view_name not in self.views:

shpc/main/modules/module.py

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import shutil
77

8+
import shpc.utils as utils
89
from shpc.logger import logger
910

1011

@@ -63,32 +64,20 @@ def container_dir(self):
6364
self._container_dir = self.container.container_dir(self.module_basepath)
6465
return self._container_dir
6566

66-
@property
67-
def container_path(self):
67+
def add_container(self, container_image=None):
6868
"""
69-
Derive the container path, if possible.
69+
Ensure a container is pulled (or provided)
70+
71+
This should be called after self.config is set in new_module.
7072
"""
71-
if self._container_path:
72-
return self._container_path
73+
# First preference goes to provided image (actual file)
74+
# This is only allowed for Singularity containers
75+
if container_image and os.path.exists(container_image):
76+
self.add_local_container(container_image)
7377

7478
# If we have a sif URI provided by path, the container needs to exist
75-
if self.config.path:
76-
self._container_path = os.path.join(
77-
self.config.entry.dirname, self.config.path
78-
)
79-
if not os.path.exists(self._container_path):
80-
logger.exit(
81-
"Expected container defined by path %s not found in %s."
82-
% (self.config.path, self.config.entry.dirname)
83-
)
84-
container_dest = os.path.join(self.container_dir, self.config.path)
85-
86-
# Note that here we are *duplicating* the container, assuming we
87-
# cannot use a link, and the registry won't be deleted but the
88-
# module container might!
89-
if not os.path.exists(container_dest):
90-
shutil.copyfile(self._container_path, container_dest)
91-
self._container_path = container_dest
79+
elif self.config.path:
80+
self._add_config_container()
9281

9382
# For Singularity this is a path, podman is a uri. If None is returned
9483
# there was an error and we cleanup
@@ -98,6 +87,55 @@ def container_path(self):
9887
)
9988
return self._container_path
10089

90+
def add_local_container(self, container_image, keep_path=False):
91+
"""
92+
Set the module container to be a local container (copied over)
93+
"""
94+
basename = os.path.basename(container_image)
95+
96+
# The user has requested to use their own image, we don't copy
97+
if keep_path:
98+
self._container_path = os.path.abspath(container_image)
99+
return
100+
101+
container_dest = os.path.join(self.container_dir, basename)
102+
if not os.path.exists(self.container_dir):
103+
utils.mkdir_p(self.container_dir)
104+
if not os.path.exists(container_dest):
105+
shutil.copyfile(container_image, container_dest)
106+
self._container_path = container_dest
107+
108+
def _add_config_container(self):
109+
"""
110+
Set the module container to be the one defined by the config.path
111+
"""
112+
self._container_path = os.path.join(self.config.entry.dirname, self.config.path)
113+
114+
if not os.path.exists(self._container_path):
115+
logger.exit(
116+
"Expected container defined by path %s not found in %s."
117+
% (self.config.path, self.config.entry.dirname)
118+
)
119+
container_dest = os.path.join(self.container_dir, self.config.path)
120+
121+
# Note that here we are *duplicating* the container, assuming we
122+
# cannot use a link, and the registry won't be deleted but the
123+
# module container might!
124+
if not os.path.exists(container_dest):
125+
shutil.copyfile(self._container_path, container_dest)
126+
self._container_path = container_dest
127+
128+
@property
129+
def container_path(self):
130+
"""
131+
Derive the container path, if possible.
132+
"""
133+
if self._container_path:
134+
return self._container_path
135+
136+
# Default to pulling container based on config
137+
return self.add_container()
138+
101139
@property
102140
def tag(self):
103141
"""

shpc/tests/test_client.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,32 @@ def test_check(tmp_path, module_sys, container_tech, remote):
305305
client.check("vanessa/salad:latest")
306306

307307

308+
@pytest.mark.parametrize(
309+
"module_sys,remote",
310+
[("lmod", True), ("tcl", True), ("lmod", False), ("tcl", False)],
311+
)
312+
def test_install_local(tmp_path, module_sys, remote):
313+
"""
314+
Test adding a custom container associated with an existing recipe
315+
"""
316+
client = init_client(str(tmp_path), module_sys, "singularity", remote=remote)
317+
318+
# Create a copy of the latest image to add
319+
container = os.path.join(str(tmp_path), "salad_latest.sif")
320+
shutil.copyfile(os.path.join(here, "testdata", "salad_latest.sif"), container)
321+
322+
# It still needs to be a known tag!
323+
with pytest.raises(SystemExit):
324+
client.install(
325+
"quay.io/biocontainers/samtools:1.2--0", container_image=container
326+
)
327+
328+
# This should install our custom image using samtools metadata
329+
container_image = "quay.io/biocontainers/samtools:1.10--h2e538c0_3"
330+
client.install(container_image, container_image=container)
331+
assert os.path.basename(client.get(container_image)) == os.path.basename(container)
332+
333+
308334
@pytest.mark.parametrize(
309335
"module_sys,remote",
310336
[("lmod", True), ("tcl", True), ("lmod", False), ("tcl", False)],

shpc/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
__copyright__ = "Copyright 2021-2022, Vanessa Sochat"
33
__license__ = "MPL 2.0"
44

5-
__version__ = "0.1.14"
5+
__version__ = "0.1.15"
66
AUTHOR = "Vanessa Sochat"
77
88
NAME = "singularity-hpc"

0 commit comments

Comments
 (0)