From b4d51dba105693092dde92ad09c54983105eaa36 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 25 Mar 2026 12:14:36 +0100 Subject: [PATCH 1/2] Remove no longer used GitHub actions Signed-off-by: Tobias Wolf On-behalf-of: SAP --- .github/actions/features_parse/action.yml | 18 ------------ .github/actions/flavors_parse/action.yml | 34 ----------------------- 2 files changed, 52 deletions(-) delete mode 100644 .github/actions/features_parse/action.yml delete mode 100644 .github/actions/flavors_parse/action.yml diff --git a/.github/actions/features_parse/action.yml b/.github/actions/features_parse/action.yml deleted file mode 100644 index 99c79a2f..00000000 --- a/.github/actions/features_parse/action.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: features_parse -description: Parses the given GardenLinux features parameters -inputs: - flags: - description: "Flags passed to `gl-features-parse`" - required: true -outputs: - result: - description: "features result" - value: ${{ steps.result.outputs.result }} -runs: - using: composite - steps: - - uses: gardenlinux/python-gardenlinux-lib/.github/actions/setup@0.10.11 - - id: result - shell: bash - run: | - echo "result=$(gl-features-parse ${{ inputs.flags }})" | tee -a $GITHUB_OUTPUT diff --git a/.github/actions/flavors_parse/action.yml b/.github/actions/flavors_parse/action.yml deleted file mode 100644 index b31b58a2..00000000 --- a/.github/actions/flavors_parse/action.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: flavors_parse -description: Parses the given GardenLinux flavors parameters -inputs: - flags: - description: "Flags passed to `gl-flavors-parse`" - required: true - flavors_matrix: - description: "Generated GitHub workflow flavors matrix" -outputs: - matrix: - description: "Flavors matrix" - value: ${{ steps.matrix.outputs.matrix }} -runs: - using: composite - steps: - - uses: gardenlinux/python-gardenlinux-lib/.github/actions/setup@0.10.11 - - id: matrix - shell: bash - run: | - MATRIX='${{ inputs.flavors_matrix }}' - - if [[ $(echo "${MATRIX}" | jq -r 'type') != 'object' ]]; then - FLAVORS=$(gl-flavors-parse ${{ inputs.flags }}) - MATRIX=$(jq -nc \ - --argjson flavors "$(echo $FLAVORS)" \ - '{ - include: ( - $flavors | reduce (to_entries[]) as $item ([]; . + ($item.value | map({"arch": $item.key, "flavor": .}))) - ) - }' - ) - fi - - echo "matrix=$MATRIX" | tee -a $GITHUB_OUTPUT From b1d55ca51a98e3b413c0d657040d788d602da041 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Mon, 7 Sep 2026 13:44:28 +0200 Subject: [PATCH 2/2] Implement changes for ADR034 and 035. Signed-off-by: Tobias Wolf On-behalf-of: --- docs/cli.rst | 16 +- pyproject.toml | 4 +- src/gardenlinux/constants.py | 5 +- src/gardenlinux/features/__init__.py | 7 +- src/gardenlinux/features/__main__.py | 370 +++++----- .../features/artifact_base_name.py | 441 ++++++++++++ src/gardenlinux/features/c_name.py | 235 +++++++ src/gardenlinux/features/cname.py | 613 ----------------- src/gardenlinux/features/cname_main.py | 131 ---- src/gardenlinux/features/flavor.py | 88 +++ src/gardenlinux/features/metadata_main.py | 118 +++- src/gardenlinux/features/parser.py | 47 +- .../features/reproducibility/diff_parser.py | 20 +- .../reproducibility/markdown_formatter.py | 20 +- src/gardenlinux/features/versioned_flavor.py | 97 +++ src/gardenlinux/flavors/parser.py | 8 +- .../github/release/release_images_metadata.py | 17 +- src/gardenlinux/oci/container.py | 40 +- src/gardenlinux/oci/image_manifest.py | 23 +- src/gardenlinux/s3/__main__.py | 14 +- src/gardenlinux/s3/s3_artifacts.py | 70 +- test-data/gardenlinux | 2 +- .../diff_files/compare_nightly.md | 2 +- .../compare_nightly_different_commit.md | 2 +- .../diff_files/different_commit.md | 2 +- .../reproducibility/diff_files/one_issue.md | 2 +- tests/features/constants.py | 20 +- tests/features/test_cname.py | 21 +- tests/features/test_cname_main.py | 159 ----- tests/features/test_main.py | 633 ++++++++++++------ tests/features/test_metadata_main.py | 79 ++- tests/features/test_parser.py | 29 +- tests/features/test_reproducibility.py | 178 ++--- tests/oci/test_image_manifest.py | 29 +- tests/s3/conftest.py | 8 +- tests/s3/constants.py | 3 +- tests/s3/test_main.py | 4 +- tests/s3/test_s3_artifacts.py | 144 ++-- 38 files changed, 2028 insertions(+), 1673 deletions(-) create mode 100644 src/gardenlinux/features/artifact_base_name.py create mode 100644 src/gardenlinux/features/c_name.py delete mode 100644 src/gardenlinux/features/cname.py delete mode 100644 src/gardenlinux/features/cname_main.py create mode 100644 src/gardenlinux/features/flavor.py create mode 100644 src/gardenlinux/features/versioned_flavor.py delete mode 100644 tests/features/test_cname_main.py diff --git a/docs/cli.rst b/docs/cli.rst index f975b2aa..189a38fe 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -6,20 +6,20 @@ Available command-line tools provided by the Garden Linux Python Library Features Commands ----------------- -gl-cname -~~~~~~~~ - -Generate a canonical name (cname) from feature sets. - -.. autoprogram:: gardenlinux.features.cname_main:get_parser() - gl-features-parse ~~~~~~~~~~~~~~~~~ -Parse and extract information from GardenLinux features. +Parse and extract information from Garden Linux features. .. autoprogram:: gardenlinux.features.__main__:get_parser() +gl-features-metadata +~~~~~~~~~~~~~~~~~~~~ + +Provides Garden Linux release metadata file handling. + +.. autoprogram:: gardenlinux.features.metadata_main:get_parser() + Flavors Commands ---------------- diff --git a/pyproject.toml b/pyproject.toml index 6db81aed..1060cf62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "gardenlinux" -version = "0.10.19" +version = "1.0.0" 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" @@ -45,12 +45,12 @@ sphinxcontrib-autoprogram = "^0.1.8" sphinx-markdown-builder = "^0.6.10" [tool.poetry.scripts] -gl-cname = "gardenlinux.features.cname_main:main" gl-feature-fs-diff = "gardenlinux.features.reproducibility.__main__:main" gl-features-parse = "gardenlinux.features.__main__:main" gl-flavors-parse = "gardenlinux.flavors.__main__:main" gl-gh-release = "gardenlinux.github.release.__main__:main" gl-oci = "gardenlinux.oci.__main__:main" +gl-metadata = "gardenlinux.features.metadata_main:main" gl-s3 = "gardenlinux.s3.__main__:main" [tool.pytest.ini_options] diff --git a/src/gardenlinux/constants.py b/src/gardenlinux/constants.py index 5c2fe8ed..d61101e6 100644 --- a/src/gardenlinux/constants.py +++ b/src/gardenlinux/constants.py @@ -4,9 +4,9 @@ ARCHS = ["amd64", "arm64"] # GardenLinux "bare" feature -BARE_FLAVOR_FEATURE_CONTENT = {"description": "Bare flavor", "type": "platform"} +BARE_CNAME_FEATURE_CONTENT = {"description": "Bare flavor", "type": "platform"} -BARE_FLAVOR_LIBC_FEATURE_CONTENT = { +BARE_CNAME_LIBC_FEATURE_CONTENT = { "description": "Bare libc feature", "type": "element", } @@ -147,6 +147,7 @@ GL_BUG_REPORT_URL = "https://github.com/gardenlinux/gardenlinux/issues" GL_COMMIT_SPECIAL_VALUES = ("local",) +GL_VERSION_SPECIAL_VALUES = ("today",) GL_CONTAINER_REGISTRY_BASE_URL = "ghcr.io/gardenlinux/gardenlinux" GL_DEB_REPO_BASE_URL = "https://packages.gardenlinux.io/gardenlinux" GL_DISTRIBUTION_NAME = "Garden Linux" diff --git a/src/gardenlinux/features/__init__.py b/src/gardenlinux/features/__init__.py index a6e7692e..ed61af2f 100644 --- a/src/gardenlinux/features/__init__.py +++ b/src/gardenlinux/features/__init__.py @@ -4,7 +4,10 @@ Features module """ -from .cname import CName +from .artifact_base_name import ArtifactBaseName +from .c_name import CName +from .flavor import Flavor from .parser import Parser +from .versioned_flavor import VersionedFlavor -__all__ = ["CName", "Parser"] +__all__ = ["ArtifactBaseName", "CName", "Flavor", "Parser", "VersionedFlavor"] diff --git a/src/gardenlinux/features/__main__.py b/src/gardenlinux/features/__main__.py index d0b7f8ad..7cd2501e 100644 --- a/src/gardenlinux/features/__main__.py +++ b/src/gardenlinux/features/__main__.py @@ -6,31 +6,32 @@ """ import argparse -import logging import os import re from os import path from typing import Any, Set -from .cname import CName +from .artifact_base_name import ArtifactBaseName from .parser import Parser _ARGS_TYPE_ALLOWED = [ + "arch", + "artifact-base-name", "cname", - "cname_base", - "container_name", - "container_tag", - "commit_id", + "commit-id", + "container-name", + "container-tag", + "elements", "features", - "platform", - "platforms", "flags", "flavor", - "elements", - "arch", - "version", - "version_and_commit_id", "graph", + "platform", + "platform-variant", + "platforms", + "version", + "version_and_commit-id", + "versioned-flavor", ] RE_CAMEL_CASE_SPLITTER = re.compile("([A-Z]+|[a-z0-9])([A-Z])(?!$)") @@ -50,20 +51,25 @@ def get_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="gl-features-parse", - description="Parse and extract information from GardenLinux features.", + description="Parse and extract information from Garden Linux features.", ) parser.add_argument( "--arch", dest="arch", - help="Target architecture (e.g., amd64, arm64). Overrides architecture from cname.", + help="Target architecture (e.g., amd64, arm64). Overrides architecture from CName.", + ) + + parser.add_argument( + "--artifact-base-name", + dest="artifact_base_name", + help="Artifact base name to parse. Must be a valid Garden Linux Artifact Base Name.", ) parser.add_argument( "--cname", dest="cname", - required=True, - help="Canonical name (cname) to parse. Must be a valid GardenLinux canonical name.", + help="Canonical Name (CName) to parse.", ) parser.add_argument( @@ -79,21 +85,23 @@ def get_parser() -> argparse.ArgumentParser: ) parser.add_argument( - "--release-file", - dest="release_file", - help="Path to a release file containing cname metadata. Either --feature-dir or --release-file must be provided.", + "--flavor", + dest="flavor", + help="Garden Linux flavor name.", ) parser.add_argument( - "--default-arch", - dest="default_arch", - help="Default architecture to use if architecture cannot be determined from cname or other sources.", + "--ignore", + dest="ignore", + type=lambda arg: set([f for f in arg.split(",") if f]), + default=set(), + help="Comma-separated list of features to ignore when processing (e.g., 'feature1,feature2').", ) parser.add_argument( - "--default-version", - dest="default_version", - help="Default version to use if version cannot be determined from files or other sources.", + "--release-file", + dest="release_file", + help="Path to a release file containing features metadata. Either --feature-dir or --release-file must be provided.", ) parser.add_argument( @@ -103,11 +111,9 @@ def get_parser() -> argparse.ArgumentParser: ) parser.add_argument( - "--ignore", - dest="ignore", - type=lambda arg: set([f for f in arg.split(",") if f]), - default=set(), - help="Comma-separated list of features to ignore when processing (e.g., 'feature1,feature2').", + "--versioned-flavor", + dest="versioned_flavor", + help="Garden Linux versioned flavor name.", ) parser.add_argument( @@ -133,143 +139,78 @@ def main() -> None: parser = get_parser() args = parser.parse_args() - assert bool(args.feature_dir) or bool(args.release_file), ( - "Please provide either `--feature_dir` or `--release_file` argument" + assert args.feature_dir or args.release_file or os.environ.get("GL_ROOT_DIR"), ( + "Please provide either `--feature-dir` or `--release-file` argument" ) arch = args.arch commit_id_or_hash = args.commit - gardenlinux_root = path.dirname(args.feature_dir) + gardenlinux_root = os.environ.get("GL_ROOT_DIR", path.dirname(args.feature_dir)) version = args.version - if arch is None or arch == "": - arch = args.default_arch - - if gardenlinux_root == "": + if not gardenlinux_root: gardenlinux_root = "." - if version is None or version == "": - try: - version, commit_id_or_hash = get_version_and_commit_id_from_files( - gardenlinux_root - ) - except RuntimeError as exc: - logging.debug( - "Failed to parse version information for GL root '{0}': {1}".format( - gardenlinux_root, exc - ) - ) - - version = args.default_version + if not version or not commit_id_or_hash: + version, commit_id_or_hash = ( + ArtifactBaseName.get_version_and_commit_id_from_files(gardenlinux_root) + ) - cname = CName(args.cname, arch=arch, commit_hash=commit_id_or_hash, version=version) + abn_object = ArtifactBaseName.new_instance( + args.artifact_base_name, + cname=args.cname, + flavor=args.flavor, + versioned_flavor=args.versioned_flavor, + arch=arch, + version=version, + commit_id_or_hash=commit_id_or_hash, + ) if args.release_file is not None: - cname.load_from_release_file(args.release_file) - - arch = cname.arch - flavor = cname.flavor - commit_id_or_hash = cname.commit_id - version = cname.version - - if (arch is None or arch == "") and ( - args.type in ("cname", "container_name", "arch") - ): - raise RuntimeError( - "Architecture could not be determined and no default architecture set" - ) + abn_object.load_from_release_file(args.release_file) - if (commit_id_or_hash is None or commit_id_or_hash == "") and ( - args.type in ("container_tag", "commit_id", "version_and_commit_id") - ): - raise RuntimeError("Commit ID not specified") - - if (version is None or version == "") and ( - args.type - in ( - "container_tag", - "commit_id", - "version", - "version_and_commit_id", - ) - ): - raise RuntimeError("Version not specified and no default version set") + arch = abn_object.arch + cname = abn_object.cname + commit_id_or_hash = abn_object.commit_id + version = abn_object.version feature_dir_name = path.basename(args.feature_dir) if args.type == "arch": print(arch) elif args.type in ( - "cname_base", + "artifact-base-name", "cname", - "container_name", + "container-name", "elements", "features", "flags", "flavor", "graph", "platform", + "platform-variant", "platforms", + "versioned-flavor", ): if args.type == "graph" or len(args.ignore) > 0: features_parser = Parser(gardenlinux_root, feature_dir_name) print_output_from_features_parser( - args.type, cname, features_parser, flavor, args.ignore + args.type, abn_object, features_parser, cname, args.ignore ) else: - print_output_from_cname(args.type, cname) - elif args.type == "commit_id": - print(commit_id_or_hash[:8]) # type: ignore[index] - elif args.type == "container_tag": - print(re.sub("\\W+", "-", f"{version}-{commit_id_or_hash[:8]}")) # type: ignore[index] + print_output_from_abn_object(args.type, abn_object) + elif args.type == "commit-id": + print(commit_id_or_hash[:8]) + elif args.type == "container-tag": + print(re.sub("\\W+", "-", f"{version}-{commit_id_or_hash[:8]}")) elif args.type == "version": print(version) - elif args.type == "version_and_commit_id": - print(f"{version}-{commit_id_or_hash[:8]}") # type: ignore[index] - - -def get_version_and_commit_id_from_files(gardenlinux_root: str) -> tuple[str, str]: - """ - Returns the version and commit ID based on files in the GardenLinux root directory. - - :param gardenlinux_root: GardenLinux root directory - - :return: (tuple) Version and commit ID if readable - :since: 0.7.0 - """ - - commit_hash = None - version = None - - if os.access(path.join(gardenlinux_root, "COMMIT"), os.R_OK): - with open(path.join(gardenlinux_root, "COMMIT"), "r") as fp: - commit_hash = fp.read().strip()[:8] - - if os.access(path.join(gardenlinux_root, "VERSION"), os.R_OK): - with open(path.join(gardenlinux_root, "VERSION"), "r") as fp: - version = fp.read().strip() - - if commit_hash is None or version is None: - raise RuntimeError("Failed to read version or commit ID from files") - - return (version, commit_hash) + elif args.type == "version_and_commit-id": + print(f"{version}-{commit_id_or_hash[:8]}") -def get_minimal_feature_set(graph: Any) -> Set[str]: - """ - Returns the minimal set of features described by the given graph. - - :param graph: networkx.Digraph - - :return: (set) Minimal set of features - :since: 0.7.0 - """ - - return set([node for (node, degree) in graph.in_degree() if degree == 0]) - - -def graph_as_mermaid_markup(flavor: str | None, graph: Any) -> str: +def graph_as_mermaid_markup(cname: str | None, graph: Any) -> str: """ Generates a mermaid.js representation of the graph. This is helpful to identify dependencies between features. @@ -277,17 +218,17 @@ def graph_as_mermaid_markup(flavor: str | None, graph: Any) -> str: Syntax docs: https://mermaid.js.org/syntax/flowchart.html?id=flowcharts-basic-syntax - :param flavor: Flavor name - :param graph: networkx.Digraph + :param cname: Garden Linux canonical name + :param graph: networkx.Digraph :return: (str) mermaid.js representation :since: 0.7.0 """ - if flavor is None: - raise RuntimeError("Error while generating graph: Flavor is None!") + if cname is None: + raise RuntimeError("Error while generating graph: CName is None!") - markup = f"---\ntitle: Dependency Graph for Feature {flavor}\n---\ngraph TD;\n" + markup = f"---\ntitle: Dependency Graph for Feature {cname}\n---\ngraph TD;\n" for u, v in graph.edges: markup += f" {u}-->{v};\n" @@ -297,9 +238,9 @@ def graph_as_mermaid_markup(flavor: str | None, graph: Any) -> str: def print_output_from_features_parser( output_type: str, - cname_instance: CName, + abn_object: ArtifactBaseName, parser: Parser, - flavor: str, + cname: str, ignores_list: Set[str], ) -> None: """ @@ -307,7 +248,7 @@ def print_output_from_features_parser( :param output_type: Output type :param parser: Features parser - :param flavor: Flavor + :param cname: Garden Linux canonical name :param ignores_list: Features to ignore :since: 1.0.0 @@ -316,54 +257,61 @@ def print_output_from_features_parser( def additional_filter_func(node: str) -> bool: return node not in ignores_list - if output_type == "features": - print( - parser.filter_as_string( - flavor, additional_filter_func=additional_filter_func + match output_type: + case "features": + print( + parser.filter_as_string( + cname, additional_filter_func=additional_filter_func + ) + ) + case "platform" | "platforms" | "elements" | "flags": + features_by_type = parser.filter_as_dict( + cname, additional_filter_func=additional_filter_func ) - ) - elif output_type in ("platform", "platforms", "elements", "flags"): - features_by_type = parser.filter_as_dict( - flavor, additional_filter_func=additional_filter_func - ) - - if output_type == "platform": - print(features_by_type["platform"][0]) - if output_type == "platforms": - print(",".join(features_by_type["platform"])) - elif output_type == "elements": - print(",".join(features_by_type["element"])) - elif output_type == "flags": - print(",".join(features_by_type["flag"])) - else: - graph = parser.filter(flavor, additional_filter_func=additional_filter_func) - - sorted_features = Parser.sort_graph_nodes(graph) - minimal_feature_set = get_minimal_feature_set(graph) - - sorted_minimal_features = Parser.subset(minimal_feature_set, sorted_features) - - cname_base = Parser.get_flavor_from_feature_set(sorted_minimal_features) - - if output_type == "cname_base": - print(cname_base) - elif output_type == "cname": - cname = flavor - - if cname_instance.arch is not None: - cname += f"-{cname_instance.arch}" - - if cname_instance.version_and_commit_id is not None: - cname += f"-{cname_instance.version_and_commit_id}" - - print(cname) - elif output_type == "container_name": - print(RE_CAMEL_CASE_SPLITTER.sub("\\1_\\2", cname_base).lower()) - elif output_type == "graph": - print(graph_as_mermaid_markup(flavor, graph)) + match output_type: + case "elements": + print(",".join(features_by_type["element"])) + case "flags": + print(",".join(features_by_type["flag"])) + case "platform": + print(features_by_type["platform"][0]) + case "platforms": + print(",".join(features_by_type["platform"])) + case "platform-variant": + print(abn_object.platform_variant) + case _: + graph = parser.filter(cname, additional_filter_func=additional_filter_func) + + sorted_features = Parser.sort_graph_nodes(graph) + minimal_feature_set = Parser.get_minimal_feature_set(graph) + + sorted_minimal_features = Parser.subset( + minimal_feature_set, sorted_features + ) -def print_output_from_cname(output_type: str, cname_instance: CName) -> None: + cname = Parser.get_cname_from_feature_set(sorted_minimal_features) + + match output_type: + case "artifact-base-name": + print( + f"{cname}-{abn_object.arch}-{abn_object.version_and_commit_id}" + ) + case "cname": + print(cname) + case "container-name": + print(RE_CAMEL_CASE_SPLITTER.sub("\\1_\\2", cname).lower()) + case "flavor": + print(f"{cname}-{abn_object.arch}") + case "graph": + print(graph_as_mermaid_markup(cname, graph)) + case "versioned-flavor": + print(f"{cname}-{abn_object.arch}-{abn_object.version}") + + +def print_output_from_abn_object( + output_type: str, abn_object: ArtifactBaseName +) -> None: """ Prints output to stdout based on the given CName instance. @@ -373,33 +321,35 @@ def print_output_from_cname(output_type: str, cname_instance: CName) -> None: :since: 1.0.0 """ - if output_type in ("cname_base", "cname", "flavor"): - sorted_features = Parser.get_flavor_as_feature_set(cname_instance.flavor) - flavor = Parser.get_flavor_from_feature_set(sorted_features) - - if output_type in ("cname_base", "flavor"): - print(flavor) - else: - if cname_instance.version_and_commit_id is None: - raise RuntimeError( - "Version and commit ID can't be provided without appropriate input." - ) - - print( - f"{flavor}-{cname_instance.arch}-{cname_instance.version_and_commit_id}" - ) - elif output_type == "container_name": - print(RE_CAMEL_CASE_SPLITTER.sub("\\1-\\2", cname_instance.flavor).lower()) - elif output_type == "platform": - print(cname_instance.platform) - elif output_type == "platforms": - print(cname_instance.feature_set_platform) - elif output_type == "elements": - print(cname_instance.feature_set_element) - elif output_type == "features": - print(cname_instance.feature_set) - elif output_type == "flags": - print(cname_instance.feature_set_flag) + match output_type: + case "artifact-base-name" | "cname" | "flavor" | "versioned-flavor": + cname = abn_object.cname + + match output_type: + case "artifact-base-name": + print( + f"{cname}-{abn_object.arch}-{abn_object.version_and_commit_id}" + ) + case "cname": + print(cname) + case "flavor": + print(f"{cname}-{abn_object.arch}") + case "versioned-flavor": + print(f"{cname}-{abn_object.arch}-{abn_object.version}") + case "container-name": + print(RE_CAMEL_CASE_SPLITTER.sub("\\1-\\2", abn_object.cname).lower()) + case "elements": + print(abn_object.feature_set_element) + case "features": + print(abn_object.feature_set) + case "flags": + print(abn_object.feature_set_flag) + case "platform": + print(abn_object.platform) + case "platform-variant": + print(abn_object.platform_variant) + case "platforms": + print(abn_object.feature_set_platform) if __name__ == "__main__": diff --git a/src/gardenlinux/features/artifact_base_name.py b/src/gardenlinux/features/artifact_base_name.py new file mode 100644 index 00000000..65bde754 --- /dev/null +++ b/src/gardenlinux/features/artifact_base_name.py @@ -0,0 +1,441 @@ +# -*- coding: utf-8 -*- + +""" +Artifact base name (ABN) +""" + +import os +import re +from configparser import UNNAMED_SECTION, ConfigParser +from os import PathLike +from pathlib import Path +from typing import Optional, Self + +from ..constants import ( + GL_BUG_REPORT_URL, + GL_COMMIT_SPECIAL_VALUES, + GL_DISTRIBUTION_NAME, + GL_HOME_URL, + GL_RELEASE_ID, + GL_SUPPORT_URL, + GL_VERSION_SPECIAL_VALUES, +) +from .versioned_flavor import VersionedFlavor + + +class ArtifactBaseName(VersionedFlavor): + """ + Class to represent an artifact base name (ABN). It can be parsed from any + file type. + + :author: Garden Linux Maintainers + :copyright: Copyright 2026 SAP SE + :package: gardenlinux + :subpackage: features + :since: 1.0.0 + :license: https://www.apache.org/licenses/LICENSE-2.0 + Apache License, Version 2.0 + """ + + def __init__( + self, + artifact: str, + ): + """ + Constructor __init__(ArtifactBaseName) + + :param artifact: Artifact name + + :since: 1.0.0 + """ + + re_object = re.compile( + "([a-zA-Z0-9]+([\\_\\-][a-zA-Z0-9]+)*?)-([a-z0-9]+)-([a-z0-9.]+)-([a-z0-9]+)(\\.|$)" + ) + + re_match = re_object.match(artifact) + + assert re_match, f"Not a valid Garden Linux artifact: {artifact}" + + commit_id_or_hash = re_match[5] + + self._commit_id = commit_id_or_hash[:8] + self._commit_hash = None + + if ( + len(commit_id_or_hash) == 40 + or commit_id_or_hash in GL_COMMIT_SPECIAL_VALUES + ): # sha1 hex + self._commit_hash = commit_id_or_hash + + VersionedFlavor.__init__(self, re_match[1], re_match[3], re_match[4]) + + @property + def commit_hash(self) -> Optional[str]: + """ + Returns the commit hash if part of the ABN parsed. + + :return: (str) Commit hash + :since: 1.0.0 + """ + + return self._commit_hash + + @commit_hash.setter + def commit_hash(self, commit_hash: str) -> None: + """ + Sets the commit hash + + :param commit_hash: Commit hash + + :since: 1.0.0 + """ + + if self._commit_id is not None and not commit_hash.startswith(self._commit_id): + raise RuntimeError("Commit hash given differs from commit ID already set") + + self._commit_id = commit_hash[:8] + self._commit_hash = commit_hash + + @property + def commit_id(self) -> str: + """ + Returns the commit ID if part of the ABN parsed. + + :return: (str) Commit ID + :since: 1.0.0 + """ + + return self._commit_id + + @property + def release_metadata_string(self) -> str: + """ + Returns the release metadata describing the given ABN instance. + + :return: (str) Release metadata + :since: 1.0.0 + """ + + commit_hash = self.commit_hash + commit_id = self.commit_id + platform_variant = self.platform_variant + version = self.version + + if commit_hash is None: + commit_hash = commit_id + + if platform_variant is None: + platform_variant = "" + + if version is None: + pretty_name = f"{GL_DISTRIBUTION_NAME} unsupported version" + version = "" + else: + pretty_name = f"{GL_DISTRIBUTION_NAME} {version}" + + metadata = f""" +ID={GL_RELEASE_ID} +ID_LIKE=debian +NAME="{GL_DISTRIBUTION_NAME}" +PRETTY_NAME="{pretty_name}" +IMAGE_VERSION={version} +VARIANT_ID="{self.flavor}" +HOME_URL="{GL_HOME_URL}" +SUPPORT_URL="{GL_SUPPORT_URL}" +BUG_REPORT_URL="{GL_BUG_REPORT_URL}" +GARDENLINUX_CNAME="{self.cname}" +GARDENLINUX_FEATURES="{self.feature_set}" +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}" +GARDENLINUX_COMMIT_ID_LONG="{commit_hash}" + """.strip() + + return metadata + + @property + def version_and_commit_id(self) -> str: + """ + Returns the version and commit ID of the ABN parsed. + + :return: (str) Version and commit ID + :since: 1.0.0 + """ + + return f"{self.version}-{self.commit_id}" + + def __str__(self) -> str: + """ + Returns the Garden Linux artifact base name. + + :return: (str) Returns the ABN + :since: 1.0.0 + """ + + versioned_flavor = VersionedFlavor.__str__(self) + versioned_flavor += f"-{self.commit_id}" + + return versioned_flavor + + def _copy_from_instance(self, artifact_base_name: Self) -> None: + """ + Copies values from a given ABN instance. + + :param artifact_base_name: ABN instance + + :since: 1.0.0 + """ + + VersionedFlavor._copy_from_instance(self, artifact_base_name) + + self._commit_hash = artifact_base_name.commit_hash + self._commit_id = artifact_base_name.commit_id + + def load_from_release_file(self, release_file: PathLike[str] | str) -> None: + """ + Loads and parses a release metadata file. + + :param release_file: Release metadata file + + :since: 1.0.0 + """ + + artifact_base_name = self.__class__.new_from_release_file(release_file) + + if ( + artifact_base_name.flavor != self.flavor + or ( + self._commit_id is not None + and self._commit_id != artifact_base_name.commit_id + ) + or ( + self._version is not None + and self._version != artifact_base_name.version + ) + or ( + not self._flag_frankenstein + and artifact_base_name.platform + not in artifact_base_name.feature_set_platform + ) + ): + raise RuntimeError( + f"Release metadata file given is invalid: {release_file} failed consistency check - {self} != {artifact_base_name}" + ) + + self._copy_from_instance(artifact_base_name) + + def save_to_release_file( + self, release_file: PathLike[str] | str, overwrite: Optional[bool] = False + ) -> None: + """ + Saves the release metadata file. + + :param release_file: Release metadata file + + :since: 1.0.0 + """ + + if not isinstance(release_file, PathLike): + release_file = Path(release_file) + + if not overwrite and release_file.exists(): # type: ignore[attr-defined] + raise RuntimeError( + f"Refused to overwrite existing release metadata file: {release_file}" + ) + + with release_file.open("w") as fp: # type: ignore[attr-defined] + fp.write(self.release_metadata_string) + + @staticmethod + def new_from_release_file(release_file: PathLike[str] | str) -> "ArtifactBaseName": + """ + Loads and parses a release metadata file to return a new ABN instance. + + :param release_file: Release metadata file + + :return: (object) ABN instance + :since: 1.0.0 + """ + + if not isinstance(release_file, PathLike): + release_file = Path(release_file) + + if not release_file.exists(): # type: ignore[attr-defined] + raise RuntimeError( + f"Release metadata file given is invalid: {release_file}" + ) + + release_config = ConfigParser(allow_unnamed_section=True) + release_config.read(release_file) + + for release_field in ( + "VARIANT_ID", + "GARDENLINUX_COMMIT_ID_LONG", + "GARDENLINUX_FEATURES", + "GARDENLINUX_PLATFORM", + "GARDENLINUX_VERSION", + ): + if not release_config.has_option(UNNAMED_SECTION, release_field): + raise RuntimeError( + f"Release metadata file given is invalid: {release_file} misses {release_field}" + ) + + artifact_base_name = release_config.get(UNNAMED_SECTION, "VARIANT_ID").strip( + "\"'" + ) + + artifact_base_name += "-" + release_config.get( + UNNAMED_SECTION, "GARDENLINUX_VERSION" + ).strip("\"'") + + artifact_base_name += "-" + release_config.get( + UNNAMED_SECTION, "GARDENLINUX_COMMIT_ID_LONG" + ).strip("\"'") + + abn_object = ArtifactBaseName(artifact_base_name) + + abn_object._feature_set_cached = release_config.get( + UNNAMED_SECTION, "GARDENLINUX_FEATURES" + ).strip("\"'") + + if release_config.has_option(UNNAMED_SECTION, "GARDENLINUX_FEATURES_ELEMENTS"): + abn_object._feature_elements_cached = ( + release_config.get(UNNAMED_SECTION, "GARDENLINUX_FEATURES_ELEMENTS") + .strip("\"'") + .split(",") + ) + + if release_config.has_option(UNNAMED_SECTION, "GARDENLINUX_FEATURES_FLAGS"): + abn_object._feature_flags_cached = ( + release_config.get(UNNAMED_SECTION, "GARDENLINUX_FEATURES_FLAGS") + .strip("\"'") + .split(",") + ) + + if release_config.has_option(UNNAMED_SECTION, "GARDENLINUX_FEATURES_PLATFORMS"): + abn_object._feature_platforms_cached = ( + release_config.get(UNNAMED_SECTION, "GARDENLINUX_FEATURES_PLATFORMS") + .strip("\"'") + .split(",") + ) + + abn_object._platform_cached = release_config.get( + UNNAMED_SECTION, "GARDENLINUX_PLATFORM" + ).strip("\"'") + + if release_config.has_option(UNNAMED_SECTION, "GARDENLINUX_PLATFORM_VARIANT"): + abn_object._platform_variant_cached = release_config.get( + UNNAMED_SECTION, "GARDENLINUX_PLATFORM_VARIANT" + ).strip("\"'") + + return abn_object + + @staticmethod + def get_version_and_commit_id_from_files( + gardenlinux_root: Path | str, + ) -> tuple[Optional[str], Optional[str]]: + """ + Returns the version and commit ID based on files in the GardenLinux root directory. + + :param gardenlinux_root: GardenLinux root directory + + :return: (tuple) Version and commit ID if readable; None for both values otherwise + :since: 1.0.0 + """ + + if not isinstance(gardenlinux_root, PathLike): + gardenlinux_root = Path(gardenlinux_root) + + commit_hash = None + version = None + + if os.access(gardenlinux_root.joinpath("COMMIT"), os.R_OK): + with gardenlinux_root.joinpath("COMMIT").open("r") as fp: + commit_hash = fp.read().strip()[:8] + + if os.access(gardenlinux_root.joinpath("VERSION"), os.R_OK): + with gardenlinux_root.joinpath("VERSION").open("r") as fp: + version = fp.read().strip() + + if commit_hash is None or version is None: + return (None, None) + + return (version, commit_hash) + + @staticmethod + def new_instance( + artifact_base_name: Optional[str] = None, + versioned_flavor: Optional[str] = None, + flavor: Optional[str] = None, + cname: Optional[str] = None, + arch: Optional[str] = None, + version: Optional[str] = None, + commit_id_or_hash: Optional[str] = None, + ) -> "ArtifactBaseName": + """ + Returns a new ABN instance based on various input data available. + + :param artifact_base_name: Artifact base name + :param versioned_flavor: Garden Linux versioned flavor + :param flavor: Garden Linux flavor + :param cname: Garden Linux Ccanonical Name + :param arch: Artifact architecture + :param version: Artifact version + :param commit_id_or_hash: Artifact commit ID or hash + + :return: (object) ABN instance + :since: 1.0.0 + """ + + if not artifact_base_name: + if versioned_flavor: + if not commit_id_or_hash: + raise ValueError("Argument missing: commit_id_or_hash") + + artifact_base_name = versioned_flavor + f"-{commit_id_or_hash}" + elif flavor: + if not version: + raise ValueError("Argument missing: version") + if not commit_id_or_hash: + raise ValueError("Argument missing: commit_id_or_hash") + + artifact_base_name = flavor + f"-{version}-{commit_id_or_hash}" + elif cname: + if not arch: + raise ValueError("Argument missing: arch") + if not version: + raise ValueError("Argument missing: version") + if not commit_id_or_hash: + raise ValueError("Argument missing: commit_id_or_hash") + + artifact_base_name = cname + f"-{arch}-{version}-{commit_id_or_hash}" + else: + raise ValueError( + "Argument missing: At least one of artifact_base_name, cname, flavor or versioned_flavor" + ) + + abn_object = ArtifactBaseName(artifact_base_name) + + if cname and abn_object.cname != cname: + raise RuntimeError("CName does not match the given argument") + if arch and abn_object.arch != arch: + raise RuntimeError("Architecture does not match the given argument") + if ( + version + and version not in GL_VERSION_SPECIAL_VALUES + and abn_object.version != version + ): + raise RuntimeError("Version does not match the given argument") + + if commit_id_or_hash: + if abn_object.commit_hash and abn_object.commit_hash != commit_id_or_hash: + raise RuntimeError("Commit hash does not match the given argument") + elif abn_object.commit_id != commit_id_or_hash: + raise RuntimeError("Commit ID does not match the given argument") + + return abn_object diff --git a/src/gardenlinux/features/c_name.py b/src/gardenlinux/features/c_name.py new file mode 100644 index 00000000..cdb6a2fa --- /dev/null +++ b/src/gardenlinux/features/c_name.py @@ -0,0 +1,235 @@ +# -*- coding: utf-8 -*- + +""" +Garden Linux Canonical Name (CName) +""" + +from os import environ +from typing import Any, Dict, List, Optional, Self + +from ..constants import GL_PLATFORM_FRANKENSTEIN +from .parser import Parser + + +class CName(object): + """ + Class to represent a Garden Linux Canonical Name (CName). + + :author: Garden Linux Maintainers + :copyright: Copyright 2024 SAP SE + :package: gardenlinux + :subpackage: features + :since: 1.0.0 + :license: https://www.apache.org/licenses/LICENSE-2.0 + Apache License, Version 2.0 + """ + + def __init__( + self, + cname: str, + ): + """ + Constructor __init__(CName) + + :param cname: CName to represent + + :since: 1.0.0 + """ + + self._cname = cname + self._feature_elements_cached: Optional[List[str]] = None + 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._flag_frankenstein = bool(environ.get("GL_ALLOW_FRANKENSTEIN", False)) + + self._flag_multiple_platforms = bool( + environ.get("GL_ALLOW_MULTIPLE_PLATFORMS", False) + ) + + if self._flag_frankenstein: + self._flag_multiple_platforms = True + + @property + def cname(self) -> str: + """ + Returns the CName parsed. + + :return: (str) CName + :since: 0.7.0 + """ + + return self._cname + + @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.cname) + + return self._features_cached + + @property + def feature_set(self) -> str: + """ + Returns the feature set for the CName parsed. + + :return: (str) Feature set of the CName + :since: 0.7.0 + """ + + if self._feature_set_cached is not None: + return self._feature_set_cached + + return Parser().filter_as_string(self.cname) + + @property + def feature_set_element(self) -> str: + """ + Returns the feature set of type "element" for the CName parsed. + + :return: (str) Feature set elements + :since: 1.0.0 + """ + + if self._feature_elements_cached is not None: + return ",".join(self._feature_elements_cached) + + return ",".join(self.features["element"]) + + @property + def feature_set_flag(self) -> str: + """ + Returns the feature set of type "flag" for the CName parsed. + + :return: (str) Feature set flags + :since: 1.0.0 + """ + + if self._feature_flags_cached is not None: + return ",".join(self._feature_flags_cached) + + return ",".join(self.features["flag"]) + + @property + def feature_set_platform(self) -> str: + """ + Returns the feature set of type "platform" for the CName parsed. + + :return: (str) Feature set platform + :since: 1.0.0 + """ + + if self._feature_platforms_cached is None: + platforms = self.features["platform"] + else: + platforms = self._feature_platforms_cached + + if self._flag_multiple_platforms: + return ",".join(platforms) + + assert len(platforms) < 2, "Only one platform is supported" + return platforms[0] # type: ignore[no-any-return] + + @property + def feature_set_list(self) -> List[str]: + """ + Returns the feature set for the CName parsed. + + :return: (list) Feature set list of the CName + :since: 0.10.12 + """ + + if self._feature_set_cached is not None: + return self._feature_set_cached.split(",") + + return Parser().filter_as_list(self.cname) + + @property + def platform(self) -> str: + """ + Returns the platform for the CName parsed. + + :return: (str) Platform + :since: 0.7.0 + """ + + 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] + + @property + def platform_variant(self) -> Optional[str]: + """ + Returns the platform variant for the CName parsed. + + :return: (str) Platform variant + :since: 1.0.0 + """ + + if self._platform_variant_cached is not None: + return self._platform_variant_cached + + # @TODO: Platform variant is set by GardenLinux features to the release file. If not read or cached it is currently invisible for this library. + return None + + @platform_variant.setter + def platform_variant(self, variant: str) -> None: + """ + Sets the the platform variant + + :param variant: Platform variant + + :since: 1.0.0 + """ + + self._platform_variant_cached = variant + + def __str__(self) -> str: + """ + Returns the Garden Linux Canonical Name. + + :return: (str) Returns the Garden Linux Canonical Name + :since: 1.0.0 + """ + + return self.cname + + def _copy_from_instance(self, c_name: Self) -> None: + """ + Copies values from a given Garden Linux Canonical Name instance. + + :param cname_object: Garden Linux Canonical Name instance + + :since: 1.0.0 + """ + + self._cname = c_name.cname + self._feature_set_cached = c_name.feature_set + self._feature_elements_cached = c_name.feature_set_element.split(",") + self._feature_flags_cached = c_name.feature_set_flag.split(",") + self._feature_platforms_cached = c_name.feature_set_platform.split(",") + self._platform_cached = c_name.platform + self._platform_variant_cached = c_name.platform_variant diff --git a/src/gardenlinux/features/cname.py b/src/gardenlinux/features/cname.py deleted file mode 100644 index 98c9cc29..00000000 --- a/src/gardenlinux/features/cname.py +++ /dev/null @@ -1,613 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Canonical name (cname) -""" - -import re -from configparser import UNNAMED_SECTION, ConfigParser -from os import PathLike, environ -from pathlib import Path -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, -) -from .parser import Parser - - -class CName(object): - """ - Class to represent a canonical name (cname). - - :author: Garden Linux Maintainers - :copyright: Copyright 2024 SAP SE - :package: gardenlinux - :subpackage: features - :since: 0.7.0 - :license: https://www.apache.org/licenses/LICENSE-2.0 - Apache License, Version 2.0 - """ - - def __init__( - self, - cname: str, - arch: Optional[str] = None, - commit_hash: Optional[str] = None, - version: Optional[str] = None, - ): - """ - Constructor __init__(CName) - - :param cname: Canonical name to represent - :param arch: Architecture if not part of cname - :param commit_hash: Commit ID or hash if not part of cname - :param version: Version if not part of cname - - :since: 0.7.0 - """ - - self._arch = None - self._commit_hash = None - self._commit_id = None - self._feature_elements_cached: Optional[List[str]] = None - 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_MULTIPLE_PLATFORMS", False) - ) - - if self._flag_frankenstein: - self._flag_multiple_platforms = True - - commit_id_or_hash = None - - if version is not None: - # Support version values formatted as - - if commit_hash is None: - re_match = re.match("([a-z0-9.]+)(-([a-z0-9]+))?$", version) - assert re_match, f"Not a valid version {version}" - - commit_id_or_hash = re_match[3] - version = re_match[1] - else: - commit_id_or_hash = commit_hash - - re_object = re.compile( - "([a-zA-Z0-9]+([\\_\\-][a-zA-Z0-9]+)*?)(-([a-z0-9]+)(-([a-z0-9.]+)-([a-z0-9]+))*)?$" - ) - - re_match = re_object.match(cname) - - # Workaround Garden Linux canonical names without mandatory final commit hash - if ( - not re_match - and commit_id_or_hash - and re.match( - "([a-zA-Z0-9]+([\\_\\-][a-zA-Z0-9]+)*?)(-([a-z0-9]+)(-([a-z0-9.]+))*)?$", - cname, - ) - ): - re_match = re_object.match(f"{cname}-{commit_id_or_hash}") - - assert re_match, f"Not a valid Garden Linux canonical name {cname}" - - if re_match.lastindex == 1: - self._flavor = re_match[1] - else: - if commit_id_or_hash is None: - commit_id_or_hash = re_match[7] - elif re_match.group(7) is not None: - assert commit_id_or_hash.startswith(re_match[7]), ( - f"Mismatch between Garden Linux canonical name '{cname}' and given commit ID '{commit_id_or_hash}' detected" - ) - - self._flavor = re_match[1] - self._version = re_match[6] - - if re_match[4] in ARCHS: - self._arch = re_match[4] - else: - self._flavor += re_match[3] - - if self._arch is None and arch is not None: - self._arch = arch - - if version is not None: - if self._version is None: - self._version = version - else: - assert version == self._version, ( - f"Mismatch between Garden Linux canonical name '{cname}' and given version '{version}' detected" - ) - - if commit_id_or_hash is not None: - self._commit_id = commit_id_or_hash[:8] - - if len(commit_id_or_hash) == 40 or commit_id_or_hash == "local": # sha1 hex - self._commit_hash = commit_id_or_hash - - @property - def arch(self) -> Optional[str]: - """ - Returns the architecture for the cname parsed. - - :return: (str) CName architecture - :since: 0.7.0 - """ - - return self._arch - - @property - def cname(self) -> str: - """ - Returns the cname parsed. - - :return: (str) CName - :since: 0.7.0 - """ - - cname = self._flavor - - if self._arch is not None: - cname += f"-{self._arch}" - - if self._commit_id is not None and self._version is not None: - cname += f"-{self.version_and_commit_id}" - - return cname - - @property - def commit_hash(self) -> Optional[str]: - """ - Returns the commit hash if part of the cname parsed. - - :return: (str) Commit hash - :since: 1.0.0 - """ - - return self._commit_hash - - @commit_hash.setter - def commit_hash(self, commit_hash: str) -> None: - """ - Sets the commit hash - - :param commit_hash: Commit hash - - :since: 1.0.0 - """ - - if self._commit_id is not None and not commit_hash.startswith(self._commit_id): - raise RuntimeError("Commit hash given differs from commit ID already set") - - self._commit_id = commit_hash[:8] - self._commit_hash = commit_hash - - @property - def commit_id(self) -> Optional[str]: - """ - Returns the commit ID if part of the cname parsed. - - :return: (str) Commit ID - :since: 0.7.0 - """ - - return self._commit_id - - @property - def flavor(self) -> str: - """ - Returns the flavor for the cname parsed. - - :return: (str) Flavor - :since: 0.7.0 - """ - - 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: - """ - Returns the feature set for the cname parsed. - - :return: (str) Feature set of the cname - :since: 0.7.0 - """ - - if self._feature_set_cached is not None: - return self._feature_set_cached - - return Parser().filter_as_string(self.flavor) - - @property - def feature_set_element(self) -> str: - """ - Returns the feature set of type "element" for the cname parsed. - - :return: (str) Feature set elements - :since: 1.0.0 - """ - - if self._feature_elements_cached is not None: - return ",".join(self._feature_elements_cached) - - return ",".join(self.features["element"]) - - @property - def feature_set_flag(self) -> str: - """ - Returns the feature set of type "flag" for the cname parsed. - - :return: (str) Feature set flags - :since: 1.0.0 - """ - - if self._feature_flags_cached is not None: - return ",".join(self._feature_flags_cached) - - return ",".join(self.features["flag"]) - - @property - def feature_set_platform(self) -> str: - """ - Returns the feature set of type "platform" for the cname parsed. - - :return: (str) Feature set platform - :since: 1.0.0 - """ - - if self._feature_platforms_cached is None: - platforms = self.features["platform"] - else: - platforms = self._feature_platforms_cached - - if self._flag_multiple_platforms: - return ",".join(platforms) - - assert len(platforms) < 2 - "Only one platform is supported" - return platforms[0] # type: ignore[no-any-return] - - @property - def feature_set_list(self) -> List[str]: - """ - Returns the feature set for the cname parsed. - - :return: (list) Feature set list of the cname - :since: 0.10.12 - """ - - if self._feature_set_cached is not None: - return self._feature_set_cached.split(",") - - return Parser().filter_as_list(self.flavor) - - @property - def platform(self) -> str: - """ - Returns the platform for the cname parsed. - - :return: (str) Platform - :since: 0.7.0 - """ - - 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] - - @property - def platform_variant(self) -> Optional[str]: - """ - Returns the platform variant for the cname parsed. - - :return: (str) Platform variant - :since: 1.0.0 - """ - - if self._platform_variant_cached is not None: - return self._platform_variant_cached - - # @TODO: Platform variant is set by GardenLinux features to the release file. If not read or cached it is currently invisible for this library. - return None - - @platform_variant.setter - def platform_variant(self, variant: str) -> None: - """ - Sets the the platform variant - - :param variant: Platform variant - - :since: 1.0.0 - """ - - self._platform_variant_cached = variant - - @property - def release_metadata_string(self) -> str: - """ - Returns the release metadata describing the given CName instance. - - :return: (str) Release metadata describing the given CName instance - :since: 1.0.0 - """ - - commit_hash = self.commit_hash - commit_id = self.commit_id - platform_variant = self.platform_variant - version = self.version - - if commit_id is None: - commit_id = "" - - if commit_hash is None: - commit_hash = commit_id - - if platform_variant is None: - platform_variant = "" - - if version is None: - pretty_name = f"{GL_DISTRIBUTION_NAME} unsupported version" - version = "" - else: - pretty_name = f"{GL_DISTRIBUTION_NAME} {version}" - - metadata = f""" -ID={GL_RELEASE_ID} -ID_LIKE=debian -NAME="{GL_DISTRIBUTION_NAME}" -PRETTY_NAME="{pretty_name}" -IMAGE_VERSION={version} -VARIANT_ID="{self.flavor}-{self.arch}" -HOME_URL="{GL_HOME_URL}" -SUPPORT_URL="{GL_SUPPORT_URL}" -BUG_REPORT_URL="{GL_BUG_REPORT_URL}" -GARDENLINUX_CNAME="{self.cname}" -GARDENLINUX_FEATURES="{self.feature_set}" -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}" -GARDENLINUX_COMMIT_ID_LONG="{commit_hash}" - """.strip() - - return metadata - - @property - def version(self) -> Optional[str]: - """ - Returns the version if part of the cname parsed. - - :return: (str) Version - :since: 0.7.0 - """ - - return self._version - - @property - def version_and_commit_id(self) -> Optional[str]: - """ - Returns the version and commit ID if part of the cname parsed. - - :return: (str) Version and commit ID - :since: 0.7.0 - """ - - if self._version is None or self._commit_id is None: - return None - - return f"{self._version}-{self._commit_id}" - - @property - def version_epoch(self) -> Optional[int]: - """ - Returns the GardenLinux version epoch of the cname parsed. - - :return: (str) GardenLinux version epoch - :since: 1.0.0 - """ - - epoch = None - - if self._version is not None and "." in self._version: - epoch = int(self._version.split(".", 1)[0]) - - return epoch - - def _copy_from_cname_object(self, cname_object: Self) -> None: - """ - Copies values from a given Garden Linux canonical name instance. - - :param cname_object: Garden Linux canonical name instance - - :since: 1.0.0 - """ - - self._arch = cname_object.arch - self._commit_hash = cname_object.commit_hash - self._commit_id = cname_object.commit_id - self._feature_set_cached = cname_object.feature_set - 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 - - def load_from_release_file(self, release_file: PathLike[str] | str) -> None: - """ - Loads and parses a release metadata file. - - :param release_file: Release metadata file - - :since: 1.0.0 - """ - - cname_object = CName.new_from_release_file(release_file) - - if ( - cname_object.flavor != self.flavor - or ( - self._commit_id is not None - and self._commit_id != cname_object.commit_id - ) - or (self._version is not None and self._version != cname_object.version) - or ( - not self._flag_frankenstein - and cname_object.platform not in cname_object.feature_set_platform - ) - ): - raise RuntimeError( - f"Release metadata file given is invalid: {release_file} failed consistency check - {self.cname} != {cname_object.cname}" - ) - - self._copy_from_cname_object(cname_object) - - def save_to_release_file( - self, release_file: PathLike[str] | str, overwrite: Optional[bool] = False - ) -> None: - """ - Saves the release metadata file. - - :param release_file: Release metadata file - - :since: 1.0.0 - """ - - if not isinstance(release_file, PathLike): - release_file = Path(release_file) - - if not overwrite and release_file.exists(): # type: ignore[attr-defined] - raise RuntimeError( - f"Refused to overwrite existing release metadata file: {release_file}" - ) - - with release_file.open("w") as fp: # type: ignore[attr-defined] - fp.write(self.release_metadata_string) - - @staticmethod - def new_from_release_file(release_file: PathLike[str] | str) -> "CName": - """ - Loads and parses a release metadata file. - - :param release_file: Release metadata file - - :since: 0.10.10 - """ - - if not isinstance(release_file, PathLike): - release_file = Path(release_file) - - if not release_file.exists(): # type: ignore[attr-defined] - raise RuntimeError( - f"Release metadata file given is invalid: {release_file}" - ) - - release_config = ConfigParser(allow_unnamed_section=True) - release_config.read(release_file) - - for release_field in ( - "GARDENLINUX_CNAME", - "GARDENLINUX_COMMIT_ID_LONG", - "GARDENLINUX_FEATURES", - "GARDENLINUX_PLATFORM", - "GARDENLINUX_VERSION", - ): - if not release_config.has_option(UNNAMED_SECTION, release_field): - raise RuntimeError( - f"Release metadata file given is invalid: {release_file} misses {release_field}" - ) - - commit_hash = release_config.get( - UNNAMED_SECTION, "GARDENLINUX_COMMIT_ID_LONG" - ).strip("\"'") - - version = release_config.get(UNNAMED_SECTION, "GARDENLINUX_VERSION").strip( - "\"'" - ) - - cname_object = CName( - release_config.get(UNNAMED_SECTION, "GARDENLINUX_CNAME").strip("\"'"), - commit_hash=commit_hash, - version=version, - ) - - cname_object._feature_set_cached = release_config.get( - UNNAMED_SECTION, "GARDENLINUX_FEATURES" - ).strip("\"'") - - 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(",") - ) - - 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(",") - ) - - 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( - UNNAMED_SECTION, "GARDENLINUX_PLATFORM_VARIANT" - ).strip("\"'") - - return cname_object diff --git a/src/gardenlinux/features/cname_main.py b/src/gardenlinux/features/cname_main.py deleted file mode 100644 index 5ce5998c..00000000 --- a/src/gardenlinux/features/cname_main.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -gl-cname main entrypoint -""" - -import argparse -import logging -import re -from os.path import basename, dirname - -from .__main__ import ( - get_minimal_feature_set, - get_version_and_commit_id_from_files, -) -from .cname import CName -from .parser import Parser - - -def get_parser() -> argparse.ArgumentParser: - """ - Get the argument parser for gl-cname. - Used for documentation generation. - - :return: ArgumentParser instance - :since: 1.0.0 - """ - - parser = argparse.ArgumentParser( - prog="gl-cname", - description="Generate a canonical name (cname) from feature sets.", - ) - - parser.add_argument( - "--arch", - dest="arch", - help="Target architecture (e.g., amd64, arm64). If not specified, will be determined from the cname or feature set.", - ) - - parser.add_argument( - "--commit", - dest="commit", - help="Git commit hash. If not specified, will be read from COMMIT file in the GardenLinux root directory.", - ) - - parser.add_argument( - "--feature-dir", - default="features", - help="Path to the features directory (default: 'features').", - ) - - parser.add_argument( - "--version", - dest="version", - help="Version string. If not specified, will be read from VERSION file in the GardenLinux root directory.", - ) - - parser.add_argument( - "cname", - help="Canonical name (cname) to process. Must be a valid GardenLinux canonical name format.", - ) - - return parser - - -def main() -> None: - """ - gl-cname main() - - :since: 0.7.0 - """ - - parser = get_parser() - args = parser.parse_args() - - re_match = re.match( - "([a-zA-Z0-9]+([\\_\\-][a-zA-Z0-9]+)*?)(-([a-z0-9]+)(-([a-z0-9.]+)-([a-z0-9]+))*)?$", - args.cname, - ) - - assert re_match, f"Not a valid Garden Linux canonical name {args.cname}" - - arch = args.arch - commit_id_or_hash = args.commit - gardenlinux_root = dirname(args.feature_dir) - version = args.version - - if gardenlinux_root == "": - gardenlinux_root = "." - - if not version: - try: - version, commit_id_or_hash = get_version_and_commit_id_from_files( - gardenlinux_root - ) - except RuntimeError as exc: - logging.warning( - "Failed to parse version information for GL root '{0}': {1}".format( - gardenlinux_root, exc - ) - ) - - cname = CName(args.cname, arch=arch, commit_hash=commit_id_or_hash, version=version) - - assert cname.arch, "Architecture could not be determined" - - feature_dir_name = basename(args.feature_dir) - - if gardenlinux_root == "": - gardenlinux_root = "." - - graph = Parser(gardenlinux_root, feature_dir_name).filter(cname.flavor) - - sorted_features = Parser.sort_graph_nodes(graph) - minimal_feature_set = get_minimal_feature_set(graph) - - sorted_minimal_features = Parser.subset(minimal_feature_set, sorted_features) - - generated_cname = Parser.get_flavor_from_feature_set(sorted_minimal_features) - - generated_cname += f"-{cname.arch}" - - if cname.version_and_commit_id is not None: - generated_cname += f"-{cname.version_and_commit_id}" - - print(generated_cname) - - -if __name__ == "__main__": - main() diff --git a/src/gardenlinux/features/flavor.py b/src/gardenlinux/features/flavor.py new file mode 100644 index 00000000..9c9f7ce7 --- /dev/null +++ b/src/gardenlinux/features/flavor.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- + +""" +Garden Linux flavor +""" + +from typing import Self + +from .c_name import CName + + +class Flavor(CName): + """ + Class to represent a Garden Linux flavor. + + :author: Garden Linux Maintainers + :copyright: Copyright 2026 SAP SE + :package: gardenlinux + :subpackage: features + :since: 1.0.0 + :license: https://www.apache.org/licenses/LICENSE-2.0 + Apache License, Version 2.0 + """ + + def __init__( + self, + cname: str, + arch: str, + ): + """ + Constructor __init__(Flavor) + + :param cname: CName to represent + :param arch: Architecture if not part of cname + + :since: 1.0.0 + """ + + CName.__init__(self, cname) + self._arch = arch + + @property + def arch(self) -> str: + """ + Returns the architecture for the CName parsed. + + :return: (str) CName architecture + :since: 0.7.0 + """ + + return self._arch + + @property + def flavor(self) -> str: + """ + Returns the Garden Linux flavor parsed. + + :return: (str) Flavor + :since: 0.7.0 + """ + + flavor = CName.__str__(self) + flavor += f"-{self.arch}" + + return flavor + + def __str__(self) -> str: + """ + Returns the Garden Linux flavor. + + :return: (str) Flavor + :since: 1.0.0 + """ + + return self.flavor + + def _copy_from_instance(self, flavor: Self) -> None: + """ + Copies values from a given Garden Linux flavor instance. + + :param cname_object: Flavor instance + + :since: 1.0.0 + """ + + CName._copy_from_instance(self, flavor) + + self._arch = flavor.arch diff --git a/src/gardenlinux/features/metadata_main.py b/src/gardenlinux/features/metadata_main.py index 9bb2c1d7..99825b89 100644 --- a/src/gardenlinux/features/metadata_main.py +++ b/src/gardenlinux/features/metadata_main.py @@ -6,8 +6,9 @@ """ import argparse +from os import environ -from .cname import CName +from .artifact_base_name import ArtifactBaseName _ARGS_ACTION_ALLOWED = [ "output-release-metadata", @@ -15,21 +16,74 @@ ] -def main() -> None: +def get_parser() -> argparse.ArgumentParser: """ - gl-metadata main() + Get the argument parser for gl-metadata. + Used for documentation generation. - :since: 0.7.0 + :return: ArgumentParser instance + :since: 1.0.0 """ - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser( + prog="gl-metadata", + description="Handle GardenLinux metadata reading and writing.", + ) + + parser.add_argument( + "--arch", + dest="arch", + help="Target architecture (e.g., amd64, arm64). Overrides architecture from CName.", + ) + + parser.add_argument( + "--artifact-base-name", + dest="artifact_base_name", + help="Artifact base name to parse. Must be a valid Garden Linux Artifact Base Name.", + ) + + parser.add_argument( + "--cname", + dest="cname", + help="Canonical Name (CName) to parse.", + ) + + parser.add_argument( + "--commit", + dest="commit", + help="Git commit hash. If not specified, will be read from COMMIT file or release file.", + ) + + parser.add_argument( + "--flavor", + dest="flavor", + help="GardenLinux flavor name.", + ) + + parser.add_argument( + "--release-file", + dest="release_file", + help="Path to a release file containing features metadata.", + ) + + parser.add_argument( + "--overwrite-file", + type=bool, + dest="overwrite_file", + help="Accept overwriting existing files.", + ) - parser.add_argument("--arch", dest="arch") - parser.add_argument("--cname", required=True, dest="cname") - parser.add_argument("--commit-hash", dest="commit_hash") - parser.add_argument("--release-file", dest="release_file") - parser.add_argument("--overwrite-file", type=bool, dest="overwrite_file") - parser.add_argument("--version", dest="version") + parser.add_argument( + "--version", + dest="version", + help="Version string. If not specified, will be read from VERSION file or release file.", + ) + + parser.add_argument( + "--versioned-flavor", + dest="versioned_flavor", + help="GardenLinux versioned flavor name.", + ) parser.add_argument( "action", @@ -38,22 +92,48 @@ def main() -> None: default="output-release-metadata", ) + return parser + + +def main() -> None: + """ + gl-metadata main() + + :since: 0.7.0 + """ + + parser = get_parser() args = parser.parse_args() - cname = CName( - args.cname, arch=args.arch, commit_hash=args.commit_hash, version=args.version + commit_id_or_hash = args.commit + gardenlinux_root = environ.get("GL_ROOT_DIR") + version = args.version + + if not gardenlinux_root: + gardenlinux_root = "." + + if not version or not commit_id_or_hash: + version, commit_id_or_hash = ( + ArtifactBaseName.get_version_and_commit_id_from_files(gardenlinux_root) + ) + + abn_object = ArtifactBaseName.new_instance( + args.artifact_base_name, + cname=args.cname, + flavor=args.flavor, + versioned_flavor=args.versioned_flavor, + arch=args.arch, + version=args.version, + commit_id_or_hash=args.commit, ) - if args.commit_hash is not None: - cname.commit_hash = args.commit_hash - if args.action == "write": - cname.save_to_release_file(args.release_file, args.overwrite_file) + abn_object.save_to_release_file(args.release_file, args.overwrite_file) else: if args.release_file is not None: - cname.load_from_release_file(args.release_file) + abn_object.load_from_release_file(args.release_file) - print(cname.release_metadata_string) + print(abn_object.release_metadata_string) if __name__ == "__main__": diff --git a/src/gardenlinux/features/parser.py b/src/gardenlinux/features/parser.py index 497e66fb..8e239e1b 100644 --- a/src/gardenlinux/features/parser.py +++ b/src/gardenlinux/features/parser.py @@ -14,7 +14,7 @@ import networkx import yaml -from ..constants import BARE_FLAVOR_FEATURE_CONTENT, BARE_FLAVOR_LIBC_FEATURE_CONTENT +from ..constants import BARE_CNAME_FEATURE_CONTENT, BARE_CNAME_LIBC_FEATURE_CONTENT from ..logger import LoggerSetup @@ -132,7 +132,7 @@ def filter( :since: 0.7.0 """ - feature_set = Parser.get_flavor_as_feature_set(cname) + feature_set = Parser.get_cname_as_feature_set(cname) return self.filter_based_on_feature_set( feature_set, ignore_excludes, additional_filter_func @@ -278,9 +278,9 @@ def filter_based_on_feature_set( # @TODO: Remove "special" handling once "bare" is a first-class citizen of the feature graph if "bare" in feature_set: if not self.graph.has_node("bare"): - self.graph.add_node("bare", content=BARE_FLAVOR_FEATURE_CONTENT) + self.graph.add_node("bare", content=BARE_CNAME_FEATURE_CONTENT) if not self.graph.has_node("libc"): - self.graph.add_node("libc", content=BARE_FLAVOR_LIBC_FEATURE_CONTENT) + self.graph.add_node("libc", content=BARE_CNAME_LIBC_FEATURE_CONTENT) for feature in feature_set: for node in networkx.descendants( @@ -363,22 +363,36 @@ def _read_feature_yaml(self, feature_yaml_file: str) -> Dict[str, Any]: return {"name": name, "content": content} @staticmethod - def get_flavor_from_feature_set(sorted_features: List[str]) -> str: + def get_cname_from_feature_set(feature_set_list: List[str]) -> str: """ Get the base cname for the feature set given. - :param sorted_features: Sorted feature set + :param feature_set_list: Feature set list - :return: (str) Base cname - :since: 0.7.0 + :return: (str) Garden Linux canonical name + :since: 1.0.0 """ + if not feature_set_list: + raise ValueError("At least one platform must be given as a feature") + + platform = feature_set_list.pop(0) + features = [] + flags = [] + + for feature in feature_set_list: + if feature[:1] == "_": + flags.append(feature) + else: + features.append(feature) + return reduce( - lambda a, b: a + ("-" if not b.startswith("_") else "") + b, sorted_features + lambda a, b: a + ("-" if not b.startswith("_") else "") + b, + [platform] + sorted(features) + sorted(flags), ) @staticmethod - def get_flavor_as_feature_set(cname: str) -> List[str]: + def get_cname_as_feature_set(cname: str) -> List[str]: """ Returns the features of a given canonical name. @@ -478,6 +492,19 @@ def _get_graph_node_type(node: str) -> str: return node.get("content", {}).get("type") # type: ignore[attr-defined, no-any-return] + @staticmethod + def get_minimal_feature_set(graph: networkx.Graph) -> Set[str]: + """ + Returns the minimal set of features described by the given graph. + + :param graph: networkx.Graph + + :return: (set) Minimal set of features + :since: 1.0.0 + """ + + return set([node for (node, degree) in graph.in_degree() if degree == 0]) + @staticmethod def set_default_gardenlinux_root_dir(root_dir: str) -> None: """ diff --git a/src/gardenlinux/features/reproducibility/diff_parser.py b/src/gardenlinux/features/reproducibility/diff_parser.py index 319622d0..924fb3b1 100644 --- a/src/gardenlinux/features/reproducibility/diff_parser.py +++ b/src/gardenlinux/features/reproducibility/diff_parser.py @@ -59,9 +59,9 @@ def __init__( self.all_flavors: set[str] = set() self.reproducible_flavors: set[str] = set() self.passed_by_whitelist: set[str] = set() - self.expected_falvors: set[str] = set() + self.expected_flavors: set[str] = set() self.missing_flavors: set[str] = set() - self.unexpected_falvors: set[str] = set() + self.unexpected_flavors: set[str] = set() def sort_features(self, graph: nx.DiGraph) -> list[str]: """ @@ -97,17 +97,17 @@ def parse( diff_dir = Path(self._gardenlinux_root).joinpath(diff_dir) - self.expected_falvors = { - f"{variant['flavor']}-{variant['arch']}" + self.expected_flavors = { + f"{variant['cname']}-{variant['arch']}" for variant in (flavors_matrix["include"] + bare_flavors_matrix["include"]) } - for flavor in os.listdir(diff_dir): - if flavor.endswith(self._SUFFIX): - with open(diff_dir.joinpath(flavor), "r") as f: + for artifact in os.listdir(diff_dir): + if artifact.endswith(self._SUFFIX): + with open(diff_dir.joinpath(artifact), "r") as f: content = f.read() - flavor = flavor.rstrip(self._SUFFIX) + flavor = artifact.rstrip(self._SUFFIX) self.all_flavors.add(flavor) if content == "": self.reproducible_flavors.add(flavor) @@ -117,8 +117,8 @@ def parse( else: non_reproducible_flavors[flavor] = content.split("\n")[:-1] - self.missing_flavors = self.expected_falvors - self.all_flavors - self.unexpected_falvors = self.all_flavors - self.expected_falvors + self.missing_flavors = self.expected_flavors - self.all_flavors + self.unexpected_flavors = self.all_flavors - self.expected_flavors # Map files to flavors affected_flavors: Dict[str, set[str]] = {} # {file: {flavors...}} diff --git a/src/gardenlinux/features/reproducibility/markdown_formatter.py b/src/gardenlinux/features/reproducibility/markdown_formatter.py index 86e9f546..a0040288 100644 --- a/src/gardenlinux/features/reproducibility/markdown_formatter.py +++ b/src/gardenlinux/features/reproducibility/markdown_formatter.py @@ -190,19 +190,19 @@ def _header( 100 * ( len(self._diff_parser.reproducible_flavors) - / len(self._diff_parser.expected_falvors) + / len(self._diff_parser.expected_flavors) ), 1, ) emoji = ( "✅" - if len(self._diff_parser.expected_falvors) + if len(self._diff_parser.expected_flavors) == len(self._diff_parser.reproducible_flavors) else ("⚠️" if successrate >= SUCCESS_TRESHOLD else "❌") ) - total_count = len(self._diff_parser.expected_falvors) + total_count = len(self._diff_parser.expected_flavors) problem_count = ( "" @@ -226,18 +226,18 @@ def _header( + "" ) - if len(self._diff_parser.unexpected_falvors) > 0: + if len(self._diff_parser.unexpected_flavors) > 0: # This should never happen, but print a warning if it somehow does explanation += ( "\n\n
⁉️ These flavors were not expected to appear in the results, please check for errors in the workflow\
"
-                + "
".join(sorted(self._diff_parser.unexpected_falvors)) + + "
".join(sorted(self._diff_parser.unexpected_flavors)) + "
" ) explanation += ( "" - if len(self._diff_parser.expected_falvors) + if len(self._diff_parser.expected_flavors) <= len(self._diff_parser.reproducible_flavors) else "\n\n*The mentioned features are included in every affected flavor and not included in every unaffected flavor.*" ) @@ -270,7 +270,7 @@ def _table( if len(self._diff_parser.missing_flavors) > 0: row = "|❌ Workflow run did not produce any results|" - row += f"**{round(100 * (len(self._diff_parser.missing_flavors) / len(self._diff_parser.expected_falvors)), 1)}%** affected
" + row += f"**{round(100 * (len(self._diff_parser.missing_flavors) / len(self._diff_parser.expected_flavors)), 1)}%** affected
" row += self._dropdown(self._diff_parser.missing_flavors) row += "|No analysis available|\n" rows += row @@ -285,7 +285,7 @@ def sorting_function(files: frozenset[str]) -> tuple[int, str]: row = "|" row += self._dropdown(files) row += "|" - row += f"**{round(100 * (len(flavors) / len(self._diff_parser.expected_falvors)), 1)}%** affected
" + row += f"**{round(100 * (len(flavors) / len(self._diff_parser.expected_flavors)), 1)}%** affected
" row += self._dropdown(flavors) row += "|" if len(tree) == 0: @@ -300,7 +300,7 @@ def sorting_function(files: frozenset[str]) -> tuple[int, str]: row = "|" row += "✅ No problems found" row += "|" - row += f"**{round(100 * (len(self._diff_parser.reproducible_flavors) / len(self._diff_parser.expected_falvors)), 1)}%**
" + row += f"**{round(100 * (len(self._diff_parser.reproducible_flavors) / len(self._diff_parser.expected_flavors)), 1)}%**
" row += self._dropdown(self._diff_parser.reproducible_flavors) row += "|" row += "-" @@ -308,7 +308,7 @@ def sorting_function(files: frozenset[str]) -> tuple[int, str]: rows += row if len(self._diff_parser.reproducible_flavors) < len( - self._diff_parser.expected_falvors + self._diff_parser.expected_flavors ): rows += "\n*To add affected files to the whitelist, edit `src/gardenlinux/features/reproducibility/nightly_whitelist.json` in python-gardenlinux-lib*\n" diff --git a/src/gardenlinux/features/versioned_flavor.py b/src/gardenlinux/features/versioned_flavor.py new file mode 100644 index 00000000..e026a8ea --- /dev/null +++ b/src/gardenlinux/features/versioned_flavor.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +""" +Garden Linux versioned flavor +""" + +from typing import Optional, Self + +from .flavor import Flavor + + +class VersionedFlavor(Flavor): + """ + Class to represent a Garden Linux versioned flavor. + + :author: Garden Linux Maintainers + :copyright: Copyright 2026 SAP SE + :package: gardenlinux + :subpackage: features + :since: 1.0.0 + :license: https://www.apache.org/licenses/LICENSE-2.0 + Apache License, Version 2.0 + """ + + def __init__( + self, + flavor: str, + arch: str, + version: str, + ): + """ + Constructor __init__(VersionedFlavor) + + :param cname: CName to represent + :param arch: Architecture + :param commit_hash: Commit ID or hash + :param version: Version + + :since: 1.0.0 + """ + + Flavor.__init__(self, flavor, arch) + + self._version = version + + @property + def version(self) -> str: + """ + Returns the version. + + :return: (str) Version + :since: 0.7.0 + """ + + return self._version + + @property + def version_epoch(self) -> Optional[int]: + """ + Returns the Garden Linux version epoch. + + :return: (str) Garden Linux version epoch + :since: 1.0.0 + """ + + epoch = None + + if self._version is not None and "." in self._version: + epoch = int(self._version.split(".", 1)[0]) + + return epoch + + def __str__(self) -> str: + """ + Returns the Garden Linux versioned flavor. + + :return: (str) Returns the Garden Linux versioned flavor + :since: 1.0.0 + """ + + flavor = Flavor.__str__(self) + flavor += f"-{self.version}" + + return flavor + + def _copy_from_instance(self, versioned_flavor: Self) -> None: + """ + Copies values from a given Garden Linux versioned flavor instance. + + :param cname_object: Garden Linux versioned flavor instance + + :since: 1.0.0 + """ + + Flavor._copy_from_instance(self, versioned_flavor) + + self._version = versioned_flavor.version diff --git a/src/gardenlinux/flavors/parser.py b/src/gardenlinux/flavors/parser.py index 08d6e3e6..48224b93 100644 --- a/src/gardenlinux/flavors/parser.py +++ b/src/gardenlinux/flavors/parser.py @@ -12,6 +12,7 @@ from jsonschema import validate as jsonschema_validate from ..constants import GL_FLAVORS_SCHEMA +from ..features import Parser as FeaturesParser from ..logger import LoggerSetup @@ -111,13 +112,10 @@ def filter( continue # Process features - formatted_features = f"-{'-'.join(features)}" if features else "" + cname = FeaturesParser.get_cname_from_feature_set([name] + features) # Construct the combination - combination = f"{name}-{formatted_features}-{arch}" - - # Format the combination to clean up "--" and "-_" - combination = combination.replace("--", "-").replace("-_", "_") + combination = f"{cname}-{arch}" # Exclude combinations explicitly if Parser.should_exclude(combination, [], wildcard_excludes): diff --git a/src/gardenlinux/github/release/release_images_metadata.py b/src/gardenlinux/github/release/release_images_metadata.py index e1446374..569a04dd 100644 --- a/src/gardenlinux/github/release/release_images_metadata.py +++ b/src/gardenlinux/github/release/release_images_metadata.py @@ -18,7 +18,7 @@ from ...apt import DebsrcFile from ...constants import GL_DEB_REPO_BASE_URL, GLVD_BASE_URL, REQUESTS_TIMEOUTS -from ...features import CName +from ...features import ArtifactBaseName from ...flavors import Parser from ...git import Repository from ...logger import LoggerSetup @@ -153,31 +153,28 @@ def grouped_flavors_metadata( f"{flavor=} version={self._version} commitish={self._commitish}" ) - cname = CName( - flavor[1], - arch=flavor[0], - commit_hash=self._commitish, - version=self._version, + abn_object = ArtifactBaseName( + f"{flavor[1]}-{self._version}-{self._commitish}" ) try: release_object = list( s3_artifacts.bucket.objects.filter( - Prefix=f"meta/singles/{cname.cname}" + Prefix=f"meta/singles/{abn_object}" ) )[0] s3_artifacts.bucket.download_file( release_object.key, - str(Path(tmpdir, f"{cname.cname}.s3_metadata.yaml")), + str(Path(tmpdir, f"{abn_object}.s3_metadata.yaml")), ) except IndexError: self._logger.warning( - f"No artifacts found for flavor {cname.cname}, skipping..." + f"No artifacts found for flavor {abn_object}, skipping..." ) continue - with Path(tmpdir, f"{cname.cname}.s3_metadata.yaml").open("r") as file: + with Path(tmpdir, f"{abn_object}.s3_metadata.yaml").open("r") as file: s3_data = ReleaseImagesMetadata.parse_s3_metadata( yaml.load(file, Loader=yaml.SafeLoader) ) diff --git a/src/gardenlinux/oci/container.py b/src/gardenlinux/oci/container.py index f01195d7..ff090f48 100644 --- a/src/gardenlinux/oci/container.py +++ b/src/gardenlinux/oci/container.py @@ -22,7 +22,6 @@ from requests import HTTPError, Response from ..constants import OCI_IMAGE_INDEX_MEDIA_TYPE -from ..features.cname import CName from ..logger import LoggerSetup from .image_manifest import ImageManifest from .index import Index @@ -141,31 +140,32 @@ def generate_image_manifest( :since: 0.10.0 """ - cname_object = CName(cname, architecture, version) - - if architecture is None: - architecture = cname_object.arch - if version is None: - version = cname_object.version - if commit is None: - commit = cname_object.commit_id - if feature_set is None: - feature_set = cname_object.feature_set - - if commit is None: + if not commit: commit = "" manifest = ImageManifest() - manifest.version = version # type: ignore[assignment] manifest.cname = cname - manifest.arch = architecture # type: ignore[assignment] - manifest.feature_set = feature_set manifest.commit = commit + if architecture: + manifest.arch = architecture + else: + architecture = "" + + if feature_set: + manifest.feature_set = feature_set + else: + feature_set = "" + + if version: + manifest.version = version + else: + version = "" + description = ( - f"Image: {cname} " - f"Flavor: {cname_object.flavor} " + f"Image: {cname}-{architecture}-{version}-{commit}" + f"Flavor: {cname}-{architecture} " f"Architecture: {architecture} " f"Features: {feature_set} " f"Commit: {commit} " @@ -594,8 +594,8 @@ def read_manifest( else: manifest_type = ImageManifest - if architecture is None: - architecture = CName(cname, architecture, version).arch + if not architecture: + architecture = "" response = self._get_manifest_without_response_parsing( f"{self._container_version}-{cname}-{architecture}" diff --git a/src/gardenlinux/oci/image_manifest.py b/src/gardenlinux/oci/image_manifest.py index 324c16c8..ded6608a 100644 --- a/src/gardenlinux/oci/image_manifest.py +++ b/src/gardenlinux/oci/image_manifest.py @@ -13,7 +13,7 @@ from oras.defaults import annotation_title as ANNOTATION_TITLE from ..constants import GL_DISTRIBUTION_NAME, GL_REPOSITORY_URL -from ..features import CName +from ..features import Flavor from .layer import Layer from .manifest import Manifest from .platform import new_platform @@ -154,7 +154,26 @@ def flavor(self) -> str: :since: 0.7.0 """ - return CName(self.cname).flavor + return Flavor(self.cname, self.arch).flavor + + @flavor.setter + def flavor(self, value: str) -> None: + """ + Sets the GardenLinux flavor of the OCI image manifest. + + :param value: OCI image GardenLinux flavor + + :since: 1.0.0 + """ + + self._ensure_annotations_dict() + + if "-" not in value: + raise RuntimeError(f"GardenLinux flavor is invalid: {value}") + + cname, arch = value.rsplit("-", 1) + self["annotations"][ImageManifest.ANNOTATION_ARCH_KEY] = arch + self["annotations"][ImageManifest.ANNOTATION_CNAME_KEY] = cname @property def extended_dict(self) -> Dict[str, Any]: diff --git a/src/gardenlinux/s3/__main__.py b/src/gardenlinux/s3/__main__.py index f794631c..a49fdf3d 100644 --- a/src/gardenlinux/s3/__main__.py +++ b/src/gardenlinux/s3/__main__.py @@ -43,16 +43,16 @@ def get_parser() -> argparse.ArgumentParser: download_parser = subparsers.add_parser("download-artifacts-from-bucket") download_parser.add_argument( - "--cname", + "--artifact-base-name", required=False, - dest="cname", - help="Canonical name (cname) used as the S3 key prefix for artifacts.", + dest="artifact_base_name", + help="Artifact base name used as the S3 key prefix for artifacts.", ) upload_parser = subparsers.add_parser("upload-artifacts-to-bucket") upload_parser.add_argument( - "--artifact-name", dest="artifact_name", help="S3 artifact base name." + "--artifact-base-name", dest="artifact_base_name", help="S3 artifact base name." ) return parser @@ -69,8 +69,10 @@ def main() -> None: args = parser.parse_args() if args.action == "download-artifacts-from-bucket": - S3Artifacts(args.bucket).download_to_directory(args.cname, args.path) + S3Artifacts(args.bucket).download_to_directory( + args.artifact_base_name, args.path + ) elif args.action == "upload-artifacts-to-bucket": S3Artifacts(args.bucket).upload_from_directory( - args.artifact_name, args.path, dry_run=args.dry_run + args.artifact_base_name, args.path, dry_run=args.dry_run ) diff --git a/src/gardenlinux/s3/s3_artifacts.py b/src/gardenlinux/s3/s3_artifacts.py index 09fd6a37..064513b6 100644 --- a/src/gardenlinux/s3/s3_artifacts.py +++ b/src/gardenlinux/s3/s3_artifacts.py @@ -18,7 +18,7 @@ import yaml -from ..features import CName +from ..features import ArtifactBaseName from .bucket import Bucket @@ -67,13 +67,13 @@ def bucket(self) -> Bucket: return self._bucket def download_to_directory( - self, cname: str, artifacts_dir: PathLike[str] | str + self, artifact_base_name: str, artifacts_dir: PathLike[str] | str ) -> None: """ Download S3 artifacts to a given directory. - :param cname: Canonical name of the GardenLinux S3 artifacts - :param artifacts_dir: Path for the image artifacts + :param artifact_base_name: Artifact base name of the Garden Linux S3 artifacts + :param artifacts_dir: Path for the image artifacts :since: 0.8.0 """ @@ -84,14 +84,17 @@ def download_to_directory( raise RuntimeError(f"Artifacts directory given is invalid: {artifacts_dir}") release_object = list( - self._bucket.objects.filter(Prefix=f"meta/singles/{cname}") + self._bucket.objects.filter(Prefix=f"meta/singles/{artifact_base_name}") )[0] self._bucket.download_file( - release_object.key, str(artifacts_dir.joinpath(f"{cname}.s3_metadata.yaml")) + release_object.key, + str(artifacts_dir.joinpath(f"{artifact_base_name}.s3_metadata.yaml")), ) - for s3_object in self._bucket.objects.filter(Prefix=f"objects/{cname}").all(): + for s3_object in self._bucket.objects.filter( + Prefix=f"objects/{artifact_base_name}" + ).all(): self._bucket.download_file( s3_object.key, str(artifacts_dir.joinpath(basename(s3_object.key))) ) @@ -121,35 +124,44 @@ def upload_from_directory( release_file = artifacts_dir.joinpath(f"{base_name}.release") try: - cname_object = CName.new_from_release_file(release_file) + abn_object = ArtifactBaseName.new_from_release_file(release_file) except RuntimeError: if not release_file.exists(): raise RuntimeError( f"Release metadata file given is invalid: {release_file}" ) + base_abn_object = ArtifactBaseName(base_name) + 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("\"'"), - ) + artifact_base_name = release_config.get( + UNNAMED_SECTION, "GARDENLINUX_CNAME" + ).strip("\"'") + + artifact_base_name += "-" + base_abn_object.arch + + artifact_base_name += "-" + release_config.get( + UNNAMED_SECTION, "GARDENLINUX_VERSION" + ).strip("\"'") - if cname_object.version_and_commit_id is None: + artifact_base_name += "-" + release_config.get( + UNNAMED_SECTION, "GARDENLINUX_COMMIT_ID_LONG" + ).strip("\"'") + + abn_object = ArtifactBaseName(artifact_base_name) + + if abn_object.version_and_commit_id is None: raise RuntimeError( "Version information could not be determined from release file" ) - arch = cname_object.arch - feature_set_list = cname_object.feature_set_list + arch = abn_object.arch + feature_set_list = abn_object.feature_set_list release_timestamp = stat(release_file).st_ctime requirements_file = artifacts_dir.joinpath(f"{base_name}.requirements") + publishing_group = "" require_uefi = None secureboot = None @@ -196,13 +208,13 @@ def upload_from_directory( re_object = re.compile("[^a-zA-Z0-9\\s+\\-=.\\_:/@]") arch = re_object.sub("+", arch) - commit_id_or_hash = cname_object.commit_hash + commit_id_or_hash = abn_object.commit_hash if commit_id_or_hash is None: - commit_id_or_hash = cname_object.commit_id + commit_id_or_hash = abn_object.commit_id metadata = { - "platform": cname_object.platform, + "platform": abn_object.platform, "architecture": arch, "build_committish": commit_id_or_hash, "build_timestamp": datetime.fromtimestamp(release_timestamp), @@ -214,14 +226,14 @@ def upload_from_directory( "s3_bucket": self._bucket.name, "s3_key": f"meta/singles/{base_name}", "test_result": None, - "version": cname_object.version, + "version": abn_object.version, "paths": [], } - if cname_object.version_epoch is not None: - metadata["gardenlinux_epoch"] = cname_object.version_epoch + if abn_object.version_epoch is not None: + metadata["gardenlinux_epoch"] = abn_object.version_epoch - platform_variant = cname_object.platform_variant + platform_variant = abn_object.platform_variant if platform_variant is not None: metadata["platform_variant"] = platform_variant @@ -253,8 +265,8 @@ def upload_from_directory( s3_tags = { "architecture": arch, - "platform": re_object.sub("+", cname_object.platform), - "version": re_object.sub("+", cname_object.version), # type: ignore[arg-type] + "platform": re_object.sub("+", abn_object.platform), + "version": re_object.sub("+", abn_object.version), "committish": commit_id_or_hash, "md5sum": md5sum, "sha256sum": sha256sum, diff --git a/test-data/gardenlinux b/test-data/gardenlinux index 8c14e750..c15e9789 160000 --- a/test-data/gardenlinux +++ b/test-data/gardenlinux @@ -1 +1 @@ -Subproject commit 8c14e75011397cd6d16c1182f3377678c885b87a +Subproject commit c15e9789b16b0e64019b52b45fb6fa3b3c0f608c diff --git a/test-data/reproducibility/diff_files/compare_nightly.md b/test-data/reproducibility/diff_files/compare_nightly.md index 1f41fd03..a5843bda 100644 --- a/test-data/reproducibility/diff_files/compare_nightly.md +++ b/test-data/reproducibility/diff_files/compare_nightly.md @@ -13,7 +13,7 @@ Comparison of nightly **[#2405](https://github.com/gardenlinux/gardenlinux/actio | Affected Files | Flavors | Features Causing the Problem | |----------------|---------|------------------------------| -|`/etc/hostname`|**4.9%** affected
`container-amd64`
`container-arm64`
`container-pythonDev-amd64`
`container-pythonDev-arm64`|
container
| +|`/etc/hostname`|**4.9%** affected
`container-amd64`
`container-arm64`
`container-pythonDev-amd64`
`container-pythonDev-arm64`|
container:
_archgrouped
| |✅ No problems found|**95.1%**
ali-gardener_prod-amd64...`ali-gardener_prod-amd64`
`aws-gardener_fips_prod-amd64`
`aws-gardener_fips_prod-arm64`
`aws-gardener_prod-amd64`
`aws-gardener_prod-arm64`
`aws-gardener_prod_tpm2_trustedboot-amd64`
`aws-gardener_prod_tpm2_trustedboot-arm64`
`aws-gardener_prod_trustedboot-amd64`
`aws-gardener_prod_trustedboot-arm64`
`aws-gardener_prod_usi-amd64`
`aws-gardener_prod_usi-arm64`
`azure-gardener_prod-amd64`
`azure-gardener_prod-arm64`
`azure-gardener_prod_tpm2_trustedboot-amd64`
`azure-gardener_prod_tpm2_trustedboot-arm64`
`azure-gardener_prod_trustedboot-amd64`
`azure-gardener_prod_trustedboot-arm64`
`azure-gardener_prod_usi-amd64`
`azure-gardener_prod_usi-arm64`
`bare-libc-amd64`
`bare-libc-arm64`
`bare-nodejs-amd64`
`bare-nodejs-arm64`
`bare-python-amd64`
`bare-python-arm64`
`bare-sapmachine-amd64`
`bare-sapmachine-arm64`
`baremetal-capi-amd64`
`baremetal-capi-arm64`
`baremetal-gardener_prod-amd64`
`baremetal-gardener_prod-arm64`
`baremetal-gardener_prod_tpm2_trustedboot-amd64`
`baremetal-gardener_prod_tpm2_trustedboot-arm64`
`baremetal-gardener_prod_trustedboot-amd64`
`baremetal-gardener_prod_trustedboot-arm64`
`baremetal-gardener_prod_usi-amd64`
`baremetal-gardener_prod_usi-arm64`
`baremetal-gardener_pxe-amd64`
`baremetal-gardener_pxe-arm64`
`baremetal-vhost-amd64`
`baremetal-vhost-arm64`
`baremetal_pxe-amd64`
`baremetal_pxe-arm64`
`gcp-gardener_prod-amd64`
`gcp-gardener_prod-arm64`
`gcp-gardener_prod_tpm2_trustedboot-amd64`
`gcp-gardener_prod_tpm2_trustedboot-arm64`
`gcp-gardener_prod_trustedboot-amd64`
`gcp-gardener_prod_trustedboot-arm64`
`gcp-gardener_prod_usi-amd64`
`gcp-gardener_prod_usi-arm64`
`gdch-gardener_prod-amd64`
`gdch-gardener_prod-arm64`
`kvm-gardener_prod-amd64`
`kvm-gardener_prod-arm64`
`kvm-gardener_prod_tpm2_trustedboot-amd64`
`kvm-gardener_prod_tpm2_trustedboot-arm64`
`kvm-gardener_prod_trustedboot-amd64`
`kvm-gardener_prod_trustedboot-arm64`
`kvm-gardener_prod_usi-amd64`
`kvm-gardener_prod_usi-arm64`
`lima-amd64`
`lima-arm64`
`openstack-gardener_prod-amd64`
`openstack-gardener_prod-arm64`
`openstack-gardener_prod_tpm2_trustedboot-amd64`
`openstack-gardener_prod_tpm2_trustedboot-arm64`
`openstack-gardener_prod_trustedboot-amd64`
`openstack-gardener_prod_trustedboot-arm64`
`openstack-gardener_prod_usi-amd64`
`openstack-gardener_prod_usi-arm64`
`openstack-metal-gardener_prod-amd64`
`openstack-metal-gardener_prod-arm64`
`openstack-metal-gardener_prod_usi-amd64`
`openstack-metal-gardener_prod_usi-arm64`
`vmware-gardener_prod-amd64`
`vmware-gardener_prod-arm64`
|-| *To add affected files to the whitelist, edit `src/gardenlinux/features/reproducibility/nightly_whitelist.json` in python-gardenlinux-lib* diff --git a/test-data/reproducibility/diff_files/compare_nightly_different_commit.md b/test-data/reproducibility/diff_files/compare_nightly_different_commit.md index e6d3a430..16b2a7ba 100644 --- a/test-data/reproducibility/diff_files/compare_nightly_different_commit.md +++ b/test-data/reproducibility/diff_files/compare_nightly_different_commit.md @@ -15,7 +15,7 @@ Comparison of nightly **[#2405](https://github.com/gardenlinux/gardenlinux/actio | Affected Files | Flavors | Features Causing the Problem | |----------------|---------|------------------------------| -|`/etc/hostname`|**4.9%** affected
`container-amd64`
`container-arm64`
`container-pythonDev-amd64`
`container-pythonDev-arm64`|
container
| +|`/etc/hostname`|**4.9%** affected
`container-amd64`
`container-arm64`
`container-pythonDev-amd64`
`container-pythonDev-arm64`|
container:
_archgrouped
| |✅ No problems found|**95.1%**
ali-gardener_prod-amd64...`ali-gardener_prod-amd64`
`aws-gardener_fips_prod-amd64`
`aws-gardener_fips_prod-arm64`
`aws-gardener_prod-amd64`
`aws-gardener_prod-arm64`
`aws-gardener_prod_tpm2_trustedboot-amd64`
`aws-gardener_prod_tpm2_trustedboot-arm64`
`aws-gardener_prod_trustedboot-amd64`
`aws-gardener_prod_trustedboot-arm64`
`aws-gardener_prod_usi-amd64`
`aws-gardener_prod_usi-arm64`
`azure-gardener_prod-amd64`
`azure-gardener_prod-arm64`
`azure-gardener_prod_tpm2_trustedboot-amd64`
`azure-gardener_prod_tpm2_trustedboot-arm64`
`azure-gardener_prod_trustedboot-amd64`
`azure-gardener_prod_trustedboot-arm64`
`azure-gardener_prod_usi-amd64`
`azure-gardener_prod_usi-arm64`
`bare-libc-amd64`
`bare-libc-arm64`
`bare-nodejs-amd64`
`bare-nodejs-arm64`
`bare-python-amd64`
`bare-python-arm64`
`bare-sapmachine-amd64`
`bare-sapmachine-arm64`
`baremetal-capi-amd64`
`baremetal-capi-arm64`
`baremetal-gardener_prod-amd64`
`baremetal-gardener_prod-arm64`
`baremetal-gardener_prod_tpm2_trustedboot-amd64`
`baremetal-gardener_prod_tpm2_trustedboot-arm64`
`baremetal-gardener_prod_trustedboot-amd64`
`baremetal-gardener_prod_trustedboot-arm64`
`baremetal-gardener_prod_usi-amd64`
`baremetal-gardener_prod_usi-arm64`
`baremetal-gardener_pxe-amd64`
`baremetal-gardener_pxe-arm64`
`baremetal-vhost-amd64`
`baremetal-vhost-arm64`
`baremetal_pxe-amd64`
`baremetal_pxe-arm64`
`gcp-gardener_prod-amd64`
`gcp-gardener_prod-arm64`
`gcp-gardener_prod_tpm2_trustedboot-amd64`
`gcp-gardener_prod_tpm2_trustedboot-arm64`
`gcp-gardener_prod_trustedboot-amd64`
`gcp-gardener_prod_trustedboot-arm64`
`gcp-gardener_prod_usi-amd64`
`gcp-gardener_prod_usi-arm64`
`gdch-gardener_prod-amd64`
`gdch-gardener_prod-arm64`
`kvm-gardener_prod-amd64`
`kvm-gardener_prod-arm64`
`kvm-gardener_prod_tpm2_trustedboot-amd64`
`kvm-gardener_prod_tpm2_trustedboot-arm64`
`kvm-gardener_prod_trustedboot-amd64`
`kvm-gardener_prod_trustedboot-arm64`
`kvm-gardener_prod_usi-amd64`
`kvm-gardener_prod_usi-arm64`
`lima-amd64`
`lima-arm64`
`openstack-gardener_prod-amd64`
`openstack-gardener_prod-arm64`
`openstack-gardener_prod_tpm2_trustedboot-amd64`
`openstack-gardener_prod_tpm2_trustedboot-arm64`
`openstack-gardener_prod_trustedboot-amd64`
`openstack-gardener_prod_trustedboot-arm64`
`openstack-gardener_prod_usi-amd64`
`openstack-gardener_prod_usi-arm64`
`openstack-metal-gardener_prod-amd64`
`openstack-metal-gardener_prod-arm64`
`openstack-metal-gardener_prod_usi-amd64`
`openstack-metal-gardener_prod_usi-arm64`
`vmware-gardener_prod-amd64`
`vmware-gardener_prod-arm64`
|-| *To add affected files to the whitelist, edit `src/gardenlinux/features/reproducibility/nightly_whitelist.json` in python-gardenlinux-lib* diff --git a/test-data/reproducibility/diff_files/different_commit.md b/test-data/reproducibility/diff_files/different_commit.md index c23088c5..49a532ae 100644 --- a/test-data/reproducibility/diff_files/different_commit.md +++ b/test-data/reproducibility/diff_files/different_commit.md @@ -13,7 +13,7 @@ Comparison of the latest nightly **[#2365](https://github.com/gardenlinux/garden | Affected Files | Flavors | Features Causing the Problem | |----------------|---------|------------------------------| -|`/etc/hostname`|**4.9%** affected
`container-amd64`
`container-arm64`
`container-pythonDev-amd64`
`container-pythonDev-arm64`|
container
| +|`/etc/hostname`|**4.9%** affected
`container-amd64`
`container-arm64`
`container-pythonDev-amd64`
`container-pythonDev-arm64`|
container:
_archgrouped
| |✅ No problems found|**95.1%**
ali-gardener_prod-amd64...`ali-gardener_prod-amd64`
`aws-gardener_fips_prod-amd64`
`aws-gardener_fips_prod-arm64`
`aws-gardener_prod-amd64`
`aws-gardener_prod-arm64`
`aws-gardener_prod_tpm2_trustedboot-amd64`
`aws-gardener_prod_tpm2_trustedboot-arm64`
`aws-gardener_prod_trustedboot-amd64`
`aws-gardener_prod_trustedboot-arm64`
`aws-gardener_prod_usi-amd64`
`aws-gardener_prod_usi-arm64`
`azure-gardener_prod-amd64`
`azure-gardener_prod-arm64`
`azure-gardener_prod_tpm2_trustedboot-amd64`
`azure-gardener_prod_tpm2_trustedboot-arm64`
`azure-gardener_prod_trustedboot-amd64`
`azure-gardener_prod_trustedboot-arm64`
`azure-gardener_prod_usi-amd64`
`azure-gardener_prod_usi-arm64`
`bare-libc-amd64`
`bare-libc-arm64`
`bare-nodejs-amd64`
`bare-nodejs-arm64`
`bare-python-amd64`
`bare-python-arm64`
`bare-sapmachine-amd64`
`bare-sapmachine-arm64`
`baremetal-capi-amd64`
`baremetal-capi-arm64`
`baremetal-gardener_prod-amd64`
`baremetal-gardener_prod-arm64`
`baremetal-gardener_prod_tpm2_trustedboot-amd64`
`baremetal-gardener_prod_tpm2_trustedboot-arm64`
`baremetal-gardener_prod_trustedboot-amd64`
`baremetal-gardener_prod_trustedboot-arm64`
`baremetal-gardener_prod_usi-amd64`
`baremetal-gardener_prod_usi-arm64`
`baremetal-gardener_pxe-amd64`
`baremetal-gardener_pxe-arm64`
`baremetal-vhost-amd64`
`baremetal-vhost-arm64`
`baremetal_pxe-amd64`
`baremetal_pxe-arm64`
`gcp-gardener_prod-amd64`
`gcp-gardener_prod-arm64`
`gcp-gardener_prod_tpm2_trustedboot-amd64`
`gcp-gardener_prod_tpm2_trustedboot-arm64`
`gcp-gardener_prod_trustedboot-amd64`
`gcp-gardener_prod_trustedboot-arm64`
`gcp-gardener_prod_usi-amd64`
`gcp-gardener_prod_usi-arm64`
`gdch-gardener_prod-amd64`
`gdch-gardener_prod-arm64`
`kvm-gardener_prod-amd64`
`kvm-gardener_prod-arm64`
`kvm-gardener_prod_tpm2_trustedboot-amd64`
`kvm-gardener_prod_tpm2_trustedboot-arm64`
`kvm-gardener_prod_trustedboot-amd64`
`kvm-gardener_prod_trustedboot-arm64`
`kvm-gardener_prod_usi-amd64`
`kvm-gardener_prod_usi-arm64`
`lima-amd64`
`lima-arm64`
`openstack-gardener_prod-amd64`
`openstack-gardener_prod-arm64`
`openstack-gardener_prod_tpm2_trustedboot-amd64`
`openstack-gardener_prod_tpm2_trustedboot-arm64`
`openstack-gardener_prod_trustedboot-amd64`
`openstack-gardener_prod_trustedboot-arm64`
`openstack-gardener_prod_usi-amd64`
`openstack-gardener_prod_usi-arm64`
`openstack-metal-gardener_prod-amd64`
`openstack-metal-gardener_prod-arm64`
`openstack-metal-gardener_prod_usi-amd64`
`openstack-metal-gardener_prod_usi-arm64`
`vmware-gardener_prod-amd64`
`vmware-gardener_prod-arm64`
|-| *To add affected files to the whitelist, edit `src/gardenlinux/features/reproducibility/nightly_whitelist.json` in python-gardenlinux-lib* diff --git a/test-data/reproducibility/diff_files/one_issue.md b/test-data/reproducibility/diff_files/one_issue.md index 5c09d2ba..d5ae281d 100644 --- a/test-data/reproducibility/diff_files/one_issue.md +++ b/test-data/reproducibility/diff_files/one_issue.md @@ -9,7 +9,7 @@ | Affected Files | Flavors | Features Causing the Problem | |----------------|---------|------------------------------| -|`/etc/hostname`|**4.9%** affected
`container-amd64`
`container-arm64`
`container-pythonDev-amd64`
`container-pythonDev-arm64`|
container
| +|`/etc/hostname`|**4.9%** affected
`container-amd64`
`container-arm64`
`container-pythonDev-amd64`
`container-pythonDev-arm64`|
container:
_archgrouped
| |✅ No problems found|**95.1%**
ali-gardener_prod-amd64...`ali-gardener_prod-amd64`
`aws-gardener_fips_prod-amd64`
`aws-gardener_fips_prod-arm64`
`aws-gardener_prod-amd64`
`aws-gardener_prod-arm64`
`aws-gardener_prod_tpm2_trustedboot-amd64`
`aws-gardener_prod_tpm2_trustedboot-arm64`
`aws-gardener_prod_trustedboot-amd64`
`aws-gardener_prod_trustedboot-arm64`
`aws-gardener_prod_usi-amd64`
`aws-gardener_prod_usi-arm64`
`azure-gardener_prod-amd64`
`azure-gardener_prod-arm64`
`azure-gardener_prod_tpm2_trustedboot-amd64`
`azure-gardener_prod_tpm2_trustedboot-arm64`
`azure-gardener_prod_trustedboot-amd64`
`azure-gardener_prod_trustedboot-arm64`
`azure-gardener_prod_usi-amd64`
`azure-gardener_prod_usi-arm64`
`bare-libc-amd64`
`bare-libc-arm64`
`bare-nodejs-amd64`
`bare-nodejs-arm64`
`bare-python-amd64`
`bare-python-arm64`
`bare-sapmachine-amd64`
`bare-sapmachine-arm64`
`baremetal-capi-amd64`
`baremetal-capi-arm64`
`baremetal-gardener_prod-amd64`
`baremetal-gardener_prod-arm64`
`baremetal-gardener_prod_tpm2_trustedboot-amd64`
`baremetal-gardener_prod_tpm2_trustedboot-arm64`
`baremetal-gardener_prod_trustedboot-amd64`
`baremetal-gardener_prod_trustedboot-arm64`
`baremetal-gardener_prod_usi-amd64`
`baremetal-gardener_prod_usi-arm64`
`baremetal-gardener_pxe-amd64`
`baremetal-gardener_pxe-arm64`
`baremetal-vhost-amd64`
`baremetal-vhost-arm64`
`baremetal_pxe-amd64`
`baremetal_pxe-arm64`
`gcp-gardener_prod-amd64`
`gcp-gardener_prod-arm64`
`gcp-gardener_prod_tpm2_trustedboot-amd64`
`gcp-gardener_prod_tpm2_trustedboot-arm64`
`gcp-gardener_prod_trustedboot-amd64`
`gcp-gardener_prod_trustedboot-arm64`
`gcp-gardener_prod_usi-amd64`
`gcp-gardener_prod_usi-arm64`
`gdch-gardener_prod-amd64`
`gdch-gardener_prod-arm64`
`kvm-gardener_prod-amd64`
`kvm-gardener_prod-arm64`
`kvm-gardener_prod_tpm2_trustedboot-amd64`
`kvm-gardener_prod_tpm2_trustedboot-arm64`
`kvm-gardener_prod_trustedboot-amd64`
`kvm-gardener_prod_trustedboot-arm64`
`kvm-gardener_prod_usi-amd64`
`kvm-gardener_prod_usi-arm64`
`lima-amd64`
`lima-arm64`
`openstack-gardener_prod-amd64`
`openstack-gardener_prod-arm64`
`openstack-gardener_prod_tpm2_trustedboot-amd64`
`openstack-gardener_prod_tpm2_trustedboot-arm64`
`openstack-gardener_prod_trustedboot-amd64`
`openstack-gardener_prod_trustedboot-arm64`
`openstack-gardener_prod_usi-amd64`
`openstack-gardener_prod_usi-arm64`
`openstack-metal-gardener_prod-amd64`
`openstack-metal-gardener_prod-arm64`
`openstack-metal-gardener_prod_usi-amd64`
`openstack-metal-gardener_prod_usi-arm64`
`vmware-gardener_prod-amd64`
`vmware-gardener_prod-arm64`
|-| *To add affected files to the whitelist, edit `src/gardenlinux/features/reproducibility/nightly_whitelist.json` in python-gardenlinux-lib* diff --git a/tests/features/constants.py b/tests/features/constants.py index e99ac286..d92df8e9 100644 --- a/tests/features/constants.py +++ b/tests/features/constants.py @@ -7,25 +7,29 @@ ) -def generate_container_amd64_release_metadata(version: str, commit_hash: str) -> str: - return f""" +def generate_container_release_metadata( + version: str, commit_hash: str, arch: str = "amd64", variant: str = "" +) -> str: + metadata = f""" ID={GL_RELEASE_ID} ID_LIKE=debian NAME="{GL_DISTRIBUTION_NAME}" PRETTY_NAME="{GL_DISTRIBUTION_NAME} {version}" IMAGE_VERSION={version} -VARIANT_ID="container-amd64" +VARIANT_ID="container-{arch}" HOME_URL="{GL_HOME_URL}" SUPPORT_URL="{GL_SUPPORT_URL}" BUG_REPORT_URL="{GL_BUG_REPORT_URL}" -GARDENLINUX_CNAME="container-amd64-{version}-{commit_hash}" -GARDENLINUX_FEATURES="_slim,base,container" +GARDENLINUX_CNAME="container" +GARDENLINUX_FEATURES="_archgrouped,_slim,base,container" GARDENLINUX_FEATURES_PLATFORMS="container" GARDENLINUX_FEATURES_ELEMENTS="base" -GARDENLINUX_FEATURES_FLAGS="_slim" +GARDENLINUX_FEATURES_FLAGS="_archgrouped,_slim" GARDENLINUX_PLATFORM="container" -GARDENLINUX_PLATFORM_VARIANT="" +GARDENLINUX_PLATFORM_VARIANT="{variant}" GARDENLINUX_VERSION="{version}" GARDENLINUX_COMMIT_ID="{commit_hash}" GARDENLINUX_COMMIT_ID_LONG="{commit_hash}" -""".strip() +""" + + return metadata.strip() diff --git a/tests/features/test_cname.py b/tests/features/test_cname.py index 3d173844..13b13b95 100644 --- a/tests/features/test_cname.py +++ b/tests/features/test_cname.py @@ -19,12 +19,12 @@ "metal_pxe", ), ( - "container-amd64", + "container", "container", ), ], ) -def test_cname_flavor(input_cname: str, expected_output: str) -> None: +def test_cname(input_cname: str, expected_output: str) -> None: """ Tests if cname returns the dict with expected features. @@ -32,19 +32,6 @@ def test_cname_flavor(input_cname: str, expected_output: str) -> None: features have changed since writing this test. In this case, update the expected output accordingly. You can print the output of cname so you have the dict in the expected format. """ - cname = CName(input_cname) - assert cname.flavor == expected_output - - -def test_cname_commit_id_setter() -> None: - """ - Tests cname setter for `commit_id` to verify a given ID before overwriting. - """ - cname = CName("container", arch="amd64", version="today", commit_hash="local") - - # Act / Assert - with pytest.raises( - RuntimeError, match="Commit hash given differs from commit ID already set" - ): - cname.commit_hash = "broken" + cname = CName(input_cname) + assert str(cname) == expected_output diff --git a/tests/features/test_cname_main.py b/tests/features/test_cname_main.py deleted file mode 100644 index 5a40c5fb..00000000 --- a/tests/features/test_cname_main.py +++ /dev/null @@ -1,159 +0,0 @@ -import logging -import sys -import types -from typing import Any, List, Tuple - -import networkx -import pytest - -import gardenlinux.features.cname_main as cname_main -from gardenlinux.features import Parser - - -def test_main_happy( - monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - """ - Test the "Happy Path" of the main() function. - """ - # Arrange - argv = ["prog", "--arch", "amd64", "--version", "1.0-abc123", "flav-amd64"] - monkeypatch.setattr(sys, "argv", argv) - - class FakeGraph: - def in_degree(self) -> List[Tuple[str, int]]: - return [("f1", 0)] - - edges = [("f1", "f2")] - - class FakeParser(Parser): - def __init__(self, *a: Any, **k: Any): - pass - - def filter(self, *a: Any, **k: Any) -> networkx.Graph: - return FakeGraph() - - @staticmethod - def sort_graph_nodes(graph: networkx.Graph) -> List[str]: - return ["f1", "f2"] - - monkeypatch.setattr(cname_main, "Parser", FakeParser) - - # Act - cname_main.main() - - # Assert - out = capsys.readouterr().out - assert "f1" in out - assert "amd64" in out - - -def test_main_version_from_file( - monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - """ - "Happy Path" test for grabbing the version and commit id from file in main(). - """ - # Arrange - argv = ["prog", "--arch", "amd64", "flav-amd64"] - monkeypatch.setattr(sys, "argv", argv) - - monkeypatch.setattr( - cname_main, - "get_version_and_commit_id_from_files", - lambda root: ("2.0", "abcdef12"), - ) - - class FakeParser(Parser): - def __init__(self, *a: Any, **k: Any): - pass - - def filter(self, *a: Any, **k: Any) -> networkx.Graph: - return types.SimpleNamespace(in_degree=lambda: [("f1", 0)], edges=[]) - - @staticmethod - def sort_graph_nodes(graph: networkx.Graph) -> List[str]: - return ["f1"] - - monkeypatch.setattr(cname_main, "Parser", FakeParser) - - # Act - cname_main.main() - - # Assert - assert "2.0-abcdef12" in capsys.readouterr().out - - -def test_cname_main_version_file_missing_warns( - monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture -) -> None: - """ - Check if a warning is logged when it fails to read version and commit id files. - - Specifically, this test simulates a scenario where the helper function - `get_version_and_commit_id_from_files` raises a RuntimeError, which would occur - if the expected version or commit files are missing or unreadable. - """ - # Arrange - argv = ["prog", "--arch", "amd64", "flav-amd64"] - monkeypatch.setattr(sys, "argv", argv) - - # Patch version fatch function to raise RuntimeError (Simulates missing files) - def raise_runtime(*args: Any, **kwargs: Any) -> None: - raise RuntimeError("missing") - - monkeypatch.setattr( - cname_main, "get_version_and_commit_id_from_files", raise_runtime - ) - - # Patch Parser for minimal valid graph - class FakeParser(Parser): - def __init__(self, *a: Any, **k: Any): - pass - - # Return object with in_degree method returning a node with zero dependencies - def filter(self, *a: Any, **k: Any) -> networkx.Graph: - return types.SimpleNamespace(in_degree=lambda: [("f1", 0)], edges=[]) - - @staticmethod - def sort_graph_nodes(graph: networkx.Graph) -> List[str]: - return ["f1"] - - monkeypatch.setattr(cname_main, "Parser", FakeParser) - - # Capture any logs with WARNING level - caplog.set_level(logging.WARNING) - - # Act - cname_main.main() - - # Assert - assert "Failed to parse version information" in caplog.text - - -def test_cname_main_invalid_cname_raises(monkeypatch: pytest.MonkeyPatch) -> None: - """ - Test if AssertionError is raised with an invalid or malformed cname. - """ - # Arrange - argv = ["prog", "--arch", "amd64", "--version", "1.0", "INVALID@NAME"] - monkeypatch.setattr(sys, "argv", argv) - - # Act / Assert - with pytest.raises(AssertionError): - cname_main.main() - - -def test_cname_main_missing_arch_in_cname_raises( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """ - Test if an assertion error is raised when the arch argument is missing. - """ - # Arrange - argv = ["prog", "--version", "1.0", "flav"] - monkeypatch.setattr(sys, "argv", argv) - - # Act / Assert - with pytest.raises(AssertionError): - cname_main.main() diff --git a/tests/features/test_main.py b/tests/features/test_main.py index a0df4326..cdac5e58 100644 --- a/tests/features/test_main.py +++ b/tests/features/test_main.py @@ -1,16 +1,14 @@ import sys -import types from pathlib import Path -from tempfile import TemporaryDirectory -from typing import Any, List, Tuple +from typing import List, Optional import pytest import gardenlinux.features.__main__ as fema -from gardenlinux.features import CName +from gardenlinux.features import ArtifactBaseName from ..constants import GL_ROOT_DIR -from .constants import generate_container_amd64_release_metadata +from .constants import generate_container_release_metadata # ------------------------------- # Helper function tests @@ -40,26 +38,11 @@ class MockGraph: # Act / Assert with pytest.raises( - RuntimeError, match="Error while generating graph: Flavor is None!" + RuntimeError, match="Error while generating graph: CName is None!" ): fema.graph_as_mermaid_markup(None, MockGraph()) -def test_get_minimal_feature_set_filters() -> None: - # Arrange - class FakeGraph: - def in_degree(self) -> List[Tuple[str, int]]: - return [("a", 0), ("b", 1), ("c", 0)] - - graph = FakeGraph() - - # Act - result = fema.get_minimal_feature_set(graph) - - # Assert - assert result == {"a", "c"} - - def test_get_version_and_commit_from_file(tmp_path: Path) -> None: # Arrange commit_file = tmp_path / "COMMIT" @@ -68,191 +51,463 @@ def test_get_version_and_commit_from_file(tmp_path: Path) -> None: version_file.write_text("1.2.3\n") # Act - version, commit = fema.get_version_and_commit_id_from_files(str(tmp_path)) + version, commit = ArtifactBaseName.get_version_and_commit_id_from_files( + str(tmp_path) + ) # Arrange assert version == "1.2.3" assert commit == "abcdef12" -def test_get_version_missing_file_raises(tmp_path: Path) -> None: - # Arrange (one file only) - (tmp_path / "COMMIT").write_text("abcdef1234\n") - - # Act / Assert - with pytest.raises(RuntimeError): - fema.get_version_and_commit_id_from_files(str(tmp_path)) - - # ------------------------------- # Tests for main() # ------------------------------- -def test_main_prints_arch( - monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - # Arrange - argv = ["prog", "--arch", "amd64", "--cname", "flav", "--version", "1.0", "arch"] - monkeypatch.setattr(sys, "argv", argv) - monkeypatch.setattr(fema, "Parser", lambda *a, **kw: None) - # Act - fema.main() - # Assert - out = capsys.readouterr().out - assert "amd64" in out - - -def test_main_prints_container_name( - monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +@pytest.mark.parametrize( + "input_argv, monkeypatch_version, release_metadata, ignored_features, expected_output", + [ + ( + [ + "--arch", + "amd64", + "--cname", + "flav", + "--version", + "1.0", + "--commit", + "local", + "arch", + ], + False, + None, + "", + "amd64", + ), + ( + [ + "--arch", + "arm64", + "--cname", + "container", + "--version", + "today", + "--commit", + "local", + "arch", + ], + False, + generate_container_release_metadata("today", "local", "arm64"), + "", + "arm64", + ), + ( + [ + "--arch", + "amd64", + "--cname", + "container-pythonDev", + "artifact-base-name", + ], + True, + None, + "", + "container-pythonDev-amd64-1.2.3-abcdef12", + ), + ( + [ + "--arch", + "amd64", + "--cname", + "container", + "--version", + "today", + "--commit", + "local", + "artifact-base-name", + ], + False, + generate_container_release_metadata("today", "local"), + "", + "container-amd64-today-local", + ), + ( + [ + "--arch", + "amd64", + "--cname", + "container-pythonDev", + "artifact-base-name", + ], + True, + None, + "_archgrouped", + "container-pythonDev-amd64-1.2.3-abcdef12", + ), + ( + ["--arch", "amd64", "--cname", "flav", "cname"], + True, + None, + "", + "flav", + ), + ( + [ + "--arch", + "amd64", + "--cname", + "container", + "--version", + "today", + "--commit", + "local", + "cname", + ], + False, + generate_container_release_metadata("today", "local"), + "", + "container", + ), + ( + ["--arch", "amd64", "--cname", "flav", "commit-id"], + True, + None, + "", + "abcdef12", + ), + ( + [ + "--arch", + "amd64", + "--cname", + "container", + "--version", + "today", + "--commit", + "local", + "commit-id", + ], + False, + generate_container_release_metadata("today", "local"), + "", + "local", + ), + ( + [ + "--arch", + "amd64", + "--cname", + "container-pythonDev", + "--version", + "1.0", + "--commit", + "local", + "container-name", + ], + True, + None, + "", + "container-python-dev", + ), + ( + [ + "--arch", + "amd64", + "--cname", + "flav", + "--version", + "1.0", + "--commit", + "post1", + "container-tag", + ], + False, + None, + "", + "1-0-post1", + ), + ( + ["--arch", "amd64", "--cname", "container-pythonDev", "elements"], + True, + None, + "", + "python,pythonDev,base", + ), + ( + [ + "--arch", + "amd64", + "--cname", + "container", + "--version", + "today", + "--commit", + "local", + "elements", + ], + False, + generate_container_release_metadata("today", "local"), + "", + "base", + ), + ( + ["--arch", "amd64", "--cname", "container-pythonDev", "features"], + True, + None, + "", + "python,pythonDev,_archgrouped,_slim,base,container", + ), + ( + ["--arch", "amd64", "--cname", "container-pythonDev", "features"], + True, + None, + "_archgrouped", + "python,pythonDev,_slim,base,container", + ), + ( + [ + "--arch", + "amd64", + "--cname", + "container", + "--version", + "today", + "--commit", + "local", + "features", + ], + False, + generate_container_release_metadata("today", "local"), + "", + "_archgrouped,_slim,base,container", + ), + ( + [ + "--arch", + "amd64", + "--cname", + "container", + "--version", + "today", + "--commit", + "local", + "features", + ], + False, + generate_container_release_metadata("today", "local"), + "_archgrouped", + "_slim,base,container", + ), + ( + ["--arch", "amd64", "--cname", "container-pythonDev", "flags"], + True, + None, + "", + "_archgrouped,_slim", + ), + ( + ["--arch", "amd64", "--cname", "container-pythonDev", "flags"], + True, + None, + "_archgrouped", + "_slim", + ), + ( + [ + "--arch", + "amd64", + "--cname", + "container", + "--version", + "today", + "--commit", + "local", + "flags", + ], + False, + generate_container_release_metadata("today", "local"), + "", + "_archgrouped,_slim", + ), + ( + ["--arch", "amd64", "--cname", "container-pythonDev", "flavor"], + True, + None, + "", + "container-pythonDev-amd64", + ), + ( + ["--arch", "amd64", "--cname", "container-pythonDev", "platform"], + True, + None, + "", + "container", + ), + ( + [ + "--arch", + "amd64", + "--cname", + "container", + "--version", + "today", + "--commit", + "local", + "platform", + ], + False, + generate_container_release_metadata( + "today", "local", variant="magicMachine" + ), + "", + "container", + ), + ( + ["--arch", "amd64", "--cname", "container-pythonDev", "platforms"], + True, + None, + "", + "container", + ), + ( + [ + "--arch", + "amd64", + "--cname", + "container", + "--version", + "today", + "--commit", + "local", + "platform-variant", + ], + False, + generate_container_release_metadata( + "today", "local", variant="magicMachine" + ), + "", + "magicMachine", + ), + ( + ["--arch", "amd64", "--cname", "container-pythonDev", "version"], + True, + None, + "", + "1.2.3", + ), + ( + [ + "--arch", + "amd64", + "--cname", + "container-pythonDev", + "version_and_commit-id", + ], + True, + None, + "", + "1.2.3-abcdef12", + ), + ( + [ + "--arch", + "amd64", + "--cname", + "container", + "version_and_commit-id", + ], + True, + generate_container_release_metadata("1.2.3", "abcdef12"), + "", + "1.2.3-abcdef12", + ), + ( + ["--arch", "amd64", "--cname", "container-pythonDev", "versioned-flavor"], + True, + None, + "", + "container-pythonDev-amd64-1.2.3", + ), + ], +) +def test_main_prints_result( + input_argv: List[str], + monkeypatch_version: bool, + release_metadata: Optional[str], + ignored_features: Optional[str], + expected_output: str, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + tmp_path: Path, ) -> None: # Arrange argv = [ - "prog", - "--arch", - "amd64", - "--cname", - "container-pythonDev", - "--version", - "1.0", - "container_name", - ] - monkeypatch.setattr(sys, "argv", argv) - monkeypatch.setattr(fema, "Parser", lambda *a, **kw: None) - - # Act - fema.main() - - # Assert - out = capsys.readouterr().out - assert "container-python-dev" in out + "gl-feature-parse", + "--feature-dir", + f"{GL_ROOT_DIR}/features", + ] + input_argv + if release_metadata: + os_release_file = Path(tmp_path, "os_release") -def test_main_prints_container_tag( - monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - # Arrange - argv = [ - "prog", - "--arch", - "amd64", - "--cname", - "flav", - "--version", - "1.0", - "--commit", - "~post1", - "container_tag", - ] - monkeypatch.setattr(sys, "argv", argv) - monkeypatch.setattr(fema, "Parser", lambda *a, **kw: None) - - # Act - fema.main() + with os_release_file.open("w") as fp: + fp.write(release_metadata) - # Assert - out = capsys.readouterr().out.strip() - assert "1-0-post1" == out + argv += [ + "--release-file", + str(os_release_file), + ] + if ignored_features: + argv += [ + "--ignore", + str(ignored_features), + ] -def test_main_prints_commit_id( - monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - # Arrange - argv = ["prog", "--arch", "amd64", "--cname", "flav", "commit_id"] monkeypatch.setattr(sys, "argv", argv) - monkeypatch.setattr( - fema, - "Parser", - lambda *a, **kw: types.SimpleNamespace(filter=lambda *a, **k: None), - ) - # Patch get_version_and_commit_id_from_files - monkeypatch.setattr( - fema, "get_version_and_commit_id_from_files", lambda root: ("1.2.3", "abcdef12") - ) + # monkeypatch.setattr(fema, "Parser", lambda *a, **kw: None) + + if monkeypatch_version: + monkeypatch.setattr( + "gardenlinux.features.artifact_base_name.ArtifactBaseName.get_version_and_commit_id_from_files", + lambda root: ("1.2.3", "abcdef12"), + ) # Act fema.main() + # Assert captured = capsys.readouterr() - assert "abcdef12" == captured.out.strip() + assert captured.out.strip() == expected_output -def test_main_prints_flags_elements_platforms( - monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +def test_get_version_missing_file_raises( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - # Arrange + # Arrange (one file only) + (tmp_path / "COMMIT").write_text("abcdef1234\n") + argv = [ "prog", "--arch", "amd64", "--cname", "flav", - "--version", - "1.0", - "flags", + "version", ] - monkeypatch.setattr(sys, "argv", argv) - - class FakeCName(CName): - def __init__(self, *a: Any, **k: Any): - CName.__init__(self, *a, **k) - self._feature_flags_cached = ["flag1"] - monkeypatch.setattr(fema, "CName", FakeCName) - - # Act - fema.main() - - # Assert - out = capsys.readouterr().out - assert "flag1" in out - - -def test_main_prints_version( - monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - # Arrange - argv = ["prog", "--arch", "amd64", "--cname", "flav", "version"] monkeypatch.setattr(sys, "argv", argv) - monkeypatch.setattr( - fema, - "Parser", - lambda *a, **kw: types.SimpleNamespace(filter=lambda *a, **k: None), - ) - # Patch get_version_and_commit_id_from_files - monkeypatch.setattr( - fema, "get_version_and_commit_id_from_files", lambda root: ("1.2.3", "abcdef12") - ) - - # Act - fema.main() - - captured = capsys.readouterr() - assert "1.2.3" == captured.out.strip() - + monkeypatch.setattr(fema, "Parser", lambda *a, **kw: None) -def test_main_prints_version_and_commit_id( - monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - # Arrange - argv = ["prog", "--arch", "amd64", "--cname", "flav", "version_and_commit_id"] - monkeypatch.setattr(sys, "argv", argv) - monkeypatch.setattr( - fema, - "Parser", - lambda *a, **kw: types.SimpleNamespace(filter=lambda *a, **k: None), - ) - # Patch get_version_and_commit_id_from_files - monkeypatch.setattr( - fema, "get_version_and_commit_id_from_files", lambda root: ("1.2.3", "abcdef12") + # Act / Assert + assert ArtifactBaseName.get_version_and_commit_id_from_files(str(tmp_path)) == ( + None, + None, ) - # Act - fema.main() - - captured = capsys.readouterr() - assert "1.2.3-abcdef12" == captured.out.strip() + with pytest.raises(ValueError, match="Argument missing: version"): + fema.main() def test_main_requires_cname(monkeypatch: pytest.MonkeyPatch) -> None: @@ -261,7 +516,10 @@ def test_main_requires_cname(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(fema, "Parser", lambda *a, **kw: None) # Act / Assert - with pytest.raises(SystemExit): + with pytest.raises( + ValueError, + match="Argument missing: At least one of artifact_base_name, cname, flavor or versioned_flavor", + ): fema.main() @@ -272,7 +530,7 @@ def test_main_cname_raises_missing_commit_id(monkeypatch: pytest.MonkeyPatch) -> "prog", "--cname", "flav", - "--default-arch", + "--arch", "amd64", "--version", "1.0", @@ -281,7 +539,7 @@ def test_main_cname_raises_missing_commit_id(monkeypatch: pytest.MonkeyPatch) -> monkeypatch.setattr(sys, "argv", argv) # Act / Assert - with pytest.raises(RuntimeError, match="Version and commit ID"): + with pytest.raises(ValueError, match="Argument missing: version"): fema.main() @@ -292,7 +550,7 @@ def test_main_raises_no_arch_no_default(monkeypatch: pytest.MonkeyPatch) -> None monkeypatch.setattr(sys, "argv", argv) # Act / Assert - with pytest.raises(RuntimeError, match="Architecture could not be determined"): + with pytest.raises(ValueError, match="Argument missing: arch"): fema.main() @@ -308,13 +566,13 @@ def test_main_raises_missing_commit_id( "flav", "--version", "1.0", - "version_and_commit_id", + "version_and_commit-id", ] monkeypatch.setattr(sys, "argv", argv) monkeypatch.setattr(fema, "Parser", lambda *a, **kw: None) # Act / Assert - with pytest.raises(RuntimeError, match="Commit ID not specified"): + with pytest.raises(ValueError, match="Argument missing: version"): fema.main() @@ -387,34 +645,3 @@ def test_main_with_exclude_cname_print_features( "sap,ssh,_fwcfg,_ignite,_legacy,_nopkg,_prod,_slim,base,server,cloud,kvm,multipath,iscsi,nvme,gardener" == captured ) - - -def test_cname_release_file( - monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - """ - Test validation between release metadata and arguments given - """ - # Arrange - with TemporaryDirectory() as tmpdir: - os_release_file = Path(tmpdir, "os_release") - - with os_release_file.open("w") as fp: - fp.write(generate_container_amd64_release_metadata("today", "local")) - - argv = [ - "prog", - "--cname", - "container-amd64-today-local", - "--release-file", - str(os_release_file), - "cname", - ] - monkeypatch.setattr(sys, "argv", argv) - - # Act / Assert - fema.main() - - # Assert - out = capsys.readouterr().out - assert "container-amd64-today-local" in out diff --git a/tests/features/test_metadata_main.py b/tests/features/test_metadata_main.py index b8f0fc69..9487c269 100644 --- a/tests/features/test_metadata_main.py +++ b/tests/features/test_metadata_main.py @@ -6,7 +6,7 @@ import gardenlinux.features.metadata_main as metadata_main -from .constants import generate_container_amd64_release_metadata +from .constants import generate_container_release_metadata def test_main_output( @@ -18,7 +18,7 @@ def test_main_output( # Arrange argv = [ "prog", - "--cname", + "--flavor", "container-amd64", "--version", "today", @@ -32,8 +32,8 @@ def test_main_output( metadata_main.main() # Assert - expected = generate_container_amd64_release_metadata("today", "local") - assert expected == capsys.readouterr().out.strip() + expected = generate_container_release_metadata("today", "local") + assert capsys.readouterr().out.strip() == expected def test_main_write( @@ -47,7 +47,7 @@ def test_main_write( os_release_file = Path(tmpdir, "os_release") argv = [ "prog", - "--cname", + "--flavor", "container-amd64", "--version", "today", @@ -63,35 +63,60 @@ def test_main_write( metadata_main.main() # Assert - expected = generate_container_amd64_release_metadata("today", "local") + expected = generate_container_release_metadata("today", "local") assert expected == os_release_file.open("r").read() -def test_main_validation(monkeypatch: pytest.MonkeyPatch) -> None: +def test_main_validation(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: """ Test validation between release metadata and arguments given """ # Arrange - with TemporaryDirectory() as tmpdir: - os_release_file = Path(tmpdir, "os_release") + os_release_file = Path(tmp_path, "os_release") - with os_release_file.open("w") as fp: - fp.write(generate_container_amd64_release_metadata("today", "local")) + with os_release_file.open("w") as fp: + fp.write(generate_container_release_metadata("today", "local")) - argv = [ - "prog", - "--cname", - "base-python-amd64", - "--version", - "today", - "--commit", - "local", - "--release-file", - str(os_release_file), - "output-release-metadata", - ] - monkeypatch.setattr(sys, "argv", argv) + argv = [ + "prog", + "--flavor", + "base-python-amd64", + "--version", + "today", + "--commit", + "local", + "--release-file", + str(os_release_file), + "output-release-metadata", + ] + monkeypatch.setattr(sys, "argv", argv) + + # Act / Assert + with pytest.raises(RuntimeError): + metadata_main.main() + + +def test_main_version_missing_file_raises( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + # Arrange + (tmp_path / "COMMIT").write_text("abcdef1234\n") - # Act / Assert - with pytest.raises(RuntimeError): - metadata_main.main() + os_release_file = Path(tmp_path, "os_release") + + with os_release_file.open("w") as fp: + fp.write(generate_container_release_metadata("today", "local")) + + argv = [ + "prog", + "--flavor", + "base-python-amd64", + "--release-file", + str(os_release_file), + "output-release-metadata", + ] + monkeypatch.setattr(sys, "argv", argv) + + # Act / Assert + with pytest.raises(ValueError, match="Argument missing: version"): + metadata_main.main() diff --git a/tests/features/test_parser.py b/tests/features/test_parser.py index 7bb49fe3..adb9ffa0 100644 --- a/tests/features/test_parser.py +++ b/tests/features/test_parser.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List +from typing import Any, Dict, List, Tuple import pytest @@ -132,21 +132,36 @@ def test_parser_return_intersection_subset() -> None: assert result == ["a", "c"] -def test_get_flavor_from_feature_set() -> None: +def test_get_cname_from_feature_set() -> None: # Arrange sorted_features = ["base", "_hidden", "extra"] # Act - result = Parser.get_flavor_from_feature_set(sorted_features) + result = Parser.get_cname_from_feature_set(sorted_features) # Assert - assert result == "base_hidden-extra" + assert result == "base-extra_hidden" -def test_gget_flavor_from_feature_set_empty_raises() -> None: +def test_get_cname_from_feature_set_empty_raises() -> None: # get_flavor with empty iterable raises TypeError - with pytest.raises(TypeError): - Parser.get_flavor_from_feature_set([]) + with pytest.raises(ValueError): + Parser.get_cname_from_feature_set([]) + + +def test_get_minimal_feature_set_filters() -> None: + # Arrange + class FakeGraph: + def in_degree(self) -> List[Tuple[str, int]]: + return [("a", 0), ("b", 1), ("c", 0)] + + graph = FakeGraph() + + # Act + result = Parser.get_minimal_feature_set(graph) + + # Assert + assert result == {"a", "c"} def test_parser_subset_nomatch() -> None: diff --git a/tests/features/test_reproducibility.py b/tests/features/test_reproducibility.py index 7cc5cdd2..0a99d466 100644 --- a/tests/features/test_reproducibility.py +++ b/tests/features/test_reproducibility.py @@ -8,94 +8,94 @@ from gardenlinux.features.reproducibility.comparator import Comparator from gardenlinux.features.reproducibility.markdown_formatter import MarkdownFormatter -FLAVORS_MATRIX = { +CNAMES_MATRIX = { "include": [ - {"arch": "amd64", "flavor": "ali-gardener_prod"}, - {"arch": "amd64", "flavor": "aws-gardener_fips_prod"}, - {"arch": "amd64", "flavor": "aws-gardener_prod"}, - {"arch": "amd64", "flavor": "aws-gardener_prod_tpm2_trustedboot"}, - {"arch": "amd64", "flavor": "aws-gardener_prod_trustedboot"}, - {"arch": "amd64", "flavor": "aws-gardener_prod_usi"}, - {"arch": "amd64", "flavor": "azure-gardener_prod"}, - {"arch": "amd64", "flavor": "azure-gardener_prod_tpm2_trustedboot"}, - {"arch": "amd64", "flavor": "azure-gardener_prod_trustedboot"}, - {"arch": "amd64", "flavor": "azure-gardener_prod_usi"}, - {"arch": "amd64", "flavor": "baremetal-capi"}, - {"arch": "amd64", "flavor": "baremetal-gardener_prod"}, - {"arch": "amd64", "flavor": "baremetal-gardener_prod_tpm2_trustedboot"}, - {"arch": "amd64", "flavor": "baremetal-gardener_prod_trustedboot"}, - {"arch": "amd64", "flavor": "baremetal-gardener_prod_usi"}, - {"arch": "amd64", "flavor": "baremetal-gardener_pxe"}, - {"arch": "amd64", "flavor": "baremetal-vhost"}, - {"arch": "amd64", "flavor": "baremetal_pxe"}, - {"arch": "amd64", "flavor": "container"}, - {"arch": "amd64", "flavor": "container-pythonDev"}, - {"arch": "amd64", "flavor": "gcp-gardener_prod"}, - {"arch": "amd64", "flavor": "gcp-gardener_prod_tpm2_trustedboot"}, - {"arch": "amd64", "flavor": "gcp-gardener_prod_trustedboot"}, - {"arch": "amd64", "flavor": "gcp-gardener_prod_usi"}, - {"arch": "amd64", "flavor": "gdch-gardener_prod"}, - {"arch": "amd64", "flavor": "kvm-gardener_prod"}, - {"arch": "amd64", "flavor": "kvm-gardener_prod_tpm2_trustedboot"}, - {"arch": "amd64", "flavor": "kvm-gardener_prod_trustedboot"}, - {"arch": "amd64", "flavor": "kvm-gardener_prod_usi"}, - {"arch": "amd64", "flavor": "lima"}, - {"arch": "amd64", "flavor": "openstack-gardener_prod"}, - {"arch": "amd64", "flavor": "openstack-gardener_prod_tpm2_trustedboot"}, - {"arch": "amd64", "flavor": "openstack-gardener_prod_trustedboot"}, - {"arch": "amd64", "flavor": "openstack-gardener_prod_usi"}, - {"arch": "amd64", "flavor": "openstack-metal-gardener_prod"}, - {"arch": "amd64", "flavor": "openstack-metal-gardener_prod_usi"}, - {"arch": "amd64", "flavor": "vmware-gardener_prod"}, - {"arch": "arm64", "flavor": "aws-gardener_fips_prod"}, - {"arch": "arm64", "flavor": "aws-gardener_prod"}, - {"arch": "arm64", "flavor": "aws-gardener_prod_tpm2_trustedboot"}, - {"arch": "arm64", "flavor": "aws-gardener_prod_trustedboot"}, - {"arch": "arm64", "flavor": "aws-gardener_prod_usi"}, - {"arch": "arm64", "flavor": "azure-gardener_prod"}, - {"arch": "arm64", "flavor": "azure-gardener_prod_tpm2_trustedboot"}, - {"arch": "arm64", "flavor": "azure-gardener_prod_trustedboot"}, - {"arch": "arm64", "flavor": "azure-gardener_prod_usi"}, - {"arch": "arm64", "flavor": "baremetal-capi"}, - {"arch": "arm64", "flavor": "baremetal-gardener_prod"}, - {"arch": "arm64", "flavor": "baremetal-gardener_prod_tpm2_trustedboot"}, - {"arch": "arm64", "flavor": "baremetal-gardener_prod_trustedboot"}, - {"arch": "arm64", "flavor": "baremetal-gardener_prod_usi"}, - {"arch": "arm64", "flavor": "baremetal-gardener_pxe"}, - {"arch": "arm64", "flavor": "baremetal-vhost"}, - {"arch": "arm64", "flavor": "baremetal_pxe"}, - {"arch": "arm64", "flavor": "container"}, - {"arch": "arm64", "flavor": "container-pythonDev"}, - {"arch": "arm64", "flavor": "gcp-gardener_prod"}, - {"arch": "arm64", "flavor": "gcp-gardener_prod_tpm2_trustedboot"}, - {"arch": "arm64", "flavor": "gcp-gardener_prod_trustedboot"}, - {"arch": "arm64", "flavor": "gcp-gardener_prod_usi"}, - {"arch": "arm64", "flavor": "gdch-gardener_prod"}, - {"arch": "arm64", "flavor": "kvm-gardener_prod"}, - {"arch": "arm64", "flavor": "kvm-gardener_prod_tpm2_trustedboot"}, - {"arch": "arm64", "flavor": "kvm-gardener_prod_trustedboot"}, - {"arch": "arm64", "flavor": "kvm-gardener_prod_usi"}, - {"arch": "arm64", "flavor": "lima"}, - {"arch": "arm64", "flavor": "openstack-gardener_prod"}, - {"arch": "arm64", "flavor": "openstack-gardener_prod_tpm2_trustedboot"}, - {"arch": "arm64", "flavor": "openstack-gardener_prod_trustedboot"}, - {"arch": "arm64", "flavor": "openstack-gardener_prod_usi"}, - {"arch": "arm64", "flavor": "openstack-metal-gardener_prod"}, - {"arch": "arm64", "flavor": "openstack-metal-gardener_prod_usi"}, - {"arch": "arm64", "flavor": "vmware-gardener_prod"}, + {"arch": "amd64", "cname": "ali-gardener_prod"}, + {"arch": "amd64", "cname": "aws-gardener_fips_prod"}, + {"arch": "amd64", "cname": "aws-gardener_prod"}, + {"arch": "amd64", "cname": "aws-gardener_prod_tpm2_trustedboot"}, + {"arch": "amd64", "cname": "aws-gardener_prod_trustedboot"}, + {"arch": "amd64", "cname": "aws-gardener_prod_usi"}, + {"arch": "amd64", "cname": "azure-gardener_prod"}, + {"arch": "amd64", "cname": "azure-gardener_prod_tpm2_trustedboot"}, + {"arch": "amd64", "cname": "azure-gardener_prod_trustedboot"}, + {"arch": "amd64", "cname": "azure-gardener_prod_usi"}, + {"arch": "amd64", "cname": "baremetal-capi"}, + {"arch": "amd64", "cname": "baremetal-gardener_prod"}, + {"arch": "amd64", "cname": "baremetal-gardener_prod_tpm2_trustedboot"}, + {"arch": "amd64", "cname": "baremetal-gardener_prod_trustedboot"}, + {"arch": "amd64", "cname": "baremetal-gardener_prod_usi"}, + {"arch": "amd64", "cname": "baremetal-gardener_pxe"}, + {"arch": "amd64", "cname": "baremetal-vhost"}, + {"arch": "amd64", "cname": "baremetal_pxe"}, + {"arch": "amd64", "cname": "container"}, + {"arch": "amd64", "cname": "container-pythonDev"}, + {"arch": "amd64", "cname": "gcp-gardener_prod"}, + {"arch": "amd64", "cname": "gcp-gardener_prod_tpm2_trustedboot"}, + {"arch": "amd64", "cname": "gcp-gardener_prod_trustedboot"}, + {"arch": "amd64", "cname": "gcp-gardener_prod_usi"}, + {"arch": "amd64", "cname": "gdch-gardener_prod"}, + {"arch": "amd64", "cname": "kvm-gardener_prod"}, + {"arch": "amd64", "cname": "kvm-gardener_prod_tpm2_trustedboot"}, + {"arch": "amd64", "cname": "kvm-gardener_prod_trustedboot"}, + {"arch": "amd64", "cname": "kvm-gardener_prod_usi"}, + {"arch": "amd64", "cname": "lima"}, + {"arch": "amd64", "cname": "openstack-gardener_prod"}, + {"arch": "amd64", "cname": "openstack-gardener_prod_tpm2_trustedboot"}, + {"arch": "amd64", "cname": "openstack-gardener_prod_trustedboot"}, + {"arch": "amd64", "cname": "openstack-gardener_prod_usi"}, + {"arch": "amd64", "cname": "openstack-metal-gardener_prod"}, + {"arch": "amd64", "cname": "openstack-metal-gardener_prod_usi"}, + {"arch": "amd64", "cname": "vmware-gardener_prod"}, + {"arch": "arm64", "cname": "aws-gardener_fips_prod"}, + {"arch": "arm64", "cname": "aws-gardener_prod"}, + {"arch": "arm64", "cname": "aws-gardener_prod_tpm2_trustedboot"}, + {"arch": "arm64", "cname": "aws-gardener_prod_trustedboot"}, + {"arch": "arm64", "cname": "aws-gardener_prod_usi"}, + {"arch": "arm64", "cname": "azure-gardener_prod"}, + {"arch": "arm64", "cname": "azure-gardener_prod_tpm2_trustedboot"}, + {"arch": "arm64", "cname": "azure-gardener_prod_trustedboot"}, + {"arch": "arm64", "cname": "azure-gardener_prod_usi"}, + {"arch": "arm64", "cname": "baremetal-capi"}, + {"arch": "arm64", "cname": "baremetal-gardener_prod"}, + {"arch": "arm64", "cname": "baremetal-gardener_prod_tpm2_trustedboot"}, + {"arch": "arm64", "cname": "baremetal-gardener_prod_trustedboot"}, + {"arch": "arm64", "cname": "baremetal-gardener_prod_usi"}, + {"arch": "arm64", "cname": "baremetal-gardener_pxe"}, + {"arch": "arm64", "cname": "baremetal-vhost"}, + {"arch": "arm64", "cname": "baremetal_pxe"}, + {"arch": "arm64", "cname": "container"}, + {"arch": "arm64", "cname": "container-pythonDev"}, + {"arch": "arm64", "cname": "gcp-gardener_prod"}, + {"arch": "arm64", "cname": "gcp-gardener_prod_tpm2_trustedboot"}, + {"arch": "arm64", "cname": "gcp-gardener_prod_trustedboot"}, + {"arch": "arm64", "cname": "gcp-gardener_prod_usi"}, + {"arch": "arm64", "cname": "gdch-gardener_prod"}, + {"arch": "arm64", "cname": "kvm-gardener_prod"}, + {"arch": "arm64", "cname": "kvm-gardener_prod_tpm2_trustedboot"}, + {"arch": "arm64", "cname": "kvm-gardener_prod_trustedboot"}, + {"arch": "arm64", "cname": "kvm-gardener_prod_usi"}, + {"arch": "arm64", "cname": "lima"}, + {"arch": "arm64", "cname": "openstack-gardener_prod"}, + {"arch": "arm64", "cname": "openstack-gardener_prod_tpm2_trustedboot"}, + {"arch": "arm64", "cname": "openstack-gardener_prod_trustedboot"}, + {"arch": "arm64", "cname": "openstack-gardener_prod_usi"}, + {"arch": "arm64", "cname": "openstack-metal-gardener_prod"}, + {"arch": "arm64", "cname": "openstack-metal-gardener_prod_usi"}, + {"arch": "arm64", "cname": "vmware-gardener_prod"}, ] } -BARE_FLAVORS_MATRIX = { +BARE_CNAMES_MATRIX = { "include": [ - {"arch": "amd64", "flavor": "bare-libc"}, - {"arch": "amd64", "flavor": "bare-nodejs"}, - {"arch": "amd64", "flavor": "bare-python"}, - {"arch": "amd64", "flavor": "bare-sapmachine"}, - {"arch": "arm64", "flavor": "bare-libc"}, - {"arch": "arm64", "flavor": "bare-nodejs"}, - {"arch": "arm64", "flavor": "bare-python"}, - {"arch": "arm64", "flavor": "bare-sapmachine"}, + {"arch": "amd64", "cname": "bare-libc"}, + {"arch": "amd64", "cname": "bare-nodejs"}, + {"arch": "amd64", "cname": "bare-python"}, + {"arch": "amd64", "cname": "bare-sapmachine"}, + {"arch": "arm64", "cname": "bare-libc"}, + {"arch": "arm64", "cname": "bare-nodejs"}, + {"arch": "arm64", "cname": "bare-python"}, + {"arch": "arm64", "cname": "bare-sapmachine"}, ] } @@ -110,16 +110,16 @@ def test_formatter(i: str) -> None: if nightly_stats.is_file(): formatter = MarkdownFormatter( - FLAVORS_MATRIX, - BARE_FLAVORS_MATRIX, + CNAMES_MATRIX, + BARE_CNAMES_MATRIX, diff_files.joinpath(i), gardenlinux_root=str(gardenlinux_root), nightly_stats=nightly_stats, ) else: formatter = MarkdownFormatter( - FLAVORS_MATRIX, - BARE_FLAVORS_MATRIX, + CNAMES_MATRIX, + BARE_CNAMES_MATRIX, diff_files.joinpath(i), gardenlinux_root=str(gardenlinux_root), ) @@ -143,8 +143,8 @@ def test_formatter_main( str(gardenlinux_root.joinpath("features")), "--diff-dir", str(diff_files.joinpath(i)), - json.dumps(FLAVORS_MATRIX), - json.dumps(BARE_FLAVORS_MATRIX), + json.dumps(CNAMES_MATRIX), + json.dumps(BARE_CNAMES_MATRIX), ] if nightly_stats.is_file(): diff --git a/tests/oci/test_image_manifest.py b/tests/oci/test_image_manifest.py index 4ac89691..3d744c52 100644 --- a/tests/oci/test_image_manifest.py +++ b/tests/oci/test_image_manifest.py @@ -22,10 +22,16 @@ def test_ImageManifest_arch() -> None: def test_ImageManifest_cname() -> None: # Arrange - cname = "container-amd64-today-local" + arch = "amd64" + cname = "container" empty_manifest = ImageManifest() - manifest = ImageManifest(annotations={ImageManifest.ANNOTATION_CNAME_KEY: cname}) + manifest = ImageManifest( + annotations={ + ImageManifest.ANNOTATION_CNAME_KEY: cname, + ImageManifest.ANNOTATION_ARCH_KEY: arch, + } + ) # Assert with pytest.raises(RuntimeError): @@ -58,18 +64,25 @@ def test_ImageManifest_feature_set() -> None: def test_ImageManifest_flavor() -> None: # Arrange - flavor = "container" - cname = f"{flavor}-amd64-today-local" + arch = "amd64" + cname = "container" + flavor = f"{cname}-{arch}" empty_manifest = ImageManifest() - manifest = ImageManifest(annotations={ImageManifest.ANNOTATION_CNAME_KEY: cname}) + manifest = ImageManifest( + annotations={ + ImageManifest.ANNOTATION_CNAME_KEY: cname, + ImageManifest.ANNOTATION_ARCH_KEY: arch, + } + ) # Assert with pytest.raises(RuntimeError): - assert empty_manifest.flavor == flavor + assert empty_manifest.cname == cname - empty_manifest.cname = cname - assert empty_manifest.flavor == flavor + empty_manifest.flavor = flavor + assert empty_manifest.cname == cname + assert empty_manifest.arch == arch assert manifest.flavor == flavor diff --git a/tests/s3/conftest.py b/tests/s3/conftest.py index 8ba1fa09..9219fd06 100644 --- a/tests/s3/conftest.py +++ b/tests/s3/conftest.py @@ -20,10 +20,10 @@ class S3Env: s3: S3ServiceResource bucket_name: str tmp_path: Path - cname: str + artifact_base_name: str -def make_cname( +def make_artifact_base_name( flavor: str = "container_trustedboot_usi", arch: str = "amd64", version: str = "1234.1", @@ -64,5 +64,5 @@ def s3_setup( monkeypatch.setattr("gardenlinux.s3.s3_artifacts.file_digest", dummy_digest) - cname = make_cname() - yield S3Env(s3, BUCKET_NAME, tmp_path, cname) + artifact_base_name = make_artifact_base_name() + yield S3Env(s3, BUCKET_NAME, tmp_path, artifact_base_name) diff --git a/tests/s3/constants.py b/tests/s3/constants.py index b8a06bf9..d63680fe 100644 --- a/tests/s3/constants.py +++ b/tests/s3/constants.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- RELEASE_DATA = """ -GARDENLINUX_CNAME="container_trustedboot_usi-amd64-1234.1" +VARIANT_ID="container_trustedboot_usi-amd64" +GARDENLINUX_CNAME="container_trustedboot_usi" GARDENLINUX_VERSION=1234.1 GARDENLINUX_COMMIT_ID="abc123lo" GARDENLINUX_COMMIT_ID_LONG="abc123long" diff --git a/tests/s3/test_main.py b/tests/s3/test_main.py index 7fa100ff..539441c6 100644 --- a/tests/s3/test_main.py +++ b/tests/s3/test_main.py @@ -85,10 +85,10 @@ def test_main_with_expected_result( str(env.tmp_path), "upload-artifacts-to-bucket", "--artifact-name", - env.cname, + env.artifact_base_name, ], ): - release_path = env.tmp_path / f"{env.cname}.release" + release_path = env.tmp_path / f"{env.artifact_base_name}.release" release_path.write_text(RELEASE_DATA) s3m.main() diff --git a/tests/s3/test_s3_artifacts.py b/tests/s3/test_s3_artifacts.py index 9983c46f..b6e6e08e 100644 --- a/tests/s3/test_s3_artifacts.py +++ b/tests/s3/test_s3_artifacts.py @@ -37,19 +37,21 @@ def test_download_to_directory_success(s3_setup: S3Env) -> None: env = s3_setup bucket = env.s3.Bucket(env.bucket_name) - bucket.put_object(Key=f"meta/singles/{env.cname}", Body=b"metadata") - bucket.put_object(Key=f"objects/{env.cname}/file1", Body=b"data1") - bucket.put_object(Key=f"objects/{env.cname}/file2", Body=b"data2") + bucket.put_object(Key=f"meta/singles/{env.artifact_base_name}", Body=b"metadata") + bucket.put_object(Key=f"objects/{env.artifact_base_name}/file1", Body=b"data1") + bucket.put_object(Key=f"objects/{env.artifact_base_name}/file2", Body=b"data2") with TemporaryDirectory() as tmpdir: outdir = Path(tmpdir) # Act artifacts = S3Artifacts(env.bucket_name) - artifacts.download_to_directory(env.cname, outdir) + artifacts.download_to_directory(env.artifact_base_name, outdir) # Assert - assert (outdir / f"{env.cname}.s3_metadata.yaml").read_bytes() == b"metadata" + assert ( + outdir / f"{env.artifact_base_name}.s3_metadata.yaml" + ).read_bytes() == b"metadata" assert (outdir / "file1").read_bytes() == b"data1" assert (outdir / "file2").read_bytes() == b"data2" @@ -64,7 +66,9 @@ def test_download_to_directory_invalid_path(s3_setup: S3Env) -> None: # Act / Assert with pytest.raises(RuntimeError): - artifacts.download_to_directory(env.cname, "/invalid/path/does/not/exist") + artifacts.download_to_directory( + env.artifact_base_name, "/invalid/path/does/not/exist" + ) def test_download_to_directory_non_pathlike_raises(s3_setup: S3Env) -> None: @@ -72,7 +76,7 @@ def test_download_to_directory_non_pathlike_raises(s3_setup: S3Env) -> None: env = s3_setup artifacts = S3Artifacts(env.bucket_name) with pytest.raises(RuntimeError): - artifacts.download_to_directory(env.cname, "nopath") + artifacts.download_to_directory(env.artifact_base_name, "nopath") def test_download_to_directory_no_metadata_raises(s3_setup: S3Env) -> None: @@ -84,7 +88,7 @@ def test_download_to_directory_no_metadata_raises(s3_setup: S3Env) -> None: # Act / Assert with TemporaryDirectory() as tmpdir: with pytest.raises(IndexError): - artifacts.download_to_directory(env.cname, tmpdir) + artifacts.download_to_directory(env.artifact_base_name, tmpdir) def test_upload_from_directory_success(s3_setup: S3Env) -> None: @@ -94,30 +98,37 @@ def test_upload_from_directory_success(s3_setup: S3Env) -> None: # Arrange env = s3_setup - release_path = env.tmp_path / f"{env.cname}.release" + release_path = env.tmp_path / f"{env.artifact_base_name}.release" release_path.write_text(RELEASE_DATA) - for filename in [f"{env.cname}-file1", f"{env.cname}-file2", "container"]: + for filename in [ + f"{env.artifact_base_name}-file1", + f"{env.artifact_base_name}-file2", + "container", + ]: (env.tmp_path / filename).write_bytes(b"dummy content") # Act artifacts = S3Artifacts(env.bucket_name) - artifacts.upload_from_directory(env.cname, env.tmp_path) + artifacts.upload_from_directory(env.artifact_base_name, env.tmp_path) # Assert bucket = env.s3.Bucket(env.bucket_name) keys = [obj.key for obj in bucket.objects.all()] - assert f"objects/{env.cname}/{env.cname}-file1" in keys - assert f"objects/{env.cname}/{env.cname}-file2" in keys - assert f"meta/singles/{env.cname}" in keys + assert f"objects/{env.artifact_base_name}/{env.artifact_base_name}-file1" in keys + assert f"objects/{env.artifact_base_name}/{env.artifact_base_name}-file2" in keys + assert f"meta/singles/{env.artifact_base_name}" in keys - meta_obj = list(bucket.objects.filter(Prefix=f"meta/singles/{env.cname}"))[0] + meta_obj = list( + bucket.objects.filter(Prefix=f"meta/singles/{env.artifact_base_name}") + )[0] metadata = yaml.safe_load(meta_obj.get()["Body"].read()) assert metadata["require_uefi"] is True assert metadata["secureboot"] is True raw_tags_response = env.s3.meta.client.get_object_tagging( - Bucket=env.bucket_name, Key=f"objects/{env.cname}/{env.cname}-file1" + Bucket=env.bucket_name, + Key=f"objects/{env.artifact_base_name}/{env.artifact_base_name}-file1", ) tags = {tag["Key"]: tag["Value"] for tag in raw_tags_response["TagSet"]} assert tags["platform"] == "container" @@ -132,28 +143,34 @@ def test_upload_from_directory_with_delete(s3_setup: S3Env) -> None: bucket = env.s3.Bucket(env.bucket_name) # Arrange: create release and artifact files locally - release = env.tmp_path / f"{env.cname}.release" + release = env.tmp_path / f"{env.artifact_base_name}.release" release.write_text(RELEASE_DATA) - artifact = env.tmp_path / f"{env.cname}.kernel" + artifact = env.tmp_path / f"{env.artifact_base_name}.kernel" artifact.write_bytes(b"fake") # Arrange: put dummy existing objects to be deleted - bucket.put_object(Key=f"objects/{env.cname}/{artifact.name}", Body=b"old data") - bucket.put_object(Key=f"meta/singles/{env.cname}", Body=b"old metadata") + bucket.put_object( + Key=f"objects/{env.artifact_base_name}/{artifact.name}", Body=b"old data" + ) + bucket.put_object( + Key=f"meta/singles/{env.artifact_base_name}", Body=b"old metadata" + ) artifacts = S3Artifacts(env.bucket_name) # Act - artifacts.upload_from_directory(env.cname, env.tmp_path, delete_before_push=True) + artifacts.upload_from_directory( + env.artifact_base_name, env.tmp_path, delete_before_push=True + ) # Assert keys = [obj.key for obj in bucket.objects.all()] # The old key should no longer be present as old data (no duplicates) # but the new upload file key should exist (artifact uploaded) - assert f"objects/{env.cname}/{artifact.name}" in keys - assert f"meta/singles/{env.cname}" in keys + assert f"objects/{env.artifact_base_name}/{artifact.name}" in keys + assert f"meta/singles/{env.artifact_base_name}" in keys def test_upload_from_directory_invalid_dir_raises(s3_setup: S3Env) -> None: @@ -161,7 +178,7 @@ def test_upload_from_directory_invalid_dir_raises(s3_setup: S3Env) -> None: env = s3_setup artifacts = S3Artifacts(env.bucket_name) with pytest.raises(RuntimeError, match="invalid"): - artifacts.upload_from_directory(env.cname, "/invalid/path") + artifacts.upload_from_directory(env.artifact_base_name, "/invalid/path") def test_upload_from_directory_version_mismatch(s3_setup: S3Env) -> None: @@ -170,13 +187,13 @@ def test_upload_from_directory_version_mismatch(s3_setup: S3Env) -> None: """ # Arrange env = s3_setup - release_path = env.tmp_path / f"{env.cname}.release" + release_path = env.tmp_path / f"{env.artifact_base_name}.release" bad_data = RELEASE_DATA.replace("1234.1", "9999.9") release_path.write_text(bad_data) artifacts = S3Artifacts(env.bucket_name) # Act / Assert - artifacts.upload_from_directory(env.cname, env.tmp_path) + artifacts.upload_from_directory(env.artifact_base_name, env.tmp_path) def test_upload_from_directory_succeeds_because_of_release_file( @@ -199,7 +216,7 @@ def test_upload_from_directory_invalid_artifact_name(s3_setup: S3Env) -> None: """ # Arrange env = s3_setup - (env.tmp_path / f"{env.cname}.release").write_text(RELEASE_DATA) + (env.tmp_path / f"{env.artifact_base_name}.release").write_text(RELEASE_DATA) # Create "bad" artifact that does not start with cname bad_file = env.tmp_path / "no_match" @@ -208,14 +225,19 @@ def test_upload_from_directory_invalid_artifact_name(s3_setup: S3Env) -> None: artifacts = S3Artifacts(env.bucket_name) # Act - artifacts.upload_from_directory(env.cname, env.tmp_path) + artifacts.upload_from_directory(env.artifact_base_name, env.tmp_path) # Assert bucket = env.s3.Bucket(env.bucket_name) - assert len(list(bucket.objects.filter(Prefix=f"meta/singles/{env.cname}"))) == 1 + assert ( + len( + list(bucket.objects.filter(Prefix=f"meta/singles/{env.artifact_base_name}")) + ) + == 1 + ) -def test_upload_from_directory_invalid_release_file_with_valid_cname( +def test_upload_from_directory_beside_of_incomplete_release_file( s3_setup: S3Env, ) -> None: """ @@ -223,20 +245,25 @@ def test_upload_from_directory_invalid_release_file_with_valid_cname( """ # Arrange env = s3_setup - release_path = env.tmp_path / f"{env.cname}.release" + release_path = env.tmp_path / f"{env.artifact_base_name}.release" bad_data = RELEASE_DATA.replace( - "GARDENLINUX_FEATURES_PLATFORMS=", "GARDENLINUX_FEATURES_PLATFORMS_UNDEFINED=" + "GARDENLINUX_PLATFORM=", "GARDENLINUX_PLATFORM_UNDEFINED=" ) release_path.write_text(bad_data) artifacts = S3Artifacts(env.bucket_name) # Act - artifacts.upload_from_directory(env.cname, env.tmp_path) + artifacts.upload_from_directory(env.artifact_base_name, env.tmp_path) # Assert bucket = env.s3.Bucket(env.bucket_name) - assert len(list(bucket.objects.filter(Prefix=f"meta/singles/{env.cname}"))) == 1 + assert ( + len( + list(bucket.objects.filter(Prefix=f"meta/singles/{env.artifact_base_name}")) + ) + == 1 + ) def test_upload_from_directory_commit_mismatch(s3_setup: S3Env) -> None: @@ -245,13 +272,13 @@ def test_upload_from_directory_commit_mismatch(s3_setup: S3Env) -> None: """ # Arrange env = s3_setup - release_path = env.tmp_path / f"{env.cname}.release" + release_path = env.tmp_path / f"{env.artifact_base_name}.release" bad_data = RELEASE_DATA.replace("abc123", "wrong") release_path.write_text(bad_data) artifacts = S3Artifacts(env.bucket_name) # Act / Assert - artifacts.upload_from_directory(env.cname, env.tmp_path) + artifacts.upload_from_directory(env.artifact_base_name, env.tmp_path) def test_upload_from_directory_with_platform_variant(s3_setup: S3Env) -> None: @@ -260,20 +287,23 @@ def test_upload_from_directory_with_platform_variant(s3_setup: S3Env) -> None: """ # Arrange env = s3_setup - release_path = env.tmp_path / f"{env.cname}.release" + release_path = env.tmp_path / f"{env.artifact_base_name}.release" release_path.write_text( RELEASE_DATA.strip() + "\nGARDENLINUX_PLATFORM_VARIANT=test" ) + print(release_path.read_text()) # Act artifacts = S3Artifacts(env.bucket_name) - artifacts.upload_from_directory(env.cname, env.tmp_path) + artifacts.upload_from_directory(env.artifact_base_name, env.tmp_path) # Assert bucket = env.s3.Bucket(env.bucket_name) meta_obj = next( - o for o in bucket.objects.all() if o.key == f"meta/singles/{env.cname}" + o + for o in bucket.objects.all() + if o.key == f"meta/singles/{env.artifact_base_name}" ) metadata = yaml.safe_load(meta_obj.get()["Body"].read()) assert metadata["platform_variant"] == "test" @@ -283,21 +313,23 @@ def test_upload_directory_with_requirements_override(s3_setup: S3Env) -> None: """Ensure .requirements file values overide feature flag defaults.""" # Arrange env = s3_setup - (env.tmp_path / f"{env.cname}.release").write_text(RELEASE_DATA) - (env.tmp_path / f"{env.cname}.requirements").write_text( + (env.tmp_path / f"{env.artifact_base_name}.release").write_text(RELEASE_DATA) + (env.tmp_path / f"{env.artifact_base_name}.requirements").write_text( "uefi = false\nsecureboot = true\n" ) - artifact_file = env.tmp_path / f"{env.cname}-artifact" + artifact_file = env.tmp_path / f"{env.artifact_base_name}-artifact" artifact_file.write_bytes(b"abc") # Act artifacts = S3Artifacts(env.bucket_name) - artifacts.upload_from_directory(env.cname, env.tmp_path) + artifacts.upload_from_directory(env.artifact_base_name, env.tmp_path) # Assert bucket = env.s3.Bucket(env.bucket_name) meta_obj = next( - o for o in bucket.objects.all() if o.key == f"meta/singles/{env.cname}" + o + for o in bucket.objects.all() + if o.key == f"meta/singles/{env.artifact_base_name}" ) metadata = yaml.safe_load(meta_obj.get()["Body"].read()) assert metadata["require_uefi"] is False @@ -308,20 +340,22 @@ def test_upload_directory_with_publishing_group(s3_setup: S3Env) -> None: """A publishing_group value in the .requirements file is copied into the metadata.""" # Arrange env = s3_setup - (env.tmp_path / f"{env.cname}.release").write_text(RELEASE_DATA) - (env.tmp_path / f"{env.cname}.requirements").write_text( + (env.tmp_path / f"{env.artifact_base_name}.release").write_text(RELEASE_DATA) + (env.tmp_path / f"{env.artifact_base_name}.requirements").write_text( "arch = amd64\npublishing_group = container\n" ) - (env.tmp_path / f"{env.cname}-artifact").write_bytes(b"abc") + (env.tmp_path / f"{env.artifact_base_name}-artifact").write_bytes(b"abc") # Act artifacts = S3Artifacts(env.bucket_name) - artifacts.upload_from_directory(env.cname, env.tmp_path) + artifacts.upload_from_directory(env.artifact_base_name, env.tmp_path) # Assert bucket = env.s3.Bucket(env.bucket_name) meta_obj = next( - o for o in bucket.objects.all() if o.key == f"meta/singles/{env.cname}" + o + for o in bucket.objects.all() + if o.key == f"meta/singles/{env.artifact_base_name}" ) metadata = yaml.safe_load(meta_obj.get()["Body"].read()) assert metadata["publishing_group"] == "container" @@ -331,20 +365,22 @@ def test_upload_directory_without_publishing_group(s3_setup: S3Env) -> None: """An empty or absent publishing_group is not emitted, marking a standalone build.""" # Arrange env = s3_setup - (env.tmp_path / f"{env.cname}.release").write_text(RELEASE_DATA) - (env.tmp_path / f"{env.cname}.requirements").write_text( + (env.tmp_path / f"{env.artifact_base_name}.release").write_text(RELEASE_DATA) + (env.tmp_path / f"{env.artifact_base_name}.requirements").write_text( "arch = amd64\npublishing_group = \n" ) - (env.tmp_path / f"{env.cname}-artifact").write_bytes(b"abc") + (env.tmp_path / f"{env.artifact_base_name}-artifact").write_bytes(b"abc") # Act artifacts = S3Artifacts(env.bucket_name) - artifacts.upload_from_directory(env.cname, env.tmp_path) + artifacts.upload_from_directory(env.artifact_base_name, env.tmp_path) # Assert bucket = env.s3.Bucket(env.bucket_name) meta_obj = next( - o for o in bucket.objects.all() if o.key == f"meta/singles/{env.cname}" + o + for o in bucket.objects.all() + if o.key == f"meta/singles/{env.artifact_base_name}" ) metadata = yaml.safe_load(meta_obj.get()["Body"].read()) assert "publishing_group" not in metadata