Skip to content

Commit 5aa1cd6

Browse files
authored
[ISV-4971] Skip subset of static checks for FBC operators (#672)
1 parent e6d1d2d commit 5aa1cd6

File tree

6 files changed

+174
-42
lines changed

6 files changed

+174
-42
lines changed

operator-pipeline-images/operatorcert/static_tests/community/bundle.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from operator_repo.checks import CheckResult, Fail, Warn
1818
from operator_repo.utils import lookup_dict
1919
from operatorcert import utils
20+
from operatorcert.static_tests.helpers import skip_fbc
2021
from semver import Version
2122

2223
from .validations import (
@@ -179,6 +180,7 @@ def check_required_fields(bundle: Bundle) -> Iterator[CheckResult]:
179180
yield Fail(message) if fatal else Warn(message)
180181

181182

183+
@skip_fbc
182184
def check_dangling_bundles(bundle: Bundle) -> Iterator[CheckResult]:
183185
"""
184186
Check dangling bundles in the operator update graph
@@ -216,6 +218,7 @@ def check_dangling_bundles(bundle: Bundle) -> Iterator[CheckResult]:
216218
yield Fail(f"Channel {channel} has dangling bundles: {dangling_bundles}")
217219

218220

221+
@skip_fbc
219222
def check_api_version_constraints(bundle: Bundle) -> Iterator[CheckResult]:
220223
"""Check that the ocp and k8s api version constraints are consistent"""
221224
ocp_versions_str = bundle.annotations.get("com.redhat.openshift.versions")
@@ -297,6 +300,7 @@ def check_api_version_constraints(bundle: Bundle) -> Iterator[CheckResult]:
297300
)
298301

299302

303+
@skip_fbc
300304
def check_upgrade_graph_loop(bundle: Bundle) -> Iterator[CheckResult]:
301305
"""
302306
Detect loops in the upgrade graph
@@ -358,6 +362,7 @@ def follow_graph(graph: Any, bundle: Bundle, visited: List[Bundle]) -> List[Bund
358362
return visited
359363

360364

365+
@skip_fbc
361366
def check_replaces_availability(bundle: Bundle) -> Iterator[CheckResult]:
362367
"""
363368
Check if the current bundle and the replaced bundle support the same OCP versions

operator-pipeline-images/operatorcert/static_tests/community/operator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010

1111
from operator_repo import Operator
1212
from operator_repo.checks import CheckResult, Fail, Warn
13+
from operatorcert.static_tests.helpers import skip_fbc
1314

1415

16+
@skip_fbc
1517
def check_operator_name_unique(operator: Operator) -> Iterator[CheckResult]:
1618
"""Ensure all operator's bundles use the same operator name in their CSV"""
1719
names = {bundle.csv_operator_name for bundle in operator}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
""" Static tests helper utilities. """
2+
3+
import logging
4+
5+
from functools import wraps
6+
from typing import Any, Callable, Iterator
7+
from operator_repo import Operator, Bundle
8+
9+
10+
LOGGER = logging.getLogger("operator-cert")
11+
12+
13+
def skip_fbc(func: Callable[..., Any]) -> Callable[..., Any]:
14+
"""
15+
Decorator to skip a static check for FBC enabled operators.
16+
17+
First argument of the decorated function should be either an Operator or a Bundle,
18+
otherwise the 'check' will be executed as usual.
19+
"""
20+
21+
@wraps(func)
22+
def wrapper(*args: Any, **kwargs: Any) -> Iterator[Any]:
23+
first_arg = args[0]
24+
if isinstance(first_arg, Bundle):
25+
operator = first_arg.operator
26+
elif isinstance(first_arg, Operator):
27+
operator = first_arg
28+
29+
config = operator.config if operator else {}
30+
if not config.get("fbc", {}).get("enabled", False):
31+
yield from func(*args, **kwargs)
32+
33+
LOGGER.info(
34+
"Skipping %s for FBC enabled operator %s",
35+
func.__name__,
36+
operator.operator_name,
37+
)
38+
yield from []
39+
40+
return wrapper

operator-pipeline-images/operatorcert/static_tests/isv/bundle.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from operator_repo import Bundle
66
from operator_repo.checks import CheckResult, Fail, Warn
7+
from operatorcert.static_tests.helpers import skip_fbc
78

89
PRUNED_GRAPH_ERROR = (
910
"olm.skipRange annotation is set but replaces field is not set in the CSV. "
@@ -15,6 +16,7 @@
1516
)
1617

1718

19+
@skip_fbc
1820
def check_pruned_graph(bundle: Bundle) -> Iterator[CheckResult]:
1921
"""
2022
Check if the update graph is pruned when the bundle is added to the index

operator-pipeline-images/tests/static_tests/community/test_bundle.py

Lines changed: 74 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import re
22
from pathlib import Path
33
from typing import Any, Dict, List, Optional, Set
4-
from unittest.mock import MagicMock, patch
4+
from unittest.mock import MagicMock, patch, PropertyMock
55

66
import pytest
77
from operator_repo import Repo
@@ -291,9 +291,7 @@ def test_metadata_container_image_validation(
291291
assert expected_successes.intersection(collected_results.keys()) == set()
292292

293293

294-
@patch("operator_repo.core.Operator.config")
295-
def test_check_dangling_bundles(mock_config: MagicMock, tmp_path: Path) -> None:
296-
mock_config.get.return_value = "replaces-mode"
294+
def test_check_dangling_bundles(tmp_path: Path) -> None:
297295
create_files(
298296
tmp_path,
299297
bundle_files("hello", "0.0.1"),
@@ -303,16 +301,23 @@ def test_check_dangling_bundles(mock_config: MagicMock, tmp_path: Path) -> None:
303301
repo = Repo(tmp_path)
304302
operator = repo.operator("hello")
305303
bundle3 = operator.bundle("0.0.3")
306-
failures = list(check_dangling_bundles(bundle3))
307-
assert failures == []
308304

309-
mock_config.get.return_value = "unknown-mode"
310-
is_loop = list(check_dangling_bundles(bundle3))
311-
assert is_loop == [
312-
Fail("Operator(hello): unsupported updateGraph value: unknown-mode")
313-
]
305+
with patch.object(
306+
type(operator), "config", new_callable=PropertyMock
307+
) as mock_config:
308+
mock_config.return_value = {"updateGraph": "replaces-mode"}
309+
failures = list(check_dangling_bundles(bundle3))
310+
assert failures == []
311+
312+
with patch.object(
313+
type(operator), "config", new_callable=PropertyMock
314+
) as mock_config:
315+
mock_config.return_value = {"updateGraph": "unknown-mode"}
316+
is_loop = list(check_dangling_bundles(bundle3))
317+
assert is_loop == [
318+
Fail("Operator(hello): unsupported updateGraph value: unknown-mode")
319+
]
314320

315-
mock_config.get.return_value = "replaces-mode"
316321
# Bundle 0.0.2 is not referenced by any bundle and it is not a HEAD of channel
317322
create_files(
318323
tmp_path,
@@ -323,11 +328,16 @@ def test_check_dangling_bundles(mock_config: MagicMock, tmp_path: Path) -> None:
323328
repo = Repo(tmp_path)
324329
operator = repo.operator("hello")
325330
bundle3 = operator.bundle("0.0.3")
326-
failures = list(check_dangling_bundles(bundle3))
327-
assert len(failures) == 1 and isinstance(failures[0], Fail)
328-
assert (
329-
failures[0].reason == "Channel beta has dangling bundles: {Bundle(hello/0.0.2)}"
330-
)
331+
with patch.object(
332+
type(operator), "config", new_callable=PropertyMock
333+
) as mock_config:
334+
mock_config.return_value = {"updateGraph": "replaces-mode"}
335+
failures = list(check_dangling_bundles(bundle3))
336+
assert len(failures) == 1 and isinstance(failures[0], Fail)
337+
assert (
338+
failures[0].reason
339+
== "Channel beta has dangling bundles: {Bundle(hello/0.0.2)}"
340+
)
331341

332342
# Malformed .spec.replaces
333343
create_files(
@@ -338,9 +348,16 @@ def test_check_dangling_bundles(mock_config: MagicMock, tmp_path: Path) -> None:
338348
repo = Repo(tmp_path)
339349
operator = repo.operator("malformed")
340350
bundle1 = operator.bundle("0.0.1")
341-
failures = list(check_dangling_bundles(bundle1))
342-
assert len(failures) == 1 and isinstance(failures[0], Fail)
343-
assert "Bundle(malformed/0.0.1) has invalid 'replaces' field:" in failures[0].reason
351+
with patch.object(
352+
type(operator), "config", new_callable=PropertyMock
353+
) as mock_config:
354+
mock_config.return_value = {"updateGraph": "replaces-mode"}
355+
failures = list(check_dangling_bundles(bundle1))
356+
assert len(failures) == 1 and isinstance(failures[0], Fail)
357+
assert (
358+
"Bundle(malformed/0.0.1) has invalid 'replaces' field:"
359+
in failures[0].reason
360+
)
344361

345362

346363
@pytest.mark.parametrize(
@@ -444,9 +461,7 @@ def test_check_api_version_constraints(
444461
assert set(check_api_version_constraints(bundle)) == expected
445462

446463

447-
@patch("operator_repo.core.Operator.config")
448-
def test_check_upgrade_graph_loop(mock_config: MagicMock, tmp_path: Path) -> None:
449-
mock_config.get.return_value = "replaces-mode"
464+
def test_check_upgrade_graph_loop(tmp_path: Path) -> None:
450465
create_files(
451466
tmp_path,
452467
bundle_files("hello", "0.0.1"),
@@ -456,16 +471,22 @@ def test_check_upgrade_graph_loop(mock_config: MagicMock, tmp_path: Path) -> Non
456471
repo = Repo(tmp_path)
457472
operator = repo.operator("hello")
458473
bundle = operator.bundle("0.0.1")
459-
is_loop = list(check_upgrade_graph_loop(bundle))
460-
assert is_loop == []
461-
462-
mock_config.get.return_value = "unknown-mode"
463-
is_loop = list(check_upgrade_graph_loop(bundle))
464-
assert is_loop == [
465-
Fail("Operator(hello): unsupported updateGraph value: unknown-mode")
466-
]
474+
with patch.object(
475+
type(operator), "config", new_callable=PropertyMock
476+
) as mock_config:
477+
mock_config.return_value = {"updateGraph": "replaces-mode"}
478+
is_loop = list(check_upgrade_graph_loop(bundle))
479+
assert is_loop == []
480+
481+
with patch.object(
482+
type(operator), "config", new_callable=PropertyMock
483+
) as mock_config:
484+
mock_config.return_value = {"updateGraph": "unknown-mode"}
485+
is_loop = list(check_upgrade_graph_loop(bundle))
486+
assert is_loop == [
487+
Fail("Operator(hello): unsupported updateGraph value: unknown-mode")
488+
]
467489

468-
mock_config.get.return_value = "replaces-mode"
469490
# Both bundles replace each other
470491
create_files(
471492
tmp_path,
@@ -476,13 +497,17 @@ def test_check_upgrade_graph_loop(mock_config: MagicMock, tmp_path: Path) -> Non
476497
repo = Repo(tmp_path)
477498
operator = repo.operator("hello")
478499
bundle = operator.bundle("0.0.1")
479-
is_loop = list(check_upgrade_graph_loop(bundle))
480-
assert len(is_loop) == 1 and isinstance(is_loop[0], Fail)
481-
assert (
482-
is_loop[0].reason
483-
== "Upgrade graph loop detected for bundle: [Bundle(hello/0.0.1), "
484-
"Bundle(hello/0.0.2), Bundle(hello/0.0.1)]"
485-
)
500+
with patch.object(
501+
type(operator), "config", new_callable=PropertyMock
502+
) as mock_config:
503+
mock_config.return_value = {"updateGraph": "replaces-mode"}
504+
is_loop = list(check_upgrade_graph_loop(bundle))
505+
assert len(is_loop) == 1 and isinstance(is_loop[0], Fail)
506+
assert (
507+
is_loop[0].reason
508+
== "Upgrade graph loop detected for bundle: [Bundle(hello/0.0.1), "
509+
"Bundle(hello/0.0.2), Bundle(hello/0.0.1)]"
510+
)
486511

487512
# Malformed .spec.replaces
488513
create_files(
@@ -493,9 +518,16 @@ def test_check_upgrade_graph_loop(mock_config: MagicMock, tmp_path: Path) -> Non
493518
repo = Repo(tmp_path)
494519
operator = repo.operator("malformed")
495520
bundle = operator.bundle("0.0.1")
496-
failures = list(check_upgrade_graph_loop(bundle))
497-
assert len(failures) == 1 and isinstance(failures[0], Fail)
498-
assert "Bundle(malformed/0.0.1) has invalid 'replaces' field:" in failures[0].reason
521+
with patch.object(
522+
type(operator), "config", new_callable=PropertyMock
523+
) as mock_config:
524+
mock_config.return_value = {"updateGraph": "replaces-mode"}
525+
failures = list(check_upgrade_graph_loop(bundle))
526+
assert len(failures) == 1 and isinstance(failures[0], Fail)
527+
assert (
528+
"Bundle(malformed/0.0.1) has invalid 'replaces' field:"
529+
in failures[0].reason
530+
)
499531

500532

501533
def test_check_replaces_availability_no_replaces(
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from typing import Any, Iterator
2+
3+
from operatorcert.static_tests.helpers import skip_fbc
4+
from operator_repo import Operator, Bundle
5+
from unittest.mock import call, MagicMock, patch
6+
7+
8+
@patch("operatorcert.static_tests.helpers.LOGGER")
9+
@patch.object(Operator, "probe", return_value=True)
10+
@patch.object(Bundle, "probe", return_value=True)
11+
def test_skip__fbc(
12+
mock_bundle: MagicMock, mock_operator: MagicMock, mock_logger: MagicMock
13+
) -> None:
14+
15+
@skip_fbc
16+
def check_bundle(bundle: Bundle) -> Iterator[str]:
17+
yield "processed"
18+
19+
@skip_fbc
20+
def check_operator(operator: Operator) -> Iterator[str]:
21+
yield "processed"
22+
23+
operator = Operator("test-operator")
24+
operator.config = {"fbc": {"enabled": True}}
25+
bundle = Bundle("test-bundle", operator)
26+
27+
assert list(check_bundle(bundle)) == []
28+
assert list(check_operator(operator)) == []
29+
30+
mock_logger.assert_has_calls(
31+
[
32+
call.info(
33+
"Skipping %s for FBC enabled operator %s",
34+
"check_bundle",
35+
"test-operator",
36+
),
37+
call.info(
38+
"Skipping %s for FBC enabled operator %s",
39+
"check_operator",
40+
"test-operator",
41+
),
42+
]
43+
)
44+
45+
operator.config = {"fbc": {"enabled": False}}
46+
assert list(check_bundle(bundle)) == ["processed"]
47+
assert list(check_operator(operator)) == ["processed"]
48+
49+
operator.config = {}
50+
assert list(check_bundle(bundle)) == ["processed"]
51+
assert list(check_operator(operator)) == ["processed"]

0 commit comments

Comments
 (0)