From 06e908226d8554736f76448a9b963598a5cbd8a6 Mon Sep 17 00:00:00 2001 From: mmulholla <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Aug 2022 22:22:38 +0000 Subject: [PATCH] Release-1.4.2 --- .github/workflows/build.yml | 6 +- .github/workflows/test.yml | 6 +- .github/workflows/version_check.yml | 168 ++- scripts/requirements.txt | 1 + .../src/chartrepomanager/indexannotations.py | 41 +- scripts/src/checkprcontent/checkpr.py | 24 +- scripts/src/indexfile/index.py | 118 +- scripts/src/pullrequest/prartifact.py | 8 +- .../saforcharttesting/saforcharttesting.py | 26 +- .../HC-16/dash-in-version/partner/report.yaml | 89 ++ .../HC-16/dash-in-version/redhat/report.yaml | 89 ++ .../HC-17/dash-in-version/partner/report.yaml | 89 ++ .../HC-17/dash-in-version/redhat/report.yaml | 89 ++ tests/data/psql-service-0.1.10-1.tgz | Bin 0 -> 2791 bytes tests/data/vault-test-timeout-0.17.0.tgz | Bin 0 -> 37518 bytes .../HC-01_chart_src_without_report.feature | 33 + .../HC-02_chart_tar_without_report.feature | 33 + ..._verifier_comes_back_with_failures.feature | 35 + .../HC-04_invalid_url_in_the_report.feature | 44 + ..._a_file_which_is_not_chart_related.feature | 25 + .../HC-06_provider_delivery_control.feature | 51 + .../HC-07_report_and_chart_src.feature | 33 + .../HC-08_report_and_chart_tar.feature | 33 + .../HC-09_report_in_json_format.feature | 27 + .../HC-10_report_only_edited.feature | 47 + .../HC-11_report_with_missing_checks.feature | 27 + .../HC-12_report_without_chart.feature | 32 + .../HC-13_sha_value_does_not_match.feature | 26 + ...-14_user_submits_chart_with_errors.feature | 49 + .../HC-15_check_submitted_charts.feature | 16 + ..._chart_test_takes_more_than_30mins.feature | 33 + .../HC-17_dash_in_version.feature | 21 + .../behave_features/common/__init__.py | 0 .../behave_features/common/utils/__init__.py | 0 .../behave_features/common/utils/chart.py | 142 +++ .../common/utils/chart_certification.py | 1036 +++++++++++++++++ .../behave_features/common/utils/github.py | 107 ++ .../behave_features/common/utils/index.py | 43 + .../behave_features/common/utils/notifier.py | 172 +++ .../behave_features/common/utils/secret.py | 43 + .../common/utils/set_directory.py | 23 + .../behave_features/common/utils/setttings.py | 14 + .../functional/behave_features/environment.py | 23 + .../behave_features/steps/implementation.py | 249 ++++ ..._chart_test_takes_more_than_30mins.feature | 31 + .../features/HC-16_dash_in_version.feature | 16 + .../step_defs/HC-16_test_dash_in_version.py | 22 + tests/functional/step_defs/conftest.py | 1 + .../test_chart_test_takes_more_than_30mins.py | 33 + tests/functional/utils/chart_certification.py | 132 ++- tests/functional/utils/github.py | 16 +- tests/functional/utils/index.py | 43 + tests/functional/utils/notifier.py | 37 +- 53 files changed, 3342 insertions(+), 160 deletions(-) create mode 100644 tests/data/HC-16/dash-in-version/partner/report.yaml create mode 100644 tests/data/HC-16/dash-in-version/redhat/report.yaml create mode 100644 tests/data/HC-17/dash-in-version/partner/report.yaml create mode 100644 tests/data/HC-17/dash-in-version/redhat/report.yaml create mode 100644 tests/data/psql-service-0.1.10-1.tgz create mode 100644 tests/data/vault-test-timeout-0.17.0.tgz create mode 100644 tests/functional/behave_features/HC-01_chart_src_without_report.feature create mode 100644 tests/functional/behave_features/HC-02_chart_tar_without_report.feature create mode 100644 tests/functional/behave_features/HC-03_chart_verifier_comes_back_with_failures.feature create mode 100644 tests/functional/behave_features/HC-04_invalid_url_in_the_report.feature create mode 100644 tests/functional/behave_features/HC-05_pr_includes_a_file_which_is_not_chart_related.feature create mode 100644 tests/functional/behave_features/HC-06_provider_delivery_control.feature create mode 100644 tests/functional/behave_features/HC-07_report_and_chart_src.feature create mode 100644 tests/functional/behave_features/HC-08_report_and_chart_tar.feature create mode 100644 tests/functional/behave_features/HC-09_report_in_json_format.feature create mode 100644 tests/functional/behave_features/HC-10_report_only_edited.feature create mode 100644 tests/functional/behave_features/HC-11_report_with_missing_checks.feature create mode 100644 tests/functional/behave_features/HC-12_report_without_chart.feature create mode 100644 tests/functional/behave_features/HC-13_sha_value_does_not_match.feature create mode 100644 tests/functional/behave_features/HC-14_user_submits_chart_with_errors.feature create mode 100644 tests/functional/behave_features/HC-15_check_submitted_charts.feature create mode 100644 tests/functional/behave_features/HC-16_chart_test_takes_more_than_30mins.feature create mode 100644 tests/functional/behave_features/HC-17_dash_in_version.feature create mode 100644 tests/functional/behave_features/common/__init__.py create mode 100644 tests/functional/behave_features/common/utils/__init__.py create mode 100644 tests/functional/behave_features/common/utils/chart.py create mode 100644 tests/functional/behave_features/common/utils/chart_certification.py create mode 100644 tests/functional/behave_features/common/utils/github.py create mode 100644 tests/functional/behave_features/common/utils/index.py create mode 100755 tests/functional/behave_features/common/utils/notifier.py create mode 100644 tests/functional/behave_features/common/utils/secret.py create mode 100644 tests/functional/behave_features/common/utils/set_directory.py create mode 100644 tests/functional/behave_features/common/utils/setttings.py create mode 100644 tests/functional/behave_features/environment.py create mode 100644 tests/functional/behave_features/steps/implementation.py create mode 100644 tests/functional/features/HC-16_chart_test_takes_more_than_30mins.feature create mode 100644 tests/functional/features/HC-16_dash_in_version.feature create mode 100644 tests/functional/step_defs/HC-16_test_dash_in_version.py create mode 100644 tests/functional/step_defs/test_chart_test_takes_more_than_30mins.py create mode 100644 tests/functional/utils/index.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c82a25f920..99d751e010 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -118,7 +118,7 @@ jobs: - name: Remove 'content-ok' label uses: actions/github-script@v3 - if: ${{ steps.check_pr_content.outcome == 'failure'}} + if: ${{ steps.check_pr_content.outcome == 'failure' && contains( github.event.pull_request.labels.*.name, 'content-ok') }} continue-on-error: true with: github-token: ${{secrets.GITHUB_TOKEN}} @@ -138,7 +138,7 @@ jobs: - name: Remove 'authorized-request' label from PR uses: actions/github-script@v3 - if: ${{ steps.check_build_required.outputs.run-build == 'true' }} + if: ${{ steps.check_build_required.outputs.run-build == 'true' && contains( github.event.pull_request.labels.*.name, 'authorized-request') }} continue-on-error: true with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -225,7 +225,7 @@ jobs: KUBECONFIG: /tmp/ci-kubeconfig run: | API_SERVER=$( echo -n ${{ secrets.API_SERVER }} | base64 -d) - oc login --token=${{ secrets.CLUSTER_TOKEN }} --server=${API_SERVER} + oc login --token=${{ secrets.CLUSTER_TOKEN }} --server=${API_SERVER} --insecure-skip-tls-verify=${{ steps.set-env.outputs.insecure_skip_tls_verify }} ve1/bin/sa-for-chart-testing --delete charts-${{ github.event.number }} - name: Save PR artifact diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3c664a7bcf..b16d02b61b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -105,10 +105,10 @@ jobs: echo "Full test in pr : {{steps.check_request.outputs.full_tests_in_pr }}" if ${{steps.check_if_release_pr.outputs.charts_release_branch == 'true' || steps.check_request.outputs.full_tests_in_pr == 'true' }} ; then echo "Release PR from dev to charts, oer PR with new full test, so running full tests" - ve1/bin/pytest tests/ --log-cli-level=WARNING --ignore=tests/functional/step_defs/test_smoke_scenarios.py --ignore=tests/functional/step_defs/test_submitted_charts.py --tb=short + ve1/bin/behave tests/functional/behave_features/ --tags=full --logging-level=WARNING --no-capture --no-color else echo "Not a release PR from dev to charts, so running only smoke tests" - ve1/bin/pytest tests/functional/step_defs/test_smoke_scenarios.py --log-cli-level=WARNING --tb=short + ve1/bin/behave tests/functional/behave_features/ --tags=smoke --logging-level=WARNING --no-capture --no-color fi - name: (Manual) Test CI Workflow @@ -130,7 +130,7 @@ jobs: echo "[INFO] Notify ID '${{ env.NOTIFY_ID }}'" echo "[INFO] Software Name '${{ env.SOFTWARE_NAME }}'" echo "[INFO] Software Version '${{ env.SOFTWARE_VERSION }}'" - ve1/bin/pytest tests/functional/step_defs/test_submitted_charts.py --log-cli-level=WARNING --tb=short + ve1/bin/behave tests/functional/behave_features/ --tags=version-change --logging-level=WARNING --no-capture --no-color - name: Approve PR id: approve_pr diff --git a/.github/workflows/version_check.yml b/.github/workflows/version_check.yml index 64aa0503f0..33c788def7 100644 --- a/.github/workflows/version_check.yml +++ b/.github/workflows/version_check.yml @@ -10,9 +10,13 @@ on: # 2. Runs this workflow whether or not a version change has occurred # 3. By default dperaza and mmulholla are tagged in any issues raised. dry-run: - description: "Dry Run? (Run tests but create issues in sandbox) {true,false}" + description: "Dry Run? (Unconditionally run tests and create issues in sandbox) {true,false}" required: true default: "true" + update-version: + description: "Dry run also checks and updates software-version file if not charts repository" + required: true + default: "false" vendor-type: description: "Vendor type {all,partner,redhat,community}" required: true @@ -32,14 +36,28 @@ jobs: run: | echo "GITHUB_EVENT_NAME : $GITHUB_EVENT_NAME" echo "GITHUB_REPOSITORY : $GITHUB_REPOSITORY" + echo "dry-run : ${{ github.event.inputs.dry-run }}" + echo "update-version : ${{ github.event.inputs.update-version }}" if [ $GITHUB_EVENT_NAME == 'workflow_dispatch' ]; then echo '::set-output name=run-job::true' + if [ "${{ github.event.inputs.dry-run }}" == "true" ]; then + if [[ "${{ github.event.inputs.update-version }}" == "true" && $GITHUB_REPOSITORY != "openshift-helm-charts/charts" ]]; then + echo '::set-output name=check-version::true' + else + echo '::set-output name=check-version::false' + fi + else + echo '::set-output name=check-version::true' + fi elif [ $GITHUB_REPOSITORY == "openshift-helm-charts/charts" ]; then echo '::set-output name=run-job::true' + echo '::set-output name=check-version::true' else echo '::set-output name=run-job::false' + echo '::set-output name=check-version::false' fi + - name: Install oc if: steps.check_repo.outputs.run-job == 'true' run: | @@ -58,56 +76,67 @@ jobs: id: get_curr_ocp_version run: | OCP_VERSION=$(./oc version -o json | jq '.openshiftVersion') + OCP_VERSION=$(sed -e 's/^"//' -e 's/"$//' <<< $OCP_VERSION) printf "[INFO] Current OCP Version: %s\n" ${OCP_VERSION} echo "::set-output name=curr_ocp_version::${OCP_VERSION}" shell: bash - name: Checkout software-version branch - if: steps.check_repo.outputs.run-job == 'true' + if: steps.check_repo.outputs.check-version == 'true' uses: actions/checkout@v2 with: ref: "software-version" - repository: "openshift-helm-charts/charts" + repository: ${{ github.repository }} - name: Read previous OpenShift version id: get_prev_ocp_version - if: steps.check_repo.outputs.run-job == 'true' + if: steps.check_repo.outputs.check-version == 'true' uses: mikefarah/yq@master with: cmd: yq e '.openshift.release-client-version' software-version.yaml - - name: Compare OpenShift versions - id: compare_ocp_versions + - name: Check if test should run + id: check_test run: | set -euo pipefail if [ "${{ steps.check_repo.outputs.run-job }}" != "true" ]; then echo "::set-output name=run_tests::false" - elif [ "${{ steps.get_curr_ocp_version.outputs.curr_ocp_version }}" = "${{ steps.get_prev_ocp_version.outputs.result }}" ]; then - # No change in the OpenShift version - do not run tests if a scheduled run or dry-run is not set - if [ "${{ github.event_name }}" == 'schedule' || "${{ github.event.inputs.dry-run }}" != 'true']; then + echo "::set-output name=update-version::false" + elif [ "${{ steps.check_repo.outputs.check-version }}" == "true" ]; then + if [ "${{ steps.get_curr_ocp_version.outputs.curr_ocp_version }}" == "${{ steps.get_prev_ocp_version.outputs.result }}" ]; then + # No change in the OpenShift versions. printf "OpenShift version has not changed since last run: '%s' -> '%s'\n" "${{ steps.get_prev_ocp_version.outputs.result }}" "${{ steps.get_curr_ocp_version.outputs.curr_ocp_version }}" - echo "::set-output name=run_tests::false" + echo "::set-output name=update-version::false" + if [ "${{ github.event.inputs.dry-run }}" == "true" ]; then + echo "Openshift version has not changed but run anyaway as dry-run is set" + echo "::set-output name=run_tests::true" + else + echo "Openshift version has not changed do not run tests" + echo "::set-output name=run_tests::false" + fi else + printf "OpenShift version has changed since last run: '%s' -> '%s'\n" "${{ steps.get_prev_ocp_version.outputs.result }}" "${{ steps.get_curr_ocp_version.outputs.curr_ocp_version }}" echo "::set-output name=run_tests::true" + echo "::set-output name=update-version::true" fi else - # New OpenShift version is set - printf "OpenShift version has changed since last run: '%s' -> '%s'\n" "${{ steps.get_prev_ocp_version.outputs.result }}" "${{ steps.get_curr_ocp_version.outputs.curr_ocp_version }}" + # Run whether open shift version has changed or not + echo "Run tests - version check skipped" + echo "::set-output name=update-version::false" echo "::set-output name=run_tests::true" fi shell: bash - name: Update software-version.yaml if: | - steps.compare_ocp_versions.outputs.run_tests == 'true' + steps.check_test.outputs.update-version == 'true' uses: mikefarah/yq@master with: - cmd: yq eval -i '.openshift.release-client-version = ${{ steps.get_curr_ocp_version.outputs.curr_ocp_version }}' 'software-version.yaml' + cmd: yq eval -i '.openshift.release-client-version = "${{ steps.get_curr_ocp_version.outputs.curr_ocp_version }}"' 'software-version.yaml' - name: Push software-version.yaml if: | - steps.compare_ocp_versions.outputs.run_tests == 'true' && - (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.dry-run != 'true')) + steps.check_test.outputs.update-version == 'true' run: | COMMIT_MESSAGE=$(printf "software-version.yaml: Update OpenShift version from '%s' to '%s'" "${{ steps.get_prev_ocp_version.outputs.result }}" "${{ steps.get_curr_ocp_version.outputs.curr_ocp_version }}") git remote -v @@ -120,7 +149,7 @@ jobs: - name: Checkout main branch if: | - steps.compare_ocp_versions.outputs.run_tests == 'true' + steps.check_test.outputs.run_tests == 'true' uses: actions/checkout@v2 with: ref: "main" @@ -129,14 +158,14 @@ jobs: - name: Set up Python 3.x Part 1 if: | - steps.compare_ocp_versions.outputs.run_tests == 'true' + steps.check_test.outputs.run_tests == 'true' uses: actions/setup-python@v2 with: python-version: "3.9" - name: Set up Python 3.x Part 2 if: | - steps.compare_ocp_versions.outputs.run_tests == 'true' + steps.check_test.outputs.run_tests == 'true' run: | # set up python python3 -m venv ve1 @@ -145,7 +174,7 @@ jobs: - name: (Manual) Run tests on existing charts if: | - github.event_name == 'workflow_dispatch' && steps.compare_ocp_versions.outputs.run_tests == 'true' + github.event_name == 'workflow_dispatch' && steps.check_test.outputs.run_tests == 'true' env: CLUSTER_TOKEN: ${{ secrets.CLUSTER_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -162,19 +191,18 @@ jobs: printf "[INFO] Notify ID: '%s'\n" "${{ env.NOTIFY_ID }}" printf "[INFO] Software Name: '%s'\n" "${{ env.SOFTWARE_NAME }}" printf "[INFO] Software Version: '%s'\n" "${{ env.SOFTWARE_VERSION }}" - ve1/bin/pytest tests/functional/step_defs/test_submitted_charts.py --log-cli-level=INFO --tb=short + ve1/bin/behave tests/functional/behave_features/ --tags=version-change --logging-level=INFO --no-capture --no-color - name: (Schedule) Run tests on existing charts id: run-schedule-tests if: | - github.event_name == 'schedule' && steps.compare_ocp_versions.outputs.run_tests == 'true' + github.event_name == 'schedule' && steps.check_test.outputs.run_tests == 'true' env: CLUSTER_TOKEN: ${{ secrets.CLUSTER_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BOT_NAME: ${{ secrets.BOT_NAME }} BOT_TOKEN: ${{ secrets.BOT_TOKEN }} - # XXX: set to false when ready to launch notifications - DRY_RUN: "true" + DRY_RUN: "false" VENDOR_TYPE: "all" NOTIFY_ID: "" SOFTWARE_NAME: "OpenShift" @@ -185,22 +213,24 @@ jobs: printf "[INFO] Notify ID: '%s'\n" "${{ env.NOTIFY_ID }}" printf "[INFO] Software Name: '%s'\n" "${{ env.SOFTWARE_NAME }}" printf "[INFO] Software Version: '%s'\n" "${{ env.SOFTWARE_VERSION }}" - ve1/bin/pytest tests/functional/step_defs/test_submitted_charts.py --log-cli-level=INFO --tb=short + ve1/bin/behave tests/functional/behave_features/ --tags=version-change --logging-level=INFO --no-capture --no-color - name: Send message to slack channel id: notify - if: always() && github.event_name == 'schedule' && steps.compare_cv_versions.outputs.run_tests == 'true' + if: always() && github.event_name == 'schedule' && steps.check_test.outputs.run_tests == 'true' uses: archive/github-actions-slack@v2.0.0 with: slack-bot-user-oauth-access-token: ${{ secrets.SLACK_BOT_USER_OAUTH_ACCESS_TOKEN }} slack-channel: C02979BDUPL - slack-text: ${{ steps.run-schedule-tests.conclusion }}! Nightly run after OCP version update detected. See '${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}' + slack-text: ${{ steps.run-schedule-tests.conclusion }}! Nightly run after an OpenShift version update to ${{ steps.get_curr_ocp_version.outputs.curr_ocp_version }} was detected. See '${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}' - name: Result from "Send Message to slack channel" run: echo "The result was ${{ steps.notify.outputs.slack-result }}" check-chart-verifier: + if: ${{ always() }} + needs: check-ocp name: Check Chart Verifier Version runs-on: ubuntu-20.04 steps: @@ -209,15 +239,25 @@ jobs: run: | echo "GITHUB_EVENT_NAME : $GITHUB_EVENT_NAME" echo "GITHUB_REPOSITORY : $GITHUB_REPOSITORY" + echo "dry-run : ${{ github.event.inputs.dry-run }}" + echo "update-version : ${{ github.event.inputs.update-version }}" if [ $GITHUB_EVENT_NAME == 'workflow_dispatch' ]; then echo '::set-output name=run-job::true' - echo "workflow_dispatch: set run-job to true" + if [ "${{ github.event.inputs.dry-run }}" == "true" ]; then + if [[ "${{ github.event.inputs.update-version }}" == "true" && $GITHUB_REPOSITORY != "openshift-helm-charts/charts" ]]; then + echo '::set-output name=check-version::true' + else + echo '::set-output name=check-version::false' + fi + else + echo '::set-output name=check-version::true' + fi elif [ $GITHUB_REPOSITORY == "openshift-helm-charts/charts" ]; then echo '::set-output name=run-job::true' - echo "charts repo: set run-job to true" + echo '::set-output name=check-version::true' else echo '::set-output name=run-job::false' - echo "set run-job to false" + echo '::set-output name=check-version::false' fi - name: Get current Chart Verifier version @@ -231,51 +271,63 @@ jobs: shell: bash - name: Checkout software-version branch - if: steps.check_repo.outputs.run-job == 'true' + if: steps.check_repo.outputs.check-version == 'true' uses: actions/checkout@v2 with: ref: "software-version" - repository: "openshift-helm-charts/charts" + repository: ${{ github.repository }} - name: Read previous Chart Verifier digest - if: steps.check_repo.outputs.run-job == 'true' + if: steps.check_repo.outputs.check-version == 'true' id: get_prev_cv_digest uses: mikefarah/yq@master with: cmd: yq e '.chart-verifier.latest-manifest-digest' software-version.yaml - name: Compare Chart Verifier versions - id: compare_cv_versions + id: check_test run: | set -euo pipefail if [ "${{ steps.check_repo.outputs.run-job }}" != "true" ]; then echo "::set-output name=run_tests::false" - elif [ "${{ steps.get_curr_cv_version.outputs.current_cv_digest }}" == "${{ steps.get_prev_cv_digest.outputs.result }}" ]; then - # No change in the Chart Verifier image - do not run tests if a scheduled run or dry-run is not set - if [ "${{ github.event_name }}" == 'schedule' || "${{ github.event.inputs.dry-run }}" != 'true']; then - printf "Chart Verifier has not changed since last run: '%s' -> '%s'\n" "${{ steps.get_prev_cv_digest.outputs.result }}" "${{ steps.get_curr_cv_version.outputs.current_cv_digest }}" - echo "::set-output name=run_tests::false" - else - echo "::set-output name=run_tests::true" - fi + echo "::set-output name=update-version::false" + elif [ "${{ steps.check_repo.outputs.check-version }}" == "true" ]; then + if [ "${{ steps.get_curr_cv_version.outputs.current_cv_digest }}" == "${{ steps.get_prev_cv_digest.outputs.result }}" ]; then + # No change in the Chart Verifier image - do not run tests if a scheduled run or dry-run is not set + printf "Chart Verifier has not changed since last run: '%s' -> '%s'\n" "${{ steps.get_prev_cv_digest.outputs.result }}" "${{ steps.get_curr_cv_version.outputs.current_cv_digest }}" + echo "::set-output name=update-version::false" + if [ "${{ github.event.inputs.dry-run }}" == "true" ]; then + echo "Chart Verifier image has not changed but run anyaway as dry-run is set" + echo "::set-output name=run_tests::true" + else + echo "Chart Verifier image has not changed do not run tests" + echo "::set-output name=run_tests::false" + fi + else + # New Chart Verifier image is found + printf "Chart Verifier has changed since last run: '%s' -> '%s'\n" "${{ steps.get_prev_cv_digest.outputs.result }}" "${{ steps.get_curr_cv_version.outputs.current_cv_digest }}" + echo "::set-output name=run_tests::true" + echo "::set-output name=update-version::true" + fi else - # New Chart Verifier image is found - printf "Chart Verifier has changed since last run: '%s' -> '%s'\n" "${{ steps.get_prev_cv_digest.outputs.result }}" "${{ steps.get_curr_cv_version.outputs.current_cv_digest }}" + # Run whether Chart Verifier image has changed or not + echo "Run tests - version check skipped" + echo "::set-output name=update-version::false" echo "::set-output name=run_tests::true" fi + shell: bash - name: Update software-version.yaml if: | - steps.compare_cv_versions.outputs.run_tests == 'true' + steps.check_test.outputs.update-version == 'true' uses: mikefarah/yq@master with: cmd: yq eval -i '.chart-verifier.latest-manifest-digest = ${{ steps.get_curr_cv_version.outputs.current_cv_digest }}' 'software-version.yaml' - name: Push software-version.yaml if: | - steps.compare_cv_versions.outputs.run_tests == 'true' && - (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.dry-run != 'true')) + steps.check_test.outputs.update-version == 'true' run: | COMMIT_MESSAGE=$(printf "software-version.yaml: Update chart-verifier version from '%s' to '%s'" "${{ steps.get_prev_ocp_version.outputs.result }}" "${{ steps.get_curr_cv_version.outputs.current_cv_digest }}") git remote -v @@ -288,7 +340,7 @@ jobs: - name: Checkout charts main branch if: | - steps.compare_cv_versions.outputs.run_tests == 'true' + steps.check_test.outputs.run_tests == 'true' uses: actions/checkout@v2 with: ref: "main" @@ -297,14 +349,14 @@ jobs: - name: Set up Python 3.x Part 1 if: | - steps.compare_cv_versions.outputs.run_tests == 'true' + steps.check_test.outputs.run_tests == 'true' uses: actions/setup-python@v2 with: python-version: "3.9" - name: Set up Python 3.x Part 2 if: | - steps.compare_cv_versions.outputs.run_tests == 'true' + steps.check_test.outputs.run_tests == 'true' run: | # set up python pwd @@ -314,7 +366,7 @@ jobs: - name: (Manual) Run tests on existing charts if: | - github.event_name == 'workflow_dispatch' + github.event_name == 'workflow_dispatch' && steps.check_test.outputs.run_tests == 'true' env: CLUSTER_TOKEN: ${{ secrets.CLUSTER_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -331,12 +383,12 @@ jobs: printf "[INFO] Notify ID: '%s'\n" "${{ env.NOTIFY_ID }}" printf "[INFO] Software Name: '%s'\n" "${{ env.SOFTWARE_NAME }}" printf "[INFO] Software Version: '%s'\n" "${{ env.SOFTWARE_VERSION }}" - ve1/bin/pytest tests/functional/step_defs/test_submitted_charts.py --log-cli-level=INFO --tb=short + ve1/bin/behave tests/functional/behave_features/ --tags=version-change --logging-level=INFO --no-capture --no-color - name: (Schedule) Run tests on existing charts id: run-schedule-tests if: | - github.event_name == 'schedule' && steps.compare_cv_versions.outputs.run_tests == 'true' + github.event_name == 'schedule' && steps.check_test.outputs.run_tests == 'true' env: CLUSTER_TOKEN: ${{ secrets.CLUSTER_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -354,17 +406,21 @@ jobs: printf "[INFO] Notify ID: '%s'\n" "${{ env.NOTIFY_ID }}" printf "[INFO] Software Name: '%s'\n" "${{ env.SOFTWARE_NAME }}" printf "[INFO] Software Version: '%s'\n" "${{ env.SOFTWARE_VERSION }}" - ve1/bin/pytest tests/functional/step_defs/test_submitted_charts.py --log-cli-level=INFO --tb=short + ve1/bin/behave tests/functional/behave_features/ --tags=version-change --logging-level=INFO --no-capture --no-color - name: Send message to slack channel id: notify - if: always() && github.event_name == 'schedule' && steps.compare_cv_versions.outputs.run_tests == 'true' + if: always() && github.event_name == 'schedule' && steps.check_test.outputs.run_tests == 'true' uses: archive/github-actions-slack@v2.0.0 + env: + SOFTWARE_NAME: "chart-verifier" + SOFTWARE_VERSION: ${{ steps.get_curr_cv_version.outputs.current_cv_digest }} with: slack-bot-user-oauth-access-token: ${{ secrets.SLACK_BOT_USER_OAUTH_ACCESS_TOKEN }} slack-channel: C02979BDUPL - slack-text: ${{ steps.run-schedule-tests.conclusion }}! Nightly run after chartverifier version update detected. See '${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}' + slack-text: ${{ steps.run-schedule-tests.conclusion }}! Nightly run after a chart-verifier version update to ${{ steps.get_curr_cv_version.outputs.current_cv_digest }} was detected. See '${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}' - name: Result from "Send Message to slack channel" + if: always() && github.event_name == 'schedule' && steps.check_test.outputs.run_tests == 'true' run: echo "The result was ${{ steps.notify.outputs.slack-result }}" diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 36dd5b5a09..c9eb576899 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -34,3 +34,4 @@ toml==0.10.2 urllib3==1.26.5 websocket-client==1.2.1 analytics-python==1.4.0 +behave==1.2.6 diff --git a/scripts/src/chartrepomanager/indexannotations.py b/scripts/src/chartrepomanager/indexannotations.py index 1b8ee44758..d2b593cf9c 100644 --- a/scripts/src/chartrepomanager/indexannotations.py +++ b/scripts/src/chartrepomanager/indexannotations.py @@ -1,18 +1,22 @@ import sys import semantic_version +import requests +import yaml sys.path.append('../') from report import report_info -kubeOpenShiftVersionMap = {"1.13": "4.1", - "1.14": "4.2", - "1.16": "4.3", - "1.17": "4.4", - "1.18": "4.5", - "1.19": "4.6", - "1.20": "4.7", - "1.21": "4.8", - "1.22": "4.9"} +kubeOpenShiftVersionMap = {} + +def getKubVersionMap(): + + if not kubeOpenShiftVersionMap: + content = requests.get("https://github.com/redhat-certification/chart-verifier/blob/main/internal/tool/kubeOpenShiftVersionMap.yaml?raw=true") + version_data = yaml.safe_load(content.text) + for kubeVersion in version_data["versions"]: + kubeOpenShiftVersionMap[kubeVersion["kube-version"]] = kubeVersion["ocp-version"] + + return kubeOpenShiftVersionMap def getOCPVersions(kubeVersion): @@ -56,21 +60,26 @@ def getOCPVersions(kubeVersion): minOCP = "" maxOCP = "" + getKubVersionMap() for kubeVersionKey in kubeOpenShiftVersionMap : + #print(f"\n Map entry : {kubeVersionKey}: {kubeOpenShiftVersionMap[kubeVersionKey]}") + #print(f" MinOCP : {minOCP}, maxOCP: {maxOCP}") coercedKubeVersionKey = semantic_version.Version.coerce(kubeVersionKey) - if minOCP == "" and coercedKubeVersionKey in semantic_version.NpmSpec(checkKubeVersion): - minOCP = kubeOpenShiftVersionMap[kubeVersionKey] - print(f" Found min : {kubeVersion}: {minOCP}") - elif coercedKubeVersionKey in semantic_version.NpmSpec(checkKubeVersion): - maxOCP = kubeOpenShiftVersionMap[kubeVersionKey] - print(f" Found new Max : {kubeVersion}: {maxOCP}") + if coercedKubeVersionKey in semantic_version.NpmSpec(checkKubeVersion): + coercedOCPVersionValue = semantic_version.Version.coerce(kubeOpenShiftVersionMap[kubeVersionKey]) + if minOCP == "" or semantic_version.Version.coerce(minOCP) > coercedOCPVersionValue: + minOCP = kubeOpenShiftVersionMap[kubeVersionKey] + #print(f" Found new min : {checkKubeVersion}: {minOCP}") + if maxOCP == "" or semantic_version.Version.coerce(maxOCP) < coercedOCPVersionValue: + maxOCP = kubeOpenShiftVersionMap[kubeVersionKey] + #print(f" Found new Max : {checkKubeVersion}: {maxOCP}") # check if minOCP is open ended if minOCP != "" and semantic_version.Version("1.999.999") in semantic_version.NpmSpec(checkKubeVersion): ocp_versions = f">={minOCP}" elif minOCP == "": ocp_versions = "N/A" - elif maxOCP == "": + elif maxOCP == "" or maxOCP == minOCP: ocp_versions = minOCP else: ocp_versions = f"{minOCP} - {maxOCP}" diff --git a/scripts/src/checkprcontent/checkpr.py b/scripts/src/checkprcontent/checkpr.py index 7976d0e950..65fadcf9e3 100644 --- a/scripts/src/checkprcontent/checkpr.py +++ b/scripts/src/checkprcontent/checkpr.py @@ -84,6 +84,24 @@ def check_provider_delivery(report_in_pr,num_files_in_pr,report_file_match): print(f"::set-output name=providerDelivery::False") print(f"[INFO] providerDelivery is a no-go") +def get_file_match_compiled_patterns(): + """Return a tuple of patterns, where the first can be used to match any file in a chart PR + and the second can be used to match a valid report file within a chart PR. The patterns + match based on the relative path of a file to the base repository + + Both patterns capture chart type, chart vendor, chart name and chart version from the file path.. + + Examples of valid file paths are: + + charts/partners/hashicorp/vault/0.20.0/ + charts/partners/hashicorp/vault/0.20.0//report.yaml + """ + + pattern = re.compile(r"charts/"+TYPE_MATCH_EXPRESSION+"/([\w-]+)/([\w-]+)/([\w\.-]+)/.*") + reportpattern = re.compile(r"charts/"+TYPE_MATCH_EXPRESSION+"/([\w-]+)/([\w-]+)/([\w\.-]+)/report.yaml") + + return pattern,reportpattern + def ensure_only_chart_is_modified(api_url, repository, branch): # api_url https://api.github.com/repos///pulls/1 @@ -95,8 +113,7 @@ def ensure_only_chart_is_modified(api_url, repository, branch): files_api_url = f'{api_url}/files' headers = {'Accept': 'application/vnd.github.v3+json'} r = requests.get(files_api_url, headers=headers) - pattern = re.compile(r"charts/"+TYPE_MATCH_EXPRESSION+"/([\w-]+)/([\w-]+)/([\w\.-]+)/.*") - reportpattern = re.compile(r"charts/"+TYPE_MATCH_EXPRESSION+"/([\w-]+)/([\w-]+)/([\w\.-]+)/report.yaml") + pattern,reportpattern = get_file_match_compiled_patterns() page_number = 1 max_page_size,page_size = 100,100 matches_found = 0 @@ -129,9 +146,10 @@ def ensure_only_chart_is_modified(api_url, repository, branch): if matches_found == 1: pattern_match = match elif pattern_match.groups() != match.groups(): - msg = f"[ERROR] PR must only include one chart" + msg = "[ERROR] A PR must contain only one chart. Current PR includes files for multiple charts." print(msg) print(f"::set-output name=pr-content-error-message::{msg}") + exit(1) if none_chart_files: if file_count > 1 or "OWNERS" not in none_chart_files: #OWNERS not present or preset but not the only file diff --git a/scripts/src/indexfile/index.py b/scripts/src/indexfile/index.py index 97f20049ff..271620cc30 100644 --- a/scripts/src/indexfile/index.py +++ b/scripts/src/indexfile/index.py @@ -2,15 +2,16 @@ import json import requests import yaml +import semantic_version +import sys -def _make_http_request(method, url, body=None, params={}, headers={}, verbose=False): - method_map = {"get": requests.get, - "post": requests.post, - "put": requests.put, - "delete": requests.delete, - "patch": requests.patch} - request_method = method_map[method] - response = request_method(url, params=params, headers=headers, json=body) +sys.path.append('../') +from chartrepomanager import indexannotations + +INDEX_FILE = "https://charts.openshift.io/index.yaml" + +def _make_http_request(url, body=None, params={}, headers={}, verbose=False): + response = requests.get(url, params=params, headers=headers, json=body) if verbose: print(json.dumps(headers, indent=4, sort_keys=True)) print(json.dumps(body, indent=4, sort_keys=True)) @@ -18,14 +19,13 @@ def _make_http_request(method, url, body=None, params={}, headers={}, verbose=Fa print(response.text) return response.text -def _load_index_yaml(url): - - yaml_text = _make_http_request('get', url) +def _load_index_yaml(): + yaml_text = _make_http_request(INDEX_FILE) dct = yaml.safe_load(yaml_text) return dct def get_chart_info(tar_name): - index_dct = _load_index_yaml("https://charts.openshift.io/index.yaml") + index_dct = _load_index_yaml() for entry, charts in index_dct["entries"].items(): if tar_name.startswith(entry): for chart in charts: @@ -38,5 +38,97 @@ def get_chart_info(tar_name): print(f"[INFO] match not found: {tar_name}") return "","","","" +def get_charts_info(): + chart_info_list = [] + + index_dct = _load_index_yaml() + for entry, charts in index_dct["entries"].items(): + for chart in charts: + chart_info = {} + chart_info["name"] = chart['name'] + chart_info["version"] = chart["version"] + chart_info["providerType"] = chart["annotations"]["charts.openshift.io/providerType"] + chart_info["provider"] = entry.removesuffix(f'-{chart["name"]}') + #print(f'[INFO] found chart : {chart_info["provider"]} {chart["name"]} {chart["version"]} ') + if 'charts.openshift.io/supportedOpenShiftVersions' in chart["annotations"]: + chart_info["supportedOCP"] = chart["annotations"]["charts.openshift.io/supportedOpenShiftVersions"] + else: + chart_info["supportedOCP"] = "" + if "kubeVersion" in chart: + chart_info["kubeVersion"] = chart["kubeVersion"] + else: + chart_info["kubeVersion"] ="" + chart_info_list.append(chart_info) + + return chart_info_list + +def get_latest_charts(): + chart_list = get_charts_info() + + print(f"{len(chart_list)} charts found in Index file") + + chart_in_process = {"name" : ""} + chart_latest_version = "" + latest_charts = [] + + for index,chart in enumerate(chart_list): + chart_name = chart["name"] + #print(f'[INFO] look for latest chart : {chart_name} {chart["version"]}') + if chart_name == chart_in_process["name"]: + new_version = semantic_version.Version.coerce(chart["version"]) + #print(f' [INFO] compare chart versions : {new_version}({chart["version"]}) : {chart_latest_version}') + if new_version > chart_latest_version: + #print(f' [INFO] a new latest chart version : {new_version}') + chart_latest_version = new_version + chart_in_process = chart + else: + if chart_in_process["name"] != "": + #print(f' [INFO] chart completed : {chart_in_process["name"]} {chart_in_process["version"]}') + latest_charts.append(chart_in_process) + + #print(f'[INFO] new chart found : {chart_name} {chart["version"]}') + chart_in_process = chart + chart_version = chart["version"] + if chart_version.startswith("v"): + chart_version = chart_version[1:] + chart_latest_version = semantic_version.Version.coerce(chart_version) + else: + chart_in_process = chart + + if index+1 == len(chart_list): + #print(f' [INFO] last chart completed : {chart_in_process["name"]} {chart_in_process["version"]}') + latest_charts.append(chart_in_process) + + return latest_charts + + if __name__ == "__main__": - get_chart_info("redhat-dotnet-0.0.1") \ No newline at end of file + get_chart_info("redhat-dotnet-0.0.1") + + chart_list = get_latest_charts() + + for chart in chart_list: + print(f'[INFO] found latest chart : {chart["name"]} {chart["version"]}') + + + OCP_VERSION = semantic_version.Version.coerce("4.11") + + for chart in chart_list: + if "supportedOCP" in chart and chart["supportedOCP"] != "N/A" and chart["supportedOCP"] != "": + if OCP_VERSION in semantic_version.NpmSpec(chart["supportedOCP"]): + print(f'PASS: Chart supported OCP version {chart["supportedOCP"]} includes: {OCP_VERSION}') + else: + print(f' ERROR: Chart supported OCP version {chart["supportedOCP"]} does not include {OCP_VERSION}') + elif "kubeVersion" in chart and chart["kubeVersion"] != "": + supportedOCPVersion = indexannotations.getOCPVersions(chart["kubeVersion"]) + if OCP_VERSION in semantic_version.NpmSpec(supportedOCPVersion): + print(f'PASS: Chart kubeVersion {chart["kubeVersion"]} (OCP: {supportedOCPVersion}) includes OCP version: {OCP_VERSION}') + else: + print(f' ERROR: Chart kubeVersion {chart["kubeVersion"]} (OCP: {supportedOCPVersion}) does not include {OCP_VERSION}') + + + + + + + diff --git a/scripts/src/pullrequest/prartifact.py b/scripts/src/pullrequest/prartifact.py index a79174e650..f290a7c7b9 100644 --- a/scripts/src/pullrequest/prartifact.py +++ b/scripts/src/pullrequest/prartifact.py @@ -7,13 +7,15 @@ import requests +sys.path.append('../') +from checkprcontent import checkpr + # TODO(baijum): Move this code under chartsubmission.chart module def get_modified_charts(api_url): files_api_url = f'{api_url}/files' headers = {'Accept': 'application/vnd.github.v3+json'} r = requests.get(files_api_url, headers=headers) - pattern = re.compile(r"charts/(\w+)/([\w-]+)/([\w-]+)/([\w\.]+)/.*") - count = 0 + pattern,_ = checkpr.get_file_match_compiled_patterns() for f in r.json(): m = pattern.match(f["filename"]) if m: @@ -25,9 +27,11 @@ def get_modified_charts(api_url): def save_metadata(directory, vendor_label, chart, number): with open(os.path.join(directory, "vendor"), "w") as fd: + print(f"add {directory}/vendor as {vendor_label}") fd.write(vendor_label) with open(os.path.join(directory, "chart"), "w") as fd: + print(f"add {directory}/chart as {chart}") fd.write(chart) with open(os.path.join(directory, "NR"), "w") as fd: diff --git a/scripts/src/saforcharttesting/saforcharttesting.py b/scripts/src/saforcharttesting/saforcharttesting.py index dc80233219..5724995216 100644 --- a/scripts/src/saforcharttesting/saforcharttesting.py +++ b/scripts/src/saforcharttesting/saforcharttesting.py @@ -6,6 +6,7 @@ import argparse import subprocess import tempfile +import re from string import Template namespace_template = """\ @@ -211,7 +212,8 @@ def delete_clusterrolebinding(name): sys.exit(1) def write_sa_token(namespace, token): - sa_found = False + secret_found = False + secrets = [] for i in range(7): out = subprocess.run(["oc", "get", "serviceaccount", namespace, "-n", namespace, "-o", "json"], capture_output=True) stdout = out.stdout.decode("utf-8") @@ -223,15 +225,29 @@ def write_sa_token(namespace, token): else: sa = json.loads(stdout) if len(sa["secrets"]) >= 2: - sa_found = True + secrets = sa["secrets"] + secret_found = True break - time.sleep(10) + else: + pattern = r'Tokens:\s+([A-Za-z0-9-]+)' + dout = subprocess.run(["oc", "describe", "serviceaccount", namespace, "-n", namespace], capture_output=True) + dstdout = dout.stdout.decode("utf-8") + match = re.search(pattern, dstdout) + if match: + token_name = match.group(1) + else: + print("[ERROR] Token not found, Exiting") + sys.exit(1) + secrets.append({"name": token_name}) + secret_found = True + break + time.sleep(10) - if not sa_found: + if not secret_found: print("[ERROR] retrieving ServiceAccount:", namespace, stderr) sys.exit(1) - for secret in sa["secrets"]: + for secret in secrets: out = subprocess.run(["oc", "get", "secret", secret["name"], "-n", namespace, "-o", "json"], capture_output=True) stdout = out.stdout.decode("utf-8") if out.returncode != 0: diff --git a/tests/data/HC-16/dash-in-version/partner/report.yaml b/tests/data/HC-16/dash-in-version/partner/report.yaml new file mode 100644 index 0000000000..b4aec81305 --- /dev/null +++ b/tests/data/HC-16/dash-in-version/partner/report.yaml @@ -0,0 +1,89 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.7.0 + profile: + VendorType: partner + version: v1.1 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/psql-service-0.1.10-1.tgz?raw=true + digests: + chart: sha256:db482b4d90349c6b276ba27f581720cc62fa7bea05184b5bfd840844178a8da6 + package: c8635dcdc8f8493abbdef85305be55c9d5dbf3a44495a88a8285c9a0d0c63408 + lastCertifiedTimestamp: "2022-06-22T15:54:59.964823+00:00" + testedOpenShiftVersion: "4.10" + supportedOpenShiftVersions: '>=4.7' + providerControlledDelivery: false + chart: + name: psql-service + home: "" + sources: [] + version: 0.1.10-1 + description: A Helm chart for a RedHat Certified PSQL + keywords: [] + maintainers: [] + icon: "" + apiversion: v2 + condition: "" + tags: "" + appversion: 10.0.0 + deprecated: false + annotations: + charts.openshift.io/archs: x86_64 + charts.openshift.io/name: PSQL RedHat Demo Chart + charts.openshift.io/provider: RedHat + charts.openshift.io/supportURL: https://github.com/dperaza4dustbit/helm-chart + kubeversion: '>=1.20.0' + dependencies: [] + type: application + chart-overrides: "" +results: + - check: v1.0/contains-test + type: Mandatory + outcome: PASS + reason: Chart test files exist + - check: v1.0/contains-values + type: Mandatory + outcome: PASS + reason: Values file exist + - check: v1.1/has-kubeversion + type: Mandatory + outcome: PASS + reason: Kubernetes version specified + - check: v1.0/not-contains-crds + type: Mandatory + outcome: PASS + reason: Chart does not contain CRDs + - check: v1.0/not-contain-csi-objects + type: Mandatory + outcome: PASS + reason: CSI objects do not exist + - check: v1.0/chart-testing + type: Mandatory + outcome: PASS + reason: Chart tests have passed + - check: v1.0/has-readme + type: Mandatory + outcome: PASS + reason: Chart has a README + - check: v1.0/is-helm-v3 + type: Mandatory + outcome: PASS + reason: API version is V2, used in Helm 3 + - check: v1.0/contains-values-schema + type: Mandatory + outcome: PASS + reason: Values schema file exist + - check: v1.0/helm-lint + type: Mandatory + outcome: PASS + reason: Helm lint successful + - check: v1.0/images-are-certified + type: Mandatory + outcome: PASS + reason: 'Image is Red Hat certified : registry.access.redhat.com/rhscl/postgresql-10-rhel7:1-66' + - check: v1.0/required-annotations-present + type: Mandatory + outcome: PASS + reason: All required annotations present + diff --git a/tests/data/HC-16/dash-in-version/redhat/report.yaml b/tests/data/HC-16/dash-in-version/redhat/report.yaml new file mode 100644 index 0000000000..eff7b99361 --- /dev/null +++ b/tests/data/HC-16/dash-in-version/redhat/report.yaml @@ -0,0 +1,89 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.7.0 + profile: + VendorType: redhat + version: v1.1 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/psql-service-0.1.10-1.tgz?raw=true + digests: + chart: sha256:db482b4d90349c6b276ba27f581720cc62fa7bea05184b5bfd840844178a8da6 + package: c8635dcdc8f8493abbdef85305be55c9d5dbf3a44495a88a8285c9a0d0c63408 + lastCertifiedTimestamp: "2022-06-22T17:21:03.1478+00:00" + testedOpenShiftVersion: "4.10" + supportedOpenShiftVersions: '>=4.7' + providerControlledDelivery: false + chart: + name: psql-service + home: "" + sources: [] + version: 0.1.10-1 + description: A Helm chart for a RedHat Certified PSQL + keywords: [] + maintainers: [] + icon: "" + apiversion: v2 + condition: "" + tags: "" + appversion: 10.0.0 + deprecated: false + annotations: + charts.openshift.io/archs: x86_64 + charts.openshift.io/name: PSQL RedHat Demo Chart + charts.openshift.io/provider: RedHat + charts.openshift.io/supportURL: https://github.com/dperaza4dustbit/helm-chart + kubeversion: '>=1.20.0' + dependencies: [] + type: application + chart-overrides: "" +results: + - check: v1.0/has-readme + type: Mandatory + outcome: PASS + reason: Chart has a README + - check: v1.0/is-helm-v3 + type: Mandatory + outcome: PASS + reason: API version is V2, used in Helm 3 + - check: v1.0/not-contains-crds + type: Mandatory + outcome: PASS + reason: Chart does not contain CRDs + - check: v1.0/not-contain-csi-objects + type: Mandatory + outcome: PASS + reason: CSI objects do not exist + - check: v1.0/images-are-certified + type: Mandatory + outcome: PASS + reason: 'Image is Red Hat certified : registry.access.redhat.com/rhscl/postgresql-10-rhel7:1-66' + - check: v1.0/chart-testing + type: Mandatory + outcome: PASS + reason: Chart tests have passed + - check: v1.0/required-annotations-present + type: Mandatory + outcome: PASS + reason: All required annotations present + - check: v1.0/contains-test + type: Mandatory + outcome: PASS + reason: Chart test files exist + - check: v1.0/contains-values + type: Mandatory + outcome: PASS + reason: Values file exist + - check: v1.0/contains-values-schema + type: Mandatory + outcome: PASS + reason: Values schema file exist + - check: v1.1/has-kubeversion + type: Mandatory + outcome: PASS + reason: Kubernetes version specified + - check: v1.0/helm-lint + type: Mandatory + outcome: PASS + reason: Helm lint successful + diff --git a/tests/data/HC-17/dash-in-version/partner/report.yaml b/tests/data/HC-17/dash-in-version/partner/report.yaml new file mode 100644 index 0000000000..b4aec81305 --- /dev/null +++ b/tests/data/HC-17/dash-in-version/partner/report.yaml @@ -0,0 +1,89 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.7.0 + profile: + VendorType: partner + version: v1.1 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/psql-service-0.1.10-1.tgz?raw=true + digests: + chart: sha256:db482b4d90349c6b276ba27f581720cc62fa7bea05184b5bfd840844178a8da6 + package: c8635dcdc8f8493abbdef85305be55c9d5dbf3a44495a88a8285c9a0d0c63408 + lastCertifiedTimestamp: "2022-06-22T15:54:59.964823+00:00" + testedOpenShiftVersion: "4.10" + supportedOpenShiftVersions: '>=4.7' + providerControlledDelivery: false + chart: + name: psql-service + home: "" + sources: [] + version: 0.1.10-1 + description: A Helm chart for a RedHat Certified PSQL + keywords: [] + maintainers: [] + icon: "" + apiversion: v2 + condition: "" + tags: "" + appversion: 10.0.0 + deprecated: false + annotations: + charts.openshift.io/archs: x86_64 + charts.openshift.io/name: PSQL RedHat Demo Chart + charts.openshift.io/provider: RedHat + charts.openshift.io/supportURL: https://github.com/dperaza4dustbit/helm-chart + kubeversion: '>=1.20.0' + dependencies: [] + type: application + chart-overrides: "" +results: + - check: v1.0/contains-test + type: Mandatory + outcome: PASS + reason: Chart test files exist + - check: v1.0/contains-values + type: Mandatory + outcome: PASS + reason: Values file exist + - check: v1.1/has-kubeversion + type: Mandatory + outcome: PASS + reason: Kubernetes version specified + - check: v1.0/not-contains-crds + type: Mandatory + outcome: PASS + reason: Chart does not contain CRDs + - check: v1.0/not-contain-csi-objects + type: Mandatory + outcome: PASS + reason: CSI objects do not exist + - check: v1.0/chart-testing + type: Mandatory + outcome: PASS + reason: Chart tests have passed + - check: v1.0/has-readme + type: Mandatory + outcome: PASS + reason: Chart has a README + - check: v1.0/is-helm-v3 + type: Mandatory + outcome: PASS + reason: API version is V2, used in Helm 3 + - check: v1.0/contains-values-schema + type: Mandatory + outcome: PASS + reason: Values schema file exist + - check: v1.0/helm-lint + type: Mandatory + outcome: PASS + reason: Helm lint successful + - check: v1.0/images-are-certified + type: Mandatory + outcome: PASS + reason: 'Image is Red Hat certified : registry.access.redhat.com/rhscl/postgresql-10-rhel7:1-66' + - check: v1.0/required-annotations-present + type: Mandatory + outcome: PASS + reason: All required annotations present + diff --git a/tests/data/HC-17/dash-in-version/redhat/report.yaml b/tests/data/HC-17/dash-in-version/redhat/report.yaml new file mode 100644 index 0000000000..eff7b99361 --- /dev/null +++ b/tests/data/HC-17/dash-in-version/redhat/report.yaml @@ -0,0 +1,89 @@ +apiversion: v1 +kind: verify-report +metadata: + tool: + verifier-version: 1.7.0 + profile: + VendorType: redhat + version: v1.1 + chart-uri: https://github.com/openshift-helm-charts/development/blob/main/tests/data/psql-service-0.1.10-1.tgz?raw=true + digests: + chart: sha256:db482b4d90349c6b276ba27f581720cc62fa7bea05184b5bfd840844178a8da6 + package: c8635dcdc8f8493abbdef85305be55c9d5dbf3a44495a88a8285c9a0d0c63408 + lastCertifiedTimestamp: "2022-06-22T17:21:03.1478+00:00" + testedOpenShiftVersion: "4.10" + supportedOpenShiftVersions: '>=4.7' + providerControlledDelivery: false + chart: + name: psql-service + home: "" + sources: [] + version: 0.1.10-1 + description: A Helm chart for a RedHat Certified PSQL + keywords: [] + maintainers: [] + icon: "" + apiversion: v2 + condition: "" + tags: "" + appversion: 10.0.0 + deprecated: false + annotations: + charts.openshift.io/archs: x86_64 + charts.openshift.io/name: PSQL RedHat Demo Chart + charts.openshift.io/provider: RedHat + charts.openshift.io/supportURL: https://github.com/dperaza4dustbit/helm-chart + kubeversion: '>=1.20.0' + dependencies: [] + type: application + chart-overrides: "" +results: + - check: v1.0/has-readme + type: Mandatory + outcome: PASS + reason: Chart has a README + - check: v1.0/is-helm-v3 + type: Mandatory + outcome: PASS + reason: API version is V2, used in Helm 3 + - check: v1.0/not-contains-crds + type: Mandatory + outcome: PASS + reason: Chart does not contain CRDs + - check: v1.0/not-contain-csi-objects + type: Mandatory + outcome: PASS + reason: CSI objects do not exist + - check: v1.0/images-are-certified + type: Mandatory + outcome: PASS + reason: 'Image is Red Hat certified : registry.access.redhat.com/rhscl/postgresql-10-rhel7:1-66' + - check: v1.0/chart-testing + type: Mandatory + outcome: PASS + reason: Chart tests have passed + - check: v1.0/required-annotations-present + type: Mandatory + outcome: PASS + reason: All required annotations present + - check: v1.0/contains-test + type: Mandatory + outcome: PASS + reason: Chart test files exist + - check: v1.0/contains-values + type: Mandatory + outcome: PASS + reason: Values file exist + - check: v1.0/contains-values-schema + type: Mandatory + outcome: PASS + reason: Values schema file exist + - check: v1.1/has-kubeversion + type: Mandatory + outcome: PASS + reason: Kubernetes version specified + - check: v1.0/helm-lint + type: Mandatory + outcome: PASS + reason: Helm lint successful + diff --git a/tests/data/psql-service-0.1.10-1.tgz b/tests/data/psql-service-0.1.10-1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..7e3673939177d2bf8188de44d518df4b63e4235e GIT binary patch literal 2791 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 literal 0 HcmV?d00001 diff --git a/tests/data/vault-test-timeout-0.17.0.tgz b/tests/data/vault-test-timeout-0.17.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..46472f2859fa10182e8d269e2c58eb2707135609 GIT binary patch literal 37518 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" has a valid identity as "" + And an error-free chart source is used in "" + When the user sends a pull request with the chart + 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 corresponding report and chart tarball + + @partners @smoke @full + Examples: + | vendor_type | vendor | chart_path | + | partners | hashicorp | tests/data/vault-0.17.0.tgz | + + @redhat @full + Examples: + | vendor_type | vendor | chart_path | + | redhat | redhat | tests/data/vault-0.17.0.tgz | + + Scenario Outline: [HC-01-002] A community user submits an error-free chart source without report + Given the vendor "" has a valid identity as "" + And an error-free chart source is used in "" + 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 + + @community @full + Examples: + | vendor_type | vendor | chart_path | message | + | community | redhat | tests/data/vault-0.17.0.tgz | Community charts require maintainer review and approval, a review will be conducted shortly | diff --git a/tests/functional/behave_features/HC-02_chart_tar_without_report.feature b/tests/functional/behave_features/HC-02_chart_tar_without_report.feature new file mode 100644 index 0000000000..4fd4a9a4c7 --- /dev/null +++ b/tests/functional/behave_features/HC-02_chart_tar_without_report.feature @@ -0,0 +1,33 @@ +Feature: Chart tarball submission without report + Partners, redhat and community users can publish their chart by submitting + error-free chart in tarball format without a report. + + Scenario Outline: [HC-02-001] A partner or redhat associate submits an error-free chart tarball + Given the vendor "" has a valid identity as "" + And an error-free chart tarball is used in "" + When the user sends a pull request with the chart + 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 corresponding report and chart tarball + + @partners @full + Examples: + | vendor_type | vendor | chart_path | + | partners | hashicorp | tests/data/vault-0.17.0.tgz | + + @redhat @smoke @full + Examples: + | vendor_type | vendor | chart_path | + | redhat | redhat | tests/data/vault-0.17.0.tgz | + + Scenario Outline: [HC-02-002] A community user submits an error-free chart tarball without report + Given the vendor "" has a valid identity as "" + And an error-free chart tarball is used in "" + 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 + + @community @full + Examples: + | vendor_type | vendor | chart_path | message | + | community | redhat | tests/data/vault-0.17.0.tgz | Community charts require maintainer review and approval, a review will be conducted shortly | diff --git a/tests/functional/behave_features/HC-03_chart_verifier_comes_back_with_failures.feature b/tests/functional/behave_features/HC-03_chart_verifier_comes_back_with_failures.feature new file mode 100644 index 0000000000..d1afebec42 --- /dev/null +++ b/tests/functional/behave_features/HC-03_chart_verifier_comes_back_with_failures.feature @@ -0,0 +1,35 @@ +Feature: Chart verifier comes back with a failure + Partners, redhat or community user submit charts which does not contain README file + + Scenario Outline: [HC-03-001] A partner or community user submits a chart which does not contain a readme file + Given the vendor "" has a valid identity as "" + And chart source is used in "" + And README file is missing in the chart + When the user pushed the chart and created pull request + 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 | + | partners | hashicorp | tests/data/vault-0.17.0.tgz | Chart does not have a README | + + @community @full + Examples: + | vendor_type | vendor | chart_path | message | + | community | redhat | tests/data/vault-0.17.0.tgz | Community charts require maintainer review and approval | + + Scenario Outline: [HC-03-002] A redhat user submits a chart which does not contain a readme file + Given the vendor "" has a valid identity as "" + And chart source is used in "" + And README file is missing in the chart + When the user pushed the chart and created pull request + Then the user sees the pull request is merged + And the index.yaml file is updated with an entry for the submitted chart with correct providerType + And a release is published with corresponding report and chart tarball + + @redhat @full + Examples: + | 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 new file mode 100644 index 0000000000..884d326fe7 --- /dev/null +++ b/tests/functional/behave_features/HC-04_invalid_url_in_the_report.feature @@ -0,0 +1,44 @@ +Feature: Report contains an invalid URL + Partners, redhat and community users submits only report with 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 "" + 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 | + + @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 | + + @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 | + diff --git a/tests/functional/behave_features/HC-05_pr_includes_a_file_which_is_not_chart_related.feature b/tests/functional/behave_features/HC-05_pr_includes_a_file_which_is_not_chart_related.feature new file mode 100644 index 0000000000..567da35184 --- /dev/null +++ b/tests/functional/behave_features/HC-05_pr_includes_a_file_which_is_not_chart_related.feature @@ -0,0 +1,25 @@ +Feature: PR includes a non chart related file + Partners, redhat or community user submit charts which includes a file which is not part of the chart + + Scenario Outline: [HC-05-001] A user submits a chart with non chart related file + Given the vendor "" has a valid identity as "" + And chart source is used in "" + And user adds a non chart related file + When the user sends a pull request with both chart and non related file + 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 | + | partners | hashicorp | tests/data/vault-0.17.0.tgz | PR includes one or more files not related to charts | + + @redhat @full + Examples: + | vendor_type | vendor | chart_path | message | + | redhat | redhat | tests/data/vault-0.17.0.tgz | PR includes one or more files not related to charts | + + @community @full + Examples: + | vendor_type | vendor | chart_path | message | + | community | redhat | tests/data/vault-0.17.0.tgz | PR includes one or more files not related to charts | diff --git a/tests/functional/behave_features/HC-06_provider_delivery_control.feature b/tests/functional/behave_features/HC-06_provider_delivery_control.feature new file mode 100644 index 0000000000..9f9a5dc9e0 --- /dev/null +++ b/tests/functional/behave_features/HC-06_provider_delivery_control.feature @@ -0,0 +1,51 @@ +Feature: Report only submission with provider control settings + Partners can prevent publication of their chart by submitting + error-free report that was generated by chart-verifier and with + prvider controlled delivery set in the report and the OWNERS file. + + @external-feedback + 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 + 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 | + + @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. | + + @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 + 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. | 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 new file mode 100644 index 0000000000..dd69ada41c --- /dev/null +++ b/tests/functional/behave_features/HC-07_report_and_chart_src.feature @@ -0,0 +1,33 @@ +Feature: Chart source submission with report + Partners, redhat and community users can publish their chart by submitting + error-free chart in source format with a report. + + Scenario Outline: [HC-07-001] A partner or redhat associate submits an error-free chart source with report + Given the vendor "" has a valid identity as "" + And an error-free chart source used in "" and report in "" + When the user sends a pull request with the chart and 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 corresponding report and chart tarball + + @partners @smoke @full + Examples: + | vendor_type | vendor | chart_path | report_path | + | partners | hashicorp | tests/data/vault-0.17.0.tgz | tests/data/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 | + + Scenario Outline: [HC-07-002] A community user submits an error-free chart source with report + Given the vendor "" has a valid identity as "" + And an error-free chart source used in "" and report in "" + 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 + + @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 | 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 new file mode 100644 index 0000000000..69e9211fa9 --- /dev/null +++ b/tests/functional/behave_features/HC-08_report_and_chart_tar.feature @@ -0,0 +1,33 @@ +Feature: Chart tarball submission with report + Partners, redhat and community users can publish their chart by submitting + error-free chart in tarball format with a report. + + Scenario Outline: [HC-08-001] A partner or redhat associate submits an error-free chart tarball with report + Given the vendor "" has a valid identity as "" + And an error-free chart tarball used in "" and report in "" + When the user sends a pull request with the chart and 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 corresponding report and chart tarball + + @partners @smoke @full + Examples: + | vendor_type | vendor | chart_path | report_path | + | partners | hashicorp | tests/data/vault-0.17.0.tgz | tests/data/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 | + + Scenario Outline: [HC-08-002] A community user submits an error-free chart tarball with report + Given the vendor "" has a valid identity as "" + And an error-free chart tarball used in "" and report in "" + 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 + + @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 | 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 new file mode 100644 index 0000000000..2de4a15c14 --- /dev/null +++ b/tests/functional/behave_features/HC-09_report_in_json_format.feature @@ -0,0 +1,27 @@ +Feature: Report only submission in json format + If partners, redhat and community users try to publish chart by submitting report + in json format then will receive an error message + + Scenario Outline: [HC-09-001] An user submits a report in json format + Given the vendor "" has a valid identity as "" + And report is used in "" + When the user sends a pull request with the report + Then the pull request is not merged + #this step is failing currently https://issues.redhat.com/browse/HELM-396 + And user gets the "" in the pull request comment + + @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 | + + @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 | + + @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 | + diff --git a/tests/functional/behave_features/HC-10_report_only_edited.feature b/tests/functional/behave_features/HC-10_report_only_edited.feature new file mode 100644 index 0000000000..a922d1c69f --- /dev/null +++ b/tests/functional/behave_features/HC-10_report_only_edited.feature @@ -0,0 +1,47 @@ +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 new file mode 100644 index 0000000000..012a8aa6cc --- /dev/null +++ b/tests/functional/behave_features/HC-11_report_with_missing_checks.feature @@ -0,0 +1,27 @@ +Feature: Report does not include a check + Partners, redhat and community users submits only report which does not include full set of checks + + 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 + 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 | + + @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 | + + @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 | + # 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 new file mode 100644 index 0000000000..d74dec2743 --- /dev/null +++ b/tests/functional/behave_features/HC-12_report_without_chart.feature @@ -0,0 +1,32 @@ +Feature: Report only submission + Partners, redhat and community users can publish their chart by submitting + error-free report that was generated by chart-verifier. + + Scenario Outline: [HC-12-001] A partner or redhat associate submits an error-free report + 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 @smoke @full + Examples: + | vendor_type | vendor | report_path | + | partners | hashicorp | tests/data/report.yaml | + + @redhat @full + Examples: + | vendor_type | vendor | report_path | + | redhat | redhat | tests/data/report.yaml | + + Scenario Outline: [HC-12-002] A community user submits an error-free report + 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 + + @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 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 new file mode 100644 index 0000000000..05d6e1d9de --- /dev/null +++ b/tests/functional/behave_features/HC-13_sha_value_does_not_match.feature @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000000..96d07ade97 --- /dev/null +++ b/tests/functional/behave_features/HC-14_user_submits_chart_with_errors.feature @@ -0,0 +1,49 @@ +Feature: Chart submission with errors + Partners, redhat or community user submit charts which result in errors + + Scenario Outline: [HC-14-001] An unauthorized user submits a chart + Given the vendor "" has a valid identity as "" + And A "" wants to submit a chart in "" + 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 | 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 | + + @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 | + + Scenario Outline: [HC-14-002] An authorized user submits a chart with incorrect version + Given the vendor "" has a valid identity as "" + And An authorized user wants to submit a chart in "" + 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 | doesn't match the directory structure | 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 | + + @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 | diff --git a/tests/functional/behave_features/HC-15_check_submitted_charts.feature b/tests/functional/behave_features/HC-15_check_submitted_charts.feature new file mode 100644 index 0000000000..8fea2fe2f2 --- /dev/null +++ b/tests/functional/behave_features/HC-15_check_submitted_charts.feature @@ -0,0 +1,16 @@ +Feature: Check submitted charts + New Openshift or chart-verifier will trigger (automatically or manually) a recursive checking on + existing submitted charts under `charts/` directory with the specified Openshift and chart-verifier + version. + + Besides, during workflow development, engineers would like to check if the changes will break checks + on existing submitted charts. + + @version-change + Scenario: [HC-15-001] A new Openshift or chart-verifier version is specified either by a cron job or manually + Given there is a github workflow for testing existing charts + When workflow for testing existing charts is triggered + And a new Openshift or chart-verifier version is specified + And the vendor type is specified, e.g. partner, and/or redhat + Then submission tests are run for existing charts + And all results are reported back to the caller diff --git a/tests/functional/behave_features/HC-16_chart_test_takes_more_than_30mins.feature b/tests/functional/behave_features/HC-16_chart_test_takes_more_than_30mins.feature new file mode 100644 index 0000000000..1084f2dcd4 --- /dev/null +++ b/tests/functional/behave_features/HC-16_chart_test_takes_more_than_30mins.feature @@ -0,0 +1,33 @@ +Feature: Chart test takes longer time and exceeds default timeout + Partners, redhat or community user submit charts which result in errors + + Scenario Outline: [HC-16-001] A partner or community user submits chart that takes more than 30 mins + Given the vendor "" has a valid identity as "" + And an error-free chart tarball is used in "" + 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 | message | + | partners | hashicorp | tests/data/vault-test-timeout-0.17.0.tgz | Chart test failure: timed out waiting for the condition | + + @community @full + Examples: + | vendor_type | vendor | chart_path | message | + | community | redhat | tests/data/vault-test-timeout-0.17.0.tgz | Community charts require maintainer review and approval, a review will be conducted shortly | + + Scenario Outline: [HC-16-002] A redhat associate submits a chart that takes more than 30 mins + Given the vendor "" has a valid identity as "" + And an error-free chart tarball is used in "" + When the user sends a pull request with the chart + 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 corresponding report and chart tarball + + @redhat @full + Examples: + | vendor_type | vendor | chart_path | + | redhat | redhat | tests/data/vault-test-timeout-0.17.0.tgz | + diff --git a/tests/functional/behave_features/HC-17_dash_in_version.feature b/tests/functional/behave_features/HC-17_dash_in_version.feature new file mode 100644 index 0000000000..7431fce613 --- /dev/null +++ b/tests/functional/behave_features/HC-17_dash_in_version.feature @@ -0,0 +1,21 @@ +Feature: Report only submission + Partners, redhat and community users can publish their chart by submitting + error-free report that was generated by chart-verifier. + + Scenario Outline: [HC-17-001] A partner or redhat associate submits report only with dash in chart version + Given the vendor "" has a valid identity as "" + And an error-free report is used in "" + When the user sends a pull request with the report + Then the user sees the pull request is merged + And the index.yaml file is updated with an entry for the submitted chart + + @partners @full + Examples: + | vendor_type | vendor | report_path | + | partners | redhat | tests/data/HC-17/dash-in-version/partner/report.yaml | + + @redhat @full + Examples: + | vendor_type | vendor | report_path | + | redhat | redhat | tests/data/HC-17/dash-in-version/redhat/report.yaml | + diff --git a/tests/functional/behave_features/common/__init__.py b/tests/functional/behave_features/common/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/functional/behave_features/common/utils/__init__.py b/tests/functional/behave_features/common/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/functional/behave_features/common/utils/chart.py b/tests/functional/behave_features/common/utils/chart.py new file mode 100644 index 0000000000..45d5938a67 --- /dev/null +++ b/tests/functional/behave_features/common/utils/chart.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +"""Utility module for processing chart files.""" + +import os +import tarfile +import yaml +import shutil +import json + +def get_name_and_version_from_report(path): + """ + Parameters: + path (str): path to the report.yaml + + Returns: + str: chart name + str: chart version + """ + if path.endswith('yaml'): + with open(path, 'r') as fd: + try: + report = yaml.safe_load(fd) + except yaml.YAMLError as err: + raise AssertionError(f"error parsing '{path}': {err}") + elif path.endswith('json'): + with open(path, 'r') as fd: + try: + report = json.load(fd) + except Exception as err: + raise AssertionError(f"error parsing '{path}': {err}") + else: + raise AssertionError("Unknown report type") + chart = report['metadata']['chart'] + return chart['name'], chart['version'] + + +def get_name_and_version_from_chart_tar(path): + """ + Parameters: + path (str): path to the chart tar file + + Returns: + str: chart name + str: chart version + """ + tar = tarfile.open(path) + for member in tar.getmembers(): + if member.name.split('/')[-1] == 'Chart.yaml': + chart = tar.extractfile(member) + if chart is not None: + content = chart.read() + try: + chart_yaml = yaml.safe_load(content) + return chart_yaml['name'], chart_yaml['version'] + except yaml.YAMLError as err: + raise AssertionError(f"error parsing '{path}': {err}") + else: + raise AssertionError(f"Chart.yaml not in {path}") + + +def get_name_and_version_from_chart_src(path): + """ + Parameters: + path (str): path to the chart src directory + + Returns: + str: chart name + str: chart version + """ + chart_path = os.path.join(path, 'Chart.yaml') + with open(chart_path, 'r') as fd: + try: + chart_yaml = yaml.safe_load(fd) + except yaml.YAMLError as err: + raise AssertionError(f"error parsing '{path}': {err}") + return chart_yaml['name'], chart_yaml['version'] + +def extract_chart_tgz(src, dst, secrets, logger): + """Extracts the chart tgz file into the target location under 'charts/' for PR submission tests + + Parameters: + src (str): path to the test chart tgz + dst (str): path to the extract destination, e.g. 'charts/partners/hashicorp/vault/0.13.0' + """ + try: + logger.info(f"Remove existing local '{dst}/src'") + shutil.rmtree(f'{dst}/src') + except FileNotFoundError: + logger.info(f"'{dst}/src' does not exist") + finally: + with tarfile.open(src, 'r') as fd: + fd.extractall(dst) + os.rename(f'{dst}/{secrets.chart_name}', f'{dst}/src') + +def get_all_charts(charts_path: str, vendor_types: str) -> list: + # TODO: Support `community` as vendor_type. + """Gets charts with src or tgz under `charts/` given vendor_types and without report. + + Parameters: + charts_path (str): path to the `charts/` directory + vendor_types (str): vendor type to look for, any combination of `partner`, `redhat`, separated + by commas, or `all` to run both `partner` and `redhat`. + + Returns: + list: list of (vendor_type, vendor, chart_name, chart_version) tuples + """ + ret = [] + # Pre-process vendor types + vendor_types = vendor_types.replace('partner', 'partners') + vendor_types = [vt.strip() for vt in vendor_types.split(',')] + vendor_types = list( + {'partners', 'redhat', 'all'}.intersection(set(vendor_types))) + vendor_types = ['partners', + 'redhat'] if 'all' in vendor_types else vendor_types + + # Iterate through `charts/` to find chart submission with src or tgz + for vt in vendor_types: + charts_path_vt = f'{charts_path}/{vt}' + vendor_names = [name for name in os.listdir( + charts_path_vt) if os.path.isdir(f'{charts_path_vt}/{name}')] + for vn in vendor_names: + charts_path_vt_vn = f'{charts_path_vt}/{vn}' + chart_names = [name for name in os.listdir( + charts_path_vt_vn) if os.path.isdir(f'{charts_path_vt_vn}/{name}')] + for cn in chart_names: + charts_path_vt_vn_cn = f'{charts_path_vt_vn}/{cn}' + file_names = [name for name in os.listdir( + charts_path_vt_vn_cn)] + if 'OWNERS' not in file_names: + continue + chart_versions = [name for name in os.listdir( + charts_path_vt_vn_cn) if os.path.isdir(f'{charts_path_vt_vn_cn}/{name}')] + # Only interest in latest chart version + if len(chart_versions) == 0: + continue + cv = max(chart_versions) + charts_path_vt_vn_cn_cv = f'{charts_path_vt_vn_cn}/{cv}' + file_names = [name for name in os.listdir( + charts_path_vt_vn_cn_cv)] + if 'report.yaml' not in file_names and (f'{cn}-{cv}.tgz' in file_names or 'src' in file_names): + ret.append((vt, vn, cn, cv)) + return ret diff --git a/tests/functional/behave_features/common/utils/chart_certification.py b/tests/functional/behave_features/common/utils/chart_certification.py new file mode 100644 index 0000000000..6b023d64fa --- /dev/null +++ b/tests/functional/behave_features/common/utils/chart_certification.py @@ -0,0 +1,1036 @@ +# -*- coding: utf-8 -*- +"""Utility class for setting up and manipulating certification workflow tests.""" + +import os +import json +import pathlib +import shutil +import logging +import time +import uuid +from tempfile import TemporaryDirectory +from dataclasses import dataclass +from string import Template +from pathlib import Path + +import git +import yaml + +from common.utils.notifier import * +from common.utils.index import * +from common.utils.github import * +from common.utils.secret import * +from common.utils.set_directory import SetDirectory +from common.utils.setttings import * +from common.utils.chart import * + +@dataclass +class ChartCertificationE2ETest: + owners_file_content: str = """\ +chart: + name: ${chart_name} + shortDescription: Test chart for testing chart submission workflows. +publicPgpKey: null +providerDelivery: ${provider_delivery} +users: +- githubUsername: ${bot_name} +vendor: + label: ${vendor} + name: ${vendor} +""" + secrets: E2ETestSecret = E2ETestSecret() + + old_cwd: str = os.getcwd() + repo: git.Repo = git.Repo() + temp_dir: TemporaryDirectory = None + temp_repo: git.Repo = None + github_actions: str = os.environ.get("GITHUB_ACTIONS") + def set_git_username_email(self, repo, username, email): + """ + Parameters: + repo (git.Repo): git.Repo instance of the local directory + username (str): git username to set + email (str): git email to set + """ + 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( + f"Remove {chart_directory}/{chart_version} from {remote_repo}:{base_branch}") + try: + self.temp_repo.git.rm('-rf', '--cached', f'{chart_directory}/{chart_version}') + self.temp_repo.git.commit( + '-m', f'Remove {chart_directory}/{chart_version}') + self.temp_repo.git.push(f'https://x-access-token:{bot_token}@github.com/{remote_repo}', + f'HEAD:refs/heads/{base_branch}') + except git.exc.GitCommandError: + logging.info( + f"{chart_directory}/{chart_version} not exist on {remote_repo}:{base_branch}") + + def remove_owners_file(self, chart_directory, remote_repo, base_branch, bot_token): + # Remove the OWNERS file from base branch + logging.info( + f"Remove {chart_directory}/OWNERS from {remote_repo}:{base_branch}") + try: + self.temp_repo.git.rm('-rf', '--cached', f'{chart_directory}/OWNERS') + self.temp_repo.git.commit( + '-m', f'Remove {chart_directory}/OWNERS') + self.temp_repo.git.push(f'https://x-access-token:{bot_token}@github.com/{remote_repo}', + f'HEAD:refs/heads/{base_branch}') + except git.exc.GitCommandError: + logging.info( + f"{chart_directory}/OWNERS not exist on {remote_repo}:{base_branch}") + + def create_test_gh_pages_branch(self, remote_repo, base_branch, bot_token): + # Get SHA from 'dev-gh-pages' branch + logging.info( + f"Create '{remote_repo}:{base_branch}-gh-pages' from '{remote_repo}:dev-gh-pages'") + r = github_api( + 'get', f'repos/{remote_repo}/git/ref/heads/dev-gh-pages', bot_token) + j = json.loads(r.text) + sha = j['object']['sha'] + + # Create a new gh-pages branch for testing + data = {'ref': f'refs/heads/{base_branch}-gh-pages', 'sha': sha} + r = github_api( + 'post', f'repos/{remote_repo}/git/refs', bot_token, json=data) + + logging.info(f'gh-pages branch created: {base_branch}-gh-pages') + + def setup_git_context(self, repo: git.Repo): + self.set_git_username_email(repo, self.secrets.bot_name, GITHUB_ACTIONS_BOT_EMAIL) + if os.environ.get('WORKFLOW_DEVELOPMENT'): + logging.info("Wokflow development enabled") + repo.git.add(A=True) + repo.git.commit('-m', 'Checkpoint') + + def send_pull_request(self, remote_repo, base_branch, pr_branch, bot_token): + pr_body = os.environ.get('PR_BODY') + data = {'head': pr_branch, 'base': base_branch, + 'title': base_branch, 'body': pr_body} + logging.debug(f"PR_BODY Content: {pr_body}") + logging.info( + f"Create PR from '{remote_repo}:{pr_branch}'") + r = github_api( + 'post', f'repos/{remote_repo}/pulls', bot_token, json=data) + j = json.loads(r.text) + if not 'number' in j: + 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): + with SetDirectory(Path(self.temp_dir.name)): + # Create the OWNERS file from the string template + values = {'bot_name': self.secrets.bot_name, + 'vendor': vendor_name, 'chart_name': chart_name, + "provider_delivery" : provider_delivery} + content = Template(self.secrets.owners_file_content).substitute(values) + logging.debug(f"OWNERS File Content: {content}") + with open(f'{chart_directory}/OWNERS', 'w') as fd: + fd.write(content) + + # Push OWNERS file to the test_repo + logging.info( + f"Push OWNERS file to '{self.secrets.test_repo}:{base_branch}'") + self.temp_repo.git.add(f'{chart_directory}/OWNERS') + self.temp_repo.git.commit( + '-m', f"Add {vendor_type} {vendor_name} {chart_name} OWNERS file") + self.temp_repo.git.push(f'https://x-access-token:{self.secrets.bot_token}@github.com/{self.secrets.test_repo}', + f'HEAD:refs/heads/{base_branch}', '-f') + + def check_index_yaml(self,base_branch, vendor, chart_name, chart_version, index_file="index.yaml", check_provider_type=False, failure_type='error'): + old_branch = self.repo.active_branch.name + self.repo.git.fetch(f'https://github.com/{self.secrets.test_repo}.git', + '{0}:{0}'.format(f'{base_branch}-gh-pages'), '-f') + + self.repo.git.checkout(f'{base_branch}-gh-pages') + + with open(index_file, 'r') as fd: + try: + index = yaml.safe_load(fd) + except yaml.YAMLError as err: + if failure_type == 'error': + raise AssertionError(f"error parsing index.yaml: {err}") + else: + logging.warning(f"error parsing index.yaml: {err}") + return False + + if index: + entry = f"{vendor}-{chart_name}" + if "entries" not in index or entry not in index['entries']: + if failure_type == 'error': + raise AssertionError(f"{entry} not added in entries to {index_file} & Found index.yaml entries: {index['entries']}") + else: + logging.warning(f"{chart_version} not added to {index_file}") + logging.warning(f"Index.yaml entry content: {index['entries'][entry]}") + return False + + version_list = [release['version'] for release in index['entries'][entry]] + if chart_version not in version_list: + raise AssertionError(f"{chart_version} not added to {index_file} & Found index.yaml entry content: {index['entries'][entry]}") + + #This check is applicable for charts submitted in redhat path when one of the chart-verifier check fails + #Check whether providerType annotations is community in index.yaml when vendor_type is redhat + if check_provider_type and self.secrets.vendor_type == 'redhat': + provider_type_in_index_yaml = index['entries'][entry][0]['annotations']['charts.openshift.io/providerType'] + if provider_type_in_index_yaml != 'community': + if failure_type == 'error': + raise AssertionError(f"{provider_type_in_index_yaml} is not correct as providerType in index.yaml") + else: + logging.warning(f"{provider_type_in_index_yaml} is not correct as providerType in index.yaml") + + logging.info("Index updated correctly, cleaning up local branch") + self.repo.git.checkout(old_branch) + self.repo.git.branch('-D', f'{base_branch}-gh-pages') + return True + else: + return False + + def check_release_result(self, vendor, chart_name, chart_version, chart_tgz, failure_type='error'): + 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] + logging.info(f"Check '{required_assets}' is in release assets") + release_id = release['id'] + get_release_assets(self.secrets, release_id, required_assets) + return True + except Exception as e: + if failure_type == 'error': + raise AssertionError(e) + else: + logging.warning(e) + return False + finally: + logging.info(f"Delete release '{expected_tag}'") + github_api( + 'delete', f'repos/{self.secrets.test_repo}/releases/{release_id}', self.secrets.bot_token) + + logging.info(f"Delete release tag '{expected_tag}'") + github_api( + 'delete', f'repos/{self.secrets.test_repo}/git/refs/tags/{expected_tag}', self.secrets.bot_token) + + # expect_result: a string representation of expected result, e.g. 'success' + def check_workflow_conclusion(self, pr_number, expect_result: str, failure_type='error'): + try: + # Check workflow conclusion + run_id = get_run_id(self.secrets, pr_number) + conclusion = get_run_result(self.secrets, run_id) + if conclusion == expect_result: + logging.info(f"PR{pr_number} Workflow run was '{expect_result}' which is expected") + else: + if failure_type == 'warning': + logging.warning(f"PR{pr_number if pr_number else self.secrets.pr_number} Workflow run was '{conclusion}' which is unexpected, run id: {run_id}") + else: + raise AssertionError( + f"PR{pr_number if pr_number else self.secrets.pr_number} Workflow run was '{conclusion}' which is unexpected, run id: {run_id}") + + return run_id, conclusion + except Exception as e: + if failure_type == 'error': + raise AssertionError(e) + else: + logging.warning(e) + return None, None + + # expect_merged: boolean representing whether the PR should be merged + def check_pull_request_result(self, pr_number, expect_merged: bool, failure_type='error'): + # Check if PR merged + r = github_api( + 'get', f'repos/{self.secrets.test_repo}/pulls/{pr_number}/merge', self.secrets.bot_token) + logging.info(f"PR{pr_number} result status_code : {r.status_code}") + if r.status_code == 204 and expect_merged: + logging.info(f"PR{pr_number} merged sucessfully as expected") + return True + elif r.status_code == 404 and not expect_merged: + logging.info(f"PR{pr_number} not merged, which is expected") + return True + elif r.status_code == 204 and not expect_merged: + if failure_type == 'error': + raise AssertionError(f"PR{pr_number} Expecting not merged but PR was merged") + else: + logging.warning(f"PR{pr_number} Expecting not merged but PR was merged") + return False + elif r.status_code == 404 and expect_merged: + if failure_type == 'error': + raise AssertionError(f"PR{pr_number} Expecting PR merged but PR was not merged") + else: + logging.warning(f"PR{pr_number} Expecting PR merged but PR was not merged") + return False + else: + if failure_type == 'error': + raise AssertionError(f"PR{pr_number} Got unexpected status code from PR: {r.status_code}") + else: + logging.warning(f"PR{pr_number} Got unexpected status code from PR: {r.status_code}") + return False + + def cleanup_release(self, expected_tag): + """Cleanup the release and release tag. + + Releases might be left behind if check_index_yam() ran before check_release_result() and fails the test. + """ + r = github_api( + 'get', f'repos/{self.secrets.test_repo}/releases', self.secrets.bot_token) + releases = json.loads(r.text) + logging.debug(f"List of releases: {releases}") + for release in releases: + if release['tag_name'] == expected_tag: + release_id = release['id'] + logging.info(f"Delete release '{expected_tag}'") + github_api( + 'delete', f'repos/{self.secrets.test_repo}/releases/{release_id}', self.secrets.bot_token) + + logging.info(f"Delete release tag '{expected_tag}'") + github_api( + 'delete', f'repos/{self.secrets.test_repo}/git/refs/tags/{expected_tag}', self.secrets.bot_token) + + def check_pull_request_labels(self, pr_number): + r = github_api( + 'get', f'repos/{self.secrets.test_repo}/issues/{pr_number}/labels', self.secrets.bot_token) + labels = json.loads(r.text) + authorized_request = False + content_ok = False + for label in labels: + logging.info(f"PR{pr_number} found label {label['name']}") + if label['name'] == "authorized-request": + authorized_request = True + if label['name'] == "content-ok": + content_ok = True + + if authorized_request and content_ok: + logging.info(f"PR{pr_number} authorized request and content-ok labels were found as expected") + return True + else: + raise AssertionError(f"PR{pr_number} authorized request and/or content-ok labels were not found as expected") + +@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 = '' + secrets: E2ETestSecretOneShot = E2ETestSecretOneShot() + + def __post_init__(self) -> None: + # unique string based on uuid.uuid4(), not using timestamp here because even + # in nanoseconds there are chances of collisions among first test cases of + # different processes. + self.uuid = uuid.uuid4().hex + + bot_name, bot_token = self.get_bot_name_and_token() + test_repo = TEST_REPO + + #Storing current branch to checkout after scenario execution + if os.environ.get('LOCAL_RUN'): + self.secrets.active_branch = self.repo.active_branch.name + 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}' + 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}") + if unique_branch not in local_branches: + self.repo.git.checkout('-b', f'{unique_branch}') + + current_branch = self.repo.active_branch.name + logging.debug(f"Current active branch name : {current_branch}") + + r = github_api( + 'get', f'repos/{test_repo}/branches', bot_token) + branches = json.loads(r.text) + branch_names = [branch['name'] for branch in branches] + logging.debug(f"Remote test repo branch names : {branch_names}") + 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}', + f'HEAD:refs/heads/{current_branch}', '-f') + + pretty_test_name = self.test_name.strip().lower().replace(' ', '-') + base_branch = f'{self.uuid}-{pretty_test_name}-{current_branch}' if pretty_test_name else f'{self.uuid}-test-{current_branch}' + 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.index_file = "index.yaml" + self.secrets.provider_delivery = False + + + def cleanup (self): + # Cleanup releases and release tags + self.cleanup_release() + # Teardown step to cleanup branches + if self.temp_dir is not None: + 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}' + 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) + + logging.info(f"Delete '{self.secrets.test_repo}:{self.secrets.base_branch}'") + github_api( + 'delete', f'repos/{self.secrets.test_repo}/git/refs/heads/{self.secrets.base_branch}', self.secrets.bot_token) + + logging.info(f"Delete '{self.secrets.test_repo}:{self.secrets.base_branch}-gh-pages'") + github_api( + 'delete', f'repos/{self.secrets.test_repo}/git/refs/heads/{self.secrets.base_branch}-gh-pages', self.secrets.bot_token) + + logging.info(f"Delete '{self.secrets.test_repo}:{self.secrets.pr_branch}'") + github_api( + 'delete', f'repos/{self.secrets.test_repo}/git/refs/heads/{self.secrets.pr_branch}', self.secrets.bot_token) + + logging.info(f"Delete local '{self.secrets.base_branch}'") + try: + self.repo.git.branch('-D', self.secrets.base_branch) + except git.exc.GitCommandError: + logging.info(f"Local '{self.secrets.base_branch}' does not exist") + + logging.info(f"Delete local '{current_branch}'") + try: + if os.environ.get('LOCAL_RUN'): + self.repo.git.checkout(f'{self.secrets.active_branch}') + self.repo.git.branch('-D', current_branch) + except git.exc.GitCommandError: + logging.info(f"Local '{current_branch}' does not exist") + + def update_bot_name(self, bot_name): + logging.debug(f"Updating bot name: {bot_name}") + self.secrets.bot_name = 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_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() + + def get_unique_vendor(self, vendor): + """Set unique vendor name. + Note that release tag is generated with this vendor name. + """ + # unique string based on uuid.uuid4() + suffix = self.uuid + if "PR_NUMBER" in os.environ: + pr_num = os.environ["PR_NUMBER"] + 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}") + self.secrets.vendor = self.get_unique_vendor(vendor) + logging.debug(f"Unique vendor value: {self.secrets.vendor}") + self.secrets.vendor_type = vendor_type + + def setup_git_context(self): + super().setup_git_context(self.repo) + + def setup_gh_pages_branch(self): + self.create_test_gh_pages_branch(self.secrets.test_repo, self.secrets.base_branch, self.secrets.bot_token) + + def setup_temp_dir(self): + self.temp_dir = TemporaryDirectory(prefix='tci-') + with SetDirectory(Path(self.temp_dir.name)): + # Make PR's from a temporary directory + logging.info(f'Worktree directory: {self.temp_dir.name}') + self.repo.git.worktree('add', '--detach', self.temp_dir.name, f'HEAD') + self.temp_repo = git.Repo(self.temp_dir.name) + + 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) + + 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) + + 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'] + + 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}") + + 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_chart(self, is_tarball: bool): + 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) + + + 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): + + 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) + + report_path = f'{self.chart_directory}/{self.secrets.chart_version}/' + self.secrets.test_report.split('/')[-1] + + 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") + 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") + + def push_chart(self, is_tarball: bool, 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}/') + else: + self.temp_repo.git.add(f'{self.chart_directory}/{self.secrets.chart_version}/src') + self.temp_repo.git.commit( + '-m', f"Add {self.secrets.vendor} {self.secrets.chart_name} {self.secrets.chart_version} chart") + + 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 send_pull_request(self): + self.secrets.pr_number = super().send_pull_request(self.secrets.test_repo, self.secrets.base_branch, self.secrets.pr_branch, self.secrets.bot_token) + logging.info(f"[INFO] PR number: {self.secrets.pr_number}") + + # expect_result: a string representation of expected result, e.g. 'success' + def check_workflow_conclusion(self, expect_result: str): + # Check workflow conclusion + super().check_workflow_conclusion(None, expect_result) + + # expect_merged: boolean representing whether the PR should be merged + def check_pull_request_result(self, expect_merged: bool): + super().check_pull_request_result(self.secrets.pr_number, expect_merged) + + def check_pull_request_labels(self): + super().check_pull_request_labels(self.secrets.pr_number) + + 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}') + + response = json.loads(r.text) + logging.debug(f"CHECK PULL_REQUEST COMMENT RESPONSE: {response}") + if len(response) == 0: + raise AssertionError("No comment found in the PR") + complete_comment = response[0]['body'] + + if expect_message in complete_comment: + logging.info("Found the expected comment in the PR") + else: + 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) + + 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 cleanup_release(self): + expected_tag = f'{self.secrets.vendor}-{self.secrets.chart_name}-{self.secrets.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() + + test_repo = TEST_REPO + base_branches = [] + pr_branches = [] + + pr_base_branch = self.repo.active_branch.name + r = github_api( + 'get', f'repos/{test_repo}/branches', bot_token) + branches = json.loads(r.text) + branch_names = [branch['name'] for branch in branches] + if pr_base_branch not in branch_names: + logging.info( + f"{test_repo}:{pr_base_branch} does not exists, creating with local branch") + self.repo.git.push(f'https://x-access-token:{bot_token}@github.com/{test_repo}', + f'HEAD:refs/heads/{pr_base_branch}', '-f') + + self.secrets = E2ETestSecretRecursive() + self.secrets.software_name = software_name + self.secrets.software_version = software_version + self.secrets.test_repo = test_repo + self.secrets.bot_name = bot_name + self.secrets.bot_token = bot_token + self.secrets.vendor_type = vendor_type + self.secrets.pr_base_branch = pr_base_branch + self.secrets.base_branches = base_branches + self.secrets.pr_branches = pr_branches + self.secrets.dry_run = dry_run + self.secrets.notify_id = notify_id + self.secrets.owners_file_content = self.owners_file_content + self.secrets.release_tags = list() + + def cleanup (self): + # Teardown step to cleanup releases and branches + for release_tag in self.secrets.release_tags: + self.cleanup_release(release_tag) + + self.repo.git.worktree('prune') + for base_branch in self.secrets.base_branches: + logging.info(f"Delete '{self.secrets.test_repo}:{base_branch}'") + github_api( + 'delete', f'repos/{self.secrets.test_repo}/git/refs/heads/{base_branch}', self.secrets.bot_token) + + logging.info(f"Delete '{self.secrets.test_repo}:{base_branch}-gh-pages'") + github_api( + 'delete', f'repos/{self.secrets.test_repo}/git/refs/heads/{base_branch}-gh-pages', self.secrets.bot_token) + + logging.info(f"Delete local '{base_branch}'") + try: + self.repo.git.branch('-D', base_branch) + except git.exc.GitCommandError: + logging.info(f"Local '{base_branch}' does not exist") + + for pr_branch in self.secrets.pr_branches: + logging.info(f"Delete '{self.secrets.test_repo}:{pr_branch}'") + github_api( + 'delete', f'repos/{self.secrets.test_repo}/git/refs/heads/{pr_branch}', self.secrets.bot_token) + + try: + logging.info("Delete local 'tmp' branch") + self.temp_repo.git.branch('-D', 'tmp') + 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)): + # Make PR's from a temporary directory + logging.info(f'Worktree directory: {self.temp_dir.name}') + self.repo.git.worktree('add', '--detach', self.temp_dir.name, f'HEAD') + self.temp_repo = git.Repo(self.temp_dir.name) + + # Run submission flow test with charts in PROD_REPO:PROD_BRANCH + self.set_git_username_email(self.temp_repo, self.secrets.bot_name, GITHUB_ACTIONS_BOT_EMAIL) + self.temp_repo.git.checkout(PROD_BRANCH, 'charts') + self.temp_repo.git.restore('--staged', 'charts') + self.secrets.submitted_charts = get_all_charts( + 'charts', self.secrets.vendor_type) + logging.info( + f"Found charts for {self.secrets.vendor_type}: {self.secrets.submitted_charts}") + self.temp_repo.git.checkout('-b', 'tmp') + + def get_owner_ids(self, chart_directory, owners_table): + + with open(f'{chart_directory}/OWNERS', 'r') as fd: + try: + owners = yaml.safe_load(fd) + # Pick owner ids for notification + owners_table[chart_directory] = [ + owner.get('githubUsername', '') for owner in owners['users']] + except yaml.YAMLError as err: + logging.warning( + f"Error parsing OWNERS of {chart_directory}: {err}") + + def push_chart(self, chart_directory, chart_name, chart_version, vendor_name, vendor_type, pr_branch): + # Push chart files to test_repo:pr_branch + self.temp_repo.git.add(f'{chart_directory}/{chart_version}') + self.temp_repo.git.commit( + '-m', f"Add {vendor_type} {vendor_name} {chart_name} {chart_version} chart files") + self.temp_repo.git.push(f'https://x-access-token:{self.secrets.bot_token}@github.com/{self.secrets.test_repo}', + f'HEAD:refs/heads/{pr_branch}', '-f') + + def report_failure(self,chart,chart_owners,failure_type,pr_html_url=None,run_html_url=None): + + os.environ['GITHUB_REPO'] = PROD_REPO.split('/')[1] + os.environ['GITHUB_AUTH_TOKEN'] = self.secrets.bot_token + if not self.secrets.dry_run: + os.environ['GITHUB_REPO'] = PROD_REPO.split('/')[1] + os.environ['GITHUB_AUTH_TOKEN'] = self.secrets.bot_token + os.environ['GITHUB_ORGANIZATION'] = PROD_REPO.split('/')[0] + logging.info(f"Send notification to '{self.secrets.notify_id}' about verification result of '{chart}'") + create_verification_issue(chart, chart_owners, failure_type,self.secrets.notify_id, pr_html_url, run_html_url, self.secrets.software_name, + self.secrets.software_version, self.secrets.bot_token, self.secrets.dry_run) + else: + os.environ['GITHUB_ORGANIZATION'] = PROD_REPO.split('/')[0] + os.environ['GITHUB_REPO'] = "sandbox" + os.environ['GITHUB_AUTH_TOKEN'] = self.secrets.bot_token + logging.info(f"Send notification to '{self.secrets.notify_id}' about dry run verification result of '{chart}'") + create_verification_issue(chart, chart_owners, failure_type,self.secrets.notify_id, pr_html_url, run_html_url, self.secrets.software_name, + self.secrets.software_version, self.secrets.bot_token, self.secrets.dry_run) + logging.info(f"Dry Run - send sandbox notification to '{chart_owners}' about verification result of '{chart}'") + + + def check_single_chart_result(self, vendor_type, vendor_name, chart_name, chart_version, pr_number, owners_table): + base_branch = f'{self.secrets.software_name}-{self.secrets.software_version}-{self.secrets.pr_base_branch}-{vendor_type}-{vendor_name}-{chart_name}-{chart_version}' + + # Check workflow conclusion + chart = f'{vendor_name} {chart_name} {chart_version}' + run_id, conclusion = super().check_workflow_conclusion(pr_number, 'success', failure_type='warning') + + if conclusion and run_id: + if conclusion != 'success': + # Send notification to owner through GitHub issues + r = github_api( + 'get', f'repos/{self.secrets.test_repo}/actions/runs/{run_id}', self.secrets.bot_token) + run = r.json() + run_html_url = run['html_url'] + + pr = get_pr(self.secrets,pr_number) + pr_html_url = pr["html_url"] + chart_directory = f'charts/{vendor_type}/{vendor_name}/{chart_name}' + chart_owners = owners_table[chart_directory] + + self.report_failure(chart,chart_owners,CHECKS_FAILED,pr_html_url,run_html_url) + + logging.warning(f"PR{pr_number} workflow failed: {vendor_name}, {chart_name}, {chart_version}") + return + else: + logging.info(f"PR{pr_number} workflow passed: {vendor_name}, {chart_name}, {chart_version}") + else: + logging.warning(f"PR{pr_number} workflow did not complete: {vendor_name}, {chart_name}, {chart_version}") + return + + + # Check PRs are merged + if not super().check_pull_request_result(pr_number, True, failure_type='warning'): + logging.warning(f"PR{pr_number} pull request was not merged: {vendor_name}, {chart_name}, {chart_version}") + return + logging.info(f"PR{pr_number} pull request was merged: {vendor_name}, {chart_name}, {chart_version}") + + # Check index.yaml is updated + if not super().check_index_yaml(base_branch, vendor_name, chart_name, chart_version, check_provider_type=False, failure_type='warning'): + logging.warning(f"PR{pr_number} - Chart was not found in Index file: {vendor_name}, {chart_name}, {chart_version}") + logging.info(f"PR{pr_number} - Chart was found in Index file: {vendor_name}, {chart_name}, {chart_version}") + + # Check release is published + chart_tgz = f'{chart_name}-{chart_version}.tgz' + if not super().check_release_result(vendor_name, chart_name, chart_version, chart_tgz, failure_type='warning'): + logging.warning(f"PR{pr_number} - Release was not created: {vendor_name}, {chart_name}, {chart_version}") + logging.info(f"PR{pr_number} - Release was created: {vendor_name}, {chart_name}, {chart_version}") + + def process_single_chart(self, vendor_type, vendor_name, chart_name, chart_version, pr_number_list, owners_table): + # Get SHA from 'pr_base_branch' branch + logging.info(f"Process chart: {vendor_type}/{vendor_name}/{chart_name}/{chart_version}") + r = github_api( + 'get', f'repos/{self.secrets.test_repo}/git/ref/heads/{self.secrets.pr_base_branch}', self.secrets.bot_token) + j = json.loads(r.text) + pr_base_branch_sha = j['object']['sha'] + + chart_directory = f'charts/{vendor_type}/{vendor_name}/{chart_name}' + base_branch = f'{self.secrets.software_name}-{self.secrets.software_version}-{self.secrets.pr_base_branch}-{vendor_type}-{vendor_name}-{chart_name}-{chart_version}' + base_branch = base_branch.replace(":","-") + pr_branch = f'{base_branch}-pr-branch' + + self.secrets.base_branches.append(base_branch) + self.secrets.pr_branches.append(pr_branch) + self.temp_repo.git.checkout('tmp') + self.temp_repo.git.checkout('-b', base_branch) + + # Create test gh-pages branch for checking index.yaml + self.create_test_gh_pages_branch(self.secrets.test_repo, base_branch, self.secrets.bot_token) + + # Create a new base branch for testing current chart + logging.info( + f"Create {self.secrets.test_repo}:{base_branch} for testing") + r = github_api( + 'get', f'repos/{self.secrets.test_repo}/branches', self.secrets.bot_token) + branches = json.loads(r.text) + branch_names = [branch['name'] for branch in branches] + if base_branch in branch_names: + logging.warning( + f"{self.secrets.test_repo}:{base_branch} already exists") + return + data = {'ref': f'refs/heads/{base_branch}', + 'sha': pr_base_branch_sha} + r = github_api( + 'post', f'repos/{self.secrets.test_repo}/git/refs', self.secrets.bot_token, json=data) + + # Remove chart and owners file from git + self.remove_chart(chart_directory, chart_version, self.secrets.test_repo, base_branch, self.secrets.bot_token) + self.remove_owners_file(chart_directory, self.secrets.test_repo, base_branch, self.secrets.bot_token) + + # Get owners id for notifications + self.get_owner_ids(chart_directory, owners_table) + + # Create and push test owners file + super().create_and_push_owners_file(chart_directory, base_branch, vendor_name, vendor_type, chart_name) + + # Push test chart to pr_branch + self.push_chart(chart_directory, chart_name, chart_version, vendor_name, vendor_type, pr_branch) + + # Create PR from pr_branch to base_branch + logging.info("sleep for 5 seconds to avoid secondary api limit") + time.sleep(5) + pr_number = super().send_pull_request(self.secrets.test_repo, base_branch, pr_branch, self.secrets.bot_token) + pr_number_list.append((vendor_type, vendor_name, chart_name, chart_version, pr_number)) + logging.info(f"PR{pr_number} created in {self.secrets.test_repo} into {base_branch} from {pr_branch}") + + # Record expected release tags + self.secrets.release_tags.append(f'{vendor_name}-{chart_name}-{chart_version}') + + def process_all_charts(self): + self.setup_git_context(self.repo) + self.setup_temp_dir() + + owners_table = dict() + pr_number_list = list() + + skip_charts = list() + + logging.info(f"Running tests for : {self.secrets.software_name} {self.secrets.software_version} :") + # First look for charts in index.yaml to see if kubeVersion is good: + if self.secrets.software_name == "OpenShift": + logging.info("check index file for invalid kubeVersions") + failed_charts = check_index_entries(self.secrets.software_version) + if failed_charts: + for chart in failed_charts: + providerDir = chart["providerType"].replace("partner","partners") + chart_directory = f'charts/{providerDir}/{chart["provider"]}/{chart["name"]}' + self.get_owner_ids(chart_directory,owners_table) + chart_owners = owners_table[chart_directory] + chart_id = f'{chart["provider"]} {chart["name"]} {chart["version"]}' + self.report_failure(chart_id,chart_owners,chart["message"],"","") + skip_charts.append(f'{chart["name"]}-{chart["version"]}') + + + # Process test charts and send PRs from temporary directory + with SetDirectory(Path(self.temp_dir.name)): + for vendor_type, vendor_name, chart_name, chart_version in self.secrets.submitted_charts: + if f'{chart_name}-{chart_version}' in skip_charts: + logging.info(f"Skip already failed chart: {vendor_type}, {vendor_name}, {chart_name}, {chart_version}") + else: + logging.info(f"Process chart: {vendor_type}, {vendor_name}, {chart_name}, {chart_version}") + self.process_single_chart(vendor_type, vendor_name, chart_name, chart_version, pr_number_list, owners_table) + logging.info("sleep for 5 seconds to avoid secondary api limit") + time.sleep(5) + + for vendor_type, vendor_name, chart_name, chart_version, pr_number in pr_number_list: + logging.info(f"PR{pr_number} Check result: {vendor_type}, {vendor_name}, {chart_name}, {chart_version}") + self.check_single_chart_result(vendor_type, vendor_name, chart_name, chart_version, pr_number, owners_table) + + diff --git a/tests/functional/behave_features/common/utils/github.py b/tests/functional/behave_features/common/utils/github.py new file mode 100644 index 0000000000..39a9aa21c5 --- /dev/null +++ b/tests/functional/behave_features/common/utils/github.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +"""Utility class for setting up and manipulating GitHub operations.""" + +import json +import requests +from retrying import retry + +from common.utils.setttings import * + +@retry(stop_max_delay=30_000, wait_fixed=1000) +def get_run_id(secrets, pr_number=None): + + pr = get_pr(secrets, pr_number) + r = github_api( + 'get', f'repos/{secrets.test_repo}/actions/runs', secrets.bot_token) + runs = json.loads(r.text) + + for run in runs['workflow_runs']: + if run['head_sha'] == pr['head']['sha'] and run['name'] == CERTIFICATION_CI_NAME: + return run['id'] + else: + raise Exception("Workflow for the submitted PR did not run.") + + +@retry(stop_max_delay=60_000*40, wait_fixed=2000) +def get_run_result(secrets, run_id): + r = github_api( + 'get', f'repos/{secrets.test_repo}/actions/runs/{run_id}', secrets.bot_token) + run = json.loads(r.text) + + if run['conclusion'] is None: + raise Exception(f"Workflow {run_id} is still running, PR: {secrets.pr_number} ") + + return run['conclusion'] + + +@retry(stop_max_delay=10_000, wait_fixed=1000) +def get_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] + missing_assets = list() + for asset in required_assets: + if asset not in asset_names: + missing_assets.append(asset) + if len(missing_assets) > 0: + raise Exception(f"Missing release asset: {missing_assets}") + + +@retry(stop_max_delay=15_000, wait_fixed=1000) +def get_release_by_tag(secrets, release_tag): + r = github_api( + 'get', f'repos/{secrets.test_repo}/releases', secrets.bot_token) + releases = json.loads(r.text) + for release in releases: + if release['tag_name'] == release_tag: + return release + raise Exception("Release not published") + + +def get_pr(secrets, pr_number=None): + pr_number = secrets.pr_number if pr_number is None else pr_number + r = github_api( + 'post', f'repos/{secrets.test_repo}/pulls/{pr_number}', secrets.bot_token) + pr = json.loads(r.text) + return pr + + +def github_api_get(endpoint, bot_token, headers={}): + if not headers: + headers = {'Accept': 'application/vnd.github.v3+json', + 'Authorization': f'Bearer {bot_token}'} + r = requests.get(f'{GITHUB_BASE_URL}/{endpoint}', headers=headers) + + return r + + +def github_api_delete(endpoint, bot_token, headers={}): + if not headers: + headers = {'Accept': 'application/vnd.github.v3+json', + 'Authorization': f'Bearer {bot_token}'} + r = requests.delete(f'{GITHUB_BASE_URL}/{endpoint}', headers=headers) + + return r + + +def github_api_post(endpoint, bot_token, headers={}, json={}): + if not headers: + headers = {'Accept': 'application/vnd.github.v3+json', + 'Authorization': f'Bearer {bot_token}'} + r = requests.post(f'{GITHUB_BASE_URL}/{endpoint}', + headers=headers, json=json) + + return r + + +def github_api(method, endpoint, bot_token, headers={}, data={}, json={}): + if method == 'get': + return github_api_get(endpoint, bot_token, headers=headers) + elif method == 'post': + return github_api_post(endpoint, bot_token, headers=headers, json=json) + elif method == 'delete': + return github_api_delete(endpoint, bot_token, headers=headers) + else: + raise ValueError( + "Github API method not implemented in helper function") diff --git a/tests/functional/behave_features/common/utils/index.py b/tests/functional/behave_features/common/utils/index.py new file mode 100644 index 0000000000..a6e6d97e6b --- /dev/null +++ b/tests/functional/behave_features/common/utils/index.py @@ -0,0 +1,43 @@ + +import logging +import semantic_version +import sys + +sys.path.append('../../../../../scripts/src') +from chartrepomanager import indexannotations +from indexfile import index + + + +def check_index_entries(ocpVersion): + + all_chart_list = index.get_latest_charts() + failed_chart_list = [] + + OCP_VERSION = semantic_version.Version.coerce(ocpVersion) + + for chart in all_chart_list: + if "supportedOCP" in chart and chart["supportedOCP"] != "N/A" and chart["supportedOCP"] != "": + if OCP_VERSION in semantic_version.NpmSpec(chart["supportedOCP"]): + logging.info(f'PASS: Chart {chart["name"]} {chart["version"]} supported OCP version {chart["supportedOCP"]} includes: {OCP_VERSION}') + else: + chart["message"] = f'chart {chart["name"]} {chart["version"]} supported OCP version {chart["supportedOCP"]} does not include latest OCP version {OCP_VERSION}' + logging.info(f' ERROR: Chart {chart["name"]} {chart["version"]} supported OCP version {chart["supportedOCP"]} does not include {OCP_VERSION}') + failed_chart_list.append(chart) + elif "kubeVersion" in chart and chart["kubeVersion"] != "": + supportedOCPVersion = indexannotations.getOCPVersions(chart["kubeVersion"]) + if OCP_VERSION in semantic_version.NpmSpec(supportedOCPVersion): + logging.info(f'PASS: Chart {chart["name"]} {chart["version"]} kubeVersion {chart["kubeVersion"]} (OCP: {supportedOCPVersion}) includes OCP version: {OCP_VERSION}') + else: + chart["message"] = f'chart {chart["name"]} {chart["version"]} kubeVersion {chart["kubeVersion"]} (OCP: {supportedOCPVersion}) does not include latest OCP version {OCP_VERSION}' + logging.info(f' ERROR: Chart {chart["name"]} {chart["version"]} kubeVersion {chart["kubeVersion"]} (OCP: {supportedOCPVersion}) does not include {OCP_VERSION}') + failed_chart_list.append(chart) + + return failed_chart_list + + + + + + + diff --git a/tests/functional/behave_features/common/utils/notifier.py b/tests/functional/behave_features/common/utils/notifier.py new file mode 100755 index 0000000000..fc1b433d1c --- /dev/null +++ b/tests/functional/behave_features/common/utils/notifier.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +"""Utility module for sending chart owners notifications.""" + +import json +import os +import sys + +import requests + +from common.utils.setttings import * + +endpoint_data = {} + +CHECKS_FAILED = "checks failed" + +def _set_endpoint_key(key, env_var): + if key not in endpoint_data: + if env_var in os.environ: + endpoint_data[key] = os.environ[env_var] + else: + raise Exception( + f"Environment variables {env_var} is required to connect to github") + + +def _set_endpoint(): + _set_endpoint_key("access_token", "GITHUB_AUTH_TOKEN") + _set_endpoint_key("organization", "GITHUB_ORGANIZATION") + _set_endpoint_key("repo", "GITHUB_REPO") + + +def _make_gihub_request(method, uri, body=None, params={}, headers={}, verbose=False): + headers.update({"Authorization": f'Bearer {endpoint_data["access_token"]}', + "Accept": "application/vnd.github.v3+json"}) + + url = f'{GITHUB_BASE_URL}/repos/{endpoint_data["organization"]}/{endpoint_data["repo"]}/{uri}' + + print(f"API url: {url}") + method_map = {"get": requests.get, + "post": requests.post, + "put": requests.put, + "delete": requests.delete, + "patch": requests.patch} + request_method = method_map[method] + response = request_method(url, params=params, headers=headers, json=body) + if verbose: + print(json.dumps(headers, indent=4, sort_keys=True)) + print(json.dumps(body, indent=4, sort_keys=True)) + print(json.dumps(params, indent=4, sort_keys=True)) + print(response.text) + response.raise_for_status() + try: + resp_json = response.json() + except Exception: + resp_json = None + if resp_json and verbose: + print(json.dumps(resp_json, indent=4, sort_keys=True)) + return resp_json + +# Call this method directly if you are not creating a verification issue nor a version change issue. +def create_an_issue(title, description, assignees=[], labels=[]): + uri = "issues" + method = "post" + body = {"title": title, + "body": description, + "assignees": assignees, + "labels": labels} + _make_gihub_request(method, uri, body=body, verbose=False) + + +def _verify_endpoint(access_token): + if "repo" not in endpoint_data: + raise Exception("GITHUB_REPO environment variable not defined") + + if "organization" not in endpoint_data: + raise Exception("GITHUB_ORGANIZATION environment variable not defined") + + if access_token: + endpoint_data["access_token"] = access_token + + +def create_verification_issue(chart, chart_owners, failure_type, notify_developers, pr_url, report_url, software_name, software_version, access_token=None, dry_run=False): + """Create and issue with chart-verifier findings after a version change trigger. + + chart_name -- Name of the chart that was verified. Include version for more verbose information\n + chart_owners -- Github IDs of the chart owners\n + failure_type - Indication of the type of failure + report_url -- URL or the report resulting from verification if applicable\n + kube-version -- The kubeVersion attribute of the chart if it is bade.\n + software_name -- Name of the software dependency that changed e.g, OCP and Chart Verifier\n + software_version -- The softwared dependency version used\n + access_token -- An optional github access token secret. If not passed will try to get from GITHUB_AUTH_TOKEN environment variable\ + dry-run -- Set if the test run is a dry-run. + """ + + + title = f"Chart {chart}" + if dry_run: + title = f"Dry Run: Chart {chart}" + + if failure_type == CHECKS_FAILED: + title = f"{title} has failures with {software_name} version {software_version}" + report_result = "some chart checks have failed. Please review the failures and, if required, consider submitting a new chart version with the appropriate additions/corrections." + body = (f"FYI @{' @'.join(notify_developers)}, in PR {pr_url} we triggered the chart certification workflow against chart {chart} because the workflow " + f"now supports {software_name} version {software_version}. We have found that {report_result}. Check details in the report: " + f"{report_url}, Chart owners are: {chart_owners}") + else: + title = f"{title} does not support {software_name} version {software_version}" + body = (f"FYI @{' @'.join(notify_developers)}, we checked the OCP versions supported by {chart} because the workflow " + f"now supports {software_name} version {software_version}. We have found that {failure_type}. Chart owners are: {chart_owners}") + + _set_endpoint() + _verify_endpoint(access_token) + create_an_issue(title, body) + + + + +def create_version_change_issue(chart_name, chart_owners, software_name, software_version, access_token=None): + """Create and issue with new version of software dependencies supported by certitifcation program. + + chart_name -- Name of the chart afected. Include version for more verbose information + chart_owners -- Github IDs of the chart owners\n + software_name -- Name of the software dependency that changed e.g, OCP and Chart Verifier\n + software_version -- The softwared dependency version used\n + access_token -- An optional github access token secret. If not passed will try to get from GITHUB_AUTH_TOKEN environment variable\n + """ + + title = f"Action needed for {chart_name} after a certification dependency change" + + body = (f"FYI @{' @'.join(chart_owners)}, {software_name} {software_version} is now supported by the certification program. " + "Consider submiting a new chart version.") + + _set_endpoint() + _verify_endpoint(access_token) + create_an_issue(title, body) + + +if __name__ == "__main__": + # Collecting info interactively + print("Enter the chart name: ") + chart_name = sys.stdin.readline().strip() + print("Enter chart owners: ") + chart_owners = sys.stdin.readline().strip().split() + print("Enter the github organization: ") + organization = sys.stdin.readline().strip() + print("Enter the github repo: ") + repo = sys.stdin.readline().strip() + + # setting endpoint + print(f"Creating custom issue in https://github.com/{organization}/{repo}") + endpoint_data["organization"] = organization + endpoint_data["repo"] = repo + + print("Enter the name of software dependency that changed: ") + software_name = sys.stdin.readline().strip() + print("Enter the version of software dependency that changed: ") + software_version = sys.stdin.readline().strip() + + print("What type of issue are you creating (verification/version-change)?: ") + issue_type = sys.stdin.readline().strip() + + if issue_type == "verification": + print("Enter the report url: ") + report_url = sys.stdin.readline().strip() + print("Did the chart verification pass (yes/no)?: ") + pass_answer = sys.stdin.readline().strip() + pass_verification = pass_answer == "yes" + create_verification_issue(chart_name, chart_owners, report_url, + software_name, software_version, pass_verification=pass_verification) + else: + create_version_change_issue( + chart_name, chart_owners, software_name, software_version) diff --git a/tests/functional/behave_features/common/utils/secret.py b/tests/functional/behave_features/common/utils/secret.py new file mode 100644 index 0000000000..d3240134cc --- /dev/null +++ b/tests/functional/behave_features/common/utils/secret.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +"""Utility class for storing test specific settings.""" + +from dataclasses import dataclass + +@dataclass +class E2ETestSecret: + # common secrets between one-shot and recursive tests + test_repo: str = '' + bot_name: str = '' + bot_token: str = '' + 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): + # one-shot testing + active_branch: str = '' + base_branch: str = '' + pr_branch: str = '' + pr_number: int = -1 + vendor: str = '' + bad_version: str = '' + provider_delivery: bool = False + index_file: str = "index.yaml" + +@dataclass +class E2ETestSecretRecursive(E2ETestSecret): + # recursive testing + software_name: str = '' + software_version: str = '' + pr_base_branch: str = '' + base_branches: list = None + pr_branches: list = None + dry_run: bool = True + notify_id: list = None + submitted_charts: list = None + release_tags: list = None diff --git a/tests/functional/behave_features/common/utils/set_directory.py b/tests/functional/behave_features/common/utils/set_directory.py new file mode 100644 index 0000000000..921c295f74 --- /dev/null +++ b/tests/functional/behave_features/common/utils/set_directory.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +"""Sets the cwd within the context. + +Reference: https://dev.to/teckert/changing-directory-with-a-python-context-manager-2bj8 +""" + +import os +from dataclasses import dataclass +from pathlib import Path + +@dataclass +class SetDirectory(object): + """ + Args: + path (Path): The path to the cwd + """ + path: Path + origin: Path = Path().absolute() + + def __enter__(self): + os.chdir(self.path) + def __exit__(self, exc_type, exc_value, traceback): + os.chdir(self.origin) diff --git a/tests/functional/behave_features/common/utils/setttings.py b/tests/functional/behave_features/common/utils/setttings.py new file mode 100644 index 0000000000..5f2439f173 --- /dev/null +++ b/tests/functional/behave_features/common/utils/setttings.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +"""Settings and global variables for e2e tests""" + +GITHUB_BASE_URL = 'https://api.github.com' +# The sandbox repository where we run all our tests on +TEST_REPO = 'openshift-helm-charts/sandbox' +# The prod repository where we create notification issues +PROD_REPO = 'openshift-helm-charts/charts' +# The prod branch where we store all chart files +PROD_BRANCH = 'main' +# This is used to find chart certification workflow run id +CERTIFICATION_CI_NAME = 'CI' +# GitHub actions bot email for git email +GITHUB_ACTIONS_BOT_EMAIL = '41898282+github-actions[bot]@users.noreply.github.com' diff --git a/tests/functional/behave_features/environment.py b/tests/functional/behave_features/environment.py new file mode 100644 index 0000000000..f34a5d2eba --- /dev/null +++ b/tests/functional/behave_features/environment.py @@ -0,0 +1,23 @@ +from behave import fixture, use_fixture +from common.utils.chart_certification import ChartCertificationE2ETestSingle +from common.utils.chart_certification import ChartCertificationE2ETestMultiple + +@fixture +def workflow_test(context): + context.workflow_test = ChartCertificationE2ETestSingle(test_name=context.test_name) + yield context.workflow_test + context.workflow_test.cleanup() + +@fixture +def submitted_chart_test(context): + context.chart_test = ChartCertificationE2ETestMultiple() + yield context.chart_test + context.chart_test.cleanup() + +def before_scenario(context, scenario): + if 'version-change' in scenario.tags: + print("[INFO] Using submitted charts fixture") + use_fixture(submitted_chart_test, context) + else: + context.test_name = scenario.name.split('@')[0][:-4].split(']')[1] + use_fixture(workflow_test, context) diff --git a/tests/functional/behave_features/steps/implementation.py b/tests/functional/behave_features/steps/implementation.py new file mode 100644 index 0000000000..8830d5af58 --- /dev/null +++ b/tests/functional/behave_features/steps/implementation.py @@ -0,0 +1,249 @@ +from behave import given, when, then + +############### Common step definitions ############### +@given(u'the vendor "{vendor}" has a valid identity as "{vendor_type}"') +def vendor_has_valid_identity(context, vendor, vendor_type): + context.workflow_test.set_vendor(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.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) + +@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.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) + +@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.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) + +@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.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_report() + context.workflow_test.push_chart(is_tarball=True) + +@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.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) + +@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.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_report() + context.workflow_test.push_chart(is_tarball=False) + +@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.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'a "{report_path}" is provided') +def user_generated_a_report(context, report_path): + context.workflow_test.update_test_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() + +@when(u'the user sends a pull request with the report') +@when(u'the user sends a pull request with the chart') +@when(u'the user sends a pull request with the chart and report') +def user_sends_a_pull_request(context): + context.workflow_test.send_pull_request() + +@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.send_pull_request() + +@then(u'the user sees the pull request is merged') +def pull_request_is_merged(context): + context.workflow_test.check_workflow_conclusion(expect_result='success') + context.workflow_test.check_pull_request_result(expect_merged=True) + context.workflow_test.check_pull_request_labels() + +@then(u'the index.yaml file is updated with an entry for the submitted chart') +def index_yaml_updated_with_submitted_chart(context): + context.workflow_test.check_index_yaml() + +@then(u'a release is published with corresponding report and chart tarball') +def release_is_published(context): + context.workflow_test.check_release_result() + +@then(u'the pull request is not merged') +def pull_request_is_not_merged(context): + context.workflow_test.check_workflow_conclusion(expect_result='failure') + context.workflow_test.check_pull_request_result(expect_merged=False) + +@then(u'user gets the "{message}" in the pull request comment') +def user_gets_a_message(context, message): + context.workflow_test.check_pull_request_comments(expect_message=message) + +########## Unique step definitions ################# + +@given(u'README file is missing in the chart') +def readme_file_is_missing(context): + context.workflow_test.remove_readme_file() + +@then(u'the index.yaml file is updated with an entry for the submitted chart with correct providerType') +def index_yaml_is_updated_with_new_entry_with_correct_provider_type(context): + context.workflow_test.check_index_yaml(check_provider_type=True) + +@given(u'the report contains an "{invalid_url}"') +def invalid_url_in_the_report(context, invalid_url): + context.workflow_test.process_report(update_url=True, url=invalid_url) + +@given(u'user adds a non chart related file') +def user_adds_a_non_chart_related_file(context): + context.workflow_test.add_non_chart_related_file() + +@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.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 + +@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): + if provider_control_report == "true": + context.workflow_test.process_report(update_provider_delivery=True, provider_delivery=True) + else: + context.workflow_test.process_report(update_provider_delivery=True, provider_delivery=False) + +@given(u'provider delivery controls is set to "{provider_control_report}" and a package digest is "{package_digest_set}" in the report') +def provider_delivery_control_and_package_digest_set_in_report(context, provider_control_report, package_digest_set=True): + if package_digest_set == "true": + no_package_digest = False + else: + no_package_digest = True + + if provider_control_report == "true": + context.workflow_test.process_report(update_provider_delivery=True, provider_delivery=True, unset_package_digest=no_package_digest) + else: + context.workflow_test.process_report(update_provider_delivery=True, provider_delivery=False, unset_package_digest=no_package_digest) + +@then(u'the "{index_file}" is updated with an entry for the submitted chart') +def index_file_is_updated(context, index_file): + context.workflow_test.secrets.index_file = index_file + context.workflow_test.check_index_yaml(True) + +@given(u'the report includes "{tested}" and "{supported}" OpenshiftVersion values and chart "{kubeversion}" value') +def report_includes_specified_versions(context, tested, supported, kubeversion): + context.workflow_test.process_report(update_versions=True, supported_versions=supported, tested_version=tested, kube_version=kubeversion) + +@given(u'the report has a "{check}" missing') +def report_has_a_check_missing(context, check): + context.workflow_test.process_report(missing_check=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_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) + +@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): + 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) + 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) + +@given(u'Chart.yaml specifies a "{bad_version}"') +def chart_yaml_specifies_bad_version(context, bad_version): + if bad_version != '': + context.workflow_test.update_bad_version(bad_version) + +@given(u'the report contains "{error}"') +def sha_value_does_not_match(context, error): + if error == 'sha_mismatch': + context.workflow_test.process_report(update_chart_sha=True) + else: + raise AssertionError(f"This {error} handling is not implemented yet") + +@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.send_pull_request() + +######## Test Submitted Charts Step definitions ########## +@given(u'there is a github workflow for testing existing charts') +def theres_github_workflow_for_testing_charts(context): + print("[INFO] Running step: there is a github workflow for testing existing charts") + +@when(u'a new Openshift or chart-verifier version is specified') +def new_openshift_or_verifier_version_is_specified(context): + print("[INFO] Running step: a new Openshift or chart-verifier version is specified") + +@when(u'the vendor type is specified, e.g. partner, and/or redhat') +def vendor_type_is_specified(context): + print("[INFO] Running step: the vendor type is specified, e.g. partner, and/or redhat") + +@when(u'workflow for testing existing charts is triggered') +def workflow_is_triggered(context): + print("[INFO] Running step: workflow for testing existing charts is triggered") + +@then(u'submission tests are run for existing charts') +def submission_tests_run_for_submitted_charts(context): + print("[INFO] Running step: submission tests are run for existing charts") + context.chart_test.process_all_charts() + +@then(u'all results are reported back to the caller') +def all_results_report_back_to_caller(context): + print("[INFO] Running step: all results are reported back to the caller") \ No newline at end of file diff --git a/tests/functional/features/HC-16_chart_test_takes_more_than_30mins.feature b/tests/functional/features/HC-16_chart_test_takes_more_than_30mins.feature new file mode 100644 index 0000000000..75083899fd --- /dev/null +++ b/tests/functional/features/HC-16_chart_test_takes_more_than_30mins.feature @@ -0,0 +1,31 @@ +Feature: Chart test takes longer time and exceeds default timeout + Partners, redhat or community user submit charts which result in errors + + Examples: + | chart_path | + | tests/data/vault-test-timeout-0.17.0.tgz | + + Scenario Outline: [HC-16-001] A partner or community user submits chart that takes more than 30 mins + Given the vendor has a valid identity as + And an error-free chart tarball is used in + 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 + + Examples: + | vendor_type | vendor | message | + | partners | hashicorp | Chart test failure: timed out waiting for the condition | + | community | redhat | Community charts require maintainer review and approval, a review will be conducted shortly | + + Scenario Outline: [HC-16-002] A redhat associate submits a chart that takes more than 30 mins + Given the vendor has a valid identity as + And an error-free chart tarball is used in + When the user sends a pull request with the chart + 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 corresponding report and chart tarball + + Examples: + | vendor_type | vendor | + | redhat | redhat | + diff --git a/tests/functional/features/HC-16_dash_in_version.feature b/tests/functional/features/HC-16_dash_in_version.feature new file mode 100644 index 0000000000..9d65c0c8fe --- /dev/null +++ b/tests/functional/features/HC-16_dash_in_version.feature @@ -0,0 +1,16 @@ +Feature: Report only submission + Partners, redhat and community users can publish their chart by submitting + error-free report that was generated by chart-verifier. + + Scenario Outline: [HC-16-001] A partner or redhat associate submits report only with dash in chart version + Given the vendor has a valid identity as + And an error-free report is used in + When the user sends a pull request with the report + Then the user sees the pull request is merged + And the index.yaml file is updated with an entry for the submitted chart + + Examples: + | vendor_type | vendor | report_path | + | partners | redhat | tests/data/HC-16/dash-in-version/partner/report.yaml | + | redhat | redhat | tests/data/HC-16/dash-in-version/redhat/report.yaml | + diff --git a/tests/functional/step_defs/HC-16_test_dash_in_version.py b/tests/functional/step_defs/HC-16_test_dash_in_version.py new file mode 100644 index 0000000000..4eaee44adc --- /dev/null +++ b/tests/functional/step_defs/HC-16_test_dash_in_version.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +"""Report only submission + +Partners, redhat and community users can publish their chart by submitting +error-free report that was generated by chart-verifier. +""" +import pytest +from pytest_bdd import scenario + +from functional.utils.chart_certification import ChartCertificationE2ETestSingle + +@pytest.fixture +def workflow_test(): + test_name = 'Test Chart Report Only' + workflow_test = ChartCertificationE2ETestSingle(test_name=test_name) + yield workflow_test + workflow_test.cleanup() + + +@scenario('../features/HC-16_dash_in_version.feature', "[HC-16-001] A partner or redhat associate submits report only with dash in chart version") +def test_partner_or_redhat_user_submits_report_dash_in_version(): + """A community user submits an error-free report""" diff --git a/tests/functional/step_defs/conftest.py b/tests/functional/step_defs/conftest.py index 4f9b4ecac5..83de389817 100644 --- a/tests/functional/step_defs/conftest.py +++ b/tests/functional/step_defs/conftest.py @@ -194,6 +194,7 @@ def user_should_see_pull_request_getting_merged(workflow_test): """the user sees the pull request is merged.""" workflow_test.check_workflow_conclusion(expect_result='success') workflow_test.check_pull_request_result(expect_merged=True) + workflow_test.check_pull_request_labels() @then("the pull request is not merged") def the_pull_request_is_not_getting_merged(workflow_test): diff --git a/tests/functional/step_defs/test_chart_test_takes_more_than_30mins.py b/tests/functional/step_defs/test_chart_test_takes_more_than_30mins.py new file mode 100644 index 0000000000..473d26281c --- /dev/null +++ b/tests/functional/step_defs/test_chart_test_takes_more_than_30mins.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" Chart test takes longer time and exceeds default timeout + Partners, redhat or community user submit charts which result in errors +""" +import logging +import datetime +import pytest +from pytest_bdd import scenario + +from functional.utils.chart_certification import ChartCertificationE2ETestSingle + +@pytest.fixture +def workflow_test(): + test_name = 'Chart test takes more than 30mins' + test_chart = 'tests/data/vault-test-timeout-0.17.0.tgz' + workflow_test = ChartCertificationE2ETestSingle(test_name=test_name, test_chart=test_chart) + start_time = datetime.datetime.now() + yield workflow_test + workflow_test.cleanup() + end_time = datetime.datetime.now() + time_diff = end_time - start_time + total_diff_seconds = time_diff.total_seconds() + if not int(total_diff_seconds) >= 1800: + pytest.fail(f"Timeout is not as expected: {total_diff_seconds}") + + +@scenario('../features/HC-16_chart_test_takes_more_than_30mins.feature', "[HC-16-001] A partner or community user submits chart that takes more than 30 mins") +def test_partner_or_community_chart_test_takes_more_than_30mins(): + """ A partner or community submitted chart takes more than 30 mins""" + +@scenario('../features/HC-16_chart_test_takes_more_than_30mins.feature', "[HC-16-002] A redhat associate submits a chart that takes more than 30 mins") +def test_redhat_chart_test_takes_more_than_30mins(): + """ A redhat submitted chart takes more than 30 mins""" \ No newline at end of file diff --git a/tests/functional/utils/chart_certification.py b/tests/functional/utils/chart_certification.py index d50a970de4..b9b4e4247b 100644 --- a/tests/functional/utils/chart_certification.py +++ b/tests/functional/utils/chart_certification.py @@ -16,8 +16,8 @@ import git import yaml import pytest -from functional.utils.notifier import create_verification_issue - +from functional.utils.notifier import * +from functional.utils.index import * from functional.utils.github import * from functional.utils.secret import * from functional.utils.set_directory import SetDirectory @@ -257,6 +257,28 @@ def check_pull_request_result(self, pr_number, expect_merged: bool, logger=pytes logger(f"PR{pr_number} Got unexpected status code from PR: {r.status_code}") return False + def check_pull_request_labels(self,pr_number,logger=pytest.fail): + r = github_api( + 'get', f'repos/{self.secrets.test_repo}/issues/{pr_number}/labels', self.secrets.bot_token) + labels = json.loads(r.text) + authorized_request = False + content_ok = False + for label in labels: + logging.info(f"PR{pr_number} found label {label['name']}") + if label['name'] == "authorized-request": + authorized_request = True + if label['name'] == "content-ok": + content_ok = True + + + if authorized_request and content_ok: + logging.info(f"PR{pr_number} authorized request and content-ok labels were found as expected") + return True + else: + logger(f"PR{pr_number} authorized request and/or content-ok labels were not found as expected") + return False + + def cleanup_release(self, expected_tag): """Cleanup the release and release tag. @@ -290,7 +312,10 @@ def __post_init__(self) -> None: # different processes. self.uuid = uuid.uuid4().hex - chart_name, chart_version = self.get_chart_name_version() + if self.test_report or self.test_chart: + self.secrets.chart_name, self.secrets.chart_version = self.get_chart_name_version() + self.chart_directory = f'charts/{self.secrets.vendor_type}/{self.secrets.vendor}/{self.secrets.chart_name}' + bot_name, bot_token = self.get_bot_name_and_token() test_repo = TEST_REPO @@ -324,12 +349,9 @@ def __post_init__(self) -> None: self.secrets.bot_token = bot_token self.secrets.base_branch = base_branch self.secrets.pr_branch = pr_branch - self.secrets.chart_name = chart_name - self.secrets.chart_version = chart_version self.secrets.index_file = "index.yaml" self.secrets.provider_delivery = False - def cleanup (self): # Cleanup releases and release tags self.cleanup_release() @@ -410,6 +432,7 @@ def set_vendor(self, vendor, vendor_type): 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}' + def setup_git_context(self): super().setup_git_context(self.repo) @@ -585,6 +608,11 @@ def check_workflow_conclusion(self, expect_result: str): def check_pull_request_result(self, expect_merged: bool): super().check_pull_request_result(self.secrets.pr_number, expect_merged, pytest.fail) + # expect_merged: boolean representing whether the PR should be merged + def check_pull_request_labels(self): + super().check_pull_request_labels(self.secrets.pr_number, pytest.fail) + + 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) @@ -762,50 +790,57 @@ def push_chart(self, chart_directory, chart_name, chart_version, vendor_name, ve self.temp_repo.git.push(f'https://x-access-token:{self.secrets.bot_token}@github.com/{self.secrets.test_repo}', f'HEAD:refs/heads/{pr_branch}', '-f') + def report_failure(self,chart,chart_owners,failure_type,pr_html_url=None,run_html_url=None): + + os.environ['GITHUB_REPO'] = PROD_REPO.split('/')[1] + os.environ['GITHUB_AUTH_TOKEN'] = self.secrets.bot_token + if not self.secrets.dry_run: + os.environ['GITHUB_REPO'] = PROD_REPO.split('/')[1] + os.environ['GITHUB_AUTH_TOKEN'] = self.secrets.bot_token + os.environ['GITHUB_ORGANIZATION'] = PROD_REPO.split('/')[0] + logging.info(f"Send notification to '{self.secrets.notify_id}' about verification result of '{chart}'") + create_verification_issue(chart, chart_owners, failure_type,self.secrets.notify_id, pr_html_url, run_html_url, self.secrets.software_name, + self.secrets.software_version, self.secrets.bot_token, self.secrets.dry_run) + else: + os.environ['GITHUB_ORGANIZATION'] = PROD_REPO.split('/')[0] + os.environ['GITHUB_REPO'] = "sandbox" + os.environ['GITHUB_AUTH_TOKEN'] = self.secrets.bot_token + logging.info(f"Send notification to '{self.secrets.notify_id}' about dry run verification result of '{chart}'") + create_verification_issue(chart, chart_owners, failure_type,self.secrets.notify_id, pr_html_url, run_html_url, self.secrets.software_name, + self.secrets.software_version, self.secrets.bot_token, self.secrets.dry_run) + logging.info(f"Dry Run - send sandbox notification to '{chart_owners}' about verification result of '{chart}'") + + def check_single_chart_result(self, vendor_type, vendor_name, chart_name, chart_version, pr_number, owners_table): base_branch = f'{self.secrets.software_name}-{self.secrets.software_version}-{self.secrets.pr_base_branch}-{vendor_type}-{vendor_name}-{chart_name}-{chart_version}' # Check workflow conclusion - chart = f'{vendor_type} {vendor_name} {chart_name} {chart_version}' + chart = f'{vendor_name} {chart_name} {chart_version}' run_id, conclusion = super().check_workflow_conclusion(pr_number, 'success', logging.warning) if conclusion and run_id: - # Send notification to owner through GitHub issues - r = github_api( - 'get', f'repos/{self.secrets.test_repo}/actions/runs/{run_id}', self.secrets.bot_token) - run = r.json() - run_html_url = run['html_url'] - chart_directory = f'charts/{vendor_type}/{vendor_name}/{chart_name}' - pass_verification = conclusion == 'success' - os.environ['GITHUB_REPO'] = PROD_REPO.split('/')[1] - os.environ['GITHUB_AUTH_TOKEN'] = self.secrets.bot_token - if not self.secrets.dry_run: - chart_owners = owners_table[chart_directory] - os.environ['GITHUB_REPO'] = PROD_REPO.split('/')[1] - os.environ['GITHUB_AUTH_TOKEN'] = self.secrets.bot_token - os.environ['GITHUB_ORGANIZATION'] = PROD_REPO.split('/')[0] - logging.info(f"PR{pr_number} Send notification to '{self.secrets.notify_id}' about verification result of '{chart}'") - create_verification_issue(f"charts/{vendor_name}/{chart_name}/{chart_version}", chart_owners, self.secrets.notify_id, run_html_url, self.secrets.software_name, - self.secrets.software_version, pass_verification, self.secrets.bot_token, self.secrets.dry_run) - else: + if conclusion != 'success': + # Send notification to owner through GitHub issues + r = github_api( + 'get', f'repos/{self.secrets.test_repo}/actions/runs/{run_id}', self.secrets.bot_token) + run = r.json() + run_html_url = run['html_url'] + + pr = get_pr(self.secrets,pr_number) + pr_html_url = pr["html_url"] + chart_directory = f'charts/{vendor_type}/{vendor_name}/{chart_name}' chart_owners = owners_table[chart_directory] - os.environ['GITHUB_ORGANIZATION'] = PROD_REPO.split('/')[0] - os.environ['GITHUB_REPO'] = "sandbox" - os.environ['GITHUB_AUTH_TOKEN'] = self.secrets.bot_token - logging.info(f"Send notification to '{self.secrets.notify_id}' about dry run verification result of '{chart}'") - create_verification_issue(f"charts/{vendor_name}/{chart_name}/{chart_version}", chart_owners, self.secrets.notify_id, run_html_url, self.secrets.software_name, - self.secrets.software_version, pass_verification, self.secrets.bot_token, self.secrets.dry_run) - logging.info(f"PR{pr_number} Dry Run - send sandbox notification to '{chart_owners}' about verification result of '{chart}'") + self.report_failure(chart,chart_owners,CHECKS_FAILED,pr_html_url,run_html_url) - if conclusion != 'success': logging.warning(f"PR{pr_number} workflow failed: {vendor_name}, {chart_name}, {chart_version}") return + else: + logging.info(f"PR{pr_number} workflow passed: {vendor_name}, {chart_name}, {chart_version}") else: logging.warning(f"PR{pr_number} workflow did not complete: {vendor_name}, {chart_name}, {chart_version}") return - logging.info(f"PR{pr_number} workflow passed: {vendor_name}, {chart_name}, {chart_version}") # Check PRs are merged if not super().check_pull_request_result(pr_number, True, logging.warning): @@ -891,13 +926,34 @@ def process_all_charts(self): owners_table = dict() pr_number_list = list() + skip_charts = list() + + logging.info(f"Running tests for : {self.secrets.software_name} {self.secrets.software_version} :") + # First look for charts in index.yaml to see if kubeVersion is good: + if self.secrets.software_name == "OpenShift": + logging.info("check index file for invalid kubeVersions") + failed_charts = check_index_entries(self.secrets.software_version) + if failed_charts: + for chart in failed_charts: + providerDir = chart["providerType"].replace("partner","partners") + chart_directory = f'charts/{providerDir}/{chart["provider"]}/{chart["name"]}' + self.get_owner_ids(chart_directory,owners_table) + chart_owners = owners_table[chart_directory] + chart_id = f'{chart["provider"]} {chart["name"]} {chart["version"]}' + self.report_failure(chart_id,chart_owners,chart["message"],"","") + skip_charts.append(f'{chart["name"]}-{chart["version"]}') + + # Process test charts and send PRs from temporary directory with SetDirectory(Path(self.temp_dir.name)): for vendor_type, vendor_name, chart_name, chart_version in self.secrets.submitted_charts: - logging.info(f"Process chart: {vendor_type}, {vendor_name}, {chart_name}, {chart_version}") - self.process_single_chart(vendor_type, vendor_name, chart_name, chart_version, pr_number_list, owners_table) - logging.info("sleep for 5 seconds to avoid secondary api limit") - time.sleep(5) + if f'{chart_name}-{chart_version}' in skip_charts: + logging.info(f"Skip already failed chart: {vendor_type}, {vendor_name}, {chart_name}, {chart_version}") + else: + logging.info(f"Process chart: {vendor_type}, {vendor_name}, {chart_name}, {chart_version}") + self.process_single_chart(vendor_type, vendor_name, chart_name, chart_version, pr_number_list, owners_table) + logging.info("sleep for 5 seconds to avoid secondary api limit") + time.sleep(5) for vendor_type, vendor_name, chart_name, chart_version, pr_number in pr_number_list: logging.info(f"PR{pr_number} Check result: {vendor_type}, {vendor_name}, {chart_name}, {chart_version}") diff --git a/tests/functional/utils/github.py b/tests/functional/utils/github.py index c815381a10..73a816d5c8 100644 --- a/tests/functional/utils/github.py +++ b/tests/functional/utils/github.py @@ -10,11 +10,8 @@ @retry(stop_max_delay=30_000, wait_fixed=1000) def get_run_id(secrets, pr_number=None): - pr_number = secrets.pr_number if pr_number is None else pr_number - r = github_api( - 'post', f'repos/{secrets.test_repo}/pulls/{pr_number}', secrets.bot_token) - pr = json.loads(r.text) + pr = get_pr(secrets, pr_number) r = github_api( 'get', f'repos/{secrets.test_repo}/actions/runs', secrets.bot_token) runs = json.loads(r.text) @@ -26,7 +23,7 @@ def get_run_id(secrets, pr_number=None): raise Exception("Workflow for the submitted PR did not run.") -@retry(stop_max_delay=60_000*10, wait_fixed=2000) +@retry(stop_max_delay=60_000*40, wait_fixed=2000) def get_run_result(secrets, run_id): r = github_api( 'get', f'repos/{secrets.test_repo}/actions/runs/{run_id}', secrets.bot_token) @@ -62,6 +59,15 @@ def get_release_by_tag(secrets, release_tag): return release raise Exception("Release not published") + +def get_pr(secrets, pr_number=None): + pr_number = secrets.pr_number if pr_number is None else pr_number + r = github_api( + 'post', f'repos/{secrets.test_repo}/pulls/{pr_number}', secrets.bot_token) + pr = json.loads(r.text) + return pr + + def github_api_get(endpoint, bot_token, headers={}): if not headers: headers = {'Accept': 'application/vnd.github.v3+json', diff --git a/tests/functional/utils/index.py b/tests/functional/utils/index.py new file mode 100644 index 0000000000..8ca0148862 --- /dev/null +++ b/tests/functional/utils/index.py @@ -0,0 +1,43 @@ + +import logging +import semantic_version +import sys + +sys.path.append('../../../scripts/src') +from chartrepomanager import indexannotations +from indexfile import index + + + +def check_index_entries(ocpVersion): + + all_chart_list = index.get_latest_charts() + failed_chart_list = [] + + OCP_VERSION = semantic_version.Version.coerce(ocpVersion) + + for chart in all_chart_list: + if "supportedOCP" in chart and chart["supportedOCP"] != "N/A" and chart["supportedOCP"] != "": + if OCP_VERSION in semantic_version.NpmSpec(chart["supportedOCP"]): + logging.info(f'PASS: Chart {chart["name"]} {chart["version"]} supported OCP version {chart["supportedOCP"]} includes: {OCP_VERSION}') + else: + chart["message"] = f'chart {chart["name"]} {chart["version"]} supported OCP version {chart["supportedOCP"]} does not include latest OCP version {OCP_VERSION}' + logging.info(f' ERROR: Chart {chart["name"]} {chart["version"]} supported OCP version {chart["supportedOCP"]} does not include {OCP_VERSION}') + failed_chart_list.append(chart) + elif "kubeVersion" in chart and chart["kubeVersion"] != "": + supportedOCPVersion = indexannotations.getOCPVersions(chart["kubeVersion"]) + if OCP_VERSION in semantic_version.NpmSpec(supportedOCPVersion): + logging.info(f'PASS: Chart {chart["name"]} {chart["version"]} kubeVersion {chart["kubeVersion"]} (OCP: {supportedOCPVersion}) includes OCP version: {OCP_VERSION}') + else: + chart["message"] = f'chart {chart["name"]} {chart["version"]} kubeVersion {chart["kubeVersion"]} (OCP: {supportedOCPVersion}) does not include latest OCP version {OCP_VERSION}' + logging.info(f' ERROR: Chart {chart["name"]} {chart["version"]} kubeVersion {chart["kubeVersion"]} (OCP: {supportedOCPVersion}) does not include {OCP_VERSION}') + failed_chart_list.append(chart) + + return failed_chart_list + + + + + + + diff --git a/tests/functional/utils/notifier.py b/tests/functional/utils/notifier.py index 9adeebc7c5..d44279e795 100755 --- a/tests/functional/utils/notifier.py +++ b/tests/functional/utils/notifier.py @@ -11,6 +11,7 @@ endpoint_data = {} +CHECKS_FAILED = "checks failed" def _set_endpoint_key(key, env_var): if key not in endpoint_data: @@ -77,35 +78,39 @@ def _verify_endpoint(access_token): endpoint_data["access_token"] = access_token -def create_verification_issue(chart_name, chart_owners, notify_developers, report_url, software_name, software_version, pass_verification, access_token=None, dry_run=False): +def create_verification_issue(chart, chart_owners, failure_type, notify_developers, pr_url, report_url, software_name, software_version, access_token=None, dry_run=False): """Create and issue with chart-verifier findings after a version change trigger. chart_name -- Name of the chart that was verified. Include version for more verbose information\n chart_owners -- Github IDs of the chart owners\n - report_url -- URL or the report resulting from verification\n + failure_type - Indication of the type of failure + report_url -- URL or the report resulting from verification if applicable\n + kube-version -- The kubeVersion attribute of the chart if it is bade.\n software_name -- Name of the software dependency that changed e.g, OCP and Chart Verifier\n software_version -- The softwared dependency version used\n - pass_verification -- A boolean indicating whether the verification passed\n - access_token -- An optional github access token secret. If not passed will try to get from GITHUB_AUTH_TOKEN environment variable\n + access_token -- An optional github access token secret. If not passed will try to get from GITHUB_AUTH_TOKEN environment variable\ + dry-run -- Set if the test run is a dry-run. """ - if not pass_verification: - title = f"Chart {chart_name}" - if dry_run: - title = f"Dry Run: Chart {chart_name}" - + title = f"Chart {chart}" + if dry_run: + title = f"Dry Run: Chart {chart}" + if failure_type == CHECKS_FAILED: title = f"{title} has failures with {software_name} version {software_version}" report_result = "some chart checks have failed. Please review the failures and, if required, consider submitting a new chart version with the appropriate additions/corrections." + body = (f"FYI @{' @'.join(notify_developers)}, in PR {pr_url} we triggered the chart certification workflow against chart {chart} because the workflow " + f"now supports {software_name} version {software_version}. We have found that {report_result}. Check details in the report: " + f"{report_url}, Chart owners are: {chart_owners}") + else: + title = f"{title} does not support {software_name} version {software_version}" + body = (f"FYI @{' @'.join(notify_developers)}, we checked the OCP versions supported by {chart} because the workflow " + f"now supports {software_name} version {software_version}. We have found that {failure_type}. Chart owners are: {chart_owners}") - body = (f"FYI @{' @'.join(notify_developers)}, we have triggered the chart certification workflow against chart {chart_name} because the workflow " - f"now supports {software_name} version {software_version}. We have found that {report_result}. Check details in the report: " - f"{report_url}, Chart owners are: {chart_owners}") - - _set_endpoint() - _verify_endpoint(access_token) - create_an_issue(title, body) + _set_endpoint() + _verify_endpoint(access_token) + create_an_issue(title, body)