diff --git a/static/compatibilities.yaml b/static/compatibilities.yaml index 3791bfd46e..8c435621aa 100644 --- a/static/compatibilities.yaml +++ b/static/compatibilities.yaml @@ -15467,6 +15467,104 @@ addons: chart_version: 0.18.2 images: ['us.gcr.io/k8s-artifacts-prod/descheduler/descheduler:v0.18.0'] name: descheduler +- icon: https://raw.githubusercontent.com/envoyproxy/gateway/main/site/assets/icons/logo.svg + git_url: https://github.com/envoyproxy/gateway + release_url: https://github.com/envoyproxy/gateway/releases/tag/v{vsn} + helm_repository_url: oci://docker.io/envoyproxy/gateway-helm + chart_name: gateway-helm + versions: + - version: 1.8.3 + kube: ['1.35', '1.34', '1.33', '1.32'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 1.8.3 + images: ['docker.io/envoyproxy/gateway:v1.8.3'] + - version: 1.8.1 + kube: ['1.35', '1.34', '1.33', '1.32'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 1.8.1 + images: ['docker.io/envoyproxy/gateway:v1.8.1'] + - version: 1.7.4 + kube: ['1.35', '1.34', '1.33', '1.32'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 1.7.4 + images: ['docker.io/envoyproxy/gateway:v1.7.4'] + - version: 1.6.7 + kube: ['1.33', '1.32', '1.31', '1.30'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 1.6.7 + images: ['docker.io/envoyproxy/gateway:v1.6.7'] + - version: 1.5.9 + kube: ['1.33', '1.32', '1.31', '1.30'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 1.5.9 + images: ['docker.io/envoyproxy/gateway:v1.5.9'] + - version: 1.4.6 + kube: ['1.33', '1.32', '1.31', '1.30'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 1.4.6 + images: ['docker.io/envoyproxy/gateway:v1.4.6'] + - version: 1.3.3 + kube: ['1.32', '1.31', '1.30', '1.29'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 1.3.3 + images: ['docker.io/envoyproxy/gateway:v1.3.3'] + - version: 1.2.8 + kube: ['1.31', '1.30', '1.29', '1.28'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 1.2.8 + images: ['docker.io/envoyproxy/gateway:v1.2.8'] + - version: 1.1.4 + kube: ['1.30', '1.29', '1.28', '1.27'] + requirements: [] + incompatibilities: [] + summary: null + - version: 1.0.2 + kube: ['1.29', '1.28', '1.27', '1.26'] + requirements: [] + incompatibilities: [] + summary: null + - version: 0.6.0 + kube: ['1.28', '1.27', '1.26'] + requirements: [] + incompatibilities: [] + summary: null + - version: 0.5.0 + kube: ['1.27', '1.26', '1.25'] + requirements: [] + incompatibilities: [] + summary: null + - version: 0.4.0 + kube: ['1.27', '1.26', '1.25'] + requirements: [] + incompatibilities: [] + summary: null + - version: 0.3.0 + kube: ['1.26', '1.25', '1.24'] + requirements: [] + incompatibilities: [] + summary: null + - version: 0.2.0 + kube: ['1.24'] + requirements: [] + incompatibilities: [] + summary: null + name: envoy-gateway - icon: https://github.com/kubernetes-sigs/external-dns/blob/master/docs/img/external-dns.png?raw=true git_url: https://github.com/kubernetes-sigs/external-dns release_url: https://github.com/kubernetes-sigs/external-dns/releases/tag/v{vsn} diff --git a/static/compatibilities/envoy-gateway.yaml b/static/compatibilities/envoy-gateway.yaml new file mode 100644 index 0000000000..7f71a9f991 --- /dev/null +++ b/static/compatibilities/envoy-gateway.yaml @@ -0,0 +1,97 @@ +icon: https://raw.githubusercontent.com/envoyproxy/gateway/main/site/assets/icons/logo.svg +git_url: https://github.com/envoyproxy/gateway +release_url: https://github.com/envoyproxy/gateway/releases/tag/v{vsn} +helm_repository_url: oci://docker.io/envoyproxy/gateway-helm +chart_name: gateway-helm +versions: +- version: 1.8.3 + kube: ['1.35', '1.34', '1.33', '1.32'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 1.8.3 + images: ['docker.io/envoyproxy/gateway:v1.8.3'] +- version: 1.8.1 + kube: ['1.35', '1.34', '1.33', '1.32'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 1.8.1 + images: ['docker.io/envoyproxy/gateway:v1.8.1'] +- version: 1.7.4 + kube: ['1.35', '1.34', '1.33', '1.32'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 1.7.4 + images: ['docker.io/envoyproxy/gateway:v1.7.4'] +- version: 1.6.7 + kube: ['1.33', '1.32', '1.31', '1.30'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 1.6.7 + images: ['docker.io/envoyproxy/gateway:v1.6.7'] +- version: 1.5.9 + kube: ['1.33', '1.32', '1.31', '1.30'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 1.5.9 + images: ['docker.io/envoyproxy/gateway:v1.5.9'] +- version: 1.4.6 + kube: ['1.33', '1.32', '1.31', '1.30'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 1.4.6 + images: ['docker.io/envoyproxy/gateway:v1.4.6'] +- version: 1.3.3 + kube: ['1.32', '1.31', '1.30', '1.29'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 1.3.3 + images: ['docker.io/envoyproxy/gateway:v1.3.3'] +- version: 1.2.8 + kube: ['1.31', '1.30', '1.29', '1.28'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: 1.2.8 + images: ['docker.io/envoyproxy/gateway:v1.2.8'] +- version: 1.1.4 + kube: ['1.30', '1.29', '1.28', '1.27'] + requirements: [] + incompatibilities: [] + summary: null +- version: 1.0.2 + kube: ['1.29', '1.28', '1.27', '1.26'] + requirements: [] + incompatibilities: [] + summary: null +- version: 0.6.0 + kube: ['1.28', '1.27', '1.26'] + requirements: [] + incompatibilities: [] + summary: null +- version: 0.5.0 + kube: ['1.27', '1.26', '1.25'] + requirements: [] + incompatibilities: [] + summary: null +- version: 0.4.0 + kube: ['1.27', '1.26', '1.25'] + requirements: [] + incompatibilities: [] + summary: null +- version: 0.3.0 + kube: ['1.26', '1.25', '1.24'] + requirements: [] + incompatibilities: [] + summary: null +- version: 0.2.0 + kube: ['1.24'] + requirements: [] + incompatibilities: [] + summary: null diff --git a/static/compatibilities/manifest.yaml b/static/compatibilities/manifest.yaml index 2be754b662..6d6a324691 100644 --- a/static/compatibilities/manifest.yaml +++ b/static/compatibilities/manifest.yaml @@ -13,6 +13,7 @@ names: - cloudnative-pg - cluster-autoscaler - descheduler +- envoy-gateway - external-dns - external-secrets - flux diff --git a/utils/compatibility/scrapers/envoy-gateway.py b/utils/compatibility/scrapers/envoy-gateway.py new file mode 100644 index 0000000000..92863575ac --- /dev/null +++ b/utils/compatibility/scrapers/envoy-gateway.py @@ -0,0 +1,164 @@ +import json +import re +from collections import OrderedDict + +from packaging.version import Version + +from utils import ( + fetch_page, + get_chart_images, + print_error, + print_warning, + update_compatibility_info, +) + + +APP_NAME = "envoy-gateway" +COMPATIBILITY_URL = "https://raw.githubusercontent.com/envoyproxy/gateway/main/site/content/en/news/releases/matrix.md" +TAGS_URL = "https://api.github.com/repos/envoyproxy/gateway/tags" +TARGET_FILE = f"../../static/compatibilities/{APP_NAME}.yaml" +CHART_URL = "oci://docker.io/envoyproxy/gateway-helm" +CHART_NAME = "gateway-helm" + + +def _decode(content): + return content.decode("utf-8") if isinstance(content, bytes) else content + + +def _clean_cell(value): + return value.strip().strip("*").strip() + + +def _version_series(value): + match = re.search(r"v?(\d+\.\d+)", value) + if not match: + return None + return match.group(1) + + +def _kube_versions(value): + return [match.group(1) for match in re.finditer(r"v?(\d+\.\d+)", value)] + + +def _latest_patch_versions(): + latest = {} + page = 1 + + while True: + content = fetch_page(f"{TAGS_URL}?per_page=100&page={page}") + if not content: + return latest + + try: + tags = json.loads(_decode(content)) + except json.JSONDecodeError as exc: + print_error(f"Failed to parse Envoy Gateway tags: {exc}") + return latest + + if not tags: + break + + for tag in tags: + tag_name = str(tag.get("name", "")) + match = re.fullmatch(r"v(\d+\.\d+\.\d+)", tag_name) + if not match: + continue + + app_version = match.group(1) + parsed = Version(app_version) + series = f"{parsed.major}.{parsed.minor}" + current = latest.get(series) + if not current or parsed > Version(current): + latest[series] = app_version + + if len(tags) < 100: + break + + page += 1 + + return latest + + +def _matrix_rows(content): + rows = {} + version_idx = None + kube_idx = None + + for line in _decode(content).splitlines(): + stripped = line.strip() + if not stripped.startswith("|"): + continue + if set(stripped.replace("|", "").strip()) <= {"-"}: + continue + + columns = [_clean_cell(column) for column in stripped.strip("|").split("|")] + if "Envoy Gateway version" in columns and "Kubernetes version" in columns: + version_idx = columns.index("Envoy Gateway version") + kube_idx = columns.index("Kubernetes version") + continue + + if version_idx is None or kube_idx is None: + continue + if len(columns) <= max(version_idx, kube_idx): + continue + + if columns[version_idx] == "latest": + continue + + series = _version_series(columns[version_idx]) + kube_versions = _kube_versions(columns[kube_idx]) + if series and kube_versions: + rows[series] = kube_versions + + return rows + + +def scrape(): + content = fetch_page(COMPATIBILITY_URL) + if not content: + print_error("Failed to fetch Envoy Gateway compatibility matrix.") + return + + compatibility = _matrix_rows(content) + if not compatibility: + print_error("No Envoy Gateway compatibility rows found.") + return + + latest_versions = _latest_patch_versions() + if not latest_versions: + print_error("No Envoy Gateway release tags found.") + return + + versions = [] + skipped_series = [] + for series, kube_versions in compatibility.items(): + app_version = latest_versions.get(series) + if not app_version: + skipped_series.append(series) + continue + + row = OrderedDict( + [ + ("version", app_version), + ("kube", kube_versions), + ("requirements", []), + ("incompatibilities", []), + ] + ) + images = get_chart_images(CHART_URL, CHART_NAME, app_version) + if images: + row["chart_version"] = app_version + row["images"] = images + versions.append(row) + + if skipped_series: + print_warning( + "Skipped Envoy Gateway series without stable release tags: " + + ", ".join(skipped_series) + ) + + if not versions: + print_error("No Envoy Gateway versions extracted from compatibility matrix.") + return + + update_compatibility_info(TARGET_FILE, versions) diff --git a/utils/compatibility/tests/test_envoy_gateway.py b/utils/compatibility/tests/test_envoy_gateway.py new file mode 100644 index 0000000000..f871b9b351 --- /dev/null +++ b/utils/compatibility/tests/test_envoy_gateway.py @@ -0,0 +1,67 @@ +import importlib +import sys +import unittest +from pathlib import Path +from unittest.mock import patch + +COMPATIBILITY_DIR = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(COMPATIBILITY_DIR)) +sys.modules.pop("utils", None) + +scraper = importlib.import_module("scrapers.envoy-gateway") + + +class EnvoyGatewayScraperTest(unittest.TestCase): + def test_matrix_rows_locates_columns_by_header(self): + markdown = """\ +| Kubernetes version | Notes | Envoy Gateway version | +|---|---|---| +| v1.35, v1.34 | stable | v1.8 | +""" + + self.assertEqual( + scraper._matrix_rows(markdown), + {"1.8": ["1.35", "1.34"]}, + ) + + @patch.object(scraper, "fetch_page") + def test_latest_patch_versions_ignore_prereleases(self, fetch_page): + fetch_page.side_effect = [ + b"""[ + {"name": "v1.8.3"}, + {"name": "v1.8.4-rc.1"}, + {"name": "v1.8.2"}, + {"name": "latest"} + ]""" + ] + + self.assertEqual(scraper._latest_patch_versions(), {"1.8": "1.8.3"}) + + @patch.object(scraper, "update_compatibility_info") + @patch.object(scraper, "get_chart_images", return_value=None) + @patch.object(scraper, "_latest_patch_versions", return_value={"0.6": "0.6.0"}) + @patch.object( + scraper, + "fetch_page", + return_value=b"""\ +| Envoy Gateway version | Kubernetes version | +|---|---| +| v0.6 | v1.28, v1.27 | +""", + ) + def test_omits_chart_metadata_when_oci_version_is_unavailable( + self, + _fetch_page, + _latest_patch_versions, + _get_chart_images, + update_compatibility_info, + ): + scraper.scrape() + + rows = update_compatibility_info.call_args.args[1] + self.assertNotIn("chart_version", rows[0]) + self.assertNotIn("images", rows[0]) + + +if __name__ == "__main__": + unittest.main()