From 919146bc199e6366c40cf10d533d4669bd3def70 Mon Sep 17 00:00:00 2001 From: mgoerens Date: Mon, 8 Jul 2024 11:44:25 +0200 Subject: [PATCH] Add checks for chart in index, and for tag in repo (#347) This commit ports the two following checks, performed at the end of the checkpr script, to the Chart data structure: * Check that the Chart in this version is not already present in the index * Check that the release tag for this chart doesn't already exists on a given GitHub repository Signed-off-by: Matthias Goerens --- scripts/src/precheck/submission.py | 76 ++++++++++++++ scripts/src/precheck/submission_test.py | 134 ++++++++++++++++++++++++ 2 files changed, 210 insertions(+) diff --git a/scripts/src/precheck/submission.py b/scripts/src/precheck/submission.py index b882d223..6a7043cf 100644 --- a/scripts/src/precheck/submission.py +++ b/scripts/src/precheck/submission.py @@ -1,6 +1,13 @@ import os import re +import requests import semver +import yaml + +try: + from yaml import CLoader as Loader +except ImportError: + from yaml import Loader from dataclasses import dataclass, field @@ -36,6 +43,14 @@ class WebCatalogOnlyError(SubmissionError): pass +class HelmIndexError(SubmissionError): + pass + + +class ReleaseTagError(SubmissionError): + pass + + @dataclass class Chart: """Represents a Helm Chart @@ -73,6 +88,53 @@ def register_chart_info(self, category, organization, name, version): def get_owners_path(self): return f"charts/{self.category}/{self.organization}/{self.name}/OWNERS" + def get_release_tag(self): + return f"{self.organization}-{self.name}-{self.version}" + + def check_index(self, index): + """Check if the chart is present in the Helm index + + Args: + index (dict): Content of the Helm repo index + + Raise: + HelmIndexError if: + * The provided index is malformed + * The Chart is already present in the index + + """ + try: + chart_entry = index["entries"].get(self.name, []) + except KeyError as e: + raise HelmIndexError(f"Malformed index {index}") from e + + for chart in chart_entry: + if chart["version"] == self.version: + msg = f"[ERROR] Helm chart release already exists in the index.yaml: {self.version}" + raise HelmIndexError(msg) + + def check_release_tag(self, repository: str): + """Check for the existence of the chart's release tag on the provided repository. + + Args: + repository (str): Name of the GitHub repository to check for existing tag. + (e.g. "openshift-helm-charts/charts") + + Raise: ReleaseTagError if the tag already exists in the GitHub repo. + + """ + tag_name = self.get_release_tag() + tag_api = f"https://api.github.com/repos/{repository}/git/ref/tags/{tag_name}" + headers = { + "Accept": "application/vnd.github.v3+json", + "Authorization": f'Bearer {os.environ.get("BOT_TOKEN")}', + } + print(f"[INFO] checking tag: {tag_api}") + r = requests.head(tag_api, headers=headers) + if r.status_code == 200: + msg = f"[ERROR] Helm chart release already exists in the GitHub Release/Tag: {tag_name}" + raise ReleaseTagError(msg) + @dataclass class Report: @@ -439,3 +501,17 @@ def get_file_type(file_path): return "owners", owners_match return "unknwown", None + + +def download_index_data(repository, branch="gh_pages"): + """Download the helm repository index""" + r = requests.get( + f"https://raw.githubusercontent.com/{repository}/{branch}/index.yaml" + ) + + if r.status_code == 200: + data = yaml.load(r.text, Loader=Loader) + else: + data = {} + + return data diff --git a/scripts/src/precheck/submission_test.py b/scripts/src/precheck/submission_test.py index 161b0480..b4be69c7 100644 --- a/scripts/src/precheck/submission_test.py +++ b/scripts/src/precheck/submission_test.py @@ -618,3 +618,137 @@ def test_is_valid_web_catalog_only(test_scenario): test_scenario.input_submission.is_valid_web_catalog_only(repo_path=temp_dir) == test_scenario.expected_output ) + + +def create_new_index(charts: list[submission.Chart] = []): + """Create the JSON representation of a Helm chart index containing the provided list of charts + + The resulting index only contains the required information for the check_index to work. + + """ + index = {"apiVersion": "v1", "entries": {}} + + for chart in charts: + chart_entries = index["entries"].get(chart.name, []) + chart_entries.append( + { + "name": f"{chart.name}", + "version": f"{chart.version}", + } + ) + + index["entries"][chart.name] = chart_entries + + return index + + +@dataclass +class CheckIndexScenario: + chart: submission.Chart = field( + default_factory=lambda: submission.Chart( + category=expected_category, + organization=expected_organization, + name=expected_name, + version=expected_version, + ) + ) + index: dict = field(default_factory=lambda: create_new_index()) + excepted_exception: contextlib.ContextDecorator = field( + default_factory=lambda: contextlib.nullcontext() + ) + + +scenarios_check_index = [ + # Chart is not present in the index + CheckIndexScenario( + index=create_new_index([submission.Chart(name="not-awesome", version="0.42")]) + ), + # Chart is present but does not contain submitted version + CheckIndexScenario( + index=create_new_index([submission.Chart(name=expected_name, version="0.42")]) + ), + # Submitted version is present in index + CheckIndexScenario( + index=create_new_index( + [submission.Chart(name=expected_name, version=expected_version)] + ), + excepted_exception=pytest.raises( + submission.HelmIndexError, + match="Helm chart release already exists in the index.yaml", + ), + ), + # Index is empty + CheckIndexScenario(), + # Index is an empty dict + CheckIndexScenario( + index={}, + excepted_exception=pytest.raises( + submission.HelmIndexError, match="Malformed index" + ), + ), +] + + +@pytest.mark.parametrize("test_scenario", scenarios_check_index) +def test_check_index(test_scenario): + with test_scenario.excepted_exception: + test_scenario.chart.check_index(test_scenario.index) + + +@dataclass +class CheckReleaseTagScenario: + chart: submission.Chart = field( + default_factory=lambda: submission.Chart( + category=expected_category, + organization=expected_organization, + name=expected_name, + version=expected_version, + ) + ) + exising_tags: list[str] = field(default_factory=lambda: list()) + excepted_exception: contextlib.ContextDecorator = field( + default_factory=lambda: contextlib.nullcontext() + ) + + +scenarios_check_release_tag = [ + # A release doesn't exist for this org + CheckReleaseTagScenario(exising_tags=["notacme-notawesome-0.42"]), + # A release exist for this org, but not for this chart + CheckReleaseTagScenario(exising_tags=[f"{expected_organization}-notawesome-0.42"]), + # A release exist for this Chart but not in this version + CheckReleaseTagScenario( + exising_tags=[f"{expected_organization}-{expected_name}-0.42"], + ), + # A release exist for this Chart in this version + CheckReleaseTagScenario( + exising_tags=[f"{expected_organization}-{expected_name}-{expected_version}"], + excepted_exception=pytest.raises( + submission.ReleaseTagError, + match="Helm chart release already exists in the GitHub Release/Tag", + ), + ), +] + + +@pytest.mark.parametrize("test_scenario", scenarios_check_release_tag) +@responses.activate +def test_check_release_tag(test_scenario): + chart_release_tag = test_scenario.chart.get_release_tag() + + if chart_release_tag not in test_scenario.exising_tags: + responses.head( + f"https://api.github.com/repos/my-fake-org/my-fake-repo/git/ref/tags/{chart_release_tag}", + # json=[{"filename": file} for file in test_scenario.modified_files], + status=404, + ) + + for tag in test_scenario.exising_tags: + # Mock GitHub API + responses.head( + f"https://api.github.com/repos/my-fake-org/my-fake-repo/git/ref/tags/{tag}", + # json=[{"filename": file} for file in test_scenario.modified_files], + ) + + with test_scenario.excepted_exception: + test_scenario.chart.check_release_tag(repository="my-fake-org/my-fake-repo")