Skip to content

Commit 35ed2d5

Browse files
authored
Uninstall (#603)
* Alternative class to Module to allow non-Registry-backed modules * Extended the test suite to test uninstallation * Fold everything back into a single class: Module * Moved the verification of the tag to the set_tag method * config.name is a ContainerName. Need to turn it into a string * Introduced get_module * Reinstated module_basepath as a property * Extracted Module.load_config and made get_module rely on that and new_module * The `tag` parameter is almost never used, and conflicts with the convention `tool:tag` * Moved the validation back to Module * Moved the block to make the file closer to its original * Made docgen work with versioned modules too * bugfix: get rid of an unnecessary / * Display the versioned name * Use the requested version as an example * Only get_module needs the container path * Preferred style * Cleanup: only pass the config object itself and access its properties from the template * Use urllib to properly parse the URL * Version bump and changelog update * There shouldn't be a space there !
1 parent 217c7bc commit 35ed2d5

File tree

7 files changed

+84
-83
lines changed

7 files changed

+84
-83
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+
- Fix bugs uninstalling all tags of a module (0.1.16)
1718
- support for install using registry recipe and local image (0.1.15)
1819
- fix views .view_module modulefile and loading (0.1.14)
1920
- support for system modules, depends on, in views and editor envars (0.1.13)

shpc/main/client.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,12 @@ def load_registry_config(self, name):
105105
logger.exit("%s is not a known recipe in any registry." % name)
106106
return container.ContainerConfig(result)
107107

108-
def _load_container(self, name, tag=None):
108+
def _load_container(self, name):
109109
"""
110110
Given a name and an optional tag to default to, load a package
111111
"""
112112
# Split name and tag
113+
tag = None
113114
if ":" in name:
114115
name, tag = name.split(":", 1)
115116

@@ -175,22 +176,23 @@ def cleanup(tmpdir):
175176
# Test all tags (this could be subsetted)
176177
for tag in tags:
177178

178-
image = self.install(module_name, tag)
179+
versioned_name = module_name + ":" + tag
180+
image = self.install(versioned_name)
179181

180182
# Do we want to test loading?
181183
if not skip_module and hasattr(self, "_test"):
182184
result = self._test(module_name, tmpdir, tag, template)
183185
if result != 0:
184186
cleanup(tmpdir)
185-
logger.exit("Test of %s was not successful." % module_name)
187+
logger.exit("Test of %s was not successful." % versioned_name)
186188

187189
# Do we want to test the test commands?
188190
if test_commands and config.test:
189191
utils.write_file(test_file, config.test)
190192
return_code = self.container.test_script(image, test_file)
191193
if return_code != 0:
192194
cleanup(tmpdir)
193-
logger.exit("Test of %s was not successful." % module_name)
195+
logger.exit("Test of %s was not successful." % versioned_name)
194196

195197
# Test the commands
196198
if not test_exec:

shpc/main/modules/base.py

Lines changed: 30 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import os
88
import subprocess
99
import sys
10+
import urllib
1011
from datetime import datetime
1112

1213
import shpc.defaults as defaults
@@ -236,14 +237,14 @@ def docgen(self, module_name, registry=None, out=None, branch="main"):
236237
"""
237238
Render documentation for a module within a local registry.
238239
"""
239-
config = self._load_container(module_name)
240+
config = self.get_module(module_name).config
240241

241242
out = out or sys.stdout
242243
aliases = config.get_aliases()
243244
template = self.template.load("docs.md")
244245
registry = registry or defaults.github_url
245-
github_url = "%s/blob/%s/%s/container.yaml" % (registry, branch, module_name)
246-
registry_bare = registry.split(".com")[-1]
246+
github_url = "%s/blob/%s/%s/container.yaml" % (registry, branch, config.name)
247+
registry_bare = urllib.parse.urlparse(registry).path.lstrip("/")
247248
raw = (
248249
"https://gitlab.com/%s/-/raw/%s/%s/container.yaml"
249250
if "gitlab" in registry
@@ -252,24 +253,19 @@ def docgen(self, module_name, registry=None, out=None, branch="main"):
252253
raw_github_url = raw % (
253254
registry_bare,
254255
branch,
255-
module_name,
256+
config.name,
256257
)
257258

258259
# Currently one doc is rendered for all containers
259260
result = template.render(
260261
parsed_name=config.name,
262+
config=config,
261263
settings=self.settings,
262-
description=config.description,
263264
aliases=aliases,
264-
versions=config.tags.keys(),
265265
github_url=github_url,
266-
container_url=config.url,
267266
config_url=raw_github_url,
268267
creation_date=datetime.now(),
269-
name=module_name,
270-
latest=config.latest.name,
271-
flatname=module_name.replace(os.sep, "-"),
272-
config=json.dumps(config.entry._config),
268+
config_json=json.dumps(config.entry._config),
273269
)
274270
out.write(result)
275271
return out
@@ -338,50 +334,48 @@ def check(self, module_name):
338334
at updates for entire tags. If a specific folder is provided with
339335
a container, check the digest.
340336
"""
341-
module = self.new_module(module_name)
337+
module = self.get_module(module_name)
342338
if not os.path.exists(module.module_dir):
343339
logger.exit(
344340
"%s does not exist. Is this a known registry entry?" % module.module_dir
345341
)
346342

347343
return module.check()
348344

349-
def new_module(
350-
self, name, tag=None, tag_exists=True, container_image=None, keep_path=False
351-
):
345+
def new_module(self, name):
352346
"""
353-
Create a new module
347+
Create a new Module just from a name, which doesn't have to exist in the registry.
348+
The name may have a tag appended with a colon.
354349
"""
355350
name = self.add_namespace(name)
356351

357-
# If the module has a version, overrides provided tag
358-
if ":" in name:
359-
name, tag = name.split(":", 1)
360-
361352
module = Module(name)
362-
module.config = self._load_container(module.name, tag)
363-
364-
# Ensure the tag exists, if required, uses config.tag
365-
if tag_exists:
366-
module.validate_tag_exists()
367353

368354
# Pass on container and settings
369355
module.container = self.container
370356
module.settings = self.settings
371357

358+
return module
359+
360+
def get_module(self, name, container_image=None, keep_path=False):
361+
"""
362+
Create a new Module from an existing registry entry, given its name.
363+
The name may have a tag appended with a colon.
364+
"""
365+
module = self.new_module(name)
366+
367+
config = self._load_container(module.name)
368+
# Ensure the tag exists, if required, uses config.tag
369+
module.load_config(config, module.name)
370+
372371
# Do we want to use a container from the local filesystem?
373372
if container_image:
374373
module.add_local_container(container_image, keep_path=keep_path)
374+
375375
return module
376376

377377
def install(
378-
self,
379-
name,
380-
tag=None,
381-
force=False,
382-
container_image=None,
383-
keep_path=False,
384-
**kwargs
378+
self, name, force=False, container_image=None, keep_path=False, **kwargs
385379
):
386380
"""
387381
Given a unique resource identifier, install a recipe.
@@ -392,12 +386,8 @@ def install(
392386
"force" is currently not used.
393387
"""
394388
# Create a new module
395-
module = self.new_module(
396-
name,
397-
tag=tag,
398-
tag_exists=True,
399-
container_image=container_image,
400-
keep_path=keep_path,
389+
module = self.get_module(
390+
name, container_image=container_image, keep_path=keep_path
401391
)
402392

403393
# We always load overrides for an install
@@ -431,16 +421,12 @@ def install(
431421
logger.info("Module %s was created." % module.tagged_name)
432422
return module.container_path
433423

434-
def view_install(
435-
self, view_name, name, tag=None, force=False, container_image=None
436-
):
424+
def view_install(self, view_name, name, force=False, container_image=None):
437425
"""
438426
Install a module in a view. The module must already be installed.
439427
Set "force" to True to allow overwriting existing symlinks.
440428
"""
441-
module = self.new_module(
442-
name, tag=tag, tag_exists=True, container_image=container_image
443-
)
429+
module = self.get_module(name, container_image=container_image)
444430

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

shpc/main/modules/module.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,24 @@ def add_environment(self):
4141
environment_file=self.settings.environment_file,
4242
)
4343

44-
def validate_tag_exists(self):
44+
def load_config(self, config, name):
4545
"""
46-
Ensure that a provided module name (and tag) exists.
46+
Load a ContainerConfig into this Module
4747
"""
48-
if not self.config.tag:
48+
# Ensure that the tag exists
49+
if not config.tag:
4950
logger.exit(
50-
"%s is not a known identifier. Choices are:\n%s"
51-
% (self.name, "\n".join(self.config.tags.keys()))
51+
"%s is not a known identifier. Valid tags are:\n%s"
52+
% (name, "\n".join(config.tags.keys()))
5253
)
54+
# We currently support gh, docker, path, or oras
55+
uri = config.get_uri()
56+
# If we have a path, the URI comes from the name
57+
if ".sif" in uri:
58+
uri = str(config.name).split(":", 1)[0]
59+
self.name = uri + ":" + config.tag.name
60+
self._uri = uri
61+
self.config = config
5362

5463
def load_override_file(self):
5564
self.config.load_override_file(self.tag.name)
@@ -154,17 +163,7 @@ def uri(self):
154163
"""
155164
Get the uri for the module, docker / path / oras / gh
156165
"""
157-
if self._uri:
158-
return self._uri
159-
160-
# We currently support gh, docker, path, or oras
161-
uri = self.config.get_uri()
162-
163-
# If we have a path, the URI comes from the name
164-
if ".sif" in uri:
165-
uri = self.name.split(":", 1)[0]
166-
self._uri = uri
167-
return uri
166+
return self._uri
168167

169168
@property
170169
def module_dir(self):
@@ -178,4 +177,4 @@ def module_basepath(self):
178177
"""
179178
Path of only the module name and tag.
180179
"""
181-
return os.path.join(self.uri, self.tag.name)
180+
return self.name.replace(":", os.sep)

shpc/main/modules/templates/docs.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
---
22
layout: container
3-
name: "{{ name }}"
3+
name: "{{ config.name }}"
44
maintainer: "@vsoch"
55
github: "{{ github_url }}"
66
config_url: "{{ config_url }}"
77
updated_at: "{{ creation_date }}"
8-
latest: "{{ latest }}"
9-
container_url: "{{ container_url }}"
8+
latest: "{{ config.latest.name }}"
9+
container_url: "{{ config.url }}"
1010
{% if aliases %}aliases:{% for alias in aliases %}
1111
- "{{ alias.name }}"{% endfor %}{% endif %}
12-
versions:{% for version in versions %}
12+
versions:{% for version in config.tags.keys() %}
1313
- "{{ version }}"{% endfor %}
14-
{% if description %}description: "{{ description }}"{% endif %}
15-
config: {{ config }}
14+
{% if config.description %}description: "{{ config.description }}"{% endif %}
15+
config: {{ config_json }}
1616
---
1717

18-
This module is a singularity container wrapper for {{ name }}.
19-
{% if description %}{{ description }}{% endif %}
18+
This module is a singularity container wrapper for {{ config.name }}.
19+
{% if config.description %}{{ config.description }}{% endif %}
2020
After [installing shpc](#install) you will want to install this container module:
2121

2222

2323
```bash
24-
$ shpc install {{ name }}
24+
$ shpc install {{ config.name }}
2525
```
2626

2727
Or a specific version:
2828

2929
```bash
30-
$ shpc install {{ name }}:{{ versions.0 }}
30+
$ shpc install {{ config.name }}:{{ config.tag.name }}
3131
```
3232

3333
And then you can tell lmod about your modules folder:
@@ -39,8 +39,8 @@ $ module use ./modules
3939
And load the module, and ask for help, or similar.
4040

4141
```bash
42-
$ module load {{ name }}/{{ versions.0 }}
43-
$ module help {{ name }}/{{ versions.0 }}
42+
$ module load {{ config.name }}/{{ config.tag.name }}
43+
$ module help {{ config.name }}/{{ config.tag.name }}
4444
```
4545

4646
You can use tab for auto-completion of module names or commands that are provided.

shpc/tests/test_client.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,24 +70,37 @@ def test_features(tmp_path, module_sys, module_file, remote):
7070
"""
7171
client = init_client(str(tmp_path), module_sys, "singularity", remote=remote)
7272

73+
module_file_392 = os.path.join(
74+
client.settings.module_base, "python", "3.9.2-alpine", module_file
75+
)
76+
module_file_394 = os.path.join(
77+
client.settings.module_base, "python", "3.9.4-alpine", module_file
78+
)
79+
7380
# Install known tag
7481
client.install("python:3.9.2-alpine")
7582

76-
module_dir = os.path.join(client.settings.module_base, "python", "3.9.2-alpine")
77-
module_file = os.path.join(module_dir, module_file)
78-
7983
# Should not have nvidia flag
80-
content = shpc.utils.read_file(module_file)
84+
content = shpc.utils.read_file(module_file_392)
8185
assert "--nv" not in content
8286

87+
client.install("python:3.9.4-alpine")
88+
assert os.path.exists(module_file_392)
89+
assert os.path.exists(module_file_394)
90+
8391
client.uninstall("python:3.9.2-alpine", force=True)
92+
assert not os.path.exists(module_file_392)
93+
assert os.path.exists(module_file_394)
94+
95+
client.uninstall("python", force=True)
96+
assert not os.path.exists(module_file_394)
8497

8598
# Now update settings
8699
client.settings.set("container_features", "gpu:nvidia")
87100

88101
# Install known tag, add extra feature of gpu
89102
client.install("python:3.9.2-alpine", features=["gpu"])
90-
content = shpc.utils.read_file(module_file)
103+
content = shpc.utils.read_file(module_file_392)
91104
assert "--nv" in content
92105

93106

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.15"
5+
__version__ = "0.1.16"
66
AUTHOR = "Vanessa Sochat"
77
88
NAME = "singularity-hpc"

0 commit comments

Comments
 (0)