Merge branch 'dev' into iskhakov/add-tracing

This commit is contained in:
Ildar Iskhakov 2023-01-04 10:46:49 +08:00
commit 2b0e4e1d14
31 changed files with 242 additions and 118 deletions

View file

@ -151,6 +151,17 @@ jobs:
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:

View file

@ -10,10 +10,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx # We need this step for docker caching
- 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
- name: Build docker image locally # using github actions docker cache
uses: docker/build-push-action@v2
with:
context: ./engine

View file

@ -5,6 +5,10 @@ repos:
- id: isort
files: ^engine
args: [--settings-file=engine/pyproject.toml, --filter-files]
- id: isort
name: isort - pd-migrator
files: ^tools/pagerduty-migrator
args: [--settings-file=tools/pagerduty-migrator/.isort.cfg, --filter-files]
- repo: https://github.com/psf/black
rev: 22.3.0
@ -12,6 +16,9 @@ repos:
- id: black
files: ^engine
args: [--config=engine/pyproject.toml]
- id: black
name: black - pd-migrator
files: ^tools/pagerduty-migrator
- repo: https://github.com/pycqa/flake8
rev: 3.9.2
@ -21,6 +28,12 @@ repos:
args: [--config=engine/tox.ini]
additional_dependencies:
- flake8-tidy-imports
- id: flake8
name: flake8 - pd-migrator
files: ^tools/pagerduty-migrator
# Make sure config is compatible with black
# https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#line-length
args: [--max-line-length=88, "--select=C,E,F,W,B,B950", "--extend-ignore=E203,E501"]
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.25.0

View file

@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Added ability to change engine deployment update strategy via values in helm chart.
- removed APNS support
- changed the `django-push-notification` library from the `iskhakov` fork to the [`grafana` fork](https://github.com/grafana/django-push-notifications).
This new fork basically patches an issue which affected the database migrations of this django app (previously the

View file

@ -86,7 +86,7 @@ restart:
$(call run_docker_compose_command,restart)
build:
$(call run_docker_compose_command,build --no-cache)
$(call run_docker_compose_command,build)
cleanup: stop
docker system prune --filter label="$(DOCKER_COMPOSE_DEV_LABEL)" --all --volumes

View file

@ -13,6 +13,7 @@
- [Could not build wheels for cryptography which use PEP 517 and cannot be installed directly](#could-not-build-wheels-for-cryptography-which-use-pep-517-and-cannot-be-installed-directly)
- [django.db.utils.OperationalError: (1366, "Incorrect string value")](#djangodbutilsoperationalerror-1366-incorrect-string-value)
- [/bin/sh: line 0: cd: grafana-plugin: No such file or directory](#binsh-line-0-cd-grafana-plugin-no-such-file-or-directory)
- [Encountered error while trying to install package - grpcio](#encountered-error-while-trying-to-install-package---grpcio)
- [IDE Specific Instructions](#ide-specific-instructions)
- [PyCharm](#pycharm)
@ -289,6 +290,34 @@ clear everything in docker by resetting or:
make cleanup
```
### Encountered error while trying to install package - grpcio
**Problem:**
We are currently using a library, `fcm-django`, which has a dependency on `grpcio`. Google does not provide `grpcio`
wheels built for Apple Silicon Macs. The best solution so far has been to use a `conda` virtualenv. There's apparently
a lot of community work put into making packages play well with M1/arm64 architecture.
```bash
pip install -r requirements.txt
...
note: This error originates from a subprocess, and is likely not a problem with pip.
error: legacy-install-failure
× Encountered error while trying to install package.
╰─> grpcio
...
```
**Solution:**
Use a `conda` virtualenv, and then run the following when installing the engine dependencies/
[See here for more details](https://stackoverflow.com/a/74307636/3902555)
```bash
GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=1 GRPC_PYTHON_BUILD_SYSTEM_ZLIB=1 pip install -r requirements.txt
```
## IDE Specific Instructions
### PyCharm

View file

@ -24,6 +24,7 @@ x-env-files: &oncall-env-files
x-env-vars: &oncall-env-vars
BROKER_TYPE: ${BROKER_TYPE}
GRAFANA_API_URL: http://localhost:3000
GOOGLE_APPLICATION_CREDENTIALS: /etc/app/gcp_service_account.json
# basically this is needed because the oncall backend containers have been configured to communicate w/ grafana via
# http://localhost:3000 (GRAFANA_API_URL). This URL is used in two scenarios:
@ -47,6 +48,7 @@ services:
labels: *oncall-labels
environment:
ONCALL_API_URL: http://host.docker.internal:8080
MOBILE_APP_QR_INTERVAL_QUEUE: 290000 # 4 minutes and 50 seconds
volumes:
- ./grafana-plugin:/etc/app
- /etc/app/node_modules
@ -105,6 +107,21 @@ services:
profiles:
- engine
flower:
container_name: flower
labels: *oncall-labels
image: mher/flower:1.2.0
environment:
# TODO: make this work properly w/ BROKER_TYPE env var
CELERY_BROKER_URL: "redis://redis:6379/0"
ports:
- "5555:5555"
depends_on:
oncall_celery:
condition: service_started
profiles:
- engine
oncall_db_migration:
container_name: oncall_db_migration
labels: *oncall-labels

1
engine/.gitignore vendored
View file

@ -3,3 +3,4 @@ extensions/
uwsgi-local.ini
celerybeat-schedule
*.db
gcp_service_account.json

View file

@ -1,8 +1,15 @@
FROM python:3.9-alpine3.16 AS base
RUN apk add bash python3-dev build-base linux-headers pcre-dev mariadb-connector-c-dev openssl-dev libffi-dev git
FROM python:3.9-slim-buster AS base
RUN apt-get update && apt-get install -y \
python3-dev \
gcc \
libmariadb-dev \
netcat \
curl \
bash
WORKDIR /etc/app
COPY ./requirements.txt ./
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
# we intentionally have two COPY commands, this is to have the requirements.txt in a separate build step
@ -16,7 +23,7 @@ RUN DJANGO_SETTINGS_MODULE=settings.prod_without_db DATABASE_TYPE=sqlite3 DATABA
RUN chown -R 1000:2000 /var/lib/oncall
FROM base AS dev
RUN apk add sqlite mysql-client postgresql-client
RUN apt-get install -y sqlite3 default-mysql-client postgresql-client
FROM dev AS dev-enterprise
RUN pip install -r requirements-enterprise-docker.txt

View file

@ -93,7 +93,7 @@ class AlertGroupQuerySet(models.QuerySet):
)
except IntegrityError:
try:
return self.get(**search_params, is_open_for_grouping=True), False
return self.get(**search_params, is_open_for_grouping__isnull=False), False
except self.model.DoesNotExist:
pass
raise

View file

@ -1,7 +1,7 @@
import json
from django.conf import settings
from push_notifications.models import GCMDevice
from fcm_django.models import FCMDevice
from apps.base.messaging import BaseMessagingBackend
from apps.mobile_app.tasks import notify_user_async
@ -35,7 +35,7 @@ class MobileAppBackend(BaseMessagingBackend):
token.delete()
# delete push notification related info for user
GCMDevice.objects.filter(user=user).delete()
FCMDevice.objects.filter(user=user).delete()
def serialize_user(self, user):
from apps.mobile_app.models import MobileAppAuthToken

View file

@ -1,4 +1,5 @@
from push_notifications.gcm import send_message
# from firebase_admin.messaging import Message
# from fcm_django.models import FCMDevice
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
@ -6,6 +7,7 @@ from rest_framework.views import APIView
REQUIRED_FIELDS = {"registration_ids", "notification", "data"}
# TODO: update thie
class FCMRelayView(APIView):
def post(self, request):
"""
@ -17,10 +19,11 @@ class FCMRelayView(APIView):
if not REQUIRED_FIELDS.issubset(request.data.keys()):
return Response(status=status.HTTP_400_BAD_REQUEST)
registration_ids = request.data["registration_ids"]
data = {
**request.data["data"],
**request.data["notification"],
}
# registration_ids = request.data["registration_ids"]
# data = {
# **request.data["data"],
# **request.data["notification"],
# }
return send_message(registration_ids=registration_ids, data=data, cloud_type="FCM")
# return FCMDevice.objects.send_message(Message(), False, ["registration_ids"])
return "TODO:"

View file

@ -1,5 +1,6 @@
from typing import Tuple
from django.conf import settings
from django.db import models
from django.utils import timezone
@ -7,7 +8,7 @@ from apps.auth_token import constants, crypto
from apps.auth_token.models import BaseAuthToken
from apps.user_management.models import Organization, User
MOBILE_APP_AUTH_VERIFICATION_TOKEN_TIMEOUT_SECONDS = 60
MOBILE_APP_AUTH_VERIFICATION_TOKEN_TIMEOUT_SECONDS = 60 * (5 if settings.DEBUG else 1)
def get_expire_date():

View file

@ -1,6 +1,7 @@
from celery.utils.log import get_task_logger
from django.conf import settings
from push_notifications.models import GCMDevice
from fcm_django.models import FCMDevice
from firebase_admin.messaging import APNSConfig, APNSPayload, Aps, ApsAlert, CriticalSound, Message
from apps.alerts.models import AlertGroup
from apps.mobile_app.alert_rendering import get_push_notification_message
@ -34,10 +35,10 @@ def notify_user_async(user_pk, alert_group_pk, notification_policy_pk, critical)
logger.warning(f"User notification policy {notification_policy_pk} does not exist")
return
gcm_devices_to_notify = GCMDevice.objects.filter(user=user)
device_to_notify = FCMDevice.objects.filter(user=user).first()
# create an error log in case user has no devices set up
if not gcm_devices_to_notify.exists():
if not device_to_notify:
UserNotificationPolicyLogRecord.objects.create(
author=user,
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
@ -47,39 +48,58 @@ def notify_user_async(user_pk, alert_group_pk, notification_policy_pk, critical)
notification_step=notification_policy.step,
notification_channel=notification_policy.notify_by,
)
logger.info(f"Error while sending a mobile push notification: user {user_pk} has no devices set up")
logger.info(f"Error while sending a mobile push notification: user {user_pk} has no device set up")
return
message = get_push_notification_message(alert_group)
thread_id = f"{alert_group.channel.organization.public_primary_key}:{alert_group.public_primary_key}"
alert_title = f"Critical page: {message}" if critical else message
number_of_alerts = alert_group.alerts.count()
if critical:
aps = {
"alert": f"Critical page: {message}",
"interruption-level": "critical",
"sound": "ambulance.aiff",
}
else:
aps = {
"alert": message,
"sound": "bingbong.aiff",
}
# TODO: we should update this to check if FCM_RELAY is set and conditionally make a call here..
extra = {
"orgId": alert_group.channel.organization.public_primary_key,
"orgName": alert_group.channel.organization.stack_slug,
"alertGroupId": alert_group.public_primary_key,
"status": alert_group.status,
"aps": aps,
}
logger.info(f"Sending push notification with message: {message}; thread-id: {thread_id}; extra: {extra}")
# TODO: rename category to USER_NEW_ALERT_GROUP
fcm_response = gcm_devices_to_notify.send_message(
message, thread_id=thread_id, category="USER_NEW_INCIDENT", extra=extra
message = Message(
token=device_to_notify.registration_id,
data={
# from the docs..
# A dictionary of data fields (optional). All keys and values in the dictionary must be strings
#
# alert_group.status is an int so it must be casted...
"orgId": alert_group.channel.organization.public_primary_key,
"orgName": alert_group.channel.organization.stack_slug,
"alertGroupId": alert_group.public_primary_key,
"status": str(alert_group.status),
"type": "oncall.critical_message" if critical else "oncall.message",
"title": alert_title,
"thread_id": thread_id,
},
apns=APNSConfig(
payload=APNSPayload(
aps=Aps(
thread_id=thread_id,
badge=number_of_alerts,
alert=ApsAlert(
title=alert_title,
subtitle="yooo this is a subtitle",
body="hello this is the body",
),
sound=CriticalSound(
critical=1 if critical else 0,
name="ambulance.aiff" if critical else "bingbong.aiff",
volume=1,
),
custom_data={
"interruption-level": "critical" if critical else "time-sensitive",
},
),
),
),
)
logger.info(f"Sending push notification with message: {message}; thread-id: {thread_id};")
fcm_response = device_to_notify.send_message(message)
# NOTE: we may want to further handle the response from FCM, but for now lets simply log it out
# https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream
logger.info(f"FCM response was: {fcm_response}")

View file

@ -1,25 +1,15 @@
from push_notifications.api.rest_framework import GCMDeviceAuthorizedViewSet, GCMDeviceSerializer
from fcm_django.api.rest_framework import FCMDeviceAuthorizedViewSet as BaseFCMDeviceAuthorizedViewSet
from rest_framework import status
from rest_framework.exceptions import NotFound
from rest_framework.response import Response
from rest_framework.serializers import HiddenField
from rest_framework.views import APIView
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication, MobileAppVerificationTokenAuthentication
from apps.mobile_app.models import MobileAppAuthToken
class FCMDeviceAuthorizedViewSet(GCMDeviceAuthorizedViewSet):
class FCMDeviceSerializer(GCMDeviceSerializer):
"""
GCMDevice has cloud_message_type equal to "GCM" by default, in this serializer cloud_message_type is always set
to "FCM" no matter what was provided in the request.
"""
cloud_message_type = HiddenField(default="FCM")
class FCMDeviceAuthorizedViewSet(BaseFCMDeviceAuthorizedViewSet):
authentication_classes = (MobileAppAuthTokenAuthentication,)
serializer_class = FCMDeviceSerializer
class MobileAppAuthTokenAPIView(APIView):

View file

@ -47,6 +47,8 @@ class TelegramMessageRenderer:
message_trimmed_text = MESSAGE_TRIMMED_TEXT.format(link=self.alert_group.web_link)
max_log_lines_length = max_message_length - len(start_line_text) - len(message_trimmed_text)
if max_log_lines_length < 0:
return ""
is_message_trimmed = len("\n".join(log_lines)) > max_log_lines_length
while len("\n".join(log_lines)) > max_log_lines_length:
log_lines.pop()

View file

@ -33,7 +33,7 @@ django-log-request-id==1.6.0
django-polymorphic==3.0.0
django-rest-polymorphic==0.1.9
pre-commit==2.15.0
https://github.com/grafana/django-push-notifications/archive/refs/tags/3.0.0-fix-migration.tar.gz
https://github.com/grafana/fcm-django/archive/refs/tags/v1.0.12r1.tar.gz
django-mirage-field==1.3.0
django-mysql==4.6.0
PyMySQL==1.0.2

View file

@ -219,7 +219,7 @@ INSTALLED_APPS = [
"debug_toolbar",
"social_django",
"polymorphic",
"push_notifications",
"fcm_django",
]
REST_FRAMEWORK = {
@ -549,18 +549,26 @@ GRAFANA_API_KEY_NAME = "Grafana OnCall"
EXTRA_MESSAGING_BACKENDS = []
if FEATURE_MOBILE_APP_INTEGRATION_ENABLED:
from firebase_admin import initialize_app
EXTRA_MESSAGING_BACKENDS += [
("apps.mobile_app.backend.MobileAppBackend", 5),
("apps.mobile_app.backend.MobileAppCriticalBackend", 6),
]
PUSH_NOTIFICATIONS_SETTINGS = {
"FCM_API_KEY": os.getenv("FCM_API_KEY"),
"FCM_POST_URL": os.getenv("FCM_POST_URL", default="https://fcm.googleapis.com/fcm/send"),
"USER_MODEL": "user_management.User",
"UPDATE_ON_DUPLICATE_REG_ID": True,
}
FIREBASE_APP = initialize_app(options={"projectId": os.environ.get("FCM_PROJECT_ID", None)})
FCM_RELAY_ENABLED = getenv_boolean("FCM_RELAY_ENABLED", default=False)
FCM_DJANGO_SETTINGS = {
# an instance of firebase_admin.App to be used as default for all fcm-django requests
# default: None (the default Firebase app)
"DEFAULT_FIREBASE_APP": None,
"APP_VERBOSE_NAME": "OnCall",
"ONE_DEVICE_PER_USER": True,
"DELETE_INACTIVE_DEVICES": False,
"UPDATE_ON_DUPLICATE_REG_ID": True,
"USER_MODEL": "user_management.User",
}
SELF_HOSTED_SETTINGS = {
"STACK_ID": 5,

View file

@ -39,6 +39,11 @@
opacity: 0.2;
}
.qr-code {
background-color: #fff;
margin-bottom: 12px;
}
.qr-loader {
position: absolute;
z-index: 10;

View file

@ -22,7 +22,9 @@ type Props = {
};
const INTERVAL_MIN_THROTTLING = 500;
const INTERVAL_QUEUE_QR = 50000;
const INTERVAL_QUEUE_QR = process.env.MOBILE_APP_QR_INTERVAL_QUEUE
? parseInt(process.env.MOBILE_APP_QR_INTERVAL_QUEUE, 10)
: 50000;
const INTERVAL_POLLING = 5000;
const BACKEND = 'MOBILE_APP';
@ -133,7 +135,7 @@ const MobileAppVerification = observer(({ userPk }: Props) => {
</Text>
<Text type="primary">Open Grafana IRM mobile application and scan this code to sync it with your account.</Text>
<div className={cx('u-width-100', 'u-flex', 'u-flex-center', 'u-position-relative')}>
<QRCode className={cx({ blurry: isQRBlurry })} value={QRCodeValue} />
<QRCode className={cx({ 'qr-code': true, blurry: isQRBlurry })} value={QRCodeValue} />
{isQRBlurry && <QRLoading />}
</div>
</VerticalGroup>

View file

@ -109,7 +109,7 @@ exports[`MobileAppVerification if we disconnect the app, it disconnects and fetc
class="u-width-100 u-flex u-flex-center u-position-relative"
>
<div
class="root root_bordered"
class="root qr-code root_bordered"
>
<svg
height="256"

View file

@ -189,7 +189,7 @@
"name": "Admin",
"description": "Read/write access to everything in OnCall",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.alert-groups:read" },
{ "action": "grafana-oncall-app.alert-groups:write" },
@ -239,7 +239,7 @@
"name": "Editor",
"description": "Similar to the Admin role, minus the abilities to: create Integrations, create Escalation Chains, create Schedules, create Outgoing Webhooks, update ChatOps settings, update other user's settings, and update general OnCall setings.",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.alert-groups:read" },
{ "action": "grafana-oncall-app.alert-groups:write" },
@ -279,7 +279,7 @@
"name": "Reader",
"description": "Read-only access to everything in OnCall",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.alert-groups:read" },
{ "action": "grafana-oncall-app.integrations:read" },
@ -300,7 +300,7 @@
"name": "OnCaller",
"description": "Grants read access to everything in OnCall. In addition, grants edit access to Alert Groups and Schedules",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.alert-groups:read" },
{ "action": "grafana-oncall-app.alert-groups:write" },
@ -326,7 +326,7 @@
"name": "Alert Groups Reader",
"description": "Read-only access to OnCall Alert Groups",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.alert-groups:read" }
]
},
@ -337,7 +337,7 @@
"name": "Alert Groups Editor",
"description": "Read/write access to OnCall Alert Groups",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.alert-groups:read" },
{ "action": "grafana-oncall-app.alert-groups:write" }
]
@ -349,7 +349,7 @@
"name": "Integrations Reader",
"description": "Read-only access to OnCall Integrations",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.integrations:read" }
]
},
@ -360,7 +360,7 @@
"name": "Integrations Editor",
"description": "Read/write access to OnCall Integrations",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.integrations:read" },
{ "action": "grafana-oncall-app.integrations:write" },
{ "action": "grafana-oncall-app.integrations:test" }
@ -373,7 +373,7 @@
"name": "Escalation Chains Reader",
"description": "Read-only access to OnCall Escalation Chains",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.escalation-chains:read" }
]
},
@ -384,7 +384,7 @@
"name": "Escalation Chains Editor",
"description": "Read/write access to OnCall Escalation Chains",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.escalation-chains:read" },
{ "action": "grafana-oncall-app.escalation-chains:write" }
]
@ -396,7 +396,7 @@
"name": "Schedules Reader",
"description": "Read-only access to OnCall Schedules",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.schedules:read" }
]
},
@ -407,7 +407,7 @@
"name": "Schedules Editor",
"description": "Read/write access to OnCall Schedules",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.schedules:read" },
{ "action": "grafana-oncall-app.schedules:write" },
{ "action": "grafana-oncall-app.schedules:export" }
@ -420,7 +420,7 @@
"name": "ChatOps Reader",
"description": "Read-only access to OnCall ChatOps",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.chatops:read" }
]
},
@ -431,7 +431,7 @@
"name": "ChatOps Editor",
"description": "Read/write access to OnCall ChatOps",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.chatops:read" },
{ "action": "grafana-oncall-app.chatops:write" },
{ "action": "grafana-oncall-app.chatops:update-settings" }
@ -444,7 +444,7 @@
"name": "Outgoing Webhooks Reader",
"description": "Read-only access to OnCall Outgoing Webhooks",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.outgoing-webhooks:read" }
]
},
@ -455,7 +455,7 @@
"name": "Outgoing Webhooks Editor",
"description": "Read/write access to OnCall Outgoing Webhooks",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.outgoing-webhooks:read" },
{ "action": "grafana-oncall-app.outgoing-webhooks:write" }
]
@ -467,7 +467,7 @@
"name": "Maintenance Reader",
"description": "Read-only access to OnCall Maintenance",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.maintenance:read" }
]
},
@ -478,7 +478,7 @@
"name": "Maintenance Editor",
"description": "Read/write access to OnCall Maintenance",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.maintenance:read" },
{ "action": "grafana-oncall-app.maintenance:write" }
]
@ -490,7 +490,7 @@
"name": "API Keys Reader",
"description": "Read-only access to OnCall API Keys",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.api-keys:read" }
]
},
@ -501,7 +501,7 @@
"name": "API Keys Editor",
"description": "Read/write access to OnCall API Keys. Also grants access to be able to consume the API.",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.api-keys:read" },
{ "action": "grafana-oncall-app.api-keys:write" }
]
@ -513,7 +513,7 @@
"name": "Notification Settings Reader",
"description": "Read-only access to OnCall Notification Settings",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.notification-settings:read" }
]
},
@ -524,7 +524,7 @@
"name": "Notification Settings Editor",
"description": "Read/write access to OnCall Notification Settings",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.notification-settings:read" },
{ "action": "grafana-oncall-app.notification-settings:write" }
]
@ -536,7 +536,7 @@
"name": "User Settings Reader",
"description": "Read-only access to OnCall User Settings",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.user-settings:read" }
]
},
@ -547,7 +547,7 @@
"name": "User Settings Editor",
"description": "Read/write access to own OnCall User Settings",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.user-settings:read" },
{ "action": "grafana-oncall-app.user-settings:write" }
]
@ -559,7 +559,7 @@
"name": "User Settings Admin",
"description": "Read/write access to your own, plus other's OnCall User Settings",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.user-settings:read" },
{ "action": "grafana-oncall-app.user-settings:write" },
{ "action": "grafana-oncall-app.user-settings:admin" }
@ -572,7 +572,7 @@
"name": "Settings Reader",
"description": "Read-only access to OnCall Settings",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.other-settings:read" }
]
},
@ -583,7 +583,7 @@
"name": "Settings Editor",
"description": "Read/write access to OnCall Settings",
"permissions": [
{ "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" },
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
{ "action": "grafana-oncall-app.other-settings:read" },
{ "action": "grafana-oncall-app.other-settings:write" }
]

View file

@ -142,6 +142,7 @@ module.exports.getWebpackConfig = (config, options) => {
*/
new webpack.EnvironmentPlugin({
ONCALL_API_URL: null,
MOBILE_APP_QR_INTERVAL_QUEUE: null,
}),
],

View file

@ -7,7 +7,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 1.1.0
version: 1.1.1
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to

View file

@ -10,10 +10,7 @@ spec:
matchLabels:
{{- include "oncall.engine.selectorLabels" . | nindent 6 }}
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
type: RollingUpdate
{{- toYaml .Values.engine.updateStrategy | nindent 4 }}
template:
metadata:
{{- with .Values.podAnnotations }}

View file

@ -29,6 +29,14 @@ engine:
# cpu: 100m
# memory: 128Mi
## Deployment update strategy
## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy
updateStrategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
type: RollingUpdate
## Affinity for pod assignment
## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity
affinity: {}

View file

@ -105,13 +105,13 @@ def escalation_policy_report(policies: list[dict]) -> str:
for policy in sorted(
policies, key=lambda p: bool(p["unmatched_users"] or p["flawed_schedules"])
):
result += f"\n" + TAB + format_escalation_policy(policy)
result += "\n" + TAB + format_escalation_policy(policy)
for user in policy["unmatched_users"]:
result += f"\n" + TAB * 2 + format_user(user)
result += "\n" + TAB * 2 + format_user(user)
for schedule in policy["flawed_schedules"]:
result += f"\n" + TAB * 2 + format_schedule(schedule)
result += "\n" + TAB * 2 + format_schedule(schedule)
if (
not policy["unmatched_users"]
@ -135,7 +135,7 @@ def integration_report(integrations: list[dict]) -> str:
key=lambda i: bool(i["oncall_type"] and not i["is_escalation_policy_flawed"]),
reverse=True,
):
result += f"\n" + TAB + format_integration(integration)
result += "\n" + TAB + format_integration(integration)
if (
integration["oncall_type"]
and not integration["is_escalation_policy_flawed"]

View file

@ -4,7 +4,7 @@ from migrator.utils import find_by_id
def match_user(user: dict, oncall_users: list[dict]) -> None:
oncall_user = None
for candidate_user in oncall_users:
if user["email"] == candidate_user["email"]:
if user["email"].lower() == candidate_user["email"].lower():
oncall_user = candidate_user
break

View file

@ -1553,21 +1553,14 @@ expected_integrations_result = [
]
def test_matching():
match_users_test()
match_schedule_test()
match_escalation_policies_test()
match_integrations_test()
def match_users_test():
def test_match_user():
for user in pd_users_payload:
match_user(user, oncall_users_payload)
assert pd_users_payload == expected_users_match_result
def match_schedule_test():
def test_match_schedule():
for schedule in pd_schedules_payload:
match_schedule(schedule, oncall_schedules_payload)
match_users_for_schedule(schedule, pd_users_payload)
@ -1575,7 +1568,7 @@ def match_schedule_test():
assert pd_schedules_payload == expected_schedules_result
def match_escalation_policies_test():
def test_match_escalation_policy():
for policy in pd_escalation_policies_payload:
match_escalation_policy(policy, oncall_escalation_chains)
match_users_and_schedules_for_escalation_policy(
@ -1585,7 +1578,7 @@ def match_escalation_policies_test():
assert pd_escalation_policies_payload == expected_escalation_policies_result
def match_integrations_test():
def test_match_integration():
integrations = []
for service in pd_services_payload:
service_integrations = service.pop("integrations")

View file

@ -0,0 +1,17 @@
from migrator.resources.users import match_user
def test_match_user_not_found():
pd_user = {"email": "test@test.com"}
oncall_users = [{"email": "test1@test.com"}]
match_user(pd_user, oncall_users)
assert pd_user["oncall_user"] is None
def test_match_user_email_case_insensitive():
pd_user = {"email": "test@test.com"}
oncall_users = [{"email": "TEST@TEST.COM"}]
match_user(pd_user, oncall_users)
assert pd_user["oncall_user"] == oncall_users[0]

View file

@ -1,6 +1,4 @@
requests==2.27.1
pdpyras==4.5.0
isort==5.10.1
black==22.3.0
pytest==7.1.2
pytest-env==0.6.2