From defd47d82d18cb8a528b40f608d46ecce43bbe96 Mon Sep 17 00:00:00 2001 From: mmulholla <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 1 Dec 2022 21:16:15 +0000 Subject: [PATCH] Release-1.5.0 --- .github/workflows/build.yml | 26 +- .github/workflows/owners.yml | 86 ++++ docs/README.md | 65 ++- scripts/requirements.txt | 2 +- scripts/setup.cfg | 1 + scripts/src/chartprreview/chartprreview.py | 47 +- .../src/chartrepomanager/chartrepomanager.py | 72 ++- scripts/src/checkprcontent/checkpr.py | 11 +- scripts/src/metrics/metrics.py | 99 +++- scripts/src/metrics/pushowners.py | 100 ++++ scripts/src/owners/owners_file.py | 43 +- scripts/src/pullrequest/prepare_pr_comment.py | 34 +- scripts/src/release/release_info.py | 57 ++- scripts/src/release/releaser.py | 45 +- scripts/src/report/get_verify_params.py | 7 + scripts/src/report/report_info.py | 28 +- scripts/src/report/verifier_report.py | 28 +- scripts/src/signedchart/__init__.py | 0 scripts/src/signedchart/signedchart.py | 167 +++++++ scripts/src/tools/gitutils.py | 1 + scripts/src/workflowtesting/checkprforci.py | 11 +- tests/data/HC-04/community/report.yaml | 100 ++++ tests/data/{ => HC-04/partner}/report.yaml | 57 +-- tests/data/HC-04/redhat/report.yaml | 100 ++++ tests/data/HC-06/partner/report.yaml | 100 ++++ tests/data/HC-09/community/report.json | 126 +++++ tests/data/{ => HC-09/partner}/report.json | 75 ++- tests/data/HC-09/redhat/report.json | 126 +++++ .../HC-10/signed_chart/public_key_bad.asc | 29 ++ .../HC-10/signed_chart/public_key_good.asc | 31 ++ .../signed_chart/report/partner/report.yaml | 105 +++++ .../signed_chart/report/redhat/report.yaml | 105 +++++ .../data/HC-10/signed_chart/vault-0.17.0.tgz | Bin 0 -> 37237 bytes .../HC-10/signed_chart/vault-0.17.0.tgz.prov | 40 ++ tests/data/HC-11/community/report.yaml | 96 ++++ tests/data/HC-11/partner/report.yaml | 96 ++++ .../partner_not_contain_crds/report.yaml | 96 ++++ .../HC-17/dash-in-version/partner/report.yaml | 51 +- .../HC-17/dash-in-version/redhat/report.yaml | 55 +-- tests/data/HC-18/community/report.yaml | 100 ++++ tests/data/HC-18/partner/report.yaml | 100 ++++ tests/data/HC-18/redhat/report.yaml | 100 ++++ .../HC-19/report_edited_sha_bad/report.yaml | 100 ++++ tests/data/HC-19/report_sha_bad/report.yaml | 100 ++++ tests/data/HC-19/report_sha_good/report.yaml | 100 ++++ tests/data/common/community/report.yaml | 100 ++++ tests/data/common/partner/report.yaml | 100 ++++ tests/data/common/redhat/report.yaml | 100 ++++ tests/data/readme_about_testdata.md | 124 +++++ tests/data/vault-0.18.0.tgz | Bin 0 -> 37067 bytes ..._verifier_comes_back_with_failures.feature | 12 +- .../HC-04_invalid_url_in_the_report.feature | 33 +- .../HC-06_provider_delivery_control.feature | 22 +- .../HC-07_report_and_chart_src.feature | 12 +- .../HC-08_report_and_chart_tar.feature | 12 +- .../HC-09_report_in_json_format.feature | 12 +- .../HC-10_report_only_edited.feature | 47 -- .../HC-11_report_with_missing_checks.feature | 15 +- .../HC-12_report_without_chart.feature | 13 +- .../HC-13_sha_value_does_not_match.feature | 26 -- ...-14_user_submits_chart_with_errors.feature | 41 +- .../HC-18_multiple_charts_in_pr.feature | 113 +++++ .../behave_features/HC-19_report_sha.feature | 34 ++ .../behave_features/common/utils/chart.py | 37 +- .../common/utils/chart_certification.py | 434 ++++++++---------- .../behave_features/common/utils/env.py | 63 +++ .../behave_features/common/utils/github.py | 4 +- .../behave_features/common/utils/secret.py | 4 - .../behave_features/steps/implementation.py | 186 ++++++-- 69 files changed, 3760 insertions(+), 702 deletions(-) create mode 100644 .github/workflows/owners.yml create mode 100644 scripts/src/metrics/pushowners.py create mode 100644 scripts/src/signedchart/__init__.py create mode 100644 scripts/src/signedchart/signedchart.py create mode 100644 tests/data/HC-04/community/report.yaml rename tests/data/{ => HC-04/partner}/report.yaml (92%) create mode 100644 tests/data/HC-04/redhat/report.yaml create mode 100644 tests/data/HC-06/partner/report.yaml create mode 100644 tests/data/HC-09/community/report.json rename tests/data/{ => HC-09/partner}/report.json (83%) create mode 100644 tests/data/HC-09/redhat/report.json create mode 100644 tests/data/HC-10/signed_chart/public_key_bad.asc create mode 100644 tests/data/HC-10/signed_chart/public_key_good.asc create mode 100644 tests/data/HC-10/signed_chart/report/partner/report.yaml create mode 100644 tests/data/HC-10/signed_chart/report/redhat/report.yaml create mode 100644 tests/data/HC-10/signed_chart/vault-0.17.0.tgz create mode 100644 tests/data/HC-10/signed_chart/vault-0.17.0.tgz.prov create mode 100644 tests/data/HC-11/community/report.yaml create mode 100644 tests/data/HC-11/partner/report.yaml create mode 100644 tests/data/HC-11/partner_not_contain_crds/report.yaml create mode 100644 tests/data/HC-18/community/report.yaml create mode 100644 tests/data/HC-18/partner/report.yaml create mode 100644 tests/data/HC-18/redhat/report.yaml create mode 100644 tests/data/HC-19/report_edited_sha_bad/report.yaml create mode 100644 tests/data/HC-19/report_sha_bad/report.yaml create mode 100644 tests/data/HC-19/report_sha_good/report.yaml create mode 100644 tests/data/common/community/report.yaml create mode 100644 tests/data/common/partner/report.yaml create mode 100644 tests/data/common/redhat/report.yaml create mode 100644 tests/data/readme_about_testdata.md create mode 100644 tests/data/vault-0.18.0.tgz delete mode 100644 tests/functional/behave_features/HC-10_report_only_edited.feature delete mode 100644 tests/functional/behave_features/HC-13_sha_value_does_not_match.feature create mode 100644 tests/functional/behave_features/HC-18_multiple_charts_in_pr.feature create mode 100644 tests/functional/behave_features/HC-19_report_sha.feature create mode 100644 tests/functional/behave_features/common/utils/env.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 99d751e010..b4bb873a9a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,9 +77,11 @@ jobs: run: | #set environemnt based on repository if [ $GITHUB_REPOSITORY == "openshift-helm-charts/charts" ]; then + echo "Use latest verifier image" echo "::set-output name=insecure_skip_tls_verify::false" echo "::set-output name=verifier-action-image::latest" else + echo "Use dev verifier image" echo "::set-output name=insecure_skip_tls_verify::true" echo "::set-output name=verifier-action-image::0.1.0" fi @@ -155,6 +157,7 @@ jobs: uses: redhat-actions/openshift-tools-installer@v1 with: source: github + skip_cache: true chart-verifier: ${{ steps.set-env.outputs.verifier-action-image }} - name: determine verify requirements @@ -205,6 +208,13 @@ jobs: report_type: all fail: false + - name: check-verifier-result + id: check-verifier-result + if: ${{ always() && steps.run-verifier.outcome == 'failure' }} + run: | + error_message="The chart verifier returned an error when trying to obtain a verification report for the chart." + echo "::set-output name=verifier_error_message::$error_message" + - name: Check Report id: check_report if: ${{ steps.check_build_required.outputs.run-build == 'true' }} @@ -214,6 +224,7 @@ jobs: REPORT_GENERATED: ${{ steps.verify_requires.outputs.report_needed }} GENERATED_REPORT_PATH: ${{ steps.run-verifier.outputs.report_file }} REPORT_SUMMARY_PATH: ${{ steps.run-verifier.outputs.report_info_file }} + WORKFLOW_WORKING_DIRECTORY: "../pr" run: | cd pr-branch ../ve1/bin/chart-pr-review --directory=../pr --verify-user=${{ github.event.pull_request.user.login }} --api-url=${{ github.event.pull_request._links.self.href }} @@ -241,8 +252,9 @@ jobs: OWNERS_ERROR_MESSAGE: ${{ steps.check_pr_content.outputs.owners-error-message }} COMMUNITY_MANUAL_REVIEW: ${{ steps.check_report.outputs.community_manual_review_required }} OC_INSTALL_RESULT: ${{ steps.install-oc.conclusion }} + VERIFIER_ERROR_MESSAGE: ${{ steps.check-verifier-result.outputs.verifier_error_message }} run: | - ve1/bin/pr-comment ${{ steps.check_pr_content.outcome }} ${{ steps.check_report.conclusion }} ${{ github.repository }} + ve1/bin/pr-comment ${{ steps.check_pr_content.outcome }} ${{ steps.run-verifier.outcome }} ${{ steps.check_report.conclusion }} - name: Comment on PR if: ${{ always() && steps.check_build_required.outputs.run-build == 'true' }} @@ -261,7 +273,7 @@ jobs: }); - name: Add 'authorized-request' label to PR - if: ${{ always() && steps.check_pr_content.outcome == 'success' && steps.check_build_required.outputs.run-build == 'true' }} + if: ${{ always() && steps.check_pr_content.outcome == 'success' && steps.run-verifier.outcome != 'failure' && steps.check_build_required.outputs.run-build == 'true' }} uses: actions/github-script@v3 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -337,7 +349,9 @@ jobs: continue-on-error: true with: tag_name: ${{ steps.release-charts.outputs.tag }} - files: pr-branch/report.yaml + files: | + ${{ steps.release-charts.outputs.report_file }} + ${{ steps.release-charts.outputs.public_key_file }} fail_on_unmatched_files: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -349,9 +363,11 @@ jobs: run: | if [ $GITHUB_REPOSITORY == "openshift-helm-charts/charts" ]; then WRITE_KEY=${{ secrets.SEGMENT_WRITE_KEY }} + ID_PREFIX="helm-metric-pr" echo "Use segment production write key" else WRITE_KEY=${{ secrets.SEGMENT_TEST_WRITE_KEY }} + ID_PREFIX="helm-test-metric-pr" echo "Use segment test write key" fi @@ -362,7 +378,9 @@ jobs: --message-file="${{ steps.pr_comment.outputs.message-file }}" \ --pr-number="${{ github.event.number }}" \ --pr-action="${{ github.event.action }}" \ - --repository="${GITHUB_REPOSITORY}" + --repository="${GITHUB_REPOSITORY}" \ + --prefix="${ID_PREFIX}" \ + --pr_dir="./pr-branch" else echo "Do not collect metrics, required segment write key is not set" fi diff --git a/.github/workflows/owners.yml b/.github/workflows/owners.yml new file mode 100644 index 0000000000..d868a2cc38 --- /dev/null +++ b/.github/workflows/owners.yml @@ -0,0 +1,86 @@ +name: Metrics for owners file + +on: + push: + branches: [ main ] + +jobs: + owners-metrics: + name: Send Owner Metrics + runs-on: ubuntu-20.04 + env: + SEGMENT_WRITE_KEY: ${{ secrets.SEGMENT_WRITE_KEY }} + SEGMENT_TEST_WRITE_KEY: ${{ secrets.SEGMENT_TEST_WRITE_KEY }} + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Exit if push is not from redhat-mercury-bot + id: check_push_by_bot + run: | + echo "${{ github.event.pusher.name}}" + if [ "${{ github.event.pusher.name}}" != "redhat-mercury-bot" ]; then + echo "The push is not from redhat mercury bot - do not continue." + exit 0 + fi + + - name: Set up Python 3.x Part 1 + uses: actions/setup-python@v2 + with: + python-version: "3.9" + + - name: Set up Python 3.x Part 2 + run: | + # set up python + python3 -m venv ve1 + cd scripts && ../ve1/bin/pip3 install -r requirements.txt && cd .. + cd scripts && ../ve1/bin/python3 setup.py install && cd .. + + - uses: jitterbit/get-changed-files@v1 + id: filesChangedOrModified + with: + format: space-delimited + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Printing changes made + run: | + echo "Added:" + echo "${{ steps.filesChangedOrModified.outputs.added }}" + echo "Modified:" + echo "${{ steps.filesChangedOrModified.outputs.modified }}" + + - name: Quit if owners files pushed are not + run: | + SUB="/OWNERS" + if [ "${{ steps.filesChangedOrModified.outputs.added[0] }}" == *"$SUB"* ] || [ "${{ steps.filesChangedOrModified.outputs.modified[0] }}" == *"$SUB"* ] ; then + echo "OWNERS files have been modified or added" + else + echo "OWNERS files not pushed" + exit 0 + fi + + - name: Add owner metrics + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [ $GITHUB_REPOSITORY == "openshift-helm-charts/charts" ]; then + WRITE_KEY=${{ secrets.SEGMENT_WRITE_KEY }} + ID_PREFIX="helm-metric-pr" + echo "Use segment production write key" + else + WRITE_KEY=${{ secrets.SEGMENT_TEST_WRITE_KEY }} + ID_PREFIX="helm-test-metric-pr" + echo "Use segment test write key" + fi + + if [ "${WRITE_KEY}" != "" ]; then + echo "add owner metric" + ve1/bin/pushowners --write-key="${WRITE_KEY}" \ + --metric-type="push_request" \ + --repository="${GITHUB_REPOSITORY}" \ + --added="${{ steps.filesChangedOrModified.outputs.added }}" \ + --modified="${{ steps.filesChangedOrModified.outputs.modified }}" \ + --prefix="${ID_PREFIX}" + else + echo "Do not collect metrics, required segment write key is not set" + fi diff --git a/docs/README.md b/docs/README.md index 6080ace692..83cb5eb58e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -42,6 +42,7 @@ submit a chart and the report together. * [Error with the chart URL when submitting report](#error-with-the-chart-url-when-submitting-report) * [Chart name and version mismatch errors](#chart-name-and-version-mismatch-errors) * [Report failures](#report-failures) + * [Signed chart failures](#signed-chart-failures) * [Frequently Asked Questions](#frequently-asked-questions) * [Can I test the pull request in my fork before submitting?](#can-i-test-the-pull-request-in-my-fork-before-submitting) * [Can I use any command-line interface to create pull request?](#can-i-use-any-command-line-interface-to-create-pull-request) @@ -55,17 +56,20 @@ submit a chart and the report together. In a nutshell, these are the actions that are performed when you submit a chart. The pull request is checked for non-chart-related changes and fails the job if -found. A check performs the chart is added to a correct directory path. If a -report is provided, confirm all mandatory checks are present and have passed. +found. A check confirms the chart is added to a correct directory path. If a +report is provided, confirm the report has not been modified after generation, all mandatory checks are present and have passed. If a chart is provided, check the digest in the report to match the digest calculated for the submitted chart. If a report is not provided, run the chart-verifier and confirm all mandatory checks pass. +Additionally, for signed charts, if a pgp public key is included in the OWNERS file a check will be made to ensure the key can be used to verify the chart. + You can submit chart related changes in three methods: 1. Submit a chart without Chart Verification Report - Chart as a tarball + - For signed chart also include the providence file. - Chart in a directory 2. Submit a Chart Verification Report without the Chart @@ -94,6 +98,7 @@ chart: name: awesome shortDescription: A Helm chart for Awesomeness publicPgpKey: null +providerDelivery: False users: - githubUsername: - githubUsername: @@ -132,13 +137,25 @@ Notice that in this case your organization will also be redhat. ### Submitting a Chart without Chart Verification Report The chart could be a tarball created using the `helm package` command or a -directory with the chart source. If it is a tarball, it can be placed directly -under the `0.1.0` directory. +directory with the chart source. If it is a tarball, it is placed directly +under the `0.1.0` directory, further if the chart is signed, the providence file is added to the same directory. ``` charts/partners/acme/awesome/0.1.0/awesome-0.1.0.tgz +charts/partners/acme/awesome/0.1.0/awesome-0.1.0.tgz.prov +``` + +For a signed chart the OWNERS file can include a base64 encoded public key for the chart. If this is present the key will be decoded and specified when the chart-verifier is used to create a report for the chart. If the public key does not match the chart the verifier report will include a check failure and, the PR will end with an error. If the public key does match the chart, and there are no other failures, a release will be created which will include the tarball, the providence file, the public key file, and the generated report. + +``` +awesome-0.1.0.tgz +awesome-0.1.0.tgz.prov +awesome-0.1.0.tgz.key +report.yaml ``` +If the OWNERS file does not include the public key the chart verifier check is skipped and will not affect the pull request outcome. Further the public key file will not be included in the release. + If the chart is a directory with the chart source, create an `src` directory to place the chart source. @@ -174,6 +191,8 @@ The file structure looks like this: [Generate the report][chart-verifier] and save it under `0.1.0` with a given file name as `report.yaml`. +#### Signed Report + When you follow the [partner connect documentation][partners], you could see details about adding a PGP public key. Adding PGP public key is optional. But if you have added that, you should see your public key in the _OWNERS_ file @@ -191,6 +210,16 @@ gpg --sign --armor --detach-sign --output report.yaml.asc report.yaml There will be `[WARNING]` message in the console if the signature verification fails. +#### Report for a Signed chart + +For a signed chart, if a pgp public key was provided to the chart verifier when generating the report, a digest of the key is included in the report. If a base64 encoded pgp public key is then included in the OWNERS file a check is made to confirm the digest of the decoded key in the OWNERS file matches the key digest in the report, and if the match fails the PR will fail. However, if the key digests match, and there are no other errors when processing the PR, a release is generated containing the public key and the report: + +``` +awesome-0.1.0.tgz.key +report.yaml +``` +Note: A release is not generated if provider control delivery is enabled. + ### Submitting a Chart Verification Report with the Chart You can also submit a chart and the report together. As mentioned in the @@ -199,10 +228,15 @@ or tarball under the version numbered directory. Similarly, as mentioned in the "Submitting a Chart Verification Report without the Chart" section, place `report.yaml` also under the save under the version numbered directory. +#### Signed Report As mentioned in the previous section, optionally, you can sign the report. There will be `[WARNING]` message in the console if the signature verification fails. +#### Signed Chart +For a signed chart the submission must include a tarball and a providence file in addition to the report file. In this case, for checking purpose the PR is treated similarly to a [report only submission](#report-for-a-signed-chart). + + ## Post Submission Manual Review After submitting the pull request, it will take a few minutes to run all the @@ -437,6 +471,29 @@ Error message: .. To fix the above failure, you need to modify the chart as per the failure messages. +## Signed chart failures + +Signed chart failures can occur when a base64 encoded pgp public key is included in the OWNERS file. + +If the pull request includes a chart verifier report you may see a failure for example: + +```PGP key in OWNERS file does not match with key digest in report.``` + +This can be caused by: +- the PGP key in the OWNERS file is not base64 encoded. +- the PGP key in the OWNERS file does not match to the PGP public key specified to the chart verifier when the report was generated. + +If the pull request includes a chart tarball and providence file, but no chart verifier report, you may see a failure for example: +``` +[ERROR] Chart verifier report includes failures: + +Number of checks passed: 12 +Number of checks failed: 1 +Error message(s): +Chart is signed : Signature verification failed : openpgp: signature made by unknown entity +``` +This is because the key generated from the PGP key in the OWNERS file does not correspond to the secret key used to sign the chart. + ## Frequently Asked Questions ### Can I test the pull request in my fork before submitting? diff --git a/scripts/requirements.txt b/scripts/requirements.txt index c9eb576899..9294268daf 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -9,7 +9,7 @@ GitPython==3.1.18 glob2==0.7 idna==2.10 iniconfig==1.1.1 -Mako==1.1.4 +mako==1.2.3 MarkupSafe==2.0.1 packaging==21.0 parse==1.19.0 diff --git a/scripts/setup.cfg b/scripts/setup.cfg index 6a832f1975..ac2def8d45 100644 --- a/scripts/setup.cfg +++ b/scripts/setup.cfg @@ -48,4 +48,5 @@ console_scripts = check-user = owners.checkuser:main metrics = metrics.metrics:main get-verify-params = report.get_verify_params:main + pushowners=metrics.pushowners:main diff --git a/scripts/src/chartprreview/chartprreview.py b/scripts/src/chartprreview/chartprreview.py index 4ef72b80cd..be17211ea5 100644 --- a/scripts/src/chartprreview/chartprreview.py +++ b/scripts/src/chartprreview/chartprreview.py @@ -1,5 +1,6 @@ import re import os +import os.path import sys import argparse import subprocess @@ -20,6 +21,7 @@ sys.path.append('../') from report import report_info from report import verifier_report +from signedchart import signedchart def write_error_log(directory, *msg): os.makedirs(directory, exist_ok=True) @@ -94,21 +96,24 @@ def check_owners_file_against_directory_structure(directory,username, category, def verify_signature(directory, category, organization, chart, version): print("[INFO] Verify signature. %s, %s, %s" % (organization, chart, version)) - data = open(os.path.join("charts", category, organization, chart, "OWNERS")).read() - out = yaml.load(data, Loader=Loader) - publickey = out.get('publicPgpKey') - if not publickey: - return - with open("public.key", "w") as fd: - fd.write(publickey) - out = subprocess.run(["gpg", "--import", "public.key"], capture_output=True) - print("[INFO]", out.stdout.decode("utf-8")) - print("[WARNING]", out.stderr.decode("utf-8")) - report = os.path.join("charts", category, organization, chart, version, "report.yaml") sign = os.path.join("charts", category, organization, chart, version, "report.yaml.asc") - out = subprocess.run(["gpg", "--verify", sign, report], capture_output=True) - print("[INFO]", out.stdout.decode("utf-8")) - print("[WARNING]", out.stderr.decode("utf-8")) + if os.path.exists(sign): + data = open(os.path.join("charts", category, organization, chart, "OWNERS")).read() + out = yaml.load(data, Loader=Loader) + publickey = out.get('publicPgpKey') + if not publickey: + return + with open("public.key", "w") as fd: + fd.write(publickey) + out = subprocess.run(["gpg", "--import", "public.key"], capture_output=True) + print("[INFO]", out.stdout.decode("utf-8")) + print("[WARNING]", out.stderr.decode("utf-8")) + report = os.path.join("charts", category, organization, chart, version, "report.yaml") + out = subprocess.run(["gpg", "--verify", sign, report], capture_output=True) + print("[INFO]", out.stdout.decode("utf-8")) + print("[WARNING]", out.stderr.decode("utf-8")) + else: + print(f"[INFO] Signed report not found: {sign}.") def match_checksum(directory,generated_report_info_path,category, organization, chart, version): print("[INFO] Check digests match. %s, %s, %s" % (organization, chart, version)) @@ -344,9 +349,19 @@ def main(): print(f"[ERROR] {msg}") write_error_log(args.directory, msg) sys.exit(1) - else: - print("[INFO] Submitted report passed validity check!") + print("[INFO] Submitted report passed validity check!") + owners_file = os.path.join("charts", category, organization, chart, "OWNERS") + pgp_key_in_owners = signedchart.get_pgp_key_from_owners(owners_file) + if pgp_key_in_owners: + if signedchart.check_report_for_signed_chart(submitted_report_path): + if not signedchart.check_pgp_public_key(pgp_key_in_owners,submitted_report_path): + msg = f"PGP key in OWNERS file does not match with key digest in report." + print(f"[ERROR] {msg}") + write_error_log(args.directory, msg) + sys.exit(1) + else: + print("[INFO] PGP key in OWNERS file matches with key digest in report.") report_generated = os.environ.get("REPORT_GENERATED") generated_report_path = os.environ.get("GENERATED_REPORT_PATH") diff --git a/scripts/src/chartrepomanager/chartrepomanager.py b/scripts/src/chartrepomanager/chartrepomanager.py index 706fccee8b..813c334b7c 100644 --- a/scripts/src/chartrepomanager/chartrepomanager.py +++ b/scripts/src/chartrepomanager/chartrepomanager.py @@ -22,6 +22,7 @@ sys.path.append('../') from report import report_info from chartrepomanager import indexannotations +from signedchart import signedchart def get_modified_charts(api_url): files_api_url = f'{api_url}/files' @@ -81,25 +82,47 @@ def prepare_chart_source_for_release(category, organization, chart, version): print(out.stdout.decode("utf-8")) print(out.stderr.decode("utf-8")) chart_file_name = f"{chart}-{version}.tgz" - new_chart_file_name = f"{organization}-{chart}-{version}.tgz" try: - os.remove(os.path.join(".cr-release-packages", new_chart_file_name)) + os.remove(os.path.join(".cr-release-packages", chart_file_name)) except FileNotFoundError: pass - shutil.copy(f"{chart}-{version}.tgz" , f".cr-release-packages/{new_chart_file_name}") + shutil.copy(f"{chart}-{version}.tgz" , f".cr-release-packages/{chart_file_name}") -def prepare_chart_tarball_for_release(category, organization, chart, version): +def prepare_chart_tarball_for_release(category, organization, chart, version,signed_chart): print("[INFO] prepare chart tarball for release. %s, %s, %s, %s" % (category, organization, chart, version)) chart_file_name = f"{chart}-{version}.tgz" - new_chart_file_name = f"{organization}-{chart}-{version}.tgz" path = os.path.join("charts", category, organization, chart, version, chart_file_name) try: - os.remove(os.path.join(".cr-release-packages", new_chart_file_name)) + os.remove(os.path.join(".cr-release-packages", chart_file_name)) except FileNotFoundError: pass - shutil.copy(path, f".cr-release-packages/{new_chart_file_name}") + shutil.copy(path, f".cr-release-packages/{chart_file_name}") shutil.copy(path, chart_file_name) + if signed_chart: + print("[INFO] Signed chart - include PROV file") + prov_file_name = f"{chart_file_name}.prov" + path = os.path.join("charts", category, organization, chart, version, prov_file_name) + try: + os.remove(os.path.join(".cr-release-packages", prov_file_name)) + except FileNotFoundError: + pass + shutil.copy(path, f".cr-release-packages/{prov_file_name}") + shutil.copy(path, prov_file_name) + return get_key_file(category, organization, chart, version) + return "" + +def get_key_file(category, organization, chart, version): + owners_path = os.path.join("charts", category, organization, chart, "OWNERS") + key_in_owners = signedchart.get_pgp_key_from_owners(owners_path) + if key_in_owners: + key_file_name = f"{chart}-{version}.tgz.key" + print(f"[INFO] Signed chart - add public key file : {key_file_name}") + signedchart.create_public_key_file(key_in_owners,key_file_name) + return key_file_name + return "" + + def push_chart_release(repository, organization, commit_hash): print("[INFO]push chart release. %s, %s, %s " % (repository, organization, commit_hash)) org, repo = repository.split("/") @@ -195,7 +218,6 @@ def set_package_digest(chart_entry): raise Exception("Was unable to compute SHA256 digest, please ensure chart url points to a chart package.") - def update_index_and_push(indexfile,indexdir, repository, branch, category, organization, chart, version, chart_url, chart_entry, pr_number, provider_delivery): token = os.environ.get("GITHUB_TOKEN") print(f"Downloading {indexfile}") @@ -293,7 +315,7 @@ def update_chart_annotation(category, organization, chart_file_name, chart, repo vendor_name = out["vendor"]["name"] annotations["charts.openshift.io/provider"] = vendor_name - out = subprocess.run(["tar", "zxvf", os.path.join(".cr-release-packages", f"{organization}-{chart_file_name}"), "-C", dr], capture_output=True) + out = subprocess.run(["tar", "zxvf", os.path.join(".cr-release-packages", f"{chart_file_name}"), "-C", dr], capture_output=True) print(out.stdout.decode("utf-8")) print(out.stderr.decode("utf-8")) @@ -344,12 +366,15 @@ def main(): else: indexfile = "index.yaml" + + public_key_file = "" print("[INFO] Report Content : ", os.environ.get("REPORT_CONTENT")) if chart_source_exists or chart_tarball_exists: if chart_source_exists: prepare_chart_source_for_release(category, organization, chart, version) if chart_tarball_exists: - prepare_chart_tarball_for_release(category, organization, chart, version) + signed_chart = signedchart.is_chart_signed(args.api_url,"") + public_key_file = prepare_chart_tarball_for_release(category, organization, chart, version, signed_chart) commit_hash = get_current_commit_sha() print("[INFO] Publish chart release to GitHub") @@ -358,26 +383,39 @@ def main(): print("[INFO] Check if report exist as part of the commit") report_exists, report_path = check_report_exists(category, organization, chart, version) chart_file_name = f"{chart}-{version}.tgz" + if report_exists: shutil.copy(report_path, "report.yaml") else: - tag = os.environ.get("CHART_NAME_WITH_VERSION") - if not tag: - print("[ERROR] Internal error: missing chart name with version (tag)") - sys.exit(1) - print(f"::set-output name=tag::{tag}") - print("[INFO] Genereate report") + print("[INFO] Generate report") report_path = generate_report(chart_file_name) print("[INFO] Updating chart annotation") update_chart_annotation(category, organization, chart_file_name, chart, report_path) - chart_url = f"https://github.com/{args.repository}/releases/download/{organization}-{chart}-{version}/{organization}-{chart}-{version}.tgz" + chart_url = f"https://github.com/{args.repository}/releases/download/{organization}-{chart}-{version}/{chart_file_name}" print("[INFO] Helm package was released at %s" % chart_url) print("[INFO] Creating index from chart") chart_entry = create_index_from_chart(indexdir, args.repository, branch, category, organization, chart, version, chart_url) else: report_path = os.path.join("charts", category, organization, chart, version, "report.yaml") + print(f"[INFO] Report only PR: {report_path}") + shutil.copy(report_path, "report.yaml") + if signedchart.check_report_for_signed_chart(report_path): + public_key_file = get_key_file(category, organization, chart, version) print("[INFO] Creating index from report") chart_entry, chart_url = create_index_from_report(category, report_path) + if not provider_delivery: + tag = os.environ.get("CHART_NAME_WITH_VERSION") + if not tag: + print("[ERROR] Internal error: missing chart name with version (tag)") + sys.exit(1) + print(f"::set-output name=tag::{tag}") + + current_dir = os.getcwd() + print(f"::set-output name=report_file::{current_dir}/report.yaml") + if public_key_file: + print(f"[INFO] Add key file for release : {current_dir}/{public_key_file}") + print(f"::set-output name=public_key_file::{current_dir}/{public_key_file}") + update_index_and_push(indexfile,indexdir, args.repository, branch, category, organization, chart, version, chart_url, chart_entry, args.pr_number, provider_delivery) diff --git a/scripts/src/checkprcontent/checkpr.py b/scripts/src/checkprcontent/checkpr.py index 65fadcf9e3..05aa3be04d 100644 --- a/scripts/src/checkprcontent/checkpr.py +++ b/scripts/src/checkprcontent/checkpr.py @@ -4,6 +4,7 @@ import argparse import requests +import semver import yaml try: from yaml import CLoader as Loader, CDumper as Dumper @@ -140,7 +141,7 @@ def ensure_only_chart_is_modified(api_url, repository, branch): else: matches_found += 1 if reportpattern.match(file_path): - print("[INFO] Report found") + print(f"[INFO] Report found: {file_path}") print("::set-output name=report-exists::true") report_found = True if matches_found == 1: @@ -183,6 +184,13 @@ def ensure_only_chart_is_modified(api_url, repository, branch): category, organization, chart, version = pattern_match.groups() print(f"::set-output name=category::{'partner' if category == 'partners' else category}") print(f"::set-output name=organization::{organization}") + + if not semver.VersionInfo.isvalid(version): + msg = f"[ERROR] Helm chart version is not a valid semantic version: {version}" + print(msg) + print(f"::set-output name=pr-content-error-message::{msg}") + sys.exit(1) + print("Downloading index.yaml", category, organization, chart, version) r = requests.get(f'https://raw.githubusercontent.com/{repository}/{branch}/index.yaml') if r.status_code == 200: @@ -191,7 +199,6 @@ def ensure_only_chart_is_modified(api_url, repository, branch): data = {"apiVersion": "v1", "entries": {}} - crtentries = [] entry_name = f"{organization}-{chart}" d = data["entries"].get(entry_name, []) print(f"::set-output name=chart-entry-name::{entry_name}") diff --git a/scripts/src/metrics/metrics.py b/scripts/src/metrics/metrics.py index 056127ae52..2b2914564d 100644 --- a/scripts/src/metrics/metrics.py +++ b/scripts/src/metrics/metrics.py @@ -12,9 +12,15 @@ sys.path.append('../') from indexfile import index from pullrequest import prepare_pr_comment as pr_comment +from collections import OrderedDict file_pattern = re.compile(r"charts/([\w-]+)/([\w-]+)/([\w\.-]+)/([\w\.-]+)/.*") -ignore_users=["zonggen","mmulholla","dperaza4dustbit","openshift-helm-charts-bot","baijum","tisutisu","rhrivero"] +chart_downloads_event="Chart Downloads v1.0" +ignore_users=["zonggen","mmulholla","dperaza4dustbit","openshift-helm-charts-bot","baijum","tisutisu","rhrivero","Kartikey-star"] +pr_submission="PR Submission v1.0" +pr_merged="PR Merged v1.0" +pr_outcome="PR Outcome v1.0" +charts="charts" def parse_response(response): result = [] @@ -41,8 +47,10 @@ def get_release_metrics(): result.extend(response_json) return parse_response(result) -def send_release_metrics(write_key, downloads): +def send_release_metrics(write_key, downloads, prefix): metrics={} + chart_downloads=[] + chart_downloads_latest=[] for release in downloads: _,provider,chart,_ = index.get_chart_info(release.get('name')) if len(provider)>0: @@ -57,7 +65,32 @@ def send_release_metrics(write_key, downloads): for provider in metrics: for chart in metrics[provider]: - send_metric(write_key,provider,f"{chart} downloads", metrics[provider][chart]) + ordered_download_perChart = OrderedDict(sorted(metrics[provider][chart].items(),key = lambda i: i[1],reverse=True)) + for key,value in ordered_download_perChart.items(): + chart_downloads_latest.append({"downloads":value,"name":key,"provider":provider}) + break + for key,value in metrics[provider][chart].items(): + chart_downloads.append({"downloads":value,"name":key,"provider":provider}) + chart_downloads.sort(key = lambda k : k['downloads'],reverse=True) + chart_downloads_latest.sort(key = lambda k : k['downloads'],reverse=True) + + for x in range(len(chart_downloads)): + send_download_metric(write_key,chart_downloads[x]["provider"],chart_downloads[x]["downloads"],chart_downloads[x]["name"],x+1,prefix) + + for x in range(5): + send_top_five_metric(write_key,chart_downloads_latest[x]["provider"],chart_downloads_latest[x]["downloads"],chart_downloads_latest[x]["name"],x+1,prefix) + +def send_download_metric(write_key,partner,downloads,artifact_name,rank,prefix): + id = f"{prefix}-{partner}-{artifact_name}" + properties = {"downloads":downloads,"rank":rank,"name":artifact_name } + + send_metric(write_key,id,chart_downloads_event,properties) + +def send_top_five_metric(write_key,partner,downloads,artifact_name,rank,prefix): + id = f"{prefix}-top5" + properties = {"downloads":downloads,"rank":rank,"name":artifact_name } + + send_metric(write_key,id,chart_downloads_event,properties) def send_pull_request_metrics(write_key,g): @@ -121,7 +154,7 @@ def process_report_fails(message_file): if "[ERROR] Chart verifier report includes failures:" in message_line: check_failures = True if pr_comment.get_verifier_errors_trailer() in message_line: - break; + break elif "Number of checks failed" in message_line: body_line_parts = message_line.split(":") fails = body_line_parts[1].strip() @@ -263,15 +296,14 @@ def check_and_get_pr_content(pr,repo): return "not-chart","","","","" return get_pr_content(pr) - -def process_pr(write_key,repo,message_file,pr_number,action): - pr = repo.get_pull(int(pr_number)) +def process_pr(write_key,repo,message_file,pr_number,action,prefix,pr_directory): + pr = repo.get_pull(int(pr_number)) pr_content,type,provider,chart,version = check_and_get_pr_content(pr,repo) if pr_content != "not-chart": if action == "opened": - send_submission_metric(write_key,type,provider,chart,pr_number,pr_content) + send_submission_metric(write_key,type,provider,chart,pr_number,pr_content,prefix,pr_directory) pr_result = process_comment_file(message_file,pr_number) num_fails=0 @@ -281,7 +313,8 @@ def process_pr(write_key,repo,message_file,pr_number,action): send_check_metric(write_key,type,provider,chart,pr_number,check) elif pr_result == "content-failure": num_fails = 1 - send_outcome_metric(write_key,type,provider,chart,pr_number,pr_result,num_fails) + + send_outcome_metric(write_key,type,provider,chart,pr_number,pr_result,num_fails,prefix) ## if pr is merged we can collect summary stats if pr.merged_at: @@ -303,7 +336,7 @@ def process_pr(write_key,repo,message_file,pr_number,action): elif elapsed_hours > 168: duration= "> 7 days" - send_merge_metric(write_key,type,provider,chart,duration,pr_number,builds_out,pr_content) + send_merge_metric(write_key,type,provider,chart,duration,pr_number,builds_out,pr_content,prefix,pr_directory) def send_summary_metric(write_key,num_submissions,num_merged,num_abandoned,num_in_progress,num_partners,num_charts): @@ -313,12 +346,12 @@ def send_summary_metric(write_key,num_submissions,num_merged,num_abandoned,num_i send_metric(write_key,id,"PR Summary",properties) -def send_outcome_metric(write_key,type,provider,chart,pr_number,outcome,num_fails): +def send_outcome_metric(write_key,type,provider,chart,pr_number,outcome,num_fails,prefix): properties = { "type": type, "provider": provider, "chart" : chart, "pr" : pr_number, "outcome" : outcome, "failures" : num_fails} - id = f"helm-metric-{provider}" + id = f"{prefix}-{type}-{provider}" - send_metric(write_key,id,"PR Outcome",properties) + send_metric(write_key,id,pr_outcome,properties) def send_check_metric(write_key,type,partner,chart,pr_number,check): @@ -328,26 +361,26 @@ def send_check_metric(write_key,type,partner,chart,pr_number,check): send_metric(write_key,id,"PR Report Fails",properties) -def send_merge_metric(write_key,type,partner,chart,duration,pr_number,num_builds,pr_content): - - id = f"helm-metric-{partner}" - properties = { "type" : type, "provider": partner, "chart" : chart, "pr" : pr_number, "builds" :num_builds, "duration" : duration, "content" : pr_content} +def send_merge_metric(write_key,type,partner,chart,duration,pr_number,num_builds,pr_content,prefix,pr_directory): + update=getChartUpdate(type,partner,chart,pr_directory) + id = f"{prefix}-{type}-{partner}" + properties = { "type" : type, "provider": partner, "chart" : chart, "pr" : pr_number, "builds" :num_builds, "duration" : duration, "content" : pr_content,"update": update} - send_metric(write_key,id,"PR Merged",properties) + send_metric(write_key,id,pr_merged,properties) -def send_submission_metric(write_key,type,partner,chart,pr_number,pr_content): +def send_submission_metric(write_key,type,partner,chart,pr_number,pr_content,prefix,pr_directory): - id = f"helm-metric-{partner}" - properties = { "type" : type, "provider": partner, "chart" : chart, "pr" : pr_number, "pr content": pr_content} + update=getChartUpdate(type,partner,chart,pr_directory) + id = f"{prefix}-{type}-{partner}" + properties = { "type" : type, "provider": partner, "chart" : chart, "pr" : pr_number, "pr content": pr_content,"update": update} - send_metric(write_key,id,"PR Submission",properties) + send_metric(write_key,id,pr_submission,properties) def on_error(error,items): print("An error occurred creating metrics:", error) print("error with items:",items) sys.exit(1) - def send_metric(write_key,id,event,properties): analytics.write_key = write_key @@ -363,6 +396,16 @@ def check_rate_limit(g,force): if force or rate_limit.core.remaining < 10: print(f"[INFO] rate limit info: {rate_limit.core}") +def getChartUpdate(type,partner,chart,cwd): + + directoryPath=os.path.join(cwd, charts,type, partner,chart) + # Checking if the directory contains only the OWNERS file + print(os.listdir(directoryPath)) + print(len(os.listdir(directoryPath))) + if len(os.listdir(directoryPath)) == 1: + return "new chart" + else: + return "new version" def main(): @@ -379,6 +422,10 @@ def main(): help="The event action of the pr") parser.add_argument("-r", "--repository", dest="repository", type=str, required=False, help="The repository of the pr") + parser.add_argument("-p", "--prefix", dest="prefix", type=str, required=False, + help="The prefix of the id in segment") + parser.add_argument("-d", "--pr_dir", dest="pr_dir", type=str, required=False, + help="Directory of pull request code.") args = parser.parse_args() print("Input arguments:") @@ -388,6 +435,8 @@ def main(): print(f" --pr-number : {args.pr_number}") print(f" --pr-action : {args.pr_action}") print(f" --repository : {args.repository}") + print(f" --prefix : {args.prefix}") + print(f" --pr_dir : {args.pr_dir}") if not args.write_key: print("Error: Segment write key not set") @@ -397,10 +446,10 @@ def main(): if args.type == "pull_request": repo_current = g.get_repo(args.repository) - process_pr(args.write_key,repo_current,args.message_file,args.pr_number,args.pr_action) + process_pr(args.write_key,repo_current,args.message_file,args.pr_number,args.pr_action,args.prefix,args.pr_dir) else: check_rate_limit(g,True) - send_release_metrics(args.write_key,get_release_metrics()) + send_release_metrics(args.write_key,get_release_metrics(),args.prefix) check_rate_limit(g,True) send_pull_request_metrics(args.write_key,g) check_rate_limit(g,True) diff --git a/scripts/src/metrics/pushowners.py b/scripts/src/metrics/pushowners.py new file mode 100644 index 0000000000..4c6d998efa --- /dev/null +++ b/scripts/src/metrics/pushowners.py @@ -0,0 +1,100 @@ +import argparse +import re +from github import Github +import yaml +import sys +import analytics +sys.path.append('../') +from owners import owners_file + +def getVendorType(changed_file): + path_as_list=changed_file.split("/") + for i in (range(len(path_as_list) - 1)): + if path_as_list[i]=='charts': + vendor_type=path_as_list[i+1] + return vendor_type + +def getFileContent(changed_file): + status,owner_data=owners_file.get_owner_data_from_file(changed_file) + if status==True: + users_included=owners_file.get_users_included(owner_data) + provider_delivery=owners_file.get_provider_delivery(owner_data) + if provider_delivery == False: + provider="No" + else: + provider="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,provider,vendor_name,chart_name,vendor_type + else: + print("Exception loading OWNERS file") + return "","","","","" + +def process_pr(added_file,modified_file): + if modified_file!='': + action="update" + update="existing-vendor" + users_included,provider_delivery,vendor_name,chart_name,vendor_type=getFileContent(modified_file) + return users_included,provider_delivery,vendor_name,chart_name,vendor_type,action,update + elif added_file!='': + action="create" + update="new-vendor" + users_included,provider_delivery,vendor_name,chart_name,vendor_type=getFileContent(added_file) + return users_included,provider_delivery,vendor_name,chart_name,vendor_type,action,update + + +def send_owner_metric(write_key,prefix,users_included,provider_delivery,partner,chart_name,type,action,update): + if chart_name!="" and partner!="": + id = f"{prefix}-{type}-{chart_name}" + properties = { "type" : type, "vendor": partner, "chart" : chart_name, "users_included" : users_included, "provider_delivery" :provider_delivery, "action" : action, "update" : update} + send_metric(write_key,id,"owners v1.0",properties) + +def on_error(error,items): + print("An error occurred creating metrics:", error) + print("error with items:",items) + sys.exit(1) + +def send_metric(write_key,id,event,properties): + + analytics.write_key = write_key + analytics.on_error = on_error + + print(f'[INFO] Add track: id: {id}, event:{event}, properties:{properties}') + + analytics.track(id, event, properties) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("-k", "--write-key", dest="write_key", type=str, required=True, + help="segment write key") + parser.add_argument("-t", "--metric-type", dest="type", type=str, required=True, + help="metric type, releases or pull_request") + parser.add_argument("-n", "--added", dest="added", nargs="*", required=False, + help="files added") + parser.add_argument("-a", "--modified", dest="modified", nargs="*", required=False, + help="files modified") + parser.add_argument("-r", "--repository", dest="repository", type=str, required=False, + help="The repository of the pr") + parser.add_argument("-p", "--prefix", dest="prefix", type=str, required=False, + help="The prefix of the id in segment") + + + args = parser.parse_args() + print("Input arguments:") + print(f" --write-key length : {len(args.write_key)}") + print(f" --metric-type : {args.type}") + print(f" --added : {args.added}") + print(f" --modified : {args.modified}") + print(f" --repository : {args.repository}") + print(f" --prefix : {args.prefix}") + + if not args.write_key: + print("Error: Segment write key not set") + sys.exit(1) + + users_included,provider_delivery,vendor_name,chart_name,vendor_type,action,update = process_pr(args.added[0],args.modified[0]) + send_owner_metric(args.write_key,args.prefix,users_included,provider_delivery,vendor_name,chart_name,vendor_type,action,update) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/scripts/src/owners/owners_file.py b/scripts/src/owners/owners_file.py index 0f4116c091..10bbd48caa 100644 --- a/scripts/src/owners/owners_file.py +++ b/scripts/src/owners/owners_file.py @@ -8,20 +8,59 @@ 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 + +def get_owner_data_from_file(owner_path): try: - with open(os.path.join("charts", category, organization, chart, "OWNERS")) as owner_data: + with open(owner_path) as owner_data: owner_content = yaml.load(owner_data,Loader=Loader) return True,owner_content except Exception as err: print(f"Exception loading OWNERS file: {err}") return False,"" +def get_vendor(owner_data): + vendor="" + try: + vendor = owner_data['vendor']['name'] + except Exception: + pass + return vendor + +def get_chart(owner_data): + chart="" + try: + chart = owner_data['chart']['name'] + except Exception: + pass + return chart def get_provider_delivery(owner_data): provider_delivery = False try: - provider_delivery = owner_data["providerDelivery"] + provider_delivery = owner_data['providerDelivery'] except Exception: pass return provider_delivery +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 + +def get_pgp_public_key(owner_data): + pgp_public_key = "null" + try: + pgp_public_key = owner_data["publicPgpKey"] + except Exception: + pass + return pgp_public_key + + diff --git a/scripts/src/pullrequest/prepare_pr_comment.py b/scripts/src/pullrequest/prepare_pr_comment.py index e7e17cab1d..67a043db56 100644 --- a/scripts/src/pullrequest/prepare_pr_comment.py +++ b/scripts/src/pullrequest/prepare_pr_comment.py @@ -17,18 +17,20 @@ def get_verifier_errors_comment(): return "[ERROR] The submitted chart has failed certification. Reason(s):" def get_verifier_errors_trailer(): - return "Please run the [chart-verifier](https://github.com/redhat-certification/chart-verifier) \ + return "Please create a valid report with the [chart-verifier](https://github.com/redhat-certification/chart-verifier) \ and ensure all mandatory checks pass." +def get_look_at_job_output_comment(): + return f"""To see the console output with the error messages, click the "Details" \ +link next to "CI / Chart Certification" job status towards the end of this page.""" + def prepare_failure_comment(): msg = f"""\ {get_failure_comment()} -To see the console output with the error messages, click the "Details" -link next to "CI / Chart Certification" job status towards the end of this page. -""" +{get_look_at_job_output_comment()}""" if os.path.exists("./pr/errors"): errors = open("./pr/errors").read() - msg += f""" + msg += f""" {get_verifier_errors_comment()} {errors} @@ -36,7 +38,9 @@ def prepare_failure_comment(): {get_verifier_errors_trailer()} """ - print(f"::set-output name=error-message::{errors}") + print(f"::set-output name=error-message::{errors}") + else: + print(f"::set-output name=error-message::{get_failure_comment()}") return msg def prepare_success_comment(): @@ -55,6 +59,17 @@ def prepare_pr_content_failure_comment(): msg += f"{owners_error_msg}\n\n" return msg +def prepare_run_verifier_failure_comment(): + verifier_error_msg = os.environ.get("VERIFIER_ERROR_MESSAGE", "") + print(f"::set-output name=error-message::{verifier_error_msg}") + msg = f""" +{verifier_error_msg} + +{get_look_at_job_output_comment()} +""" + return msg + + def prepare_community_comment(): msg = f"{get_community_review_message()}\n\n" if os.path.exists("./pr/errors"): @@ -82,8 +97,8 @@ def get_comment_footer(vendor_label, chart_name): def main(): pr_content_result = sys.argv[1] - verify_result = sys.argv[2] - repository = sys.argv[3] + run_verifier_result = sys.argv[2] + verify_result = sys.argv[3] issue_number = open("./pr/NR").read().strip() vendor_label = open("./pr/vendor").read().strip() chart_name = open("./pr/chart").read().strip() @@ -92,6 +107,9 @@ def main(): if pr_content_result == "failure": msg += prepare_pr_content_failure_comment() print(f"::set-output name=pr_passed::false") + elif run_verifier_result == "failure": + msg += prepare_run_verifier_failure_comment() + print(f"::set-output name=pr_passed::false") elif verify_result == "failure": community_manual_review = os.environ.get("COMMUNITY_MANUAL_REVIEW",False) if community_manual_review: diff --git a/scripts/src/release/release_info.py b/scripts/src/release/release_info.py index 3685463fdf..35e59bb39b 100644 --- a/scripts/src/release/release_info.py +++ b/scripts/src/release/release_info.py @@ -39,34 +39,34 @@ def get_info(directory): return info["info"] -def get_replaces(repo,directory): - print(f"get replaces for {repo}") +def get_replaces(from_repo,to_repo,directory): + print(f"get replaces for {from_repo} to {to_repo} ") info = _get_release_info(directory) - if repo in info: - if "replace" in info[repo]: - print(f"replaces found: {info[repo]['replace']}") - return info[repo]["replace"] + if from_repo in info: + if "replace" in info[from_repo][to_repo]: + print(f"replaces found: {info[from_repo][to_repo]['replace']}") + return info[from_repo][to_repo]["replace"] print("no replaces found") return [] -def get_merges(repo,directory): - print(f"get merges for {repo}") +def get_merges(from_repo,to_repo,directory): + print(f"get merges for {from_repo} to {to_repo}") info = _get_release_info(directory) - if repo in info: - if "merge" in info[repo]: - print(f"merges found: {info[repo]['merge']}") - return info[repo]["merge"] + if from_repo in info: + if "merge" in info[from_repo][to_repo]: + print(f"merges found: {info[from_repo][to_repo]['merge']}") + return info[from_repo][to_repo]["merge"] print("no merges found") return [] -def get_ignores(repo,directory): - print(f"get ignores for {repo}") +def get_ignores(from_repo,to_repo,directory): + print(f"get ignores for {from_repo} to {to_repo}") info = _get_release_info(directory) - if repo in info: - if "ignore" in info[repo]: - print(f"ignores found: {info[repo]['ignore']}") - return info[repo]["ignore"] + if from_repo in info: + if "ignore" in info[from_repo][to_repo]: + print(f"ignores found: {info[from_repo][to_repo]['ignore']}") + return info[from_repo][to_repo]["ignore"] print("no ignores found") return [] @@ -75,17 +75,26 @@ def main(): print(f"[INFO] Version : {get_version('.')}") - print(f"[INFO] Dev repo merges : {get_merges('development','.')}") + #from development to charts + print(f"[INFO] Dev to charts repo merges : {get_merges('development','charts','.')}") - print(f"[INFO] Dev repo replace : {get_replaces('development','.')}") + print(f"[INFO] Dev to charts repo replace : {get_replaces('development','charts','.')}") - print(f"[INFO] Dev repo ignore : {get_ignores('development','.')}") + print(f"[INFO] Dev to charts repo ignore : {get_ignores('development','charts','.')}") - print(f"[INFO] Chart repo merges : {get_merges('charts','.')}") + #from development to stage + print(f"[INFO] Dev to stage repo merges : {get_merges('development','stage','.')}") - print(f"[INFO] Chart repo replace : {get_replaces('charts','.')}") + print(f"[INFO] Dev to stage repo replace : {get_replaces('development','stage','.')}") - print(f"[INFO] Chart repo ignore : {get_ignores('charts','.')}") + print(f"[INFO] Dev to stage repo ignore : {get_ignores('development','stage','.')}") + + #From charts to development + print(f"[INFO] Chart to dev repo merges : {get_merges('charts','development','.')}") + + print(f"[INFO] Chart to dev repo replace : {get_replaces('charts','development','.')}") + + print(f"[INFO] Chart to dev repo ignore : {get_ignores('charts','development','.')}") if __name__ == "__main__": diff --git a/scripts/src/release/releaser.py b/scripts/src/release/releaser.py index 02cd03ca57..2b5faa9478 100644 --- a/scripts/src/release/releaser.py +++ b/scripts/src/release/releaser.py @@ -35,6 +35,8 @@ DEV_PR_BRANCH_NAME_PREFIX="Auto-Release-" CHARTS_PR_BRANCH_BODY_PREFIX="Workflow and script updates from development repository" CHARTS_PR_BRANCH_NAME_PREFIX="Release-" +STAGE_PR_BRANCH_BODY_PREFIX="Workflow and script updates from development repository" +STAGE_PR_BRANCH_NAME_PREFIX="Release-" SCHEDULE_INSERT = [ ' # Daily trigger to check updates', @@ -66,11 +68,19 @@ def make_required_changes(release_info_dir,origin,destination): print(f"Make required changes from {origin} to {destination}") - repository = "development" - if "charts" in origin or "development" in destination: - repository = "charts" + if "charts" in origin and "dev" in destination: + from_repository = "charts" + to_repository = "development" + elif "dev" in origin and "charts" in destination: + from_repository = "development" + to_repository = "charts" + elif "dev" in origin and "stage" in destination: + from_repository = "development" + to_repository = "stage" + else: + sys.exit("Wrong arguments while calling make_required_changes") - replaces = release_info.get_replaces(repository,release_info_dir) + replaces = release_info.get_replaces(from_repository,to_repository,release_info_dir) for replace in replaces: replace_this=f"{destination}/{replace}" @@ -84,7 +94,7 @@ def make_required_changes(release_info_dir,origin,destination): print(f"Replace file {replace_this} with {with_this}") shutil.copy2(with_this,replace_this) - merges = release_info.get_merges(repository,release_info_dir) + merges = release_info.get_merges(from_repository,to_repository,release_info_dir) for merge in merges: merge_this = f"{origin}/{merge}" @@ -98,7 +108,7 @@ def make_required_changes(release_info_dir,origin,destination): shutil.copy2(merge_this,into_this) - ignores = release_info.get_ignores(repository,release_info_dir) + ignores = release_info.get_ignores(from_repository,to_repository,release_info_dir) for ignore in ignores: ignore_this = f"{destination}/{ignore}" if os.path.isdir(ignore_this): @@ -119,6 +129,8 @@ def main(): help="Directory of development code with latest release info.") parser.add_argument("-c", "--charts_dir", dest="charts_dir", type=str, required=True, help="Directory of charts code.") + parser.add_argument("-s", "--stage_dir", dest="stage_dir", type=str, required=True, + help="Directory of stage code.") parser.add_argument("-p", "--pr_dir", dest="pr_dir", type=str, required=True, help="Directory of pull request code.") parser.add_argument("-b", "--dev_pr_body", dest="dev_pr_body", type=str, required=True, @@ -134,6 +146,7 @@ def main(): print(f"[INFO] arg version : {args.version}") print(f"[INFO] arg dev_dir : {args.dev_dir}") print(f"[INFO] arg charts_dir : {args.charts_dir}") + print(f"[INFO] arg stage_dir : {args.stage_dir}") print(f"[INFO] arg pr_dir : {args.pr_dir}") print(f"[INFO] arg dev_pr_body : {args.dev_pr_body}") print(f"[INFO] arg target_branch : {args.target_branch}") @@ -187,5 +200,25 @@ def main(): os.chdir(start_directory) + print(f"make changes to stage from development") + make_required_changes(args.pr_dir,args.dev_dir,args.stage_dir) + os.chdir(args.stage_dir) + stage_repository=f"{organization}{gitutils.STAGE_REPO}" + print(f"create stage pull request, repository: {stage_repository}, branch: {args.target_branch} ") + branch_name = f"{STAGE_PR_BRANCH_NAME_PREFIX}{args.version}" + message = f'{STAGE_PR_BRANCH_BODY_PREFIX} {branch_name}' + outcome = gitutils.create_pr(branch_name,[],stage_repository,message,args.target_branch) + if outcome == gitutils.PR_CREATED: + print(f'::set-output name=stage_pr_created::true') + elif outcome == gitutils.PR_NOT_NEEDED: + print(f'::set-output name=stage_pr_not_needed::true') + else: + print("[ERROR] error creating stage PR") + print(f'::set-output name=stage_pr_error::true') + os.chdir(start_directory) + return + + os.chdir(start_directory) + if __name__ == "__main__": main() diff --git a/scripts/src/report/get_verify_params.py b/scripts/src/report/get_verify_params.py index c8a69116b9..d369b27a99 100644 --- a/scripts/src/report/get_verify_params.py +++ b/scripts/src/report/get_verify_params.py @@ -5,6 +5,7 @@ sys.path.append('../') from chartprreview import chartprreview +from signedchart import signedchart def generate_verify_options(directory,category, organization, chart, version): print("[INFO] Generate verify options. %s, %s, %s" % (organization,chart,version)) @@ -28,6 +29,12 @@ def generate_verify_options(directory,category, organization, chart, version): return flags,src,True, cluster_needed elif os.path.exists(tar) and not os.path.exists(src): print("[INFO] tarball included") + if not os.path.exists(report_path): + owners_file = os.path.join(os.getcwd(),"charts", category, organization, chart, "OWNERS") + signed_flags = signedchart.get_verifier_flags(tar,owners_file,directory) + if signed_flags: + print(f"[INFO] include flags for signed chart: {signed_flags}") + flags = f"{flags} {signed_flags}" return flags,tar,True, cluster_needed elif os.path.exists(tar) and os.path.exists(src): msg = "[ERROR] Both chart source directory and tarball should not exist" diff --git a/scripts/src/report/report_info.py b/scripts/src/report/report_info.py index b78a65e796..96ede706e0 100644 --- a/scripts/src/report/report_info.py +++ b/scripts/src/report/report_info.py @@ -9,6 +9,20 @@ REPORT_RESULTS = "results" REPORT_DIGESTS = "digests" REPORT_METADATA = "metadata" +SHA_ERROR = "Digest in report did not match report content" + +def write_error_log(*msg): + directory = os.environ.get("WORKFLOW_WORKING_DIRECTORY") + if directory: + os.makedirs(directory, exist_ok=True) + with open(os.path.join(directory, "errors"), "w") as fd: + for line in msg: + fd.write(line) + fd.write("\n") + + for line in msg: + print(line) + def _get_report_info(report_path, report_info_path,info_type, profile_type, profile_version): @@ -44,15 +58,23 @@ def _get_report_info(report_path, report_info_path,info_type, profile_type, prof out = subprocess.run(["chart-verifier",command,info_type,os.path.abspath(report_path)],capture_output=True) output = out.stdout.decode("utf-8") + if SHA_ERROR in output: + msg = f"[ERROR] {SHA_ERROR}" + write_error_log(msg) + sys.exit(1) + try: report_out = json.loads(output) except BaseException as err: - print(f"[ERROR] loading report output: /n{output}") - print(f"[ERROR] exception was: {err=}, {type(err)=}") + msgs = [] + msgs.append(f"[ERROR] loading report output: /n{output}") + msgs.append(f"[ERROR] exception was: {err=}, {type(err)=}") + write_error_log(*msgs) sys.exit(1) if not info_type in report_out: - print(f"Error extracting {info_type} from the report:", report_out.strip()) + msg = f"Error extracting {info_type} from the report:", report_out.strip() + write_error_log(msg) sys.exit(1) if info_type == REPORT_ANNOTATIONS: diff --git a/scripts/src/report/verifier_report.py b/scripts/src/report/verifier_report.py index d05c671434..dd4d250a2d 100644 --- a/scripts/src/report/verifier_report.py +++ b/scripts/src/report/verifier_report.py @@ -24,7 +24,6 @@ """ import sys -import os import semantic_version import yaml @@ -54,12 +53,14 @@ def get_report_data(report_path): def get_result(report_data,check_name): outcome = False + reason = "Not Found" for result in report_data["results"]: if result["check"].endswith(check_name): + reason = result["reason"] if result["outcome"] == "PASS": outcome = True break - return outcome + return outcome,reason def get_chart_testing_result(report_data): return get_result(report_data,"/chart-testing") @@ -67,6 +68,9 @@ def get_chart_testing_result(report_data): def get_has_kubeversion_result(report_data): return get_result(report_data,"/has-kubeversion") +def get_signature_is_valid_result(report_data): + return get_result(report_data,"/signature-is-valid") + def get_profile_version(report_data): profile_version = "1.1" try: @@ -91,10 +95,22 @@ def get_package_digest(report_data): if "package" in digests: package_digest = digests["package"] except Exception as err: - print(f"Exception getting providerControlledDelivery {err=}, {type(err)=}") + print(f"Exception getting package digest {err=}, {type(err)=}") pass return package_digest +def get_public_key_digest(report_data): + public_key_digest = None + try: + digests = report_data["metadata"]["tool"]["digests"] + if "publicKey" in digests: + public_key_digest = digests["publicKey"] + except Exception as err: + print(f"Exception getting publicKey digest {err=}, {type(err)=}") + pass + return public_key_digest + + def report_is_valid(report_data): outcome = True @@ -130,7 +146,8 @@ def validate(report_path): return False,f"Report is incomplete and cannot be processed: {report_path}" ## No value in checking if chart testing failed - if get_chart_testing_result(report_data): + chart_testing_outcome,_ = get_chart_testing_result(report_data) + if chart_testing_outcome: profile_version_string = get_profile_version(report_data) @@ -164,7 +181,8 @@ def validate(report_path): except ValueError: return False,f"{tested_version_annotation} {tested_version_string} is not a valid semantic version." - if get_has_kubeversion_result: + has_kubeversion_outcome,_ = get_chart_testing_result(report_data) + if has_kubeversion_outcome: chart = report_info.get_report_chart(report_path) if KUBE_VERSION_ATTRIBUTE in chart: diff --git a/scripts/src/signedchart/__init__.py b/scripts/src/signedchart/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/src/signedchart/signedchart.py b/scripts/src/signedchart/signedchart.py new file mode 100644 index 0000000000..c4f91c0a87 --- /dev/null +++ b/scripts/src/signedchart/signedchart.py @@ -0,0 +1,167 @@ +import requests +import sys +import subprocess +import base64 +import filecmp +import os +import re + +try: + from yaml import CLoader as Loader, CDumper as Dumper +except ImportError: + from yaml import Loader, Dumper + +sys.path.append('../') +from report import verifier_report +from owners import owners_file + +def check_and_prepare_signed_chart(api_url,report_path,owner_path,key_file_path): + + signed_chart = is_chart_signed(api_url,report_path) + key_in_owners = False + keys_match = False + if signed_chart: + owners_pgp_key = get_pgp_key_from_owners(owner_path) + if owners_pgp_key: + key_in_owners = True + if report_path: + keys_match = check_pgp_public_key(owners_pgp_key,report_path) + elif key_file_path: + create_public_key_file(owners_pgp_key,key_file_path) + + return signed_chart,key_in_owners,keys_match + +def get_verifier_flags(tar_file,owners_file,temp_dir): + prov_file = f"{tar_file}.prov" + if os.path.exists(prov_file): + gpg_key = get_pgp_key_from_owners(owners_file) + if gpg_key: + key_file = os.path.join(temp_dir,"pgp",f"{tar_file}.key") + create_public_key_file(gpg_key,key_file) + return f"--pgp-public-key {key_file}" + return "" + + +def is_chart_signed(api_url,report_path): + + if api_url: + files_api_url = f'{api_url}/files' + headers = {'Accept': 'application/vnd.github.v3+json'} + r = requests.get(files_api_url, headers=headers) + tgz_pattern = re.compile(r"charts/(\w+)/([\w-]+)/([\w-]+)/([\w\.-]+)/.*.tgz") + tgz_found = False + prov_pattern = re.compile(r"charts/(\w+)/([\w-]+)/([\w-]+)/([\w\.-]+)/.*.tgz.prov") + prov_found = False + + for f in r.json(): + if tgz_pattern.match(f["filename"]): + tgz_found = True + if prov_pattern.match(f["filename"]): + prov_found = True + + if tgz_found and prov_found: + return True + elif report_path: + return check_report_for_signed_chart(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) + if pgp_key == "null": + pgp_key = "" + + return pgp_key + return "" + + +def check_report_for_signed_chart(report_path): + + found,report_data = verifier_report.get_report_data(report_path) + if found: + outcome,reason = verifier_report.get_signature_is_valid_result(report_data) + if "Chart is signed" in reason: + return True + return False + + +def check_pgp_public_key(owner_pgp_key,report_path): + + ## return True if one of: + # - report not found + # - report is not for a signed chart + # - digests match + found,report_data = verifier_report.get_report_data(report_path) + if found: + pgp_public_key_digest_owners = subprocess.getoutput(f'echo {owner_pgp_key} | sha256sum').split(" ")[0] + print(f"[INFO] digest of PGP key from OWNERS :{pgp_public_key_digest_owners}:") + pgp_public_digest_report = verifier_report.get_public_key_digest(report_data) + print(f"[INFO] PGP key digest in report :{pgp_public_digest_report}:") + if pgp_public_digest_report: + return pgp_public_key_digest_owners == pgp_public_digest_report + else: + return not check_report_for_signed_chart(report_path) + return True + + +def create_public_key_file(pgp_public_key_from_owners,key_file_path): + + key_content = base64.b64decode(pgp_public_key_from_owners) + + key_file = open(key_file_path, "w") + key_file.write(key_content.decode('utf-8')) + key_file.close() + + +def main(): + + if not is_chart_signed("","./partner-report.yaml"): + print("ERROR chart is signed") + else: + print("PASS chart is signed") + + if not check_report_for_signed_chart("./partner-report.yaml"): + print("ERROR report indicates chart is signed") + else: + print("PASS report is signed") + + + encoded_key_in_owners = get_pgp_key_from_owners("./OWNERS") + if not check_pgp_public_key(encoded_key_in_owners,"./partner-report.yaml"): + print("ERROR key digests do not match") + else: + print("PASS key digests match") + + + signed,key_in_owners,keys_match = check_and_prepare_signed_chart("","./partner-report.yaml","./OWNERS","./pgp.key") + if signed and key_in_owners and keys_match: + print("PASS all is good") + else: + print(f"ERROR, all true expected: signed = {signed}, key_in_owners = {key_in_owners}. keys_match = {keys_match}") + + create_public_key_file(encoded_key_in_owners,"./pgp.key") + if os.path.exists("./pgp.key"): + if not filecmp.cmp("./psql-service-0.1.11.tgz.key","./pgp.key"): + print("ERROR public key files file do not match") + else: + print("PASS public key files do match") + os.remove("./pgp.key") + else: + print("ERROR pgp key file was not created") + + + +if __name__ == '__main__': + main() + + + + diff --git a/scripts/src/tools/gitutils.py b/scripts/src/tools/gitutils.py index 6c6707e0fe..877688385e 100644 --- a/scripts/src/tools/gitutils.py +++ b/scripts/src/tools/gitutils.py @@ -21,6 +21,7 @@ GITHUB_BASE_URL = 'https://api.github.com' CHARTS_REPO = "/charts" DEVELOPMENT_REPO = "/development" +STAGE_REPO = "/stage" PR_CREATED = "PR_CREATED" PR_NOT_NEEDED = "PR_NOT_NEEDED" diff --git a/scripts/src/workflowtesting/checkprforci.py b/scripts/src/workflowtesting/checkprforci.py index 936469ea7b..697effe837 100644 --- a/scripts/src/workflowtesting/checkprforci.py +++ b/scripts/src/workflowtesting/checkprforci.py @@ -18,15 +18,14 @@ def check_if_ci_only_is_modified(api_url): headers = {'Accept': 'application/vnd.github.v3+json'} workflow_files = [re.compile(r".github/workflows/.*"),re.compile(r"scripts/.*"),re.compile(r"tests/.*")] - test_files = [re.compile(r"tests/functional/step_defs/test.*"),re.compile(r"tests/functional/features/.*.feature")] - smoke_test_files = [re.compile(r"tests/functional/features/smoke/.*"),re.compile(r"tests/functional/step_defs/.*smoke.*.py")] + test_files = [re.compile(r"tests/functional/step_defs/.*_test_.*"),re.compile(r"tests/functional/behave_features/.*.feature")] skip_build_files = [re.compile(r"release/release_info.json"),re.compile(r"README.md"),re.compile(r"docs/([\w-]+)\.md")] page_number = 1 max_page_size,page_size = 100,100 workflow_found = False others_found = False - full_tests_included = False + tests_included = False while (page_size == max_page_size): @@ -41,9 +40,7 @@ def check_if_ci_only_is_modified(api_url): if any([pattern.match(filename) for pattern in workflow_files]): workflow_found = True if any([pattern.match(filename) for pattern in test_files]): - if not any([pattern.match(filename) for pattern in smoke_test_files]): - print(f"[INFO] full test file found: {filename}") - full_tests_included = True + tests_included = True elif any([pattern.match(filename) for pattern in skip_build_files]): others_found = True else: @@ -51,7 +48,7 @@ def check_if_ci_only_is_modified(api_url): if others_found and not workflow_found: print("::set-output name=do-not-build::true") - elif full_tests_included: + elif tests_included: print(f"[INFO] set full_tests_in_pr to true") print("::set-output name=full_tests_in_pr::true") diff --git a/tests/data/HC-04/community/report.yaml b/tests/data/HC-04/community/report.yaml new file mode 100644 index 0000000000..e723c14c0f --- /dev/null +++ b/tests/data/HC-04/community/report.yaml @@ -0,0 +1,100 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.9.0 + profile: + VendorType: community + version: v1.1 + reportDigest: uint64:6109394152551061817 + chart-uri: vault-0.17.0.tgz + digests: + chart: sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a + package: 9cb7e3a5d82537f512319c754ccd6e9a4be08118b34849609d0fc261934d062a + lastCertifiedTimestamp: "2022-10-09T19:21:54.5696+05:30" + testedOpenShiftVersion: "4.11" + supportedOpenShiftVersions: '>=4.2' + providerControlledDelivery: false + chart: + name: vault + home: https://www.vaultproject.io + sources: + - https://github.com/hashicorp/vault + - https://github.com/hashicorp/vault-helm + - https://github.com/hashicorp/vault-k8s + - https://github.com/hashicorp/vault-csi-provider + version: 0.17.0 + description: Official HashiCorp Vault Chart + keywords: + - vault + - security + - encryption + - secrets + - management + - automation + - infrastructure + maintainers: [] + icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png + apiversion: v2 + condition: "" + tags: "" + appversion: 1.8.4 + deprecated: false + annotations: + charts.openshift.io/name: vault + kubeversion: '>= 1.14.0-0' + dependencies: [] + type: "" + chart-overrides: "" +results: + - check: v1.0/not-contains-crds + type: Optional + outcome: PASS + reason: Chart does not contain CRDs + - check: v1.0/is-helm-v3 + type: Optional + outcome: PASS + reason: API version is V2, used in Helm 3 + - check: v1.0/required-annotations-present + type: Optional + outcome: PASS + reason: All required annotations present + - check: v1.0/chart-testing + type: Optional + outcome: PASS + reason: Chart tests have passed + - check: v1.0/has-readme + type: Optional + outcome: PASS + reason: Chart has a README + - check: v1.0/images-are-certified + type: Optional + outcome: PASS + reason: |- + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi + - check: v1.0/not-contain-csi-objects + type: Optional + outcome: PASS + reason: CSI objects do not exist + - check: v1.0/contains-values-schema + type: Optional + outcome: PASS + reason: Values schema file exist + - check: v1.0/contains-test + type: Optional + outcome: PASS + reason: Chart test files exist + - check: v1.0/contains-values + type: Optional + outcome: PASS + reason: Values file exist + - check: v1.1/has-kubeversion + type: Optional + outcome: PASS + reason: Kubernetes version specified + - check: v1.0/helm-lint + type: Mandatory + outcome: PASS + reason: Helm lint successful + diff --git a/tests/data/report.yaml b/tests/data/HC-04/partner/report.yaml similarity index 92% rename from tests/data/report.yaml rename to tests/data/HC-04/partner/report.yaml index f046843ff6..be892bc91b 100644 --- a/tests/data/report.yaml +++ b/tests/data/HC-04/partner/report.yaml @@ -2,16 +2,17 @@ apiversion: v1 kind: verify-report metadata: tool: - verifier-version: 1.4.0 + verifier-version: 1.9.0 profile: VendorType: partner version: v1.1 - chart-uri: https://github.com/${repository}/raw/${branch}/tests/data/vault-0.17.0.tgz + reportDigest: uint64:2083954931931251669 + chart-uri: vault-0.17.0.tgz digests: chart: sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a package: 9cb7e3a5d82537f512319c754ccd6e9a4be08118b34849609d0fc261934d062a - lastCertifiedTimestamp: "2021-11-24T11:42:48.736244+05:30" - testedOpenShiftVersion: "4.9" + lastCertifiedTimestamp: "2022-10-09T19:11:54.765745+05:30" + testedOpenShiftVersion: "4.11" supportedOpenShiftVersions: '>=4.2' providerControlledDelivery: false chart: @@ -46,54 +47,54 @@ metadata: type: "" chart-overrides: "" results: - - check: v1.0/contains-values-schema + - check: v1.0/has-readme type: Mandatory outcome: PASS - reason: Values schema file exist - - check: v1.0/helm-lint + reason: Chart has a README + - check: v1.0/chart-testing type: Mandatory outcome: PASS - reason: Helm lint successful - - check: v1.0/images-are-certified + reason: Chart tests have passed + - check: v1.0/contains-test type: Mandatory outcome: PASS - reason: |- - Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi - Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi - - check: v1.0/chart-testing + reason: Chart test files exist + - check: v1.0/helm-lint type: Mandatory outcome: PASS - reason: Chart tests have passed - - check: v1.0/has-readme + reason: Helm lint successful + - check: v1.0/required-annotations-present type: Mandatory outcome: PASS - reason: Chart has a README - - check: v1.0/is-helm-v3 + reason: All required annotations present + - check: v1.0/contains-values-schema type: Mandatory outcome: PASS - reason: API version is V2, used in Helm 3 + reason: Values schema file exist - check: v1.0/contains-values type: Mandatory outcome: PASS reason: Values file exist - - check: v1.0/not-contain-csi-objects - type: Mandatory - outcome: PASS - reason: CSI objects do not exist - - check: v1.0/required-annotations-present + - check: v1.1/has-kubeversion type: Mandatory outcome: PASS - reason: All required annotations present - - check: v1.0/contains-test + reason: Kubernetes version specified + - check: v1.0/not-contain-csi-objects type: Mandatory outcome: PASS - reason: Chart test files exist - - check: v1.1/has-kubeversion + reason: CSI objects do not exist + - check: v1.0/is-helm-v3 type: Mandatory outcome: PASS - reason: Kubernetes version specified + 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/images-are-certified + type: Mandatory + outcome: PASS + reason: |- + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi diff --git a/tests/data/HC-04/redhat/report.yaml b/tests/data/HC-04/redhat/report.yaml new file mode 100644 index 0000000000..10970983d4 --- /dev/null +++ b/tests/data/HC-04/redhat/report.yaml @@ -0,0 +1,100 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.9.0 + profile: + VendorType: redhat + version: v1.1 + reportDigest: uint64:8490969933058949466 + chart-uri: vault-0.17.0.tgz + digests: + chart: sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a + package: 9cb7e3a5d82537f512319c754ccd6e9a4be08118b34849609d0fc261934d062a + lastCertifiedTimestamp: "2022-10-09T19:18:05.749707+05:30" + testedOpenShiftVersion: "4.11" + supportedOpenShiftVersions: '>=4.2' + providerControlledDelivery: false + chart: + name: vault + home: https://www.vaultproject.io + sources: + - https://github.com/hashicorp/vault + - https://github.com/hashicorp/vault-helm + - https://github.com/hashicorp/vault-k8s + - https://github.com/hashicorp/vault-csi-provider + version: 0.17.0 + description: Official HashiCorp Vault Chart + keywords: + - vault + - security + - encryption + - secrets + - management + - automation + - infrastructure + maintainers: [] + icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png + apiversion: v2 + condition: "" + tags: "" + appversion: 1.8.4 + deprecated: false + annotations: + charts.openshift.io/name: vault + kubeversion: '>= 1.14.0-0' + dependencies: [] + type: "" + chart-overrides: "" +results: + - 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.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/contains-test + type: Mandatory + outcome: PASS + reason: Chart test files 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 + - check: v1.0/images-are-certified + type: Mandatory + outcome: PASS + reason: |- + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi + - check: v1.0/is-helm-v3 + type: Mandatory + outcome: PASS + reason: API version is V2, used in Helm 3 + - check: v1.0/not-contain-csi-objects + type: Mandatory + outcome: PASS + reason: CSI objects do not exist + - check: v1.0/contains-values-schema + type: Mandatory + outcome: PASS + reason: Values schema file exist + diff --git a/tests/data/HC-06/partner/report.yaml b/tests/data/HC-06/partner/report.yaml new file mode 100644 index 0000000000..cf301528a9 --- /dev/null +++ b/tests/data/HC-06/partner/report.yaml @@ -0,0 +1,100 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.9.0 + profile: + VendorType: partner + version: v1.1 + reportDigest: uint64:14723461504266211875 + chart-uri: N/A + digests: + chart: sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a + package: 9cb7e3a5d82537f512319c754ccd6e9a4be08118b34849609d0fc261934d062a + lastCertifiedTimestamp: "2022-10-09T19:33:32.953809+05:30" + testedOpenShiftVersion: "4.11" + supportedOpenShiftVersions: '>=4.2' + providerControlledDelivery: true + chart: + name: vault + home: https://www.vaultproject.io + sources: + - https://github.com/hashicorp/vault + - https://github.com/hashicorp/vault-helm + - https://github.com/hashicorp/vault-k8s + - https://github.com/hashicorp/vault-csi-provider + version: 0.17.0 + description: Official HashiCorp Vault Chart + keywords: + - vault + - security + - encryption + - secrets + - management + - automation + - infrastructure + maintainers: [] + icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png + apiversion: v2 + condition: "" + tags: "" + appversion: 1.8.4 + deprecated: false + annotations: + charts.openshift.io/name: vault + kubeversion: '>= 1.14.0-0' + dependencies: [] + type: "" + chart-overrides: "" +results: + - check: v1.0/helm-lint + type: Mandatory + outcome: PASS + reason: Helm lint successful + - check: v1.0/contains-test + type: Mandatory + outcome: PASS + reason: Chart test files exist + - check: v1.0/images-are-certified + type: Mandatory + outcome: PASS + reason: |- + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi + - check: v1.0/not-contains-crds + type: Mandatory + outcome: PASS + reason: Chart does not contain CRDs + - check: v1.0/required-annotations-present + type: Mandatory + outcome: PASS + reason: All required annotations present + - check: v1.1/has-kubeversion + type: Mandatory + outcome: PASS + reason: Kubernetes version specified + - check: v1.0/is-helm-v3 + type: Mandatory + outcome: PASS + reason: API version is V2, used in Helm 3 + - check: v1.0/has-readme + type: Mandatory + outcome: PASS + reason: Chart has a README + - 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/contains-values-schema + type: Mandatory + outcome: PASS + reason: Values schema file exist + - check: v1.0/contains-values + type: Mandatory + outcome: PASS + reason: Values file exist + diff --git a/tests/data/HC-09/community/report.json b/tests/data/HC-09/community/report.json new file mode 100644 index 0000000000..04c6ff94c5 --- /dev/null +++ b/tests/data/HC-09/community/report.json @@ -0,0 +1,126 @@ +{ + "apiversion": "v1", + "kind": "verify-report", + "metadata": { + "tool": { + "verifier-version": "1.9.0", + "profile": { + "vendorType": "community", + "version": "v1.1" + }, + "reportDigest": "uint64:589776597986644110", + "chart-uri": "https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true", + "digests": { + "chart": "sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a", + "package": "9cb7e3a5d82537f512319c754ccd6e9a4be08118b34849609d0fc261934d062a" + }, + "lastCertifiedTimestamp": "2022-10-10T12:53:48.691795+05:30", + "testedOpenShiftVersion": "4.11", + "supportedOpenShiftVersions": ">=4.2", + "providerControlledDelivery": false + }, + "chart": { + "name": "vault", + "home": "https://www.vaultproject.io", + "sources": [ + "https://github.com/hashicorp/vault", + "https://github.com/hashicorp/vault-helm", + "https://github.com/hashicorp/vault-k8s", + "https://github.com/hashicorp/vault-csi-provider" + ], + "version": "0.17.0", + "description": "Official HashiCorp Vault Chart", + "keywords": [ + "vault", + "security", + "encryption", + "secrets", + "management", + "automation", + "infrastructure" + ], + "icon": "https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png", + "apiVersion": "v2", + "appVersion": "1.8.4", + "annotations": { + "charts.openshift.io/name": "vault" + }, + "kubeVersion": ">= 1.14.0-0" + }, + "chart-overrides": "" + }, + "results": [ + { + "check": "v1.0/not-contain-csi-objects", + "type": "Optional", + "outcome": "PASS", + "reason": "CSI objects do not exist" + }, + { + "check": "v1.0/required-annotations-present", + "type": "Optional", + "outcome": "PASS", + "reason": "All required annotations present" + }, + { + "check": "v1.0/contains-test", + "type": "Optional", + "outcome": "PASS", + "reason": "Chart test files exist" + }, + { + "check": "v1.0/contains-values", + "type": "Optional", + "outcome": "PASS", + "reason": "Values file exist" + }, + { + "check": "v1.0/not-contains-crds", + "type": "Optional", + "outcome": "PASS", + "reason": "Chart does not contain CRDs" + }, + { + "check": "v1.0/contains-values-schema", + "type": "Optional", + "outcome": "PASS", + "reason": "Values schema file exist" + }, + { + "check": "v1.0/helm-lint", + "type": "Mandatory", + "outcome": "PASS", + "reason": "Helm lint successful" + }, + { + "check": "v1.0/chart-testing", + "type": "Optional", + "outcome": "PASS", + "reason": "Chart tests have passed" + }, + { + "check": "v1.1/has-kubeversion", + "type": "Optional", + "outcome": "PASS", + "reason": "Kubernetes version specified" + }, + { + "check": "v1.0/has-readme", + "type": "Optional", + "outcome": "PASS", + "reason": "Chart has a README" + }, + { + "check": "v1.0/images-are-certified", + "type": "Optional", + "outcome": "PASS", + "reason": "Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi\nImage is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi" + }, + { + "check": "v1.0/is-helm-v3", + "type": "Optional", + "outcome": "PASS", + "reason": "API version is V2, used in Helm 3" + } + ] +} diff --git a/tests/data/report.json b/tests/data/HC-09/partner/report.json similarity index 83% rename from tests/data/report.json rename to tests/data/HC-09/partner/report.json index 24c812fb67..e70608ab62 100644 --- a/tests/data/report.json +++ b/tests/data/HC-09/partner/report.json @@ -3,20 +3,21 @@ "kind": "verify-report", "metadata": { "tool": { - "verifier-version": "1.4.0", + "verifier-version": "1.9.0", "profile": { - "VendorType": "partner", + "vendorType": "partner", "version": "v1.1" }, - "chart-uri": "https://github.com/${repository}/raw/${branch}/tests/data/vault-0.17.0.tgz", + "reportDigest": "uint64:7581213721422938469", + "chart-uri": "https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true", "digests": { "chart": "sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a", - "package" : "9cb7e3a5d82537f512319c754ccd6e9a4be08118b34849609d0fc261934d062a" + "package": "9cb7e3a5d82537f512319c754ccd6e9a4be08118b34849609d0fc261934d062a" }, - "lastCertifiedTimestamp": "2021-11-24T11:42:48.736244+05:30", - "testedOpenShiftVersion": "4.9", + "lastCertifiedTimestamp": "2022-10-10T12:45:13.619288+05:30", + "testedOpenShiftVersion": "4.11", "supportedOpenShiftVersions": ">=4.2", - "providerControlledDelivery": "false" + "providerControlledDelivery": false }, "chart": { "name": "vault", @@ -38,94 +39,88 @@ "automation", "infrastructure" ], - "maintainers": [], "icon": "https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png", - "apiversion": "v2", - "condition": "", - "tags": "", - "appversion": "1.8.4", - "deprecated": false, + "apiVersion": "v2", + "appVersion": "1.8.4", "annotations": { "charts.openshift.io/name": "vault" }, - "kubeversion": ">= 1.14.0-0", - "dependencies": [], - "type": "" + "kubeVersion": ">= 1.14.0-0" }, "chart-overrides": "" }, "results": [ { - "check": "v1.0/contains-values-schema", + "check": "v1.0/has-readme", "type": "Mandatory", "outcome": "PASS", - "reason": "Values schema file exist" + "reason": "Chart has a README" }, { - "check": "v1.0/helm-lint", + "check": "v1.0/images-are-certified", "type": "Mandatory", "outcome": "PASS", - "reason": "Helm lint successful" + "reason": "Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi\nImage is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi" }, { - "check": "v1.0/images-are-certified", + "check": "v1.0/contains-values", "type": "Mandatory", "outcome": "PASS", - "reason": "Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi\nImage is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi" + "reason": "Values file exist" }, { - "check": "v1.0/chart-testing", + "check": "v1.0/not-contain-csi-objects", "type": "Mandatory", "outcome": "PASS", - "reason": "Chart tests have passed" + "reason": "CSI objects do not exist" }, { - "check": "v1.0/has-readme", + "check": "v1.0/not-contains-crds", "type": "Mandatory", "outcome": "PASS", - "reason": "Chart has a README" + "reason": "Chart does not contain CRDs" }, { - "check": "v1.0/is-helm-v3", + "check": "v1.0/chart-testing", "type": "Mandatory", "outcome": "PASS", - "reason": "API version is V2, used in Helm 3" + "reason": "Chart tests have passed" }, { - "check": "v1.0/contains-values", + "check": "v1.0/contains-test", "type": "Mandatory", "outcome": "PASS", - "reason": "Values file exist" + "reason": "Chart test files exist" }, { - "check": "v1.0/not-contain-csi-objects", + "check": "v1.1/has-kubeversion", "type": "Mandatory", "outcome": "PASS", - "reason": "CSI objects do not exist" + "reason": "Kubernetes version specified" }, { - "check": "v1.0/required-annotations-present", + "check": "v1.0/contains-values-schema", "type": "Mandatory", "outcome": "PASS", - "reason": "All required annotations present" + "reason": "Values schema file exist" }, { - "check": "v1.0/contains-test", + "check": "v1.0/required-annotations-present", "type": "Mandatory", "outcome": "PASS", - "reason": "Chart test files exist" + "reason": "All required annotations present" }, { - "check": "v1.1/has-kubeversion", + "check": "v1.0/helm-lint", "type": "Mandatory", "outcome": "PASS", - "reason": "Kubernetes version specified" + "reason": "Helm lint successful" }, { - "check": "v1.0/not-contains-crds", + "check": "v1.0/is-helm-v3", "type": "Mandatory", "outcome": "PASS", - "reason": "Chart does not contain CRDs" + "reason": "API version is V2, used in Helm 3" } ] } diff --git a/tests/data/HC-09/redhat/report.json b/tests/data/HC-09/redhat/report.json new file mode 100644 index 0000000000..eccd4427fa --- /dev/null +++ b/tests/data/HC-09/redhat/report.json @@ -0,0 +1,126 @@ +{ + "apiversion": "v1", + "kind": "verify-report", + "metadata": { + "tool": { + "verifier-version": "1.9.0", + "profile": { + "vendorType": "redhat", + "version": "v1.1" + }, + "reportDigest": "uint64:2143053838337354791", + "chart-uri": "https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true", + "digests": { + "chart": "sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a", + "package": "9cb7e3a5d82537f512319c754ccd6e9a4be08118b34849609d0fc261934d062a" + }, + "lastCertifiedTimestamp": "2022-10-10T12:50:52.638518+05:30", + "testedOpenShiftVersion": "4.11", + "supportedOpenShiftVersions": ">=4.2", + "providerControlledDelivery": false + }, + "chart": { + "name": "vault", + "home": "https://www.vaultproject.io", + "sources": [ + "https://github.com/hashicorp/vault", + "https://github.com/hashicorp/vault-helm", + "https://github.com/hashicorp/vault-k8s", + "https://github.com/hashicorp/vault-csi-provider" + ], + "version": "0.17.0", + "description": "Official HashiCorp Vault Chart", + "keywords": [ + "vault", + "security", + "encryption", + "secrets", + "management", + "automation", + "infrastructure" + ], + "icon": "https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png", + "apiVersion": "v2", + "appVersion": "1.8.4", + "annotations": { + "charts.openshift.io/name": "vault" + }, + "kubeVersion": ">= 1.14.0-0" + }, + "chart-overrides": "" + }, + "results": [ + { + "check": "v1.0/contains-test", + "type": "Mandatory", + "outcome": "PASS", + "reason": "Chart test files exist" + }, + { + "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/required-annotations-present", + "type": "Mandatory", + "outcome": "PASS", + "reason": "All required annotations present" + }, + { + "check": "v1.1/has-kubeversion", + "type": "Mandatory", + "outcome": "PASS", + "reason": "Kubernetes version specified" + }, + { + "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/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.0/images-are-certified", + "type": "Mandatory", + "outcome": "PASS", + "reason": "Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi\nImage is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi" + }, + { + "check": "v1.0/not-contains-crds", + "type": "Mandatory", + "outcome": "PASS", + "reason": "Chart does not contain CRDs" + }, + { + "check": "v1.0/contains-values", + "type": "Mandatory", + "outcome": "PASS", + "reason": "Values file exist" + } + ] +} diff --git a/tests/data/HC-10/signed_chart/public_key_bad.asc b/tests/data/HC-10/signed_chart/public_key_bad.asc new file mode 100644 index 0000000000..f3805ac77b --- /dev/null +++ b/tests/data/HC-10/signed_chart/public_key_bad.asc @@ -0,0 +1,29 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFngl4UBCADDnQPW5RoaE4JbCiLnQcsC4QmFtuJkCNtsbgpunhXuU/I12WNy +IWYyT60YnXa8mNhu3D9zmypUaaDbfIyoNV7ITYHo1CIS73fGDd+Mb4TuAqc7uRv4 +YcwNVaZ/tZx875GBgscxix9m25mSwVSXkM6VQYZdhkMC9h7PPMBL9bmCV7OcHkz3 +MwJ6ZSGGXtAKWSU7nPdcRPgHpMeB0kVvJQtlCLVr6uWuX6Y0FBoSdfukc4mGGebF +fq3NjXQhTRwcoDtaYD2z+07JwsWFVHNejZ2O38A4zunuym8ZXUx9MthHY3FJ07vJ +A6V0FVwPmhxg8VvQcvCKqMpXzhfA2GJNOWJvABEBAAG0IFBhdmVsIE1hY8OtayA8 +cG1hY2lrQHJlZGhhdC5jb20+iQE3BBMBCAAhBQJZ4JeFAhsDBQsJCAcCBhUICQoL +AgQWAgMBAh4BAheAAAoJEJxz0J5nr4YidnoH/1p624eLXwfZ+Hzd6h77LPHxdYKU +88ISyt4colhc15ukLGzl2jP4MW9rP9DP3/NpZir6lArkr2E1M4olv1aJgKOrv2zq +J/Vkw0tp5Ecl1u+Ty3Zl9ZCo/GcxXDXFQkQxIyHfPS6EHKrEscz1tHvLVwNswO5I ++qFa+UDham5ZL36gkyVSJfNd7s2ZsK+YIvv64yJZG4P20M2Gv2iR0NIIe/osrrE4 +MoE2rlsROOqXEp8RQPMeFCEKomZeLWZCRA3dp7JQJqjAlgYP5PGaxrgJ5MDBKrts +4iMR/AC9oSEIk5eagUhScEsW4wbe0czL3mEFUflDM8Gc6E5HCSAwtLmqV6y5AQ0E +WeCXhQEIAL2VtQ4W455uGy4+Kt/z3sL5oV1VJDJoPmQ9IiSAHd/qy/+w7HDcSHp1 +TkNRmLpb6lZt9cDAmmg59qwutuAX4IydKN2iF5n+SEo15nme582/oTrkLysH+Zdn +b6L7BlVKtANEhAE6j2ZAzljvIBMKb3wiVS+VYDS/UiO2cSnunL4IpcecXFRvOevU +w4LoINWfyHSA4RtoozgiGC3HSbc2b/wGVXvGLN0EAlIjx/vy5KN9AhcaaEyAYaAe +lbB1Etzyvb6Dy3hjjLNCIOtILqkxKzFF0Kskh1FEOljPCc5gkE8Og3272LVu0lRr +F3hODs0pPUSi3aCNWaiGu81sUzZViJ0AEQEAAYkBHwQYAQgACQUCWeCXhQIbDAAK +CRCcc9CeZ6+GIhjwB/9uWw2fg7SENHgSYZkw4WT7Bi/nX3QN83DskA6VCKc9m7ls +0YKxTHrHfY+KQxRy/sI82Q0aX0oYEzh2UrHo3N6u5jw69htwbi1ug+JcTenhMgfX +tmLZ5rhvXLXjIxYOmhnQn4XBMoz+z0RcCphQ9AoSQnArQHJcz8YdxDr0Xl8et6OK +0B41do58S4dyh9NiseV794XnIbk6wyMuhdEl99+DR7mUGOEKN8DLUriOBFT6TMwx +dNMCkd1mpleqg/ed4KeMO4nOV18jmzc8TxC4H/nEUCqKFOg7JEq77GkcN/BVN9jF +qq+KihliSFpvLIPjlaBgpu4Yj+sl1s95i4XyIod8 +=bo+t +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/data/HC-10/signed_chart/public_key_good.asc b/tests/data/HC-10/signed_chart/public_key_good.asc new file mode 100644 index 0000000000..61109d34ef --- /dev/null +++ b/tests/data/HC-10/signed_chart/public_key_good.asc @@ -0,0 +1,31 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBGNx5MIBCADmJeRcX8NzOKfugtJslI65oZYm3ZEXA4vEkwud+U73u3oi+FaI +/OzFPdmuN85g4u5V9Q2Y2Sw4ZacoeKVOi6do79ms/zNN9B0zB6wlwLoiSIYu01Rn +wcPrzKqZeLufV33pXDLhlTjmrtGV8R/UjUFGnImJrRf9rhkYE4QN509Y4aBe6ONm +eb0GrcjrTk42OZWOsesvE+M825b26MAJCwDM7rN4FchchTFE1zsMu6fX4cW5GR/A +2+yKUZgJYUNB4E5ByNjCN6pOYvedkNsh7qy5P+LzKSORDfYdxmTR8l0m5H7BCTvz +Ui7FbVRBjynjfKxgsSVpWaLbQIWXBNbGaB3HABEBAAG0QVN1c2hhbnRhIERhcyAo +S2V5IGdlbmVyYXRlZCBpbiBsb2FuZXIgbGFwdG9wKSA8c3VzZGFzQHJlZGhhdC5j +b20+iQFOBBMBCAA4FiEEFJPdcWnqgH2kX1N7MeOXrtTfLKAFAmNx5MICGwMFCwkI +BwIGFQoJCAsCBBYCAwECHgECF4AACgkQMeOXrtTfLKB9eAgAw5m/qGJ4fnA11aVf +kPHsB7QiMa2NYaCYQj0y0XISby9knxfrZYm83hoZijjaAe8N899N2/o+9TvMPKFr +7or45tiugJNF5EmiwscrpIdNIaRNHFKXnlv7lZBWfUnftt29hQfal5GBBskTVSBo +OrrzEQ86mAyC9Pi2L6uvbFyJwfS/tIMiOABAA1SR06Itx4zcD348F4HqFbKBgs6y +pMYDnbdUB/3aDOojsYQfByOnyo5nCOVinIOWAHzyA92Qy/OR+tMKDPq0xswPu2b/ +rpsMzMwL6JFjvcOr2mq4+dDT8kdYJUwouJdNF5L95jwPmkWUWsDm3shU/bVHz7ks +Ca+7JLkBDQRjceTCAQgA1UQ3YtMUyjyyjWYhSqMSIogzD+/4tF/i90EadNbgQ4aG +sSsydVm3rfo0Q38bjsqweLBZ4c5xxfkuqrusaWM6yZ1gWEWS+ww67x9KUplzqQWs +KDI2JuB6ad6JyK+YsKlTnk9RkHA0xLdTHniAcN9Z6nzQqppGUq/Ib2llXlENVnqs +lqLSbXW+AxwP/rTh/EsKHRvibKI57geyrwF/XrPdCcuFSpVs7MeRLurokQQfXJrC +TVwAdq1MwBnthkskZRNLb0VCyHRKwNOQpHHT0WmA2fP90jHLpYGbB3zeuUuSgeEw +lGbeO5ijC6KHc0RuK7FCu6Ur52s+naIzF0f5J/lDzwARAQABiQE2BBgBCAAgFiEE +FJPdcWnqgH2kX1N7MeOXrtTfLKAFAmNx5MICGwwACgkQMeOXrtTfLKAzzQgApG5f +y3t1weif2CmAh9oXvo9mkQFhBV5iB7NHTGq6tBzqj9vrKK/5D1TOebAvvYEefYAR +Xr1zcU8avOVjaYCumvf+oibjOtYe0R7L6v4+koKUlzVmOtPxuikMoHy1HvIlVZ3S +OZLxImvPheM3xQLX8eipF2n0csxIkRX+JHR1QZgWGskUV9u7HhtG0K6HqGW9IFEp +pU3bQftsTFps99QXZ/ZGDBI3F+z9TLz4WQ86Kzhv96a2ImHgwVKFERhUdDG3rk8D +xyeJw2STGRbm+ZqxXJY41XMV1h3aQWbA1R3rP6RMaCmtytP3K/MzW0Z8ZNjFG0Zg +2BOiqlHu5xKCP8D8qA== +=0AwU +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/data/HC-10/signed_chart/report/partner/report.yaml b/tests/data/HC-10/signed_chart/report/partner/report.yaml new file mode 100644 index 0000000000..3e91a22764 --- /dev/null +++ b/tests/data/HC-10/signed_chart/report/partner/report.yaml @@ -0,0 +1,105 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.9.0 + profile: + VendorType: partner + version: v1.2 + reportDigest: uint64:16989833708667280020 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/HC-10/signed_chart/vault-0.17.0.tgz?raw=true + digests: + chart: sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a + package: df206272be1282a05af0576a054c9b35a8d2cebb836dc990d8b87dced82dcdb9 + publicKey: 662ba24b23e80f0b26a634f3c215e74d973943e32d0ee85868759365a5640995 + lastCertifiedTimestamp: "2022-11-22T11:49:38.322793+05:30" + testedOpenShiftVersion: "4.11" + supportedOpenShiftVersions: '>=4.2' + providerControlledDelivery: false + chart: + name: vault + home: https://www.vaultproject.io + sources: + - https://github.com/hashicorp/vault + - https://github.com/hashicorp/vault-helm + - https://github.com/hashicorp/vault-k8s + - https://github.com/hashicorp/vault-csi-provider + version: 0.17.0 + description: Official HashiCorp Vault Chart + keywords: + - vault + - security + - encryption + - secrets + - management + - automation + - infrastructure + maintainers: [] + icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png + apiversion: v2 + condition: "" + tags: "" + appversion: 1.8.4 + deprecated: false + annotations: + charts.openshift.io/name: vault + kubeversion: '>= 1.14.0-0' + dependencies: [] + type: "" + chart-overrides: "" +results: + - 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.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi + - check: v1.0/is-helm-v3 + type: Mandatory + outcome: PASS + reason: API version is V2, used in Helm 3 + - check: v1.0/chart-testing + type: Mandatory + outcome: PASS + reason: Chart tests have passed + - check: v1.0/not-contains-crds + type: Mandatory + outcome: PASS + reason: Chart does not contain CRDs + - check: v1.0/has-readme + type: Mandatory + outcome: PASS + reason: Chart has a README + - 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/signature-is-valid + type: Mandatory + outcome: PASS + reason: 'Chart is signed : Signature verification passed' + - 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 + diff --git a/tests/data/HC-10/signed_chart/report/redhat/report.yaml b/tests/data/HC-10/signed_chart/report/redhat/report.yaml new file mode 100644 index 0000000000..530095ccde --- /dev/null +++ b/tests/data/HC-10/signed_chart/report/redhat/report.yaml @@ -0,0 +1,105 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.9.0 + profile: + VendorType: redhat + version: v1.2 + reportDigest: uint64:16923642309854382860 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/HC-10/signed_chart/vault-0.17.0.tgz?raw=true + digests: + chart: sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a + package: df206272be1282a05af0576a054c9b35a8d2cebb836dc990d8b87dced82dcdb9 + publicKey: 662ba24b23e80f0b26a634f3c215e74d973943e32d0ee85868759365a5640995 + lastCertifiedTimestamp: "2022-11-22T11:51:58.909069+05:30" + testedOpenShiftVersion: "4.11" + supportedOpenShiftVersions: '>=4.2' + providerControlledDelivery: false + chart: + name: vault + home: https://www.vaultproject.io + sources: + - https://github.com/hashicorp/vault + - https://github.com/hashicorp/vault-helm + - https://github.com/hashicorp/vault-k8s + - https://github.com/hashicorp/vault-csi-provider + version: 0.17.0 + description: Official HashiCorp Vault Chart + keywords: + - vault + - security + - encryption + - secrets + - management + - automation + - infrastructure + maintainers: [] + icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png + apiversion: v2 + condition: "" + tags: "" + appversion: 1.8.4 + deprecated: false + annotations: + charts.openshift.io/name: vault + kubeversion: '>= 1.14.0-0' + dependencies: [] + type: "" + chart-overrides: "" +results: + - check: v1.0/contains-values-schema + type: Mandatory + outcome: PASS + reason: Values schema file exist + - check: v1.0/images-are-certified + type: Mandatory + outcome: PASS + reason: |- + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi + - check: v1.0/required-annotations-present + type: Mandatory + outcome: PASS + reason: All required annotations present + - check: v1.0/is-helm-v3 + type: Mandatory + outcome: PASS + reason: API version is V2, used in Helm 3 + - check: v1.0/chart-testing + type: Mandatory + outcome: PASS + reason: Chart tests have passed + - check: v1.0/not-contains-crds + type: Mandatory + outcome: PASS + reason: Chart does not contain CRDs + - check: v1.0/contains-test + type: Mandatory + outcome: PASS + reason: Chart test files exist + - check: v1.1/has-kubeversion + type: Mandatory + outcome: PASS + reason: Kubernetes version specified + - check: v1.0/signature-is-valid + type: Mandatory + outcome: PASS + reason: 'Chart is signed : Signature verification passed' + - check: v1.0/contains-values + type: Mandatory + outcome: PASS + reason: Values file exist + - check: v1.0/has-readme + type: Mandatory + outcome: PASS + reason: Chart has a README + - check: v1.0/helm-lint + type: Mandatory + outcome: PASS + reason: Helm lint successful + - check: v1.0/not-contain-csi-objects + type: Mandatory + outcome: PASS + reason: CSI objects do not exist + diff --git a/tests/data/HC-10/signed_chart/vault-0.17.0.tgz b/tests/data/HC-10/signed_chart/vault-0.17.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..240887d24f5351e54bd740843a115c56893579d1 GIT binary patch literal 37237 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 Z>(8%0{}2EC-v9sr|NlN>50L<70|045tndH; literal 0 HcmV?d00001 diff --git a/tests/data/HC-10/signed_chart/vault-0.17.0.tgz.prov b/tests/data/HC-10/signed_chart/vault-0.17.0.tgz.prov new file mode 100644 index 0000000000..4d5586e639 --- /dev/null +++ b/tests/data/HC-10/signed_chart/vault-0.17.0.tgz.prov @@ -0,0 +1,40 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +annotations: + charts.openshift.io/name: vault +apiVersion: v2 +appVersion: 1.8.4 +description: Official HashiCorp Vault Chart +home: https://www.vaultproject.io +icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png +keywords: +- - vault +- - security +- - encryption +- - secrets +- - management +- - automation +- - infrastructure +kubeVersion: '>= 1.14.0-0' +name: vault +sources: +- - https://github.com/hashicorp/vault +- - https://github.com/hashicorp/vault-helm +- - https://github.com/hashicorp/vault-k8s +- - https://github.com/hashicorp/vault-csi-provider +version: 0.17.0 + +... +files: + vault-0.17.0.tgz: sha256:df206272be1282a05af0576a054c9b35a8d2cebb836dc990d8b87dced82dcdb9 +-----BEGIN PGP SIGNATURE----- + +wsBcBAEBCgAQBQJjdgpMCRAx45eu1N8soAAACCwIAE3wbtdvhBpTx/RmNDej9RlP +lSgG6qvvXpC2GOJRyJbqQQymQ9uU5IDg5GNWGpurH02v+HQL2lyHKLvfZHkzCzS2 +WcDWt1pmAOLAWUngwlG1HvTtSg23lheKPBiYuLSJRG5cS+zAczpI9eFmWs1KCBTN ++dE/dfWb8h4O8vRgrpqDK3Ml89cZbv7Axz0esx4f5vHRNEmdsSam30V5SgOf8zfm +J6vkJV4l2QJOqlIQd4GAb3YDveoALyBGhEN/IAGtg/nCaRAbpR5AeXtOPqRTPAEd +hOHdkpiYUGok043leZn9ZiPzPH2VFTqDdg6A6J/vbZBaBpqu/fjoE5gxhsvtnP0= +=lTB4 +-----END PGP SIGNATURE----- \ No newline at end of file diff --git a/tests/data/HC-11/community/report.yaml b/tests/data/HC-11/community/report.yaml new file mode 100644 index 0000000000..4af552799a --- /dev/null +++ b/tests/data/HC-11/community/report.yaml @@ -0,0 +1,96 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.9.0 + profile: + VendorType: community + version: v1.1 + reportDigest: uint64:6188835510583069887 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true + digests: + chart: sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a + package: 9cb7e3a5d82537f512319c754ccd6e9a4be08118b34849609d0fc261934d062a + lastCertifiedTimestamp: "2022-10-10T15:07:06.610015+05:30" + testedOpenShiftVersion: "4.11" + supportedOpenShiftVersions: '>=4.2' + providerControlledDelivery: false + chart: + name: vault + home: https://www.vaultproject.io + sources: + - https://github.com/hashicorp/vault + - https://github.com/hashicorp/vault-helm + - https://github.com/hashicorp/vault-k8s + - https://github.com/hashicorp/vault-csi-provider + version: 0.17.0 + description: Official HashiCorp Vault Chart + keywords: + - vault + - security + - encryption + - secrets + - management + - automation + - infrastructure + maintainers: [] + icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png + apiversion: v2 + condition: "" + tags: "" + appversion: 1.8.4 + deprecated: false + annotations: + charts.openshift.io/name: vault + kubeversion: '>= 1.14.0-0' + dependencies: [] + type: "" + chart-overrides: "" +results: + - check: v1.0/is-helm-v3 + type: Optional + outcome: PASS + reason: API version is V2, used in Helm 3 + - check: v1.0/not-contains-crds + type: Optional + outcome: PASS + reason: Chart does not contain CRDs + - check: v1.0/contains-test + type: Optional + outcome: PASS + reason: Chart test files exist + - check: v1.0/not-contain-csi-objects + type: Optional + outcome: PASS + reason: CSI objects do not exist + - check: v1.1/has-kubeversion + type: Optional + outcome: PASS + reason: Kubernetes version specified + - check: v1.0/required-annotations-present + type: Optional + outcome: PASS + reason: All required annotations present + - check: v1.0/contains-values-schema + type: Optional + outcome: PASS + reason: Values schema file exist + - check: v1.0/contains-values + type: Optional + outcome: PASS + reason: Values file exist + - check: v1.0/images-are-certified + type: Optional + outcome: PASS + reason: |- + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi + - check: v1.0/chart-testing + type: Optional + outcome: PASS + reason: Chart tests have passed + - check: v1.0/has-readme + type: Optional + outcome: PASS + reason: Chart has a README + diff --git a/tests/data/HC-11/partner/report.yaml b/tests/data/HC-11/partner/report.yaml new file mode 100644 index 0000000000..1d19de4f0f --- /dev/null +++ b/tests/data/HC-11/partner/report.yaml @@ -0,0 +1,96 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.9.0 + profile: + VendorType: partner + version: v1.1 + reportDigest: uint64:7728592976812301878 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true + digests: + chart: sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a + package: 9cb7e3a5d82537f512319c754ccd6e9a4be08118b34849609d0fc261934d062a + lastCertifiedTimestamp: "2022-10-10T14:51:53.556914+05:30" + testedOpenShiftVersion: "4.11" + supportedOpenShiftVersions: '>=4.2' + providerControlledDelivery: false + chart: + name: vault + home: https://www.vaultproject.io + sources: + - https://github.com/hashicorp/vault + - https://github.com/hashicorp/vault-helm + - https://github.com/hashicorp/vault-k8s + - https://github.com/hashicorp/vault-csi-provider + version: 0.17.0 + description: Official HashiCorp Vault Chart + keywords: + - vault + - security + - encryption + - secrets + - management + - automation + - infrastructure + maintainers: [] + icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png + apiversion: v2 + condition: "" + tags: "" + appversion: 1.8.4 + deprecated: false + annotations: + charts.openshift.io/name: vault + kubeversion: '>= 1.14.0-0' + dependencies: [] + type: "" + chart-overrides: "" +results: + - check: v1.0/chart-testing + type: Mandatory + outcome: PASS + reason: Chart tests have passed + - 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/has-readme + type: Mandatory + outcome: PASS + reason: Chart has a README + - check: v1.0/not-contain-csi-objects + type: Mandatory + outcome: PASS + reason: CSI objects do not exist + - check: v1.0/contains-values-schema + type: Mandatory + outcome: PASS + reason: Values schema file exist + - check: v1.0/images-are-certified + type: Mandatory + outcome: PASS + reason: |- + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi + - 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/not-contains-crds + type: Mandatory + outcome: PASS + reason: Chart does not contain CRDs + diff --git a/tests/data/HC-11/partner_not_contain_crds/report.yaml b/tests/data/HC-11/partner_not_contain_crds/report.yaml new file mode 100644 index 0000000000..bd1488751c --- /dev/null +++ b/tests/data/HC-11/partner_not_contain_crds/report.yaml @@ -0,0 +1,96 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.9.0 + profile: + VendorType: partner + version: v1.1 + reportDigest: uint64:11593295899204523336 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true + digests: + chart: sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a + package: 9cb7e3a5d82537f512319c754ccd6e9a4be08118b34849609d0fc261934d062a + lastCertifiedTimestamp: "2022-10-10T15:02:15.476386+05:30" + testedOpenShiftVersion: "4.11" + supportedOpenShiftVersions: '>=4.2' + providerControlledDelivery: false + chart: + name: vault + home: https://www.vaultproject.io + sources: + - https://github.com/hashicorp/vault + - https://github.com/hashicorp/vault-helm + - https://github.com/hashicorp/vault-k8s + - https://github.com/hashicorp/vault-csi-provider + version: 0.17.0 + description: Official HashiCorp Vault Chart + keywords: + - vault + - security + - encryption + - secrets + - management + - automation + - infrastructure + maintainers: [] + icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png + apiversion: v2 + condition: "" + tags: "" + appversion: 1.8.4 + deprecated: false + annotations: + charts.openshift.io/name: vault + kubeversion: '>= 1.14.0-0' + dependencies: [] + type: "" + chart-overrides: "" +results: + - check: v1.0/chart-testing + type: Mandatory + outcome: PASS + reason: Chart tests have passed + - 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.connect.redhat.com/hashicorp/vault:1.8.4-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi + - check: v1.0/required-annotations-present + type: Mandatory + outcome: PASS + reason: All required annotations present + - check: v1.0/not-contain-csi-objects + type: Mandatory + outcome: PASS + reason: CSI objects do not exist + - 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/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/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 + diff --git a/tests/data/HC-17/dash-in-version/partner/report.yaml b/tests/data/HC-17/dash-in-version/partner/report.yaml index b4aec81305..af331752cb 100644 --- a/tests/data/HC-17/dash-in-version/partner/report.yaml +++ b/tests/data/HC-17/dash-in-version/partner/report.yaml @@ -2,16 +2,17 @@ apiversion: v1 kind: verify-report metadata: tool: - verifier-version: 1.7.0 + verifier-version: 1.9.0 profile: VendorType: partner version: v1.1 + reportDigest: uint64:12593951720348144952 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" + lastCertifiedTimestamp: "2022-10-11T12:51:01.384305+05:30" + testedOpenShiftVersion: "4.11" supportedOpenShiftVersions: '>=4.7' providerControlledDelivery: false chart: @@ -38,52 +39,52 @@ metadata: 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 + - check: v1.0/has-readme type: Mandatory outcome: PASS - reason: Kubernetes version specified - - check: v1.0/not-contains-crds + reason: Chart has a README + - check: v1.0/images-are-certified type: Mandatory outcome: PASS - reason: Chart does not contain CRDs - - check: v1.0/not-contain-csi-objects + 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: CSI objects do not exist + reason: All required annotations present - check: v1.0/chart-testing type: Mandatory outcome: PASS reason: Chart tests have passed - - check: v1.0/has-readme + - check: v1.1/has-kubeversion type: Mandatory outcome: PASS - reason: Chart has a README + reason: Kubernetes version specified + - 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/contains-test + type: Mandatory + outcome: PASS + reason: Chart test files exist - check: v1.0/contains-values-schema type: Mandatory outcome: PASS reason: Values schema file exist - - check: v1.0/helm-lint + - check: v1.0/contains-values type: Mandatory outcome: PASS - reason: Helm lint successful - - check: v1.0/images-are-certified + reason: Values file exist + - check: v1.0/not-contain-csi-objects 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 + reason: CSI objects do not exist + - check: v1.0/not-contains-crds type: Mandatory outcome: PASS - reason: All required annotations present + reason: Chart does not contain CRDs diff --git a/tests/data/HC-17/dash-in-version/redhat/report.yaml b/tests/data/HC-17/dash-in-version/redhat/report.yaml index eff7b99361..04af94a054 100644 --- a/tests/data/HC-17/dash-in-version/redhat/report.yaml +++ b/tests/data/HC-17/dash-in-version/redhat/report.yaml @@ -2,16 +2,17 @@ apiversion: v1 kind: verify-report metadata: tool: - verifier-version: 1.7.0 + verifier-version: 1.9.0 profile: VendorType: redhat version: v1.1 + reportDigest: uint64:13966675880356106002 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" + lastCertifiedTimestamp: "2022-10-11T12:56:10.138339+05:30" + testedOpenShiftVersion: "4.11" supportedOpenShiftVersions: '>=4.7' providerControlledDelivery: false chart: @@ -38,52 +39,52 @@ metadata: type: application chart-overrides: "" results: - - check: v1.0/has-readme + - check: v1.0/contains-test type: Mandatory outcome: PASS - reason: Chart has a README - - check: v1.0/is-helm-v3 + reason: Chart test files exist + - check: v1.0/contains-values type: Mandatory outcome: PASS - reason: API version is V2, used in Helm 3 - - check: v1.0/not-contains-crds + reason: Values file exist + - check: v1.1/has-kubeversion type: Mandatory outcome: PASS - reason: Chart does not contain CRDs - - check: v1.0/not-contain-csi-objects + reason: Kubernetes version specified + - check: v1.0/chart-testing type: Mandatory outcome: PASS - reason: CSI objects do not exist - - check: v1.0/images-are-certified + reason: Chart tests have passed + - check: v1.0/contains-values-schema 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 + reason: Values schema file exist + - check: v1.0/helm-lint type: Mandatory outcome: PASS - reason: Chart tests have passed - - check: v1.0/required-annotations-present + reason: Helm lint successful + - check: v1.0/has-readme type: Mandatory outcome: PASS - reason: All required annotations present - - check: v1.0/contains-test + reason: Chart has a README + - check: v1.0/is-helm-v3 type: Mandatory outcome: PASS - reason: Chart test files exist - - check: v1.0/contains-values + reason: API version is V2, used in Helm 3 + - check: v1.0/not-contains-crds type: Mandatory outcome: PASS - reason: Values file exist - - check: v1.0/contains-values-schema + reason: Chart does not contain CRDs + - check: v1.0/images-are-certified type: Mandatory outcome: PASS - reason: Values schema file exist - - check: v1.1/has-kubeversion + reason: 'Image is Red Hat certified : registry.access.redhat.com/rhscl/postgresql-10-rhel7:1-66' + - check: v1.0/not-contain-csi-objects type: Mandatory outcome: PASS - reason: Kubernetes version specified - - check: v1.0/helm-lint + reason: CSI objects do not exist + - check: v1.0/required-annotations-present type: Mandatory outcome: PASS - reason: Helm lint successful + reason: All required annotations present diff --git a/tests/data/HC-18/community/report.yaml b/tests/data/HC-18/community/report.yaml new file mode 100644 index 0000000000..548b1777e4 --- /dev/null +++ b/tests/data/HC-18/community/report.yaml @@ -0,0 +1,100 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.9.0 + profile: + VendorType: community + version: v1.1 + reportDigest: uint64:17697596350982566196 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.18.0.tgz?raw=true + digests: + chart: sha256:e976630aa2d8a6aed25284b627b0e9255a942e1aec456657357f9e6e81ec13da + package: 840f81ece382ad64a0e9b6e07b799931c030eb60985c20bab2ea57311b84c08b + lastCertifiedTimestamp: "2022-10-11T19:23:54.029529+05:30" + testedOpenShiftVersion: "4.11" + supportedOpenShiftVersions: '>=4.2' + providerControlledDelivery: false + chart: + name: vault + home: https://www.vaultproject.io + sources: + - https://github.com/hashicorp/vault + - https://github.com/hashicorp/vault-helm + - https://github.com/hashicorp/vault-k8s + - https://github.com/hashicorp/vault-csi-provider + version: 0.18.0 + description: Official HashiCorp Vault Chart + keywords: + - vault + - security + - encryption + - secrets + - management + - automation + - infrastructure + maintainers: [] + icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png + apiversion: v2 + condition: "" + tags: "" + appversion: 1.9.0 + deprecated: false + annotations: + charts.openshift.io/name: vault + kubeversion: '>= 1.14.0-0' + dependencies: [] + type: "" + chart-overrides: "" +results: + - check: v1.0/contains-values-schema + type: Optional + outcome: PASS + reason: Values schema file exist + - check: v1.0/helm-lint + type: Mandatory + outcome: PASS + reason: Helm lint successful + - check: v1.0/is-helm-v3 + type: Optional + outcome: PASS + reason: API version is V2, used in Helm 3 + - check: v1.0/not-contains-crds + type: Optional + outcome: PASS + reason: Chart does not contain CRDs + - check: v1.0/required-annotations-present + type: Optional + outcome: PASS + reason: All required annotations present + - check: v1.0/contains-values + type: Optional + outcome: PASS + reason: Values file exist + - check: v1.1/has-kubeversion + type: Optional + outcome: PASS + reason: Kubernetes version specified + - check: v1.0/chart-testing + type: Optional + outcome: PASS + reason: Chart tests have passed + - check: v1.0/contains-test + type: Optional + outcome: PASS + reason: Chart test files exist + - check: v1.0/has-readme + type: Optional + outcome: PASS + reason: Chart has a README + - check: v1.0/images-are-certified + type: Optional + outcome: PASS + reason: |- + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.1-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.9.0-ubi + - check: v1.0/not-contain-csi-objects + type: Optional + outcome: PASS + reason: CSI objects do not exist + diff --git a/tests/data/HC-18/partner/report.yaml b/tests/data/HC-18/partner/report.yaml new file mode 100644 index 0000000000..15c5dd8807 --- /dev/null +++ b/tests/data/HC-18/partner/report.yaml @@ -0,0 +1,100 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.9.0 + profile: + VendorType: partner + version: v1.1 + reportDigest: uint64:7580685454878718317 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.18.0.tgz?raw=true + digests: + chart: sha256:e976630aa2d8a6aed25284b627b0e9255a942e1aec456657357f9e6e81ec13da + package: 840f81ece382ad64a0e9b6e07b799931c030eb60985c20bab2ea57311b84c08b + lastCertifiedTimestamp: "2022-10-11T19:17:07.929076+05:30" + testedOpenShiftVersion: "4.11" + supportedOpenShiftVersions: '>=4.2' + providerControlledDelivery: false + chart: + name: vault + home: https://www.vaultproject.io + sources: + - https://github.com/hashicorp/vault + - https://github.com/hashicorp/vault-helm + - https://github.com/hashicorp/vault-k8s + - https://github.com/hashicorp/vault-csi-provider + version: 0.18.0 + description: Official HashiCorp Vault Chart + keywords: + - vault + - security + - encryption + - secrets + - management + - automation + - infrastructure + maintainers: [] + icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png + apiversion: v2 + condition: "" + tags: "" + appversion: 1.9.0 + deprecated: false + annotations: + charts.openshift.io/name: vault + kubeversion: '>= 1.14.0-0' + dependencies: [] + type: "" + chart-overrides: "" +results: + - check: v1.0/contains-values-schema + type: Mandatory + outcome: PASS + reason: Values schema file exist + - check: v1.0/contains-values + type: Mandatory + outcome: PASS + reason: Values file exist + - check: v1.0/has-readme + type: Mandatory + outcome: PASS + reason: Chart has a README + - check: v1.0/images-are-certified + type: Mandatory + outcome: PASS + reason: |- + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.1-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.9.0-ubi + - check: v1.0/not-contain-csi-objects + type: Mandatory + outcome: PASS + reason: CSI objects do not exist + - check: v1.0/not-contains-crds + type: Mandatory + outcome: PASS + reason: Chart does not contain CRDs + - 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.1/has-kubeversion + type: Mandatory + outcome: PASS + reason: Kubernetes version specified + - check: v1.0/helm-lint + type: Mandatory + outcome: PASS + reason: Helm lint successful + - check: v1.0/chart-testing + type: Mandatory + outcome: PASS + reason: Chart tests have passed + diff --git a/tests/data/HC-18/redhat/report.yaml b/tests/data/HC-18/redhat/report.yaml new file mode 100644 index 0000000000..906b57e65c --- /dev/null +++ b/tests/data/HC-18/redhat/report.yaml @@ -0,0 +1,100 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.9.0 + profile: + VendorType: redhat + version: v1.1 + reportDigest: uint64:13622060880342329981 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.18.0.tgz?raw=true + digests: + chart: sha256:e976630aa2d8a6aed25284b627b0e9255a942e1aec456657357f9e6e81ec13da + package: 840f81ece382ad64a0e9b6e07b799931c030eb60985c20bab2ea57311b84c08b + lastCertifiedTimestamp: "2022-10-11T19:19:32.612485+05:30" + testedOpenShiftVersion: "4.11" + supportedOpenShiftVersions: '>=4.2' + providerControlledDelivery: false + chart: + name: vault + home: https://www.vaultproject.io + sources: + - https://github.com/hashicorp/vault + - https://github.com/hashicorp/vault-helm + - https://github.com/hashicorp/vault-k8s + - https://github.com/hashicorp/vault-csi-provider + version: 0.18.0 + description: Official HashiCorp Vault Chart + keywords: + - vault + - security + - encryption + - secrets + - management + - automation + - infrastructure + maintainers: [] + icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png + apiversion: v2 + condition: "" + tags: "" + appversion: 1.9.0 + deprecated: false + annotations: + charts.openshift.io/name: vault + kubeversion: '>= 1.14.0-0' + dependencies: [] + type: "" + chart-overrides: "" +results: + - check: v1.0/images-are-certified + type: Mandatory + outcome: PASS + reason: |- + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.1-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.9.0-ubi + - check: v1.0/is-helm-v3 + type: Mandatory + outcome: PASS + reason: API version is V2, used in Helm 3 + - check: v1.0/contains-test + type: Mandatory + outcome: PASS + reason: Chart test files 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/contains-values + type: Mandatory + outcome: PASS + reason: Values file exist + - check: v1.0/has-readme + type: Mandatory + outcome: PASS + reason: Chart has a README + - check: v1.0/required-annotations-present + type: Mandatory + outcome: PASS + reason: All required annotations present + - check: v1.0/helm-lint + type: Mandatory + outcome: PASS + reason: Helm lint successful + - check: v1.0/not-contain-csi-objects + type: Mandatory + outcome: PASS + reason: CSI objects do not exist + - check: v1.0/not-contains-crds + type: Mandatory + outcome: PASS + reason: Chart does not contain CRDs + - check: v1.0/chart-testing + type: Mandatory + outcome: PASS + reason: Chart tests have passed + diff --git a/tests/data/HC-19/report_edited_sha_bad/report.yaml b/tests/data/HC-19/report_edited_sha_bad/report.yaml new file mode 100644 index 0000000000..ac587c41b7 --- /dev/null +++ b/tests/data/HC-19/report_edited_sha_bad/report.yaml @@ -0,0 +1,100 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.9.0 + reportDigest: uint64:17691208852129424495 + profile: + VendorType: partner + version: v1.1 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true + digests: + chart: sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a + package: 9cb7e3a5d82537f512319c754ccd6e9a4be08118b34849609d0fc261934d062a + lastCertifiedTimestamp: "2022-06-13T10:43:25.314602-04:00" + testedOpenShiftVersion: "4.11" + supportedOpenShiftVersions: '>=4.3' + providerControlledDelivery: false + chart: + name: vault + home: https://www.vaultproject.io + sources: + - https://github.com/hashicorp/vault + - https://github.com/hashicorp/vault-helm + - https://github.com/hashicorp/vault-k8s + - https://github.com/hashicorp/vault-csi-provider + version: 0.17.0 + description: Official HashiCorp Vault Chart + keywords: + - vault + - security + - encryption + - secrets + - management + - automation + - infrastructure + maintainers: [] + icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png + apiversion: v2 + condition: "" + tags: "" + appversion: 1.8.4 + deprecated: false + annotations: + charts.openshift.io/name: vault + kubeversion: '>= 1.14.0-0' + dependencies: [] + type: "" + chart-overrides: "" +results: + - check: v1.0/not-contains-crds + type: Mandatory + outcome: PASS + reason: Chart does not contain CRDs + - 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.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi + - 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-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/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 + diff --git a/tests/data/HC-19/report_sha_bad/report.yaml b/tests/data/HC-19/report_sha_bad/report.yaml new file mode 100644 index 0000000000..1bfd60ccef --- /dev/null +++ b/tests/data/HC-19/report_sha_bad/report.yaml @@ -0,0 +1,100 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.9.0 + reportDigest: uint64:934293765184967420 + profile: + VendorType: partner + version: v1.1 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true + digests: + chart: sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a + package: 9cb7e3a5d82537f512319c754ccd6e9a4be08118b34849609d0fc261934d062a + lastCertifiedTimestamp: "2022-06-13T10:43:25.314602-04:00" + testedOpenShiftVersion: "4.10" + supportedOpenShiftVersions: '>=4.2' + providerControlledDelivery: false + chart: + name: vault + home: https://www.vaultproject.io + sources: + - https://github.com/hashicorp/vault + - https://github.com/hashicorp/vault-helm + - https://github.com/hashicorp/vault-k8s + - https://github.com/hashicorp/vault-csi-provider + version: 0.17.0 + description: Official HashiCorp Vault Chart + keywords: + - vault + - security + - encryption + - secrets + - management + - automation + - infrastructure + maintainers: [] + icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png + apiversion: v2 + condition: "" + tags: "" + appversion: 1.8.4 + deprecated: false + annotations: + charts.openshift.io/name: vault + kubeversion: '>= 1.14.0-0' + dependencies: [] + type: "" + chart-overrides: "" +results: + - check: v1.0/not-contains-crds + type: Mandatory + outcome: PASS + reason: Chart does not contain CRDs + - 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.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi + - 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-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/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 + diff --git a/tests/data/HC-19/report_sha_good/report.yaml b/tests/data/HC-19/report_sha_good/report.yaml new file mode 100644 index 0000000000..07d892637e --- /dev/null +++ b/tests/data/HC-19/report_sha_good/report.yaml @@ -0,0 +1,100 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.9.0 + reportDigest: uint64:4706854001499919657 + profile: + VendorType: partner + version: v1.1 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true + digests: + chart: sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a + package: 9cb7e3a5d82537f512319c754ccd6e9a4be08118b34849609d0fc261934d062a + lastCertifiedTimestamp: "2022-06-13T10:43:25.314602-04:00" + testedOpenShiftVersion: "4.10" + supportedOpenShiftVersions: '>=4.2' + providerControlledDelivery: false + chart: + name: vault + home: https://www.vaultproject.io + sources: + - https://github.com/hashicorp/vault + - https://github.com/hashicorp/vault-helm + - https://github.com/hashicorp/vault-k8s + - https://github.com/hashicorp/vault-csi-provider + version: 0.17.0 + description: Official HashiCorp Vault Chart + keywords: + - vault + - security + - encryption + - secrets + - management + - automation + - infrastructure + maintainers: [] + icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png + apiversion: v2 + condition: "" + tags: "" + appversion: 1.8.4 + deprecated: false + annotations: + charts.openshift.io/name: vault + kubeversion: '>= 1.14.0-0' + dependencies: [] + type: "" + chart-overrides: "" +results: + - check: v1.0/not-contains-crds + type: Mandatory + outcome: PASS + reason: Chart does not contain CRDs + - 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.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi + - 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-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/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 + diff --git a/tests/data/common/community/report.yaml b/tests/data/common/community/report.yaml new file mode 100644 index 0000000000..67703d6654 --- /dev/null +++ b/tests/data/common/community/report.yaml @@ -0,0 +1,100 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.9.0 + profile: + VendorType: community + version: v1.1 + reportDigest: uint64:6486223755507931085 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true + digests: + chart: sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a + package: 9cb7e3a5d82537f512319c754ccd6e9a4be08118b34849609d0fc261934d062a + lastCertifiedTimestamp: "2022-10-09T18:47:37.831969+05:30" + testedOpenShiftVersion: "4.11" + supportedOpenShiftVersions: '>=4.2' + providerControlledDelivery: false + chart: + name: vault + home: https://www.vaultproject.io + sources: + - https://github.com/hashicorp/vault + - https://github.com/hashicorp/vault-helm + - https://github.com/hashicorp/vault-k8s + - https://github.com/hashicorp/vault-csi-provider + version: 0.17.0 + description: Official HashiCorp Vault Chart + keywords: + - vault + - security + - encryption + - secrets + - management + - automation + - infrastructure + maintainers: [] + icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png + apiversion: v2 + condition: "" + tags: "" + appversion: 1.8.4 + deprecated: false + annotations: + charts.openshift.io/name: vault + kubeversion: '>= 1.14.0-0' + dependencies: [] + type: "" + chart-overrides: "" +results: + - check: v1.0/helm-lint + type: Mandatory + outcome: PASS + reason: Helm lint successful + - check: v1.0/images-are-certified + type: Optional + outcome: PASS + reason: |- + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi + - check: v1.0/is-helm-v3 + type: Optional + outcome: PASS + reason: API version is V2, used in Helm 3 + - check: v1.0/has-readme + type: Optional + outcome: PASS + reason: Chart has a README + - check: v1.0/not-contain-csi-objects + type: Optional + outcome: PASS + reason: CSI objects do not exist + - check: v1.0/required-annotations-present + type: Optional + outcome: PASS + reason: All required annotations present + - check: v1.0/contains-values + type: Optional + outcome: PASS + reason: Values file exist + - check: v1.1/has-kubeversion + type: Optional + outcome: PASS + reason: Kubernetes version specified + - check: v1.0/not-contains-crds + type: Optional + outcome: PASS + reason: Chart does not contain CRDs + - check: v1.0/chart-testing + type: Optional + outcome: PASS + reason: Chart tests have passed + - check: v1.0/contains-test + type: Optional + outcome: PASS + reason: Chart test files exist + - check: v1.0/contains-values-schema + type: Optional + outcome: PASS + reason: Values schema file exist + diff --git a/tests/data/common/partner/report.yaml b/tests/data/common/partner/report.yaml new file mode 100644 index 0000000000..0383895361 --- /dev/null +++ b/tests/data/common/partner/report.yaml @@ -0,0 +1,100 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.9.0 + profile: + VendorType: partner + version: v1.1 + reportDigest: uint64:1709340413986663090 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true + digests: + chart: sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a + package: 9cb7e3a5d82537f512319c754ccd6e9a4be08118b34849609d0fc261934d062a + lastCertifiedTimestamp: "2022-10-09T18:17:00.692384+05:30" + testedOpenShiftVersion: "4.11" + supportedOpenShiftVersions: '>=4.2' + providerControlledDelivery: false + chart: + name: vault + home: https://www.vaultproject.io + sources: + - https://github.com/hashicorp/vault + - https://github.com/hashicorp/vault-helm + - https://github.com/hashicorp/vault-k8s + - https://github.com/hashicorp/vault-csi-provider + version: 0.17.0 + description: Official HashiCorp Vault Chart + keywords: + - vault + - security + - encryption + - secrets + - management + - automation + - infrastructure + maintainers: [] + icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png + apiversion: v2 + condition: "" + tags: "" + appversion: 1.8.4 + deprecated: false + annotations: + charts.openshift.io/name: vault + kubeversion: '>= 1.14.0-0' + dependencies: [] + type: "" + chart-overrides: "" +results: + - 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.0/contains-values + type: Mandatory + outcome: PASS + reason: Values file exist + - check: v1.0/required-annotations-present + type: Mandatory + outcome: PASS + reason: All required annotations present + - check: v1.0/is-helm-v3 + type: Mandatory + outcome: PASS + reason: API version is V2, used in Helm 3 + - check: v1.0/chart-testing + type: Mandatory + outcome: PASS + reason: Chart tests have passed + - check: v1.0/contains-test + type: Mandatory + outcome: PASS + reason: Chart test files 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 + - check: v1.0/not-contain-csi-objects + type: Mandatory + outcome: PASS + reason: CSI objects do not exist + - check: v1.0/not-contains-crds + type: Mandatory + outcome: PASS + reason: Chart does not contain CRDs + - check: v1.0/images-are-certified + type: Mandatory + outcome: PASS + reason: |- + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi + diff --git a/tests/data/common/redhat/report.yaml b/tests/data/common/redhat/report.yaml new file mode 100644 index 0000000000..bafcb9cda9 --- /dev/null +++ b/tests/data/common/redhat/report.yaml @@ -0,0 +1,100 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.9.0 + profile: + VendorType: redhat + version: v1.1 + reportDigest: uint64:14250977800867714981 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true + digests: + chart: sha256:f01dd362d81fe4b1ef99bb7fa5be268fa94dc96f3009d28bc93a18517aa1ef7a + package: 9cb7e3a5d82537f512319c754ccd6e9a4be08118b34849609d0fc261934d062a + lastCertifiedTimestamp: "2022-10-09T18:45:02.882159+05:30" + testedOpenShiftVersion: "4.11" + supportedOpenShiftVersions: '>=4.2' + providerControlledDelivery: false + chart: + name: vault + home: https://www.vaultproject.io + sources: + - https://github.com/hashicorp/vault + - https://github.com/hashicorp/vault-helm + - https://github.com/hashicorp/vault-k8s + - https://github.com/hashicorp/vault-csi-provider + version: 0.17.0 + description: Official HashiCorp Vault Chart + keywords: + - vault + - security + - encryption + - secrets + - management + - automation + - infrastructure + maintainers: [] + icon: https://github.com/hashicorp/vault/raw/f22d202cde2018f9455dec755118a9b84586e082/Vault_PrimaryLogo_Black.png + apiversion: v2 + condition: "" + tags: "" + appversion: 1.8.4 + deprecated: false + annotations: + charts.openshift.io/name: vault + kubeversion: '>= 1.14.0-0' + dependencies: [] + type: "" + chart-overrides: "" +results: + - 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/chart-testing + type: Mandatory + outcome: PASS + reason: Chart tests have passed + - 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/contains-values-schema + type: Mandatory + outcome: PASS + reason: Values schema file 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/has-readme + type: Mandatory + outcome: PASS + reason: Chart has a README + - check: v1.0/contains-test + type: Mandatory + outcome: PASS + reason: Chart test files 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.connect.redhat.com/hashicorp/vault-k8s:0.14.0-ubi + Image is Red Hat certified : registry.connect.redhat.com/hashicorp/vault:1.8.4-ubi + diff --git a/tests/data/readme_about_testdata.md b/tests/data/readme_about_testdata.md new file mode 100644 index 0000000000..ff71c466b4 --- /dev/null +++ b/tests/data/readme_about_testdata.md @@ -0,0 +1,124 @@ +# This document is created to guide us in case we need to regenerate the test data +## It also captures traceability between tests and how the test data is generated + +### HC-01, HC-02, HC-03, HC-05, HC-07, HC-08, HC-12, HC-14, HC-16 + +``` +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true > report.yaml +Copy report.yaml to tests/data/common/partner/ + +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true --set profile.vendorType=redhat > report.yaml +Copy report.yaml to tests/data/common/redhat/ + +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true --set profile.vendorType=community > report.yaml +Copy report.yaml to tests/data/common/community/ + +``` + +### HC-04 + +``` +chart-verifier verify tests/data/common/vault-0.17.0.tgz > report.yaml +Copy report.yaml to tests/data/HC-04/partner/ + +chart-verifier verify tests/data/common/vault-0.17.0.tgz --set profile.vendorType=redhat > report.yaml +Copy report.yaml to tests/data/HC-04/redhat/ + +chart-verifier verify tests/data/common/vault-0.17.0.tgz --set profile.vendorType=community > report.yaml +Copy report.yaml to tests/data/HC-04/community/ +``` + +### HC-06 + +``` +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true --provider-delivery > report.yaml +Copy report.yaml to tests/data/HC-06/partner/ +``` + +### HC-09 + +``` +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true -o json | jq . > report.json +Copy report.json to tests/data/HC-09/partner/ + +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true --set profile.vendorType=redhat -o json | jq . > report.json +Copy report.json to tests/data/HC-09/redhat/ + +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true --set profile.vendorType=community -o json | jq . > report.json +Copy report.json to tests/data/HC-09/community/ +``` + +### HC-11 + +``` +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true -x helm-lint > report.yaml +Copy report.yaml to tests/data/HC-11/partner/ + +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true -x not-contains-crds > report.yaml +Copy report.yaml to tests/data/HC-11/partner_not_contain_crds/ + +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true --set profile.vendorType=community -x helm-lint > report.yaml +Copy report.yaml to tests/data/HC-11/community/ +``` + +### HC-17 + +``` +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/psql-service-0.1.10-1.tgz?raw=true > report.yaml +Copy report.yaml to tests/data/HC-17/dash-in-version/partner/ + +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/psql-service-0.1.10-1.tgz?raw=true --set profile.vendorType=redhat > report.yaml +Copy report.yaml to tests/data/HC-17/dash-in-version/redhat/ +``` + +### HC-18 + +``` +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.18.0.tgz?raw=true > report.yaml +Copy report.yaml to tests/data/HC-18/partner/ + +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.18.0.tgz?raw=true --set profile.vendorType=redhat > report.yaml +Copy report.yaml to tests/data/HC-18/redhat/ + +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.18.0.tgz?raw=true --set profile.vendorType=community > report.yaml +Copy report.yaml to tests/data/HC-18/community/ + +``` + +### HC-19 + +``` +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true > report.yaml +Copy report.yaml to tests/data/HC-19/report_sha_good/ +Also modify the testedOpenShiftVersion value in report.yaml and copy to tests/data/HC-19/report_edited_sha_bad/ + +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/vault-0.17.0.tgz?raw=true > report.yaml +Modify the reportDigest value itself in report.yaml and copy to tests/data/HC-19/report_sha_bad/ + +``` + +### HC-10 + +``` +Unpack the unsigned vault-0.17.0.tgz into vault/ directory + +Sign the chart using your private key and package it using below cmd: +$ helm package --sign --key 'Sushanta Das' --keyring /home/susdas/.gnupg/secring.gpg vault/ +Password for key "Sushanta Das (Key generated in loaner laptop) " > +Successfully packaged chart and saved it to: /home/susdas/go/src/github.com/tisutisu/development/tests/data/HC-10/signed_chart/vault-0.17.0.tgz +$ + +Verify the resulting chart tar and .prov file should be under tests/data/HC-10/signed_chart + +Also generate your public key and copy it into the same tests/data/HC-10/signed_chart directory using below cmd: +gpg --export -a susdas@redhat.com > public_key_good.asc + + +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/HC-10/signed_chart/vault-0.17.0.tgz?raw=true -k tests/data/HC-10/signed_chart/public_key_good.asc > report.yaml +Copy report.yaml to tests/data/HC-10/signed_chart/report/partner/ + +chart-verifier verify https://github.com/openshift-helm-charts/development/blob/main/tests/data/HC-10/signed_chart/vault-0.17.0.tgz?raw=true -k tests/data/HC-10/signed_chart/public_key_good.asc --set profile.vendorType=redhat > report.yaml +Copy report.yaml to tests/data/HC-10/signed_chart/report/redhat/ + +``` + diff --git a/tests/data/vault-0.18.0.tgz b/tests/data/vault-0.18.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..21803f6989799cc25d287e2b3d70a4ed170edd22 GIT binary patch literal 37067 zcmV)sK$yQDiwG0|00000|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0POvHciT9UI1JC<`V@GT-bp-b$&znLYv1hexNUdG8^`uzIqBb= zKK(c_2}xK}1P1`+Xwv!a&p`no!J9-`vXXR*bEYGcKmjNe>V`sLg5yB$9FH)U?I|7y ze|v(b)9G{$_xIu7PN$Rqx7*$8{;j*ex7*#_>+J6D{jJm8-RtiD4RxNPGx;YL67#=x zmToIQxbNh_VaOzwl!c<#K*&>sAlfVJ1M~)q5j|!+LLU?u1oYMzF$H)eWh8n#I}Z;JZD=Io z?0<=;nr=`}fw3iqRF2}iw#UXhBL&c7JlfF&%<;p{V0YKw?d*C!+3j>+4Sw7|IPi)0 z^5CG`eT9F#d$oV?>X39^?e0JyZm&2UV?I4+Lw5U9fW7;66b>8rWct9kFM5rZ8FC8= z;>DcGX`_Wm=NT zuTi(%-EVhVo#zb~&4R_;BhZQYgRfPnH6p>dUdj7cqFzBy&{o9Rg!+UxCU#VvcK21g z)A-tiy?JUqdihLn5EJoK0$?`(e|fOGx0~nx2Zsk6{=bgr89JI^8sNKtpa^q3CX#T0 zaOfjBB9tSa41i^85N!+phM_x zYP;QTAlr5i8Ufg6V>~1%ii5!LaMgua5EvaNLnHNKV6{P*A99{VEQEzLp<7^xN+Myzl|u zBGf`!Ho*6m>UK+a8@}J1_o3e;^jng(Na$NBMSLVV4n@ScDk=kUqOs_rVt{-S1#Akl z!O&$y!oHIH4TKUcz$t&s36=z1F~4u5;uQ<1H-#=l%oh!WA`z*sTg>;8Sp^Ix3I|jv z0BW0ZH)=YmKp2hVK+;w~LxLn3M*&s?)A&SV9Bl)!dJ5TrU@!?pD)j&J=-s)7<|;Yp z4|s*t%qt>lTj}1$K{Uc`C82ppBoRvY!9l=5Z+L znApD!cpOETj~P$l-w|00d^x;Y-8EGlcs*1BuU7wR!K0y4*BIC2dZSh{9iS+DoneL;h0G_2d{QGM9F2+O z)Dvp`M~sFNVTpFkOVyJ3nQJ3uw5Okdr3Ges1mP3H5%zsfM9S}z@o7$(bHtdYnIkS zXM+nSuQ(CPLQtcDMQ}EMNLtL64o81F8d{JZ3niu@;i8Q+_cW`*a6~0#M+7lzlR2Q< z$o$o9@3p)CiKqaGoct@MocIc1X@{4?nu{^87%jpS$0t?VhMqd&yMuNIY&&?Os~ci} zhJI!ojTu(}28?T?46#sE1%{#sm=j}dZ6iSlH23zEP)t5n^b2(?JZlu?xFqw$j>kfe zL=t`gY^b=SEyGsQNGt924gmF{_?(WZ?4jmCr!#Kam)GQ9F%jzZ?ty(hCSzN+yZh=L zP2Q%35BKfsXUJe9k(x+OBTYZXazzg#5~7eHFBX!G(}Ie4EMK$&`HBfaH5-Syp`jPV zKIx(6{|Xj1fpVK;95rVxwS6Gg+Ywm3_#po!OBlM2Q z5z|W{mLpZZhnniALk!I_X%(;`nh<`cmdn((;qszqs2@kb%zY>vkO>JK{7Q~JvWC!t4n zu;&pWT8b7z`0u}Wlo$(PwY0E5rb18_!pi3?2-MPlHpbE$(Qw$}dg+Q*+P)bA^kQH+ zm`^08f$7`nCp?Y<(ksV>wHpvLa*ppvkk}GxQRum7C3>6H3L2;c7W(`JsnIcp)ZuklOw%yj;_v-@jzOTzmmH*3Z$084BZ9HyI01XR$x`hhY=0Z38ezXA+~;l zZLMB_Vh%l(41HAG_#?U_0ee`-92-=%bPrU64V1-S51kD|#uJ^FW7jNHea2E^Z)Ei( zGu8&Ozm8lZ5++OO^ogpWMXEB9?AlOk$)ec08I;X1!uos8EKmZ}z=}AXs5kcnkO^vY zRtrz4K$!@}QV!!F0GEWFY*3q%8p?#Q6e1a-QY4;g&v9U*v9d0pqfiA4L5HD%hM_Oi zBM5sQ2u_17*E|@KkZ{lwa7${o_ zhl%}?;3=T@1e$GW6mzXyD}eWeM3z-1Bab5=ORIF*w?=Y~*urUM2P?bP;u>?oV z9Wfdu{WtLy;zFXVvGd6DmrARMOOFK6e-#85?l(SGRmJdp$0b&iP0L2PQAd(hA6U?bX zme5OCBAPPeWZ9(lvax~-%_JCj`~IiX>xO(CIjM7Wws$NgrUZj6I(4OzhogmlIHenhu!{1A*GHL|}qwWe*EP#*v%?kff?bnsI%ApG-R#GLD6!OGn|DhQokpFNs-( z0*TlgskYwHkSauhB5zu=KEa6jN1>z^SJuX`X7CBvi+yiI{5YWDP|FkZ`t%d=;?z_` z;I;)F$5j~8C#N;I500Xo8&11_KYvaQv?k~mZS|pv+UU!d7TCXuqA?dF4VCWhDc17& zGiqOxfM7w|z)rq=Y3AxD-u9j&7wsci!~!;)z9rKP5mL4@VnV7u8qFdg+A6L^g{k+# z&?2r&icUk|e>Y1FVN0ZH@?}yeb-*i|NA0 z+-uN#gcCtG+KQOpQeE76rv95zU$X{*GYre%%AC*K8}$$#`-l5V^LHy*UUS7M<7z#6 z0T$)66`3N(s)Xj>#{tv* z6oye)dj%542_e`XIk3t)W2Jy$5w;(F4YAN|#N*It`A97~6cYKs_&s_N3o?iUZDGfX zHERE3h_KnpBIx+yOf8SoxBaaKg6bE#wrg>pCBAl}*(8>nrqu?rU#v#F$o!kC#oe0E zUmAM#^{hucWHRw*f^+stnKp^Dro=tANkAbuOH56v<`y`>?#9g!d+T(o5rXu?(t zDDn7NJFs;B@VC=Y5*~3X2)Yb|>F;{F^-dsIGJ3486UnszrYr_kO8aeLlfc(sjCW9| zA{>#lnSgpE6a*B0Rb8okYM3yk2#03xH7T!M#%fr)d=%gi)P7|&`LU-JMgtWLjR{qg z#nh_Pyo$w=7<(chO7U~l-@5>Z_vl5lxn*QIqA(*$AJ(kyT1!n~rpLipxBUtMf0ac0 z{F6`Tx;wpO3ovU$RbX?(0$*EHW%#;DOd(UpYW7wuRt+R>4-L_{#zjg&3U91vyCiD4 z>Z(Gbqi#Q@w@w2fO`GOJ)1XxAzKPDeZM4{JM5NbFOdzuqydZ|X_b9*~5VkgLv>QhA z5yg*u%84ga4y38rpgd8xwK4}yWW?w*^i#TP7BT}Zsb{;YkU7RaS<&80ZK&63UsY&cYmB6UP( zw~;i-tlS$tt1HGCUAG^`E1h=4`kuGZ{|eagbsf{qZ_s!=3x}|!3dW2Yi2gG%Q6vN} zB;tVSf~iYEJI1uPlC!bPU{qoFYS5@uf_65(1573guD~S5LkF-7m}*B&iFW)czNbwa z10qC>`viPDzr??#1;9sb@!6;%XUvkcLA$ik)X^6RorfXd&M*N75aEgUO-2 zZoB*AHf;D<0>8p?WISl#Fv2Lpas>R++BHfiEA01bnsK+SW7w?L@giuf^jZ%K>qJ}? z8dT?6avGWaX3&68l-=#rE!tu9&=n^GYUln}r6RpK(w zZ8^3B=9#5tM{4b2gq~St9$rI#IEhH;+a7_*k+E6ZNdT8?fEe8C*pHOvz}4#~oKTMi z%%7`lKTr$0&_g@_NY?5d_U=jO_mUR{LqtE?i6TVM;zVmHna>a}lzD?t7?1A=@2Mf= zSBMPUZlP92EzDrHB?FPdb!Ot3osRN}c$V^M9cI+B@kH3L+ERX;gi=^RqRZMiCZ*S@ zg&1KW4#3OU9z97ir+Rb2vLV;y;bW}TGK{4N)c&>qQg zU;-AB!BxeY!E!qbsXR^sRI;9J8q!S64eDoNUA2Ql6>g^vkaYPQGc+W6v^)+GM*66T z<|k}IVNWt2EQgFk<$+U4zN)V3B2;xRFv1C2Ku>}w$zZBiResELD!hDOB)zw8)?eh>X7M%$?6{chf4K6JFx z7V>&}KZw23z1x^L?rn^{jId^C83piUey4N@C}}H>_MQ zavM~~3HE77gt+2NZz#jo2NH5y!phawppPoODk(%lhL zu`?n#kfZ+t_VeA8-M@yWTuG1B)ZP&Ypb*=xPy#hV@~MrjxEXOGMlA4=q>7Q8}@+MK#~x>prLJT3q%j$1IEWXfJK4Tsy^WqjwWQmyrUr<$79tw z0ULfwLy5ukcEVM4?~FaqLbV>b(r!xV8))t!SXEn1vbVIEj4 zP1qzI6Bf&4AbS}~O+v~8Iw2{ICgac>BAJhsTB0Vh=1eMU{z<9qu(Khv8DwVT!$d$x z1Z9!0gReC}(w8hkLXrqJ6yeXlDw{z}k-=*1MaC6E@r&yQ+J-+Fo@zfYI?$@KMtMjwtqnoqKi`(Pt z)05MSo3o?yeyIS(|;Oe()C76%&D(u*}bLm>QL?^ zaSSRYRq6PkbUZia%5$r7v{STxFqTXSBCU}lnbvPD@mZII_F?FARg&4wH*Fw+g7dLB z-T!~3bN=_vN9{#)y@zIl%RPH(FuZMzJ#+AD#2&&lLe(9GgKY`!KC^-Mf^pQ;J$Q$s z=9WRK*5=1k_f)2phf0-BL*x{Cilh0cu|6q7p?x{VK0!&yt=S#OX!shbYGfQvIv9*j zC`OX~6ORMLF}OD62#1Qxwk+H8b(CVFqJJn_r3%YY%)!~DPwb8rSyPD!7CJ~(Y28V8 ztpSidqcgGSO$>}`V+ex6PN7Wj9ihosjagjPF%T^V z%kipY;$;|2jfGJj9r$E1dv(L}RuKj^DtT5Cs4W!!y`h8P@ey7Hz zW5dRx^iY3x&P)LlGf`y=620e}ZjcaX6BxuQOZPxph(A;eR{3 z`^ndcad!`;rSx~nJQnoO&GD53M4tiQ-`}eWX3UOlUMOQ#_}?6=t{wZAg;N&(S0kjZ zv@`1ssLXbuG@LToU>OnY2TmA-F=xRRxtluMLH#r)CLd~8<}))}E@xNB1lGV|SHoV8 z&3??;RdUW<=@Z!v9RnzJ0IJ@Z_^#Af(ME0nR5p-BS4d8`+=0ge93RCFY}=T|3l;(r z%t`t-(#{g|r#&H*ca|hOgH4OjX0mgJ(3>2RttD)LS;J4+({ZRz$AEzYk)>&TnBFc# zCl`I!=Loxzp^rGbqxPIoz_5?*aG<0PZ|A3^2ZOX8$J{y9gbv*BD$wOJh;1WutgZQD z3)aA699=M~CLs(6q zG@yWLBl(BEL$3x;oohL`q)DSsV6RhJllCmTm(y;9Cb>(($f!`t;vP^s5?|38T<%Q~GbBRFYq)DXbWQIUh?*NQ0_lNgAv*dzjBL z-I~S)pgLq-;ur2|QG*h#sMw4v5d0nofifqd1rvCekF4+$R$>N=K%p_g#roB;iC|05 zULNWA!FS3m%XPyiK)-1)M?%_HAC662hN`UU_RuxK{;!-$av6HXq)$a{A%U?{QpB-O zwJ991p_0<7H)&wEHc5%UoqBprIE(gdZadqM4I{iA1e;wrIC-12W?Qo`n(olT!inh;)PWCORyCqjk>F$W<5 zAwt{g4S1cML)+U8tBQmyw|7wp#DiRMpCZDk_3nVKr@NwlvRIR5NQe!0vWr4t4GucM zk%KnEK^q+f0dQ83xlDjW<^*=qPz(7Cg-qK01%+-MLL zOj;(2yi^8|$*dJZB>Hgl{`}_lUf(KVHtJE9IX-_4vgnC!a^#&PrLQZL0mO+>G*RP zZ?>A5b*-Jb96u!PltF@?Y_ON3PvArZ7+B5`>Lkq&6=}f472yI@La80M#{s6}gg;}6 z9HH#!_L}4Yp^8eKSp=0ISVpE>*BS#z=SN@z)ZKPcJJ?eB-MEa$;LY)Q+bX#iOe*80 zB<2k^F;wp$ob1?cO9&(&NJdcDHg<+%ovaZS0+JjTU2loFGPYjzj#O%G*|Pe$o7pRA zqw?fif{=VNt;#j6lw4JF=l+%0Q3Suk8rj?AcWfBs*SkgVzuhOiKF-5OY z8t{UQnLs#dN-t`n&xyBF2IAJHvwLm!$AOm{kHKry{H@pfy{Bw|)Q4(}+}n~;U0cd} zZ0!OyD>7{wcD-JgsEMks`H{E+^YM_3TOvz0YPZd+Ul>!1^f+L#kKVrPr^n-9*Y)kY zJ}`G3eT4(qKi3)ABZYjCx6y#*6kYEey4QIE&J(=DDk10UXm}=c~w>accWw-Za zT2-7(lk#P9{moIz{FNT1&uu1CZ+N$6@HUNzRVFoiwQS2HC}sAFZ=aE> z1?-B2F`R-)d;7sArU7oP_l@WPk^)IJPTR=_LL*!$!qd#5dyYs4!#JX_C`*=$Zu-qp zQe#ou^O*&n`bdmjVsr917J-wC@rwEA>lM3o9!0{BCV)T&r~hEdtRwjzF|Kv4-OQ3GZu0@bR(JY}GsI^YO(M<-AOneD|4tso0R3*+0& zVVtIur7}G~xla9Cvyw#kyg4e!pJZ)sl;OXyGVB4Jl&Qnc(f?fAZ!Y^cw`W(qR|jeH z$%?489yVHaPOEP1?jIg>_rEFS8r*Hjr-H|kJ~tZsLn6&_d@W#;_+{9<)cccKDFib= z(m(AnPJ>LvJ$&{MB0Vc`|~L)Q$FuH!1eFUR!~0wOiAv`SR^W`lbQ%Fq|iGO zY8D|&5!RrnNb+O)?#@^o1n}MN?E%m*d-Q(p7Neb)**lE}+GC?Y3^o!M+3Yxwv{%&| zj+qcKH>g@^(xo)!6sXQNRRZnnZoE4ED8+>hHhF*6h-vk08p&RlIR}kM2xnz7iB#U7 z!CKV2vAUd2z>qxxVS;0<#YptQ4-M9=j<(WWOrST2#_c2y)ZX6Hbi*B**@zTpkp~QG zm$8iuK`$T(E6MZn_Ep~=#MNloas(UX;5o}&7h;X2D837*7+o-Vtz@gQr-^WmH{TR@ z_Q8K^B5#vK)@DCXpFK~KsWj}-b9i&bOa!fM-$8H>HIqP-X8P$OiC{3?)gmPmM^F-4 zu!=C16$Giw4^esg0F}AnDbE6DKz+#cbB)6xaV}Cy!U?;F>AUvJSMA-s_TJ9kZlhtt zmn@vKt6rmS>|IQASs2zHdY9=zk-yk8(O@aT{e3>C6T*{g{Iu=R@Wc%MV^%0g)>`0@ zkI9&Y_JoY~lG;rx1I-G;Dv(H!2rH=>Ot-Dw^sMakdP;aLL5GUi$^kwz4Y%Zow5kdv z-Tl>`_c6WE*^>1j(=2hR&IP7pqSffjHXwU|@r!A`Ovg09Je&67$hjVgacv-2sGvrP zY3Lz2VuEBK=QSnst(0F@Wni0i!DIHSo4t0Y#w|C0#S<;|Jd4iylAgIrkN+3WWk3D)toS|*P3Isu+G{L zsAfv(HJZ)#XN^o9)q2fjj1pOqRIn&i95x^_Z5-a;>$lg8NdtL;`A$IZ zcFdP191!;B8!L%WffRmUb21|4&zgp+BR-h{lzVhnaLfml9P}wW>Qn9|__G?)?^CPJ z?5gIHkGP2)+7%f{{(RXPHaS3MC1*&9r8)^nyF3fXhE)CVIM-f5G5Cr4H9sToJQy<%ED(gd)*10w8 z+laG>a7l@1_M8perYC6TMKy@zYXzv}dtxh;exBbz-G-8Kj>0vwpxg9sGfP3qnVrj& z)S6@dxPJEez4Ggq>K2+=1GDz%>h--Yf|K%D^R-r;Ot0$T3C+ex9_CZ^U}lNxue0XT zHF4+kymo(QwU;%0SJqsO7z+pvi+w1AX66%aH__~VdXVw$a~<%}J- zrZ)3N+?txrntH2jbO|btSPdS#(dXt(ZR!VI<%c`fxz!G$pb$o8v=@YfQ zVW%r-bSW}7d!Mx_ycXE(e#C}hJI~XwD|fqmF1oA7JdcA{SsAEwVAUP{hWgkj|I6vS zjbmT2f9h#0MzHO!;vmq+P*!U=vWSFYLP7_pp!BROzNn=Ye zBpHYkeH>^RTB)%(Z6MsLm#0kty9}qAOBFMzp&mnptEJ6xy82Rl$yMY<@i`sSYCceC zcNrIp7sb~)Bx^nGjmdb8qZ^a)T1RIiP1f6*K5+T9cMixWDlfxpBKh=ZZE7vO^oNkYOQE_^+$-IMA18cT!TzlKNh>sjtkws8nYq2S2K zk?&XUyFO}(mEGyHS`U(w+q1h*O69jDdGlavhb|nUEcuppyT8VxX)G?8qdTUcHzO9)`?r^nNgRGV< zyO)kUrE_>4YOQYLY48*=USq9P?irPW%-?FV+Ert-k*v7aPqcHz$%NgL%dkI+rOzJ1 z6$^5+F}Cq`JI~YT)pbG~R<_ynr0nE@+>IE!Kl(=P`bPO*u8&@yK>o{m=)cj4D|N`E z_XMnsMuqt4q$zavBt2hbo;8~O3wJN$rVNay)LJhb0NiF%k70@IXWQ$ z8REaJ?dxPM-tgKDuifz4CvNF4%WHiySr>zGZ@yX2w0>JW{fTySLP>bUsUYXnBcWKE zywIC+Z^@cG^N-jAd{PJf0>|$b-aR``(p`N!v9Monr|&GJ@_WL_Q|wGuXOCZL5U-oJ zZKZ%mcx`H|z60eul;X2uI~I(PTZYl*^OAq{xV)M|ywIJM2IDQ6S71JsB{gbNTT*`R z86fwmCO`W;MOuL;(8FfM4B#1u5DHB_4bj$Iu!Y{NGX2E2v2c>>iJr0_Pe8MCwwK69b*rMU6N&9@>%f^L3=fF0GE+|usMndnS!zKS?C zoNj&`QBq?Sj6RrciWl0dGRf<-B^POE-oji$rhJuLKkH?Mf4x)~Yuj_#B;8m+qsy$` z?46pRPh!^&7?5d9oN*ww) zVBy+W>^h@Ro*;zUbf?C8sI~F@H1W93J>MJl<2;*#ptDDky@lmQcX@@LiA@sO$M&&N z{+D|OHeUG!N5u>0S1f(Lql2qYn?-4+_WDRCV|*7-F}h&#nqYr=E#?1(a_h`yuxQ>*DLSnd49KFF}M2{Jd5XhkaU!!rs9*`Vi z38hDPLXc0QfK7?7i)_OObI)_Y*gawsb33cgJb_q%6LOx zzO>N|Ljl1&L}SJY!gnl|&^J?i8%aLxH5v&0fJP)3Bc;d3q6}7AW=JF&>ljx=GXtr&H7)@^ZE16KN`p2;z1bsI?*^9#6d7c z|B7)y2bA~-M-i;7cH>uqB#%Q6%1d;(hrAKy0^Jc0$ATci#>8FJsumhh68HjPxMO-u zC0sn+kRvM4i@PZ-!jp@>DytZeh={k}tv!Lxt{rG7^xTru6aBPbTCp8{d% zyy@*fcD{UReE!_h7r7JE1afby65we9`*c2J{nZw$cVjAW}#_K<9g}6MVOi)tEHXu+5bAfeunUXee2Zlk_$7cHs_2oQ86M zn*SkM{~?+=Xu926FhhUl?}kf#&a~ptM-##Y-1sl_)EYX`rC^h!a(bamvvM#luJ!K^ zrma5^CsBYsf|~!?M9tggLZj59IsFu=?yJ7SSo9_Ty5E&O&Jts7s#v^t$7~$Ml0cWn z_!C@C9wt|X$LaAXWDidyx0GKo|0H*=tlhX#YuNM`ZuWi=?(A;0y68d4a1<($45&!j z7!&nNSiI=mQOFBoAqj6cj(i_sU;z{d$WyCdb-?!Kb$8o(Sh z7m*4=B5aNG7^3w2Ee)Isjraac%ND}NF%*S3Z9*owJj>(dE( zA>upq0)CC-pvGuATWEI+b=9vLi?vU*(lo)GLPwsDnNOZ8IpkpKs(EzkRPze-M*?s!2XIcCp^s)@ zEshK(#yCQdpGK%2f_g|r_*{ZwIx)uPSmDn~SiqS^l@c3XEF>Ezcm>~P8=)iLr|KtG zN-dRyD@#Y)rZ}87;8YGWPUnoy$Vous7%ZtxD!)o!MFb zr3UaKBz6wen%zP#)Z(e8em$(MvSn;OZP|%XPYd*qp23FW+|n=J;i#AKcz z7qsOz`tqffz)NRV4~ye)Nb>aKrXwipokF#S*iv#ZObl%MsJC(&skgn%Yrvyo82WQF z8CG*E3&9y$Dg2aqInea{7D1&sOlAVKcu60#F5WTU)O21rD{FtZgEIW4OzIijPO@lK zg(UkY9tTwg|3D!PeYJpIRkra$k!C^mOS#_+IPe zES5?$zGI&d`4kaON$3$ou%LuDRHJIW2ym_`Q&}?gLbFP383FiX7K%7PZ%8n%RMk{GgbJ@SSJ-hnSM8BiI{~amG+B&7EDgH2D{z%IFpD%w5n+pNIx;!a| ztv~!>ODqmqb?M^h-DyR?)u%twQj5bsx;nc(Iyt#6fp0(kk(OE<{>}B#MgQ!kstxz^ zAKB82w{?B=b7e!xw?CY6^_$p<1>Xsnhl8Cv8t(YayC=LwLdmBQqoHhzk!D8Lh%u7x zbvf`d24wR|ne|bsZh?9}bNwvdQKcQt?aq8f3@3ULB58)A#MBd6ZB`AZlDm*_QmL#K zF_21w4@d9MZ*EUcKipnlUf$f^T)sWMxIH_`ZPo$*C=&t7baHdl&2Q%XtbcQQ0i<-= z@0YjJ{H@pfz4vOj(`nYiBMe8Dcu} z_|7YeZu;k0mR|+Zpn48NNjk~0*~qk)l+06O9q|L|a!v-7*^0_q!2>zV#@s@wzEov^ zr~iXdN{!lCeuS|er8WW zq;Q;S-A&W;v>i3QJK45sYvek_s;*jYN~gMcclWC+=ctB)UOxBbZ|p85dKi%q7MkK3z-EvU(1;3fJW|Ck5qfjf7}McMqL4{Ym7$U?E2z@0jkZh$ z3g|rn7r}ES>%4ms`rBy49>|37ZNvX;T@Ch*AfE`zvrf)A&HzshrnX0*26L*x=gst% zVbj`JZbu%?6%GHHVMo;G*(hPPDNAahF_BpHB{dC8_%4hk^n%z=aIe09NQd8c!{1Qb zS$~J=N)Qq8(st7H+M<`y-)1xQw3^Ftt_k*k3HNE9l_HG1tq0D<@(;f{kA=ctE=FI1CjJq9ICI7%;~-N7{d1?OL%n z*VN$f`SIIcB_p$MdJdJMA7q4juaTTab^y#G-T)=cEI5 z-_~B1<)9wBE;GYIjM6$4*}*s7KFR8N{V0tPJR?W(smR)|n?L zSbvrZ#UDFI6@VT+Lq&K?&rcELs%9q>r`E48{p;)6S)NdZ=Q>Pq_1y1C*xrig=?e1N z#+=HjP6A$MdLj|kn3`e0?r@MVyzn}j1!nOXs(|+R*{J}ysug6ji3w6q-$;{>EwFgX zD?M7Nm<8_fYpMe3^6RPs@bWY0#0|J;1b-_8BI$6Ps=1sQu~H*=Xnp|C>_Sd^TaTrM z3V2J>LKW1dX`u@6)6#;DV4F=0vjb5Ur-t$%v6-b-K@U{`mn4WvuuD@!CFEsE!fd=H zq2Fozjz0Zpjvv8D^ysry+N6P2#s}*J4t@Syr6MME|3LXT)fyX5%Z)Pe zPR$!=vEU*@Evfw#2I~nrcbu6riwrSaapBQvmE^d8hI~xMES#&9sP=TxdJ0Kp`Gxt- z97v0EnreWL=P}hVmlrroL}I9JMa-`eh*4599q>YN7&9Two2$YuZyB_e?D3WXUfm9_ zT3Hci6Y7&1dx^F$^_Eqx-AnjvqdWmv;`}r8wgmFhnz%xkHWuQGM)g0JpcHdou083BYZImxx7ifF!4pnt_o8uX{9C{26L*9s&qfY1bA=x%Bb?u!Kw*@qhQ(OFt9htydmv#4y7aFV7A z0IdAX*%%gBa#HG=3=E%4>y>00P~_u$drPLXuvx$?>U$$H*4eK{1P5}2yb)mtaWuveGWh`-N9kW^91A#ZrPlr@=&?C)r6~Tnnu+ISmP4*D zDKa2YLJl2;o~y$7mpzxVT-++IdnQuO+18}sk{8M)41>X-wz>DBSU$f)xlmp`n|A7k z#=9%jxUlubKL^29Qj87|jv(*PT|nB6ejG)NtJO@yD3&^xW2t)@=rh^SF3yPc zVwX0XPR|dJhNbt~CSbDkE7Ba&xw5azKPI`^WNDSmTNjtck5b;x!O`{|N^; z^awY{+{~9GHMc#&RtnEBpVFq>aa5Cnp7OxyM>hFLlEzQ{|9zsw%%0g<^XE2|nIe5& z%bkkM!K@6bkTbI`z_Tljtn53DrDC}pO7zNhi}k_Yy;e(B6al+mvy0oT$IQyDUWK{W zUpupFy(gdl*Auj5k}+}?koFXhm(Bw`C;xZ1vzyQVd$@Oau*v_sj^}g5pjXeOwg7!rd^s=%O5V;(^?#kqV|1~uvqmVX&@J-Q~|pJ3ZzmGJtN7B8N|!#jB$vE z#BbeAv$gEb`Ij#Z9tVm|wGfWzFPy~@45Njb%|`ABXv;ErW2lN9$?i$06kEdYOc5QV z$aO9nbf{|9{eCxKc4-Nc%gN z1}q@PA7SOe4BR#lDvH(>2viZn9Z(>abOZg2|L&1(m}+qIVi9lfI;WO@?bY=CJ!Skq z+1p!*_CJ^ZAHLi@$m{=``+wH-d@cTuqe!Hz{>1KqF3sU<@{I4A>7y)8X6&(p+rnm} z$=)xiM=qyPHV;b{ShkAI!|ZMs)vA}fyTCx;3@(~_4`uDnW437hu9Y{D*%%U;Gsj~r zz0tWt1gj#0#}I;@Ow+YovZ5f<%bESmuawyxeH_h}kY8aJx@`-)sgSapdKZsd-N~k5 zp#zy{g?8H8Bpk;f=b4x=*yR0X%UHs z=*yR89^zFT1g_(_P%lafWc!2pFqa9cg<37UkMw%Sj&{dvATjU0P}(PV@$hwiU8bjeO|TEoqdSk_b_>AIU=F+XZO{dT+)7UX*4WHxLjXla%LfuexGmQQ&# zoJv-v7GOZ+HDmIGa`ffPPQ-js3W&1^4}r#e=nWI{h8-uVW9Oj0EH~v`?9Wb7IS2m% zosf_SamCpk$xgsX%IKGTdUu2(ok~*gckXb4{b{}g zegWH=zO?hj^Z1}4l@tdjB*4=?@mT1KIS@-~!MsOXRIS8 zG%suvX`qrjT2Mms#zIq-?DtWv&F00GY&APyGnr}e8jhq+G5e*}O^R_0h18RebAy!G z|JiMW<(&ZY?*DyxxR>|;b`Ewo_WwGbuVw!yrhm>;xUv5qW&h6#G0n#=WadA={%yo~ z%OhNhmX6h0k_F6N|NAfV>;ItB-K_t0JXPyIy;yOTOrX4pfMB2S(_mhMTj<45mehAH zsH35TyDY1@jiTogE~B&DXQ_V`TmlbCn8IBzpQxh|RImTl1p&?5|Lqpn|Nh?oX8o_@ zS?~H^e-Kc1{jE9-=xeU-l0YEzTNC?ZDin4(8B!s+i`C}unGhlLTXP&MEg>A5tY4Xk zQ31tus1Rysi>o;#vT1(@RHT1AVCjvLzasrtdYUi+Q^wlK9&4Gy={AG2jFb} z-#s|&cJuoG?#BPOo@ed+{~0<-?{T+>l9Kg4BSa{vwE_XKJ}=?d6b}f3AI1?2Nho_Z zUvd%;WZfp2-H@VFW$Dr{Y{ejD((#Tr!xF zov6}UZia+bF2RC5&}QE_$SBWdUmE;VQ7^f3sa+9XRDLVW+|t==D%yn{6$NZWoH9=3 zG`XHEty1_@zG7xTC~WSPg+etK{$U|g8Ib6ytsa=2y3BWucX$Th_76^8ev~ETFC@|HFM0MHl56zbzpEoz#gtRozQ>` z$*J&gU;>i{ILO=pTN)fh!by2d*3mDg7dN+O7yom5d}FTX&V_`ei7RaCi{5`snk5U8 zaf$MQ59Q5V{&IVM`r-7vvZdmt1sk4|2?^Z08EH6RmG?qt8q^n%ADx_B&kEaeSv8yt z|Df7A@_k+m;Iem>m-V%*db1e6Rk)lZi-MezW3x-BRnXhnnajM|RccmSOuSz4DoA_K z9A>djxY=w}zrT5Ndv$d4rv3~o8``~#yGAQ#?~Z;sU0`B>pP$v_4Nl=AgQ%t**vcwP zP%>*cH|PD^+Qs|K_~q_<;qlpSbgr>xt$ef3|{dU+IOq4~F`|6Ez?_heey zcoo@KcIxQ;&1EG8E$BcQ4C}Xf`{uHL(_c}`V&ZK-rc&h!UM)1m4<5bR6(|4s^7`G; z%|fjFXPxrN;pO^*JMP!x=hvqnF5jNsUS8bx-@Ly$x%{<4W|eXQPA2T0T!#HoEPeJ6 zI^6(KI&Z1Z7UC5>?w_jvAHTmoyZPts@#V$M>Hn=jSra-@vT%x1QpJPDL~`nh#5YiD zxt*S$zB|3S{^ty~-P|#d7wKKKuj6R~5$2BcegE|K{PgJL^!oPn{Pg(d^15QQ(0D1l zi=%g^{i~znQf`@~u*y6GN&-L4WmWv9qt>m_+)f-Ok6xR{SMP7HPye6yr~N8Qs}y@D zdOe7?7sc25O6z)(M74Q-_U>${7SHLJ)@<+H>AR)JI40wT#;Dp{Zj56xUSNzTr$2+X zb94Ic>ip;BE4W z%FFPYNIv~phmO})xMso?5=-joFr;e|`h=f_lJE%*YBEKGgZ|CY&DpWy;Mb>0{9GvR zadvTYdi~+3!irfO2{SuepXFxlhVnd#`Hsyz?PF1Mb$N2DIN#GUJ*Q~d`6*Yk$$i@W z@4t7QS5J2yrqmr(VmBeAt+pO|b8~Z*)hCN5Dt0s9OHW7~WQydDJ7nt@PcU>d-}5ID zT+mN=vgFT+y2k*%KtaD5gYyr}8G|c{k8~gBs9VuE%av90aTx|v<1);pP+P3$m4{HO zI;(+X2bcn`V3Z~8HybNBwDFa9t}FL{ZD?4_U~!3E;5qR>yD#(mzq_5igT2lE?>e5f z@BiBMeT*Y~M*}J;5$&U^Gqde0+HbHxGs75nCf#Nsoy6?&nGBi>)Vm`RcimO4_oCmK zwehQ78pm$f2@ zj?e`QwT_ld<~5m6@?a@SW3YvqW6$=wX-x90Kn>Cw$;a~n0^U!AC5zb8G( zgcoPuCm2S*DP*SKGC{@>^F{ObO2M9BC4aep;ls>3TZ&+k+f5ELg0`w33}V>woyfkO z*@GN_8O>*MPbc+_7Mac4ka?-5y`6)$r2bGT|3ke6+=`&(rDDM5#{cdWb&4wyqH|Y^aik!$PF6iCNGqWi>HkH6`$3!9k0PU>igeDf95VjKVV4|5`ab$arR2 zC43%zPC{=OmyXaXNd^&dCYi^A9=bWc`ug_N_x4os|42lw6=eXQ$NzWt^YOojhr65n z-)nikKL3wIWa_Nw$jGYB3PvJYTb%HEjkD6DgU_|5_dI_{@VV08O&}N)p+r1)js~jd zx971SnVNtf%kdW$y_=M->w4z${}p5apRfNP=Jo&X=KhEEJbzODzrwSEe_}C^9v^fh z=aPu3a}Y5-FQ}wL#fia3+ie@+vA{En|F0LPua(qf zYew^qE7&;)4fQLyf4&iJp51TJeNnirP6!^^!yA5jX5$&UCjW{lCq9x4MVw4XC=p3c zcL?>GGbY=OI!8bH9^emPz-V|G+o$ROd(7Ra3Yv&9U zoRvJMVf@Lqa5JL9kS)SOKn6(F8k|sr+xB8X!y!6~q9ZPOx&(P0|-%%f7U+GEEAsFRQfoUfWw=oUfV3RTa z)I)~{2YUw%ga+nt*BYC&VIvCDKhuDSsgPuBr*|Xx7k?_{|H^Lvn{WT^7xI4|cJ?;$ zA8UF38u|a$=rz=XX_>8z)}JWT7zDeP1OsF?`_1Lf&27}w!hVb+VG1VK4r)f<)OQay z(-%p0v3yFgT)r{*Q(pkmtk*3QgHYCsY@<=GQJ(}vwen|`Ei?K5N^bv~&;Jj)oqYb6 zy@Qt<{=bfA!~dVgYnT%Qf9-^0%lQ?{BV?|PoEn*T9>3BW2CuMW8VOJw&dPf%kQ?hL=54XUzX)*~q+?XuAUR{Q&V@R!7V zNaR(Gb5hy-U|$oLB@t+U0yh0*&piIWic4VT@c+)sP9gtqx3kgz*YbQ*{C}BCVE&2; z9#;_UnqU7q`O+%0f6v$d4m-Q~^}m0x(f`)+lq{?<^NDk7kmU{`LGTGRD>ia#8~!H2 zmogboNI-^op4ngf*x)QXzX?e!YkLK%P7KbfRB>EzMLEpP(kyo+j7TtUi_wlZ!bV%m zu`Kv7b6@&f+t6NNc5|~&sOq(ROL9x4Eze!JmBG>Xl(M{v|AX(omALSqQvciQ=J|j3 z<;%m({a< zQ=ZMIcS$7CCy%;i5Nbb4vHTMSJH60Zzwh=RmV?)=nNRCFITVK$fp5 zou6-Ob`O&Uvd*5R`rFKo0e~94OLMJc6L;nq(TckK`7;_5J|yUcPd$m68KP+d&1}sk zNq1bTktaBwk{ zm`{^$-UyDfwQC(m&G+CjLSe)Hy-WhMG5+*E#1l*dMFOP=jQqQHwph~PWp1#XjffTG zO4DT6aBo|5e?Q^+ui*b9+`^uu6S9gp;CcIho$hX)|93X|pV#xOMgPy;=(hSRg)O%j zFpf0cBDm`gMa+MKh+}AB!=CK`ok(MyAh%o{&P60UoIRJ4H%BgV8WCtm{m29&mlVwG zU$~n?6?KesKfBTulRsy*ldDtMIY}laA$YTDZa5wcsHbyOG%LVsI8l5TP%*k-@|s|O zdgS|@2th=0$EuSVvxTZ?!o8!>$*4JNaM`;rDk8x1h)+tP{5`wC94e}X%qObSY;FOw znpyc5re4#zUdzIo)lFLlRb3UEiHC2|P|sQhaj$ke-4fiG*UN{&uOjZ{G4(K}f^hbOj3FeEqLr|L^W^_W#!M{Q2}hL$^<<0GcK?8sJ6)+-QIs z4e)!?0L@*#8x?T#z*D9F5$p#Nz4{qwB3k*K;&1b9LKeRGsE*U8lKLW#`YOHf)rGzs^%3|IL+6t4RPlNB$qE-x>Me zJ>1>g|FfRQN_Zww<-Ke=4ry66A;(NDg;`;5_q>0A6J=Lejs)txuB??7N-7%m47{!P zI?pLxxn2^x@^ze7WewROYLm^1-Wf5InSsAX5vQRXpyq#w)_;g*_G?oEZDt}2a$rEN z6)~R?m7v4q+k7 zx%kVVMq^O7tbP>{q*VR%7E8tCv^hiv84+e=r=}zKLvk3i+bMK9KW#67iK;q28Gya} zBA`N|=1y~K%dG;JD8S9pFsqp_aDa9}9_rS%d>mk>jKA1MZW!vh!`?j!{a)tfvK=b9 zq}x6T|H(+dLVaVK72YBg#^XD}Ga#~lV|TpCIpmVQUDWG|(?-oki?s5n)4LV={|aw^ zu95$9|1hWjzdYF7|Gb_jO?i;lLqqpIv{fc{C}B4 z_h|aP@n8FgdHw%j?`3Dh|JU(+H~jzUE`I**xcOh} zre`-TYu-nxX1XsK$HSQ$^U0=8q^LiXxo_TS6>ZeISs+}-5=S<932)0fA7Egs#a>8B9e^|T>cy3uvUO<7|qmLzMeLQfJb z8Y^{q-Q+J8nxoOU^1vKJCzaY#8v0Sz9S8@}kvUAyO7@finiqfYKWBOV(Qlw`3($~ad8lp`{I&I!cpD|=-MPy`JY03WNi}P%MZKYvg|!4H1h7*YnceoG#y-F5esz+dOmizg67+TOad6=6@tIM8W~a_QQ$sn~H8 zi8L0-fwnfg|NCI*MSrMBaW%s$nm^D~KXFRL{CCi|F$tx%i+kt_a~uRDfQ`2(pdJ>b zVC}~QUHV$7zT{Yv;ncWCE1U+|vgvuXWs<=^!4Zyf`wAsZnQl5{tezm#<1z&#-_^VZ zvg}A7JJzaPWsY5+pGi15spkkbVGwdFl%E%I>!kYxuQK)BH2d~VVc^9@p$giCQB`R$ zpdpph48}^zNfubg%)Fwot8Yj*SGLUk6>-WqmDAk$T5vYxKb5VX8TAXtRh5|;e|>LR@Tgp zVTYz8k&am?$tUMnDR)9RGR>J!CaAJF0YmCLXMVe#PRCtCX}C!l=gU`hYYraQWMR@X zF+UYFYs%#KM&b!$W8$o38ztnNQ7b2HSlVYHmB%)$#@QgGnfKY=RWphlG>2hSBfu+Z z#SRUIpW#y_vdtX6Xyy-V(=i^FAADCIlu%@XO2((?%a>kNDT#;9TsA?$m(7xfuHqoD za=8jvl$6N=@Yr~avq!sIsJU}T!yPedX5Y4m=f1&wST6I`hXP6#T8lH+BeXp?pv`T! zaHO4tlPo+t>u)amH@9b3nQsWerRYCrPG;I?KuO@6*zD48S8$$R>1eS|QjmjgTfVwH zdBTPx=2x`);pqMO&F$M){ae+8i=%g^EANBVd_WG&>4fS)|LW-YiF*OEV@6kwPEM}N zdN4O}U2kWnyZf@;X?NP)-mBeCr#Y*YtFy9}%6aApw{b*|e4nF2Mgh~e;^LoG>2u~1 z_D2o%hZmY`w@QXuvGh_L^xvLc-5$TW{Pm)ud41hMW!3)a&GAv`ikVqgE%C2P=Evvn z`!}c8vuF%qf2qn~eXFD_3` zZ_iHFI3;sv^UY;Bf%H&wM=3o!Faf1F&Y>t^!*enr!E7v?U;c7?e){3`ym}>8!!35B zWMq}S`vu;ti}WvnyhsOMC>qTxc#5ziP6auq9tj0;(-+k&`<6`S zP~!R7@##hX^!DoL=1uu9^{np%8t_hW&@&n@8*V8U3agxilcGA6i()V(l_Go^PAF&L zn1u2J<{}@$;AqC(-MTRQFp9@R)>gzjWekemY^8b(g-XU-VRNVl>gW~ktWKv;CR2t5 zmuUZ2#l#uVHIUs$^sL26R8+w8L6HReG$cY?adwvmdu7IzEz_&P zR&r1yEJt}%DFpmN3fY^|^jGsC7Lt0o(_K2D8a=ZS_Xmojh!pgtDsAZ*Ivx@4-YDAt zh2l^UtWfB>yanP!#ymuwT-}=yLF5ya$YVZ1oD2fuNnzrNaA2VJa5hr5A0>8$j=d0vF(vV7ugA)?qY2vz?3$>(H9&nWPhRvr=w|pL$h&_)8vB<#l zRq`Mu9TOJI`W?(xC>rX8`Kr(!=!DGS9hoA%WjH{#vGp^6Is}{T#9&-?MQlS&B-C#O@$K39c^kn=zZ@VB$ASn%g@}pJ2PgpAAP$fi z#nNXFp->YrLRcW5a5AJq5@lNWL4HecE79)kc3H2mpA;P5~yb*>d zS%xCcB0R*BY$L%CC;y6xkP3l80Pe**g2vd->xdePWIE1PHTW>10YOwg7wPs#I4m2Y z?seG?3t+LYKBx4F{{DA#uR7DR1ogvm}91N~0~Wm|qfA z(=6HXZ#ksYtQ}R8Icx$P)CM<<7`t!wkOG%ISyFGo8Mi`y-_AMVFlzv2fv;tAZmEsA z%=XmGjfvFpKig41*LmEon$EVEcqLdUKMzunEiGi!1LsZ4bN=|l84U483^l==R@wsG z2C8jo8V(quA$sl_p3f1BWfW(;utzsHr|+(A`mejL<{!Rx|B6FN1N0ke{tz}%Ye>)! zwpKc*{2W8|S)6>w^cIW{8hJFb1L;EkKX@w_S1tdK-yB{1 za(aIGOMC3EzKz-Pe}DJzsMztMR140YPwSM3gZals~J zd`CFyzLcYFwAc`wJVoG^BDA-?cq9-Wg#rq8{VXPEuhjsbR7oq@Y6ffWR@1 zLuB{I(U^E698xhxk|CdHe=RY66eAj;-+p=1I^6%URtJ>C-%*D5&f)%#TaAC1odGb& z)8Og$kL^zK?X4PQ=6k2z-EVgrjk9sY6|M=yHv9t}`9ARz!Fvpe6yb~x^|VjpnuI># zXKH8@93apx5R1U~ttLUCHo*=IYqz`Tuwb$^HN0$h(Tht@GDQr#2M#f0dyWwF-R+6F zx(;30+oGU1d;5#_W^do=O~>g?SIDF{Z!a!?y+CLE{(DWIHzQ(j1D^c>rh%S(MWTp> z&JGRSIQ5^JFh7dY3o2hL(f<+M5!iJ>AwwVS9atX`6-W@7SsmpR@v6N)k0Lrnis+ja zX3;4mBn3$?+CfKGXMo#-A3F=;_Tb0PmeYhFQrZNHA4{d{N0`}<=Z^IJ2pZN$E9F1h zO(PKO!jRzM!X$WD*0I8kZ!EOO0xfzpku8dd!@bT{qw!Nb1eBkKBIX2rRC;JY;nYjWz^mQ*#Ks?r_lsb8z@-%k=T(!hL+1thEER1difi zEF{|9<9>xa)Y34&AK~yTNrcoRdAYmjNM7!4*#->Bw;#yeh_U;l)I86s2GG*0MP={7 zt5o*xtAYPd98A&fD`(mNOt6eOsa9UdCn_()Ya;pdXC2Z~LuolUTv(JHc1k9}Qk5lk z#fKv1Bj?=ZHoUM0Hn)M&l|`F4Gz7u|{qyMEx#`)#!aX|xSw^H`3!1Y|9BwQbf}CFv zgP{r~&=BK89GZI;Oa~Sv--E-KmR?+SoyTElr9JjM5=k8+0r3!ch-oOKV%p5Z0s44P zL;s^ae7}50@`_d_aqCBo5ItqYq&~C4ABho*10VRM$q_&&NvMihQVUbh%TB&$B?)EA z`vnD`vR7q9{Ly$;^Bm@*rXh?3cuW!I98XR2iz;CUFAujGcKPq=bU~r3S z9;ZkxlfVHwv$v>Gaa!DB$jPq9BFvOUJ1-iK*c!Ay$_^R&H#6jes{ zv^>J4uu3xM#%jF?>Jtz19Dmd{9H+Y_{9m7I?zRX(UV5c+e(heGQ#2Mb#-2g-UW$|53-Q$u*@ev?>gsm`zl+@#E zZ~;W2=V4Jvffh!TW46!Y|b`Q(c!jBAYhO^gDf6UVz)A_!}v4k*c zg#9rUARmo#p?h<%FeVQYO#ayJq8C4tJ04>`MY~~Yxm!Z8455bx^pn}-H$E(Fvf)tD zmbDb&VjA78{e_XV-`(0abr-e%_xE?LIjMvCPEJ4Eo}cw^PA?$7@3h}XG(Q{{4hVYO&qt3^pGaMO`w_arP8k(q4`pG?&MuNjj! zs)p%9cVPzF1@+%ynC9wDlc3GlYIaw{-&>eL@BIky-x+rBtKk7MHhwU?%}@K5hDfU} zEUaJcf_`N#Q9~v^xpXy@-Rl1l{kFHTPPg|e(V|~vHVHe1VU>9Y!9i|R;tRJYh=x9i zhz=WP1IHNv^!#ekHM{r9abCE~Q_B||1WHif5o@dIRu(mA_FjP-;GI3su3`Uw?7eAo z8%KUH{#x@XI#!H!?-rjCGYL?Uu{gM`&OgKraW=@N^F$#NMj?8mUUY z+7=mKFz-fbdc3z#J0_vFWqH0qs7(ZV z)(Xdu_cck6AKyD<|85Wollg*mt{}=MedZ-!ERl@8CFtWspH9pQD%La**Rs~}N^5k} z14V>o=qjcU&h>(~zAtKzA3q|_-KN$&}cj ziv?eQS`@YSX7dX7l}*NvM{QFVI^R$W<)Ck7z~8EJC4MvVA^$Yp!@UpWVeo-IOx-f~ z5}fZ*ow+I<>oRF|336|v=?Yf)URK-p=*I>kz=kbc>dD?l%qX%waQ$2-AClq@!*OkKh z52>K;{phWpXapR4FL!i5RU$$H_as(F5U7x$dVy-6<>pnGZIKJ7cG;e@)i#kuFQjV31A+9<#S^W<g>Cuw=_pN*^OKSOc&Y` zK%1(i?h)~$$UO71O9KRiy(bJ{Fmz*-6TQdo|&+o)Pqcf6Dl{I!v(2Z?61H?12z4!rz+^w&P_ z)j0!XBZ)}S>)WR+gA!Ejy@(MV^~=<%M!fYrM8)vn_xC>H`zca>_s9mD!yBw3#PWKw z4g2R!)lvj8!u9Dl=P%#Bzj*mC=Vx!(dZ$7AR;sdxke|U&oJ3F9tR<0O)Vv2 z<}h}e0AQIaw^6D}?CU-hYDeCBxbM-NJiPa5w%Klg;+B+GLJt3)fX zPOUvM?kz(;YBMg1m4b7lP$*}DB1l5&-q`*)U^~!C%=o<76bkeCXwD?F08>ANf0~UP z1bKVMm5+`Q@<{ez9W^pEf}41@8wtV~D(Ak29IA*3IOYD5_qN?Ud>n9KoR6cw_awEY zdV5^L*LvKV&X8UxN+Z!HXy&QSJT-(jK5&e;08d+0S$99+>)tS736T%rLy4+NAV0FB}e!fi-EB&uk>2{ z1`DF%y}P4xXtQX92P3gNkxX^f~W<=O?0_VCo*4M$v|5j&b_vCW}w?vELq2{Q{NZ zR&*c2=_D(tt5O~dM}QSEm3frFY1pc_m`q@{S5>)vV~qP6 zP#N)I5|G3jBtd)ecFJmzh&gfA)c<@Va?pJOjTBQ(rSz8{z76qzozGiPFi&C_DChFv z%=fP!2p?fS5NXopKqxm$4MrG6QmY>uT@&665K8K%!t@t{t;VW8?4mVA0f>}MVgu2t zN)cez%X2*eLb?jdZ%igyDj{RQu`CKPYpgS?mN1B-a_7y$%&wHz^wNBnsRUsH?b?+% z)Ag)WXBX*FsNgZMLe{3KiqvV~OR6UG1G3!&K0R)v?5?=LRlNq4)khrkJl91?pOFL@ zX+^C{9}RtXG}(KWrz@}g^$ZXQ zI=mdK%IiO;sj5(Bwiq zOKeaRZixXmMO&Ys{EuwMGk0giNj}O)JJ=AR%`>VEPKdewK2=|@Rt+W0b3A7h=Q6=} zS%?FQM_@B2KG}yBiBy21Ac>6uD@i6t1jIo2fRqVjb-m20x{fhel$j{zy~yO2y1D|` zY}rio@1;bGurpok3%v#BOQOC$Tg4rHEUMO5W9nKN=BF1c_tBPUjMsvIzXEK4nC z&I+qnx|B7EPhpmb#wP)dAAX$mO*(=JGcAGf*FK&woJ}|OdX5bWrp7qyYPnv+!?ei8 zEyYT?r73RYRg+E~%9-bv6V4Ap-Eg}qCxX&FSax-0u_ytaqtF^SuMtZnixSxN)W^>U zv=@_?rc#sqM(_?P_DLMegXH`!sBpXBYAt`63PfE0=he%%Z!W%i_x9rDU!erhC;ESn z?w{Q2*8h1h`lbHQ-{j{Dpe3%>)5f)M4X9z3-M#x7VGVe{I=y>W{8kAkuNd$#9fHgO zC{&oHCPcXdnvQj#1#AI3v}tU)&9jTS*qTO6RjJjr!rzzj8bmV?uw)z!1#RI&<_2O- ztEiP-o672Gxy=e~XQi>I5~J=hbEKaO>B2z&983fG%&cP^}Ln z-?h{QD4OqOwFPp!cs~%ohrLL91y9&!y3*+K%V?3NCbt8Zkr%SsZsgYH;(ODGjUf+& z*s2mVElFzHg2W*0UA@E!L*gPL*3& z97%!1%`%HYWV%}9aAR`4uGLI~0xPSnu)zM!98ZldKA3e2T<=C!PDruAkiB_UH>E;e zy-?1Z{Wfw-6)mL8v+OM_%Sv4g_*7{KjB3N9B~7_~P}+)8U%~3h)UZte__|gK*!Dm;WAGrg1+4`WRjG!_%v`HlY+!#I1<;z-*hPVq8GxpT+7{g6 zoXrx8rj&KL71PFvN^Sh&=pj>;TP#z|D_2Me=V~KXrgBR-pu6HVWm%!UoFZh@x;8~K zV|z245@DsDhs}m6hHgpQnmKI6R4YrFif~_L3IDTX*r3FK-{yj;IVX4tr#`p=>UDMo zkO=a+v;ROg5g@e%479GzYVDB9#C3p*Rc!^LGC5<=Otg(+5%#Z_SSklW1UHn6OD9Ve zL%WEOAtf)k3kt<6=L9(jEaJJhfM?kWkx1qcmcd>Xd6tbjUvVCFfrmJgPAyEW|BY~8 zCVWoJOj*jQsX++En%rmpVIPQwC9(SwS4tdMPgG!WD>(R#QdjnH;8}@j zoP`?=cxY-XhC>*7isu5*2)0tdzBKp+K@m%1vJtOveGo!g-Yn@GQf&Jk=H6^Zm{e33?LJJ0R=a z4N&HcxKh;FF5ZEwCEAK$zN#}m#b@cNP#+kWVhIJ(LBzZP)ka)vwNbSl2+l}A3;NKk z6^4V&MWtr2^O~SryanW`7C`qELn0NUTIw~&Jz{`UQ*r)Aw?<1&uj2RQAtMgX>K&pe zYllg(J0BQ`Xkh?`oyw3zF_Fjp+eCejZjZUNPRx}mrgC;A3aUg>sB2Z4H88Cl7qnAL zCM>9L9h{|+1#Ue^j2ZnXnWcGo}1aq6nb?)pj+py^Mv2_P4izl!hW*L9UIvz3tt{w>>4q`p$oi-NHS_>2U86hhDJP^3OmTQxQMk znEJ|+^*irFm%HMJucWiR9$Of0I5V}1za3^mOeT}5#8w~ET5Uo)NO*=GfswaA#&Or% zwa!hwI9k^Rc73${@#rI?QH=X=`(qpp4m6I(<=j6qL@Z8@cfkXti2jxWo?VMLpHepq zJj*-*&-Cdov4Gmt{K`&p=HfZPz>fT&2; z3h-261Qdv4p04!JbZ0W`(cx=R;Yh-QOrNK8@GY7*LCz3wRb9)usaKtx5L$Bf!-V0S zpE{a+elUQml{x}x{-{01VI-zmP`5q1bH?*SF|8@FSxE8Ccjp}j%3Lc8OpB7rh*&ch z$gzLlO~c!@u4r_moql+f1-l$yyo=2KePQ6r-Y4wUBW%)rkcVSd`dbh{s|5`Mz-qIM zd;`|I;#-o4=SOme0qT`z`FRaMQftaw@+(E8^2(NJ}L^P$=?^srkc8d zGqr3o^}!c%y;idJPX@!RT!C~j6x7jn%BJ{92yq=x^$82$x*Xrz|Q(9e+#1;%lQqnV=CXewSNH5jy9{I6o$yPOYyy#wCY#4#ZPhd z@XK$Ps=^2#9?6n|DAWf{>xSEAZi(yillv71DB$}yZW#(-`914W5IQ$|QCX7E`gC zJ7_F)sz!_yx*7+)^QPX))Kp=pdPAA8wn*D5UN=0j}8_@700ArSHk|-fTTM`UEkS3 zT$QBst6EzkcOc+;^BS`fj6#jzV&-B8Py%4U3GuooKMSXnI4acjk#&WtYXJ``5XR_= zP)p|qvSl(nf_-NOlq}%*)6_a1)*Dl_3yp~pjOG~Lh!+P$aMGfo=cLHkEc+ItK59kT zBCOz8aulpqrt&x7oT@93ONZRJo{jr-N@-eI8duiCfEsf zh}G{4T~GK!DoIQB%_m;dg9&VS5;!4< z4~L#u0XEl2jV3pp6F_Sh(!xClkP8e0q>iZ2Q@G71K#!lMAVu|e1UtMsai`t;=t&x~ zcjs>hrq5f=k$S-uL4wTe+cYGEd^grbI94ApG;^cwT&n=dGAV-{M0L;VJq)voRg>oz zI5}vgJVNRNp+WLb!3vxBtgz}@)fl`Ff9 zw{}40)+gcR)~%Z$g}csLp4BQBAckK8Sn)9|VVSLNC3kus4vWrx=kt7}R;J!Q8Qp*I zLVxy4vvu=ibpLqeU+4!%_@)hryhZve28RD|{JWc%)(g3}dcYR90Pyz^OR>7r1p$^(5PCv=ejm(}$_j~GW8JpICT1f=-C=ND(^FE7vkD&7A+75_JWcyj-^8~=BFeD9a|zrV%L3-fQf zETwqeOiMiz+(cmznk9|$V~n|YrYM2HR2Kd|$;F@k_#bC6+Tc%r{7>%Hy;8EWLP=1p zt3qGvq5-wG7NuHXy*!7B9B6IFl5}zrduItuyiKrtqj1D^bB2w zt)uyO+ZrDiJZ>+zXQkbJl1-(54jl3x57~aLJM1MDH$O9lLOVSiI9m{xR8mOGvbDdZ zv_d*3i5&X7sf^PzwBVnASn8>E;whOp&(YzFW<(s{%qd}}J23_~(O54`0ScQ8r+>o> zx+1^=Es{f_sGUz8E$Xm_TA4W>$$dPMiz+Fu^IOm}E)~cgvfg@#w{T4Hl=#GC9xb4s zb29!=$ap?Q<`9^pKc=s|lGMGae-x(XiKofLJ8-a(9BviEP@0PT8!E;F`V>4r{YicT zD8yaFJlqF_$3QmYxVypba54NFx6!Up>UcWD&df)8>emvR*{%7rrnQK(Y66yD(}WE!hvQBhi~9QJ}S^G~25 zEUQwRPb@9i72xq+FgYQX`C8(oot09r;Dm^%Cp3j;PR?viugu_;1A90~2p_;S#u|av zh8rAk-pjyOr5Nydc(ZE+^v2R)mf$~55up+|9n#>G+QCkFw4kc#YF_JVp$eiPz61z^ zSeM3Z8GzBs$U~uzXCSP?HYEdL=5wl=i%G2#0Gn|FXx>2R)f#hIVWwJEc8>uGLh|zp+8k&9G)N5mv_5DV9xFX2YxFCKP z7Db0}AP+fOf1~Pw_Y`5sKmCsE023U2Z~vu=DGY)tRLF($%|{WNX^)XpaPCYkacmw4 zdE`a|NSRt3ObcWvHCh7O2Vh;&UNKfSt^RJr+6v1_3(I*h%`)EiCPtE zCF`rhz@t7za53?45HkY?fMAK-HwYsk#%%#m?7EcO0bI)*W#&R#)~zU;AY3mti!#~) zVGQG(wUdIF++mY4(Nu)jfCAELJ0MVRF@2&uaXfFOlOhq|{t+erz+&k{Tbwb#L+3bP z`vW=@U>M9Td=xhR1eT#X45PjIz&mbY8DYSWvW^jGp6BzqqA2TUAV$)gZB7?vL5BE(e9%!<)Tg<0k($ET;mrO1#5c!O!WU<$g4puoFpAFqu4*n;07=9whdkWi61 zGw7K0Aa^uN-@6*O_i-telEFw6qb7&Q8ej>h;y@n)=_5l0O5seNbX_2VZvXVhf46N? zk}BifsfjOWeb{GAxsjNp3Yhh$f55ImX#=I`;93}raqckL_^h9H1ep{rVCTZU?|Euf zHB%p5avp#^LI=iHpf*S{E}SQWs1Jwoblwg^YB>Mtw^N#hA+0(^csSK~9-I{vQbxE# z@VeXvS82--lU8m?>C8C4cZg_xFBg|V;R|*T-r`HsS_jbCR`*Qipc1rg(!*f+2vxJC z(IKTsBt)j)dOfV3i7+IXTWphX8(k7K&ZV1e)*Gs$Oa5*l*MK zW<07f9o79@yiw~~Syd4gyh*;O{Zlrz*TnBR)!E*}UJxLWy6FXm|9Fe%fjuO~gOtEF zbC8&@J0ZcEw+cish+ph#KZye0)!j$=gIs(qbuHdm)d%lv`v8Hb;(A@EZl;j`z0c(4 z%UxruElSQr*?GMsV@6?Su15QenTxrk!m|VVtx%FRz?#VfK=idN;dpVS5A&A%hhtxQ zC*K$prg1)kf5yb^$N4?-8HJZZ8~$C^)fx_{JP4uVoHHS<#tK9l5M~T==d*;>APkZA z1d0={>}grH7*a3$>9HuJ3cjqieHK!LHB6313%VfTh^)Byaj>Tk#Hz6v)F0rUO*kI- z+pG`X#y2QdP}FLs*SZbh-32qJo%iEA@I&~l^5CcX=cX%LnoY3cZqX%YrcR=n4xs3U z1#{|xbWW|-BuSN^_p788D{SdcfBg3j?d~x^jzPlf-gd{%$v1w16fvcMefQn9 zkGrL+63KvNvd(*WCup0Us;=?34~7CmleT4zPH5c;o%ly?yrs;EorN#W<|EhCYr+o} zM7$Jf4RyUCofltsdZntaWi-OT?3AzKwT1or>iXPTQF@4^~Hnx_A?R1n54>!qW z;flMP>|qxzs7=)bi*^~JnvUNr^c*909lbyB(dR|PC;v{W#q>E+g5;6FIgw&)AjX{( z+VCAu`=fB)Ck6xPk9R@8rOEMm`om;$gry|nxwhW$Gx@_zLJ4shR*ecJL) z=T^KO();Q<_E^DbQ|YpN5O6BXc3-67IHv@yUnylXl_GOqcXoq$Ru3t z=eVi{?Y)wGW8+P@RDUXWLHtG?D8hK69`4MKHs~~SI{;iT_$KGNl3T#yK-9Ib7d;U3 zrbagZc@pF9@`5ckISjI`pQm<5vpu3SZh#_QFjJA9m7;CAy0ODa%XOp|i^`di|8Mi?##A62h zg6-3W`}W6#l-y08pCaxR6j`-W#a3KZW>bkdSDet5oXwhA&NSMX#~@KZC8^LO*VCw6 zyoGD+l9Ht{mJ)S=fHL|R&kLlOD@A2X;^}e8I;JDKD%1+w^x~<8Dzej3FJjTi6l>e8 zulMkB@vYgYYgka=AO^4jEgYsIdIm=^RZCgUeYte(Z%iMBTkR-+iSUJ1J`k~|)|3sF z_`yO5K1_{lg-niiwjXysxM}M^Ztz@*bbW6cmOn~UsVEn)`BW}y#T~eD;Q5wInzT$z zWYJ&+H_WN4SyS^gI~Ki;I=Saf5;cHz;#MM0_eD}76%49rAKKw|%hckPU|ftQC;J?Y z^Z}+1S@8R&*0!Jup;R>EqDpF%o)NElMm10`opcRJ>2ENjJZO{`b1rkCS0ObW&XK8j z%JLHLX|EuXdCruW6ETy6n~UBgo)@%6OKTj6-!}!?<}(y&3IKG}bN#w0^xs43DLN{; ztog;jUedDibO1rmebxdw?Yuu)D%xIZg*p8&2mmsQo|fgN zfoQ0554NMMoGy0=7|S6afU#*TkyI68u1V|)$76{pkO^%NfSjBvrdk-Puwb=!*6SYU z2PM2YO!x)@Ok|IPj;f(Qsu_8bg419}$-?$gFwTQxQOK=t!4@nyc<@-<1=B|3xVbk( z)XI4N=Qs87xv*E)OlKj-b<^4x~iD>}L;70U!hR%EILzuiQ(+bv${b_C>=c!|^*Ai`) zGNpQWG2@)l_7q4&tN}%u+8)`8`xJwBKOcGj5sav)-L3ZxFm$2NMcQq8J)S?ohNu^j zRb{Da>+`@7rl(3VhG8YFa+~_35R(|>TI2Q5Q4g-sXy~^_r(v7=T3@U5n>@=NpxJlr%ae{1zKz$mnX)!a_`Fn}(EOL#Z!M|2(scY{Z!*o9`#7PA) zTc8wfdS)u?w8NlrSqZq{3ga7NW)QiSQ^#^+@of4xOg+<=TqST95=p8Ccso0j0)7dtC$^rCsnHC3o2QLzXBg+u}LAxbe=iL?F@({!OK z2ft(ez>2+qaC3!R$%XQsQ3Qsigk538jYrX5{PeFKG#*P!kBcRW15#S5KU+a%jJRh5jf?SD z6vx)yY=oacE_xt{4V0Kn?}ut@`O#_Qj|oX)_6QKzOIC=pPthh8ge~m3!L_nmY>FNB z(;CYYlR+qyy=-ddfl#(~pEOMXN=wO!E2%l27;Ajgu<12WHc({Y6sC>EUN{+xUwqJE zkpu;pN*N8*rQBFwwYjy17AFlwJH0D;W~zCqXKrZbL%v>8k}R51m!T|C68VGkBlOfH z7s&*U5<>|U)=Gs(A_}RLnP=JKT)fbBrpi)Q$~0svYhfC1oeaxfl9vCnOxdJjumNwF!^-U|^q__O)rM&=9`mFSAZv<^e z(8QZg{Io=HZP@L0*8vKWrW8J6mdCpx(#aPguT|v>!38xCbv$%S1?zL9(LoQu_u9cN z0i1nM85xf_+%P&2!WzSiO(Ni1IHAszm`)GJ&qZpewz_Ora1@zOAICAfVOaQ^)9 zd9dACZuH}~ZY0T7iGdG2IekekU-u@x&Do~~=2;f zS+Z?>zA>Yh4R~Ud9}|;HB5IU)@%p*%@|Dmgx_}}veo-MmZp^?>a_k3C){r5zbZ&im zbhM(?U}3Zuub&@{^5df%>z3+}Ru^0!D4kjz`fOn$f6S_+0_0I=4mAa5LEXZiN3*ObDNTVsqIqIU1 z6{~D{w{ggdNMPqHG2A1Q^>d-AGa^5J^SA>FCz|UjNe27`)FHh6kF1UGzu|}KKfI7v z3ee!MGsnm4KO8?8k00#Rf4DdLrT)X;;^*!8<=gicFP?sL{_7{*8t&h`diBlo^Y>?O z&YzvXe0%Zq`Q^?t`R}c%va{!}-aWf~_TBrlS8vYWzkc)TU(V0oww^82^>B3W!Tpna z*%#Q-o{O$ZVRf}AxxC|0sCJocq*mU5^-Azatms$o&*Q|WB>^z7{X_1mW}&(7b& zE-rulB(0471<-ZSVz6{}(jkFn&a-zfFW%CN@4voy{`UOMub@7~Gc0`E{AC%@Eg zjcqRNOswNdS4AkL{ruJ0)93GZ*M}u;V1o2%6y37#E?z!+fA;*{<=gW&?_WNB(YlWC z%GIMFu+SUdJ-U4VoAWo97q4Fa`pNZ4K03}v@HAzLIwjzKsIRa@29=ga&Bdjn%^bZs zfBNjjdA{N-PE#xPG}}C*hle!CX?6?i5z$}61$Dyp6`94a{9p7vfky^Y46!A_X?E+M z+f4_s(7h919z;ky#8w0cXYbxTyLfZ>@oohEDV_xGIhg~?>EjIAiQ%>I)%0Tp{r0>F zD>(TMbR3HKR$99qr-k=UMOXp-mO>+ZzzQy@JlzXF*agnN_i29K z&Ux`ePvEjI68qLCw;mq-@3#5R2BptCrLfQRoKR_Uvc~3lcBdsv7wxkzSTVw?;GEIf zX-`AwPIZ$Vmqs3@1BKaib#1Oxk+q3J^;uM5F)K||P%l;NAtash)k}xI1?zBlrM5#L z#r=1~h;l3R4>|hsr|yh@xZ#+>ar(1L0PVkgOc88%Pig1=PXAJ;a4TR0#&3vr7mD<2 z@V~#qdXQ`~Q6T(~cDOhcCapEq?wP zP@JZx#rRbKZD58DU&C=%rckFmQX-uZ!5N~Nu5TVe-!skW2tP13lOq(fvRmA0gu9Q3 z#;PHfwE5t0A;Z2Yh+Uyq1Wv3avC`+QW8z2sRzJ4(pMMsoS(d;4_SMVpPen^!+>v>r zd=#7T?+~yS7`4$hd29z?clgxbTf6%2|3mtJ5dZ%wb9_SoAKyFa)_**CaPOD?&wrDj zFVa4kXP_Kr*{{UuMxv+mRJ1xk-@SYK=XHP<7ju#QS6V>h`Uc|tZX;;n8$rJpSzjyY zANqSi|L$NVb?MA$|JR4U8E>ty!-lE1vBnn`w3u@Vn{VN@GgGhAwnbgYP4_2(2(&=@H-q&m!MI=Ma3$(3~u;tg)BYSj6y)!$#8Q)T9m!rnTaCS>otAY68+rHjzM~A^r zlXRVgTicqZ&UX8m<>FE~$4$VJuVG_&aMT?Ed$3}3!OW~Nk{FMc$*XU_i{3l$xa%9N zbw1ojB)FMF?v<>mEYV$Y_b$FR&X529yLZJfROZL{0<8CFTSXSWrHazlsiLQwvY=Yr z^%aK-`ihZZ1ygj(1Zc%NSTq`k*cuzxoAU~jeV*he_dai-$xFOkTwi<1KH?!P=u>M) zu{I;$v>z5g>YbGfg`ngc{khnNy6d^csRroSfv3Lp)I*?oERqdGeh{IlemRdkp)Q;= zQ(050?92ETj<}4YrGU!~rET7#RoU(E=WPsKm?~<V!<4>hJ(Y^heOtZRs9hH~lEoOp(wYqD&o;ofDv~I08iNQ%woy|+?0sni zZ0j&3Un^6qEac4ahJ9PX*z59tuYZcyLbP= z$uIKX-{R*B#!z#u>YAG30j?(99l2|P<>1kEX||+v2YPEN@oKHAOMsj#qm}}vnGgY- zP6bAcWxgvde97(rie~Y|@>JZe)k0fWZ}XX{DnRsF6-()QdwX6zvbS*rCl{yU_Hll4 zFFzSJQ++$bHvXz|7k~aV=@cjV" has a valid identity as "" @@ -30,6 +30,6 @@ Feature: Chart verifier comes back with a failure @redhat @full Examples: - | vendor_type | vendor | chart_path | - | redhat | redhat | tests/data/vault-0.17.0.tgz | + | vendor_type | vendor | chart_path | + | redhat | redhat | tests/data/vault-0.17.0.tgz | diff --git a/tests/functional/behave_features/HC-04_invalid_url_in_the_report.feature b/tests/functional/behave_features/HC-04_invalid_url_in_the_report.feature index 884d326fe7..e9b01e21b9 100644 --- a/tests/functional/behave_features/HC-04_invalid_url_in_the_report.feature +++ b/tests/functional/behave_features/HC-04_invalid_url_in_the_report.feature @@ -3,42 +3,23 @@ Feature: Report contains an invalid URL Scenario Outline: [HC-04-001] A user submits a report with an invalid url Given the vendor "" has a valid identity as "" - And a "" is provided - And the report contains an "" + And report is used in "" When the user sends a pull request with the report Then the pull request is not merged And user gets the "" in the pull request comment @partners @smoke @full Examples: - | vendor_type | vendor | report_path | invalid_url | message | - | partners | hashicorp | tests/data/report.yaml | example.com/vault-0.13.0.tgz | Missing schema in URL | - - @redhat @smoke @full - Examples: - | vendor_type | vendor | report_path | invalid_url | message | - | redhat | redhat | tests/data/report.yaml | htts://example.com/vault-0.13.0.tgz | Invalid schema | - - @community @smoke @full - Examples: - | vendor_type | vendor | report_path | invalid_url | message | - | community | redhat | tests/data/report.yaml | https:example.comvault-0.13.0.tgz | Invalid URL | - - @partners @full - Examples: - | vendor_type | vendor | report_path | invalid_url | message | - | partners | hashicorp | tests/data/report.yaml | htts://example.com/vault-0.13.0.tgz | Invalid schema | - | partners | hashicorp | tests/data/report.yaml | https:example.comvault-0.13.0.tgz | Invalid URL | + | vendor_type | vendor | report_path | message | + | partners | hashicorp | tests/data/HC-04/partner/report.yaml | Missing schema in URL | @redhat @full Examples: - | vendor_type | vendor | report_path | invalid_url | message | - | redhat | redhat | tests/data/report.yaml | example.com/vault-0.13.0.tgz | Missing schema in URL | - | redhat | redhat | tests/data/report.yaml | https:example.comvault-0.13.0.tgz | Invalid URL | + | vendor_type | vendor | report_path | message | + | redhat | redhat | tests/data/HC-04/redhat/report.yaml | Missing schema in URL | @community @full Examples: - | vendor_type | vendor | report_path | invalid_url | message | - | community | redhat | tests/data/report.yaml | example.com/vault-0.13.0.tgz | Missing schema in URL | - | community | redhat | tests/data/report.yaml | htts://example.com/vault-0.13.0.tgz | Invalid schema | + | vendor_type | vendor | report_path | message | + | community | redhat | tests/data/HC-04/community/report.yaml | Missing schema in URL | diff --git a/tests/functional/behave_features/HC-06_provider_delivery_control.feature b/tests/functional/behave_features/HC-06_provider_delivery_control.feature index 9f9a5dc9e0..d4068001d3 100644 --- a/tests/functional/behave_features/HC-06_provider_delivery_control.feature +++ b/tests/functional/behave_features/HC-06_provider_delivery_control.feature @@ -7,45 +7,41 @@ Feature: Report only submission with provider control settings Scenario Outline: [HC-06-001] A partner associate submits an error-free report with provider controlled delivery Given the vendor "" has a valid identity as "" And provider delivery control is set to "" in the OWNERS file - And a "" is provided - And provider delivery control is set to "" in the report + And 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 "" is updated with an entry for the submitted chart @partners @smoke @full Examples: - | vendor_type | vendor | report_path | index_file | provider_control_owners | provider_control_report | - | partners | hashicorp | tests/data/report.yaml | unpublished-certified-charts.yaml | true | true | + | vendor_type | vendor | report_path | index_file | provider_control_owners | + | partners | hashicorp | tests/data/HC-06/partner/report.yaml | unpublished-certified-charts.yaml | true | @external-feedback Scenario Outline: [HC-06-002] A partner associate submits an error-free report and chart with provider controlled delivery Given the vendor "" has a valid identity as "" And provider delivery control is set to "" in the OWNERS file And an error-free chart tarball used in "" and report in "" - And provider delivery control is set to "" in the report When the user sends a pull request with the report Then the pull request is not merged And user gets the "" in the pull request comment @partners @full Examples: - | vendor_type | vendor | chart_path | report_path | provider_control_owners | provider_control_report | message | - | partners | hashicorp | tests/data/vault-0.17.0.tgz | tests/data/report.yaml | true | true | OWNERS file and/or report indicate provider controlled delivery but pull request is not report only. | + | vendor_type | vendor | chart_path | report_path | provider_control_owners | message | + | partners | hashicorp | tests/data/vault-0.17.0.tgz | tests/data/HC-06/partner/report.yaml | true | OWNERS file and/or report indicate provider controlled delivery but pull request is not report only. | @external-feedback Scenario Outline: [HC-06-003] A partner associate submits an error-free report with inconsistent provider controlled delivery setting Given the vendor "" has a valid identity as "" And provider delivery control is set to "" in the OWNERS file - And a "" is provided - And provider delivery controls is set to "" and a package digest is "" in the report + And report is used in "" When the user sends a pull request with the report Then the pull request is not merged And user gets the "" in the pull request comment @partners @full Examples: - | vendor_type | vendor | report_path | provider_control_owners | provider_control_report | package_digest_set | message | - | partners | hashicorp | tests/data/report.yaml | true | false | true | OWNERS file indicates provider controlled delivery but report does not. | - | partners | hashicorp | tests/data/report.yaml | false | true | true | Report indicates provider controlled delivery but OWNERS file does not. | - | partners | hashicorp | tests/data/report.yaml | true | true | false | Provider delivery control requires a package digest in the report. | + | vendor_type | vendor | report_path | provider_control_owners | message | + | partners | hashicorp | tests/data/common/partner/report.yaml | true | OWNERS file indicates provider controlled delivery but report does not. | + | partners | hashicorp | tests/data/HC-06/partner/report.yaml | false | Report indicates provider controlled delivery but OWNERS file does not. | diff --git a/tests/functional/behave_features/HC-07_report_and_chart_src.feature b/tests/functional/behave_features/HC-07_report_and_chart_src.feature index dd69ada41c..1a84eb63b1 100644 --- a/tests/functional/behave_features/HC-07_report_and_chart_src.feature +++ b/tests/functional/behave_features/HC-07_report_and_chart_src.feature @@ -12,13 +12,13 @@ Feature: Chart source submission with report @partners @smoke @full Examples: - | vendor_type | vendor | chart_path | report_path | - | partners | hashicorp | tests/data/vault-0.17.0.tgz | tests/data/report.yaml | + | vendor_type | vendor | chart_path | report_path | + | partners | hashicorp | tests/data/vault-0.17.0.tgz | tests/data/common/partner/report.yaml | @redhat @full Examples: - | vendor_type | vendor | chart_path | report_path | - | redhat | redhat | tests/data/vault-0.17.0.tgz | tests/data/report.yaml | + | vendor_type | vendor | chart_path | report_path | + | redhat | redhat | tests/data/vault-0.17.0.tgz | tests/data/common/redhat/report.yaml | Scenario Outline: [HC-07-002] A community user submits an error-free chart source with report Given the vendor "" has a valid identity as "" @@ -29,5 +29,5 @@ Feature: Chart source submission with report @community @smoke @full Examples: - | vendor_type | vendor | chart_path | report_path | message | - | community | redhat | tests/data/vault-0.17.0.tgz | tests/data/report.yaml | Community charts require maintainer review and approval, a review will be conducted shortly | + | vendor_type | vendor | chart_path | report_path | message | + | community | redhat | tests/data/vault-0.17.0.tgz | tests/data/common/community/report.yaml | Community charts require maintainer review and approval, a review will be conducted shortly | diff --git a/tests/functional/behave_features/HC-08_report_and_chart_tar.feature b/tests/functional/behave_features/HC-08_report_and_chart_tar.feature index 69e9211fa9..7096d63295 100644 --- a/tests/functional/behave_features/HC-08_report_and_chart_tar.feature +++ b/tests/functional/behave_features/HC-08_report_and_chart_tar.feature @@ -12,13 +12,13 @@ Feature: Chart tarball submission with report @partners @smoke @full Examples: - | vendor_type | vendor | chart_path | report_path | - | partners | hashicorp | tests/data/vault-0.17.0.tgz | tests/data/report.yaml | + | vendor_type | vendor | chart_path | report_path | + | partners | hashicorp | tests/data/vault-0.17.0.tgz | tests/data/common/partner/report.yaml | @redhat @full Examples: - | vendor_type | vendor | chart_path | report_path | - | redhat | redhat | tests/data/vault-0.17.0.tgz | tests/data/report.yaml | + | vendor_type | vendor | chart_path | report_path | + | redhat | redhat | tests/data/vault-0.17.0.tgz | tests/data/common/redhat/report.yaml | Scenario Outline: [HC-08-002] A community user submits an error-free chart tarball with report Given the vendor "" has a valid identity as "" @@ -29,5 +29,5 @@ Feature: Chart tarball submission with report @community @smoke @full Examples: - | vendor_type | vendor | chart_path | report_path | message | - | community | redhat | tests/data/vault-0.17.0.tgz | tests/data/report.yaml | Community charts require maintainer review and approval, a review will be conducted shortly | + | vendor_type | vendor | chart_path | report_path | message | + | community | redhat | tests/data/vault-0.17.0.tgz | tests/data/common/community/report.yaml | Community charts require maintainer review and approval, a review will be conducted shortly | diff --git a/tests/functional/behave_features/HC-09_report_in_json_format.feature b/tests/functional/behave_features/HC-09_report_in_json_format.feature index 2de4a15c14..fd3abe4a5a 100644 --- a/tests/functional/behave_features/HC-09_report_in_json_format.feature +++ b/tests/functional/behave_features/HC-09_report_in_json_format.feature @@ -12,16 +12,16 @@ Feature: Report only submission in json format @partners Examples: - | vendor_type | vendor | report_path | message | - | partners | hashicorp | tests/data/report.json | One of these must be modified: report, chart source, or tarball | + | vendor_type | vendor | report_path | message | + | partners | hashicorp | tests/data/HC-09/partner/report.json | One of these must be modified: report, chart source, or tarball | @redhat Examples: - | vendor_type | vendor | report_path | message | - | redhat | redhat | tests/data/report.json | One of these must be modified: report, chart source, or tarball | + | vendor_type | vendor | report_path | message | + | redhat | redhat | tests/data/HC-09/redhat/report.json | One of these must be modified: report, chart source, or tarball | @community Examples: - | vendor_type | vendor | report_path | message | - | community | redhat | tests/data/report.json | One of these must be modified: report, chart source, or tarball | + | vendor_type | vendor | report_path | message | + | community | redhat | tests/data/HC-09/community/report.json | One of these must be modified: report, chart source, or tarball | diff --git a/tests/functional/behave_features/HC-10_report_only_edited.feature b/tests/functional/behave_features/HC-10_report_only_edited.feature deleted file mode 100644 index a922d1c69f..0000000000 --- a/tests/functional/behave_features/HC-10_report_only_edited.feature +++ /dev/null @@ -1,47 +0,0 @@ -Feature: Edited report only submission - Partners, redhat and community users attempt to publish their chart by submitting - report that was edited after it was generated by chart-verifier. - - Scenario Outline: [HC-10-001] A partner or redhat associate submits an edited report - Given the vendor "" has a valid identity as "" - And a "" is provided - And the report includes "" and "" OpenshiftVersion values and chart "" value - When the user sends a pull request with the report - Then the pull request is not merged - And user gets the "" in the pull request comment - - @partners @smoke @full - Examples: - | vendor_type | vendor | report_path | tested | supported | kubeversion | message | - | partners | hashicorp | tests/data/report.yaml | 4.9 | 4.6-4.9 | >=1.20.0 | is not a valid semantic version | - | partners | hashicorp | tests/data/report.yaml | 4.8 | >=4.7 | >=1.21.0 | does not match supportedOpenShiftVersions | - - @partners @full - Examples: - | vendor_type | vendor | report_path | tested | supported | kubeversion | message | - | partners | hashicorp | tests/data/report.yaml | 4.0 | >=4.7 | >=1.20.0 | is not a supported OpenShift version | - | partners | hashicorp | tests/data/report.yaml | 4.6 | >=4.7 | >=1.20.0 | not within specified kube-versions | - - @redhat @smoke @full - Examples: - | vendor_type | vendor | report_path | tested | supported | kubeversion | message | - | redhat | redhat | tests/data/report.yaml | 4.0 | >=4.7 | >=1.20.0 | is not a supported OpenShift version | - - @redhat @full - Examples: - | vendor_type | vendor | report_path | tested | supported | kubeversion | message | - | redhat | redhat | tests/data/report.yaml | 4.9 | 4.6-4.9 | >=1.20.0 | is not a valid semantic version | - | redhat | redhat | tests/data/report.yaml | 4.6 | >=4.7 | >=1.20.0 | not within specified kube-versions | - | redhat | redhat | tests/data/report.yaml | 4.8 | >=4.7 | >=1.21.0 | does not match supportedOpenShiftVersions | - - @community @smoke @full - Examples: - | vendor_type | vendor | report_path | tested | supported | kubeversion | message | - | community | redhat | tests/data/report.yaml | 4.6 | >=4.7 | >=1.20.0 | not within specified kube-versions | - - @community @full - Examples: - | vendor_type | vendor | report_path | tested | supported | kubeversion | message | - | community | redhat | tests/data/report.yaml | 4.9 | 4.6-4.9 | >=1.20.0 | is not a valid semantic version | - | community | redhat | tests/data/report.yaml | 4.0 | >=4.7 | >=1.20.0 | is not a supported OpenShift version | - | community | redhat | tests/data/report.yaml | 4.8 | >=4.7 | >=1.21.0 | does not match supportedOpenShiftVersions | \ No newline at end of file diff --git a/tests/functional/behave_features/HC-11_report_with_missing_checks.feature b/tests/functional/behave_features/HC-11_report_with_missing_checks.feature index 012a8aa6cc..d6a65e193f 100644 --- a/tests/functional/behave_features/HC-11_report_with_missing_checks.feature +++ b/tests/functional/behave_features/HC-11_report_with_missing_checks.feature @@ -3,25 +3,24 @@ Feature: Report does not include a check Scenario Outline: [HC-11-001] A user submits a report with missing checks Given the vendor "" has a valid identity as "" - And a "" is provided - And the report has a "" missing + And report is used in "" When the user sends a pull request with the report Then the pull request is not merged And user gets the "" in the pull request comment @partners @smoke @full Examples: - | vendor_type | vendor | report_path | check | message | - | partners | hashicorp | tests/data/report.yaml | v1.0/helm-lint | Missing mandatory check : v1.0/helm-lint | + | vendor_type | vendor | report_path | message | + | partners | hashicorp | tests/data/HC-11/partner/report.yaml | Missing mandatory check : v1.0/helm-lint | @community @full Examples: - | vendor_type | vendor | report_path | check | message | - | community | redhat | tests/data/report.yaml | v1.0/helm-lint | Missing mandatory check : v1.0/helm-lint | + | vendor_type | vendor | report_path | message | + | community | redhat | tests/data/HC-11/community/report.yaml | Missing mandatory check : v1.0/helm-lint | @partners @full Examples: - | vendor_type | vendor | report_path | check | message | - | partners | hashicorp | tests/data/report.yaml | v1.0/not-contains-crds | Missing mandatory check : v1.0/not-contains-crds | + | vendor_type | vendor | report_path | message | + | partners | hashicorp | tests/data/HC-11/partner_not_contain_crds/report.yaml | Missing mandatory check : v1.0/not-contains-crds | # Commented this scenario, since it is failing , raised bug : https://issues.redhat.com/browse/HELM-289 , we can uncomment again when the issue fixed #| redhat | redhat | tests/data/report.yaml |v1.0/helm-lint | Missing mandatory check : v1.0/helm-lint | \ No newline at end of file diff --git a/tests/functional/behave_features/HC-12_report_without_chart.feature b/tests/functional/behave_features/HC-12_report_without_chart.feature index d74dec2743..8cfe1699b5 100644 --- a/tests/functional/behave_features/HC-12_report_without_chart.feature +++ b/tests/functional/behave_features/HC-12_report_without_chart.feature @@ -8,16 +8,17 @@ Feature: Report only submission 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 + And a release is published with report only @partners @smoke @full Examples: - | vendor_type | vendor | report_path | - | partners | hashicorp | tests/data/report.yaml | + | vendor_type | vendor | report_path | + | partners | hashicorp | tests/data/common/partner/report.yaml | @redhat @full Examples: - | vendor_type | vendor | report_path | - | redhat | redhat | tests/data/report.yaml | + | vendor_type | vendor | report_path | + | redhat | redhat | tests/data/common/redhat/report.yaml | Scenario Outline: [HC-12-002] A community user submits an error-free report Given the vendor "" has a valid identity as "" @@ -28,5 +29,5 @@ Feature: Report only submission @community @smoke @full Examples: - | vendor_type | vendor | report_path | message | - | community | redhat | tests/data/report.yaml | Community charts require maintainer review and approval, a review will be conducted shortly | \ No newline at end of file + | vendor_type | vendor | report_path | message | + | community | redhat | tests/data/common/community/report.yaml | Community charts require maintainer review and approval, a review will be conducted shortly | \ No newline at end of file diff --git a/tests/functional/behave_features/HC-13_sha_value_does_not_match.feature b/tests/functional/behave_features/HC-13_sha_value_does_not_match.feature deleted file mode 100644 index 05d6e1d9de..0000000000 --- a/tests/functional/behave_features/HC-13_sha_value_does_not_match.feature +++ /dev/null @@ -1,26 +0,0 @@ -Feature: SHA value in the report does not match - Partners, redhat and community users submits chart tar with report - where chart sha does not match with sha value digests.chart in the report - - Scenario Outline: [HC-13-001] A user submits a chart tarball with report - Given the vendor "" has a valid identity as "" - And a chart tarball is used in "" and report in "" - And the report contains "" - When the user sends a pull request with the chart tar and report - Then the pull request is not merged - And user gets the "" in the pull request comment - - @partners @full - Examples: - | vendor_type | vendor | error | message | chart_path | report_path | - | partners | hashicorp | sha_mismatch | Digest is not matching | tests/data/vault-0.17.0.tgz | tests/data/report.yaml | - - @redhat @full - Examples: - | vendor_type | vendor | error | message | chart_path | report_path | - | redhat | redhat | sha_mismatch | Digest is not matching | tests/data/vault-0.17.0.tgz | tests/data/report.yaml | - - @community @full - Examples: - | vendor_type | vendor | error | message | chart_path | report_path | - | community | redhat | sha_mismatch | Digest is not matching | tests/data/vault-0.17.0.tgz | tests/data/report.yaml | diff --git a/tests/functional/behave_features/HC-14_user_submits_chart_with_errors.feature b/tests/functional/behave_features/HC-14_user_submits_chart_with_errors.feature index 96d07ade97..5e4a1a697e 100644 --- a/tests/functional/behave_features/HC-14_user_submits_chart_with_errors.feature +++ b/tests/functional/behave_features/HC-14_user_submits_chart_with_errors.feature @@ -11,18 +11,18 @@ Feature: Chart submission with errors @partners @smoke @full Examples: - | vendor_type | vendor | chart_path | message | user | - | partners | hashicorp | tests/data/vault-0.17.0.tgz | is not allowed to submit the chart on behalf of | unauthorized | + | vendor_type | vendor | chart_path | message | user | + | partners | hashicorp | tests/data/vault-0.17.0.tgz | is not allowed to submit the chart on behalf of | unauthorized | @redhat @full Examples: - | vendor_type | vendor | chart_path | message | user | - | redhat | redhat | tests/data/vault-0.17.0.tgz | is not allowed to submit the chart on behalf of | unauthorized | + | vendor_type | vendor | chart_path | message | user | + | redhat | redhat | tests/data/vault-0.17.0.tgz | is not allowed to submit the chart on behalf of | unauthorized | @community @full Examples: - | vendor_type | vendor | chart_path | message | user | - | community | redhat | tests/data/vault-0.17.0.tgz | is not allowed to submit the chart on behalf of | unauthorized | + | vendor_type | vendor | chart_path | message | user | + | community | redhat | tests/data/vault-0.17.0.tgz | is not allowed to submit the chart on behalf of | unauthorized | Scenario Outline: [HC-14-002] An authorized user submits a chart with incorrect version Given the vendor "" has a valid identity as "" @@ -35,15 +35,30 @@ Feature: Chart submission with errors @partners @smoke @full Examples: - | vendor_type | vendor | chart_path | message | bad_version | - | partners | hashicorp | tests/data/vault-0.17.0.tgz | doesn't match the directory structure | 9.9.9 | - + | vendor_type | vendor | chart_path | message | bad_version | + | partners | hashicorp | tests/data/vault-0.17.0.tgz | doesn't match the directory structure | 9.9.9 | + | partners | hashicorp | tests/data/vault-0.17.0.tgz | The chart verifier returned an error | abc-9.9.9 | + @redhat @full Examples: - | vendor_type | vendor | chart_path | message | bad_version | - | redhat | redhat | tests/data/vault-0.17.0.tgz | doesn't match the directory structure | 9.9.9 | + | vendor_type | vendor | chart_path | message | bad_version | + | redhat | redhat | tests/data/vault-0.17.0.tgz | doesn't match the directory structure | 9.9.9 | @community @full Examples: - | vendor_type | vendor | chart_path | message | bad_version | - | community | redhat | tests/data/vault-0.17.0.tgz | doesn't match the directory structure | 9.9.9 | + | vendor_type | vendor | chart_path | message | bad_version | + | community | redhat | tests/data/vault-0.17.0.tgz | doesn't match the directory structure | 9.9.9 | + + Scenario Outline: [HC-14-003] A user submits a chart with bad semantic version + Given the vendor "" has a valid identity as "" + And a chart source used in "" and directory structure contains "" + And Chart.yaml specifies a "" + And the user creates a branch to add a new chart version + When the user sends a pull request with the chart + Then the pull request is not merged + And user gets the "" in the pull request comment + + @partners @smoke @full + Examples: + | vendor_type | vendor | chart_path | message | bad_version | + | partners | hashicorp | tests/data/vault-0.17.0.tgz | Helm chart version is not a valid semantic version | abc-0.17.0 | diff --git a/tests/functional/behave_features/HC-18_multiple_charts_in_pr.feature b/tests/functional/behave_features/HC-18_multiple_charts_in_pr.feature new file mode 100644 index 0000000000..f27075c0c5 --- /dev/null +++ b/tests/functional/behave_features/HC-18_multiple_charts_in_pr.feature @@ -0,0 +1,113 @@ +Feature: Multiple charts submission in one PR + If partners, redhat and community users try to submit multiple charts in one single PR + they will get an appropriate error message + + Scenario Outline: [HC-18-001] A user submits a PR with multiple reports + Given the vendor "" has a valid identity as "" + And user wants to send two reports as in "" and "" + When the user sends a pull request with the report + Then the pull request is not merged + And user gets the "" in the pull request comment + + @partners @full @smoke + Examples: + | vendor_type | vendor | report_path_1 | report_path_2 | message | + | partners | hashicorp | tests/data/common/partner/report.yaml | tests/data/HC-18/partner/report.yaml | A PR must contain only one chart. Current PR includes files for multiple charts | + + @redhat @full + Examples: + | vendor_type | vendor | report_path_1 | report_path_2 | message | + | redhat | redhat | tests/data/common/redhat/report.yaml | tests/data/HC-18/redhat/report.yaml | A PR must contain only one chart. Current PR includes files for multiple charts | + + @community @full + Examples: + | vendor_type | vendor | report_path_1 | report_path_2 | message | + | community | redhat | tests/data/common/community/report.yaml | tests/data/HC-18/community/report.yaml | A PR must contain only one chart. Current PR includes files for multiple charts | + + Scenario Outline: [HC-18-002] A user submits a PR with multiple chart sources + Given the vendor "" has a valid identity as "" + And user wants to send two chart sources as in "" and "" + When the user sends a pull request with the chart + Then the pull request is not merged + And user gets the "" in the pull request comment + + @partners @full + Examples: + | vendor_type | vendor | chart_path_1 | chart_path_2 | message | + | partners | hashicorp | tests/data/vault-0.17.0.tgz | tests/data/vault-0.18.0.tgz | A PR must contain only one chart. Current PR includes files for multiple charts | + + @redhat @full + Examples: + | vendor_type | vendor | chart_path_1 | chart_path_2 | message | + | redhat | redhat | tests/data/vault-0.17.0.tgz | tests/data/vault-0.18.0.tgz | A PR must contain only one chart. Current PR includes files for multiple charts | + + @community @full + Examples: + | vendor_type | vendor | chart_path_1 | chart_path_2 | message | + | community | redhat | tests/data/vault-0.17.0.tgz | tests/data/vault-0.18.0.tgz | A PR must contain only one chart. Current PR includes files for multiple charts | + + Scenario Outline: [HC-18-003] A user submits a PR with multiple chart tars + Given the vendor "" has a valid identity as "" + And user wants to send two chart tars as in "" and "" + When the user sends a pull request with the chart + Then the pull request is not merged + And user gets the "" in the pull request comment + + @partners @full + Examples: + | vendor_type | vendor | chart_path_1 | chart_path_2 | message | + | partners | hashicorp | tests/data/vault-0.17.0.tgz | tests/data/vault-0.18.0.tgz | A PR must contain only one chart. Current PR includes files for multiple charts | + + @redhat @full @smoke + Examples: + | vendor_type | vendor | chart_path_1 | chart_path_2 | message | + | redhat | redhat | tests/data/vault-0.17.0.tgz | tests/data/vault-0.18.0.tgz | A PR must contain only one chart. Current PR includes files for multiple charts | + + @community @full + Examples: + | vendor_type | vendor | chart_path_1 | chart_path_2 | message | + | community | redhat | tests/data/vault-0.17.0.tgz | tests/data/vault-0.18.0.tgz | A PR must contain only one chart. Current PR includes files for multiple charts | + + Scenario Outline: [HC-18-004] A user submits a PR with multiple chart one with source and other with report + Given the vendor "" has a valid identity as "" + And user wants to send two charts one with source "" and other with report "" + When the user sends a pull request with the chart and report + Then the pull request is not merged + And user gets the "" in the pull request comment + + @partners @full + Examples: + | vendor_type | vendor | chart_path | report_path | message | + | partners | hashicorp | tests/data/vault-0.17.0.tgz | tests/data/HC-18/partner/report.yaml | A PR must contain only one chart. Current PR includes files for multiple charts | + + @redhat @full + Examples: + | vendor_type | vendor | chart_path | report_path | message | + | redhat | redhat | tests/data/vault-0.17.0.tgz | tests/data/HC-18/redhat/report.yaml | A PR must contain only one chart. Current PR includes files for multiple charts | + + @community @full @smoke + Examples: + | vendor_type | vendor | chart_path | report_path | message | + | community | redhat | tests/data/vault-0.17.0.tgz | tests/data/HC-18/community/report.yaml | A PR must contain only one chart. Current PR includes files for multiple charts | + + Scenario Outline: [HC-18-005] A user submits a PR with multiple chart one with tar and other with report + Given the vendor "" has a valid identity as "" + And user wants to send two charts one with tar "" and other with report "" + When the user sends a pull request with the chart and report + Then the pull request is not merged + And user gets the "" in the pull request comment + + @partners @full + Examples: + | vendor_type | vendor | chart_path | report_path | message | + | partners | hashicorp | tests/data/vault-0.17.0.tgz | tests/data/HC-18/partner/report.yaml | A PR must contain only one chart. Current PR includes files for multiple charts | + + @redhat @full + Examples: + | vendor_type | vendor | chart_path | report_path | message | + | redhat | redhat | tests/data/vault-0.17.0.tgz | tests/data/HC-18/redhat/report.yaml | A PR must contain only one chart. Current PR includes files for multiple charts | + + @community @full + Examples: + | vendor_type | vendor | chart_path | report_path | message | + | community | redhat | tests/data/vault-0.17.0.tgz | tests/data/HC-18/community/report.yaml | A PR must contain only one chart. Current PR includes files for multiple charts | diff --git a/tests/functional/behave_features/HC-19_report_sha.feature b/tests/functional/behave_features/HC-19_report_sha.feature new file mode 100644 index 0000000000..6abcb04fd1 --- /dev/null +++ b/tests/functional/behave_features/HC-19_report_sha.feature @@ -0,0 +1,34 @@ +Feature: Report sha value in report + Users attempt to publish their chart by submitting report with a report sha value. + The sha value must match the report. + + Scenario Outline: [HC-19-001] A partner or redhat associate submits an error-free report with report sha value + 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 @reportSha + Examples: + | vendor_type | vendor | report_path | + | partners | hashicorp | tests/data/HC-19/report_sha_good/report.yaml | + + + + Scenario Outline: [HC-19-002] A partner or redhat associate submits a report with invalid report sha value + 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 pull request is not merged + And user gets the "" in the pull request comment + + @partners @full @smoke @reportSha + Examples: + | vendor_type | vendor | report_path | message | + | partners | hashicorp | tests/data/HC-19/report_sha_bad/report.yaml | digest in report did not match report content | + + @partners @full @reportSha + Examples: + | vendor_type | vendor | report_path | message | + | partners | hashicorp | tests/data/HC-19/report_edited_sha_bad/report.yaml | digest in report did not match report content | diff --git a/tests/functional/behave_features/common/utils/chart.py b/tests/functional/behave_features/common/utils/chart.py index 45d5938a67..3aeec916ec 100644 --- a/tests/functional/behave_features/common/utils/chart.py +++ b/tests/functional/behave_features/common/utils/chart.py @@ -6,6 +6,39 @@ import yaml import shutil import json +from enum import Enum +from dataclasses import dataclass + +class Chart_Type(Enum): + SRC = 1 + TAR = 2 + REPORT = 3 + SRC_AND_REPORT = 4 + TAR_AND_REPORT = 5 + +class Release_Type(Enum): + CHART_ONLY = 1 + REPORT_ONLY = 2 + CHART_AND_REPORT = 3 + REPORT_AND_KEY = 4 + CHART_PROV_AND_REPORT = 5 + CHART_REPORT_PROV_AND_KEY = 6 + +@dataclass +class Chart: + chart_file_path : str = '' + report_file_path : str = '' + chart_name: str = '' + chart_version: str = '' + chart_directory: str = '' + chart_type: Chart_Type = None + + def update_chart_directory(self, secrets): + base_branch_without_uuid = "-".join(secrets.base_branch.split("-")[:-1]) + vendor_without_suffix = secrets.vendor.split("-")[0] + secrets.base_branch = f'{base_branch_without_uuid}-{secrets.vendor_type}-{vendor_without_suffix}-{self.chart_name}-{self.chart_version}' + secrets.pr_branch = f'{secrets.base_branch}-pr-branch' + self.chart_directory = f'charts/{secrets.vendor_type}/{secrets.vendor}/{self.chart_name}' def get_name_and_version_from_report(path): """ @@ -75,7 +108,7 @@ def get_name_and_version_from_chart_src(path): raise AssertionError(f"error parsing '{path}': {err}") return chart_yaml['name'], chart_yaml['version'] -def extract_chart_tgz(src, dst, secrets, logger): +def extract_chart_tgz(src, dst, chart_name, logger): """Extracts the chart tgz file into the target location under 'charts/' for PR submission tests Parameters: @@ -90,7 +123,7 @@ def extract_chart_tgz(src, dst, secrets, logger): finally: with tarfile.open(src, 'r') as fd: fd.extractall(dst) - os.rename(f'{dst}/{secrets.chart_name}', f'{dst}/src') + os.rename(f'{dst}/{chart_name}', f'{dst}/src') def get_all_charts(charts_path: str, vendor_types: str) -> list: # TODO: Support `community` as vendor_type. diff --git a/tests/functional/behave_features/common/utils/chart_certification.py b/tests/functional/behave_features/common/utils/chart_certification.py index 6b023d64fa..b56079b9c3 100644 --- a/tests/functional/behave_features/common/utils/chart_certification.py +++ b/tests/functional/behave_features/common/utils/chart_certification.py @@ -8,8 +8,9 @@ import logging import time import uuid +import base64 from tempfile import TemporaryDirectory -from dataclasses import dataclass +from dataclasses import dataclass, field from string import Template from pathlib import Path @@ -23,6 +24,7 @@ from common.utils.set_directory import SetDirectory from common.utils.setttings import * from common.utils.chart import * +from common.utils.env import * @dataclass class ChartCertificationE2ETest: @@ -30,7 +32,7 @@ class ChartCertificationE2ETest: chart: name: ${chart_name} shortDescription: Test chart for testing chart submission workflows. -publicPgpKey: null +publicPgpKey: ${public_key} providerDelivery: ${provider_delivery} users: - githubUsername: ${bot_name} @@ -55,21 +57,6 @@ def set_git_username_email(self, repo, username, email): repo.config_writer().set_value("user", "name", username).release() repo.config_writer().set_value("user", "email", email).release() - def get_bot_name_and_token(self): - bot_name = os.environ.get("BOT_NAME") - logging.debug(f"Enviroment variable value BOT_NAME: {bot_name}") - bot_token = os.environ.get("BOT_TOKEN") - if not bot_name and not bot_token: - bot_name = "github-actions[bot]" - bot_token = os.environ.get("GITHUB_TOKEN") - if not bot_token: - raise Exception("BOT_TOKEN environment variable not defined") - elif not bot_name: - raise Exception("BOT_TOKEN set but BOT_NAME not specified") - elif not bot_token: - raise Exception("BOT_NAME set but BOT_TOKEN not specified") - return bot_name, bot_token - def remove_chart(self, chart_directory, chart_version, remote_repo, base_branch, bot_token): # Remove chart files from base branch logging.info( @@ -135,10 +122,17 @@ def send_pull_request(self, remote_repo, base_branch, pr_branch, bot_token): raise AssertionError(f"error sending pull request, response was: {r.text}") return j['number'] - def create_and_push_owners_file(self, chart_directory, base_branch, vendor_name, vendor_type, chart_name, provider_delivery=False): + def create_and_push_owners_file(self, chart_directory, base_branch, vendor_name, vendor_type, chart_name, provider_delivery=False, public_key_file=None): with SetDirectory(Path(self.temp_dir.name)): # Create the OWNERS file from the string template - values = {'bot_name': self.secrets.bot_name, + if public_key_file != None: + with open(public_key_file, 'r') as f: + content = f.read() + encoded_content = content.encode('utf-8') + public_key_value = base64.b64encode(encoded_content).decode('utf-8') + else: + public_key_value = 'null' + values = {'bot_name': self.secrets.bot_name, 'public_key': public_key_value, 'vendor': vendor_name, 'chart_name': chart_name, "provider_delivery" : provider_delivery} content = Template(self.secrets.owners_file_content).substitute(values) @@ -203,17 +197,34 @@ def check_index_yaml(self,base_branch, vendor, chart_name, chart_version, index_ else: return False - def check_release_result(self, vendor, chart_name, chart_version, chart_tgz, failure_type='error'): + def check_release_result(self, vendor, chart_name, chart_version, chart_tgz, failure_type='error', release_type=Release_Type.CHART_ONLY): expected_tag = f'{vendor}-{chart_name}-{chart_version}' try: release = get_release_by_tag(self.secrets, expected_tag) logging.info(f"Released '{expected_tag}' successfully") - expected_chart_asset = f'{vendor}-{chart_tgz}' - required_assets = [expected_chart_asset] + required_assets = [] + if release_type == Release_Type.CHART_ONLY: + required_assets.append(chart_tgz) + elif release_type == Release_Type.REPORT_ONLY: + required_assets.append('report.yaml') + elif release_type == Release_Type.CHART_AND_REPORT: + required_assets.extend([chart_tgz, 'report.yaml']) + elif release_type == Release_Type.REPORT_AND_KEY: + key_file = chart_name + '-' + chart_version + '.tgz' + '.key' + required_assets.extend(['report.yaml', key_file]) + elif release_type == Release_Type.CHART_PROV_AND_REPORT: + prov_file = chart_tgz + '.prov' + required_assets.extend([chart_tgz, 'report.yaml', prov_file]) + elif release_type == Release_Type.CHART_REPORT_PROV_AND_KEY: + key_file = chart_tgz + '.key' + prov_file = chart_tgz + '.prov' + required_assets.extend([chart_tgz, 'report.yaml', prov_file, key_file]) + else: + sys.exit('Trying to check wrong release type') logging.info(f"Check '{required_assets}' is in release assets") release_id = release['id'] - get_release_assets(self.secrets, release_id, required_assets) + check_release_assets(self.secrets, release_id, required_assets) return True except Exception as e: if failure_type == 'error': @@ -326,9 +337,11 @@ def check_pull_request_labels(self, pr_number): @dataclass class ChartCertificationE2ETestSingle(ChartCertificationE2ETest): test_name: str = '' # Meaningful test name for this test, displayed in PR title - test_chart: str = '' - test_report: str = '' - chart_directory: str = '' + test_charts: list[Chart] = field(default_factory=list) + #test_report: str = '' + #chart_directory: str = '' + uuid: str = '' + head_sha: str = '' secrets: E2ETestSecretOneShot = E2ETestSecretOneShot() def __post_init__(self) -> None: @@ -337,7 +350,7 @@ def __post_init__(self) -> None: # different processes. self.uuid = uuid.uuid4().hex - bot_name, bot_token = self.get_bot_name_and_token() + bot_name, bot_token = get_bot_name_and_token() test_repo = TEST_REPO #Storing current branch to checkout after scenario execution @@ -346,8 +359,8 @@ def __post_init__(self) -> None: logging.debug(f"Active branch name : {self.secrets.active_branch}") # Create a new branch locally from detached HEAD - head_sha = self.repo.git.rev_parse('--short', 'HEAD') - unique_branch = f'{head_sha}-{self.uuid}' + self.head_sha = self.repo.git.rev_parse('--short', 'HEAD') + unique_branch = f'{self.head_sha}-{self.uuid}' logging.debug(f"Unique branch name : {unique_branch}") local_branches = [h.name for h in self.repo.heads] logging.debug(f"Local branch names : {local_branches}") @@ -365,7 +378,7 @@ def __post_init__(self) -> None: if current_branch not in branch_names: logging.info( f"{test_repo}:{current_branch} does not exists, creating with local branch") - self.repo.git.push(f'https://x-access-token:{bot_token}@github.com/{test_repo}', + self.repo.git.push(f'https://x-access-token:{bot_token}@github.com/{test_repo}', f'HEAD:refs/heads/{current_branch}', '-f') pretty_test_name = self.test_name.strip().lower().replace(' ', '-') @@ -373,12 +386,12 @@ def __post_init__(self) -> None: logging.debug(f"Base branch name : {base_branch}") pr_branch = base_branch + '-pr-branch' - self.secrets.owners_file_content = self.owners_file_content self.secrets.test_repo = test_repo self.secrets.bot_name = bot_name self.secrets.bot_token = bot_token self.secrets.base_branch = base_branch self.secrets.pr_branch = pr_branch + self.secrets.owners_file_content = self.owners_file_content self.secrets.index_file = "index.yaml" self.secrets.provider_delivery = False @@ -391,8 +404,7 @@ def cleanup (self): self.temp_dir.cleanup() self.repo.git.worktree('prune') - head_sha = self.repo.git.rev_parse('--short', 'HEAD') - current_branch = f'{head_sha}-{self.uuid}' + current_branch = f'{self.head_sha}-{self.uuid}' logging.info(f"Delete remote '{current_branch}' branch") github_api( 'delete', f'repos/{self.secrets.test_repo}/git/refs/heads/{current_branch}', self.secrets.bot_token) @@ -430,34 +442,42 @@ def update_bot_name(self, bot_name): def update_bad_version(self, bad_version): logging.debug(f"Updating bad version: {bad_version}") self.secrets.bad_version = bad_version + + def update_provided_delivery(self, value): + if value == "true": + self.secrets.provider_delivery=True + else: + self.secrets.provider_delivery=False + + def update_test_charts(self, test_charts, new_chart_version=''): + logging.debug(f"Updating test charts: {test_charts}") + for chart in test_charts: + if chart[0] == Chart_Type.SRC or chart[0] == Chart_Type.TAR: + if new_chart_version == '': + chart_name, chart_version = get_name_and_version_from_chart_tar(chart[1]) + test_chart = Chart(chart_name=chart_name, chart_version=chart_version, chart_type=chart[0], chart_file_path=chart[1]) + else: + chart_name, _ = get_name_and_version_from_chart_tar(chart[1]) + test_chart = Chart(chart_name=chart_name, chart_version=new_chart_version, chart_type=chart[0], chart_file_path=chart[1]) + elif chart[0] == Chart_Type.REPORT: + if new_chart_version == '': + chart_name, chart_version = get_name_and_version_from_report(chart[1]) + test_chart = Chart(chart_name=chart_name, chart_version=chart_version, chart_type=chart[0], report_file_path=chart[1]) + else: + chart_name, _ = get_name_and_version_from_report(chart[1]) + test_chart = Chart(chart_name=chart_name, chart_version=new_chart_version, chart_type=chart[0], report_file_path=chart[1]) + elif chart[0] == Chart_Type.SRC_AND_REPORT or chart[0] == Chart_Type.TAR_AND_REPORT: + if new_chart_version == '': + chart_name, chart_version = get_name_and_version_from_report(chart[2]) + test_chart = Chart(chart_name=chart_name, chart_version=chart_version, chart_type=chart[0], chart_file_path=chart[1], report_file_path=chart[2]) + else: + chart_name, _ = get_name_and_version_from_report(chart[2]) + test_chart = Chart(chart_name=chart_name, chart_version=new_chart_version, chart_type=chart[0], chart_file_path=chart[1], report_file_path=chart[2]) + else: + raise AssertionError(f"Chart_Type: {chart[0]} is not correct or yet to be handled") - def update_chart_directory(self): - base_branch_without_uuid = "-".join(self.secrets.base_branch.split("-")[:-1]) - vendor_without_suffix = self.secrets.vendor.split("-")[0] - self.secrets.base_branch = f'{base_branch_without_uuid}-{self.secrets.vendor_type}-{vendor_without_suffix}-{self.secrets.chart_name}-{self.secrets.chart_version}' - self.secrets.pr_branch = f'{self.secrets.base_branch}-pr-branch' - self.chart_directory = f'charts/{self.secrets.vendor_type}/{self.secrets.vendor}/{self.secrets.chart_name}' - logging.debug(f"Updating chart_directory: {self.chart_directory}") - - def update_test_chart(self, test_chart): - logging.debug(f"Updating test chart: {test_chart}") - self.test_chart = test_chart - chart_name, chart_version = self.get_chart_name_version() - logging.debug(f"Got chart_name: {chart_name} and chart_version: {chart_version} from the chart") - self.secrets.test_chart = self.test_chart - self.secrets.chart_name = chart_name - self.secrets.chart_version = chart_version - self.update_chart_directory() - - def update_test_report(self, test_report): - logging.debug(f"Updating test report: {test_report}") - self.test_report = test_report - chart_name, chart_version = self.get_chart_name_version() - logging.debug(f"Got chart_name: {chart_name} and chart_version: {chart_version} from the report") - self.secrets.test_report = self.test_report - self.secrets.chart_name = chart_name - self.secrets.chart_version = chart_version - self.update_chart_directory() + test_chart.update_chart_directory(self.secrets) + self.test_charts.append(test_chart) def get_unique_vendor(self, vendor): """Set unique vendor name. @@ -470,15 +490,6 @@ def get_unique_vendor(self, vendor): suffix = f"{suffix}-{pr_num}" return f"{vendor}-{suffix}" - def get_chart_name_version(self): - if not self.test_report and not self.test_chart: - raise AssertionError("Provide at least one of test report or test chart.") - if self.test_report: - chart_name, chart_version = get_name_and_version_from_report(self.test_report) - else: - chart_name, chart_version = get_name_and_version_from_chart_tar(self.test_chart) - return chart_name, chart_version - def set_vendor(self, vendor, vendor_type): # use unique vendor id to avoid collision between tests logging.debug(f"Setting vendor: {vendor} vendor_type: {vendor_type}") @@ -502,168 +513,120 @@ def setup_temp_dir(self): self.set_git_username_email(self.temp_repo, self.secrets.bot_name, GITHUB_ACTIONS_BOT_EMAIL) self.temp_repo.git.checkout('-b', self.secrets.base_branch) - pathlib.Path( - f'{self.chart_directory}/{self.secrets.chart_version}').mkdir(parents=True, exist_ok=True) + for chart in self.test_charts: + pathlib.Path( + f'{chart.chart_directory}/{chart.chart_version}').mkdir(parents=True, exist_ok=True) - self.remove_chart(self.chart_directory, self.secrets.chart_version, self.secrets.test_repo, self.secrets.base_branch, self.secrets.bot_token) - self.remove_owners_file(self.chart_directory, self.secrets.test_repo, self.secrets.base_branch, self.secrets.bot_token) + self.remove_chart(chart.chart_directory, chart.chart_version, self.secrets.test_repo, self.secrets.base_branch, self.secrets.bot_token) + self.remove_owners_file(chart.chart_directory, self.secrets.test_repo, self.secrets.base_branch, self.secrets.bot_token) def update_chart_version_in_chart_yaml(self, new_version): with SetDirectory(Path(self.temp_dir.name)): - path = f'{self.chart_directory}/{self.secrets.chart_version}/src/Chart.yaml' - with open(path, 'r') as fd: - try: - chart = yaml.safe_load(fd) - except yaml.YAMLError as err: - raise AssertionError(f"error parsing '{path}': {err}") - current_version = chart['version'] + for chart in self.test_charts: + path = f'{chart.chart_directory}/{chart.chart_version}/src/Chart.yaml' + with open(path, 'r') as fd: + try: + chart = yaml.safe_load(fd) + except yaml.YAMLError as err: + raise AssertionError(f"error parsing '{path}': {err}") + current_version = chart['version'] - if current_version != new_version: - chart['version'] = new_version - try: - with open(path, 'w') as fd: - fd.write(yaml.dump(chart)) - except Exception as e: - raise AssertionError("Failed to update version in yaml file") + if current_version != new_version: + chart['version'] = new_version + try: + with open(path, 'w') as fd: + fd.write(yaml.dump(chart)) + except Exception as e: + raise AssertionError("Failed to update version in yaml file") def remove_readme_file(self): with SetDirectory(Path(self.temp_dir.name)): - path = f'{self.chart_directory}/{self.secrets.chart_version}/src/README.md' - try: - os.remove(path) - except Exception as e: - raise AssertionError(f"Failed to remove readme file : {e}") + for chart in self.test_charts: + path = f'{chart.chart_directory}/{chart.chart_version}/src/README.md' + try: + os.remove(path) + except Exception as e: + raise AssertionError(f"Failed to remove readme file : {e}") - def process_owners_file(self): - super().create_and_push_owners_file(self.chart_directory, self.secrets.base_branch, self.secrets.vendor, self.secrets.vendor_type, self.secrets.chart_name,self.secrets.provider_delivery) + def process_owners_file(self, public_key_file=None): + super().create_and_push_owners_file(self.test_charts[0].chart_directory, self.secrets.base_branch, self.secrets.vendor, self.secrets.vendor_type, self.test_charts[0].chart_name, self.secrets.provider_delivery, public_key_file) - def process_chart(self, is_tarball: bool): + def process_charts(self, include_prov_file=False): with SetDirectory(Path(self.temp_dir.name)): - if is_tarball: - # Copy the chart tar into temporary directory for PR submission - chart_tar = self.secrets.test_chart.split('/')[-1] - shutil.copyfile(f'{self.old_cwd}/{self.secrets.test_chart}', - f'{self.chart_directory}/{self.secrets.chart_version}/{chart_tar}') - else: - # Unzip files into temporary directory for PR submission - extract_chart_tgz(self.secrets.test_chart, f'{self.chart_directory}/{self.secrets.chart_version}', self.secrets, logging) - + for chart in self.test_charts: + if chart.chart_type == Chart_Type.TAR or chart.chart_type == Chart_Type.TAR_AND_REPORT: + # Copy the chart tar into temporary directory for PR submission + chart_tar = chart.chart_file_path.split('/')[-1] + shutil.copyfile(f'{self.old_cwd}/{chart.chart_file_path}', + f'{chart.chart_directory}/{chart.chart_version}/{chart_tar}') + if include_prov_file == True: + prov_file_dir = '/'.join(chart.chart_file_path.split('/')[:-1]) + prov_file_name = chart_tar + '.prov' + logging.debug(f'PROV FILE DIR: {prov_file_dir}') + logging.debug(f'PROV FILE NAME: {prov_file_name}') + shutil.copyfile(f'{self.old_cwd}/{prov_file_dir}/{prov_file_name}', + f'{chart.chart_directory}/{chart.chart_version}/{prov_file_name}') + elif chart.chart_type == Chart_Type.SRC or chart.chart_type == Chart_Type.SRC_AND_REPORT: + # Unzip files into temporary directory for PR submission + logging.debug(f"CHART SRC FILE PATH: {chart.chart_file_path}") + extract_chart_tgz(chart.chart_file_path, f'{chart.chart_directory}/{chart.chart_version}', chart.chart_name, logging) + elif chart.chart_type == Chart_Type.REPORT: + logging.debug("Skip adding chart since chart_type is report") + else: + raise AssertionError(f"Yet To be implemented for chart_type {chart.chart_type}") - def process_report(self, update_chart_sha=False, update_url=False, url=None, - update_versions=False,supported_versions=None,tested_version=None,kube_version=None, - update_provider_delivery=False, provider_delivery=False, missing_check=None,unset_package_digest=False): + def process_report(self): with SetDirectory(Path(self.temp_dir.name)): # Copy report to temporary location and push to test_repo:pr_branch logging.info( f"Push report to '{self.secrets.test_repo}:{self.secrets.pr_branch}'") - - if self.secrets.test_report.endswith('json'): - logging.debug("Report type is json") - report_path = f'{self.chart_directory}/{self.secrets.chart_version}/' + self.secrets.test_report.split('/')[-1] - with open(self.secrets.test_report, 'r') as fd: - try: - report = json.load(fd) - except Exception as e: - raise AssertionError("Failed to read json file") - - with open(report_path, 'w') as fd: - try: - fd.write(json.dumps(report, indent=4)) - except Exception as e: - raise AssertionError("Failed to write report in json format") - elif self.secrets.test_report.endswith('yaml'): - logging.debug("Report type is yaml") - tmpl = open(self.secrets.test_report).read() - values = {'repository': self.secrets.test_repo, - 'branch': self.secrets.base_branch} - content = Template(tmpl).substitute(values) + for chart in self.test_charts: + if chart.chart_type == Chart_Type.REPORT or chart.chart_type == Chart_Type.SRC_AND_REPORT or chart.chart_type == Chart_Type.TAR_AND_REPORT: + if chart.report_file_path.endswith('json'): + logging.debug("Report type is json") + report_path = f'{chart.chart_directory}/{chart.chart_version}/' + chart.report_file_path.split('/')[-1] + shutil.copyfile(f'{chart.report_file_path}', f'{report_path}') + elif chart.report_file_path.endswith('yaml'): + logging.debug("Report type is yaml") + report_path = f'{chart.chart_directory}/{chart.chart_version}/' + chart.report_file_path.split('/')[-1] + shutil.copyfile(f'{chart.report_file_path}', f'{report_path}') + else: + raise AssertionError("Unknown report type") - report_path = f'{self.chart_directory}/{self.secrets.chart_version}/' + self.secrets.test_report.split('/')[-1] + self.temp_repo.git.add(report_path) - try: - report = yaml.safe_load(content) - except yaml.YAMLError as err: - raise AssertionError(f"error parsing '{report_path}': {err}") - - if self.secrets.vendor_type != "partners": - report["metadata"]["tool"]["profile"]["VendorType"] = self.secrets.vendor_type - logging.info(f'VendorType set to {report["metadata"]["tool"]["profile"]["VendorType"]} in report.yaml') - - if update_chart_sha or update_url or update_versions or update_provider_delivery or unset_package_digest: - #For updating the report.yaml, for chart sha mismatch scenario - if update_chart_sha: - new_sha_value = 'sha256:5b85ae00b9ca2e61b2d70a59f98fd72136453b1a185676b29d4eb862981c1xyz' - logging.info(f"Current SHA Value in report: {report['metadata']['tool']['digests']['chart']}") - report['metadata']['tool']['digests']['chart'] = new_sha_value - logging.info(f"Updated sha value in report: {new_sha_value}") - - #For updating the report.yaml, for invalid_url sceanrio - if update_url: - logging.info(f"Current chart-uri in report: {report['metadata']['tool']['chart-uri']}") - report['metadata']['tool']['chart-uri'] = url - logging.info(f"Updated chart-uri value in report: {url}") - - if update_versions: - report['metadata']['tool']['testedOpenShiftVersion'] = tested_version - report['metadata']['tool']['supportedOpenShiftVersions'] = supported_versions - report['metadata']['chart']['kubeversion'] = kube_version - logging.info(f"Updated testedOpenShiftVersion value in report: {tested_version}") - logging.info(f"Updated supportedOpenShiftVersions value in report: {supported_versions}") - logging.info(f"Updated kubeversion value in report: {kube_version}") - - if update_provider_delivery: - report['metadata']['tool']['providerControlledDelivery'] = provider_delivery - - if unset_package_digest: - del report['metadata']['tool']['digests']['package'] - - with open(report_path, 'w') as fd: - try: - fd.write(yaml.dump(report)) - logging.info("Report updated with new values") - except Exception as e: - raise AssertionError("Failed to update report yaml with new values") - - #For removing the check for missing check scenario - if missing_check: - logging.info(f"Updating report with {missing_check}") - with open(report_path, 'r+') as fd: - report_content = yaml.safe_load(fd) - results = report_content["results"] - new_results = filter(lambda x: x['check'] != missing_check, results) - report_content["results"] = list(new_results) - fd.seek(0) - yaml.dump(report_content, fd) - fd.truncate() - else: - raise AssertionError("Unknown report type") - - self.temp_repo.git.add(report_path) self.temp_repo.git.commit( - '-m', f"Add {self.secrets.vendor} {self.secrets.chart_name} {self.secrets.chart_version} report") + '-m', f"Add {self.secrets.vendor} {self.test_charts} report") self.temp_repo.git.push(f'https://x-access-token:{self.secrets.bot_token}@github.com/{self.secrets.test_repo}', f'HEAD:refs/heads/{self.secrets.pr_branch}', '-f') def add_non_chart_related_file(self): with SetDirectory(Path(self.temp_dir.name)): - path = f'{self.chart_directory}/Notes.txt' - with open(path, 'w') as fd: - fd.write("This is a test file") + for chart in self.test_charts: + path = f'{chart.chart_directory}/Notes.txt' + with open(path, 'w') as fd: + fd.write("This is a test file") - def push_chart(self, is_tarball: bool, add_non_chart_file=False): + def push_charts(self, add_non_chart_file=False): # Push chart to test_repo:pr_branch - if is_tarball: - chart_tar = self.secrets.test_chart.split('/')[-1] - self.temp_repo.git.add(f'{self.chart_directory}/{self.secrets.chart_version}/{chart_tar}') - else: - if add_non_chart_file: - self.temp_repo.git.add(f'{self.chart_directory}/') + for chart in self.test_charts: + if chart.chart_type == Chart_Type.TAR or chart.chart_type == Chart_Type.TAR_AND_REPORT: + chart_tar = chart.chart_file_path.split('/')[-1] + self.temp_repo.git.add(f'{chart.chart_directory}/{chart.chart_version}/') + elif chart.chart_type == Chart_Type.SRC or chart.chart_type == Chart_Type.SRC_AND_REPORT: + if add_non_chart_file: + self.temp_repo.git.add(f'{chart.chart_directory}/') + else: + self.temp_repo.git.add(f'{chart.chart_directory}/{chart.chart_version}/src') + elif chart.chart_type == Chart_Type.REPORT: + logging.debug("Skip adding chart since chart_type is report") else: - self.temp_repo.git.add(f'{self.chart_directory}/{self.secrets.chart_version}/src') + raise AssertionError(f"YTD: chart_type {chart.chart_type} is yet to be supported") + self.temp_repo.git.commit( - '-m', f"Add {self.secrets.vendor} {self.secrets.chart_name} {self.secrets.chart_version} chart") + '-m', f"Adding {self.secrets.vendor} {self.test_charts} charts") self.temp_repo.git.push(f'https://x-access-token:{self.secrets.bot_token}@github.com/{self.secrets.test_repo}', f'HEAD:refs/heads/{self.secrets.pr_branch}', '-f') @@ -687,7 +650,7 @@ def check_pull_request_labels(self): def check_pull_request_comments(self, expect_message: str): r = github_api( 'get', f'repos/{self.secrets.test_repo}/issues/{self.secrets.pr_number}/comments', self.secrets.bot_token) - logging.info(f'STATUS_CODE: {r.status_code}') + logging.debug(f'STATUS_CODE: {r.status_code}') response = json.loads(r.text) logging.debug(f"CHECK PULL_REQUEST COMMENT RESPONSE: {response}") @@ -701,26 +664,29 @@ def check_pull_request_comments(self, expect_message: str): raise AssertionError(f"Was expecting '{expect_message}' in the comment {complete_comment}") def check_index_yaml(self, check_provider_type=False): - super().check_index_yaml(self.secrets.base_branch, self.secrets.vendor, self.secrets.chart_name, self.secrets.chart_version, self.secrets.index_file,check_provider_type) + for chart in self.test_charts: + super().check_index_yaml(self.secrets.base_branch, self.secrets.vendor, chart.chart_name, chart.chart_version, self.secrets.index_file, check_provider_type) - def check_release_result(self): - chart_tgz = self.secrets.test_chart.split('/')[-1] - super().check_release_result(self.secrets.vendor, self.secrets.chart_name, self.secrets.chart_version, chart_tgz) + def check_release_result(self, release_type): + for chart in self.test_charts: + chart_tgz = chart.chart_file_path.split('/')[-1] + super().check_release_result(self.secrets.vendor, chart.chart_name, chart.chart_version, chart_tgz, release_type=release_type) def cleanup_release(self): - expected_tag = f'{self.secrets.vendor}-{self.secrets.chart_name}-{self.secrets.chart_version}' - super().cleanup_release(expected_tag) + for chart in self.test_charts: + expected_tag = f'{self.secrets.vendor}-{chart.chart_name}-{chart.chart_version}' + super().cleanup_release(expected_tag) @dataclass class ChartCertificationE2ETestMultiple(ChartCertificationE2ETest): secrets: E2ETestSecretRecursive = E2ETestSecretRecursive() def __post_init__(self) -> None: - bot_name, bot_token = self.get_bot_name_and_token() - dry_run = self.get_dry_run() - notify_id = self.get_notify_id() - software_name, software_version = self.get_software_name_version() - vendor_type = self.get_vendor_type() + bot_name, bot_token = get_bot_name_and_token() + dry_run = get_dry_run() + notify_id = get_notify_id() + software_name, software_version = get_software_name_version() + vendor_type = get_vendor_type() test_repo = TEST_REPO base_branches = [] @@ -784,48 +750,6 @@ def cleanup (self): except git.exc.GitCommandError: logging.info(f"Local 'tmp' branch does not exist") - def get_dry_run(self): - # Accepts 'true' or 'false', depending on whether we want to notify - # Don't notify on dry runs, default to True - dry_run = False if os.environ.get("DRY_RUN") == 'false' else True - # Don't notify if not triggerd on PROD_REPO and PROD_BRANCH - if not dry_run: - triggered_branch = os.environ.get("GITHUB_REF").split('/')[-1] - triggered_repo = os.environ.get("GITHUB_REPOSITORY") - if triggered_repo != PROD_REPO or triggered_branch != PROD_BRANCH: - dry_run = True - return dry_run - - def get_notify_id(self): - # Accepts comma separated Github IDs or empty strings to override people to tag in notifications - notify_id = os.environ.get("NOTIFY_ID") - if notify_id: - notify_id = [vt.strip() for vt in notify_id.split(',')] - else: - notify_id = ["dperaza","mmulholla"] - return notify_id - - def get_software_name_version(self): - software_name = os.environ.get("SOFTWARE_NAME") - if not software_name: - raise Exception("SOFTWARE_NAME environment variable not defined") - - software_version = os.environ.get("SOFTWARE_VERSION").strip('\"') - if not software_version: - raise Exception("SOFTWARE_VERSION environment variable not defined") - elif software_version.startswith("sha256"): - software_version = software_version[-8:] - - return software_name, software_version - - def get_vendor_type(self): - vendor_type = os.environ.get("VENDOR_TYPE") - if not vendor_type: - logging.info( - f"VENDOR_TYPE environment variable not defined, default to `all`") - vendor_type = 'all' - return vendor_type - def setup_temp_dir(self): self.temp_dir = TemporaryDirectory(prefix='tci-') with SetDirectory(Path(self.temp_dir.name)): diff --git a/tests/functional/behave_features/common/utils/env.py b/tests/functional/behave_features/common/utils/env.py new file mode 100644 index 0000000000..d92f1a42cd --- /dev/null +++ b/tests/functional/behave_features/common/utils/env.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +"""Utility class to reading environments.""" +import logging +import os + +from common.utils.setttings import * + +def get_bot_name_and_token(): + bot_name = os.environ.get("BOT_NAME") + logging.debug(f"Enviroment variable value BOT_NAME: {bot_name}") + bot_token = os.environ.get("BOT_TOKEN") + if not bot_name and not bot_token: + bot_name = "github-actions[bot]" + bot_token = os.environ.get("GITHUB_TOKEN") + if not bot_token: + raise Exception("BOT_TOKEN environment variable not defined") + elif not bot_name: + raise Exception("BOT_TOKEN set but BOT_NAME not specified") + elif not bot_token: + raise Exception("BOT_NAME set but BOT_TOKEN not specified") + return bot_name, bot_token + +def get_dry_run(): + # Accepts 'true' or 'false', depending on whether we want to notify + # Don't notify on dry runs, default to True + dry_run = False if os.environ.get("DRY_RUN") == 'false' else True + # Don't notify if not triggerd on PROD_REPO and PROD_BRANCH + if not dry_run: + triggered_branch = os.environ.get("GITHUB_REF").split('/')[-1] + triggered_repo = os.environ.get("GITHUB_REPOSITORY") + if triggered_repo != PROD_REPO or triggered_branch != PROD_BRANCH: + dry_run = True + return dry_run + +def get_notify_id(): + # Accepts comma separated Github IDs or empty strings to override people to tag in notifications + notify_id = os.environ.get("NOTIFY_ID") + if notify_id: + notify_id = [vt.strip() for vt in notify_id.split(',')] + else: + notify_id = ["dperaza","mmulholla"] + return notify_id + +def get_software_name_version(): + software_name = os.environ.get("SOFTWARE_NAME") + if not software_name: + raise Exception("SOFTWARE_NAME environment variable not defined") + + software_version = os.environ.get("SOFTWARE_VERSION").strip('\"') + if not software_version: + raise Exception("SOFTWARE_VERSION environment variable not defined") + elif software_version.startswith("sha256"): + software_version = software_version[-8:] + + return software_name, software_version + +def get_vendor_type(): + vendor_type = os.environ.get("VENDOR_TYPE") + if not vendor_type: + logging.info( + f"VENDOR_TYPE environment variable not defined, default to `all`") + vendor_type = 'all' + return vendor_type \ No newline at end of file diff --git a/tests/functional/behave_features/common/utils/github.py b/tests/functional/behave_features/common/utils/github.py index 39a9aa21c5..ffcc044e7f 100644 --- a/tests/functional/behave_features/common/utils/github.py +++ b/tests/functional/behave_features/common/utils/github.py @@ -3,6 +3,7 @@ import json import requests +import logging from retrying import retry from common.utils.setttings import * @@ -35,11 +36,12 @@ def get_run_result(secrets, run_id): @retry(stop_max_delay=10_000, wait_fixed=1000) -def get_release_assets(secrets, release_id, required_assets): +def check_release_assets(secrets, release_id, required_assets): r = github_api( 'get', f'repos/{secrets.test_repo}/releases/{release_id}/assets', secrets.bot_token) asset_list = json.loads(r.text) asset_names = [asset['name'] for asset in asset_list] + logging.debug(f'FOUND RELEASE ASSETS: {asset_names}') missing_assets = list() for asset in required_assets: if asset not in asset_names: diff --git a/tests/functional/behave_features/common/utils/secret.py b/tests/functional/behave_features/common/utils/secret.py index d3240134cc..a509da4043 100644 --- a/tests/functional/behave_features/common/utils/secret.py +++ b/tests/functional/behave_features/common/utils/secret.py @@ -12,10 +12,6 @@ class E2ETestSecret: pr_number: int = -1 vendor_type: str = '' owners_file_content: str = '' - test_chart: str = '' - test_report: str = '' - chart_name: str = '' - chart_version: str = '' @dataclass class E2ETestSecretOneShot(E2ETestSecret): diff --git a/tests/functional/behave_features/steps/implementation.py b/tests/functional/behave_features/steps/implementation.py index 8830d5af58..e261a227f0 100644 --- a/tests/functional/behave_features/steps/implementation.py +++ b/tests/functional/behave_features/steps/implementation.py @@ -1,4 +1,5 @@ from behave import given, when, then +from common.utils.chart import Chart, Chart_Type, Release_Type ############### Common step definitions ############### @given(u'the vendor "{vendor}" has a valid identity as "{vendor_type}"') @@ -7,83 +8,185 @@ def vendor_has_valid_identity(context, vendor, vendor_type): @given(u'an error-free chart source is used in "{chart_path}"') def chart_source_is_used(context, chart_path): - context.workflow_test.update_test_chart(chart_path) + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.SRC, chart_path)]) context.workflow_test.setup_git_context() context.workflow_test.setup_gh_pages_branch() context.workflow_test.setup_temp_dir() context.workflow_test.process_owners_file() - context.workflow_test.process_chart(is_tarball=False) - context.workflow_test.push_chart(is_tarball=False) + context.workflow_test.process_charts() + context.workflow_test.push_charts() @given(u'chart source is used in "{chart_path}"') def user_has_used_chart_src(context, chart_path): - context.workflow_test.update_test_chart(chart_path) + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.SRC, chart_path)]) context.workflow_test.setup_git_context() context.workflow_test.setup_gh_pages_branch() context.workflow_test.setup_temp_dir() context.workflow_test.process_owners_file() - context.workflow_test.process_chart(is_tarball=False) + context.workflow_test.process_charts() @given(u'an error-free chart tarball is used in "{chart_path}"') def user_has_created_error_free_chart_tarball(context, chart_path): - context.workflow_test.update_test_chart(chart_path) + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.TAR, chart_path)]) context.workflow_test.setup_git_context() context.workflow_test.setup_gh_pages_branch() context.workflow_test.setup_temp_dir() context.workflow_test.process_owners_file() - context.workflow_test.process_chart(is_tarball=True) - context.workflow_test.push_chart(is_tarball=True) + context.workflow_test.process_charts() + context.workflow_test.push_charts() + +@given(u'a signed chart tarball is used in "{chart_path}" and public key in "{public_key_file}"') +def user_has_created_signed_chart_tarball(context, chart_path, public_key_file): + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.TAR, chart_path)]) + context.workflow_test.setup_git_context() + context.workflow_test.setup_gh_pages_branch() + context.workflow_test.setup_temp_dir() + context.workflow_test.process_owners_file(public_key_file=public_key_file) + context.workflow_test.process_charts(include_prov_file=True) + context.workflow_test.push_charts() + +@given(u'signed chart tar used in "{chart_path}"') +def user_has_created_signed_chart_tarball(context, chart_path): + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.TAR, chart_path)]) + context.workflow_test.setup_git_context() + context.workflow_test.setup_gh_pages_branch() + context.workflow_test.setup_temp_dir() + context.workflow_test.process_owners_file() + context.workflow_test.process_charts(include_prov_file=True) + context.workflow_test.push_charts() @given(u'an error-free chart tarball used in "{chart_path}" and report in "{report_path}"') def user_has_created_error_free_chart_tarball_and_report(context, chart_path, report_path): - context.workflow_test.update_test_chart(chart_path) - context.workflow_test.update_test_report(report_path) + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.TAR_AND_REPORT, chart_path, report_path)]) context.workflow_test.setup_git_context() context.workflow_test.setup_gh_pages_branch() context.workflow_test.setup_temp_dir() context.workflow_test.process_owners_file() - context.workflow_test.process_chart(is_tarball=True) + context.workflow_test.process_charts() + context.workflow_test.process_report() + context.workflow_test.push_charts() + +@given(u'a signed chart tar is used in "{chart_path}", report in "{report_path}" and public key in "{public_key_file}"') +def user_has_created_error_free_chart_tarball_and_report(context, chart_path, report_path, public_key_file): + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.TAR_AND_REPORT, chart_path, report_path)]) + + context.workflow_test.setup_git_context() + context.workflow_test.setup_gh_pages_branch() + context.workflow_test.setup_temp_dir() + context.workflow_test.process_owners_file(public_key_file=public_key_file) + context.workflow_test.process_charts(include_prov_file=True) context.workflow_test.process_report() - context.workflow_test.push_chart(is_tarball=True) + context.workflow_test.push_charts() + +@given(u'unsigned chart tarball is used in "{chart_path}" and public key used "{public_key_file}" in owners') +def user_has_created_error_free_chart_tarball(context, chart_path, public_key_file): + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.TAR, chart_path)]) + + context.workflow_test.setup_git_context() + context.workflow_test.setup_gh_pages_branch() + context.workflow_test.setup_temp_dir() + context.workflow_test.process_owners_file(public_key_file=public_key_file) + context.workflow_test.process_charts() + context.workflow_test.push_charts() @given(u'a chart tarball is used in "{chart_path}" and report in "{report_path}"') def user_has_created_a_chart_tarball_and_report(context, chart_path, report_path): - context.workflow_test.update_test_chart(chart_path) - context.workflow_test.update_test_report(report_path) + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.TAR_AND_REPORT, chart_path, report_path)]) context.workflow_test.setup_git_context() context.workflow_test.setup_gh_pages_branch() context.workflow_test.setup_temp_dir() context.workflow_test.process_owners_file() - context.workflow_test.process_chart(is_tarball=True) + context.workflow_test.process_charts() @given(u'an error-free chart source used in "{chart_path}" and report in "{report_path}"') def user_has_created_error_free_chart_src_and_report(context, chart_path, report_path): - context.workflow_test.update_test_chart(chart_path) - context.workflow_test.update_test_report(report_path) + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.SRC_AND_REPORT, chart_path, report_path)]) context.workflow_test.setup_git_context() context.workflow_test.setup_gh_pages_branch() context.workflow_test.setup_temp_dir() context.workflow_test.process_owners_file() - context.workflow_test.process_chart(is_tarball=False) + context.workflow_test.process_charts() context.workflow_test.process_report() - context.workflow_test.push_chart(is_tarball=False) + context.workflow_test.push_charts() @given(u'report is used in "{report_path}"') @given(u'an error-free report is used in "{report_path}"') def user_has_created_error_free_report(context, report_path): - context.workflow_test.update_test_report(report_path) + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.REPORT, report_path)]) + context.workflow_test.setup_git_context() + context.workflow_test.setup_gh_pages_branch() + context.workflow_test.setup_temp_dir() + context.workflow_test.process_owners_file() + context.workflow_test.process_report() + +@given(u'signed chart report used in "{report_path}" and public key in "{public_key_file}"') +def user_has_created_error_free_report(context, report_path, public_key_file): + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.REPORT, report_path)]) + context.workflow_test.setup_git_context() + context.workflow_test.setup_gh_pages_branch() + context.workflow_test.setup_temp_dir() + context.workflow_test.process_owners_file(public_key_file=public_key_file) + context.workflow_test.process_report() + +@given(u'user wants to send two reports as in "{report_path_1}" and "{report_path_2}"') +def user_has_created_error_free_report(context, report_path_1, report_path_2): + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.REPORT, report_path_1), (Chart_Type.REPORT, report_path_2)]) context.workflow_test.setup_git_context() context.workflow_test.setup_gh_pages_branch() context.workflow_test.setup_temp_dir() context.workflow_test.process_owners_file() context.workflow_test.process_report() +@given(u'user wants to send two chart sources as in "{chart_path_1}" and "{chart_path_2}"') +def user_wants_to_send_two_chart_sources(context, chart_path_1, chart_path_2): + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.SRC, chart_path_1), (Chart_Type.SRC, chart_path_2)]) + context.workflow_test.setup_git_context() + context.workflow_test.setup_gh_pages_branch() + context.workflow_test.setup_temp_dir() + context.workflow_test.process_owners_file() + context.workflow_test.process_charts() + context.workflow_test.push_charts() + +@given(u'user wants to send two chart tars as in "{chart_path_1}" and "{chart_path_2}"') +def user_wants_to_send_two_chart_tars(context, chart_path_1, chart_path_2): + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.TAR, chart_path_1), (Chart_Type.TAR, chart_path_2)]) + context.workflow_test.setup_git_context() + context.workflow_test.setup_gh_pages_branch() + context.workflow_test.setup_temp_dir() + context.workflow_test.process_owners_file() + context.workflow_test.process_charts() + context.workflow_test.push_charts() + +@given(u'user wants to send two charts one with source "{chart_path}" and other with report "{report_path}"') +def user_wants_to_send_multiple_chart_one_with_src_and_other_with_report(context, chart_path, report_path): + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.SRC, chart_path), (Chart_Type.REPORT, report_path)]) + + context.workflow_test.setup_git_context() + context.workflow_test.setup_gh_pages_branch() + context.workflow_test.setup_temp_dir() + context.workflow_test.process_owners_file() + context.workflow_test.process_charts() + context.workflow_test.process_report() + context.workflow_test.push_charts() + +@given(u'user wants to send two charts one with tar "{chart_path}" and other with report "{report_path}"') +def user_wants_to_send_multiple_chart_one_with_tar_and_other_with_report(context, chart_path, report_path): + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.TAR, chart_path), (Chart_Type.REPORT, report_path)]) + + context.workflow_test.setup_git_context() + context.workflow_test.setup_gh_pages_branch() + context.workflow_test.setup_temp_dir() + context.workflow_test.process_owners_file() + context.workflow_test.process_charts() + context.workflow_test.process_report() + context.workflow_test.push_charts() + @given(u'a "{report_path}" is provided') def user_generated_a_report(context, report_path): - context.workflow_test.update_test_report(report_path) + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.REPORT, report_path)]) context.workflow_test.setup_git_context() context.workflow_test.setup_gh_pages_branch() context.workflow_test.setup_temp_dir() @@ -97,7 +200,7 @@ def user_sends_a_pull_request(context): @when(u'the user pushed the chart and created pull request') def user_pushed_the_chart_and_created_pull_request(context): - context.workflow_test.push_chart(is_tarball=False) + context.workflow_test.push_charts() context.workflow_test.send_pull_request() @then(u'the user sees the pull request is merged') @@ -110,9 +213,25 @@ def pull_request_is_merged(context): def index_yaml_updated_with_submitted_chart(context): context.workflow_test.check_index_yaml() +@then(u'a release is published with report only') +def release_is_published(context): + context.workflow_test.check_release_result(release_type=Release_Type.REPORT_ONLY) + @then(u'a release is published with corresponding report and chart tarball') def release_is_published(context): - context.workflow_test.check_release_result() + context.workflow_test.check_release_result(release_type=Release_Type.CHART_AND_REPORT) + +@then(u'a release is published with corresponding report, tarball, prov and key') +def release_is_published_for_signed_chart(context): + context.workflow_test.check_release_result(release_type=Release_Type.CHART_REPORT_PROV_AND_KEY) + +@then(u'a release is published with corresponding report and key') +def release_is_published_for_signed_chart(context): + context.workflow_test.check_release_result(release_type=Release_Type.REPORT_AND_KEY) + +@then(u'a release is published with corresponding report, chart tar and prov file') +def release_is_published_for_signed_chart_and_report(context): + context.workflow_test.check_release_result(release_type=Release_Type.CHART_PROV_AND_REPORT) @then(u'the pull request is not merged') def pull_request_is_not_merged(context): @@ -143,15 +262,12 @@ def user_adds_a_non_chart_related_file(context): @when(u'the user sends a pull request with both chart and non related file') def user_sends_pull_request_with_chart_and_non_related_file(context): - context.workflow_test.push_chart(is_tarball=False, add_non_chart_file=True) + context.workflow_test.push_charts(add_non_chart_file=True) context.workflow_test.send_pull_request() @given(u'provider delivery control is set to "{provider_control_owners}" in the OWNERS file') def provider_delivery_control_set_in_owners(context, provider_control_owners): - if provider_control_owners == "true": - context.workflow_test.secrets.provider_delivery=True - else: - context.workflow_test.secrets.provider_delivery=False + context.workflow_test.update_provided_delivery(provider_control_owners) @given(u'provider delivery control is set to "{provider_control_report}" in the report') def provider_delivery_control_set_in_report(context, provider_control_report): @@ -187,12 +303,16 @@ def report_has_a_check_missing(context, check): @given(u'A "{user}" wants to submit a chart in "{chart_path}"') def user_wants_to_submit_a_chart(context, user, chart_path): - context.workflow_test.update_test_chart(chart_path) + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.SRC, chart_path)]) context.workflow_test.update_bot_name(user) @given(u'An authorized user wants to submit a chart in "{chart_path}"') def authorized_user_wants_to_submit_a_chart(context, chart_path): - context.workflow_test.update_test_chart(chart_path) + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.SRC, chart_path)]) + +@given(u'a chart source used in "{chart_path}" and directory structure contains "{bad_version}"') +def a_user_wants_to_submit_a_chart_with_bad_semver(context, chart_path, bad_version): + context.workflow_test.update_test_charts(test_charts=[(Chart_Type.SRC, chart_path)], new_chart_version=bad_version) @given(u'the user creates a branch to add a new chart version') def the_user_creates_a_branch_to_add_a_new_chart_version(context): @@ -200,10 +320,10 @@ def the_user_creates_a_branch_to_add_a_new_chart_version(context): context.workflow_test.setup_gh_pages_branch() context.workflow_test.setup_temp_dir() context.workflow_test.process_owners_file() - context.workflow_test.process_chart(is_tarball=False) + context.workflow_test.process_charts() if context.workflow_test.secrets.bad_version: context.workflow_test.update_chart_version_in_chart_yaml(context.workflow_test.secrets.bad_version) - context.workflow_test.push_chart(is_tarball=False) + context.workflow_test.push_charts() @given(u'Chart.yaml specifies a "{bad_version}"') def chart_yaml_specifies_bad_version(context, bad_version): @@ -219,7 +339,7 @@ def sha_value_does_not_match(context, error): @when(u'the user sends a pull request with the chart tar and report') def user_sends_pull_request_with_chart_tarball_and_report(context): - context.workflow_test.push_chart(is_tarball=True) + context.workflow_test.push_charts() context.workflow_test.send_pull_request() ######## Test Submitted Charts Step definitions ##########