diff --git a/operator-pipeline-images/operatorcert/entrypoints/add_bundle_to_fbc.py b/operator-pipeline-images/operatorcert/entrypoints/add_bundle_to_fbc.py new file mode 100644 index 00000000..c6abbb7d --- /dev/null +++ b/operator-pipeline-images/operatorcert/entrypoints/add_bundle_to_fbc.py @@ -0,0 +1,412 @@ +""" +A module responsible for automated release of bundle to the FBC catalog. +""" + +import argparse +import logging +import os +from abc import ABC, abstractmethod +from typing import Any + +from operator_repo import Bundle, Operator, Repo +from operatorcert import utils +from operatorcert.logger import setup_logger +from ruamel.yaml import YAML + +LOGGER = logging.getLogger("operator-cert") + + +def setup_argparser() -> argparse.ArgumentParser: + """ + Setup argument parser + + Returns: + Any: Initialized argument parser + """ + parser = argparse.ArgumentParser(description="Inject a bundle into FBC.") + + parser.add_argument( + "--repo-path", + default=".", + help="Path to the repository with operators and catalogs", + ) + parser.add_argument( + "--bundle-pullspec", type=str, help="A pullspec of bundle image." + ) + parser.add_argument( + "--operator-name", + type=str, + help="A name of operator that is going to be modified.", + ) + parser.add_argument( + "--operator-version", + type=str, + help="A version of operator that is going to be modified.", + ) + + parser.add_argument("--verbose", action="store_true", help="Verbose output") + + return parser + + +class CatalogTemplate(ABC): + """ + Abstract class for base catalog template. + """ + + def __init__( + self, + operator: Operator, + template_type: str, + template_name: str, + catalog_names: list[str], + **_: Any, + ): + self.operator = operator + self.template_name = template_name + self.template_type = template_type + self.catalog_names = catalog_names + + self._template: dict[str, Any] = {} + self.template_path = ( + self.operator.root / "catalog-templates" / self.template_name + ) + + def exists(self) -> bool: + """ + Check if the template exists by template path. + + Returns: + bool: A boolean value indicating if the template exists. + """ + return os.path.exists(self.template_path) + + @property + def template(self) -> dict[str, Any]: + """ + Get the template content. Load it from the file if it's not loaded yet. + + Returns: + dict[str, Any]: A template object. + """ + if not self._template: + with open(self.template_path, "r", encoding="utf8") as f: + self._template = YAML().load(f) + return self._template + + def save(self) -> None: + """ + Save the template to the file. + """ + with open(self.template_path, "w", encoding="utf8") as f: + yaml = YAML() + yaml.explicit_start = True + yaml.dump(self.template, f) + + @abstractmethod + def create( + self, release_config: dict[str, Any], bundle_pullspec: str, bundle: Bundle + ) -> None: + """ + Abstract method to create a new catalog template. + """ + + @abstractmethod + def amend( + self, release_config: dict[str, Any], bundle_pullspec: str, bundle: Bundle + ) -> None: + """ + Abstract method to amend an existing catalog template. + """ + + def add_new_bundle( + self, release_config: dict[str, Any], bundle_pullspec: str, bundle: Bundle + ) -> None: + """ + Add a new bundle to the catalog template. + + Args: + release_config (dict[str, Any]): A release configuration for the bundle. + bundle_pullspec (str): A pullspec of the bundle image. + bundle (Bundle): A bundle object. + """ + if self.exists(): + self.amend(release_config, bundle_pullspec, bundle) + else: + self.create(release_config, bundle_pullspec, bundle) + + def render(self) -> None: + """ + Render the catalog from the template using opm. + """ + command = [ + "opm", + "alpha", + "render-template", + self.template_type, + "-o", + "yaml", + self.template_path, + ] + + response = utils.run_command(command) + for catalog in self.catalog_names: + LOGGER.info( + f"Rendering catalog '{catalog}' from template %s", self.template_path + ) + catalog_path = ( + self.operator.repo.root + / "catalogs" + / catalog + / self.operator.operator_name + ) + os.makedirs(catalog_path, exist_ok=True) + with open(catalog_path / "catalog.yaml", "w", encoding="utf8") as f: + f.write(response.stdout.decode("utf-8")) + + +class BasicTemplate(CatalogTemplate): + """ + Class for basic catalog template. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + kwargs["template_type"] = "basic" + super().__init__(*args, **kwargs) + + @staticmethod + def create_basic_channel_entry( + release_config: dict[str, Any], bundle: Bundle + ) -> dict[str, Any]: + """ + Create a basic channel entry with a new bundle. + + Args: + release_config (dict[str, Any]): A release configuration for the bundle. + bundle (Bundle): A bundle object to create a channel entry for. + + Returns: + dict[str, Any]: A channel entry for the basic template. + """ + channel_entry = {"name": bundle.csv["metadata"]["name"]} + + if "replaces" in release_config: + channel_entry["replaces"] = release_config["replaces"] + if "skips" in release_config: + channel_entry["skips"] = release_config["skips"] + if "skipRange" in release_config: + channel_entry["skipRange"] = release_config["skipRange"] + return channel_entry + + def create( + self, release_config: dict[str, Any], bundle_pullspec: str, bundle: Bundle + ) -> None: + """ + Create a new basic template and add a new bundle to it based on + the release configuration. + + Args: + release_config (dict[str, Any]): A release configuration for the bundle. + bundle_pullspec (str): A pullspec of the bundle image. + bundle (Bundle): A bundle object to be added to the template. + """ + LOGGER.info("Creating a new basic template %s", self.template_name) + channels = [] + for channel in release_config["channels"]: + channels.append( + { + "schema": "olm.channel", + "name": channel, + "package": self.operator.operator_name, + "entries": [ + self.create_basic_channel_entry(release_config, bundle) + ], + } + ) + package = { + "schema": "olm.package", + "name": self.operator.operator_name, + "defaultChannel": channels[0]["name"], + } + bundle = {"schema": "olm.bundle", "image": bundle_pullspec} + + self._template = { + "schema": "olm.template.basic", + "entries": [package] + channels + [bundle], + } + + @staticmethod + def add_bundle_if_not_present( + template: dict[str, Any], bundle_pullspec: str + ) -> None: + """ + Add a bundle to the template if it's not present. + + Args: + template (dict[str, Any]): A template object. + bundle_pullspec (str): A pullspec of the bundle image. + """ + for item in template["entries"]: + if item["schema"] == "olm.bundle" and item["image"] == bundle_pullspec: + return + template["entries"].append( + { + "schema": "olm.bundle", + "image": bundle_pullspec, + } + ) + + def amend( + self, release_config: dict[str, Any], bundle_pullspec: str, bundle: Bundle + ) -> None: + """ + Amend the basic template by adding a new bundle to it. + + Args: + release_config (dict[str, Any]): A release configuration for the bundle. + bundle_pullspec (str): A pullspec of the bundle image. + bundle (Bundle): A bundle object to be added to the template. + """ + LOGGER.info("Amending basic template %s", self.template_name) + self.add_bundle_if_not_present(self.template, bundle_pullspec) + channels_names = release_config["channels"] + for item in self.template["entries"]: + if item["schema"] == "olm.channel" and item["name"] in channels_names: + new_entry = self.create_basic_channel_entry(release_config, bundle) + if new_entry not in item["entries"]: + item["entries"].append(new_entry) + + +class SemverTemplate(CatalogTemplate): + """ + Class for semver catalog template. + """ + + def __init__(self, *args: Any, **kwargs: Any): + kwargs["template_type"] = "semver" + super().__init__(*args, **kwargs) + + def create( + self, release_config: dict[str, Any], bundle_pullspec: str, _: Bundle + ) -> None: + """ + Create a new semver template and add a new bundle to it based on + the release configuration. + + Args: + release_config (dict[str, Any]): A release configuration for the bundle. + bundle_pullspec (str): A pullspec of the bundle image. + _ (Bundle): A bundle object to be added to the template. + """ + LOGGER.info("Creating a new semver template %s", self.template_name) + self._template = { + "Schema": "olm.semver", + "GenerateMajorChannels": True, + "GenerateMinorChannels": True, + } + for channel in release_config["channels"]: + self._template[channel] = {"Bundles": [{"Image": bundle_pullspec}]} + + def amend( + self, release_config: dict[str, Any], bundle_pullspec: str, _: Bundle + ) -> None: + """ + Amend the semver template by adding a new bundle to it. + + Args: + release_config (dic[str,Any]): A release configuration for the bundle. + bundle_pullspec (str): A pullspec of the bundle image. + _ (Bundle): A bundle object to be added to the template. + """ + LOGGER.info("Amending semver template %s", self.template_name) + for channel in release_config["channels"]: + if channel not in self.template: + self.template[channel] = {"Bundles": []} + new_bundle = {"Image": bundle_pullspec} + if new_bundle not in self.template[channel]["Bundles"]: + self.template[channel]["Bundles"].append({"Image": bundle_pullspec}) + + +def get_catalog_mapping(ci_file: dict[str, Any], template_name: str) -> Any: + """ + Get a catalog mapping for a specific template. + + Args: + ci_file (dict[str, Any]): A content of the ci.yaml file. + template_name (str): A name of the template to get a mapping for. + + Returns: + Optional[dict[str, Any]]: A catalog mapping for the template. + """ + fbc = ci_file.get("fbc", {}) + for mapping in fbc.get("catalog_mapping", []) or []: + if mapping["template_name"] == template_name: + return mapping + return None + + +def release_bundle_to_fbc(args: argparse.Namespace, bundle: Bundle) -> None: + """ + Release a new bundle to a FBC catalog templates and render the catalog with + a new changes. + + Args: + args (argparse.Namespace): CLI arguments. + bundle (Bundle): A bundle object to be released to FBC. + + Raises: + ValueError: An exception is raised if the template type is unknown. + """ + + if not bundle.release_config: + raise ValueError( + f"Release config not found for {args.operator_name} {args.operator_version}" + ) + + # Update a catalog template + for release_config_template in bundle.release_config["catalog_templates"]: + template_name = release_config_template["template_name"] + template_mapping = get_catalog_mapping(bundle.operator.config, template_name) + if not template_mapping: + raise ValueError( + f"Template mapping not found for '{template_name}' in ci.yaml." + ) + + template_class_map = { + "olm.template.basic": BasicTemplate, + "olm.semver": SemverTemplate, + } + template_class = template_class_map.get(template_mapping["type"]) + if not template_class: + raise ValueError( + f"Unknown template type '{template_mapping['type']}' in ci.yaml." + ) + + template: CatalogTemplate = template_class(bundle.operator, **template_mapping) + + template.add_new_bundle(release_config_template, args.bundle_pullspec, bundle) + template.save() + template.render() + + +def main() -> None: + """ + Main function for the cli tool. + """ + parser = setup_argparser() + args = parser.parse_args() + + # Logging + log_level = "INFO" + if args.verbose: + log_level = "DEBUG" + setup_logger(level=log_level) + + operator_repo = Repo(args.repo_path) + bundle = operator_repo.operator(args.operator_name).bundle(args.operator_version) + + release_bundle_to_fbc(args, bundle) + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/operator-pipeline-images/tests/entrypoints/test_add_bundle_to_fbc.py b/operator-pipeline-images/tests/entrypoints/test_add_bundle_to_fbc.py new file mode 100644 index 00000000..f12e1dea --- /dev/null +++ b/operator-pipeline-images/tests/entrypoints/test_add_bundle_to_fbc.py @@ -0,0 +1,413 @@ +from pathlib import Path +from unittest import mock +from unittest.mock import MagicMock, patch + +import pytest +from operatorcert.entrypoints import add_bundle_to_fbc + + +@pytest.fixture +def basic_catalog_template() -> add_bundle_to_fbc.BasicTemplate: + operator = MagicMock() + return add_bundle_to_fbc.BasicTemplate( + operator=operator, + template_type="fake-type", + template_name="fake-template.yaml", + catalog_names=["v4.12-fake"], + ) + + +@pytest.fixture +def semver_template() -> add_bundle_to_fbc.SemverTemplate: + operator = MagicMock() + return add_bundle_to_fbc.SemverTemplate( + operator=operator, + template_type="fake-type", + template_name="fake-template.yaml", + catalog_names=["v4.12-fake"], + ) + + +@patch("operatorcert.entrypoints.add_bundle_to_fbc.os.path.exists") +def test_BasicCatalogTemplate( + mock_exists: MagicMock, + basic_catalog_template: add_bundle_to_fbc.BasicTemplate, +) -> None: + mock_exists.return_value = False + assert basic_catalog_template.operator + assert basic_catalog_template.template_type == "basic" + assert basic_catalog_template.template_name == "fake-template.yaml" + assert basic_catalog_template.catalog_names == ["v4.12-fake"] + + assert basic_catalog_template.exists() is False + + +@patch("operatorcert.entrypoints.add_bundle_to_fbc.YAML") +def test_BasicCatalogTemplate_template( + mock_yaml: MagicMock, + basic_catalog_template: add_bundle_to_fbc.BasicTemplate, +) -> None: + mock_open = mock.mock_open() + mock_yaml.return_value.load.return_value = {"foo": "bar"} + with patch("builtins.open", mock_open): + template = basic_catalog_template.template + assert template == {"foo": "bar"} + + +@patch("operatorcert.entrypoints.add_bundle_to_fbc.YAML") +def test_BasicCatalogTemplate_save( + mock_yaml: MagicMock, + basic_catalog_template: add_bundle_to_fbc.BasicTemplate, +) -> None: + mock_open = mock.mock_open() + with patch("builtins.open", mock_open): + basic_catalog_template.save() + mock_yaml.return_value.dump.assert_called_once() + + +def test_BasicCatalogTemplate_create( + basic_catalog_template: add_bundle_to_fbc.BasicTemplate, +) -> None: + release_config = { + "channels": ["alpha"], + "replaces": "fake-replaces", + "skipRange": "fake-range", + "skips": "fake-skips", + } + bundle = MagicMock() + basic_catalog_template.operator.operator_name = "fake-operator" + bundle.csv = {"metadata": {"name": "fake-bundle"}} + basic_catalog_template.create(release_config, "quay.io/fake-bundle:1", bundle) + + expected_template = { + "entries": [ + { + "defaultChannel": "alpha", + "name": "fake-operator", + "schema": "olm.package", + }, + { + "entries": [ + { + "name": "fake-bundle", + "replaces": "fake-replaces", + "skipRange": "fake-range", + "skips": "fake-skips", + }, + ], + "name": "alpha", + "package": "fake-operator", + "schema": "olm.channel", + }, + { + "image": "quay.io/fake-bundle:1", + "schema": "olm.bundle", + }, + ], + "schema": "olm.template.basic", + } + assert basic_catalog_template.template == expected_template + + +def test_BasicCatalogTemplate_amend( + basic_catalog_template: add_bundle_to_fbc.BasicTemplate, +) -> None: + release_config = { + "channels": ["alpha"], + "replaces": "fake-replaces", + "skipRange": "fake-range", + "skips": "fake-skips", + } + basic_catalog_template._template = { + "entries": [ + { + "defaultChannel": "alpha", + "name": "fake-operator", + "schema": "olm.package", + }, + { + "entries": [ + { + "name": "fake-bundle", + }, + ], + "name": "alpha", + "package": "fake-operator", + "schema": "olm.channel", + }, + { + "image": "quay.io/fake-bundle:1", + "schema": "olm.bundle", + }, + ], + "schema": "olm.template.basic", + } + bundle = MagicMock() + basic_catalog_template.operator.operator_name = "fake-operator" + bundle.csv = {"metadata": {"name": "fake-bundle-2"}} + basic_catalog_template.amend(release_config, "quay.io/fake-bundle:2", bundle) + + expected_template = { + "entries": [ + { + "defaultChannel": "alpha", + "name": "fake-operator", + "schema": "olm.package", + }, + { + "entries": [ + { + "name": "fake-bundle", + }, + { + "name": "fake-bundle-2", + "replaces": "fake-replaces", + "skipRange": "fake-range", + "skips": "fake-skips", + }, + ], + "name": "alpha", + "package": "fake-operator", + "schema": "olm.channel", + }, + { + "image": "quay.io/fake-bundle:1", + "schema": "olm.bundle", + }, + { + "image": "quay.io/fake-bundle:2", + "schema": "olm.bundle", + }, + ], + "schema": "olm.template.basic", + } + assert basic_catalog_template.template == expected_template + + # Do it again to test that the bundle is not added twice + basic_catalog_template.amend(release_config, "quay.io/fake-bundle:2", bundle) + assert basic_catalog_template.template == expected_template + + +@patch("operatorcert.entrypoints.add_bundle_to_fbc.os.makedirs") +@patch("operatorcert.entrypoints.add_bundle_to_fbc.utils.run_command") +def test_BasicCatalogTemplate_render( + mock_run_command: MagicMock, + mock_mkdir: MagicMock, + basic_catalog_template: add_bundle_to_fbc.BasicTemplate, +) -> None: + mock_open = mock.mock_open() + basic_catalog_template.operator.operator_name = "fake-operator" + basic_catalog_template.operator.repo.root = Path("./") + basic_catalog_template.template_path = ( + "operators/fake-operator/catalog-templates/fake-template.yaml" + ) + + with patch("builtins.open", mock_open): + basic_catalog_template.render() + + mock_mkdir.assert_called_once_with( + Path("./") / "catalogs/v4.12-fake/fake-operator", exist_ok=True + ) + mock_open.assert_called_once_with( + Path("./") / "catalogs/v4.12-fake/fake-operator/catalog.yaml", + "w", + encoding="utf8", + ) + mock_run_command.assert_called_once_with( + [ + "opm", + "alpha", + "render-template", + "basic", + "-o", + "yaml", + "operators/fake-operator/catalog-templates/fake-template.yaml", + ] + ) + + +@patch("operatorcert.entrypoints.add_bundle_to_fbc.BasicTemplate.amend") +@patch("operatorcert.entrypoints.add_bundle_to_fbc.BasicTemplate.create") +@patch("operatorcert.entrypoints.add_bundle_to_fbc.BasicTemplate.exists") +def test_BasicCatalogTemplate_add_new_bundle( + mock_exists: MagicMock, + mock_create: MagicMock, + mock_amend: MagicMock, + basic_catalog_template: add_bundle_to_fbc.BasicTemplate, +) -> None: + + mock_exists.return_value = False + bundle = MagicMock() + basic_catalog_template.add_new_bundle({}, "fake-image", bundle) + mock_create.assert_called_once_with({}, "fake-image", bundle) + mock_amend.assert_not_called() + + mock_exists.return_value = True + mock_create.reset_mock() + + basic_catalog_template.add_new_bundle({}, "fake-image", bundle) + mock_create.assert_not_called() + mock_amend.assert_called_once_with({}, "fake-image", bundle) + + +def test_SemverTemplate( + semver_template: add_bundle_to_fbc.SemverTemplate, +) -> None: + assert semver_template.template_type == "semver" + + +def test_SemverTemplate_create( + semver_template: add_bundle_to_fbc.SemverTemplate, +) -> None: + release_config = { + "channels": ["Fast", "Candidate"], + } + bundle = MagicMock() + semver_template.create(release_config, "quay.io/fake-bundle:1", bundle) + + expected_template = { + "GenerateMajorChannels": True, + "GenerateMinorChannels": True, + "Schema": "olm.semver", + "Fast": {"Bundles": [{"Image": "quay.io/fake-bundle:1"}]}, + "Candidate": {"Bundles": [{"Image": "quay.io/fake-bundle:1"}]}, + } + assert semver_template.template == expected_template + + +def test_SemverTemplate_amend( + semver_template: add_bundle_to_fbc.SemverTemplate, +) -> None: + release_config = { + "channels": ["Fast", "Candidate"], + } + semver_template._template = { + "GenerateMajorChannels": True, + "GenerateMinorChannels": True, + "Schema": "olm.semver", + "Candidate": {"Bundles": [{"Image": "quay.io/fake-bundle:1"}]}, + } + bundle = MagicMock() + semver_template.amend(release_config, "quay.io/fake-bundle:2", bundle) + + expected_template = { + "GenerateMajorChannels": True, + "GenerateMinorChannels": True, + "Schema": "olm.semver", + "Fast": {"Bundles": [{"Image": "quay.io/fake-bundle:2"}]}, + "Candidate": { + "Bundles": [ + {"Image": "quay.io/fake-bundle:1"}, + {"Image": "quay.io/fake-bundle:2"}, + ], + }, + } + assert semver_template.template == expected_template + + # Do it again to test that the bundle is not added twice + semver_template.amend(release_config, "quay.io/fake-bundle:2", bundle) + assert semver_template.template == expected_template + + +def test_get_catalog_mapping() -> None: + ci_file = { + "fbc": { + "catalog_mapping": [ + { + "template_name": "fake-template.yaml", + "catalog_names": ["v4.12-fake"], + }, + ] + } + } + result = add_bundle_to_fbc.get_catalog_mapping(ci_file, "unknown") + assert result is None + + result = add_bundle_to_fbc.get_catalog_mapping(ci_file, "fake-template.yaml") + assert result == { + "template_name": "fake-template.yaml", + "catalog_names": ["v4.12-fake"], + } + + +@patch("operatorcert.entrypoints.add_bundle_to_fbc.SemverTemplate") +@patch("operatorcert.entrypoints.add_bundle_to_fbc.BasicTemplate") +@patch("operatorcert.entrypoints.add_bundle_to_fbc.get_catalog_mapping") +def test_release_bundle_to_fbc( + mock_catalog_mapping: MagicMock, mock_basic: MagicMock, mock_semver: MagicMock +) -> None: + bundle = MagicMock() + args = MagicMock() + + bundle.release_config = None + with pytest.raises(ValueError): + # release config is missing + add_bundle_to_fbc.release_bundle_to_fbc(args, bundle) + + bundle.release_config = { + "catalog_templates": [{"template_name": "fake-template.yaml"}] + } + mock_catalog_mapping.return_value = None + with pytest.raises(ValueError): + # A catalog mapping is missing + add_bundle_to_fbc.release_bundle_to_fbc(args, bundle) + + mock_catalog_mapping.return_value = { + "template_name": "fake-template.yaml", + "catalog_names": ["v4.12-fake"], + "type": "unknown", + } + with pytest.raises(ValueError): + # Unknown template type + add_bundle_to_fbc.release_bundle_to_fbc(args, bundle) + + bundle.release_config = { + "catalog_templates": [ + {"template_name": "fake-basic.yaml"}, + {"template_name": "fake-semver.yaml"}, + ] + } + mock_catalog_mapping.side_effect = [ + { + "template_name": "fake-basic.yaml", + "catalog_names": ["v4.12-fake"], + "type": "olm.template.basic", + }, + { + "template_name": "fake-semver.yaml", + "catalog_names": ["v4.13-fake"], + "type": "olm.semver", + }, + ] + add_bundle_to_fbc.release_bundle_to_fbc(args, bundle) + + mock_basic.assert_called_once() + mock_semver.assert_called_once() + + mock_basic.return_value.add_new_bundle.assert_called_once() + mock_semver.return_value.add_new_bundle.assert_called_once() + + mock_basic.return_value.save.assert_called_once() + mock_semver.return_value.save.assert_called_once() + + mock_basic.return_value.render.assert_called_once() + mock_semver.return_value.render.assert_called_once() + + +def test_setup_argparser() -> None: + parser = add_bundle_to_fbc.setup_argparser() + + assert parser + + +@patch("operatorcert.entrypoints.add_bundle_to_fbc.release_bundle_to_fbc") +@patch("operatorcert.entrypoints.add_bundle_to_fbc.Repo") +@patch("operatorcert.entrypoints.add_bundle_to_fbc.setup_argparser") +def test_main( + mock_arg_parser: MagicMock, mock_repo: MagicMock, mock_release_bundle: MagicMock +) -> None: + add_bundle_to_fbc.main() + + mock_arg_parser.assert_called_once() + mock_repo.assert_called_once() + mock_release_bundle.assert_called_once() diff --git a/pdm.lock b/pdm.lock index c41912ea..27deb775 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "operatorcert-dev", "tox"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:2c516ee6e786f2dfffd3f271d0fb7ffbc9e265d138b3f33bfdecb74b976e1e5e" +content_hash = "sha256:c3980d49c0faded3fcf7b7f57f48dd473c897ee117ad5f3a88c23712d297d684" [[metadata.targets]] requires_python = ">=3.10" @@ -1981,16 +1981,16 @@ files = [ [[package]] name = "ruamel-yaml" -version = "0.18.8" +version = "0.18.10" requires_python = ">=3.7" summary = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -groups = ["operatorcert-dev"] +groups = ["default", "operatorcert-dev"] dependencies = [ "ruamel-yaml-clib>=0.2.7; platform_python_implementation == \"CPython\" and python_version < \"3.13\"", ] files = [ - {file = "ruamel.yaml-0.18.8-py3-none-any.whl", hash = "sha256:a7c02af6ec9789495b4d19335addabc4d04ab1e0dad3e491c0c9457bbc881100"}, - {file = "ruamel.yaml-0.18.8.tar.gz", hash = "sha256:1b7e14f28a4b8d09f8cd40dca158852db9b22ac84f22da5bb711def35cb5c548"}, + {file = "ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1"}, + {file = "ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58"}, ] [[package]] @@ -1998,7 +1998,7 @@ name = "ruamel-yaml-clib" version = "0.2.12" requires_python = ">=3.9" summary = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -groups = ["operatorcert-dev"] +groups = ["default", "operatorcert-dev"] marker = "platform_python_implementation == \"CPython\" and python_version < \"3.13\"" files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, diff --git a/pyproject.toml b/pyproject.toml index 3153458e..7c64054b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,9 +35,11 @@ dependencies = [ "urllib3>=2.2.2", "openshift-client>=2.0.4", "pydantic>=2.10.0", + "ruamel-yaml>=0.18.10", ] [project.scripts] +add-bundle-to-fbc = "operatorcert.entrypoints.add_bundle_to_fbc:main" add-fbc-fragments-to-index = "operatorcert.entrypoints.add_fbc_fragments_to_index:main" apply-test-waivers = "operatorcert.entrypoints.apply_test_waivers:main" build-fragment-images = "operatorcert.entrypoints.build_fragment_images:main" diff --git a/tox.ini b/tox.ini index fd6335ef..0ff5fee9 100644 --- a/tox.ini +++ b/tox.ini @@ -26,6 +26,7 @@ commands = pytest -v \ --cov {[vars]OPERATOR_MODULE} \ --cov-report term-missing \ --cov-fail-under 100 \ + --cov-report json \ {posargs} [testenv:black] @@ -82,7 +83,7 @@ commands = pdm lock --check [testenv:hadolint] allowlist_externals = hadolint groups = dev -commands = hadolint -V --failure-threshold warning \ +commands = hadolint --failure-threshold warning \ --info DL3013 --info DL3041 \ operator-pipeline-images/Dockerfile