-
Notifications
You must be signed in to change notification settings - Fork 192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Download: Refactor CLI commands and introduce --download-configs
as well as --container-library
.
#2336
Conversation
Codecov Report
@@ Coverage Diff @@
## dev #2336 +/- ##
==========================================
- Coverage 73.08% 72.69% -0.39%
==========================================
Files 78 78
Lines 8779 8864 +85
==========================================
+ Hits 6416 6444 +28
- Misses 2363 2420 +57
|
9047c94
to
2a091aa
Compare
--download-configs
as well as --container-library
.
…gularity_images()
2a091aa
to
6e836c3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only thing I really don't like is container-library
. It's not a library, it's a registry. I think using library is confusing - I personally wouldn't consider that meant the default registry. Perhaps we could use -l
but call it container-registry
everywhere? Not consistent but at least it's obvious what it does.
3. If they start with `http` they are downloaded directly within Python (default 4 at a time, you can customise this with `--parallel-downloads`) | ||
4. If they look like a Docker image name, they are fetched using a `singularity pull` command | ||
4. If they look like a Docker image name, they are fetched using a `singularity pull` command. Choose the container libraries (registries) queried by providing one or multiple `--container-library` parameter(s). For example, if you call `nf-core download` with `-l quay.io -l ghcr.io -l docker.io`, every image will be pulled from `quay.io` unless an error is encountered. Subsequently, `ghcr.io` and then `docker.io` will be queried for any image that has failed before. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a cool idea. You can also try to read this from /etc/containers/registries.conf
if you really wanted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am open to improvements, but I felt that reading various config files (e.g. also the various Nextflow configs) was overengineering considering how desperately the next tools release is anticipated? If I recall correctly, it was you who rightfully pointed out via personal message that I was not really focussing enough on the essentials of that feature?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup I agree that's not important! I was just adding a comment because I thought you might be interested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am interested for sure, but ironically I have neither a /etc/containers/registries.conf
nor a $HOME/.config/containers/registries.conf
on my system. I presume that this is GNU/Linux specific?
nf_core/download.py
Outdated
if not self.container_library: | ||
log.error(e.message) | ||
log.error(e.helpmessage) | ||
sys.exit(1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sys.exit(1) | |
raise e |
You should never system.exit(1)
unless you absolutely have to, you will lose all traceback, error handling etc. If running this in a different system you may cause it to break without explanation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair point. However, mind that the download command in the 2.8 version of tools is littered with those sys.exit(1)
commands. So I have just adapted every new and refactored function to the existing code style and implementation patterns of download.py
.
I agree that we should find a better mechanism then, but I think it is beyond the scope of the PR to change it here. For this line specifically, I think, that just raising the error will not suffice, as it is technically a non-recoverable critical error that should stop the execution. Do you have a suggestion (link to a blog post or doc entry) how to exit more gracefully and cleaner?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, use raise e
. That will cause the program to drop out with exit status 1, while showing a nice traceback and providing all the appropriate logs. If you had imported the module you could now do this if you wanted to ignore the error:
except RegistryNotFound:
continue
Or more importantly:
except RegistryNotFound as err:
# delete giant temporary files created during process
# so we don't fill up storage with old cruft.
raise err
Think of it like this, if you were running a website which was running this code, you've just caused it to die. No logs, no error, no traceback. Just dead.
Using sys.exit
is generally bad in everything except in the __main__
block and even then, it should only be used in extreme circumstances. The most common reason to use system.exit(1)
is if your software will do something Very Bad before it finishes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation and suggestion(s).
I think we should indeed change that. I have now created the DownloadError
as an exception that will be raised when the program needs to terminate, but the user should see no traceback.
# Docker.io: denied: requested access to the resource is denied | ||
# unauthorized: authentication required | ||
# Quay.io: StatusCode: 404, <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n'] | ||
# ghcr.io: Requesting bearer token: invalid status code from registry 400 (Bad Request) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should all return the same 404 error in response to the request, I guess it's because this is in a subprocess that you don't get a standardised response?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, the actual problem is that all the error handling is just done by "parsing" (actually just running through regexes) whatever singularity prints to stdout
/stderr
in response to trying singularity pull
with the container path. This was acceptable, because in the previous version, scanning for the keyword "FATAL" and then killing the command was all that was needed.
Now, for the first time, it was required to distinguish different errors based on what was returned. I was/am not really happy with that regex solution, because I anticipate that this will be very flaky (e.g. because the wording changes with different versions of Singularity/Apptainer or the error messages are printed in a different language due to localization) but also had no better idea how to address this.
Indeed, avoiding singularity pull
entirely and testing the path directly from Python now seems like an excellent idea to me. However, I fear that the Python request library just handles http? I think, this might be a different protocol and thus the 404 code returned by Quay.io is not standard?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Darn, that's frustrating. I'm not certain how singularity works but certainly docker pull
is standardised so you can use the normal requests. Oh well, I agree this might break but it looks pretty good to me now.
class RegistryNotFound(ConnectionRefusedError): | ||
"""The specified registry does not resolve to a valid IP address""" | ||
|
||
def __init__(self, error_log): | ||
self.error_log = error_log | ||
self.message = ( | ||
f'[bold red]The specified container library "{self.error_log.registry}" is invalid or unreachable.[/]\n' | ||
) | ||
self.helpmessage = ( | ||
f'Please check, if you made a typo when providing "-l / --library {self.error_log.registry}"\n' | ||
) | ||
super().__init__(self.message, self.helpmessage, self.error_log) | ||
|
||
class ImageNotFound(FileNotFoundError): | ||
"""The image can not be found in the registry""" | ||
|
||
def __init__(self, error_log): | ||
self.error_log = error_log | ||
self.message = ( | ||
f'[bold red]"Pulling "{self.error_log.container}" from "{self.error_log.address}" failed.[/]\n' | ||
) | ||
self.helpmessage = f'Saving image of "{self.error_log.container}" failed.\nPlease troubleshoot the command \n"{" ".join(self.error_log.singularity_command)}" manually.f\n' | ||
super().__init__(self.message) | ||
|
||
class InvalidTag(AttributeError): | ||
"""Image and registry are valid, but the (version) tag is not""" | ||
|
||
def __init__(self, error_log): | ||
self.error_log = error_log | ||
self.message = f'[bold red]"{self.error_log.address.split(":")[-1]}" is not a valid tag of "{self.error_log.container}"[/]\n' | ||
self.helpmessage = f'Please chose a different library than {self.error_log.registry}\nor try to locate the "{self.error_log.address.split(":")[-1]}" version of "{self.error_log.container}" manually.\nPlease troubleshoot the command \n"{" ".join(self.error_log.singularity_command)}" manually.\n' | ||
super().__init__(self.message) | ||
|
||
class ImageExists(FileExistsError): | ||
"""Image already exists in cache/output directory.""" | ||
|
||
def __init__(self, error_log): | ||
self.error_log = error_log | ||
self.message = ( | ||
f'[bold red]"{self.error_log.container}" already exists at destination and cannot be pulled[/]\n' | ||
) | ||
self.helpmessage = f'Saving image of "{self.error_log.container}" failed, because "{self.error_log.out_path}" exists.\nPlease troubleshoot the command \n"{" ".join(self.error_log.singularity_command)}" manually.\n' | ||
super().__init__(self.message) | ||
|
||
class OtherError(RuntimeError): | ||
"""Undefined error with the container""" | ||
|
||
def __init__(self, error_log): | ||
self.error_log = error_log | ||
self.message = f'[bold red]"{self.error_log.container}" failed for unclear reasons.[/]\n' | ||
self.helpmessage = f'Pulling of "{self.error_log.container}" failed.\nPlease troubleshoot the command \n"{" ".join(self.error_log.singularity_command)}" manually.\n' | ||
super().__init__(self.message, self.helpmessage, self.error_log) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This definitely feels like it's trying to reinvent the wheel. I'd guess this is because it's using a subprocess to call singularity instead of using a requests library to handle the HTTP error codes but if Singularity cant support this I guess that will have to be the solution. If we write a Docker implementation it can be a lot simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could not agree more. But that was the best idea I had, given the time pressure @drpatelh put on me. I think we can definitively avoid much of that mess by taking a step back and somehow avoiding singularity pull
in the first place.
If we write a Docker implementation, it can be a lot simpler. Do you fancy doing this? Or have a suggestion how to test for valid docker images/tags without actually running singularity pull?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep we already have that in modules lint
. I want to add docker to this and I have an open ticket for it, will start looking at it once this is merged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about using the Biocontainers API? This to me looks even better than relying purely on Docker.
Well, I pondered a while over this. While |
Hmm OK. In my head, registry = the hosting service and library = a collection of containers. It's not a big thing as long as the docs make it clear so maybe we just stick with |
Admittedly, I would have preferred registry, too - also for consistency with other tool subcommands. But We can't use |
…ect the UX negatively.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've put 2 comments on but neither are of critical importance so I'll leave it up to you whether you want to add them or not. Nice work, this is looking like one of the best written parts of nf-core/tools!
… be used to supress printing the traceback.
…not specified. This was not handled atm. Also print the AssertionError error messages in DownloadError.
To address issue nf-core download interprets custom registry #2311, a new parameter
--container-library
was created allowing to specify the container library (registry) from which container images in OCI format (Docker) should be pulled.In the course of this, all CLI parameters related to container images were refactored for consistency. Although downloading other images than those of the Singularity/Apptainer container system is not supported for the time being, a generic name for the parameters seemed preferable. So the new parameter
--singularity-cache-index
introduced in Download pipelines for Tower #2247 has been renamed to--container-cache-index
prior to release. No previously released single letter parameters were reused as shorthand notation for a different parameter. Therefore, I refrained from using-c
for--configuration
, because it had been used before for--container
.Tabular representation of CLI parameter changes in this PR:
-d
/--download-configuration
-c
/--container <VALUE>
-s
/--container-system <VALUE>
-l
/--container-library <VALUE>
--singularity-cache
-u
/--container-cache-utilisation <VALUE>
-i
/--singularity-cache-index <VALUE>
-i
/--container-cache-index <VALUE>
PR checklist
CHANGELOG.md
is updateddocs
is updated