Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions static/compatibilities.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30910,3 +30910,45 @@ addons:
chart_version: 0.1.1
images: ['wiziopublic.azurecr.io/wiz-app/wiz-network-analyzer:0.1']
name: wiz-network-analyzer
- 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
42 changes: 42 additions & 0 deletions static/compatibilities/eck-operator.yaml
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
1 change: 1 addition & 0 deletions static/compatibilities/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ names:
- wiz-sensor
- wiz-admission-controller
- wiz-network-analyzer
- eck-operator

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Aggregate Matrix Omits ECK

This registers eck-operator only in the per-addon manifest. Merging this change triggers the S3 synchronization workflow, which uploads the existing static/compatibilities.yaml without 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 include static/compatibilities.yaml in this PR.

107 changes: 107 additions & 0 deletions utils/compatibility/scrapers/eck-operator.py
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,
)
128 changes: 128 additions & 0 deletions utils/compatibility/tests/test_eck_operator.py
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()