From ca6737a1aeb41016e37a44388f3260a76086b6e2 Mon Sep 17 00:00:00 2001 From: goodguypeci-design <247882324+goodguypeci-design@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:43:52 +0300 Subject: [PATCH 1/3] Add MariaDB Operator compatibility scraper --- static/compatibilities/manifest.yaml | 1 + static/compatibilities/mariadb-operator.yaml | 95 ++++++++++ .../scrapers/mariadb-operator.py | 178 ++++++++++++++++++ .../tests/test_mariadb_operator.py | 139 ++++++++++++++ 4 files changed, 413 insertions(+) create mode 100644 static/compatibilities/mariadb-operator.yaml create mode 100644 utils/compatibility/scrapers/mariadb-operator.py create mode 100644 utils/compatibility/tests/test_mariadb_operator.py diff --git a/static/compatibilities/manifest.yaml b/static/compatibilities/manifest.yaml index 13378e1e2f..088eb79446 100644 --- a/static/compatibilities/manifest.yaml +++ b/static/compatibilities/manifest.yaml @@ -53,3 +53,4 @@ names: - wiz-network-analyzer - kuberay-operator - mongodb-kubernetes +- mariadb-operator diff --git a/static/compatibilities/mariadb-operator.yaml b/static/compatibilities/mariadb-operator.yaml new file mode 100644 index 0000000000..e208d79ce2 --- /dev/null +++ b/static/compatibilities/mariadb-operator.yaml @@ -0,0 +1,95 @@ +icon: https://mariadb-operator.github.io/mariadb-operator/assets/mariadb_profile.svg +git_url: https://github.com/mariadb-operator/mariadb-operator +release_url: https://github.com/mariadb-operator/mariadb-operator/releases/tag/{vsn} +helm_repository_url: https://mariadb-operator.github.io/mariadb-operator +chart_name: mariadb-operator +versions: +- version: 26.6.0 + kube: ['1.36', '1.35', '1.34', '1.33', '1.32', '1.31', '1.30', '1.29', '1.28', '1.27', + '1.26'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 26.6.0 + images: ['ghcr.io/mariadb-operator/mariadb-operator:26.6.0'] +- version: 26.3.0 + kube: ['1.36', '1.35', '1.34', '1.33', '1.32', '1.31', '1.30', '1.29', '1.28', '1.27', + '1.26'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 26.3.0 + images: ['docker-registry3.mariadb.com/mariadb-operator/mariadb-operator:26.3.0'] +- version: 25.10.4 + kube: ['1.36', '1.35', '1.34', '1.33', '1.32', '1.31', '1.30', '1.29', '1.28', '1.27', + '1.26'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 25.10.4 + images: ['docker-registry3.mariadb.com/mariadb-operator/mariadb-operator:25.10.4'] +- version: 25.8.4 + kube: ['1.36', '1.35', '1.34', '1.33', '1.32', '1.31', '1.30', '1.29', '1.28', '1.27', + '1.26'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 25.8.4 + images: ['docker-registry3.mariadb.com/mariadb-operator/mariadb-operator:25.8.4'] +- version: 0.38.1 + kube: ['1.36', '1.35', '1.34', '1.33', '1.32', '1.31', '1.30', '1.29', '1.28', '1.27', + '1.26'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 0.38.1 + images: ['docker-registry3.mariadb.com/mariadb-operator/mariadb-operator:0.38.1'] +- version: 0.37.1 + kube: ['1.36', '1.35', '1.34', '1.33', '1.32', '1.31', '1.30', '1.29', '1.28', '1.27', + '1.26'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 0.37.1 + images: ['docker-registry3.mariadb.com/mariadb-operator/mariadb-operator:0.37.1'] +- version: 0.36.0 + kube: ['1.36', '1.35', '1.34', '1.33', '1.32', '1.31', '1.30', '1.29', '1.28', '1.27', + '1.26'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 0.36.0 + images: ['docker-registry3.mariadb.com/mariadb-operator/mariadb-operator:0.36.0'] +- version: 0.35.1 + kube: ['1.36', '1.35', '1.34', '1.33', '1.32', '1.31', '1.30', '1.29', '1.28', '1.27', + '1.26'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 0.35.1 + images: ['docker-registry3.mariadb.com/mariadb-operator/mariadb-operator:0.35.1'] +- version: 0.34.0 + kube: ['1.36', '1.35', '1.34', '1.33', '1.32', '1.31', '1.30', '1.29', '1.28', '1.27', + '1.26'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 0.34.0 + images: ['docker-registry3.mariadb.com/mariadb-operator/mariadb-operator:0.34.0'] +- version: 0.0.33 + kube: ['1.36', '1.35', '1.34', '1.33', '1.32', '1.31', '1.30', '1.29', '1.28', '1.27', + '1.26'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 0.33.0 + images: ['docker-registry3.mariadb.com/mariadb-operator/mariadb-operator:v0.0.33'] +- version: 0.0.29 + kube: ['1.36', '1.35', '1.34', '1.33', '1.32', '1.31', '1.30', '1.29', '1.28', '1.27', + '1.26', '1.25', '1.24', '1.23', '1.22', '1.21', '1.20', '1.19', '1.18', '1.17', + '1.16'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 0.29.0 + images: ['docker-registry3.mariadb.com/mariadb-operator/mariadb-operator:v0.0.29'] diff --git a/utils/compatibility/scrapers/mariadb-operator.py b/utils/compatibility/scrapers/mariadb-operator.py new file mode 100644 index 0000000000..7936a3e944 --- /dev/null +++ b/utils/compatibility/scrapers/mariadb-operator.py @@ -0,0 +1,178 @@ +from __future__ import annotations + +import re +from collections import OrderedDict + +import yaml + +from utils import ( + current_kube_version, + expand_kube_versions, + fetch_page, + print_error, + read_yaml, + update_compatibility_info, + validate_semver, + write_yaml, +) + + +APP_NAME = "mariadb-operator" +CHART_NAME = "mariadb-operator" +HELM_INDEX_URL = "https://mariadb-operator.github.io/mariadb-operator/index.yaml" +OUTPUT_PATH = f"../../static/compatibilities/{APP_NAME}.yaml" + + +def _clean_version(version): + parsed = validate_semver(str(version).strip().lstrip("v")) + return str(parsed) if parsed else None + + +def _minor(version): + parsed = validate_semver(version) + if not parsed: + return None + return f"{parsed.major}.{parsed.minor}" + + +def _minor_bounds(version): + parsed = validate_semver(version) + if not parsed: + return None + return parsed.major, parsed.minor + + +def parse_kube_constraint(spec, latest_kube): + if not spec or not latest_kube: + return [] + + latest = _minor_bounds(latest_kube) + if not latest: + return [] + + lower = None + upper = latest + normalized = spec.replace(",", " ") + + for operator, major, minor in re.findall(r"(>=|<=|<|>|=)?\s*v?(\d+)\.(\d+)", normalized): + bound = (int(major), int(minor)) + + if operator in ("", ">=", ">", "="): + if operator == ">": + bound = (bound[0], bound[1] + 1) + if lower is None or bound > lower: + lower = bound + elif operator in ("<", "<="): + if operator == "<": + bound = (bound[0], bound[1] - 1) + if upper is None or bound < upper: + upper = bound + + if lower is None or upper is None or lower > upper: + return [] + + start = f"{lower[0]}.{lower[1]}" + end = f"{upper[0]}.{upper[1]}" + return expand_kube_versions(start, end) + + +def load_index(content): + try: + return yaml.safe_load(content) + except yaml.YAMLError as exc: + print_error(f"Failed to parse MariaDB Operator Helm index: {exc}") + return None + + +def extract_rows(index_yaml, latest_kube): + rows_by_version = {} + entries = index_yaml.get("entries", {}).get(CHART_NAME, []) + + for entry in entries: + app_version = _clean_version(entry.get("appVersion", "")) + chart_version = _clean_version(entry.get("version", "")) + if not app_version or not chart_version: + continue + + kube_versions = parse_kube_constraint(entry.get("kubeVersion", ""), latest_kube) + if not kube_versions: + continue + + current = rows_by_version.get(app_version) + if current and validate_semver(chart_version) <= validate_semver(current["chart_version"]): + continue + + rows_by_version[app_version] = OrderedDict( + [ + ("version", app_version), + ("kube", kube_versions), + ("chart_version", chart_version), + ("images", []), + ("requirements", []), + ("incompatibilities", []), + ] + ) + + rows = sorted( + rows_by_version.values(), + key=lambda row: validate_semver(row["version"]), + reverse=True, + ) + return _representative_rows(rows) + + +def _representative_rows(rows): + selected = [] + seen = {} + + for row in rows: + minor = _minor(row["version"]) + if not minor: + continue + + kube = tuple(row["kube"]) + if minor not in seen: + selected.append(row) + seen[minor] = kube + continue + + if seen[minor] != kube: + selected.append(row) + seen[minor] = kube + + return selected + + +def prune_stale_representatives(filepath, rows): + data = read_yaml(filepath) + if not data or "versions" not in data: + return + + keep = {row["version"] for row in rows} + data["versions"] = [ + version + for version in data.get("versions", []) + if version.get("version") in keep + ] + write_yaml(filepath, data) +def scrape(): + latest_kube = current_kube_version() + if not latest_kube: + print_error("Could not determine current Kubernetes version from KUBE_VERSION.") + return + + content = fetch_page(HELM_INDEX_URL) + if not content: + return + + index_yaml = load_index(content) + if not index_yaml: + return + + rows = extract_rows(index_yaml, latest_kube) + if not rows: + print_error("No MariaDB Operator versions extracted from Helm index.") + return + + prune_stale_representatives(OUTPUT_PATH, rows) + update_compatibility_info(OUTPUT_PATH, rows) diff --git a/utils/compatibility/tests/test_mariadb_operator.py b/utils/compatibility/tests/test_mariadb_operator.py new file mode 100644 index 0000000000..2c935f9c97 --- /dev/null +++ b/utils/compatibility/tests/test_mariadb_operator.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +import importlib.util +import sys +import tempfile +import unittest +from pathlib import Path + + +COMPATIBILITY_DIR = Path(__file__).resolve().parents[1] +MODULE_PATH = COMPATIBILITY_DIR / "scrapers" / "mariadb-operator.py" +sys.path.insert(0, str(COMPATIBILITY_DIR)) + +spec = importlib.util.spec_from_file_location("mariadb_operator", MODULE_PATH) +mariadb_operator = importlib.util.module_from_spec(spec) +assert spec.loader is not None +spec.loader.exec_module(mariadb_operator) + + +class MariaDBOperatorScraperTest(unittest.TestCase): + def test_parse_kube_constraint_expands_minimum_to_current_kubernetes(self): + self.assertEqual( + mariadb_operator.parse_kube_constraint(">=1.26.0-0", "1.36"), + [ + "1.26", + "1.27", + "1.28", + "1.29", + "1.30", + "1.31", + "1.32", + "1.33", + "1.34", + "1.35", + "1.36", + ], + ) + + def test_parse_kube_constraint_honors_upper_boundaries(self): + self.assertEqual( + mariadb_operator.parse_kube_constraint(">=1.26.0-0 <1.29.0-0", "1.36"), + ["1.26", "1.27", "1.28"], + ) + + def test_extract_rows_uses_only_the_operator_chart_and_stable_app_versions(self): + index_yaml = { + "entries": { + "mariadb-cluster": [ + { + "version": "26.6.0", + "appVersion": "0.0.0", + "kubeVersion": ">=1.26.0-0", + } + ], + "mariadb-operator": [ + { + "version": "26.6.0", + "appVersion": "26.6.0", + "kubeVersion": ">=1.26.0-0", + }, + { + "version": "25.10.4", + "appVersion": "25.10.4", + "kubeVersion": ">=1.26.0-0", + }, + { + "version": "25.10.0", + "appVersion": "25.10.0", + "kubeVersion": ">=1.26.0-0", + }, + { + "version": "26.3.0", + "appVersion": "26.3.0", + "kubeVersion": ">=1.26.0-0", + }, + { + "version": "26.3.0-rc.0", + "appVersion": "26.3.0-rc.0", + "kubeVersion": ">=1.26.0-0", + }, + { + "version": "0.33.0", + "appVersion": "v0.0.33", + "kubeVersion": ">=1.26.0-0", + }, + { + "version": "0.30.0", + "appVersion": "v0.0.30", + "kubeVersion": ">=1.16.0-0", + }, + ], + "mariadb-operator-crds": [ + { + "version": "26.6.0", + "appVersion": "0.0.0", + "kubeVersion": ">=1.26.0-0", + } + ], + } + } + + rows = mariadb_operator.extract_rows(index_yaml, "1.36") + + self.assertEqual( + [row["version"] for row in rows], + ["26.6.0", "26.3.0", "25.10.4", "0.0.33", "0.0.30"], + ) + self.assertEqual( + [row["chart_version"] for row in rows], + ["26.6.0", "26.3.0", "25.10.4", "0.33.0", "0.30.0"], + ) + self.assertEqual(rows[0]["kube"][0], "1.26") + self.assertEqual(rows[0]["kube"][-1], "1.36") + self.assertEqual(rows[-1]["kube"][0], "1.16") + + def test_prune_stale_representatives_removes_old_patch_rows(self): + with tempfile.TemporaryDirectory() as temp: + path = Path(temp) / "mariadb-operator.yaml" + path.write_text( + """ +versions: +- version: 25.10.0 + summary: old +- version: 25.10.4 + summary: current +""", + encoding="utf-8", + ) + + mariadb_operator.prune_stale_representatives( + path, + [{"version": "25.10.4"}], + ) + + self.assertNotIn("25.10.0", path.read_text(encoding="utf-8")) + + +if __name__ == "__main__": + unittest.main() From b1794c09f25353b0d43d3820220f9497886c4320 Mon Sep 17 00:00:00 2001 From: goodguypeci-design <247882324+goodguypeci-design@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:53:49 +0300 Subject: [PATCH 2/3] Harden MariaDB Kubernetes constraint parsing --- .../scrapers/mariadb-operator.py | 83 ++++++++++++++----- .../tests/test_mariadb_operator.py | 10 +++ 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/utils/compatibility/scrapers/mariadb-operator.py b/utils/compatibility/scrapers/mariadb-operator.py index 7936a3e944..336988583f 100644 --- a/utils/compatibility/scrapers/mariadb-operator.py +++ b/utils/compatibility/scrapers/mariadb-operator.py @@ -7,7 +7,6 @@ from utils import ( current_kube_version, - expand_kube_versions, fetch_page, print_error, read_yaml, @@ -42,6 +41,48 @@ def _minor_bounds(version): return parsed.major, parsed.minor +def _next_minor(major, minor): + return major, minor + 1 + + +def _version_tuple(major, minor, patch=0): + return int(major), int(minor), int(patch or 0) + + +def _parse_constraints(spec): + normalized = spec.replace(",", " ") + constraints = [] + + for operator, major, minor, patch in re.findall( + r"(>=|<=|<|>|=)?\s*v?(\d+)\.(\d+)(?:\.(\d+))?", + normalized, + ): + constraints.append((operator or ">=", _version_tuple(major, minor, patch))) + + return constraints + + +def _minor_satisfies_constraints(major, minor, constraints): + start = (major, minor, 0) + end = (*_next_minor(major, minor), 0) + + for operator, bound in constraints: + if operator in (">=", ">"): + if end <= bound: + return False + elif operator == "<": + if start >= bound: + return False + elif operator == "<=": + if start > bound: + return False + elif operator == "=": + if not (start <= bound < end): + return False + + return True + + def parse_kube_constraint(spec, latest_kube): if not spec or not latest_kube: return [] @@ -50,30 +91,24 @@ def parse_kube_constraint(spec, latest_kube): if not latest: return [] - lower = None - upper = latest - normalized = spec.replace(",", " ") - - for operator, major, minor in re.findall(r"(>=|<=|<|>|=)?\s*v?(\d+)\.(\d+)", normalized): - bound = (int(major), int(minor)) - - if operator in ("", ">=", ">", "="): - if operator == ">": - bound = (bound[0], bound[1] + 1) - if lower is None or bound > lower: - lower = bound - elif operator in ("<", "<="): - if operator == "<": - bound = (bound[0], bound[1] - 1) - if upper is None or bound < upper: - upper = bound - - if lower is None or upper is None or lower > upper: + constraints = _parse_constraints(spec) + lower_bounds = [ + (bound[0], bound[1]) + for operator, bound in constraints + if operator in (">=", ">", "=") + ] + if not constraints or not lower_bounds: return [] - start = f"{lower[0]}.{lower[1]}" - end = f"{upper[0]}.{upper[1]}" - return expand_kube_versions(start, end) + major, minor = max(lower_bounds) + kube_versions = [] + + while (major, minor) <= latest: + if _minor_satisfies_constraints(major, minor, constraints): + kube_versions.append(f"{major}.{minor}") + major, minor = _next_minor(major, minor) + + return kube_versions def load_index(content): @@ -155,6 +190,8 @@ def prune_stale_representatives(filepath, rows): if version.get("version") in keep ] write_yaml(filepath, data) + + def scrape(): latest_kube = current_kube_version() if not latest_kube: diff --git a/utils/compatibility/tests/test_mariadb_operator.py b/utils/compatibility/tests/test_mariadb_operator.py index 2c935f9c97..cd4fd99f15 100644 --- a/utils/compatibility/tests/test_mariadb_operator.py +++ b/utils/compatibility/tests/test_mariadb_operator.py @@ -42,6 +42,16 @@ def test_parse_kube_constraint_honors_upper_boundaries(self): ["1.26", "1.27", "1.28"], ) + def test_parse_kube_constraint_keeps_minors_with_valid_patch_overlap(self): + self.assertEqual( + mariadb_operator.parse_kube_constraint(">=1.26.0 <1.29.1", "1.36"), + ["1.26", "1.27", "1.28", "1.29"], + ) + self.assertEqual( + mariadb_operator.parse_kube_constraint(">1.26.0 <=1.28.0", "1.36"), + ["1.26", "1.27", "1.28"], + ) + def test_extract_rows_uses_only_the_operator_chart_and_stable_app_versions(self): index_yaml = { "entries": { From e587988b08fe4c8fce38a9673891b6d13e6e33da Mon Sep 17 00:00:00 2001 From: goodguypeci-design <247882324+goodguypeci-design@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:57:10 +0300 Subject: [PATCH 3/3] Load local utils in MariaDB compatibility tests --- .../tests/test_mariadb_operator.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/utils/compatibility/tests/test_mariadb_operator.py b/utils/compatibility/tests/test_mariadb_operator.py index cd4fd99f15..02adce803e 100644 --- a/utils/compatibility/tests/test_mariadb_operator.py +++ b/utils/compatibility/tests/test_mariadb_operator.py @@ -11,6 +11,26 @@ MODULE_PATH = COMPATIBILITY_DIR / "scrapers" / "mariadb-operator.py" sys.path.insert(0, str(COMPATIBILITY_DIR)) +utils_spec = importlib.util.spec_from_file_location("utils", COMPATIBILITY_DIR / "utils.py") +compat_utils = importlib.util.module_from_spec(utils_spec) +assert utils_spec.loader is not None +utils_spec.loader.exec_module(compat_utils) + +utils_module = sys.modules.get("utils") +if utils_module is None: + sys.modules["utils"] = compat_utils +else: + for name in ( + "current_kube_version", + "fetch_page", + "print_error", + "read_yaml", + "update_compatibility_info", + "validate_semver", + "write_yaml", + ): + setattr(utils_module, name, getattr(compat_utils, name)) + spec = importlib.util.spec_from_file_location("mariadb_operator", MODULE_PATH) mariadb_operator = importlib.util.module_from_spec(spec) assert spec.loader is not None