diff --git a/docs/users/static_checks.md b/docs/users/static_checks.md index 26dca7504..ed55c6090 100644 --- a/docs/users/static_checks.md +++ b/docs/users/static_checks.md @@ -108,6 +108,7 @@ The test aims to verify if a bundle referenced by the `replaces` value is availa catalog version where the given bundle is going to be released to. The list of catalog version is determined by the `com.redhat.openshift.versions` annotation if present. If the annotation is not present the bundle targets all supported ocp version. +It also check that specified bundle version exists in operator directory structure. To fix the issue either change a range of versions where a bundle is going to be released by updating the annotation or change the `replaces` value. diff --git a/operatorcert/static_tests/common/bundle.py b/operatorcert/static_tests/common/bundle.py index 909381946..edc656a53 100644 --- a/operatorcert/static_tests/common/bundle.py +++ b/operatorcert/static_tests/common/bundle.py @@ -6,8 +6,10 @@ from typing import Any from jsonschema.validators import Draft202012Validator +from operatorcert import utils from operatorcert.operator_repo import Bundle from operatorcert.operator_repo.checks import CheckResult, Fail, Warn +from operatorcert.static_tests.helpers import skip_fbc def _check_consistency( @@ -225,3 +227,63 @@ def check_operator_version_directory_name(bundle: Bundle) -> Iterator[CheckResul f"the expected operator CSV version '{csv_version}' from " f"./{bundle.csv_file_name.relative_to(bundle.operator.repo.root)}." ) + + +@skip_fbc +def check_replaces_availability(bundle: Bundle) -> Iterator[CheckResult]: + """ + Check if the current bundle and the replaced bundle support the same OCP versions + + Args: + bundle (Bundle): Operator bundle + + Yields: + Iterator[CheckResult]: Failure if the version of the replaced bundle + does not match with the current bundle + """ + + replaces = bundle.csv.get("spec", {}).get("replaces") + if not replaces: + return + delimiter = ".v" if ".v" in replaces else "." + replaces_version = replaces.split(delimiter, 1)[1] + + ver_to_dir = { + x.csv_operator_version: x.operator_version + for x in bundle.operator.all_bundles() + } + + if replaces_version not in ver_to_dir: + yield Fail( + f"{bundle} attempts to replace version '{replaces_version}' which" + f" does not exist. Available versions: {sorted(ver_to_dir.keys())}" + ) + return + + replaces_bundle = bundle.operator.bundle(ver_to_dir[replaces_version]) + + ocp_versions_str = bundle.annotations.get("com.redhat.openshift.versions") + replaces_ocp_version_str = replaces_bundle.annotations.get( + "com.redhat.openshift.versions" + ) + if ocp_versions_str == replaces_ocp_version_str: + return + organization = bundle.operator.repo.config.get("organization") + + indexes = set(utils.get_ocp_supported_versions(organization, ocp_versions_str)) + replaces_indexes = set( + utils.get_ocp_supported_versions(organization, replaces_ocp_version_str) + ) + + if indexes - replaces_indexes == set(): + return + yield Fail( + f"Replaces bundle {replaces_bundle} {sorted(replaces_indexes)} does not support " + f"the same OCP versions as bundle {bundle} {sorted(indexes)}. In order to fix this issue, " + "align the OCP version range to match the range of the replaced bundle. " + "This can be done by setting the `com.redhat.openshift.versions` annotation in the " + "`metadata/annotations.yaml` file.\n" + f"`{bundle}` - `{ocp_versions_str}`\n" + f"`{replaces_bundle}` - `{replaces_ocp_version_str}`" + ) + yield from [] diff --git a/operatorcert/static_tests/community/bundle.py b/operatorcert/static_tests/community/bundle.py index 516c0e040..5b00c8403 100644 --- a/operatorcert/static_tests/community/bundle.py +++ b/operatorcert/static_tests/community/bundle.py @@ -321,60 +321,6 @@ def check_api_version_constraints(bundle: Bundle) -> Iterator[CheckResult]: ) -@skip_fbc -def check_replaces_availability(bundle: Bundle) -> Iterator[CheckResult]: - """ - Check if the current bundle and the replaced bundle support the same OCP versions - - Args: - bundle (Bundle): Operator bundle - - Yields: - Iterator[CheckResult]: Failure if the version of the replaced bundle - does not match with the current bundle - """ - - replaces = bundle.csv.get("spec", {}).get("replaces") - if not replaces: - return - delimiter = ".v" if ".v" in replaces else "." - replaces_version = replaces.split(delimiter, 1)[1] - - ver_to_dir = { - x.csv_operator_version: x.operator_version - for x in bundle.operator.all_bundles() - } - replaces_bundle = bundle.operator.bundle(ver_to_dir[replaces_version]) - - ocp_versions_str = bundle.annotations.get("com.redhat.openshift.versions") - replaces_ocp_version_str = replaces_bundle.annotations.get( - "com.redhat.openshift.versions" - ) - if ocp_versions_str == replaces_ocp_version_str: - # The annotations match, no need to check further - return - organization = bundle.operator.repo.config.get("organization") - - indexes = set(utils.get_ocp_supported_versions(organization, ocp_versions_str)) - replaces_indexes = set( - utils.get_ocp_supported_versions(organization, replaces_ocp_version_str) - ) - - if indexes - replaces_indexes == set(): - # The replaces bundle supports all the same versions as the current bundle - return - yield Fail( - f"Replaces bundle {replaces_bundle} {sorted(replaces_indexes)} does not support " - f"the same OCP versions as bundle {bundle} {sorted(indexes)}. In order to fix this issue, " - "align the OCP version range to match the range of the replaced bundle. " - "This can be done by setting the `com.redhat.openshift.versions` annotation in the " - "`metadata/annotations.yaml` file.\n" - f"`{bundle}` - `{ocp_versions_str}`\n" - f"`{replaces_bundle}` - `{replaces_ocp_version_str}`" - ) - yield from [] - - NON_FBC_SUGGESTION = ( "[File Based Catalog (FBC)]" "(https://github.com/redhat-openshift-ecosystem/community-operators-prod/" diff --git a/tests/static_tests/common/test_bundle.py b/tests/static_tests/common/test_bundle.py index 74b47b24c..bda75f1dd 100644 --- a/tests/static_tests/common/test_bundle.py +++ b/tests/static_tests/common/test_bundle.py @@ -1,5 +1,6 @@ from pathlib import Path -from typing import Any +from typing import Any, Optional +from unittest.mock import MagicMock, patch import pytest from operatorcert.operator_repo import Repo @@ -10,6 +11,7 @@ check_validate_schema_bundle_release_config, check_network_policy_presence, check_operator_version_directory_name, + check_replaces_availability, ) from tests.utils import bundle_files, create_files @@ -752,3 +754,91 @@ def test_check_operator_version_directory_name( assert { (x.__class__, x.reason) for x in check_operator_version_directory_name(bundle) } == expected_results + + +@pytest.mark.parametrize( + "bundle_version_annotation,replaces_version_annotation,replaces_csv_value,ocp_range,expected", + [ + pytest.param(None, None, None, [], set(), id="No replaces"), + pytest.param(None, None, "hello.v0.0.1", [], set(), id="No annotations"), + pytest.param( + "v4.10", "v4.10", "hello.v0.0.1", [], set(), id="Same annotations" + ), + pytest.param( + "v4.15", + "v4.15,v4.16", + "hello.v0.0.1", + [["v4.15", "v4.16"], ["v4.15", "v4.16"]], + set(), + id="Different annotation, versions match", + ), + pytest.param( + "v4.15", + "v4.16", + "hello.v0.0.1", + [["v4.15", "v4.16"], ["v4.16"]], + { + Fail( + "Replaces bundle Bundle(hello/0.0.1) ['v4.16'] does not support " + "the same OCP versions as bundle Bundle(hello/0.0.2) ['v4.15', 'v4.16']. " + "In order to fix this issue, align the OCP version range to match the " + "range of the replaced bundle. " + "This can be done by setting the `com.redhat.openshift.versions` annotation " + "in the `metadata/annotations.yaml` file.\n" + "`Bundle(hello/0.0.2)` - `v4.15`\n" + "`Bundle(hello/0.0.1)` - `v4.16`" + ) + }, + id="Different annotation, different version", + ), + pytest.param( + None, + None, + "hello.v0.0.5", + [], + { + Fail( + "Bundle(hello/0.0.2) attempts to replace version '0.0.5' which" + " does not exist. Available versions: ['0.0.1', '0.0.2']" + ) + }, + id="Nonexistent replaces version", + ), + ], +) +@patch("operatorcert.static_tests.common.bundle.utils.get_ocp_supported_versions") +def test_check_replaces_availability( + mock_get_ocp_supported_versions: MagicMock, + bundle_version_annotation: str, + replaces_version_annotation: str, + replaces_csv_value: Optional[str], + ocp_range: Any, + expected: Any, + tmp_path: Path, +) -> None: + bundle_annotation = { + "com.redhat.openshift.versions": bundle_version_annotation, + } + replaces_bundle_annotation = { + "com.redhat.openshift.versions": replaces_version_annotation, + } + csv = {"spec": {"replaces": replaces_csv_value}} if replaces_csv_value else {} + create_files( + tmp_path, + bundle_files("hello", "0.0.1", annotations=replaces_bundle_annotation), + bundle_files( + "hello", + "0.0.2", + annotations=bundle_annotation, + csv=csv, + ), + ) + + mock_get_ocp_supported_versions.side_effect = ocp_range + + repo = Repo(tmp_path) + operator = repo.operator("hello") + bundle = operator.bundle("0.0.2") + errors = list(check_replaces_availability(bundle)) + + assert set(errors) == expected diff --git a/tests/static_tests/community/test_bundle.py b/tests/static_tests/community/test_bundle.py index d1d1d4b42..c64510e6b 100644 --- a/tests/static_tests/community/test_bundle.py +++ b/tests/static_tests/community/test_bundle.py @@ -11,7 +11,6 @@ check_dangling_bundles, check_osdk_bundle_validate_operator_framework, check_osdk_bundle_validate_operatorhub, - check_replaces_availability, check_required_fields, check_using_fbc, ocp_to_k8s_ver, @@ -511,101 +510,6 @@ def test_check_api_version_constraints( assert set(check_api_version_constraints(bundle)) == expected -def test_check_replaces_availability_no_replaces( - tmp_path: Path, -) -> None: - bundle_annotation = { - "com.redhat.openshift.versions": "v4.10", - } - replaces_bundle_annotation = { - "com.redhat.openshift.versions": "v4.11", - } - create_files( - tmp_path, - bundle_files("hello", "0.0.1", annotations=replaces_bundle_annotation), - bundle_files( - "hello", - "0.0.2", - annotations=bundle_annotation, - ), - ) - - repo = Repo(tmp_path) - operator = repo.operator("hello") - bundle = operator.bundle("0.0.2") - errors = list(check_replaces_availability(bundle)) - - assert set(errors) == set() - - -@pytest.mark.parametrize( - "bundle_version_annotation,replaces_version_annotation,ocp_range,expected", - [ - pytest.param(None, None, [], set(), id="No annotations"), - pytest.param("v4.10", "v4.10", [], set(), id="Same annotations"), - pytest.param( - "v4.15", - "v4.15,v4.16", - [["v4.15", "v4.16"], ["v4.15", "v4.16"]], - set(), - id="Different annotation, versions match", - ), - pytest.param( - "v4.15", - "v4.16", - [["v4.15", "v4.16"], ["v4.16"]], - { - Fail( - "Replaces bundle Bundle(hello/0.0.1) ['v4.16'] does not support " - "the same OCP versions as bundle Bundle(hello/0.0.2) ['v4.15', 'v4.16']. " - "In order to fix this issue, align the OCP version range to match the " - "range of the replaced bundle. " - "This can be done by setting the `com.redhat.openshift.versions` annotation " - "in the `metadata/annotations.yaml` file.\n" - "`Bundle(hello/0.0.2)` - `v4.15`\n" - "`Bundle(hello/0.0.1)` - `v4.16`" - ) - }, - id="Different annotation, different version", - ), - ], -) -@patch("operatorcert.static_tests.community.bundle.utils.get_ocp_supported_versions") -def test_check_replaces_availability( - mock_get_ocp_supported_versions: MagicMock, - bundle_version_annotation: str, - replaces_version_annotation: str, - ocp_range: Any, - expected: Any, - tmp_path: Path, -) -> None: - bundle_annotation = { - "com.redhat.openshift.versions": bundle_version_annotation, - } - replaces_bundle_annotation = { - "com.redhat.openshift.versions": replaces_version_annotation, - } - create_files( - tmp_path, - bundle_files("hello", "0.0.1", annotations=replaces_bundle_annotation), - bundle_files( - "hello", - "0.0.2", - annotations=bundle_annotation, - csv={"spec": {"replaces": "hello.v0.0.1"}}, - ), - ) - - mock_get_ocp_supported_versions.side_effect = ocp_range - - repo = Repo(tmp_path) - operator = repo.operator("hello") - bundle = operator.bundle("0.0.2") - errors = list(check_replaces_availability(bundle)) - - assert set(errors) == expected - - @pytest.mark.parametrize( "files, bundle_to_check, expected", [