Source code for fm_weck.image_mgr
# This file is part of fm-weck: executing fm-tools in containerized environments.
# https://gitlab.com/sosy-lab/software/fm-weck
#
# SPDX-FileCopyrightText: 2024 Dirk Beyer <https://www.sosy-lab.org>
#
# SPDX-License-Identifier: Apache-2.0
import logging
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING
try:
from fm_tools.fmtoolversion import FmImageConfig
except ImportError:
if not TYPE_CHECKING:
class FmImageConfig:
def __init__(self, full_images, base_images, required_packages):
raise ImportError("fm_tools is not imported.")
from fm_weck.exceptions import NoImageError
if TYPE_CHECKING:
from fm_weck.engine import Engine
CONTAINERFILE = Path(__file__).parent / "resources" / "Containerfile"
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
[docs]
class ImageMgr(object):
"""
The image manager singleton is responsible for preparing the images for the container.
"""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(ImageMgr, cls).__new__(cls)
return cls._instance
[docs]
def prepare_image(self, engine: "Engine", image: FmImageConfig) -> str:
if image.full_images:
return image.full_images[0]
if image.base_images and not image.required_packages:
return image.base_images[0]
if not image.base_images:
raise NoImageError("No base image specified")
logger.info(
"Building image from from base image %s with packages %s", image.base_images[0], image.required_packages
)
image_cmd = engine.image_from(CONTAINERFILE)
image_cmd.base_image(image.base_images[0])
image_cmd.packages(image.required_packages)
from yaspin import yaspin
with yaspin(text="Building image", color="cyan") as spinner:
tag = image_cmd.build()
spinner.ok("✅ ")
return tag
[docs]
def prepare_image_for_zenodo(self, engine: "Engine", image_name: str) -> tuple[str, str, Path]:
"""
Prepare an image for Zenodo upload, by retreiving the image tarball.
:return: A tuple containing the normalized image name and tarball path.
Raises:
ValueError: If the image is invalid or does not exist.
"""
if ":" not in image_name:
raise ValueError(f"Image specification should contain a tag: '{image_name}:tag'.")
elif image_name.split(":")[1] == "latest":
raise ValueError("Image specification should not use the 'latest' tag.")
build_cmd = engine.image_from(CONTAINERFILE)
if build_cmd.inspect_image(image_name) != 0:
self.print_dockerfile_warning()
raise ValueError(f"Image '{image_name}' is invalid or does not exist. Check your chosen engine.")
with tempfile.NamedTemporaryFile(delete=False) as image_tar:
image_tar_path = Path(image_tar.name)
build_cmd.save_image(image_name, image_tar_path)
return image_name.split(":")[0], image_name.split(":")[1], image_tar_path
[docs]
def check_if_doi_exists_locally(self, engine: "Engine", doi: str) -> str:
"""
Check if an image tagged with the given DOI already exists locally.
:return: The DOI if it does not exist locally, otherwise empty string.
"""
build_cmd = engine.image_from(CONTAINERFILE)
tag = doi.split(".")[-1]
if build_cmd.tag_exists(tag):
return ""
else:
return doi
[docs]
def load_image(self, engine: "Engine", image_tar: Path, tag: str | None = None) -> None:
image_cmd = engine.image_from(CONTAINERFILE)
image_cmd.load_image(image_tar)
if tag:
image_name = image_tar.stem.split(".")[0]
image_cmd.tag_image(image_name, tag)
[docs]
def tag_image(self, engine: "Engine", image: str, tag: str) -> None:
image_cmd = engine.image_from(CONTAINERFILE)
image_cmd.tag_image(image, tag)
[docs]
@staticmethod
def print_dockerfile_warning():
print(
"\033[31m" + "\nWarning:" + "\033[0m\n"
"Building images from Dockerfiles is not supported for this process. "
"Please use pre-built images instead.\n"
"To build your own image, you can use Docker or Podman, ensuring that "
"the image is fully functional and tested before permanently "
"storing it on Zenodo.\n"
"This approach helps maintain Zenodo's quality and avoids cluttering it "
"with unfinished or untested images.\n"
"We encourage verifying and finalizing images before uploading them to Zenodo.\n"
)