diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 5f8851e91..4e3864b24 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -23,9 +23,7 @@ jobs: - name: Install dependencies run: | - pdm sync -dG operator-pipelines-dev pdm sync -dG tox - name: Run Tests - run: | - pdm run -v tox + run: tox diff --git a/operator-pipeline-images/operatorcert/static_tests/community/bundle.py b/operator-pipeline-images/operatorcert/static_tests/community/bundle.py index bf2ae05d8..c1783a1ac 100644 --- a/operator-pipeline-images/operatorcert/static_tests/community/bundle.py +++ b/operator-pipeline-images/operatorcert/static_tests/community/bundle.py @@ -145,7 +145,11 @@ def check_dangling_bundles(bundle: Bundle) -> Iterator[CheckResult]: for channel in sorted(all_channels): channel_bundles = operator.channel_bundles(channel) channel_head = operator.head(channel) - graph = operator.update_graph(channel) + try: + graph = operator.update_graph(channel) + except NotImplementedError as exc: + yield Fail(str(exc)) + return dangling_bundles = { x for x in channel_bundles if x not in graph and x != channel_head } @@ -175,7 +179,11 @@ def check_upgrade_graph_loop(bundle: Bundle) -> Iterator[CheckResult]: visited: List[Bundle] = [] try: channel_bundles = operator.channel_bundles(channel) - graph = operator.update_graph(channel) + try: + graph = operator.update_graph(channel) + except NotImplementedError as exc: + yield Fail(str(exc)) + return follow_graph(graph, channel_bundles[0], visited) except GraphLoopException as exc: yield Fail(str(exc)) diff --git a/operator-pipeline-images/operatorcert/static_tests/community/operator.py b/operator-pipeline-images/operatorcert/static_tests/community/operator.py index a2fbde894..482e06627 100644 --- a/operator-pipeline-images/operatorcert/static_tests/community/operator.py +++ b/operator-pipeline-images/operatorcert/static_tests/community/operator.py @@ -9,7 +9,7 @@ from collections.abc import Iterator from operator_repo import Operator -from operator_repo.checks import CheckResult, Fail +from operator_repo.checks import CheckResult, Fail, Warn def check_operator_name(operator: Operator) -> Iterator[CheckResult]: @@ -17,3 +17,19 @@ def check_operator_name(operator: Operator) -> Iterator[CheckResult]: names = {bundle.csv_operator_name for bundle in operator} if len(names) > 1: yield Fail(f"Bundles use multiple operator names: {names}") + + +def check_ci_upgrade_graph(operator: Operator) -> Iterator[CheckResult]: + """Ensure the operator has a valid upgrade graph for ci.yaml""" + upgrade_graph = operator.config.get("updateGraph") + if not upgrade_graph: + yield Warn( + "The 'updateGraph' option is missing in ci.yaml. " + "The default upgrade graph 'replaces-mode' will be used." + ) + else: + allowed_graphs = ["replaces-mode", "semver-mode"] + if upgrade_graph not in allowed_graphs: + yield Fail( + f"The 'updateGraph' option in ci.yaml must be one of {allowed_graphs}" + ) diff --git a/operator-pipeline-images/tests/static_tests/community/test_bundle.py b/operator-pipeline-images/tests/static_tests/community/test_bundle.py index aa2ff1783..dd545b256 100644 --- a/operator-pipeline-images/tests/static_tests/community/test_bundle.py +++ b/operator-pipeline-images/tests/static_tests/community/test_bundle.py @@ -204,7 +204,9 @@ def test_required_fields( assert expected_successes.intersection(collected_results.keys()) == set() -def test_check_dangling_bundles(tmp_path: Path) -> None: +@patch("operator_repo.core.Operator.config") +def test_check_dangling_bundles(mock_config: MagicMock, tmp_path: Path) -> None: + mock_config.get.return_value = "replaces-mode" create_files( tmp_path, bundle_files("hello", "0.0.1"), @@ -217,6 +219,13 @@ def test_check_dangling_bundles(tmp_path: Path) -> None: failures = list(check_dangling_bundles(bundle3)) assert failures == [] + mock_config.get.return_value = "unknown-mode" + is_loop = list(check_dangling_bundles(bundle3)) + assert is_loop == [ + Fail("Operator(hello): unsupported updateGraph value: unknown-mode") + ] + + mock_config.get.return_value = "replaces-mode" # Bundle 0.0.2 is not referenced by any bundle and it is not a HEAD of channel create_files( tmp_path, @@ -234,7 +243,9 @@ def test_check_dangling_bundles(tmp_path: Path) -> None: ) -def test_check_upgrade_graph_loop(tmp_path: Path) -> None: +@patch("operator_repo.core.Operator.config") +def test_check_upgrade_graph_loop(mock_config: MagicMock, tmp_path: Path) -> None: + mock_config.get.return_value = "replaces-mode" create_files( tmp_path, bundle_files("hello", "0.0.1"), @@ -247,6 +258,13 @@ def test_check_upgrade_graph_loop(tmp_path: Path) -> None: is_loop = list(check_upgrade_graph_loop(bundle)) assert is_loop == [] + mock_config.get.return_value = "unknown-mode" + is_loop = list(check_upgrade_graph_loop(bundle)) + assert is_loop == [ + Fail("Operator(hello): unsupported updateGraph value: unknown-mode") + ] + + mock_config.get.return_value = "replaces-mode" # Both bundles replace each other create_files( tmp_path, diff --git a/operator-pipeline-images/tests/static_tests/community/test_operator.py b/operator-pipeline-images/tests/static_tests/community/test_operator.py index 71a61362c..9ea63d1c4 100644 --- a/operator-pipeline-images/tests/static_tests/community/test_operator.py +++ b/operator-pipeline-images/tests/static_tests/community/test_operator.py @@ -1,9 +1,15 @@ from pathlib import Path +from typing import Any +import pytest +import yaml from operator_repo import Repo - -from operatorcert.static_tests.community.operator import check_operator_name -from tests.utils import create_files, bundle_files +from operator_repo.checks import Fail, Warn +from operatorcert.static_tests.community.operator import ( + check_ci_upgrade_graph, + check_operator_name, +) +from tests.utils import bundle_files, create_files def test_check_operator_name(tmp_path: Path) -> None: @@ -20,3 +26,54 @@ def test_check_operator_name(tmp_path: Path) -> None: ), ) assert [x.kind for x in check_operator_name(operator)] == ["failure"] + + +@pytest.mark.parametrize( + "graph_mode, expected", + [ + pytest.param( + "", + { + Warn( + "The 'updateGraph' option is missing in ci.yaml. The default upgrade graph 'replaces-mode' will be used." + ) + }, + id="empty", + ), + pytest.param( + "replaces-mode", + set(), + id="replaces", + ), + pytest.param( + "semver-mode", + set(), + id="semver", + ), + pytest.param( + "unknown-mode", + { + Fail( + "The 'updateGraph' option in ci.yaml must be one of ['replaces-mode', 'semver-mode']", + ) + }, + id="unknown", + ), + ], +) +def test_check_ci_upgrade_graph(graph_mode: str, expected: Any, tmp_path: Path) -> None: + create_files( + tmp_path, + bundle_files( + "test-operator", + "0.0.1", + other_files={ + "operators/test-operator/ci.yaml": {"updateGraph": graph_mode} + }, + ), + ) + repo = Repo(tmp_path) + operator = repo.operator("test-operator") + result = check_ci_upgrade_graph(operator) + + assert set(result) == expected