diff --git a/esd/__init__.py b/esd/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/esd/build_cache/__init__.py b/esd/build_cache/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/esd/build_cache/manage_build_cache.py b/esd/build_cache/manage_build_cache.py new file mode 100644 index 0000000000000000000000000000000000000000..9697e88d0f8ef0eab7b9f75f23b7b7873fc491cd --- /dev/null +++ b/esd/build_cache/manage_build_cache.py @@ -0,0 +1,123 @@ +import os +import logging +import oras.client +from pathlib import Path + +from esd.utils.utils import clean_up + + +class BuildCacheManager: + """ + This class aims to manage the push/pull/delete of build cache files + """ + + def __init__(self, auth_backend='basic', log_path='./'): + self.home_path = Path(os.environ.get("HOME_PATH", os.getcwd())) + self.log_file = Path(log_path) / "log_oras.txt" + self.registry_project = os.environ.get("REGISTRY_PROJECT") + + self._registry_username = str(os.environ.get("REGISTRY_USERNAME")) + self._registry_password = str(os.environ.get("REGISTRY_PASSWORD")) + + self.registry_host = str(os.environ.get("REGISTRY_HOST")) + # Initialize an OrasClient instance. + # This method utilizes the OCI Registry for container image and artifact management. + # Refer to the official OCI Registry documentation for detailed information on the available authentication methods. + # Supported authentication types may include basic authentication (username/password), token-based authentication, + self.client = oras.client.OrasClient(hostname=self.registry_host, auth_backend=auth_backend) + self.client.login(username=self._registry_username, password=self._registry_password) + self.oci_registry_path = f'{self.registry_host}/{self.registry_project}/cache' + + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + handlers=[ + logging.FileHandler(self.log_file), + logging.StreamHandler() + ] + ) + + def oci_registry_upload_build_cache(self, cache_dir: Path): + """ + This method pushed all the files from the build cache folder into the OCI Registry + """ + build_cache_path = self.home_path / "shared" / cache_dir + # build cache folder must exist before pushing all the artifacts + if not build_cache_path.exists(): + raise FileNotFoundError( + f"BuildCacheManager::oci_registry_upload_build_cache::Path {build_cache_path} not found.") + + for sub_path in build_cache_path.rglob("*"): + if sub_path.is_file(): + rel_path = str(sub_path.relative_to(build_cache_path)).replace(str(sub_path.name), "") + target = f"{self.registry_host}/{self.registry_project}/cache:{str(sub_path.name)}" + try: + logging.info(f"Pushing folder '{sub_path}' to ORAS target '{target}' ...") + self.client.push( + files=[str(sub_path)], + target=target, + # save in manifest the relative path for reconstruction + manifest_annotations={"path": rel_path}, + disable_path_validation=True, + ) + logging.info(f"Successfully pushed {sub_path.name}") + except Exception as e: + logging.error( + f"BuildCacheManager::registry_upload_build_cache::An error occurred while pushing: {e}") + # delete the build cache after being pushed to the OCI Registry + clean_up([str(build_cache_path)], logging) + + def oci_registry_get_tags(self): + """ + This method retrieves all tags from an OCI Registry + """ + try: + return self.client.get_tags(self.oci_registry_path) + except Exception as e: + logging.error(f"BuildCacheManager::oci_registry_get_tags::Failed to list tags: {e}") + return None + + def oci_registry_download_build_cache(self, cache_dir: Path): + """ + This method pulls all the files from the OCI Registry into the build cache folder + """ + build_cache_path = self.home_path / "shared" / cache_dir + # create the buildcache dir if it does not exist + os.makedirs(build_cache_path, exist_ok=True) + tags = self.oci_registry_get_tags() + if tags is not None: + for tag in tags: + ref = f"{self.registry_host}/{self.registry_project}/cache:{tag}" + # reconstruct the relative path of each artifact by getting it from the manifest + cache_path = \ + self.client.get_manifest(f'{self.registry_host}/{self.registry_project}/cache:{tag}')[ + 'annotations'][ + 'path'] + try: + self.client.pull( + ref, + # missing dirs to outdir are created automatically by OrasClient pull method + outdir=str(build_cache_path / cache_path), + overwrite=True + ) + logging.info(f"Successfully pulled artifact {tag}.") + except Exception as e: + logging.error( + f"BuildCacheManager::registry_download_build_cache::Failed to pull artifact {tag} : {e}") + + def oci_registry_delete_build_cache(self): + """ + Deletes all artifacts from an OCI Registry based on their tags. + This method removes artifacts identified by their tags in the specified OCI Registry. + It requires appropriate permissions to delete artifacts from the registry. + If the registry or user does not have the necessary delete permissions, the operation might fail. + """ + tags = self.oci_registry_get_tags() + if tags is not None: + try: + self.client.delete_tags(self.oci_registry_path, tags) + logging.info(f"Successfully deleted all artifacts form OCI registry.") + except RuntimeError as e: + logging.error( + f"BuildCacheManager::registry_delete_build_cache::Failed to delete artifacts: {e}") diff --git a/fetch_cached_buildresults.py b/esd/fetch_cached_buildresults.py similarity index 100% rename from fetch_cached_buildresults.py rename to esd/fetch_cached_buildresults.py diff --git a/fetch_cached_sources.py b/esd/fetch_cached_sources.py similarity index 100% rename from fetch_cached_sources.py rename to esd/fetch_cached_sources.py diff --git a/specfile_dag_hash.py b/esd/specfile_dag_hash.py similarity index 96% rename from specfile_dag_hash.py rename to esd/specfile_dag_hash.py index 6e001b84accd5b4cd7c11a79842b7d88df306ec8..e44e1c6296c2499580f286d1b2ec7e753243f53a 100644 --- a/specfile_dag_hash.py +++ b/esd/specfile_dag_hash.py @@ -3,7 +3,7 @@ from collections.abc import Iterable import pathlib import ruamel.yaml as yaml import spack -import spack.binary_distribution as bindist + parser = argparse.ArgumentParser( prog='specfile_dag_hash.py', diff --git a/specfile_storage_path_build.py b/esd/specfile_storage_path_build.py similarity index 100% rename from specfile_storage_path_build.py rename to esd/specfile_storage_path_build.py diff --git a/specfile_storage_path_source.py b/esd/specfile_storage_path_source.py similarity index 100% rename from specfile_storage_path_source.py rename to esd/specfile_storage_path_source.py diff --git a/update_cached_buildresults.py b/esd/update_cached_buildresults.py similarity index 99% rename from update_cached_buildresults.py rename to esd/update_cached_buildresults.py index caacf86e46efec1ddb0c8b8c044f4ff8506eace1..58af242b3bcfb03f153a0f4fa1fa22ff2c66d371 100644 --- a/update_cached_buildresults.py +++ b/esd/update_cached_buildresults.py @@ -1,5 +1,4 @@ import argparse -import glob import os import pathlib import subprocess diff --git a/update_cached_sources.py b/esd/update_cached_sources.py similarity index 100% rename from update_cached_sources.py rename to esd/update_cached_sources.py diff --git a/esd/utils/__init__.py b/esd/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/esd/utils/utils.py b/esd/utils/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c1aa4c0da97a36ed386ebd28a8ad211bef91b3df --- /dev/null +++ b/esd/utils/utils.py @@ -0,0 +1,15 @@ +import shutil +from pathlib import Path + + +def clean_up(dirs: list[str], logging): + """ + All the folders from the list dirs are removed with all the content in them + """ + for cleanup_dir in dirs: + cleanup_dir = Path(cleanup_dir).resolve() + if cleanup_dir.exists(): + logging.info(f"Removing {cleanup_dir}") + shutil.rmtree(Path(cleanup_dir)) + else: + logging.info(f"{cleanup_dir} does not exist") diff --git a/wscript b/esd/wscript old mode 100755 new mode 100644 similarity index 100% rename from wscript rename to esd/wscript diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..dd9ac1021b7091a7d1d4bb8066b7c5d8300cb76e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "esd" +authors = [ + {name = "Eric Müller", email = "mueller@kip.uni-heidelberg.de"}, + {name = "Adrian Ciu", email = "adrian.ciu@codemart.ro"}, +] +description = "This package provides all the necessary tools to create an Ebrains Software Distribution environment" +version = "0.1.0" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "oras", + "spack", + "ruamel.yaml" +] \ No newline at end of file