diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 60f6446b..ed4b6a42 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -4,7 +4,7 @@ description: Installs the given GardenLinux Python library inputs: version: description: GardenLinux Python library version - default: "0.10.13" + default: "0.10.14" python_version: description: Python version to setup default: "3.13" diff --git a/pyproject.toml b/pyproject.toml index 454924d6..62bcce9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "gardenlinux" -version = "0.10.13" +version = "0.10.14" description = "Contains tools to work with the features directory of gardenlinux, for example deducting dependencies from feature sets or validating cnames" authors = ["Garden Linux Maintainers "] license = "Apache-2.0" diff --git a/src/gardenlinux/constants.py b/src/gardenlinux/constants.py index 293086d8..9171f6a7 100644 --- a/src/gardenlinux/constants.py +++ b/src/gardenlinux/constants.py @@ -148,6 +148,7 @@ GL_DEB_REPO_BASE_URL = "https://packages.gardenlinux.io/gardenlinux" GL_DISTRIBUTION_NAME = "Garden Linux" GL_HOME_URL = "https://gardenlinux.io" +GL_PLATFORM_FRANKENSTEIN = "frankenstein" GL_RELEASE_ID = "gardenlinux" GL_REPOSITORY_URL = "https://github.com/gardenlinux/gardenlinux" GL_SUPPORT_URL = "https://github.com/gardenlinux/gardenlinux" diff --git a/src/gardenlinux/features/cname.py b/src/gardenlinux/features/cname.py index 1fb5a0ce..22394df1 100644 --- a/src/gardenlinux/features/cname.py +++ b/src/gardenlinux/features/cname.py @@ -8,13 +8,14 @@ from configparser import UNNAMED_SECTION, ConfigParser from os import PathLike, environ from pathlib import Path -from typing import List, Optional, Self +from typing import Any, Dict, List, Optional, Self from ..constants import ( ARCHS, GL_BUG_REPORT_URL, GL_DISTRIBUTION_NAME, GL_HOME_URL, + GL_PLATFORM_FRANKENSTEIN, GL_RELEASE_ID, GL_SUPPORT_URL, ) @@ -59,14 +60,21 @@ def __init__( self._feature_flags_cached: Optional[List[str]] = None self._feature_platforms_cached: Optional[List[str]] = None self._feature_set_cached: Optional[str] = None + self._features_cached: Optional[Dict[str, Any]] = None + self._platform_cached: Optional[str] = None self._platform_variant_cached: Optional[str] = None self._flavor = "" self._version = None + self._flag_frankenstein = bool(environ.get("GL_ALLOW_FRANKENSTEIN", False)) + self._flag_multiple_platforms = bool( - environ.get("GL_ALLOW_FRANKENSTEIN", False) + environ.get("GL_ALLOW_MULTIPLE_PLATFORMS", False) ) + if self._flag_frankenstein: + self._flag_multiple_platforms = True + commit_id_or_hash = None if version is not None: @@ -213,6 +221,20 @@ def flavor(self) -> str: return self._flavor + @property + def features(self) -> Dict[str, Any]: + """ + Returns the features for the cname parsed. + + :return: (dict) Features of the cname + :since: 0.10.14 + """ + + if self._features_cached is None: + self._features_cached = Parser().filter_as_dict(self.flavor) + + return self._features_cached + @property def feature_set(self) -> str: """ @@ -239,7 +261,7 @@ def feature_set_element(self) -> str: if self._feature_elements_cached is not None: return ",".join(self._feature_elements_cached) - return ",".join(Parser().filter_as_dict(self.flavor)["element"]) + return ",".join(self.features["element"]) @property def feature_set_flag(self) -> str: @@ -253,7 +275,7 @@ def feature_set_flag(self) -> str: if self._feature_flags_cached is not None: return ",".join(self._feature_flags_cached) - return ",".join(Parser().filter_as_dict(self.flavor)["flag"]) + return ",".join(self.features["flag"]) @property def feature_set_platform(self) -> str: @@ -265,7 +287,7 @@ def feature_set_platform(self) -> str: """ if self._feature_platforms_cached is None: - platforms = Parser().filter_as_dict(self.flavor)["platform"] + platforms = self.features["platform"] else: platforms = self._feature_platforms_cached @@ -274,7 +296,7 @@ def feature_set_platform(self) -> str: assert len(platforms) < 2 "Only one platform is supported" - return platforms[0] + return platforms[0] # type: ignore[no-any-return] @property def feature_set_list(self) -> List[str]: @@ -293,19 +315,25 @@ def feature_set_list(self) -> List[str]: @property def platform(self) -> str: """ - Returns the feature set of type "platform" for the cname parsed. + Returns the platform for the cname parsed. - :return: (str) Feature set platforms + :return: (str) Platform :since: 0.7.0 """ - if self._feature_platforms_cached is None: - platforms = Parser().filter_as_dict(self.flavor)["platform"] - else: + if self._platform_cached is not None: + platforms = [self._platform_cached] + elif self._feature_platforms_cached is not None: platforms = self._feature_platforms_cached + else: + platforms = self.features["platform"] + + if self._flag_frankenstein and len(platforms) > 1: + return GL_PLATFORM_FRANKENSTEIN if not self._flag_multiple_platforms: assert len(platforms) < 2 + "Only one platform is supported" return platforms[0] @@ -345,18 +373,8 @@ def release_metadata_string(self) -> str: :since: 1.0.0 """ - features = Parser().filter_as_dict(self.flavor) - - if not self._flag_multiple_platforms: - assert len(features["platform"]) < 2 - "Only one platform is supported" - commit_hash = self.commit_hash commit_id = self.commit_id - elements = ",".join(features["element"]) - flags = ",".join(features["flag"]) - platform = features["platform"][0] - platforms = ",".join(features["platform"]) platform_variant = self.platform_variant version = self.version @@ -387,10 +405,10 @@ def release_metadata_string(self) -> str: BUG_REPORT_URL="{GL_BUG_REPORT_URL}" GARDENLINUX_CNAME="{self.cname}" GARDENLINUX_FEATURES="{self.feature_set}" -GARDENLINUX_FEATURES_PLATFORMS="{platforms}" -GARDENLINUX_FEATURES_ELEMENTS="{elements}" -GARDENLINUX_FEATURES_FLAGS="{flags}" -GARDENLINUX_PLATFORM="{platform}" +GARDENLINUX_FEATURES_PLATFORMS="{self.feature_set_platform}" +GARDENLINUX_FEATURES_ELEMENTS="{self.feature_set_element}" +GARDENLINUX_FEATURES_FLAGS="{self.feature_set_flag}" +GARDENLINUX_PLATFORM="{self.platform}" GARDENLINUX_PLATFORM_VARIANT="{platform_variant}" GARDENLINUX_VERSION="{version}" GARDENLINUX_COMMIT_ID="{commit_id}" @@ -456,6 +474,7 @@ def _copy_from_cname_object(self, cname_object: Self) -> None: self._feature_elements_cached = cname_object.feature_set_element.split(",") self._feature_flags_cached = cname_object.feature_set_flag.split(",") self._feature_platforms_cached = cname_object.feature_set_platform.split(",") + self._platform_cached = cname_object.platform self._platform_variant_cached = cname_object.platform_variant self._version = cname_object.version @@ -531,9 +550,7 @@ def new_from_release_file(release_file: PathLike[str] | str) -> "CName": "GARDENLINUX_CNAME", "GARDENLINUX_COMMIT_ID_LONG", "GARDENLINUX_FEATURES", - "GARDENLINUX_FEATURES_ELEMENTS", - "GARDENLINUX_FEATURES_FLAGS", - "GARDENLINUX_FEATURES_PLATFORMS", + "GARDENLINUX_PLATFORM", "GARDENLINUX_VERSION", ): if not release_config.has_option(UNNAMED_SECTION, release_field): @@ -559,23 +576,30 @@ def new_from_release_file(release_file: PathLike[str] | str) -> "CName": UNNAMED_SECTION, "GARDENLINUX_FEATURES" ).strip("\"'") - cname_object._feature_elements_cached = ( - release_config.get(UNNAMED_SECTION, "GARDENLINUX_FEATURES_ELEMENTS") - .strip("\"'") - .split(",") - ) + if release_config.has_option(UNNAMED_SECTION, "GARDENLINUX_FEATURES_ELEMENTS"): + cname_object._feature_elements_cached = ( + release_config.get(UNNAMED_SECTION, "GARDENLINUX_FEATURES_ELEMENTS") + .strip("\"'") + .split(",") + ) - cname_object._feature_flags_cached = ( - release_config.get(UNNAMED_SECTION, "GARDENLINUX_FEATURES_FLAGS") - .strip("\"'") - .split(",") - ) + if release_config.has_option(UNNAMED_SECTION, "GARDENLINUX_FEATURES_FLAGS"): + cname_object._feature_flags_cached = ( + release_config.get(UNNAMED_SECTION, "GARDENLINUX_FEATURES_FLAGS") + .strip("\"'") + .split(",") + ) - cname_object._feature_platforms_cached = ( - release_config.get(UNNAMED_SECTION, "GARDENLINUX_FEATURES_PLATFORMS") - .strip("\"'") - .split(",") - ) + if release_config.has_option(UNNAMED_SECTION, "GARDENLINUX_FEATURES_PLATFORMS"): + cname_object._feature_platforms_cached = ( + release_config.get(UNNAMED_SECTION, "GARDENLINUX_FEATURES_PLATFORMS") + .strip("\"'") + .split(",") + ) + + cname_object._platform_cached = release_config.get( + UNNAMED_SECTION, "GARDENLINUX_PLATFORM" + ).strip("\"'") if release_config.has_option(UNNAMED_SECTION, "GARDENLINUX_PLATFORM_VARIANT"): cname_object._platform_variant_cached = release_config.get( diff --git a/src/gardenlinux/s3/s3_artifacts.py b/src/gardenlinux/s3/s3_artifacts.py index afa1f895..d8ab9f8b 100644 --- a/src/gardenlinux/s3/s3_artifacts.py +++ b/src/gardenlinux/s3/s3_artifacts.py @@ -120,7 +120,26 @@ def upload_from_directory( release_file = artifacts_dir.joinpath(f"{base_name}.release") - cname_object = CName.new_from_release_file(release_file) + try: + cname_object = CName.new_from_release_file(release_file) + except RuntimeError: + if not release_file.exists(): + raise RuntimeError( + f"Release metadata file given is invalid: {release_file}" + ) + + release_config = ConfigParser(allow_unnamed_section=True) + release_config.read(release_file) + + cname_object = CName( + release_config.get(UNNAMED_SECTION, "GARDENLINUX_CNAME").strip("\"'"), + commit_hash=release_config.get( + UNNAMED_SECTION, "GARDENLINUX_COMMIT_ID_LONG" + ).strip("\"'"), + version=release_config.get( + UNNAMED_SECTION, "GARDENLINUX_VERSION" + ).strip("\"'"), + ) if cname_object.version_and_commit_id is None: raise RuntimeError( @@ -170,7 +189,7 @@ def upload_from_directory( commit_id_or_hash = cname_object.commit_id metadata = { - "platform": cname_object.feature_set_platform, + "platform": cname_object.platform, "architecture": arch, "base_image": None, "build_committish": commit_id_or_hash, diff --git a/tests/constants.py b/tests/constants.py index 3eec28d9..2e9cb350 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -14,7 +14,7 @@ CONTAINER_NAME_ZOT_EXAMPLE = f"{REGISTRY}/{REPO_NAME}" GARDENLINUX_ROOT_DIR_EXAMPLE = f"{TEST_DATA_DIR}/gardenlinux/.build" -TEST_PLATFORMS = ["aws", "azure", "gcp", "openstack", "openstackbaremetal", "metal"] +TEST_PLATFORMS = ["aws", "azure", "baremetal", "gcp", "openstack"] TEST_ARCHITECTURES = ["arm64", "amd64"] TEST_FEATURE_STRINGS_SHORT = ["gardener_prod"] TEST_FEATURE_SET = "_slim,base,container" diff --git a/tests/s3/conftest.py b/tests/s3/conftest.py index 792843cd..8ba1fa09 100644 --- a/tests/s3/conftest.py +++ b/tests/s3/conftest.py @@ -24,7 +24,7 @@ class S3Env: def make_cname( - flavor: str = "container", + flavor: str = "container_trustedboot_usi", arch: str = "amd64", version: str = "1234.1", commit: str = "abc123long", diff --git a/tests/s3/constants.py b/tests/s3/constants.py index 1bfecf6f..5595c863 100644 --- a/tests/s3/constants.py +++ b/tests/s3/constants.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- RELEASE_DATA = """ -GARDENLINUX_CNAME="container-amd64-1234.1" +GARDENLINUX_CNAME="container_trustedboot_usi-amd64-1234.1" GARDENLINUX_VERSION=1234.1 GARDENLINUX_COMMIT_ID="abc123lo" GARDENLINUX_COMMIT_ID_LONG="abc123long" +GARDENLINUX_PLATFORM="container" GARDENLINUX_FEATURES="_usi,_trustedboot" GARDENLINUX_FEATURES_ELEMENTS= GARDENLINUX_FEATURES_FLAGS="_usi,_trustedboot" @@ -19,19 +20,23 @@ build_timestamp: {build_timestamp} logs: null modifiers: +- _ephemeral +- _slim +- base +- container - _usi - _trustedboot require_uefi: true secureboot: true published_image_metadata: null s3_bucket: test-bucket -s3_key: meta/singles/container-amd64-1234.1-abc123lo +s3_key: meta/singles/container_trustedboot_usi-amd64-1234.1-abc123lo test_result: null version: '1234.1' paths: -- name: container-amd64-1234.1-abc123lo.release +- name: container_trustedboot_usi-amd64-1234.1-abc123lo.release s3_bucket_name: test-bucket - s3_key: objects/container-amd64-1234.1-abc123lo/container-amd64-1234.1-abc123lo.release + s3_key: objects/container_trustedboot_usi-amd64-1234.1-abc123lo/container_trustedboot_usi-amd64-1234.1-abc123lo.release suffix: .release md5sum: {md5sum} sha256sum: {sha256sum} diff --git a/tests/s3/test_s3_artifacts.py b/tests/s3/test_s3_artifacts.py index 1fb4f867..2d5eda59 100644 --- a/tests/s3/test_s3_artifacts.py +++ b/tests/s3/test_s3_artifacts.py @@ -215,6 +215,30 @@ def test_upload_from_directory_invalid_artifact_name(s3_setup: S3Env) -> None: assert len(list(bucket.objects.filter(Prefix=f"meta/singles/{env.cname}"))) == 1 +def test_upload_from_directory_invalid_release_file_with_valid_cname( + s3_setup: S3Env, +) -> None: + """ + Raise RuntimeError if artifact release file is invalid but contains a valid cname. + """ + # Arrange + env = s3_setup + release_path = env.tmp_path / f"{env.cname}.release" + bad_data = RELEASE_DATA.replace( + "GARDENLINUX_FEATURES_PLATFORMS=", "GARDENLINUX_FEATURES_PLATFORMS_UNDEFINED=" + ) + release_path.write_text(bad_data) + + artifacts = S3Artifacts(env.bucket_name) + + # Act + artifacts.upload_from_directory(env.cname, env.tmp_path) + + # Assert + bucket = env.s3.Bucket(env.bucket_name) + assert len(list(bucket.objects.filter(Prefix=f"meta/singles/{env.cname}"))) == 1 + + def test_upload_from_directory_commit_mismatch(s3_setup: S3Env) -> None: """ Validate that the release file may contain a different commit hash not matching the artifact name.