File: image_puller.py

package info (click to toggle)
cwl-utils 0.40-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 5,164 kB
  • sloc: python: 88,875; makefile: 141; javascript: 91
file content (138 lines) | stat: -rw-r--r-- 4,759 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# SPDX-License-Identifier: Apache-2.0
"""Classes for docker-extract."""
import logging
import os
import subprocess  # nosec
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Optional, Union

from .singularity import get_version as get_singularity_version
from .singularity import is_version_2_6 as is_singularity_version_2_6
from .singularity import is_version_3_or_newer as is_singularity_version_3_or_newer

logging.basicConfig(level=logging.INFO)
_LOGGER = logging.getLogger(__name__)


class ImagePuller(ABC):
    def __init__(
        self,
        req: str,
        save_directory: Optional[Union[str, Path]],
        cmd: str,
        force_pull: bool,
    ) -> None:
        """Create an ImagePuller."""
        self.req = req
        self.save_directory = save_directory
        self.cmd = cmd
        self.force_pull = force_pull

    @abstractmethod
    def get_image_name(self) -> str:
        """Get the engine-specific image name."""

    @abstractmethod
    def save_docker_image(self) -> None:
        """Download and save the image to disk."""

    @staticmethod
    def _run_command_pull(cmd_pull: list[str]) -> None:
        try:
            subprocess.run(  # nosec
                cmd_pull, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
            )
        except subprocess.CalledProcessError as err:
            if err.output:
                raise subprocess.SubprocessError(err.output) from err
            raise err


class DockerImagePuller(ImagePuller):
    """Pull docker image with Docker."""

    def get_image_name(self) -> str:
        """Get the name of the tarball."""
        name = "".join(self.req.split("/")) + ".tar"
        # Replace colons with underscores in the name.
        # See https://github.com/containers/podman/issues/489
        name = name.replace(":", "_")
        return name

    def generate_udocker_loading_command(self) -> str:
        """Generate the udocker loading command."""
        return f"udocker load -i {self.get_image_name()}"

    def save_docker_image(self) -> None:
        """Download and save the software container image to disk as a docker tarball."""
        _LOGGER.info(f"Pulling {self.req} with {self.cmd}...")
        cmd_pull = [self.cmd, "pull", self.req]
        ImagePuller._run_command_pull(cmd_pull)
        _LOGGER.info(f"Image successfully pulled: {self.req}")
        if self.save_directory:
            dest = os.path.join(self.save_directory, self.get_image_name())
            if self.save_directory and self.force_pull:
                os.remove(dest)
            cmd_save = [
                self.cmd,
                "save",
                "-o",
                dest,
                self.req,
            ]
            subprocess.run(cmd_save, check=True)  # nosec
            _LOGGER.info(f"Image successfully saved: {dest!r}.")
            print(self.generate_udocker_loading_command())


class SingularityImagePuller(ImagePuller):
    """Pull docker image with Singularity."""

    CHARS_TO_REPLACE = ["/", ":"]
    NEW_CHAR = "_"

    def get_image_name(self) -> str:
        """Determine the file name appropriate to the installed version of Singularity."""
        image_name = self.req
        for char in self.CHARS_TO_REPLACE:
            image_name = image_name.replace(char, self.NEW_CHAR)
        if is_singularity_version_2_6():
            suffix = ".img"
        elif is_singularity_version_3_or_newer():
            suffix = ".sif"
        else:
            raise Exception(
                f"Don't know how to handle this version of singularity: {get_singularity_version()}."
            )
        return f"{image_name}{suffix}"

    def save_docker_image(self) -> None:
        """Pull down the Docker software container image and save it in the Singularity image format."""
        save_directory: Union[str, Path]
        if self.save_directory:
            save_directory = self.save_directory
        if (
            os.path.exists(os.path.join(save_directory, self.get_image_name()))
            and not self.force_pull
        ):
            _LOGGER.info(f"Already cached {self.req} with Singularity.")
            return
        _LOGGER.info(f"Pulling {self.req} with Singularity...")
        cmd_pull = [
            self.cmd,
            "pull",
        ]
        if self.force_pull:
            cmd_pull.append("--force")
        cmd_pull.extend(
            [
                "--name",
                os.path.join(save_directory, self.get_image_name()),
                f"docker://{self.req}",
            ]
        )
        ImagePuller._run_command_pull(cmd_pull)
        _LOGGER.info(
            f"Image successfully pulled: {save_directory}/{self.get_image_name()}"
        )