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..336988583f --- /dev/null +++ b/utils/compatibility/scrapers/mariadb-operator.py @@ -0,0 +1,215 @@ +from __future__ import annotations + +import re +from collections import OrderedDict + +import yaml + +from utils import ( + current_kube_version, + 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 _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 [] + + latest = _minor_bounds(latest_kube) + if not latest: + return [] + + 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 [] + + 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): + 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..02adce803e --- /dev/null +++ b/utils/compatibility/tests/test_mariadb_operator.py @@ -0,0 +1,169 @@ +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)) + +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 +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_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": { + "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()