-
Notifications
You must be signed in to change notification settings - Fork 77
feat: add Elastic ECK Operator compatibility scraper #4155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MiTM-1
wants to merge
17
commits into
pluralsh:master
Choose a base branch
from
MiTM-1:feat/eck-operator-compatibility
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
334032b
feat: add ECK compatibility scraper
MiTM-1 f05204c
test: cover ECK compatibility scraper
MiTM-1 8f2a537
data: add ECK compatibility metadata
MiTM-1 f31c27f
manifest: register ECK operator
MiTM-1 286bfca
test: isolate ECK scraper utils stub
MiTM-1 462c9b7
ci: regenerate ECK compatibility aggregate
MiTM-1 26f7ce3
chore: regenerate compatibility aggregate for ECK
github-actions[bot] b93cb4a
chore: regenerate ECK aggregate with repo formatter
MiTM-1 105b438
ci: remove temporary aggregate workflow
MiTM-1 65369c8
ci: regenerate ECK aggregate with repo formatter
MiTM-1 265111c
chore: regenerate compatibility aggregate for ECK
github-actions[bot] d0fca40
ci: fix ECK aggregate diff
MiTM-1 74ffd13
fix: keep ECK aggregate update minimal
github-actions[bot] 29cc7ae
ci: remove temporary ECK aggregate workflow
MiTM-1 15c9a77
ci: remove temporary ECK aggregate workflow
MiTM-1 95742d3
ci: verify ECK compatibility fixes
MiTM-1 16f71db
ci: remove temporary ECK verification workflow
MiTM-1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| icon: https://helm.elastic.co/icons/eck.png | ||
| git_url: https://github.com/elastic/cloud-on-k8s | ||
| release_url: https://github.com/elastic/cloud-on-k8s/releases/tag/v{vsn} | ||
| helm_repository_url: https://helm.elastic.co | ||
| versions: | ||
| - version: 3.5.0 | ||
| kube: ['1.36', '1.35', '1.34', '1.33', '1.32'] | ||
| chart_version: 3.5.0 | ||
| requirements: [] | ||
| incompatibilities: [] | ||
| summary: null | ||
| - version: 3.4.1 | ||
| kube: ['1.36', '1.35', '1.34', '1.33', '1.32', '1.31'] | ||
| chart_version: 3.4.1 | ||
| requirements: [] | ||
| incompatibilities: [] | ||
| summary: null | ||
| - version: 3.3.2 | ||
| kube: ['1.35', '1.34', '1.33', '1.32', '1.31'] | ||
| chart_version: 3.3.2 | ||
| requirements: [] | ||
| incompatibilities: [] | ||
| summary: null | ||
| - version: 3.2.0 | ||
| kube: ['1.34', '1.33', '1.32', '1.31', '1.30'] | ||
| chart_version: 3.2.0 | ||
| requirements: [] | ||
| incompatibilities: [] | ||
| summary: null | ||
| - version: 3.1.0 | ||
| kube: ['1.33', '1.32', '1.31', '1.30', '1.29'] | ||
| chart_version: 3.1.0 | ||
| requirements: [] | ||
| incompatibilities: [] | ||
| summary: null | ||
| - version: 3.0.0 | ||
| kube: ['1.32', '1.31', '1.30', '1.29', '1.28'] | ||
| chart_version: 3.0.0 | ||
| requirements: [] | ||
| incompatibilities: [] | ||
| summary: null | ||
| name: eck-operator |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,3 +49,4 @@ names: | |
| - wiz-sensor | ||
| - wiz-admission-controller | ||
| - wiz-network-analyzer | ||
| - eck-operator | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import re | ||
| from collections import OrderedDict | ||
|
|
||
| from packaging.version import InvalidVersion, Version | ||
|
|
||
| from utils import fetch_page, get_chart_versions, update_compatibility_info | ||
|
|
||
| app_name = "eck-operator" | ||
| README_URL = "https://raw.githubusercontent.com/elastic/cloud-on-k8s/v{version}/README.md" | ||
| MIN_MAJOR = 3 | ||
|
|
||
|
|
||
| def parse_kube_range(markdown: str) -> list[str]: | ||
| match = re.search( | ||
| r"^\s*[*-]\s+Kubernetes\s+1\.(\d+)\s*-\s*1\.(\d+)\s*$", | ||
| markdown, | ||
| re.MULTILINE, | ||
| ) | ||
| if not match: | ||
| raise ValueError("ECK Kubernetes support range not found") | ||
|
|
||
| start, end = map(int, match.groups()) | ||
| if start > end: | ||
| raise ValueError("ECK Kubernetes support range is reversed") | ||
|
|
||
| return [f"1.{minor}" for minor in range(end, start - 1, -1)] | ||
|
|
||
|
|
||
| def latest_chart_per_minor(chart_versions: dict[str, str]) -> list[tuple[str, str]]: | ||
| latest: dict[tuple[int, int], tuple[Version, str]] = {} | ||
|
|
||
| for app_version, chart_version in chart_versions.items(): | ||
| try: | ||
| parsed = Version(app_version) | ||
| except InvalidVersion: | ||
| continue | ||
|
|
||
| if parsed.is_prerelease or parsed.is_devrelease or parsed.major < MIN_MAJOR: | ||
| continue | ||
|
|
||
| key = (parsed.major, parsed.minor) | ||
| current = latest.get(key) | ||
| if current is None or parsed > current[0]: | ||
| latest[key] = (parsed, chart_version) | ||
|
|
||
| return [ | ||
| (str(parsed), chart_version) | ||
| for parsed, chart_version in sorted( | ||
| latest.values(), | ||
| key=lambda item: item[0], | ||
| reverse=True, | ||
| ) | ||
| ] | ||
|
|
||
|
|
||
| def build_rows( | ||
| chart_versions: dict[str, str], | ||
| fetcher=fetch_page, | ||
| ) -> list[OrderedDict[str, object]]: | ||
| candidates = latest_chart_per_minor(chart_versions) | ||
| if not candidates: | ||
| raise ValueError("No supported ECK Helm releases found") | ||
|
|
||
| rows: list[OrderedDict[str, object]] = [] | ||
| for version, chart_version in candidates: | ||
| url = README_URL.format(version=version) | ||
| content = fetcher(url) | ||
| if not content: | ||
| raise ValueError(f"Could not fetch ECK {version} README") | ||
|
|
||
| if isinstance(content, bytes): | ||
| try: | ||
| markdown = content.decode("utf-8") | ||
| except UnicodeDecodeError as exc: | ||
| raise ValueError(f"Could not decode ECK {version} README") from exc | ||
| elif isinstance(content, str): | ||
| markdown = content | ||
| else: | ||
| raise ValueError(f"Unexpected ECK {version} README payload") | ||
|
|
||
| rows.append( | ||
| OrderedDict( | ||
| [ | ||
| ("version", version), | ||
| ("kube", parse_kube_range(markdown)), | ||
| ("chart_version", chart_version), | ||
| ("requirements", []), | ||
| ("incompatibilities", []), | ||
| ] | ||
| ) | ||
| ) | ||
|
|
||
| return rows | ||
|
|
||
|
|
||
| def scrape() -> None: | ||
| chart_versions = get_chart_versions(app_name) | ||
| if not chart_versions: | ||
| raise ValueError("No official ECK Helm releases found") | ||
|
|
||
| rows = build_rows(chart_versions) | ||
| update_compatibility_info( | ||
| f"../../static/compatibilities/{app_name}.yaml", | ||
| rows, | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| import importlib.util | ||
| from pathlib import Path | ||
| from types import ModuleType | ||
| import sys | ||
| import unittest | ||
| from unittest.mock import Mock, patch | ||
|
|
||
| helpers = ModuleType("utils") | ||
| helpers.fetch_page = Mock() | ||
| helpers.get_chart_versions = Mock() | ||
| helpers.update_compatibility_info = Mock() | ||
|
|
||
| # The scraper imports its helpers from a top-level `utils` module. Keep that | ||
| # temporary stub strictly scoped to module loading so this test cannot affect | ||
| # compatibility tests imported later in the same unittest discovery process. | ||
| _original_utils = sys.modules.get("utils") | ||
| sys.modules["utils"] = helpers | ||
| try: | ||
| SCRAPER_PATH = Path(__file__).parents[1] / "scrapers" / "eck-operator.py" | ||
| spec = importlib.util.spec_from_file_location("eck_operator", SCRAPER_PATH) | ||
| scraper = importlib.util.module_from_spec(spec) | ||
| spec.loader.exec_module(scraper) | ||
| finally: | ||
| if _original_utils is None: | ||
| sys.modules.pop("utils", None) | ||
| else: | ||
| sys.modules["utils"] = _original_utils | ||
|
|
||
|
|
||
| def readme(kube_range: str) -> bytes: | ||
| return f"""# Elastic Cloud on Kubernetes (ECK) | ||
|
|
||
| Supported versions: | ||
|
|
||
| * Kubernetes {kube_range} | ||
| * OpenShift 4.16-4.22 | ||
| """.encode("utf-8") | ||
|
|
||
|
|
||
| class EckOperatorTests(unittest.TestCase): | ||
| def test_parses_kubernetes_range_descending(self): | ||
| self.assertEqual( | ||
| scraper.parse_kube_range(readme("1.32-1.36").decode()), | ||
| ["1.36", "1.35", "1.34", "1.33", "1.32"], | ||
| ) | ||
|
|
||
| def test_accepts_whitespace_around_range_separator(self): | ||
| self.assertEqual( | ||
| scraper.parse_kube_range(readme("1.31 - 1.35").decode()), | ||
| ["1.35", "1.34", "1.33", "1.32", "1.31"], | ||
| ) | ||
|
|
||
| def test_missing_kubernetes_range_fails_closed(self): | ||
| with self.assertRaisesRegex(ValueError, "support range not found"): | ||
| scraper.parse_kube_range("# ECK\n* OpenShift 4.16-4.22") | ||
|
|
||
| def test_reversed_range_fails_closed(self): | ||
| with self.assertRaisesRegex(ValueError, "reversed"): | ||
| scraper.parse_kube_range(readme("1.36-1.32").decode()) | ||
|
|
||
| def test_latest_patch_per_minor_and_legacy_filter(self): | ||
| charts = { | ||
| "3.5.0": "3.5.0", | ||
| "3.4.0": "3.4.0", | ||
| "3.4.1": "3.4.1", | ||
| "3.3.2": "3.3.2", | ||
| "3.6.0-beta1": "3.6.0-beta1", | ||
| "2.16.0": "2.16.0", | ||
| "not-a-version": "x", | ||
| } | ||
| self.assertEqual( | ||
| scraper.latest_chart_per_minor(charts), | ||
| [ | ||
| ("3.5.0", "3.5.0"), | ||
| ("3.4.1", "3.4.1"), | ||
| ("3.3.2", "3.3.2"), | ||
| ], | ||
| ) | ||
|
|
||
| def test_build_rows_uses_tagged_release_readmes(self): | ||
| charts = {"3.5.0": "3.5.0", "3.4.1": "3.4.1"} | ||
| payloads = { | ||
| scraper.README_URL.format(version="3.5.0"): readme("1.32-1.36"), | ||
| scraper.README_URL.format(version="3.4.1"): readme("1.31-1.36"), | ||
| } | ||
|
|
||
| rows = scraper.build_rows(charts, payloads.get) | ||
|
|
||
| self.assertEqual([row["version"] for row in rows], ["3.5.0", "3.4.1"]) | ||
| self.assertEqual(rows[0]["kube"], ["1.36", "1.35", "1.34", "1.33", "1.32"]) | ||
| self.assertEqual(rows[1]["chart_version"], "3.4.1") | ||
|
|
||
| def test_missing_tagged_readme_fails_closed(self): | ||
| with self.assertRaisesRegex(ValueError, "Could not fetch"): | ||
| scraper.build_rows({"3.5.0": "3.5.0"}, lambda _: None) | ||
|
|
||
| def test_invalid_utf8_fails_closed(self): | ||
| with self.assertRaisesRegex(ValueError, "Could not decode"): | ||
| scraper.build_rows({"3.5.0": "3.5.0"}, lambda _: b"\xff") | ||
|
|
||
| def test_scrape_connects_helm_versions_to_shared_updater(self): | ||
| charts = {"3.5.0": "3.5.0"} | ||
| with ( | ||
| patch.object(scraper, "get_chart_versions", return_value=charts) as get_charts, | ||
| patch.object(scraper, "fetch_page", return_value=readme("1.32-1.36")), | ||
| patch.object(scraper, "update_compatibility_info") as update, | ||
| ): | ||
| original = scraper.build_rows | ||
|
|
||
| def wired(chart_versions): | ||
| return original(chart_versions, scraper.fetch_page) | ||
|
|
||
| with patch.object(scraper, "build_rows", side_effect=wired): | ||
| scraper.scrape() | ||
|
|
||
| get_charts.assert_called_once_with("eck-operator") | ||
| path, rows = update.call_args.args | ||
| self.assertEqual(path, "../../static/compatibilities/eck-operator.yaml") | ||
| self.assertEqual(rows[0]["version"], "3.5.0") | ||
|
|
||
| def test_scrape_rejects_empty_helm_mapping(self): | ||
| with patch.object(scraper, "get_chart_versions", return_value={}): | ||
| with self.assertRaisesRegex(ValueError, "No official ECK Helm"): | ||
| scraper.scrape() | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This registers
eck-operatoronly in the per-addon manifest. Merging this change triggers the S3 synchronization workflow, which uploads the existingstatic/compatibilities.yamlwithout an ECK entry, while remote compatibility consumers load that aggregate file. As a result, those consumers cannot discover the new operator until a later updater run regenerates the aggregate matrix. Please regenerate and includestatic/compatibilities.yamlin this PR.