From cbb06492ae7eaae10b416f571da0c8e9abad470a Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Tue, 28 Mar 2023 13:01:49 +0800 Subject: [PATCH] Revert "speed up ci builds from 15 to <7 minutes" (#1639) Reverted due to stuck ci --- .github/workflows/ci.yml | 211 +++++++++ .github/workflows/helm_release.yml | 2 +- .github/workflows/integration_tests.yml | 294 ++++++++++++ .github/workflows/issue_commands.yml | 2 +- .github/workflows/linting-and-tests.yml | 427 ------------------ .../publish-technical-documentation-next.yml | 2 +- ...ublish-technical-documentation-release.yml | 2 +- .github/workflows/snyk.yml | 17 +- engine/settings/ci-test.py | 2 +- engine/wait_for_test_mysql_start.sh | 4 +- .../alerts/onCallSchedule.test.ts | 5 + .../integration-tests/alerts/sms.test.ts | 5 + .../escalationChains/searching.test.ts | 25 +- .../integration-tests/globalSetup.ts | 42 +- .../uniqueIntegrationNames.test.ts | 5 + .../schedules/addOverride.test.ts | 5 + .../schedules/quality.test.ts | 13 +- .../utils/configurePlugin.ts | 38 ++ .../utils/escalationChain.ts | 12 +- .../integration-tests/utils/forms.ts | 14 +- grafana-plugin/package.json | 2 +- grafana-plugin/playwright.config.ts | 9 +- grafana-plugin/src/PluginPage.tsx | 6 +- .../components/Policy/EscalationPolicy.tsx | 2 + .../src/containers/GSelect/GSelect.tsx | 11 +- .../escalation-chains/EscalationChains.tsx | 7 +- grafana-plugin/yarn.lock | 22 +- 27 files changed, 624 insertions(+), 562 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/integration_tests.yml delete mode 100644 .github/workflows/linting-and-tests.yml create mode 100644 grafana-plugin/integration-tests/utils/configurePlugin.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..8d3d6d06 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,211 @@ +name: ci + +on: + push: + branches: + - main + - dev + pull_request: + # You can use the merge_group event to trigger your GitHub Actions workflow when + # a pull request is added to a merge queue + # https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue#triggering-merge-group-checks-with-github-actions + merge_group: + +jobs: + lint: + runs-on: ubuntu-latest + container: python:3.9 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 14.17.0 + - name: Build + run: | + pip install $(grep "pre-commit" engine/requirements.txt) + npm install -g yarn + cd grafana-plugin/ + yarn --network-timeout 500000 + yarn build + # pre-commit uses git, which is not working in the action without this workaround + # see https://github.com/actions/runner-images/issues/6775 + - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + - name: Lint All + run: | + pre-commit run --all-files + + test-frontend: + runs-on: ubuntu-latest + container: python:3.9 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 14.17.0 + - name: Unit Testing Frontend + run: | + pip install $(grep "pre-commit" engine/requirements.txt) + npm install -g yarn + cd grafana-plugin/ + yarn --network-timeout 500000 + yarn test + + test-technical-documentation: + runs-on: ubuntu-latest + steps: + - name: "Check out code" + uses: "actions/checkout@v3" + - name: "Build website" + # -e HUGO_REFLINKSERRORLEVEL=ERROR prevents merging broken refs with the downside + # that no refs to external content can be used as these refs will not resolve in the + # docs-base image. + run: | + docker run -v ${PWD}/docs/sources:/hugo/content/docs/oncall/latest -e HUGO_REFLINKSERRORLEVEL=ERROR --rm grafana/docs-base:latest /bin/bash -c 'make hugo' + + lint-migrations-backend-mysql-rabbitmq: + name: "Lint migrations" + runs-on: ubuntu-latest + container: python:3.9 + env: + DJANGO_SETTINGS_MODULE: settings.ci-test + SLACK_CLIENT_OAUTH_ID: 1 + services: + rabbit_test: + image: rabbitmq:3.7.19 + env: + RABBITMQ_DEFAULT_USER: rabbitmq + RABBITMQ_DEFAULT_PASS: rabbitmq + mysql_test: + image: mysql:5.7.25 + env: + MYSQL_DATABASE: oncall_local_dev + MYSQL_ROOT_PASSWORD: local_dev_pwd + steps: + - uses: actions/checkout@v3 + - name: Lint migrations + run: | + cd engine/ + pip install -r requirements.txt + python manage.py lintmigrations + + unit-test-backend-mysql-rabbitmq: + name: "Backend Tests: MySQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})" + runs-on: ubuntu-latest + container: python:3.9 + strategy: + matrix: + rbac_enabled: ["True", "False"] + env: + DJANGO_SETTINGS_MODULE: settings.ci-test + SLACK_CLIENT_OAUTH_ID: 1 + services: + rabbit_test: + image: rabbitmq:3.7.19 + env: + RABBITMQ_DEFAULT_USER: rabbitmq + RABBITMQ_DEFAULT_PASS: rabbitmq + mysql_test: + image: mysql:5.7.25 + env: + MYSQL_DATABASE: oncall_local_dev + MYSQL_ROOT_PASSWORD: local_dev_pwd + steps: + - uses: actions/checkout@v2 + - name: Unit Test Backend + run: | + apt-get update && apt-get install -y netcat + cd engine/ + pip install -r requirements.txt + ./wait_for_test_mysql_start.sh && ONCALL_TESTING_RBAC_ENABLED=${{ matrix.rbac_enabled }} pytest -x + + unit-test-backend-postgresql-rabbitmq: + name: "Backend Tests: PostgreSQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})" + runs-on: ubuntu-latest + container: python:3.9 + strategy: + matrix: + rbac_enabled: ["True", "False"] + env: + DATABASE_TYPE: postgresql + DJANGO_SETTINGS_MODULE: settings.ci-test + SLACK_CLIENT_OAUTH_ID: 1 + services: + rabbit_test: + image: rabbitmq:3.7.19 + env: + RABBITMQ_DEFAULT_USER: rabbitmq + RABBITMQ_DEFAULT_PASS: rabbitmq + postgresql_test: + image: postgres:14.4 + env: + POSTGRES_DB: oncall_local_dev + POSTGRES_PASSWORD: local_dev_pwd + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v2 + - name: Unit Test Backend + run: | + cd engine/ + pip install -r requirements.txt + ONCALL_TESTING_RBAC_ENABLED=${{ matrix.rbac_enabled }} pytest -x + + unit-test-backend-sqlite-redis: + name: "Backend Tests: SQLite + Redis (RBAC enabled: ${{ matrix.rbac_enabled }})" + runs-on: ubuntu-latest + container: python:3.9 + strategy: + matrix: + rbac_enabled: ["True", "False"] + env: + DATABASE_TYPE: sqlite3 + BROKER_TYPE: redis + REDIS_URI: redis://redis_test:6379 + DJANGO_SETTINGS_MODULE: settings.ci-test + SLACK_CLIENT_OAUTH_ID: 1 + services: + redis_test: + image: redis:7.0.5 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v2 + - name: Unit Test Backend + run: | + apt-get update && apt-get install -y netcat + cd engine/ + pip install -r requirements.txt + ONCALL_TESTING_RBAC_ENABLED=${{ matrix.rbac_enabled }} pytest -x + + unit-test-pd-migrator: + runs-on: ubuntu-latest + container: python:3.9 + steps: + - uses: actions/checkout@v2 + - name: Unit Test PD Migrator + run: | + cd tools/pagerduty-migrator + pip install -r requirements.txt + pytest -x + + docker-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Test docker build (no push) + id: docker_build + uses: docker/build-push-action@v2 + with: + context: ./engine + file: ./engine/Dockerfile + push: false + target: prod + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/helm_release.yml b/.github/workflows/helm_release.yml index d9fafeab..3c152260 100644 --- a/.github/workflows/helm_release.yml +++ b/.github/workflows/helm_release.yml @@ -1,4 +1,4 @@ -name: Helm Release +name: helm-release on: push: diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml new file mode 100644 index 00000000..e1d98938 --- /dev/null +++ b/.github/workflows/integration_tests.yml @@ -0,0 +1,294 @@ +name: Integration Tests + +on: + pull_request: + # You can use the merge_group event to trigger your GitHub Actions workflow when + # a pull request is added to a merge queue + # https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue#triggering-merge-group-checks-with-github-actions + merge_group: + +# TODO: ideally we would be able to have one CI job which spins up the kind cluster and does the helm release +# then we could have the UI and backend integration tests dependent on this job and not have to each +# independently spin up the cluster. This doesn't seem to be supported however +# https://github.com/docker/build-push-action/issues/225 +# +# Probably one way to get around this would be to deploy the helm release to a sandbox k8s cluster somewhere? and reference +# that in the various integration test jobs +jobs: + build-engine-docker-image: + name: Build engine Docker image + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Docker Buildx # We need this step for docker caching + uses: docker/setup-buildx-action@v2 + + - name: Build docker image locally # using github actions docker cache + uses: docker/build-push-action@v2 + with: + context: ./engine + file: ./engine/Dockerfile + push: false + load: true + tags: oncall/engine:latest + cache-from: type=gha + cache-to: type=gha,mode=max + outputs: type=docker,dest=/tmp/oncall-engine.tar + + # https://github.com/docker/build-push-action/issues/225#issuecomment-727639184 + - name: Persist engine Docker image between jobs + uses: actions/upload-artifact@v2 + with: + name: oncall-engine + path: /tmp/oncall-engine.tar + + backend-integration-tests: + name: Backend Integration Tests + needs: build-engine-docker-image + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Download engine Docker image + uses: actions/download-artifact@v2 + with: + name: oncall-engine + path: /tmp + + - name: Create k8s Kind Cluster + uses: helm/kind-action@v1.3.0 + with: + config: ./helm/kind.yml + + - name: Load image on the nodes of the cluster + run: kind load image-archive --name=chart-testing /tmp/oncall-engine.tar + + - name: Install helm chart + run: | + helm install test-release \ + --values ./simple.yml \ + --values ./values-local-image.yml \ + ./oncall + working-directory: helm + + - name: Await k8s pods and other resources up + uses: jupyterhub/action-k8s-await-workloads@v1 + with: + workloads: "" # all + namespace: "" # default + timeout: 300 + max-restarts: -1 + + # GitHub Action reference: https://github.com/jupyterhub/action-k8s-namespace-report + - name: Kubernetes namespace report + uses: jupyterhub/action-k8s-namespace-report@v1 + if: always() + + - name: Bootstrap organization and integration + run: | + export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=oncall,app.kubernetes.io/instance=test-release,app.kubernetes.io/component=engine" -o jsonpath="{.items[0].metadata.name}") + export ONCALL_INTEGRATION_URL=http://localhost:30001$(kubectl exec -it $POD_NAME -- bash -c "python manage.py setup_end_to_end_test --bootstrap_integration") + echo "ONCALL_INTEGRATION_URL=$ONCALL_INTEGRATION_URL" >> $GITHUB_ENV + + - name: Send an alert to the integration + run: | + echo $ONCALL_INTEGRATION_URL + export TEST_ID=test-0 + echo "TEST_ID=$TEST_ID" >> $GITHUB_ENV + curl -X POST "$ONCALL_INTEGRATION_URL" \ + -H 'Content-Type: Application/json' \ + -d '{ + "alert_uid": "08d6891a-835c-e661-39fa-96b6a9e26552", + "title": "'"$TEST_ID"'", + "image_url": "https://upload.wikimedia.org/wikipedia/commons/e/ee/Grumpy_Cat_by_Gage_Skidmore.jpg", + "state": "alerting", + "link_to_upstream_details": "https://en.wikipedia.org/wiki/Downtime", + "message": "Smth happened. Oh no!" + }' + + - name: Await 1 alert group and 1 alert created during the test (timeout 30 seconds) + run: | + export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=oncall,app.kubernetes.io/instance=test-release,app.kubernetes.io/component=engine" -o jsonpath="{.items[0].metadata.name}") + tries=30 + while [ "$tries" -gt 0 ]; do + if kubectl exec -it $POD_NAME -c oncall -- bash -c "python manage.py setup_end_to_end_test --return_results_for_test_id $TEST_ID" | grep -q '1, 1' + then + break + fi + + tries=$(( tries - 1 )) + sleep 1 + done + + if [ "$tries" -eq 0 ]; then + echo 'Expected "1, 1" (alert groups, alerts). They were not created in 30 seconds during this integration test. Something is broken' >&2 + exit 1 + fi + + ui-integration-tests: + needs: build-engine-docker-image + runs-on: ubuntu-latest + name: "UI Integration Tests - Grafana: ${{ matrix.grafana-image-tag }}" + strategy: + matrix: + grafana-image-tag: + - 9.2.6 + - main + - latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Download engine Docker image + uses: actions/download-artifact@v2 + with: + name: oncall-engine + path: /tmp + + - name: Create k8s Kind Cluster + uses: helm/kind-action@v1.3.0 + with: + config: ./helm/kind.yml + + - name: Load image on the nodes of the cluster + run: kind load image-archive --name=chart-testing /tmp/oncall-engine.tar + + # yarn caching doesn't properly work with subdirectories hence the following two steps + # which calculate a cache key and restore the cache manually + # see this GH issue for more details https://github.com/actions/setup-node/issues/488#issue-1231822552 + - uses: actions/setup-node@v3 + with: + node-version: 14.17.0 + cache: "yarn" + cache-dependency-path: grafana-plugin/yarn.lock + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT + shell: bash + working-directory: ./grafana-plugin + + - name: Restore yarn cache + uses: actions/cache@v3 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: yarn-cache-folder-${{ hashFiles('**/yarn.lock', '.yarnrc.yml') }} + restore-keys: | + yarn-cache-folder- + + # https://stackoverflow.com/a/62244232 + # --prefer-offline tells yarn to use cached downloads (in the cache directory mentioned above) + # during installation whenever possible instead of downloading from the server. + - name: Install dependencies + working-directory: ./grafana-plugin + run: yarn install --frozen-lockfile --prefer-offline + + # build the plugin frontend + - name: Build plugin frontend + working-directory: ./grafana-plugin + run: yarn build:dev + + # by settings grafana.plugins to [] and configuring grafana.extraVolumeMounts we are using the locally built + # OnCall plugin rather than the latest published version + # the /oncall-plugin hostPath refers to the kind volumeMount that points to the ./grafana-plugin dir + # see ./helm/kind.yml for more details + - name: Install helm chart + run: | + helm install helm-testing \ + --values ./helm/simple.yml \ + --values ./helm/values-local-image.yml \ + --set-json 'env=[{"name":"GRAFANA_CLOUD_NOTIFICATIONS_ENABLED","value":"False"}]' \ + --set oncall.twilio.accountSid="${{ secrets.TWILIO_ACCOUNT_SID }}" \ + --set oncall.twilio.authToken="${{ secrets.TWILIO_AUTH_TOKEN }}" \ + --set oncall.twilio.phoneNumber="\"${{ secrets.TWILIO_PHONE_NUMBER }}"\" \ + --set oncall.twilio.verifySid="${{ secrets.TWILIO_VERIFY_SID }}" \ + --set grafana.image.tag=${{ matrix.grafana-image-tag }} \ + --set grafana.env.GF_SECURITY_ADMIN_USER=oncall \ + --set grafana.env.GF_SECURITY_ADMIN_PASSWORD=oncall \ + --set grafana.env.GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=grafana-oncall-app \ + --set-json "grafana.plugins=[]" \ + --set-json 'grafana.securityContext={"runAsUser": 0, "runAsGroup": 0, "fsGroup": 0}' \ + --set-json 'grafana.extraVolumeMounts=[{"name":"plugins","mountPath":"/var/lib/grafana/plugins/grafana-plugin","hostPath":"/oncall-plugin","readOnly":true}]' \ + ./helm/oncall + + # https://github.com/microsoft/playwright/issues/7249#issuecomment-1154603556 + # Figures out the version of playwright that's installed. + # The result is stored in steps.playwright-version.outputs.version + - name: Get installed Playwright version + id: playwright-version + working-directory: ./grafana-plugin + run: echo "::set-output name=version::$(yarn list --pattern @playwright/test | grep @playwright/test@ | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')" + + # https://github.com/microsoft/playwright/issues/7249#issuecomment-1317670494 + # Attempt to restore the correct Playwright browser binaries based on the + # currently installed version of Playwright (The browser binary versions + # may change with Playwright versions). + # Note: Playwright's cache directory is hard coded because that's what it + # says to do in the docs. There doesn't appear to be a command that prints + # it out for us. + - name: Cache Playwright binaries + uses: actions/cache@v3 + id: playwright-cache + with: + path: "~/.cache/ms-playwright" + key: "${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}" + + # TODO: If the Playwright browser binaries weren't able to be restored, install them + # https://github.com/microsoft/playwright/issues/7249#issuecomment-1256878540 + - name: Install Playwright + # if: steps.playwright-cache.outputs.cache-hit != 'true' + working-directory: ./grafana-plugin + run: | + npx playwright install + npx playwright install-deps + + # - name: Install Playwright system dependencies + # run: npx playwright install-deps + # if: steps.playwright-cache.outputs.cache-hit == 'true' + # working-directory: ./grafana-plugin + + - name: Await k8s pods and other resources up + uses: jupyterhub/action-k8s-await-workloads@v1 + with: + workloads: "" # all + namespace: "" # default + timeout: 300 + max-restarts: -1 + + # GitHub Action reference: https://github.com/jupyterhub/action-k8s-namespace-report + - name: Kubernetes namespace report + uses: jupyterhub/action-k8s-namespace-report@v1 + if: always() + + - name: Run Integration Tests + env: + # BASE_URL represents what is accessed via a browser + BASE_URL: http://localhost:30002/grafana + # ONCALL_API_URL is what is configured in the plugin configuration form + # it is what the grafana container uses to communicate with the OnCall backend + # + # 172.17.0.1 is the docker bridge network default gateway. Requests originate in the grafana container + # hit 172.17.0.1 which proxies the request onto the host where port 30001 is the node port that is mapped + # to the OnCall API + ONCALL_API_URL: http://172.17.0.1:30001 + GRAFANA_USERNAME: oncall + GRAFANA_PASSWORD: oncall + MAILSLURP_API_KEY: ${{ secrets.MAILSLURP_API_KEY }} + working-directory: ./grafana-plugin + run: yarn test:integration + + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: ./grafana-plugin/playwright-report/ + retention-days: 30 diff --git a/.github/workflows/issue_commands.yml b/.github/workflows/issue_commands.yml index 6187ea56..e33a1468 100644 --- a/.github/workflows/issue_commands.yml +++ b/.github/workflows/issue_commands.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v2 with: repository: "grafana/grafana-github-actions" path: ./actions diff --git a/.github/workflows/linting-and-tests.yml b/.github/workflows/linting-and-tests.yml deleted file mode 100644 index d7b0dcc8..00000000 --- a/.github/workflows/linting-and-tests.yml +++ /dev/null @@ -1,427 +0,0 @@ -name: Linting and Unit/e2e Tests - -on: - push: - branches: - - main - - dev - pull_request: - # You can use the merge_group event to trigger your GitHub Actions workflow when - # a pull request is added to a merge queue - # https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue#triggering-merge-group-checks-with-github-actions - merge_group: - -concurrency: - # Cancel any running workflow for the same branch when new commits are pushed. - # We group both by ref_name (available when CI is triggered by a push to a branch/tag) - # and head_ref (available when CI is triggered by a PR). - group: "${{ github.ref_name }}-${{ github.head_ref }}" - cancel-in-progress: true - -jobs: - lint-entire-project: - name: "Lint entire project" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: "3.9.12" - cache: "pip" - cache-dependency-path: engine/requirements.txt - # following 2 steps - need to install the frontend dependencies for the eslint/prettier/stylelint steps - - uses: actions/setup-node@v3 - with: - node-version: 14.17.0 - cache: "yarn" - cache-dependency-path: grafana-plugin/yarn.lock - - name: Use cached frontend dependencies - id: cache-frontend-dependencies - uses: actions/cache@v3 - with: - path: grafana-plugin/node_modules - key: ${{ runner.os }}-frontend-node-modules-${{ hashFiles('grafana-plugin/yarn.lock') }} - - name: Install frontend dependencies - if: steps.cache-frontend-dependencies.outputs.cache-hit != 'true' - working-directory: grafana-plugin - run: yarn install --frozen-lockfile --prefer-offline --network-timeout 500000 - - uses: pre-commit/action@v3.0.0 - - lint-test-and-build-frontend: - name: "Lint, test, and build frontend" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 14.17.0 - cache: "yarn" - cache-dependency-path: grafana-plugin/yarn.lock - - name: Use cached frontend dependencies - id: cache-frontend-dependencies - uses: actions/cache@v3 - with: - path: grafana-plugin/node_modules - key: ${{ runner.os }}-frontend-node-modules-${{ hashFiles('grafana-plugin/yarn.lock') }} - - name: Install frontend dependencies - if: steps.cache-frontend-dependencies.outputs.cache-hit != 'true' - working-directory: grafana-plugin - run: yarn install --frozen-lockfile --prefer-offline --network-timeout 500000 - - name: Build frontend (will run linter and tests) - working-directory: grafana-plugin - run: yarn build - - test-technical-documentation: - name: "Test technical documentation" - runs-on: ubuntu-latest - steps: - - name: "Check out code" - uses: "actions/checkout@v3" - - name: "Build website" - # -e HUGO_REFLINKSERRORLEVEL=ERROR prevents merging broken refs with the downside - # that no refs to external content can be used as these refs will not resolve in the - # docs-base image. - run: | - docker run -v ${PWD}/docs/sources:/hugo/content/docs/oncall/latest -e HUGO_REFLINKSERRORLEVEL=ERROR --rm grafana/docs-base:latest /bin/bash -c 'make hugo' - - lint-migrations-backend-mysql-rabbitmq: - name: "Lint database migrations" - runs-on: ubuntu-latest - env: - DATABASE_HOST: localhost - RABBITMQ_URI: amqp://rabbitmq:rabbitmq@localhost:5672 - DJANGO_SETTINGS_MODULE: settings.ci-test - SLACK_CLIENT_OAUTH_ID: 1 - services: - rabbit_test: - image: rabbitmq:3.7.19 - env: - RABBITMQ_DEFAULT_USER: rabbitmq - RABBITMQ_DEFAULT_PASS: rabbitmq - ports: - - 5672:5672 - mysql_test: - image: mysql:5.7.25 - env: - MYSQL_DATABASE: oncall_local_dev - MYSQL_ROOT_PASSWORD: local_dev_pwd - ports: - - 3306:3306 - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: "3.9.12" - cache: "pip" - cache-dependency-path: engine/requirements.txt - - name: Lint migrations - working-directory: engine - run: | - pip install -r requirements.txt - python manage.py lintmigrations - - unit-test-backend-mysql-rabbitmq: - name: "Backend Tests: MySQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})" - runs-on: ubuntu-latest - strategy: - matrix: - rbac_enabled: ["True", "False"] - env: - DJANGO_SETTINGS_MODULE: settings.ci-test - DATABASE_HOST: localhost - RABBITMQ_URI: amqp://rabbitmq:rabbitmq@localhost:5672 - SLACK_CLIENT_OAUTH_ID: 1 - ONCALL_TESTING_RBAC_ENABLED: ${{ matrix.rbac_enabled }} - services: - rabbit_test: - image: rabbitmq:3.7.19 - env: - RABBITMQ_DEFAULT_USER: rabbitmq - RABBITMQ_DEFAULT_PASS: rabbitmq - ports: - - 5672:5672 - mysql_test: - image: mysql:5.7.25 - env: - MYSQL_DATABASE: oncall_local_dev - MYSQL_ROOT_PASSWORD: local_dev_pwd - ports: - - 3306:3306 - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: "3.9.12" - cache: "pip" - cache-dependency-path: engine/requirements.txt - - name: Unit Test Backend - working-directory: engine - run: | - apt-get update && apt-get install -y netcat - pip install -r requirements.txt - ./wait_for_test_mysql_start.sh && pytest -x - - unit-test-backend-postgresql-rabbitmq: - name: "Backend Tests: PostgreSQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})" - runs-on: ubuntu-latest - strategy: - matrix: - rbac_enabled: ["True", "False"] - env: - DATABASE_TYPE: postgresql - DATABASE_HOST: localhost - RABBITMQ_URI: amqp://rabbitmq:rabbitmq@localhost:5672 - DJANGO_SETTINGS_MODULE: settings.ci-test - SLACK_CLIENT_OAUTH_ID: 1 - ONCALL_TESTING_RBAC_ENABLED: ${{ matrix.rbac_enabled }} - services: - rabbit_test: - image: rabbitmq:3.7.19 - env: - RABBITMQ_DEFAULT_USER: rabbitmq - RABBITMQ_DEFAULT_PASS: rabbitmq - ports: - - 5672:5672 - postgresql_test: - image: postgres:14.4 - env: - POSTGRES_DB: oncall_local_dev - POSTGRES_PASSWORD: local_dev_pwd - ports: - - 5432:5432 - # Set health checks to wait until postgres has started - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: "3.9.12" - cache: "pip" - cache-dependency-path: engine/requirements.txt - - name: Unit Test Backend - working-directory: engine - run: | - pip install -r requirements.txt - pytest -x - - unit-test-backend-sqlite-redis: - name: "Backend Tests: SQLite + Redis (RBAC enabled: ${{ matrix.rbac_enabled }})" - runs-on: ubuntu-latest - strategy: - matrix: - rbac_enabled: ["True", "False"] - env: - DATABASE_TYPE: sqlite3 - BROKER_TYPE: redis - REDIS_URI: redis://localhost:6379 - DJANGO_SETTINGS_MODULE: settings.ci-test - SLACK_CLIENT_OAUTH_ID: 1 - ONCALL_TESTING_RBAC_ENABLED: ${{ matrix.rbac_enabled }} - services: - redis_test: - image: redis:7.0.5 - ports: - - 6379:6379 - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: "3.9.12" - cache: "pip" - cache-dependency-path: engine/requirements.txt - - name: Unit Test Backend - working-directory: engine - run: | - apt-get update && apt-get install -y netcat - pip install -r requirements.txt - pytest -x - - unit-test-pd-migrator: - name: "Unit tests - PagerDuty Migrator" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: "3.9.12" - cache: "pip" - cache-dependency-path: tools/pagerduty-migrator/requirements.txt - - name: Unit Test PD Migrator - working-directory: tools/pagerduty-migrator - run: | - pip install -r requirements.txt - pytest -x - - end-to-end-tests: - runs-on: ubuntu-latest - name: "End to end tests - Grafana: ${{ matrix.grafana-image-tag }}" - strategy: - matrix: - grafana-image-tag: - - 9.2.6 - - main - - latest - fail-fast: false - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Create k8s Kind Cluster - uses: helm/kind-action@v1.3.0 - with: - config: ./helm/kind.yml - - - uses: actions/setup-node@v3 - with: - node-version: 14.17.0 - cache: "yarn" - cache-dependency-path: grafana-plugin/yarn.lock - - - name: Use cached frontend dependencies - id: cache-frontend-dependencies - uses: actions/cache@v3 - with: - path: grafana-plugin/node_modules - key: ${{ runner.os }}-frontend-node-modules-${{ hashFiles('grafana-plugin/yarn.lock') }} - - - name: Install frontend dependencies - if: steps.cache-frontend-dependencies.outputs.cache-hit != 'true' - working-directory: grafana-plugin - run: yarn install --frozen-lockfile --prefer-offline --network-timeout 500000 - - - name: Use cached plugin frontend build - id: cache-plugin-frontend - uses: actions/cache@v3 - with: - path: grafana-plugin/dist - key: ${{ runner.os }}-plugin-frontend-${{ hashFiles('grafana-plugin/src/**/*', 'grafana-plugin/yarn.lock') }} - - - name: Build plugin frontend - if: steps.cache-plugin-frontend.outputs.cache-hit != 'true' - working-directory: grafana-plugin - run: yarn build:dev - - - name: Set up Docker Buildx # We need this step for docker caching - uses: docker/setup-buildx-action@v2 - - - name: Build engine Docker image locally # using github actions docker cache - uses: docker/build-push-action@v2 - with: - context: ./engine - file: ./engine/Dockerfile - push: false - load: true - tags: oncall/engine:latest - cache-from: type=gha - cache-to: type=gha,mode=max - outputs: type=docker,dest=/tmp/oncall-engine.tar - - - name: Load engine Docker image on the nodes of the cluster - run: kind load image-archive --name=chart-testing /tmp/oncall-engine.tar - - # spin up 2 engine, 2 celery, and 2 grafana pods, this will allow us to parralelize the integration tests - # and complete them much faster by using multiple test processes - # With just 1 engine/celery/grafana pod, the backend crawls to a halt when there is > 1 parallelized integration - # test process - # - # by settings grafana.plugins to [] and configuring grafana.extraVolumeMounts we are using the locally built - # OnCall plugin rather than the latest published version - # the /oncall-plugin hostPath refers to the kind volumeMount that points to the ./grafana-plugin dir - # see ./helm/kind.yml for more details - - name: Install helm chart - run: | - helm install helm-testing \ - --values ./helm/simple.yml \ - --values ./helm/values-local-image.yml \ - --set-json 'env=[{"name":"GRAFANA_CLOUD_NOTIFICATIONS_ENABLED","value":"False"}]' \ - --set engine.replicaCount=1 \ - --set celery.replicaCount=1 \ - --set celery.worker_beat_enabled="False" \ - --set oncall.twilio.accountSid="${{ secrets.TWILIO_ACCOUNT_SID }}" \ - --set oncall.twilio.authToken="${{ secrets.TWILIO_AUTH_TOKEN }}" \ - --set oncall.twilio.phoneNumber="\"${{ secrets.TWILIO_PHONE_NUMBER }}"\" \ - --set oncall.twilio.verifySid="${{ secrets.TWILIO_VERIFY_SID }}" \ - --set grafana.replicas=1 \ - --set grafana.image.tag=${{ matrix.grafana-image-tag }} \ - --set grafana.env.GF_SECURITY_ADMIN_USER=oncall \ - --set grafana.env.GF_SECURITY_ADMIN_PASSWORD=oncall \ - --set grafana.env.GF_FEATURE_TOGGLES_ENABLE=topnav \ - --set grafana.env.GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=grafana-oncall-app \ - --set-json "grafana.plugins=[]" \ - --set-json 'grafana.securityContext={"runAsUser": 0, "runAsGroup": 0, "fsGroup": 0}' \ - --set-json 'grafana.extraVolumeMounts=[{"name":"plugins","mountPath":"/var/lib/grafana/plugins/grafana-plugin","hostPath":"/oncall-plugin","readOnly":true}]' \ - ./helm/oncall - - # helpful reference for properly caching the playwright binaries/dependencies - # https://playwrightsolutions.com/playwright-github-action-to-cache-the-browser-binaries/ - - name: Get installed Playwright version - id: playwright-version - working-directory: grafana-plugin - run: echo "PLAYWRIGHT_VERSION=$(cat ./package.json | jq -r '.devDependencies["@playwright/test"]')" >> $GITHUB_ENV - - - name: Cache Playwright binaries/dependencies - id: playwright-cache - uses: actions/cache@v3 - with: - path: "~/.cache/ms-playwright" - key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} - - - name: Install Playwright binaries/dependencies - if: steps.playwright-cache.outputs.cache-hit != 'true' - # if more browsers are added, will need to modify the "npx playwright install" command - # https://stackoverflow.com/questions/65900299/install-single-dependency-from-package-json-with-yarn - run: | - yarn add "@playwright/test@${{ env.PLAYWRIGHT_VERSION }}" - npx playwright install --with-deps chromium firefox - npx playwright install-deps - - - name: Await k8s pods and other resources up - uses: jupyterhub/action-k8s-await-workloads@v1 - with: - workloads: "" # all - namespace: "" # default - timeout: 300 - max-restarts: -1 - - - name: Run Integration Tests - env: - # BASE_URL represents what is accessed via a browser - BASE_URL: http://localhost:30002/grafana - # ONCALL_API_URL is what is configured in the plugin configuration form - # it is what the grafana container uses to communicate with the OnCall backend - # - # 172.17.0.1 is the docker bridge network default gateway. Requests originate in the grafana container - # hit 172.17.0.1 which proxies the request onto the host where port 30001 is the node port that is mapped - # to the OnCall API - ONCALL_API_URL: http://172.17.0.1:30001 - GRAFANA_USERNAME: oncall - GRAFANA_PASSWORD: oncall - MAILSLURP_API_KEY: ${{ secrets.MAILSLURP_API_KEY }} - working-directory: ./grafana-plugin - # -x = exit command after first failing test - run: yarn test:integration -x - - # always spit out the engine and celery logs, AFTER the e2e tests have completed - # can be helpful for debugging failing/flaky tests - # GitHub Action reference: https://github.com/jupyterhub/action-k8s-namespace-report - - name: Kubernetes namespace report - uses: jupyterhub/action-k8s-namespace-report@v1 - if: failure() - with: - important-workloads: "deploy/helm-testing-oncall-engine deploy/helm-testing-oncall-celery" - - - uses: actions/upload-artifact@v3 - if: failure() - with: - name: playwright-report-grafana-${{ matrix.grafana-image-tag }} - path: ./grafana-plugin/playwright-report/ - retention-days: 30 diff --git a/.github/workflows/publish-technical-documentation-next.yml b/.github/workflows/publish-technical-documentation-next.yml index c242bb78..f8d1adcb 100644 --- a/.github/workflows/publish-technical-documentation-next.yml +++ b/.github/workflows/publish-technical-documentation-next.yml @@ -1,4 +1,4 @@ -name: "Publish Technical Documentation (next)" +name: "publish-technical-documentation-next" on: push: diff --git a/.github/workflows/publish-technical-documentation-release.yml b/.github/workflows/publish-technical-documentation-release.yml index 1078d0e0..82c26cd4 100644 --- a/.github/workflows/publish-technical-documentation-release.yml +++ b/.github/workflows/publish-technical-documentation-release.yml @@ -1,4 +1,4 @@ -name: "Publish Technical Documentation (release)" +name: "publish-technical-documentation-release" on: push: diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml index d0035215..086cba96 100644 --- a/.github/workflows/snyk.yml +++ b/.github/workflows/snyk.yml @@ -17,21 +17,16 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: "3.9.12" - cache: "pip" - cache-dependency-path: engine/requirements.txt + python-version: "3.9" - uses: actions/setup-node@v3 with: node-version: 14.17.0 - cache: "yarn" - cache-dependency-path: grafana-plugin/yarn.lock - uses: snyk/actions/setup@master - - name: Install backend dependencies - working-directory: engine - run: pip install -r requirements.txt - - name: Install frontend dependencies - working-directory: grafana-plugin - run: yarn install --frozen-lockfile --prefer-offline --network-timeout 500000 + - name: Install Dependencies + run: | + pip install -r engine/requirements.txt + cd grafana-plugin/ + yarn --network-timeout 500000 - name: Run Snyk continue-on-error: true run: snyk test --all-projects --severity-threshold=high diff --git a/engine/settings/ci-test.py b/engine/settings/ci-test.py index 9751c49d..23439c13 100644 --- a/engine/settings/ci-test.py +++ b/engine/settings/ci-test.py @@ -21,7 +21,7 @@ else: } if BROKER_TYPE == BrokerTypes.RABBITMQ: - CELERY_BROKER_URL = RABBITMQ_URI + CELERY_BROKER_URL = "amqp://rabbitmq:rabbitmq@rabbit_test:5672" elif BROKER_TYPE == BrokerTypes.REDIS: CELERY_BROKER_URL = REDIS_URI diff --git a/engine/wait_for_test_mysql_start.sh b/engine/wait_for_test_mysql_start.sh index 8eb15a13..bb89e036 100755 --- a/engine/wait_for_test_mysql_start.sh +++ b/engine/wait_for_test_mysql_start.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -until nc -z -v -w30 "${DATABASE_HOST:=mysql_test}" 3306 +until nc -z -v -w30 mysql_test 3306 do echo "Waiting for database connection..." sleep 1 -done +done \ No newline at end of file diff --git a/grafana-plugin/integration-tests/alerts/onCallSchedule.test.ts b/grafana-plugin/integration-tests/alerts/onCallSchedule.test.ts index 800faea6..a8b13efd 100644 --- a/grafana-plugin/integration-tests/alerts/onCallSchedule.test.ts +++ b/grafana-plugin/integration-tests/alerts/onCallSchedule.test.ts @@ -1,10 +1,15 @@ import { test } from '@playwright/test'; +import { configureOnCallPlugin } from '../utils/configurePlugin'; import { verifyThatAlertGroupIsTriggered } from '../utils/alertGroup'; import { createEscalationChain, EscalationStep } from '../utils/escalationChain'; import { generateRandomValue } from '../utils/forms'; import { createIntegrationAndSendDemoAlert } from '../utils/integrations'; import { createOnCallSchedule } from '../utils/schedule'; +test.beforeEach(async ({ page }) => { + await configureOnCallPlugin(page); +}); + test('we can create an oncall schedule + receive an alert', async ({ page }) => { const escalationChainName = generateRandomValue(); const integrationName = generateRandomValue(); diff --git a/grafana-plugin/integration-tests/alerts/sms.test.ts b/grafana-plugin/integration-tests/alerts/sms.test.ts index f4a1d8f7..dac42bb8 100644 --- a/grafana-plugin/integration-tests/alerts/sms.test.ts +++ b/grafana-plugin/integration-tests/alerts/sms.test.ts @@ -1,4 +1,5 @@ import { test, expect } from '@playwright/test'; +import { configureOnCallPlugin } from '../utils/configurePlugin'; import { GRAFANA_USERNAME } from '../utils/constants'; import { createEscalationChain, EscalationStep } from '../utils/escalationChain'; import { generateRandomValue } from '../utils/forms'; @@ -6,6 +7,10 @@ import { createIntegrationAndSendDemoAlert } from '../utils/integrations'; import { waitForSms } from '../utils/phone'; import { configureUserNotificationSettings, verifyUserPhoneNumber } from '../utils/userSettings'; +test.beforeEach(async ({ page }) => { + await configureOnCallPlugin(page); +}); + // TODO: enable once we've signed up for a MailSlurp account to receieve SMSes test.skip('we can verify our phone number + receive an SMS alert', async ({ page }) => { const escalationChainName = generateRandomValue(); diff --git a/grafana-plugin/integration-tests/escalationChains/searching.test.ts b/grafana-plugin/integration-tests/escalationChains/searching.test.ts index 5013f00f..97d0edf8 100644 --- a/grafana-plugin/integration-tests/escalationChains/searching.test.ts +++ b/grafana-plugin/integration-tests/escalationChains/searching.test.ts @@ -1,7 +1,12 @@ import { test, expect, Page } from '@playwright/test'; +import { configureOnCallPlugin } from '../utils/configurePlugin'; import { generateRandomValue } from '../utils/forms'; import { createEscalationChain } from '../utils/escalationChain'; +test.beforeEach(async ({ page }) => { + await configureOnCallPlugin(page); +}); + const assertEscalationChainSearchWorks = async ( page: Page, searchTerm: string, @@ -15,18 +20,18 @@ const assertEscalationChainSearchWorks = async ( await expect(page.getByTestId('escalation-chains-list')).toHaveText(escalationChainFullName); }; -// TODO: add tests for the new filtering. Commented out as this search doesn't exist anymore -test.skip('searching allows case-insensitive partial matches', async ({ page }) => { +test('searching allows case-insensitive partial matches', async ({ page }) => { const escalationChainName = `${generateRandomValue()} ${generateRandomValue()}`; - const [firstHalf, secondHalf] = escalationChainName.split(' '); + // const [firstHalf, secondHalf] = escalationChainName.split(' '); await createEscalationChain(page, escalationChainName); - await assertEscalationChainSearchWorks(page, firstHalf, escalationChainName); - await assertEscalationChainSearchWorks(page, firstHalf.toUpperCase(), escalationChainName); - await assertEscalationChainSearchWorks(page, firstHalf.toLowerCase(), escalationChainName); - - await assertEscalationChainSearchWorks(page, secondHalf, escalationChainName); - await assertEscalationChainSearchWorks(page, secondHalf.toUpperCase(), escalationChainName); - await assertEscalationChainSearchWorks(page, secondHalf.toLowerCase(), escalationChainName); + // Commented as this search doesn't exist anymore TODO: add tests for the new filtering + // await assertEscalationChainSearchWorks(page, firstHalf, escalationChainName); + // await assertEscalationChainSearchWorks(page, firstHalf.toUpperCase(), escalationChainName); + // await assertEscalationChainSearchWorks(page, firstHalf.toLowerCase(), escalationChainName); + // + // await assertEscalationChainSearchWorks(page, secondHalf, escalationChainName); + // await assertEscalationChainSearchWorks(page, secondHalf.toUpperCase(), escalationChainName); + // await assertEscalationChainSearchWorks(page, secondHalf.toLowerCase(), escalationChainName); }); diff --git a/grafana-plugin/integration-tests/globalSetup.ts b/grafana-plugin/integration-tests/globalSetup.ts index 244158d5..3fc05399 100644 --- a/grafana-plugin/integration-tests/globalSetup.ts +++ b/grafana-plugin/integration-tests/globalSetup.ts @@ -1,39 +1,6 @@ -import { chromium, FullConfig, expect, Page } from '@playwright/test'; +import { chromium, FullConfig, expect } from '@playwright/test'; -import { BASE_URL, GRAFANA_PASSWORD, GRAFANA_USERNAME, IS_OPEN_SOURCE, ONCALL_API_URL } from './utils/constants'; -import { clickButton, getInputByName } from './utils/forms'; -import { goToGrafanaPage } from './utils/navigation'; - -/** - * go to config page and wait for plugin icon to be available on left-hand navigation - */ -export const configureOnCallPlugin = async (page: Page): Promise => { - // plugin configuration can safely be skipped for non open-source environments - if (!IS_OPEN_SOURCE) { - return; - } - - /** - * go to the oncall plugin configuration page and wait for the page to be loaded - */ - await goToGrafanaPage(page, '/plugins/grafana-oncall-app'); - await page.waitForSelector('text=Configure Grafana OnCall'); - - /** - * we may need to fill in the OnCall API URL if it is not set in the process.env - * of the frontend build - */ - const onCallApiUrlInput = getInputByName(page, 'onCallApiUrl'); - const pluginIsAutoConfigured = (await onCallApiUrlInput.count()) === 0; - - if (!pluginIsAutoConfigured) { - await onCallApiUrlInput.fill(ONCALL_API_URL); - await clickButton({ page, buttonText: 'Connect' }); - } - - // wait for the "Connected to OnCall" message to know that everything is properly configured - await expect(page.getByTestId('status-message-block')).toHaveText(/Connected to OnCall.*/); -}; +import { BASE_URL, GRAFANA_PASSWORD, GRAFANA_USERNAME } from './utils/constants'; /** * Borrowed from our friends on the Incident team @@ -53,11 +20,6 @@ const globalSetup = async (config: FullConfig): Promise => { expect(res.ok()).toBeTruthy(); await browserContext.storageState({ path: './storageState.json' }); - - // make sure the plugin has been configured - const page = await browserContext.newPage(); - await configureOnCallPlugin(page); - await browserContext.close(); }; diff --git a/grafana-plugin/integration-tests/integrations/uniqueIntegrationNames.test.ts b/grafana-plugin/integration-tests/integrations/uniqueIntegrationNames.test.ts index e3c8df80..06fc9284 100644 --- a/grafana-plugin/integration-tests/integrations/uniqueIntegrationNames.test.ts +++ b/grafana-plugin/integration-tests/integrations/uniqueIntegrationNames.test.ts @@ -1,6 +1,11 @@ import { test, expect } from '@playwright/test'; +import { configureOnCallPlugin } from '../utils/configurePlugin'; import { openCreateIntegrationModal } from '../utils/integrations'; +test.beforeEach(async ({ page }) => { + await configureOnCallPlugin(page); +}); + test('integrations have unique names', async ({ page }) => { await openCreateIntegrationModal(page); diff --git a/grafana-plugin/integration-tests/schedules/addOverride.test.ts b/grafana-plugin/integration-tests/schedules/addOverride.test.ts index f76b6ef9..3104cc68 100644 --- a/grafana-plugin/integration-tests/schedules/addOverride.test.ts +++ b/grafana-plugin/integration-tests/schedules/addOverride.test.ts @@ -1,8 +1,13 @@ import { test, expect } from '@playwright/test'; +import { configureOnCallPlugin } from '../utils/configurePlugin'; import { clickButton, generateRandomValue } from '../utils/forms'; import { createOnCallSchedule, getOverrideFormDateInputs } from '../utils/schedule'; import dayjs from 'dayjs'; +test.beforeEach(async ({ page }) => { + await configureOnCallPlugin(page); +}); + test('default dates in override creation modal are correct', async ({ page }) => { const onCallScheduleName = generateRandomValue(); await createOnCallSchedule(page, onCallScheduleName); diff --git a/grafana-plugin/integration-tests/schedules/quality.test.ts b/grafana-plugin/integration-tests/schedules/quality.test.ts index 94b8e2a0..952e22d8 100644 --- a/grafana-plugin/integration-tests/schedules/quality.test.ts +++ b/grafana-plugin/integration-tests/schedules/quality.test.ts @@ -1,7 +1,12 @@ import { test, expect } from '@playwright/test'; +import { configureOnCallPlugin } from '../utils/configurePlugin'; import { generateRandomValue } from '../utils/forms'; import { createOnCallSchedule } from '../utils/schedule'; +test.beforeEach(async ({ page }) => { + await configureOnCallPlugin(page); +}); + test('check schedule quality for simple 1-user schedule', async ({ page }) => { const onCallScheduleName = generateRandomValue(); await createOnCallSchedule(page, onCallScheduleName); @@ -9,10 +14,6 @@ test('check schedule quality for simple 1-user schedule', async ({ page }) => { await expect(page.locator('div[class*="ScheduleQuality"]')).toHaveText('Quality: Great'); await page.hover('div[class*="ScheduleQuality"]'); - await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=2 ')).toHaveText( - 'Schedule has no gaps' - ); - await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=3 ')).toHaveText( - 'Schedule is perfectly balanced' - ); + await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=2 ')).toHaveText('Schedule has no gaps'); + await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=3 ')).toHaveText('Schedule is perfectly balanced'); }); diff --git a/grafana-plugin/integration-tests/utils/configurePlugin.ts b/grafana-plugin/integration-tests/utils/configurePlugin.ts new file mode 100644 index 00000000..d838cf15 --- /dev/null +++ b/grafana-plugin/integration-tests/utils/configurePlugin.ts @@ -0,0 +1,38 @@ +import type { Page } from '@playwright/test'; +import { ONCALL_API_URL, IS_OPEN_SOURCE } from './constants'; +import { clickButton, getInputByName } from './forms'; +import { goToGrafanaPage } from './navigation'; + +/** + * go to config page and wait for plugin icon to be available on left-hand navigation + */ +export const configureOnCallPlugin = async (page: Page): Promise => { + // plugin configuration can safely be skipped for non open-source environments + if (!IS_OPEN_SOURCE) { + return; + } + + /** + * go to the oncall plugin configuration page and wait for the page to be loaded + */ + await goToGrafanaPage(page, '/plugins/grafana-oncall-app'); + await page.waitForSelector('text=Configure Grafana OnCall'); + + /** + * we may need to fill in the OnCall API URL if it is not set in the process.env + * of the frontend build + */ + const onCallApiUrlInput = getInputByName(page, 'onCallApiUrl'); + const pluginIsAutoConfigured = (await onCallApiUrlInput.count()) === 0; + + if (!pluginIsAutoConfigured) { + await onCallApiUrlInput.fill(ONCALL_API_URL); + await clickButton({ page, buttonText: 'Connect' }); + } + + /** + * wait for the page to be refreshed and the icon to show up, this means the plugin + * has been successfully configured + */ + await page.waitForSelector('div.scrollbar-view img[src*="grafana-oncall-app/img/logo.svg"]'); +}; diff --git a/grafana-plugin/integration-tests/utils/escalationChain.ts b/grafana-plugin/integration-tests/utils/escalationChain.ts index a0f908e4..a4fdf3ee 100644 --- a/grafana-plugin/integration-tests/utils/escalationChain.ts +++ b/grafana-plugin/integration-tests/utils/escalationChain.ts @@ -1,4 +1,4 @@ -import { expect, Page } from '@playwright/test'; +import { Page } from '@playwright/test'; import { clickButton, fillInInput, selectDropdownValue } from './forms'; import { goToOnCallPage } from './navigation'; @@ -22,14 +22,6 @@ export const createEscalationChain = async ( // go to the escalation chains page await goToOnCallPage(page, 'escalations'); - /** - * wait for Esclation Chains page to fully load. this is because this can change which "New Escalation Chain" - * button is present - * ie. the one on the left hand side in the list vs the one in the center when no escalation chains exist - */ - await page.getByTestId('page-title').locator('text=Escalation Chains').waitFor({ state: 'visible' }); - await page.locator('text=Loading...').waitFor({ state: 'detached' }); - // open the create escalation chain modal (await page.waitForSelector('text=New Escalation Chain')).click(); @@ -38,7 +30,7 @@ export const createEscalationChain = async ( // submit the form and wait for it to be created await clickButton({ page, buttonText: 'Create' }); - await expect(page.getByTestId('escalation-chain-name')).toHaveText(escalationChainName); + await page.waitForSelector(`text=${escalationChainName}`); if (!escalationStep || !escalationStepValue) { return; diff --git a/grafana-plugin/integration-tests/utils/forms.ts b/grafana-plugin/integration-tests/utils/forms.ts index 46142e64..4981ec5f 100644 --- a/grafana-plugin/integration-tests/utils/forms.ts +++ b/grafana-plugin/integration-tests/utils/forms.ts @@ -54,7 +54,7 @@ const openSelect = async ({ placeholderText, selectType, startingLocator, -}: SelectDropdownValueArgs): Promise => { +}: SelectDropdownValueArgs): Promise => { /** * we currently mix three different dropdown components in the UI.. * so we need to support all of them :( @@ -73,8 +73,6 @@ const openSelect = async ({ const selectElement: Locator = (startingLocator || page).locator(selector); await selectElement.waitFor({ state: 'visible' }); await selectElement.click(); - - return selectElement; }; /** @@ -87,15 +85,7 @@ const chooseDropdownValue = async ({ page, value, optionExactMatch = true }: Sel page.locator(`div[id^="react-select-"][id$="-listbox"] >> ${textMatchSelector(optionExactMatch, value)}`).click(); export const selectDropdownValue = async (args: SelectDropdownValueArgs): Promise => { - const selectElement = await openSelect(args); - - /** - * use the select search to filter down the options - * TODO: get rid of the slice when we fix the GSelect component.. - * without slicing this would fire off an API request for every key-stroke - */ - await selectElement.type(args.value.slice(0, 5)); - + await openSelect(args); await chooseDropdownValue(args); }; diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index dfc6334e..ae9ad6a4 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -59,7 +59,7 @@ "@grafana/eslint-config": "^5.0.0", "@grafana/toolkit": "^9.2.4", "@jest/globals": "^27.5.1", - "@playwright/test": "^1.32.0", + "@playwright/test": "^1.28.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "12", "@testing-library/user-event": "^14.4.3", diff --git a/grafana-plugin/playwright.config.ts b/grafana-plugin/playwright.config.ts index e1bfa6d8..4977535d 100644 --- a/grafana-plugin/playwright.config.ts +++ b/grafana-plugin/playwright.config.ts @@ -14,8 +14,7 @@ const config: PlaywrightTestConfig = { testDir: './integration-tests', globalSetup: './integration-tests/globalSetup.ts', /* Maximum time one test can run for. */ - // TODO: set this back to 60 when GSelect component is refactored - timeout: 90 * 1000, + timeout: 60 * 1000, expect: { /** * Maximum time expect() should wait for the condition to be met. @@ -28,10 +27,8 @@ const config: PlaywrightTestConfig = { /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ - retries: process.env.CI ? 1 : 0, - // TODO: when GSelect component is refactored, run using 3 workers - // locally use one worker, on CI use 3 - // workers: process.env.CI ? 3 : 1, + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ workers: 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', diff --git a/grafana-plugin/src/PluginPage.tsx b/grafana-plugin/src/PluginPage.tsx index a80120f2..21a86341 100644 --- a/grafana-plugin/src/PluginPage.tsx +++ b/grafana-plugin/src/PluginPage.tsx @@ -23,11 +23,7 @@ function RealPlugin(props: AppPluginPageProps): React.ReactNode { {/* Render alerts at the top */}
- {pages[page]?.text && !pages[page]?.hideTitle && ( -

- {pages[page].text} -

- )} + {pages[page]?.text && !pages[page]?.hideTitle &&

{pages[page].text}

} {props.children} ); diff --git a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx index ac0811c3..329f8a10 100644 --- a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx +++ b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx @@ -297,6 +297,7 @@ export class EscalationPolicy extends React.Component { const team = teamStore.items[scheduleStore.items[item.value].team]; return ( @@ -350,6 +351,7 @@ export class EscalationPolicy extends React.Component { const team = teamStore.items[outgoingWebhookStore.items[item.value].team]; return ( diff --git a/grafana-plugin/src/containers/GSelect/GSelect.tsx b/grafana-plugin/src/containers/GSelect/GSelect.tsx index 919a9850..a471d2c2 100644 --- a/grafana-plugin/src/containers/GSelect/GSelect.tsx +++ b/grafana-plugin/src/containers/GSelect/GSelect.tsx @@ -9,7 +9,6 @@ import { observer } from 'mobx-react'; import { useStore } from 'state/useStore'; import styles from './GSelect.module.css'; -// import { debounce } from 'lodash'; const cx = cn.bind(styles); @@ -31,6 +30,7 @@ interface GSelectProps { showWarningIfEmptyValue?: boolean; showError?: boolean; nullItemName?: string; + fromOrganization?: boolean; filterOptions?: (id: any) => boolean; dropdownRender?: (menu: ReactElement) => ReactElement; getOptionLabel?: (item: SelectableValue) => React.ReactNode; @@ -61,6 +61,7 @@ const GSelect = observer((props: GSelectProps) => { showWarningIfEmptyValue = false, getDescription, filterOptions, + // fromOrganization, width = null, icon = null, } = props; @@ -88,11 +89,6 @@ const GSelect = observer((props: GSelectProps) => { [model, onChange] ); - /** - * without debouncing this function when search is available - * we risk hammering the API endpoint for every single key stroke - * some context on 250ms as the choice here - https://stackoverflow.com/a/44755058/3902555 - */ const loadOptions = (query: string) => { return model.updateItems(query).then(() => { const searchResult = model.getSearchResult(query); @@ -110,9 +106,6 @@ const GSelect = observer((props: GSelectProps) => { }); }; - // TODO: why doesn't this work properly? - // const loadOptions = debounce(_loadOptions, showSearch ? 250 : 0); - const values = isMulti ? (value ? (value as string[]) : []) .filter((id) => id in model.items) diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index 9581a193..dbcb04ed 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -287,12 +287,7 @@ class EscalationChainsPage extends React.Component - + {escalationChain.name}
diff --git a/grafana-plugin/yarn.lock b/grafana-plugin/yarn.lock index 57472da4..838cf5ab 100644 --- a/grafana-plugin/yarn.lock +++ b/grafana-plugin/yarn.lock @@ -2603,15 +2603,13 @@ resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.6.6.tgz#641f73913a6be402b34e4bdfca98d6832ed55586" integrity sha512-3MUulwMtsdCA9lw8a/Kc0XDBJJVCkYTQ5aGd+///TbfkOMXoOGAzzoiYKwPEsLYZv7He7fKJ/mCacqKOO7REyg== -"@playwright/test@^1.32.0": - version "1.32.0" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.32.0.tgz#0cc4c179e62995cc123adb12fdfaa093fed282c4" - integrity sha512-zOdGloaF0jeec7hqoLqM5S3L2rR4WxMJs6lgiAeR70JlH7Ml54ZPoIIf3X7cvnKde3Q9jJ/gaxkFh8fYI9s1rg== +"@playwright/test@^1.28.0": + version "1.28.0" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.28.0.tgz#8de83f9d2291bba3f37883e33431b325661720d9" + integrity sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ== dependencies: "@types/node" "*" - playwright-core "1.32.0" - optionalDependencies: - fsevents "2.3.2" + playwright-core "1.28.0" "@polka/url@^1.0.0-next.20": version "1.0.0-next.21" @@ -7085,7 +7083,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.2: +fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -10349,10 +10347,10 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" -playwright-core@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.32.0.tgz#730c2d1988d30377480b925aaa6c1b1e2442d67e" - integrity sha512-Z9Ij17X5Z3bjpp6XKujGBp9Gv4eViESac9aDmwgQFUEJBW0K80T21m/Z+XJQlu4cNsvPygw33b6V1Va6Bda5zQ== +playwright-core@1.28.0: + version "1.28.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.28.0.tgz#61df5c714f45139cca07095eccb4891e520e06f2" + integrity sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA== please-upgrade-node@^3.2.0: version "3.2.0"