Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/users/static_checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
62 changes: 62 additions & 0 deletions operatorcert/static_tests/common/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 []
54 changes: 0 additions & 54 deletions operatorcert/static_tests/community/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
Expand Down
92 changes: 91 additions & 1 deletion tests/static_tests/common/test_bundle.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameters are not easy to understand from their names. What is annotated in the new operator version and what was already present in the repo?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests were just moved from the previous version from community static-tests, but I can update parameters naming.

[
pytest.param(None, None, None, [], set(), id="No replaces"),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this case really return no failure? What is tested here? Is the added bundle valid by other validation standards?

Copy link
Copy Markdown
Contributor Author

@RichardPlesnik RichardPlesnik Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was moved form the community validation.

Replaces field is not required (and not present in check_required_fields validation). It is valid to omit this field when there is no previous version of the bundle in the channel. In other cases, this would cause dangling bundle and the issue would be caught by check_dangling_bundles static check.

pytest.param(None, None, "hello.v0.0.1", [], set(), id="No annotations"),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it ok to replace an obsolete version? This would create 2 channel heads in reality, no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was moved form the community validation.

It is not replacing obsolete version. Annotations field is not required and when it is missing, it uses all available OCP versions.

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
96 changes: 0 additions & 96 deletions tests/static_tests/community/test_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
[
Expand Down
Loading