From 876ac1b270a19b3f8b41c8e3c098df0d7c16caad Mon Sep 17 00:00:00 2001 From: OpenShift Helm Charts Bot <83200018+openshift-helm-charts-bot@users.noreply.github.com> Date: Tue, 11 Jun 2024 12:18:25 -0500 Subject: [PATCH] Release-1.7.1 (#1460) Co-authored-by: openshift-helm-charts-bot <41898282+github-actions[bot]@users.noreply.github.com> --- .../actions/generate-chart-locks/action.yaml | 4 +- .github/workflows/build.yml | 23 +- .../check-locks-on-owners-submission.yml | 4 +- .github/workflows/lock-sanity-check.yml | 2 +- .github/workflows/mercury_bot.yml | 4 +- .github/workflows/owners-redhat.yml | 32 +- .github/workflows/owners.yml | 2 +- scripts/requirements.txt | 1 + scripts/src/checkprcontent/checkpr.py | 2 +- scripts/src/metrics/pushowners.py | 41 +- scripts/src/owners/owners_file.py | 101 +-- scripts/src/precheck/__init__.py | 0 scripts/src/precheck/serializer.py | 47 ++ scripts/src/precheck/serializer_test.py | 98 +++ scripts/src/precheck/submission.py | 441 +++++++++++++ scripts/src/precheck/submission_test.py | 620 ++++++++++++++++++ scripts/src/report/verifier_report.py | 27 +- scripts/src/signedchart/signedchart.py | 19 +- .../HC-16/dash-in-version/partner/report.yaml | 89 --- .../HC-16/dash-in-version/redhat/report.yaml | 89 --- .../HC-17/plus-in-version/partner/report.yaml | 120 ++++ .../HC-17/plus-in-version/redhat/report.yaml | 120 ++++ tests/data/cryostat-0.4.0+1.tgz | Bin 0 -> 16999 bytes .../HC-17_dash_in_version.feature | 18 +- .../common/utils/chart_certification.py | 10 +- .../behave_features/common/utils/github.py | 4 +- 26 files changed, 1642 insertions(+), 276 deletions(-) create mode 100644 scripts/src/precheck/__init__.py create mode 100644 scripts/src/precheck/serializer.py create mode 100644 scripts/src/precheck/serializer_test.py create mode 100644 scripts/src/precheck/submission.py create mode 100644 scripts/src/precheck/submission_test.py delete mode 100644 tests/data/HC-16/dash-in-version/partner/report.yaml delete mode 100644 tests/data/HC-16/dash-in-version/redhat/report.yaml create mode 100644 tests/data/HC-17/plus-in-version/partner/report.yaml create mode 100644 tests/data/HC-17/plus-in-version/redhat/report.yaml create mode 100644 tests/data/cryostat-0.4.0+1.tgz diff --git a/.github/actions/generate-chart-locks/action.yaml b/.github/actions/generate-chart-locks/action.yaml index dc15d63627..7c1245aee6 100644 --- a/.github/actions/generate-chart-locks/action.yaml +++ b/.github/actions/generate-chart-locks/action.yaml @@ -54,12 +54,12 @@ runs: echo "ref=${resolvedRef}" | tee -a $GITHUB_OUTPUT - name: Checkout id: clone-repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ steps.resolve.outputs.ref }} path: temp-gen-chart-lock-repo - name: Setting up python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" - name: Generate lock file JSON from existing charts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eea038fbc4..2da4fc8d0a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -266,7 +266,7 @@ jobs: - name: Get profile version set in report provided by the user id: get-profile-version if: ${{ needs.setup.outputs.run_build == 'true' && steps.verify_requires.outputs.report_provided == 'true' }} - uses: mikefarah/yq@v4 + uses: mikefarah/yq@master with: cmd: yq '.metadata.tool.profile.version' ${{ format('./pr-branch/{0}', steps.verify_requires.outputs.provided_report_relative_path) }} @@ -274,7 +274,7 @@ jobs: id: get-kube-range if: ${{ needs.setup.outputs.run_build == 'true' && steps.verify_requires.outputs.report_provided == 'true' }} continue-on-error: true - uses: mikefarah/yq@v4 + uses: mikefarah/yq@master with: cmd: yq '.metadata.chart.kubeversion' ${{ format('./pr-branch/{0}', steps.verify_requires.outputs.provided_report_relative_path) }} @@ -481,7 +481,7 @@ jobs: # The release tag format is -- - name: Create GitHub release if: ${{ needs.chart-verifier.outputs.web_catalog_only == 'False' }} - uses: softprops/action-gh-release@v0.1.15 + uses: softprops/action-gh-release@v1 with: tag_name: ${{ needs.chart-verifier.outputs.release_tag }} files: | @@ -533,6 +533,23 @@ jobs: https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} \ HEAD:refs/heads/${INDEX_BRANCH} + - name: Add a GitHub comment if release has failed + uses: actions/github-script@v7 + if: ${{ failure() && env.GITHUB_REPOSITORY != 'openshift-helm-charts/sandbox' }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `### Release job failed + + An error occured while updating the Helm repository index. + + cc @komish @mgoerens` + }); + # Note(komish): This step is a temporary workaround. Metrics requires the PR comment # to be available, but it is written to the filesystem in the previous job. # This can be removed once the metrics execution is restructured to have access to the PR diff --git a/.github/workflows/check-locks-on-owners-submission.yml b/.github/workflows/check-locks-on-owners-submission.yml index 0e9807a015..682eff97e5 100644 --- a/.github/workflows/check-locks-on-owners-submission.yml +++ b/.github/workflows/check-locks-on-owners-submission.yml @@ -27,7 +27,7 @@ jobs: github.event.pull_request.draft == false steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python 3.x Part 1 uses: actions/setup-python@v5 @@ -168,7 +168,7 @@ jobs: | - | - | | ${{ steps.gather-metadata.outputs.chart-name }} | ${{ steps.determine-lock-status.outputs.locked-to-path }} | - This OWNERS file is being ${{ steps.populate-file-mod-type.outputs.net-new-owners-file && '**created**' || '**modified**'}} in this pull request. + This OWNERS file is being ${{ steps.populate-file-mod-type.outputs.net-new-owners-file == 'true' && '**created**' || '**modified**'}} in this pull request. _This comment was auto-generated by [GitHub Actions](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})._ diff --git a/.github/workflows/lock-sanity-check.yml b/.github/workflows/lock-sanity-check.yml index f3696d33ae..923417aaa7 100644 --- a/.github/workflows/lock-sanity-check.yml +++ b/.github/workflows/lock-sanity-check.yml @@ -36,7 +36,7 @@ jobs: - name: notify maintainers on failure id: notify if: failure() && steps.generate.outcome == 'failure' && github.repository == 'openshift-helm-charts/charts' - uses: archive/github-actions-slack@v2.7.0 + uses: archive/github-actions-slack@v2.8.0 with: slack-bot-user-oauth-access-token: ${{ secrets.SLACK_BOT_USER_OAUTH_ACCESS_TOKEN }} slack-channel: C02979BDUPL diff --git a/.github/workflows/mercury_bot.yml b/.github/workflows/mercury_bot.yml index da09c27db3..c712bfdd28 100644 --- a/.github/workflows/mercury_bot.yml +++ b/.github/workflows/mercury_bot.yml @@ -79,7 +79,7 @@ jobs: - name: Determine if net-new OWNERS file id: populate-file-mod-type if: ${{ steps.check_for_owners.outputs.merge_pr == 'true' }} - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -105,7 +105,7 @@ jobs: # Only used to assert content of the OWNERS file. - name: Checkout Pull Request - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} diff --git a/.github/workflows/owners-redhat.yml b/.github/workflows/owners-redhat.yml index 29540fab7f..b888ff1bae 100644 --- a/.github/workflows/owners-redhat.yml +++ b/.github/workflows/owners-redhat.yml @@ -5,7 +5,7 @@ name: Red Hat OWNERS Files # submissions. on: - pull_request: + pull_request_target: paths: - charts/redhat/**/OWNERS - charts/community/redhat/**/OWNERS @@ -23,10 +23,10 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Clean up stale label - uses: actions/github-script@v3 + uses: actions/github-script@v7 continue-on-error: true with: - github-token: ${{secrets.GITHUB_TOKEN}} + github-token: ${{ secrets.GITHUB_TOKEN }} script: | const successLabel = '${{ env.SUCCESS_LABEL }}'; try { @@ -46,10 +46,10 @@ jobs: throw error } - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" @@ -81,23 +81,35 @@ jobs: needs: [gather-metadata] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" - name: Install Python CI tooling working-directory: scripts run: | make venv.tools + + # Only used to assert content of the OWNERS file. + - name: Checkout Pull Request + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + token: ${{ secrets.BOT_TOKEN }} + fetch-depth: 0 + path: "pr-branch" + - name: Assert Owners File Content id: assert-content + working-directory: pr-branch env: CATEGORY: ${{ needs.gather-metadata.outputs.category }} ORGANIZATION: ${{ needs.gather-metadata.outputs.organization }} CHARTNAME: ${{ needs.gather-metadata.outputs.chartname }} run: | - ./scripts/venv.tools/bin/assert-redhat-owners-file-meta \ + ../scripts/venv.tools/bin/assert-redhat-owners-file-meta \ "${CATEGORY}" \ "${ORGANIZATION}" \ "${CHARTNAME}" @@ -108,7 +120,7 @@ jobs: if: failure() && (needs.ensure-prefix-in-path.result == 'failure' || needs.ensure-owners-file-contents.result == 'failure') steps: - name: Send PR Comment - uses: actions/github-script@v6 + uses: actions/github-script@v7 env: PREFIX_CHECK_RESULT: ${{ needs.ensure-prefix-in-path.result != '' && needs.ensure-prefix-in-path.result || 'unknown' }} CONTENT_CHECK_RESULT: ${{ needs.ensure-owners-file-contents.result != '' && needs.ensure-owners-file-contents.result || 'unknown' }} @@ -149,7 +161,7 @@ jobs: if: success() steps: - name: Label PR - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/owners.yml b/.github/workflows/owners.yml index e8680070e3..f57b360276 100644 --- a/.github/workflows/owners.yml +++ b/.github/workflows/owners.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Python 3.x Part 1 - uses: actions/setup-python@4 + uses: actions/setup-python@v5 with: python-version: "3.10" diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 98e38b4ffd..ef110623c0 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -25,6 +25,7 @@ pytest-forked==1.3.0 pytest-xdist==2.4.0 PyYAML==6.0.1 requests==2.26.0 +responses==0.23.1 retrying==1.3.3 semantic-version==2.8.5 semver==2.13.0 diff --git a/scripts/src/checkprcontent/checkpr.py b/scripts/src/checkprcontent/checkpr.py index 6973880e22..29932af7d8 100644 --- a/scripts/src/checkprcontent/checkpr.py +++ b/scripts/src/checkprcontent/checkpr.py @@ -113,7 +113,7 @@ def get_file_match_compiled_patterns(): pattern = re.compile(base + r"/.*") reportpattern = re.compile(base + r"/report.yaml") - tarballpattern = re.compile(base + r"/(.*\.tgz$)") + tarballpattern = re.compile(base + r"/(.*\.tgz)") return pattern, reportpattern, tarballpattern diff --git a/scripts/src/metrics/pushowners.py b/scripts/src/metrics/pushowners.py index d1d8b2937b..8f49dd8f6a 100644 --- a/scripts/src/metrics/pushowners.py +++ b/scripts/src/metrics/pushowners.py @@ -7,6 +7,10 @@ from owners import owners_file +def bool_to_yes_no(my_bool): + return "Yes" if my_bool else "No" + + def getVendorType(changed_file): path_as_list = changed_file.split("/") for i in range(len(path_as_list) - 1): @@ -16,28 +20,25 @@ def getVendorType(changed_file): def getFileContent(changed_file): - status, owner_data = owners_file.get_owner_data_from_file(changed_file) - if status is True: - users_included = owners_file.get_users_included(owner_data) - web_catalog_only = owners_file.get_web_catalog_only(owner_data) - if not web_catalog_only: - web_catalog_only_string = "No" - else: - web_catalog_only_string = "Yes" - vendor_name = owners_file.get_vendor(owner_data) - chart_name = owners_file.get_chart(owner_data) - vendor_type = getVendorType(changed_file) - return ( - users_included, - web_catalog_only_string, - vendor_name, - chart_name, - vendor_type, - ) - else: - print("Exception loading OWNERS file") + try: + owner_data = owners_file.get_owner_data_from_file(changed_file) + except owners_file.OwnersFileError as e: + print("Exception loading OWNERS file: {e}") return "", "", "", "", "" + users_included = owners_file.get_users_included(owner_data) + web_catalog_only = owners_file.get_web_catalog_only(owner_data) + vendor_name = owners_file.get_vendor(owner_data) + chart_name = owners_file.get_chart(owner_data) + vendor_type = getVendorType(changed_file) + return ( + bool_to_yes_no(users_included), + bool_to_yes_no(web_catalog_only), + vendor_name, + chart_name, + vendor_type, + ) + def process_pr(added_file, modified_file): if modified_file != "": diff --git a/scripts/src/owners/owners_file.py b/scripts/src/owners/owners_file.py index f8913cd1e4..c348922a11 100644 --- a/scripts/src/owners/owners_file.py +++ b/scripts/src/owners/owners_file.py @@ -1,3 +1,4 @@ +import contextlib import os import yaml @@ -8,70 +9,92 @@ from yaml import SafeLoader +class OwnersFileError(Exception): + pass + + +class ConfigKeyMissing(Exception): + pass + + def get_owner_data(category, organization, chart): path = os.path.join("charts", category, organization, chart, "OWNERS") - status, owner_content = get_owner_data_from_file(path) - return status, owner_content + success = False + + try: + owner_content = get_owner_data_from_file(path) + success = True + except OwnersFileError as e: + print(f"Error getting OWNERS file data: {e}") + + return success, owner_content def get_owner_data_from_file(owner_path): try: with open(owner_path) as owner_data: owner_content = yaml.load(owner_data, Loader=SafeLoader) - return True, owner_content - except Exception as err: - print(f"Exception loading OWNERS file: {err}") - return False, "" + except yaml.YAMLError as e: + print(f"Exception loading OWNERS file: {e}") + raise OwnersFileError from e + except OSError as e: + print(f"Error opening OWNERS file: {e}") + raise OwnersFileError from e + + return owner_content def get_vendor(owner_data): - vendor = "" - try: - vendor = owner_data["vendor"]["name"] - except Exception: - pass - return vendor + vendor_name = "" + with contextlib.suppress(KeyError): + vendor_name = owner_data["vendor"]["name"] + return vendor_name def get_vendor_label(owner_data): - vendor = "" - try: - vendor = owner_data["vendor"]["label"] - except Exception: - pass - return vendor + vendor_label = "" + with contextlib.suppress(KeyError): + vendor_label = owner_data["vendor"]["label"] + return vendor_label def get_chart(owner_data): chart = "" - try: + with contextlib.suppress(KeyError): chart = owner_data["chart"]["name"] - except Exception: - pass return chart -def get_web_catalog_only(owner_data): - web_catalog_only = False - try: - if "webCatalogOnly" in owner_data: - web_catalog_only = owner_data["webCatalogOnly"] - elif "providerDelivery" in owner_data: - web_catalog_only = owner_data["providerDelivery"] - except Exception: - pass - return web_catalog_only +def get_web_catalog_only(owner_data, raise_if_missing=False): + """Check the delivery method set in the OWNERS file data + + Args: + owner_data (dict): Content of the OWNERS file. Typically this is the return value of the + get_owner_data or get_owner_data_from_file function. + raise_if_missing (bool, optional): Whether to raise an Exception if the delivery method is + not set in the OWNERS data. If set to False, the function returns False. + + Raises: + ConfigKeyMissing: if the key is not found in OWNERS and raise_if_missing is set to True + + """ + if ( + "web_catalog_only" not in owner_data + and "providerDelivery" not in owner_data + and raise_if_missing + ): + raise ConfigKeyMissing( + "Neither web_catalog_only nor providerDelivery keys were set" + ) + + return owner_data.get("web_catalog_only", False) or owner_data.get( + "providerDelivery", False + ) def get_users_included(owner_data): - users_included = "No" - try: - users = owner_data["users"] - if len(users) != 0: - return "Yes" - except Exception: - pass - return users_included + users = owner_data.get("users", list()) + return len(users) != 0 def get_pgp_public_key(owner_data): diff --git a/scripts/src/precheck/__init__.py b/scripts/src/precheck/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/src/precheck/serializer.py b/scripts/src/precheck/serializer.py new file mode 100644 index 0000000000..8dba1b6a25 --- /dev/null +++ b/scripts/src/precheck/serializer.py @@ -0,0 +1,47 @@ +"""Contains the logic to serialize / deserialize a Submission object to / from JSON. + +A pair of custom JSONEncoder / JSONDecoder is required due to the fact that the Submission class +contains nested classes. + +""" + +import copy +import json + +from precheck import submission + + +class SubmissionEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, submission.Submission): + obj_dict = copy.deepcopy(obj.__dict__) + obj_dict["chart"] = obj_dict["chart"].__dict__ + obj_dict["report"] = obj_dict["report"].__dict__ + obj_dict["source"] = obj_dict["source"].__dict__ + obj_dict["tarball"] = obj_dict["tarball"].__dict__ + return obj_dict + + return json.JSONEncoder.default(self, obj) + + +class SubmissionDecoder(json.JSONDecoder): + def __init__(self, *args, **kwargs): + json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs) + + def object_hook(self, dct): + if "chart" in dct: + chart_obj = submission.Chart(**dct["chart"]) + report_obj = submission.Report(**dct["report"]) + source_obj = submission.Source(**dct["source"]) + tarball_obj = submission.Tarball(**dct["tarball"]) + + to_merge_dct = { + "chart": chart_obj, + "report": report_obj, + "source": source_obj, + "tarball": tarball_obj, + } + + new_dct = dct | to_merge_dct + return submission.Submission(**new_dct) + return dct diff --git a/scripts/src/precheck/serializer_test.py b/scripts/src/precheck/serializer_test.py new file mode 100644 index 0000000000..51cf4ed708 --- /dev/null +++ b/scripts/src/precheck/serializer_test.py @@ -0,0 +1,98 @@ +import json + +from precheck import serializer +from precheck import submission + +submission_json = """ +{ + "api_url": "https://api.github.com/repos/openshift-helm-charts/charts/pulls/1", + "modified_files": ["charts/partners/acme/awesome/1.42.0/report.yaml"], + "chart": { + "category": "partners", + "organization": "acme", + "name": "awesome", + "version": "1.42.0" + }, + "report": { + "found": true, + "signed": false, + "path": "charts/partners/acme/awesome/1.42.0/report.yaml" + }, + "source": { + "found": false, + "path": null + }, + "tarball": { + "found": false, + "path": null, + "provenance": null + }, + "modified_owners": [], + "modified_unknown": [], + "is_web_catalog_only": true +} +""" + + +def sanitize_json_string(json_string: str): + """Remove the newlines from the JSON string. This is done by + loading and dumping the string representation of the JSON object. + Goal is to allow comparison with other JSON string. + """ + json_dict = json.loads(json_string) + return json.dumps(json_dict) + + +def test_submission_serializer(): + s = json.loads(submission_json, cls=serializer.SubmissionDecoder) + + assert isinstance(s, submission.Submission) + assert ( + s.api_url == "https://api.github.com/repos/openshift-helm-charts/charts/pulls/1" + ) + assert "charts/partners/acme/awesome/1.42.0/report.yaml" in s.modified_files + assert s.chart.category == "partners" + assert s.chart.organization == "acme" + assert s.chart.name == "awesome" + assert s.chart.version == "1.42.0" + assert s.report.found + assert not s.report.signed + assert s.report.path == "charts/partners/acme/awesome/1.42.0/report.yaml" + assert not s.source.found + assert not s.source.path + assert not s.tarball.found + assert not s.tarball.path + assert not s.tarball.provenance + assert s.is_web_catalog_only + + +def test_submission_deserializer(): + s = submission.Submission( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/1", + modified_files=["charts/partners/acme/awesome/1.42.0/report.yaml"], + chart=submission.Chart( + category="partners", + organization="acme", + name="awesome", + version="1.42.0", + ), + report=submission.Report( + found=True, + signed=False, + path="charts/partners/acme/awesome/1.42.0/report.yaml", + ), + source=submission.Source( + found=False, + path=None, + ), + tarball=submission.Tarball( + found=False, + path=None, + provenance=None, + ), + is_web_catalog_only=True, + ) + + assert serializer.SubmissionEncoder().encode(s) == sanitize_json_string( + submission_json + ) diff --git a/scripts/src/precheck/submission.py b/scripts/src/precheck/submission.py new file mode 100644 index 0000000000..2ed6175aaf --- /dev/null +++ b/scripts/src/precheck/submission.py @@ -0,0 +1,441 @@ +import os +import re +import semver + +from dataclasses import dataclass, field + +from checkprcontent import checkpr +from owners import owners_file +from tools import gitutils +from reporegex import matchers +from report import verifier_report + +xRateLimit = "X-RateLimit-Limit" +xRateRemain = "X-RateLimit-Remaining" + + +class SubmissionError(Exception): + """Root Exception for handling any error with the submission""" + + pass + + +class DuplicateChartError(SubmissionError): + """This Exception is to be raised when the user attempts to submit a PR with more than one chart""" + + pass + + +class VersionError(SubmissionError): + """This Exception is to be raised when the version of the chart is not semver compatible""" + + pass + + +class WebCatalogOnlyError(SubmissionError): + pass + + +@dataclass +class Chart: + """Represents a Helm Chart + + Once set, the category, organization, name and version of the chart cannot be modified. + + """ + + category: str = None + organization: str = None + name: str = None + version: str = None + + def register_chart_info(self, category, organization, name, version): + if ( + (self.category and self.category != category) + or (self.organization and self.organization != organization) + or (self.name and self.name != name) + or (self.version and self.version != version) + ): + msg = "[ERROR] A PR must contain only one chart. Current PR includes files for multiple charts." + raise DuplicateChartError(msg) + + if not semver.VersionInfo.isvalid(version): + msg = ( + f"[ERROR] Helm chart version is not a valid semantic version: {version}" + ) + raise VersionError(msg) + + self.category = category + self.organization = organization + self.name = name + self.version = version + + def get_owners_path(self): + return f"charts/{self.category}/{self.organization}/{self.name}/OWNERS" + + +@dataclass +class Report: + found: bool = False + signed: bool = False + path: str = None + + +@dataclass +class Source: + found: bool = False + path: str = None # Path to the Chart.yaml + + +@dataclass +class Tarball: + found: bool = False + path: str = None + provenance: str = None + + +@dataclass +class Submission: + """Represents a GitHub PR, opened to either certify a new Helm chart or add / modify an OWNERS file. + + A Submission can be instantiated either: + * by solely providing the URL of a given PR (represented by the api_url attribute). Upon + initialization (see __post_init__ method), the rest of the information is retrieved from the + GitHub API. This should typically occur once per pipeline run, at the start. + * by providing all class attributes. This is typically done by loading a JSON representation of + a Submission from a file, and should be done several times per pipeline runs, in later jobs. + + """ + + api_url: str + modified_files: list[str] = None + chart: Chart = field(default_factory=lambda: Chart()) + report: Report = field(default_factory=lambda: Report()) + source: Source = field(default_factory=lambda: Source()) + tarball: Tarball = field(default_factory=lambda: Tarball()) + modified_owners: list[str] = field(default_factory=list) + modified_unknown: list[str] = field(default_factory=list) + is_web_catalog_only: bool = None + + def __post_init__(self): + """Complete the initialization of the Submission object. + + Only retrieve PR information from the GitHub API if requiered, by checking for the presence + of a value for the modified_files attributes. This check allows to make the distinction + between the two aforementioned cases of initialization of a Submission object: + * If modified_files is not set, we're in the case of initializing a brand new Submission + and need to retrieve the rest of the information from the GitHub API. + * If a value is set for modified_files, that means we are loading an existing Submission + object from a file. + + """ + if not self.modified_files: + self.modified_files = [] + self._get_modified_files() + self._parse_modified_files() + + def _get_modified_files(self): + """Query the GitHub API in order to retrieve the list of files that are added / modified by + this PR""" + page_number = 1 + max_page_size, page_size = 100, 100 + files_api_url = re.sub(r"^https://api\.github\.com/", "", self.api_url) + + while page_size == max_page_size: + files_api_query = ( + f"{files_api_url}/files?per_page={page_size}&page={page_number}" + ) + print(f"[INFO] Query files : {files_api_query}") + + try: + r = gitutils.github_api( + "get", files_api_query, os.environ.get("BOT_TOKEN") + ) + except SystemExit as e: + raise SubmissionError(e) + + files = r.json() + page_size = len(files) + page_number += 1 + + if xRateLimit in r.headers: + print(f"[DEBUG] {xRateLimit} : {r.headers[xRateLimit]}") + if xRateRemain in r.headers: + print(f"[DEBUG] {xRateRemain} : {r.headers[xRateRemain]}") + + if "message" in files: + msg = f'[ERROR] getting pr files: {files["message"]}' + raise SubmissionError(msg) + else: + for file in files: + if "filename" in file: + self.modified_files.append(file["filename"]) + + def _parse_modified_files(self): + """Classify the list of modified files. + + Modified files are categorized into 5 groups, mapping to 5 class attributes: + - The `report` attribute has information about files related to the chart-verifier report: + the report.yaml itself and, if signed, its signature report.yaml.asc. + - The `source` attribute has information about files related to the chart's source: all + files, if any, under the src/ directory. + - The `tarball` attribute has information about files related to the chart's source as + tarball: the .tgz tarball itself and, if signed, the .prov provenance file. + - A list of added / modified OWNERS files is recorded in the `modified_owners` attribute. + - The rest of the files are classified in the `modified_unknown` attribute. + + Raises a SubmissionError if: + * The Submission concerns more than one chart + * The version of the chart is not SemVer compatible + * The tarball file is named incorrectly + + """ + for file_path in self.modified_files: + file_category, match = get_file_type(file_path) + if file_category == "report": + self.chart.register_chart_info(*match.groups()) + self.set_report(file_path) + elif file_category == "source": + self.chart.register_chart_info(*match.groups()) + self.set_source(file_path) + elif file_category == "tarball": + category, organization, name, version, _ = match.groups() + self.chart.register_chart_info(category, organization, name, version) + self.set_tarball(file_path, match) + elif file_category == "owners": + self.modified_owners.append(file_path) + elif file_category == "unknwown": + self.modified_unknown.append(file_path) + + def set_report(self, file_path): + """Action to take when a file related to the chart-verifier is found. + + This can either be the report.yaml itself, or the signing key report.yaml.asc + + """ + if os.path.basename(file_path) == "report.yaml": + print(f"[INFO] Report found: {file_path}") + self.report.found = True + self.report.path = file_path + elif os.path.basename(file_path) == "report.yaml.asc": + self.report.signed = True + else: + self.modified_unknown.append(file_path) + + def set_source(self, file_path): + """Action to take when a file related to the chart's source is found. + + Note that while the source of the Chart can be composed of many files, only the Chart.yaml + is actually registered. + + """ + if os.path.basename(file_path) == "Chart.yaml": + self.source.found = True + self.source.path = file_path + + def set_tarball(self, file_path, tarball_match): + """Action to take when a file related to the tarball is found. + + This can either be the .tgz tarball itself, or the .prov provenance key. + + """ + _, file_extension = os.path.splitext(file_path) + if file_extension == ".tgz": + print(f"[INFO] tarball found: {file_path}") + self.tarball.found = True + self.tarball.path = file_path + + _, _, chart_name, chart_version, tar_name = tarball_match.groups() + expected_tar_name = f"{chart_name}-{chart_version}.tgz" + if tar_name != expected_tar_name: + msg = f"[ERROR] the tgz file is named incorrectly. Expected: {expected_tar_name}. Got: {tar_name}" + raise SubmissionError(msg) + elif file_extension == ".prov": + self.tarball.provenance = file_path + else: + self.modified_unknown.append(file_path) + + def is_valid_certification_submission(self): + """Check wether the files in this Submission are valid to attempt to certify a Chart + + We expect the user to provide either: + * Only a report file + * Only a chart - either as source or tarball + * Both the report and the chart + + Returns False if: + * The user attempts to create the OWNERS file for its project. + * The PR contains additional files, not related to the Chart being submitted + + Returns True in all other cases + + """ + if self.modified_owners: + return False, "[ERROR] Send OWNERS file by itself in a separate PR." + + if self.modified_unknown: + msg = ( + "[ERROR] PR includes one or more files not related to charts: " + + ", ".join(self.modified_unknown) + ) + return False, msg + + if self.report.found or self.source.found or self.tarball.found: + return True, "" + + return False, "" + + def is_valid_owners_submission(self): + """Check wether the file in this Submission are valid for an OWNERS PR + + Returns True if the PR only modified files is an OWNERS file. + + Returns False in all other cases. + """ + if len(self.modified_owners) == 1 and len(self.modified_files) == 1: + return True, "" + + msg = "" + if self.modified_owners: + msg = "[ERROR] Send OWNERS file by itself in a separate PR." + else: + msg = "No OWNERS file provided" + + return False, msg + + def parse_web_catalog_only(self, repo_path=""): + """Set the web_catalog_only attribute + + This is achieved by: + - Parsing the associated OWNERS file and check the value of the WebCatalogOnly key + - Parsing report file (if submitted) and check the value of the WebCatalogOnly key + + Args: + repo_path (str): path under which the repo has been cloned on the local filesystem + + Returns: + bool: True if WebCatalog is set to True in both the OWNERS and in the report file. + False if WebCatalogOnly is set to False in the OWNERS file + + Raise: + WebCatalogOnlyError in one of the following cases: + * The OWNERS file doesn't exist at the expected path + * The OWNERS file doesn't contain WebCatalogOnly + * The submitted report cannot be found or read at the expected path (although + report.found is set to True) + * The submitted report doesn't contain WebCatalogOnly + * The WebCatalogOnly key don't match between the OWNERS and the report files + + """ + owners_path = os.path.join(repo_path, self.chart.get_owners_path()) + + try: + owners_data = owners_file.get_owner_data_from_file(owners_path) + except owners_file.OwnersFileError as e: + raise WebCatalogOnlyError( + f"Failed to get OWNERS data at {owners_path}" + ) from e + + try: + owners_web_catalog_only = owners_file.get_web_catalog_only( + owners_data, raise_if_missing=True + ) + except owners_file.ConfigKeyMissing as e: + raise WebCatalogOnlyError( + f"Failed to find webCatalogOnly key in OWNERS data at {owners_path}" + ) from e + + if self.report.found: + report_path = os.path.join(repo_path, self.report.path) + + found, report_data = verifier_report.get_report_data(report_path) + if not found: + raise WebCatalogOnlyError(f"Failed to get report data at {report_path}") + + try: + report_web_catalog_only = verifier_report.get_web_catalog_only( + report_data, raise_if_missing=True + ) + except verifier_report.ConfigKeyMissing as e: + raise WebCatalogOnlyError( + f"Failed to find webCatalogOnly key in report data at {owners_path}" + ) from e + print( + f"[INFO] webCatalogOnly/providerDelivery from report : {report_web_catalog_only}" + ) + + if not owners_web_catalog_only == report_web_catalog_only: + raise WebCatalogOnlyError( + f"Value of web_catalog_only in OWNERS ({owners_web_catalog_only}) doesn't match the value in report ({report_web_catalog_only})" + ) + + self.is_web_catalog_only = owners_web_catalog_only + + def is_valid_web_catalog_only(self, repo_path=""): + """Verify that the submission is coherent with being a WebCatalogOnly submission + + A valid web_catalog_only submission must: + * contain only a report + * the report must specify a package digest + + Args: + repo_path (str): path under which the repo has been cloned on the local filesystem + + Returns: + bool: set to True if the submission is a valid WebCatalogOnly submission. + + Raise: + WebCatalogOnlyError if the submitted report cannot be found or read at the expected path. + + """ + if not self.report.found: + return False + + if len(self.modified_files) > 1: + return False + + report_path = os.path.join(repo_path, self.report.path) + found, report_data = verifier_report.get_report_data(report_path) + if not found: + raise WebCatalogOnlyError(f"Failed to get report data at {report_path}") + + return verifier_report.get_package_digest(report_data) is not None + + +def get_file_type(file_path): + """Determine the category of a given file + + As part of a PR, a modified file can relate to one of 5 categories: + - The chart-verifier report + - The source of the chart + - The tarball of the chart + - OWNERS file + - or another "unknown" category + + """ + pattern, reportpattern, tarballpattern = checkpr.get_file_match_compiled_patterns() + owners_pattern = re.compile( + matchers.submission_path_matcher(include_version_matcher=False) + r"/OWNERS" + ) + src_pattern = re.compile(matchers.submission_path_matcher() + r"/src/") + + # Match all files under charts//// + match = pattern.match(file_path) + if match: + report_match = reportpattern.match(file_path) + if report_match: + return "report", report_match + + src_match = src_pattern.match(file_path) + if src_match: + return "source", src_match + + tar_match = tarballpattern.match(file_path) + if tar_match: + return "tarball", tar_match + else: + owners_match = owners_pattern.match(file_path) + if owners_match: + return "owners", owners_match + + return "unknwown", None diff --git a/scripts/src/precheck/submission_test.py b/scripts/src/precheck/submission_test.py new file mode 100644 index 0000000000..161b048063 --- /dev/null +++ b/scripts/src/precheck/submission_test.py @@ -0,0 +1,620 @@ +"""Unit tests for the Submission Class + +Each test is run against a list of "Scenarios": +- A dataclass named after the function under test and suffixed with "Scenario" is defined +- A list of scenarios to be run against the function under test is created +- The test function is called using the pytest.mark.parametrize decorator + +""" + +import contextlib +import pytest +import os +import responses +import tempfile + +from dataclasses import dataclass, field + +from precheck import submission + +# Define assets that are being reused accross tests +expected_category = "partners" +expected_organization = "acme" +expected_name = "awesome" +expected_version = "1.42.0" + +expected_chart = submission.Chart( + category=expected_category, + organization=expected_organization, + name=expected_name, + version=expected_version, +) + + +def make_new_report_only_submission(): + """Return an initialized Submission that contains only an unsigned report file + + This is a relatively common use case that is used for many scenarios. + """ + s = submission.Submission( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/1", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/report.yaml" + ], + chart=expected_chart, + report=submission.Report( + found=True, + signed=False, + path=f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/report.yaml", + ), + ) + + return s + + +def make_new_tarball_only_submission(): + """Return an initialized Submission that contains only an unsigned tarball""" + s = submission.Submission( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/4", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/{expected_name}-{expected_version}.tgz" + ], + chart=expected_chart, + tarball=submission.Tarball( + found=True, + provenance=None, + path=f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/{expected_name}-{expected_version}.tgz", + ), + ) + return s + + +@dataclass +class SubmissionInitScenario: + api_url: str + modified_files: list[str] + expected_submission: submission.Submission = None + excepted_exception: contextlib.ContextDecorator = field( + default_factory=lambda: contextlib.nullcontext() + ) + + +scenarios_submission_init = [ + # PR contains a unique and unsigned report.yaml + SubmissionInitScenario( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/1", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/report.yaml" + ], + expected_submission=make_new_report_only_submission(), + ), + # PR contains a signed report + SubmissionInitScenario( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/2", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/report.yaml", + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/report.yaml.asc", + ], + expected_submission=submission.Submission( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/2", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/report.yaml", + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/report.yaml.asc", + ], + chart=expected_chart, + report=submission.Report( + found=True, + signed=True, + path=f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/report.yaml", + ), + ), + ), + # PR contains the chart's source + SubmissionInitScenario( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/3", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/src/Chart.yaml", + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/src/templates/buildconfig.yam" + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/src/templates/deployment.yaml", + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/src/templates/imagestream.yam" + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/src/templates/route.yaml", + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/src/templates/service.yaml", + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/src/values.schema.json", + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/src/values.yaml", + ], + expected_submission=submission.Submission( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/3", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/src/Chart.yaml", + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/src/templates/buildconfig.yam" + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/src/templates/deployment.yaml", + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/src/templates/imagestream.yam" + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/src/templates/route.yaml", + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/src/templates/service.yaml", + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/src/values.schema.json", + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/src/values.yaml", + ], + chart=expected_chart, + source=submission.Source( + found=True, + path=f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/src/Chart.yaml", + ), + ), + ), + # PR contains an unsigned tarball + SubmissionInitScenario( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/4", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/{expected_name}-{expected_version}.tgz" + ], + expected_submission=make_new_tarball_only_submission(), + ), + # PR contains a signed tarball + SubmissionInitScenario( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/5", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/{expected_name}-{expected_version}.tgz", + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/{expected_name}-{expected_version}.tgz.prov", + ], + expected_submission=submission.Submission( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/5", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/{expected_name}-{expected_version}.tgz", + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/{expected_name}-{expected_version}.tgz.prov", + ], + chart=expected_chart, + tarball=submission.Tarball( + found=True, + provenance=f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/{expected_name}-{expected_version}.tgz.prov", + path=f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/{expected_name}-{expected_version}.tgz", + ), + ), + ), + # PR contains an OWNERS file + SubmissionInitScenario( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/6", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/OWNERS" + ], + expected_submission=submission.Submission( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/6", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/OWNERS" + ], + modified_owners=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/OWNERS" + ], + ), + ), + # PR contains additional files, not fitting into any expected category + SubmissionInitScenario( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/7", + modified_files=["charts/path/to/some/file"], + expected_submission=submission.Submission( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/7", + modified_files=["charts/path/to/some/file"], + modified_unknown=["charts/path/to/some/file"], + ), + ), + # Invalid PR contains multiple reports, referencing multiple charts + SubmissionInitScenario( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/101", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/report.yaml", + f"charts/{expected_category}/{expected_organization}/other-chart/{expected_version}/report.yaml", + ], + excepted_exception=pytest.raises(submission.DuplicateChartError), + ), + # Invalid PR contains a tarball with an incorrect name + SubmissionInitScenario( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/102", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/incorrectly-named.tgz" + ], + excepted_exception=pytest.raises(submission.SubmissionError), + ), + # Invalid PR references a Chart with a version that is not Semver compatible + SubmissionInitScenario( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/103", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/0.1.2.3.4/report.yaml" + ], + excepted_exception=pytest.raises(submission.VersionError), + ), +] + + +@pytest.mark.parametrize("test_scenario", scenarios_submission_init) +@responses.activate +def test_submission_init(test_scenario): + """Test the instantiation of a Submission in different scenarios""" + + # Mock GitHub API + responses.get( + f"{test_scenario.api_url}/files", + json=[{"filename": file} for file in test_scenario.modified_files], + ) + + with test_scenario.excepted_exception: + s = submission.Submission(api_url=test_scenario.api_url) + assert s == test_scenario.expected_submission + + +@responses.activate +def test_submission_not_exist(): + """Test creating a Submission for an unexisting PR""" + + api_url_doesnt_exist = ( + "https://api.github.com/repos/openshift-helm-charts/charts/pulls/9999" + ) + + responses.get( + f"{api_url_doesnt_exist}/files", + json={ + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest/pulls/pulls#list-pull-requests-files", + }, + ) + + with pytest.raises(submission.SubmissionError): + submission.Submission(api_url=api_url_doesnt_exist) + + +@dataclass +class CertificationScenario: + input_submission: submission.Submission + expected_is_valid_certification: bool + expected_reason: str = "" + + +scenarios_certification_submission = [ + # Valid certification Submission contains only a report + CertificationScenario( + input_submission=make_new_report_only_submission(), + expected_is_valid_certification=True, + ), + # Invalid certification Submission contains OWNERS file + CertificationScenario( + input_submission=submission.Submission( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/1", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/OWNERS" + ], + modified_owners=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/OWNERS" + ], + ), + expected_is_valid_certification=False, + expected_reason="[ERROR] Send OWNERS file by itself in a separate PR.", + ), + # Invalid certification Submission contains unknown files + CertificationScenario( + input_submission=submission.Submission( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/1", + modified_files=["charts/path/to/some/file"], + modified_unknown=["charts/path/to/some/file"], + ), + expected_is_valid_certification=False, + expected_reason="[ERROR] PR includes one or more files not related to charts:", + ), +] + + +@pytest.mark.parametrize("test_scenario", scenarios_certification_submission) +def test_is_valid_certification(test_scenario): + is_valid_certification, reason = ( + test_scenario.input_submission.is_valid_certification_submission() + ) + assert test_scenario.expected_is_valid_certification == is_valid_certification + assert test_scenario.expected_reason in reason + + +@dataclass +class OwnersScenario: + input_submission: submission.Submission + expected_is_valid_owners: bool + expected_reason: str = "" + + +scenarios_owners_submission = [ + # Valid submission contains only one OWNERS file + OwnersScenario( + input_submission=submission.Submission( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/1", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/OWNERS" + ], + modified_owners=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/OWNERS" + ], + ), + expected_is_valid_owners=True, + ), + # Invalid submission contains multiple OWNERS file + OwnersScenario( + input_submission=submission.Submission( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/1", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/OWNERS", + f"charts/{expected_category}/{expected_organization}/another_chart/OWNERS", + ], + modified_owners=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/OWNERS", + f"charts/{expected_category}/{expected_organization}/another_chart/OWNERS", + ], + ), + expected_is_valid_owners=False, + expected_reason="[ERROR] Send OWNERS file by itself in a separate PR.", + ), + # Invalid submission contains unknown files + OwnersScenario( + input_submission=make_new_report_only_submission(), + expected_is_valid_owners=False, + expected_reason="No OWNERS file provided", + ), + # Invalid submission doesn't contain an OWNER file +] + + +@pytest.mark.parametrize("test_scenario", scenarios_owners_submission) +def test_is_valid_owners(test_scenario): + is_valid_owners, reason = ( + test_scenario.input_submission.is_valid_owners_submission() + ) + assert test_scenario.expected_is_valid_owners == is_valid_owners + assert test_scenario.expected_reason in reason + + +@dataclass +class WebCatalogOnlyScenario: + input_submission: submission.Submission + create_owners: bool = True + create_report: bool = True + owners_web_catalog_only: str = None # Set to None to skip key creation in OWNERS + report_web_catalog_only: str = None # Set to None to skip key creation in report + expected_output: bool = None + excepted_exception: contextlib.ContextDecorator = field( + default_factory=lambda: contextlib.nullcontext() + ) + + +scenarios_web_catalog_only = [ + # Submission contains a report file with WebCatalogOnly set to True, matching the content of OWNERS + WebCatalogOnlyScenario( + input_submission=make_new_report_only_submission(), + owners_web_catalog_only="true", + report_web_catalog_only="true", + expected_output=True, + ), + # Submission contains a report file with WebCatalogOnly set to False, matching the content of OWNERS + WebCatalogOnlyScenario( + input_submission=make_new_report_only_submission(), + owners_web_catalog_only="false", + report_web_catalog_only="false", + expected_output=False, + ), + # Submission contains a report file with WebCatalogOnly set to True, not matching the content of OWNERS + WebCatalogOnlyScenario( + input_submission=make_new_report_only_submission(), + owners_web_catalog_only="true", + report_web_catalog_only="false", + excepted_exception=pytest.raises( + submission.WebCatalogOnlyError, match="doesn't match the value" + ), + ), + # Submission contains a report file with WebCatalogOnly set to False, not matching the content of OWNERS + WebCatalogOnlyScenario( + input_submission=make_new_report_only_submission(), + owners_web_catalog_only="false", + report_web_catalog_only="true", + excepted_exception=pytest.raises( + submission.WebCatalogOnlyError, match="doesn't match the value" + ), + ), + # Submission doesn't relate to an existing OWNERS + WebCatalogOnlyScenario( + input_submission=make_new_report_only_submission(), + create_owners=False, + report_web_catalog_only="true", + excepted_exception=pytest.raises( + submission.WebCatalogOnlyError, match="Failed to get OWNERS data" + ), + ), + # Submission contains a report, but it can't be found + WebCatalogOnlyScenario( + input_submission=make_new_report_only_submission(), + owners_web_catalog_only="true", + create_report=False, + excepted_exception=pytest.raises( + submission.WebCatalogOnlyError, match="Failed to get report data" + ), + ), + # The OWNERS file for this chart doesn't contain the WebCatalogOnly key + WebCatalogOnlyScenario( + input_submission=make_new_report_only_submission(), + owners_web_catalog_only=None, + report_web_catalog_only="false", + excepted_exception=pytest.raises(submission.WebCatalogOnlyError), + ), + # Submission contains a report, that doesn't contain the WebCatalogOnly key + WebCatalogOnlyScenario( + input_submission=make_new_report_only_submission(), + owners_web_catalog_only=None, + report_web_catalog_only="false", + excepted_exception=pytest.raises(submission.WebCatalogOnlyError), + ), + # Submission doesn't contain a report, OWNERS file has WebCatalogOnly set to True + # Note that this is not a valid scenario: if webCatalogOnly is set to True in the OWNERS file, + # all chart submissions should contain only a report. This check is part of the + # is_valid_web_catalog_only method. + WebCatalogOnlyScenario( + input_submission=make_new_tarball_only_submission(), + owners_web_catalog_only="true", + report_web_catalog_only=None, + expected_output=True, + ), + # Submission doesn't contain a report, OWNERS file has WebCatalogOnly set to False + WebCatalogOnlyScenario( + input_submission=make_new_tarball_only_submission(), + owners_web_catalog_only="false", + report_web_catalog_only=None, + expected_output=False, + ), +] + + +@pytest.mark.parametrize("test_scenario", scenarios_web_catalog_only) +def test_parse_web_catalog_only(test_scenario): + """Use a temporary directory, which content mimic the certification repository + + A OWNERS file and a report.yaml are placed at the correct location, containing the minimum + information required for this test to function (a webCatalogOnly value). + + """ + with tempfile.TemporaryDirectory(dir=".") as temp_dir: + # Create directory structure + owners_base_path = os.path.join( + temp_dir, + "charts", + expected_category, + expected_organization, + expected_name, + ) + chart_base_path = os.path.join( + owners_base_path, + expected_version, + ) + os.makedirs(chart_base_path) + + # Populate OWNERS file + if test_scenario.create_owners: + owners_file = open(os.path.join(owners_base_path, "OWNERS"), "w") + owners_file.write( + "publicPgpKey: unknown" + ) # Make sure OWNERS is not an empty file + if test_scenario.owners_web_catalog_only: + owners_file.write( + f"\nproviderDelivery: {test_scenario.owners_web_catalog_only}" + ) + owners_file.close() + + # Populate report.yaml file + if test_scenario.create_report: + report_file = open(os.path.join(chart_base_path, "report.yaml"), "w") + report_file.writelines( + [ + "apiversion: v1", + "\nkind: verify-report", + ] + ) + if test_scenario.report_web_catalog_only: + report_file.writelines( + [ + "\nmetadata:", + "\n tool:", + f"\n webCatalogOnly: {test_scenario.report_web_catalog_only}", + ] + ) + report_file.close() + + with test_scenario.excepted_exception: + test_scenario.input_submission.parse_web_catalog_only(repo_path=temp_dir) + + assert ( + test_scenario.input_submission.is_web_catalog_only + == test_scenario.expected_output + ) + + +@dataclass +class IsWebCatalogOnlyScenario: + input_submission: submission.Submission + create_report: bool = True + report_has_digest: bool = None + expected_output: bool = None + + +scenarios_is_web_catalog_only = [ + # Submission contains only a report, and report contains a digest + IsWebCatalogOnlyScenario( + input_submission=make_new_report_only_submission(), + report_has_digest=True, + expected_output=True, + ), + # Submission contains only a report, but report contains no digest + IsWebCatalogOnlyScenario( + input_submission=make_new_report_only_submission(), + report_has_digest=False, + expected_output=False, + ), + # Submission contains no report + IsWebCatalogOnlyScenario( + input_submission=make_new_tarball_only_submission(), + create_report=False, + expected_output=False, + ), + # Submission contains a report and other files + IsWebCatalogOnlyScenario( + input_submission=submission.Submission( + api_url="https://api.github.com/repos/openshift-helm-charts/charts/pulls/1", + modified_files=[ + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/report.yaml", + f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/{expected_name}-{expected_version}.tgz", + ], + chart=expected_chart, + report=submission.Report( + found=True, + signed=False, + path=f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/report.yaml", + ), + tarball=submission.Tarball( + found=True, + provenance=None, + path=f"charts/{expected_category}/{expected_organization}/{expected_name}/{expected_version}/{expected_name}-{expected_version}.tgz", + ), + ), + report_has_digest=True, + expected_output=False, + ), +] + + +@pytest.mark.parametrize("test_scenario", scenarios_is_web_catalog_only) +def test_is_valid_web_catalog_only(test_scenario): + + with tempfile.TemporaryDirectory(dir=".") as temp_dir: + # Create directory structure + chart_base_path = os.path.join( + temp_dir, + "charts", + expected_category, + expected_organization, + expected_name, + expected_version, + ) + os.makedirs(chart_base_path) + + # Populate report.yaml file + if test_scenario.create_report: + report_file = open(os.path.join(chart_base_path, "report.yaml"), "w") + report_file.writelines( + [ + "apiversion: v1", + "\nkind: verify-report", + ] + ) + if test_scenario.report_has_digest: + report_file.writelines( + [ + "\nmetadata:", + "\n tool:", + "\n digests:", + "\n package: 7755e7cf43e55bbf2edafd9788b773b844fb15626c5ff8ff7a30a6d9034f3a33", + ] + ) + report_file.close() + + assert ( + test_scenario.input_submission.is_valid_web_catalog_only(repo_path=temp_dir) + == test_scenario.expected_output + ) diff --git a/scripts/src/report/verifier_report.py b/scripts/src/report/verifier_report.py index 875444e6a9..cd3a7a863b 100644 --- a/scripts/src/report/verifier_report.py +++ b/scripts/src/report/verifier_report.py @@ -43,6 +43,10 @@ KUBE_VERSION_ATTRIBUTE = "kubeVersion" +class ConfigKeyMissing(Exception): + pass + + def get_report_data(report_path): """Load and returns the report data contained in report.yaml @@ -105,20 +109,41 @@ def get_profile_version(report_data): return profile_version -def get_web_catalog_only(report_data): +def get_web_catalog_only(report_data, raise_if_missing=False): + """Check the delivery method set in the report data. + + Args: + report_data (dict): Content of the report file. Typically this is the return value of the + get_report_data function. + raise_if_missing (bool, optional): Whether to raise an Exception if the delivery method is + not set in the report data. If set to False, the function returns False. + + Raises: + ConfigKeyMissing: if the key is not found in OWNERS and raise_if_missing is set to True + + """ + keyFound = False web_catalog_only = False try: if "webCatalogOnly" in report_data["metadata"]["tool"]: web_catalog_only = report_data["metadata"]["tool"]["webCatalogOnly"] + keyFound = True if "providerControlledDelivery" in report_data["metadata"]["tool"]: web_catalog_only = report_data["metadata"]["tool"][ "providerControlledDelivery" ] + keyFound = True except Exception as err: print( f"Exception getting webCatalogOnly/providerControlledDelivery {err=}, {type(err)=}" ) pass + + if not keyFound and raise_if_missing: + raise ConfigKeyMissing( + "Neither webCatalogOnly nor providerControlledDelivery keys were set" + ) + return web_catalog_only diff --git a/scripts/src/signedchart/signedchart.py b/scripts/src/signedchart/signedchart.py index fc852f880a..7df533562a 100644 --- a/scripts/src/signedchart/signedchart.py +++ b/scripts/src/signedchart/signedchart.py @@ -65,19 +65,14 @@ def is_chart_signed(api_url, report_path): return False -def key_in_owners_match_report(owner_path, report_path): - owner_key = get_pgp_key_from_owners(owner_path) - if not owner_key: - return True - return check_pgp_public_key(owner_key, report_path) - - def get_pgp_key_from_owners(owner_path): - found, owner_data = owners_file.get_owner_data_from_file(owner_path) - if found: - pgp_key = owners_file.get_pgp_public_key(owner_data) - return pgp_key - return "" + try: + owner_data = owners_file.get_owner_data_from_file(owner_path) + except owners_file.OwnersFileError: + return "" + + pgp_key = owners_file.get_pgp_public_key(owner_data) + return pgp_key def check_report_for_signed_chart(report_path): diff --git a/tests/data/HC-16/dash-in-version/partner/report.yaml b/tests/data/HC-16/dash-in-version/partner/report.yaml deleted file mode 100644 index b4aec81305..0000000000 --- a/tests/data/HC-16/dash-in-version/partner/report.yaml +++ /dev/null @@ -1,89 +0,0 @@ -apiversion: v1 -kind: verify-report -metadata: - tool: - verifier-version: 1.7.0 - profile: - VendorType: partner - version: v1.1 - chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/psql-service-0.1.10-1.tgz?raw=true - digests: - chart: sha256:db482b4d90349c6b276ba27f581720cc62fa7bea05184b5bfd840844178a8da6 - package: c8635dcdc8f8493abbdef85305be55c9d5dbf3a44495a88a8285c9a0d0c63408 - lastCertifiedTimestamp: "2022-06-22T15:54:59.964823+00:00" - testedOpenShiftVersion: "4.10" - supportedOpenShiftVersions: '>=4.7' - providerControlledDelivery: false - chart: - name: psql-service - home: "" - sources: [] - version: 0.1.10-1 - description: A Helm chart for a RedHat Certified PSQL - keywords: [] - maintainers: [] - icon: "" - apiversion: v2 - condition: "" - tags: "" - appversion: 10.0.0 - deprecated: false - annotations: - charts.openshift.io/archs: x86_64 - charts.openshift.io/name: PSQL RedHat Demo Chart - charts.openshift.io/provider: RedHat - charts.openshift.io/supportURL: https://github.com/dperaza4dustbit/helm-chart - kubeversion: '>=1.20.0' - dependencies: [] - type: application - chart-overrides: "" -results: - - check: v1.0/contains-test - type: Mandatory - outcome: PASS - reason: Chart test files exist - - check: v1.0/contains-values - type: Mandatory - outcome: PASS - reason: Values file exist - - check: v1.1/has-kubeversion - type: Mandatory - outcome: PASS - reason: Kubernetes version specified - - check: v1.0/not-contains-crds - type: Mandatory - outcome: PASS - reason: Chart does not contain CRDs - - check: v1.0/not-contain-csi-objects - type: Mandatory - outcome: PASS - reason: CSI objects do not exist - - check: v1.0/chart-testing - type: Mandatory - outcome: PASS - reason: Chart tests have passed - - check: v1.0/has-readme - type: Mandatory - outcome: PASS - reason: Chart has a README - - check: v1.0/is-helm-v3 - type: Mandatory - outcome: PASS - reason: API version is V2, used in Helm 3 - - check: v1.0/contains-values-schema - type: Mandatory - outcome: PASS - reason: Values schema file exist - - check: v1.0/helm-lint - type: Mandatory - outcome: PASS - reason: Helm lint successful - - check: v1.0/images-are-certified - type: Mandatory - outcome: PASS - reason: 'Image is Red Hat certified : registry.access.redhat.com/rhscl/postgresql-10-rhel7:1-66' - - check: v1.0/required-annotations-present - type: Mandatory - outcome: PASS - reason: All required annotations present - diff --git a/tests/data/HC-16/dash-in-version/redhat/report.yaml b/tests/data/HC-16/dash-in-version/redhat/report.yaml deleted file mode 100644 index eff7b99361..0000000000 --- a/tests/data/HC-16/dash-in-version/redhat/report.yaml +++ /dev/null @@ -1,89 +0,0 @@ -apiversion: v1 -kind: verify-report -metadata: - tool: - verifier-version: 1.7.0 - profile: - VendorType: redhat - version: v1.1 - chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/psql-service-0.1.10-1.tgz?raw=true - digests: - chart: sha256:db482b4d90349c6b276ba27f581720cc62fa7bea05184b5bfd840844178a8da6 - package: c8635dcdc8f8493abbdef85305be55c9d5dbf3a44495a88a8285c9a0d0c63408 - lastCertifiedTimestamp: "2022-06-22T17:21:03.1478+00:00" - testedOpenShiftVersion: "4.10" - supportedOpenShiftVersions: '>=4.7' - providerControlledDelivery: false - chart: - name: psql-service - home: "" - sources: [] - version: 0.1.10-1 - description: A Helm chart for a RedHat Certified PSQL - keywords: [] - maintainers: [] - icon: "" - apiversion: v2 - condition: "" - tags: "" - appversion: 10.0.0 - deprecated: false - annotations: - charts.openshift.io/archs: x86_64 - charts.openshift.io/name: PSQL RedHat Demo Chart - charts.openshift.io/provider: RedHat - charts.openshift.io/supportURL: https://github.com/dperaza4dustbit/helm-chart - kubeversion: '>=1.20.0' - dependencies: [] - type: application - chart-overrides: "" -results: - - check: v1.0/has-readme - type: Mandatory - outcome: PASS - reason: Chart has a README - - check: v1.0/is-helm-v3 - type: Mandatory - outcome: PASS - reason: API version is V2, used in Helm 3 - - check: v1.0/not-contains-crds - type: Mandatory - outcome: PASS - reason: Chart does not contain CRDs - - check: v1.0/not-contain-csi-objects - type: Mandatory - outcome: PASS - reason: CSI objects do not exist - - check: v1.0/images-are-certified - type: Mandatory - outcome: PASS - reason: 'Image is Red Hat certified : registry.access.redhat.com/rhscl/postgresql-10-rhel7:1-66' - - check: v1.0/chart-testing - type: Mandatory - outcome: PASS - reason: Chart tests have passed - - check: v1.0/required-annotations-present - type: Mandatory - outcome: PASS - reason: All required annotations present - - check: v1.0/contains-test - type: Mandatory - outcome: PASS - reason: Chart test files exist - - check: v1.0/contains-values - type: Mandatory - outcome: PASS - reason: Values file exist - - check: v1.0/contains-values-schema - type: Mandatory - outcome: PASS - reason: Values schema file exist - - check: v1.1/has-kubeversion - type: Mandatory - outcome: PASS - reason: Kubernetes version specified - - check: v1.0/helm-lint - type: Mandatory - outcome: PASS - reason: Helm lint successful - diff --git a/tests/data/HC-17/plus-in-version/partner/report.yaml b/tests/data/HC-17/plus-in-version/partner/report.yaml new file mode 100644 index 0000000000..44761b1b8c --- /dev/null +++ b/tests/data/HC-17/plus-in-version/partner/report.yaml @@ -0,0 +1,120 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.13.4 + profile: + VendorType: partner + version: v1.3 + reportDigest: uint64:4054779787745617517 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/cryostat-0.4.0%2B1.tgz?raw=true + digests: + chart: sha256:6b5babbd4329410e21cf6ae9011bf517c38352592aceea06c227988656e0bdb0 + package: e210e4887fd7ed5cdfece4844f73f827ef69afa09d9cda8963232022f194efa2 + lastCertifiedTimestamp: "2024-05-22T15:18:04.379024-04:00" + testedOpenShiftVersion: "4.14" + supportedOpenShiftVersions: '>=4.6' + webCatalogOnly: false + chart: + name: cryostat + home: https://cryostat.io + sources: + - https://github.com/cryostatio/cryostat + - https://github.com/cryostatio/cryostat-core + - https://github.com/cryostatio/cryostat-web + - https://github.com/cryostatio/jfr-datasource + - https://github.com/cryostatio/cryostat-grafana-dashboard + version: 0.4.0+1 + description: Securely manage JFR recordings for your containerized Java workloads + keywords: + - flightrecorder + - java + - jdk + - jfr + - jmc + - missioncontrol + - monitoring + - profiling + - diagnostic + maintainers: + - name: The Cryostat Community + email: "" + url: https://groups.google.com/g/cryostat-development + icon: https://raw.githubusercontent.com/cryostatio/cryostat-helm/main/docs/images/cryostat-icon.svg + apiversion: v2 + condition: "" + tags: "" + appversion: 2.4.0.redhat + deprecated: false + annotations: + charts.openshift.io/archs: x86_64 + charts.openshift.io/digest: sha256:e5f987974557cb190c02579e7dfd0c56c8715dc5e45ddcfd2af944f57c38c150 + charts.openshift.io/lastCertifiedTimestamp: "2024-02-21T18:36:30.02064+00:00" + charts.openshift.io/name: Red Hat build of Cryostat + charts.openshift.io/provider: RedHat + charts.openshift.io/providerType: redhat + charts.openshift.io/supportURL: https://github.com/cryostatio/cryostat-helm + charts.openshift.io/supportedOpenShiftVersions: '>=4.6' + charts.openshift.io/testedOpenShiftVersion: "4.14" + kubeversion: '>= 1.19.0-0' + dependencies: [] + type: application + chart-overrides: "" +results: + - check: v1.0/contains-test + type: Mandatory + outcome: PASS + reason: Chart test files exist + - check: v1.0/is-helm-v3 + type: Mandatory + outcome: PASS + reason: API version is V2, used in Helm 3 + - check: v1.0/required-annotations-present + type: Mandatory + outcome: PASS + reason: All required annotations present + - check: v1.0/signature-is-valid + type: Mandatory + outcome: SKIPPED + reason: 'Chart is not signed : Signature verification not required' + - check: v1.0/contains-values-schema + type: Mandatory + outcome: PASS + reason: Values schema file exist + - check: v1.0/has-readme + type: Mandatory + outcome: PASS + reason: Chart has a README + - check: v1.1/images-are-certified + type: Mandatory + outcome: PASS + reason: No images to certify + - check: v1.0/not-contain-csi-objects + type: Mandatory + outcome: PASS + reason: CSI objects do not exist + - check: v1.1/has-kubeversion + type: Mandatory + outcome: PASS + reason: Kubernetes version specified + - check: v1.0/chart-testing + type: Mandatory + outcome: PASS + reason: Chart tests have passed + - check: v1.0/contains-values + type: Mandatory + outcome: PASS + reason: Values file exist + - check: v1.0/helm-lint + type: Mandatory + outcome: PASS + reason: Helm lint successful + - check: v1.0/not-contains-crds + type: Mandatory + outcome: PASS + reason: Chart does not contain CRDs + - check: v1.0/has-notes + type: Optional + outcome: PASS + reason: Chart does contain NOTES.txt + diff --git a/tests/data/HC-17/plus-in-version/redhat/report.yaml b/tests/data/HC-17/plus-in-version/redhat/report.yaml new file mode 100644 index 0000000000..c17577386d --- /dev/null +++ b/tests/data/HC-17/plus-in-version/redhat/report.yaml @@ -0,0 +1,120 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.13.4 + profile: + VendorType: redhat + version: v1.3 + reportDigest: uint64:5964035041144068952 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/cryostat-0.4.0%2B1.tgz?raw=true + digests: + chart: sha256:6b5babbd4329410e21cf6ae9011bf517c38352592aceea06c227988656e0bdb0 + package: e210e4887fd7ed5cdfece4844f73f827ef69afa09d9cda8963232022f194efa2 + lastCertifiedTimestamp: "2024-05-22T15:18:41.525481-04:00" + testedOpenShiftVersion: "4.14" + supportedOpenShiftVersions: '>=4.6' + webCatalogOnly: false + chart: + name: cryostat + home: https://cryostat.io + sources: + - https://github.com/cryostatio/cryostat + - https://github.com/cryostatio/cryostat-core + - https://github.com/cryostatio/cryostat-web + - https://github.com/cryostatio/jfr-datasource + - https://github.com/cryostatio/cryostat-grafana-dashboard + version: 0.4.0+1 + description: Securely manage JFR recordings for your containerized Java workloads + keywords: + - flightrecorder + - java + - jdk + - jfr + - jmc + - missioncontrol + - monitoring + - profiling + - diagnostic + maintainers: + - name: The Cryostat Community + email: "" + url: https://groups.google.com/g/cryostat-development + icon: https://raw.githubusercontent.com/cryostatio/cryostat-helm/main/docs/images/cryostat-icon.svg + apiversion: v2 + condition: "" + tags: "" + appversion: 2.4.0.redhat + deprecated: false + annotations: + charts.openshift.io/archs: x86_64 + charts.openshift.io/digest: sha256:e5f987974557cb190c02579e7dfd0c56c8715dc5e45ddcfd2af944f57c38c150 + charts.openshift.io/lastCertifiedTimestamp: "2024-02-21T18:36:30.02064+00:00" + charts.openshift.io/name: Red Hat build of Cryostat + charts.openshift.io/provider: RedHat + charts.openshift.io/providerType: redhat + charts.openshift.io/supportURL: https://github.com/cryostatio/cryostat-helm + charts.openshift.io/supportedOpenShiftVersions: '>=4.6' + charts.openshift.io/testedOpenShiftVersion: "4.14" + kubeversion: '>= 1.19.0-0' + dependencies: [] + type: application + chart-overrides: "" +results: + - check: v1.1/has-kubeversion + type: Mandatory + outcome: PASS + reason: Kubernetes version specified + - check: v1.0/signature-is-valid + type: Mandatory + outcome: SKIPPED + reason: 'Chart is not signed : Signature verification not required' + - check: v1.0/contains-values-schema + type: Mandatory + outcome: PASS + reason: Values schema file exist + - check: v1.0/contains-test + type: Mandatory + outcome: PASS + reason: Chart test files exist + - check: v1.0/not-contain-csi-objects + type: Mandatory + outcome: PASS + reason: CSI objects do not exist + - check: v1.0/required-annotations-present + type: Mandatory + outcome: PASS + reason: All required annotations present + - check: v1.0/contains-values + type: Mandatory + outcome: PASS + reason: Values file exist + - check: v1.0/not-contains-crds + type: Mandatory + outcome: PASS + reason: Chart does not contain CRDs + - check: v1.1/images-are-certified + type: Mandatory + outcome: PASS + reason: No images to certify + - check: v1.0/helm-lint + type: Mandatory + outcome: PASS + reason: Helm lint successful + - check: v1.0/is-helm-v3 + type: Mandatory + outcome: PASS + reason: API version is V2, used in Helm 3 + - check: v1.0/has-notes + type: Optional + outcome: PASS + reason: Chart does contain NOTES.txt + - check: v1.0/has-readme + type: Mandatory + outcome: PASS + reason: Chart has a README + - check: v1.0/chart-testing + type: Mandatory + outcome: PASS + reason: Chart tests have passed + diff --git a/tests/data/cryostat-0.4.0+1.tgz b/tests/data/cryostat-0.4.0+1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..d26c0788ad2272d63cf7fad282de97f581fdc242 GIT binary patch literal 16999 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 literal 0 HcmV?d00001 diff --git a/tests/functional/behave_features/HC-17_dash_in_version.feature b/tests/functional/behave_features/HC-17_dash_in_version.feature index 7431fce613..58eb3d689f 100644 --- a/tests/functional/behave_features/HC-17_dash_in_version.feature +++ b/tests/functional/behave_features/HC-17_dash_in_version.feature @@ -13,9 +13,25 @@ Feature: Report only submission Examples: | vendor_type | vendor | report_path | | partners | redhat | tests/data/HC-17/dash-in-version/partner/report.yaml | - + @redhat @full Examples: | vendor_type | vendor | report_path | | redhat | redhat | tests/data/HC-17/dash-in-version/redhat/report.yaml | + Scenario Outline: [HC-17-002] A partner or redhat associate submits report only with plus in chart version + Given the vendor "" has a valid identity as "" + And an error-free report is used in "" + When the user sends a pull request with the report + Then the user sees the pull request is merged + And the index.yaml file is updated with an entry for the submitted chart + + @partners @full + Examples: + | vendor_type | vendor | report_path | + | partners | redhat | tests/data/HC-17/plus-in-version/partner/report.yaml | + + @redhat @full + Examples: + | vendor_type | vendor | report_path | + | redhat | redhat | tests/data/HC-17/plus-in-version/redhat/report.yaml | diff --git a/tests/functional/behave_features/common/utils/chart_certification.py b/tests/functional/behave_features/common/utils/chart_certification.py index 3414a53ab7..15c2491cca 100644 --- a/tests/functional/behave_features/common/utils/chart_certification.py +++ b/tests/functional/behave_features/common/utils/chart_certification.py @@ -135,9 +135,15 @@ def send_pull_request(self, remote_repo, base_branch, pr_branch, bot_token): logging.debug(f"PR_BODY Content: {pr_body}") logging.info(f"Create PR from '{remote_repo}:{pr_branch}'") r = github_api("post", f"repos/{remote_repo}/pulls", bot_token, json=data) - j = json.loads(r.text) + + try: + j = json.loads(r.text) + except json.JSONDecodeError as e: + raise AssertionError(f"error decoding GitHub response: {r.__dict__}") from e + if "number" not in j: raise AssertionError(f"error sending pull request, response was: {r.text}") + return j["number"] def create_and_push_owners_file( @@ -333,7 +339,7 @@ def check_workflow_conclusion( conclusion = get_run_result(self.secrets, run_id) if conclusion == expect_result: logging.info( - f"PR{pr_number} Workflow run was '{expect_result}' which is expected" + f"PR{pr_number if pr_number else self.secrets.pr_number} Workflow run was '{expect_result}' which is expected" ) else: if failure_type == "warning": diff --git a/tests/functional/behave_features/common/utils/github.py b/tests/functional/behave_features/common/utils/github.py index 5b525e5f1a..5c966d3f97 100644 --- a/tests/functional/behave_features/common/utils/github.py +++ b/tests/functional/behave_features/common/utils/github.py @@ -31,7 +31,9 @@ def get_run_id(secrets, workflow_name: str, pr_number: str = None): logging.debug(f'workflow found with id "{run["id"]}"') return run["id"] else: - raise Exception(f"Workflow for the submitted PR (#{pr_number}) did not run.") + raise Exception( + f"Workflow for the submitted PR (#{pr_number if pr_number else secrets.pr_number}) did not run." + ) @retry(stop_max_delay=60_000 * 40, wait_fixed=2000)