diff --git a/.github/workflows/linting-and-tests.yml b/.github/workflows/linting-and-tests.yml index f7cb062b..4bf8ae8d 100644 --- a/.github/workflows/linting-and-tests.yml +++ b/.github/workflows/linting-and-tests.yml @@ -388,7 +388,6 @@ jobs: --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 diff --git a/.markdownlint.json b/.markdownlint.json index 62ff374f..31dead19 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -1,7 +1,9 @@ { "default": true, "MD013": { - "line_length": "120" + "line_length": "120", + "code_blocks": false, + "tables": false }, "MD024": { "siblings_only": true diff --git a/CHANGELOG.md b/CHANGELOG.md index ed5dc29e..a66d6cdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## v1.2.42 (2023-06-12) ### Changed -- Run containers as a non-root user by @alexintech [#2053](https://github.com/grafana/oncall/pull/2053) +- Helm chart: Upgrade helm dependecies, improve local setup [#2144](https://github.com/grafana/oncall/pull/2144) + +### Fixed + +- Fixed bug on Filters where team param from URL was discarded [#6237](https://github.com/grafana/support-escalations/issues/6237) +- Fix receive channel filter in alert groups API [#2140](https://github.com/grafana/oncall/pull/2140) +- Helm chart: Fix usage of `env` settings as map; + Fix usage of `mariadb.auth.database` and `mariadb.auth.username` for MYSQL env variables by @alexintech [#2146](https://github.com/grafana/oncall/pull/2146) ## v1.2.41 (2023-06-08) @@ -17,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Twilio Provider improvements by @Konstantinov-Innokentii, @mderynck and @joeyorlando [#2074](https://github.com/grafana/oncall/pull/2074) [#2034](https://github.com/grafana/oncall/pull/2034) +- Run containers as a non-root user by @alexintech [#2053](https://github.com/grafana/oncall/pull/2053) ## v1.2.40 (2023-06-07) diff --git a/README.md b/README.md index 3cc78af2..398b7250 100644 --- a/README.md +++ b/README.md @@ -45,17 +45,45 @@ We prepared multiple environments: ```bash echo "DOMAIN=http://localhost:8080 - COMPOSE_PROFILES=with_grafana # Remove this line if you want to use existing grafana + # Remove 'with_grafana' below if you want to use existing grafana + # Add 'with_prometheus' below to optionally enable a local prometheus for oncall metrics + # e.g. COMPOSE_PROFILES=with_grafana,with_prometheus + COMPOSE_PROFILES=with_grafana + # to setup an auth token for prometheus exporter metrics: + # PROMETHEUS_EXPORTER_SECRET=my_random_prometheus_secret + # also, make sure to enable the /metrics endpoint: + # FEATURE_PROMETHEUS_EXPORTER_ENABLED=True SECRET_KEY=my_random_secret_must_be_more_than_32_characters_long" > .env ``` -3. Launch services: +3. (Optional) If you want to enable/setup the prometheus metrics exporter +(besides the changes above), create a `prometheus.yml` file (replacing +`my_random_prometheus_secret` accordingly), next to your `docker-compose.yml`: + + ```bash + echo "global: + scrape_interval: 15s + evaluation_interval: 15s + + scrape_configs: + - job_name: prometheus + metrics_path: /metrics/ + authorization: + - credentials: my_random_prometheus_secret + static_configs: + - targets: [\"host.docker.internal:8080\"]" > prometheus.yml + ``` + + NOTE: you will need to setup a Prometheus datasource using `http://prometheus:9090` + as the URL in the Grafana UI. + +4. Launch services: ```bash docker-compose pull && docker-compose up -d ``` -4. Go to [OnCall Plugin Configuration](http://localhost:3000/plugins/grafana-oncall-app), using log in credentials +5. Go to [OnCall Plugin Configuration](http://localhost:3000/plugins/grafana-oncall-app), using log in credentials as defined above: `admin`/`admin` (or find OnCall plugin in configuration->plugins) and connect OnCall _plugin_ with OnCall _backend_: @@ -63,7 +91,7 @@ We prepared multiple environments: OnCall backend URL: http://engine:8080 ``` -5. Enjoy! Check our [OSS docs](https://grafana.com/docs/oncall/latest/open-source/) if you want to set up +6. Enjoy! Check our [OSS docs](https://grafana.com/docs/oncall/latest/open-source/) if you want to set up Slack, Telegram, Twilio or SMS/calls through Grafana Cloud. ## Update version diff --git a/dev/README.md b/dev/README.md index 82aa0567..212bb727 100644 --- a/dev/README.md +++ b/dev/README.md @@ -269,26 +269,18 @@ ERROR: Failed building wheel for cryptography **Solution:** - - ```bash LDFLAGS="-L$(brew --prefix openssl@1.1)/lib" CFLAGS="-I$(brew --prefix openssl@1.1)/include" pip install `cat engine/requirements.txt | grep cryptography` ``` - - ### django.db.utils.OperationalError: (1366, "Incorrect string value") **Problem:** - - ```bash django.db.utils.OperationalError: (1366, "Incorrect string value: '\\xF0\\x9F\\x98\\x8A\\xF0\\x9F...' for column 'cached_name' at row 1") ``` - - **Solution:** Recreate the database with the correct encoding. @@ -321,15 +313,11 @@ $ CDPATH="" make init When running `make init start`: - - ```bash Error response from daemon: open /var/lib/docker/overlay2/ac57b871108ee1b98ff4455e36d2175eae90cbc7d4c9a54608c0b45cfb7c6da5/committed: is a directory make: *** [start] Error 1 ``` - - **Solution:** clear everything in docker by resetting or: @@ -376,8 +364,6 @@ See solution for "Encountered error while trying to install package - grpcio" [h This problem seems to occur when running the Celery process, outside of `docker-compose` (via `make run-backend-celery`), and using a `conda` virtual environment. - - ```bash conda create --name oncall-dev python=3.9.13 conda activate oncall-dev @@ -396,8 +382,6 @@ File "~/oncall/engine/engine/__init__.py", line 5, in ImportError: dlopen(/opt/homebrew/Caskroom/miniconda/base/envs/oncall-dev/lib/python3.9/site-packages/grpc/_cython/cygrpc.cpython-39-darwin.so, 0x0002): symbol not found in flat namespace '_EVP_DigestSignUpdate' ``` - - **Solution:** [This solution](https://github.com/grpc/grpc/issues/15510#issuecomment-392012594) posted in a GitHub issue thread for diff --git a/docker-compose.yml b/docker-compose.yml index 6bb3e6cf..ad8cc97a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,8 @@ x-environment: &oncall-environment BROKER_TYPE: redis BASE_URL: $DOMAIN SECRET_KEY: $SECRET_KEY + FEATURE_PROMETHEUS_EXPORTER_ENABLED: $FEATURE_PROMETHEUS_EXPORTER_ENABLED + PROMETHEUS_EXPORTER_SECRET: $PROMETHEUS_EXPORTER_SECRET REDIS_URI: redis://redis:6379/0 DJANGO_SETTINGS_MODULE: settings.hobby CELERY_WORKER_QUEUE: "default,critical,long,slack,telegram,webhook,retry,celery" @@ -72,6 +74,18 @@ services: interval: 5s retries: 10 + prometheus: + image: prom/prometheus + hostname: prometheus + restart: always + ports: + - "9090:9090" + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + profiles: + - with_prometheus + grafana: image: "grafana/${GRAFANA_IMAGE:-grafana:latest}" restart: always @@ -94,5 +108,6 @@ services: volumes: grafana_data: + prometheus_data: oncall_data: redis_data: diff --git a/docs/sources/oncall-api-reference/_index.md b/docs/sources/oncall-api-reference/_index.md index e289f07b..b4f8987b 100644 --- a/docs/sources/oncall-api-reference/_index.md +++ b/docs/sources/oncall-api-reference/_index.md @@ -53,7 +53,7 @@ Rate limited response HTTP status: 429 | Scope | Amount | Time Frame | | ---------------------------- | :----: | :--------: | | Alerts from each integration | 300 | 5 minutes | -| Alerts from the whole team | 500 | 5 minutes | +| Alerts from the whole organization | 500 | 5 minutes | ## API rate limits diff --git a/docs/sources/oncall-api-reference/alertgroups.md b/docs/sources/oncall-api-reference/alertgroups.md index f0d68157..9dfe58e4 100644 --- a/docs/sources/oncall-api-reference/alertgroups.md +++ b/docs/sources/oncall-api-reference/alertgroups.md @@ -63,14 +63,10 @@ curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/" \ }' ``` - - | Parameter | Required | Description | | --------- | :------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mode` | No | Default setting is `wipe`. `wipe` will remove the payload of all Grafana OnCall group alerts. This is useful if you sent sensitive data to OnCall. All metadata will remain. `DELETE` will trigger the removal of alert groups, alerts, and all related metadata. It will also remove alert group notifications in Slack and other destinations. | - - > **NOTE:** `DELETE` can take a few moments to delete alert groups because Grafana OnCall interacts with 3rd party APIs > such as Slack. Please check objects using `GET` to be sure the data is removed. diff --git a/docs/sources/oncall-api-reference/escalation_policies.md b/docs/sources/oncall-api-reference/escalation_policies.md index 1c538759..02c740a8 100644 --- a/docs/sources/oncall-api-reference/escalation_policies.md +++ b/docs/sources/oncall-api-reference/escalation_policies.md @@ -30,8 +30,6 @@ The above command returns JSON structured in the following way: } ``` - - | Parameter | Required | Description | | ---------------------------------- | :--------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `escalation_chain_id` | Yes | Each escalation policy is assigned to a specific escalation chain. | @@ -47,8 +45,6 @@ The above command returns JSON structured in the following way: | `notify_if_time_from` | If type = `notify_if_time_from_to` | UTC time represents the beginning of the time period, for example `09:00:00Z`. | | `notify_if_time_to` | If type = `notify_if_time_from_to` | UTC time represents the end of the time period, for example `18:00:00Z`. | - - **HTTP request** `POST {{API_URL}}/api/v1/escalation_policies/` diff --git a/docs/sources/oncall-api-reference/on_call_shifts.md b/docs/sources/oncall-api-reference/on_call_shifts.md index 74917bf0..98eeab49 100644 --- a/docs/sources/oncall-api-reference/on_call_shifts.md +++ b/docs/sources/oncall-api-reference/on_call_shifts.md @@ -41,8 +41,6 @@ The above command returns JSON structured in the following way: } ``` - - | Parameter | Unique | Required | Description | | -------------------------------- | :----: | :--------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Yes | Yes | On-call shift name. | @@ -53,7 +51,7 @@ The above command returns JSON structured in the following way: | `start` | No | Yes | Start time of the on-call shift. This parameter takes a date format as `yyyy-MM-dd'T'HH:mm:ss` (for example "2020-09-05T08:00:00"). | | `duration` | No | Yes | Duration of the event. | | `frequency` | No | If type = `recurrent_event` or `rolling_users` | One of: `hourly`, `daily`, `weekly`, `monthly`. | -| `interval` | No | Optional | This parameter takes a positive integer that represents the intervals that the recurrence rule repeats. If `frequency` is set, the default assumed value for this will be `1`. | +| `interval` | No | Optional | This parameter takes a positive integer that represents the intervals that the recurrence rule repeats. If `frequency` is set, the default assumed value for this will be `1`. | | `until` | No | Optional | When the recurrence rule ends (endless if None). This parameter takes a date format as `yyyy-MM-dd'T'HH:mm:ss` (for example "2020-09-05T08:00:00"). | | `week_start` | No | Optional | Start day of the week in iCal format. One of: `SU` (Sunday), `MO` (Monday), `TU` (Tuesday), `WE` (Wednesday), `TH` (Thursday), `FR` (Friday), `SA` (Saturday). Default: `SU`. | | `by_day` | No | Optional | List of days in iCal format. Valid values are: `SU`, `MO`, `TU`, `WE`, `TH`, `FR`, `SA`. | @@ -63,8 +61,6 @@ The above command returns JSON structured in the following way: | `rolling_users` | No | Optional | List of lists with on-call users (for `rolling_users` event type). Grafana OnCall will iterate over lists of users for every time frame specified in `frequency`. For example: there are two lists of users in `rolling_users` : [[Alex, Bob], [Alice]] and `frequency` = `daily` . This means that the first day Alex and Bob will be notified. The next day: Alice. The day after: Alex and Bob again and so on. | | `start_rotation_from_user_index` | No | Optional | Index of the list of users in `rolling_users`, from which on-call rotation starts. By default, the start index is `0` | - - Please see [RFC 5545](https://tools.ietf.org/html/rfc5545#section-3.3.10) for more information about recurrence rules. **HTTP request** diff --git a/docs/sources/oncall-api-reference/personal_notification_rules.md b/docs/sources/oncall-api-reference/personal_notification_rules.md index 01fe45d0..853cc47c 100644 --- a/docs/sources/oncall-api-reference/personal_notification_rules.md +++ b/docs/sources/oncall-api-reference/personal_notification_rules.md @@ -29,8 +29,6 @@ The above command returns JSON structured in the following way: } ``` - - | Parameter | Required | Description | | ----------- | :------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `user_id` | Yes | User ID | @@ -39,8 +37,6 @@ The above command returns JSON structured in the following way: | `duration` | Optional | A time in secs when type `wait` is chosen for `type`. | | `important` | Optional | Boolean value indicates if a rule is "important". Default is `false`. | - - **HTTP request** `POST {{API_URL}}/api/v1/personal_notification_rules/` diff --git a/docs/sources/oncall-api-reference/routes.md b/docs/sources/oncall-api-reference/routes.md index 4d8987a2..45c39ae6 100644 --- a/docs/sources/oncall-api-reference/routes.md +++ b/docs/sources/oncall-api-reference/routes.md @@ -44,10 +44,8 @@ Routes allow you to direct different alerts to different messenger channels and - Alerts for different engineering groups - Snoozing spam & debugging alerts - - | Parameter | Unique | Required | Description | -|-----------------------| :----: |:--------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| --------------------- | :----: | :------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `integration_id` | No | Yes | Each route is assigned to a specific integration. | | `escalation_chain_id` | No | Yes | Each route is assigned a specific escalation chain. Explicitly pass `null` to create a route without an escalation chain assigned. | | `routing_type` | Yes | No | Routing type that can be either `jinja2` or `regex`(default value) | @@ -55,8 +53,6 @@ Routes allow you to direct different alerts to different messenger channels and | `position` | Yes | Optional | Route matching is performed one after another starting from position=`0`. Position=`-1` will put the route to the end of the list before `is_the_last_route`. A new route created with a position of an existing route will move the old route (and all following routes) down in the list. | | `slack` | Yes | Optional | Dictionary with Slack-specific settings for a route. | - - **HTTP request** `POST {{API_URL}}/api/v1/routes/` diff --git a/docs/sources/oncall-api-reference/schedules.md b/docs/sources/oncall-api-reference/schedules.md index 9f9410a9..9076985c 100644 --- a/docs/sources/oncall-api-reference/schedules.md +++ b/docs/sources/oncall-api-reference/schedules.md @@ -39,8 +39,6 @@ The above command returns JSON structured in the following way: } ``` - - | Parameter | Unique | Required | Description | | -------------------- | :----: | :--------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Yes | Yes | Schedule name. | @@ -52,8 +50,6 @@ The above command returns JSON structured in the following way: | `slack` | No | Optional | Dictionary with Slack-specific settings for a schedule. Includes `channel_id` and `user_group_id` fields, that take a channel ID and a user group ID from Slack. | | `shifts` | No | Optional | List of shifts. Used for manually added on-call shifts in Schedules with type `calendar`. | - - **HTTP request** `POST {{API_URL}}/api/v1/schedules/` diff --git a/docs/sources/oncall-api-reference/user_groups.md b/docs/sources/oncall-api-reference/user_groups.md index b7f6e328..e8f6a806 100644 --- a/docs/sources/oncall-api-reference/user_groups.md +++ b/docs/sources/oncall-api-reference/user_groups.md @@ -36,16 +36,12 @@ The above command returns JSON structured in the following way: } ``` - - | Parameter | Unique | Description | | --------- | :----: | :---------------------------------------------------------------------------------------------------- | | `id` | Yes | User Group ID | | `type` | No | [Slack-defined user groups](https://slack.com/intl/en-ru/help/articles/212906697-Create-a-user-group) | | `slack` | No | Metadata retrieved from Slack. | - - **HTTP request** `GET {{API_URL}}/api/v1/user_groups/` diff --git a/engine/apps/api/serializers/alert_receive_channel.py b/engine/apps/api/serializers/alert_receive_channel.py index 3fdb8c03..ad86fdb4 100644 --- a/engine/apps/api/serializers/alert_receive_channel.py +++ b/engine/apps/api/serializers/alert_receive_channel.py @@ -49,7 +49,7 @@ class AlertReceiveChannelSerializer(EagerLoadingMixin, serializers.ModelSerializ heartbeat = serializers.SerializerMethodField() allow_delete = serializers.SerializerMethodField() description_short = serializers.CharField(max_length=250, required=False, allow_null=True) - demo_alert_payload = serializers.CharField(source="config.example_payload", read_only=True) + demo_alert_payload = serializers.JSONField(source="config.example_payload", read_only=True) routes_count = serializers.SerializerMethodField() connected_escalations_chains_count = serializers.SerializerMethodField() diff --git a/engine/apps/api/tests/test_alert_group.py b/engine/apps/api/tests/test_alert_group.py index ea815f28..736ff69c 100644 --- a/engine/apps/api/tests/test_alert_group.py +++ b/engine/apps/api/tests/test_alert_group.py @@ -43,6 +43,33 @@ def alert_group_internal_api_setup( return user, token, alert_groups +@pytest.mark.django_db +def test_get_filter_by_integration( + alert_group_internal_api_setup, make_alert_receive_channel, make_alert_group, make_user_auth_headers +): + user, token, alert_groups = alert_group_internal_api_setup + + ag = alert_groups[0] + # channel filter could be None, but the alert group still belongs to the original integration + ag.channel_filter = None + ag.save() + + # make an alert group in other integration + alert_receive_channel = make_alert_receive_channel(user.organization) + make_alert_group(alert_receive_channel) + + client = APIClient() + url = reverse("api-internal:alertgroup-list") + response = client.get( + url + f"?integration={ag.channel.public_primary_key}", + format="json", + **make_user_auth_headers(user, token), + ) + + assert response.status_code == status.HTTP_200_OK + assert len(response.data["results"]) == 4 + + @pytest.mark.django_db def test_get_filter_started_at(alert_group_internal_api_setup, make_user_auth_headers): user, token, _ = alert_group_internal_api_setup diff --git a/engine/apps/api/views/alert_group.py b/engine/apps/api/views/alert_group.py index bcae5306..1e9f4bef 100644 --- a/engine/apps/api/views/alert_group.py +++ b/engine/apps/api/views/alert_group.py @@ -104,7 +104,7 @@ class AlertGroupFilter(DateRangeFilterMixin, ByTeamModelFieldFilterMixin, ModelF method=ModelFieldFilterMixin.filter_model_field.__name__, ) integration = filters.ModelMultipleChoiceFilter( - field_name="channel_filter__alert_receive_channel", + field_name="channel", queryset=None, to_field_name="public_primary_key", method=ModelFieldFilterMixin.filter_model_field.__name__, diff --git a/engine/apps/metrics_exporter/tests/test_views.py b/engine/apps/metrics_exporter/tests/test_views.py new file mode 100644 index 00000000..3a2381c9 --- /dev/null +++ b/engine/apps/metrics_exporter/tests/test_views.py @@ -0,0 +1,24 @@ +import pytest +from django.urls import reverse +from rest_framework.test import APIClient + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "token,auth,expected", + [ + (None, None, 200), + ("secret", "invalid", 401), + ("secret", "secret", 200), + ], +) +def test_metrics_exporter_auth(settings, token, auth, expected): + settings.PROMETHEUS_EXPORTER_SECRET = token + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="Bearer {}".format(auth)) + + url = reverse("metrics-exporter") + response = client.get(url) + + assert response.status_code == expected diff --git a/engine/apps/metrics_exporter/urls.py b/engine/apps/metrics_exporter/urls.py index 4768cd79..3b22480e 100644 --- a/engine/apps/metrics_exporter/urls.py +++ b/engine/apps/metrics_exporter/urls.py @@ -3,5 +3,5 @@ from django.urls import path from .views import MetricsExporterView urlpatterns = [ - path("", MetricsExporterView.as_view()), + path("", MetricsExporterView.as_view(), name="metrics-exporter"), ] diff --git a/engine/apps/metrics_exporter/views.py b/engine/apps/metrics_exporter/views.py index 7a03de78..2028700a 100644 --- a/engine/apps/metrics_exporter/views.py +++ b/engine/apps/metrics_exporter/views.py @@ -1,11 +1,23 @@ +import re + +from django.conf import settings from django.http import HttpResponse from prometheus_client import generate_latest from rest_framework.views import APIView from .metrics_collectors import application_metrics_registry +RE_AUTH_TOKEN = re.compile(r"^[Bb]earer\s{1}(.+)$") + class MetricsExporterView(APIView): def get(self, request): + if settings.PROMETHEUS_EXPORTER_SECRET: + authorization = request.headers.get("Authorization", "") + match = RE_AUTH_TOKEN.match(authorization) + token = match.groups()[0] if match else None + if not token or token != settings.PROMETHEUS_EXPORTER_SECRET: + return HttpResponse(status=401) + result = generate_latest(application_metrics_registry).decode("utf-8") return HttpResponse(result, content_type="text/plain; version=0.0.4; charset=utf-8") diff --git a/engine/apps/phone_notifications/models/phone_call.py b/engine/apps/phone_notifications/models/phone_call.py index b4a9182b..cbad80f9 100644 --- a/engine/apps/phone_notifications/models/phone_call.py +++ b/engine/apps/phone_notifications/models/phone_call.py @@ -60,6 +60,7 @@ class PhoneCallRecord(models.Model): class ProviderPhoneCall(models.Model): """ ProviderPhoneCall is an interface between PhoneCallRecord and call data returned from PhoneProvider. + Concrete provider phone call should be inherited from ProviderPhoneCall. Some phone providers allows to track status of call or gather pressed digits (we use it to ack/resolve alert group). It is needed to link phone call and alert group without exposing internals of concrete phone provider to PhoneBackend. diff --git a/engine/apps/phone_notifications/models/sms.py b/engine/apps/phone_notifications/models/sms.py index 4bad9eb4..9763d706 100644 --- a/engine/apps/phone_notifications/models/sms.py +++ b/engine/apps/phone_notifications/models/sms.py @@ -67,8 +67,9 @@ class SMSRecord(models.Model): class ProviderSMS(models.Model): """ ProviderSMS is an interface between SMSRecord and call data returned from PhoneProvider. + Concrete provider sms be inherited from ProviderSMS. - The idea is same as for ProviderCall - to save provider specific data without exposing them to ProheBackend. + The idea is same as for ProviderCall - to save provider specific data without exposing them to PhoneBackend. """ class Meta: diff --git a/engine/apps/phone_notifications/phone_provider.py b/engine/apps/phone_notifications/phone_provider.py index 68f47721..ad72924a 100644 --- a/engine/apps/phone_notifications/phone_provider.py +++ b/engine/apps/phone_notifications/phone_provider.py @@ -15,9 +15,16 @@ class ProviderFlags: """ ProviderFlags is set of feature flags enabled for concrete provider. It is needed to show correct buttons in UI. + + Attributes: + configured: Indicates if provider LiveSettings are valid. If LiveSettings cannot be validated, return True. + test_sms: Indicates if provider allows to send test_sms + test_call: Indicates if provider allows to make test_call + verification_call: Indicates if provider allows to validate number via call + verification_sms: Indicates if provider allows to validate number via sms """ - configured: bool # indicates if provider live settings are present and valid + configured: bool test_sms: bool test_call: bool verification_call: bool @@ -29,7 +36,10 @@ class PhoneProvider(ABC): PhoneProvider is an interface to all phone providers. It is needed to hide details of external phone providers from core code. - New PhoneProviders should be added to settings.PHONE_PROVIDERS dict. + To implement custom phone provider: + 1. Implement your ConcretePhoneProvider inherited from PhoneProvider. + 2. Add needed env variables to django settings and to LiveSettings. + 3. Add your PhoneProvider to settings.PHONE_PROVIDERS dict. For reference, you can check: SimplePhoneProvider as example of tiny, but working provider. diff --git a/engine/apps/phone_notifications/simple_phone_provider.py b/engine/apps/phone_notifications/simple_phone_provider.py index f6d0df03..0333e2d8 100644 --- a/engine/apps/phone_notifications/simple_phone_provider.py +++ b/engine/apps/phone_notifications/simple_phone_provider.py @@ -1,9 +1,13 @@ +import logging from random import randint from django.core.cache import cache +from .exceptions import FailedToSendSMS, FailedToStartVerification from .phone_provider import PhoneProvider, ProviderFlags +logger = logging.getLogger(__name__) + class SimplePhoneProvider(PhoneProvider): """ @@ -15,12 +19,22 @@ class SimplePhoneProvider(PhoneProvider): self.send_sms(number, message) def send_sms(self, number, text): - print(f'SimplePhoneProvider.send_sms: send message "{text}" to {number}') + try: + self._write_to_stdout(number, text) + except Exception as e: + # example of handling provider exceptions and converting them to exceptions from core OnCall code. + logger.error(f"SimplePhoneProvider.send_sms: failed {e}") + raise FailedToSendSMS def send_verification_sms(self, number): code = str(randint(100000, 999999)) cache.set(self._cache_key(number), code, timeout=10 * 60) - self.send_sms(number, f"Your verification code is {code}") + try: + self._write_to_stdout(number, f"Your verification code is {code}") + except Exception as e: + # Example of handling provider exceptions and converting them to exceptions from core OnCall code. + logger.error(f"SimplePhoneProvider.send_verification_sms: failed {e}") + raise FailedToStartVerification def finish_verification(self, number, code): has = cache.get(self._cache_key(number)) @@ -32,6 +46,11 @@ class SimplePhoneProvider(PhoneProvider): def _cache_key(self, number): return f"simple_provider_{number}" + def _write_to_stdout(self, number, text): + # print is just example of sending sms. + # In real-life provider it will be some external api call. + print(f'send message "{text}" to {number}') + @property def flags(self) -> ProviderFlags: return ProviderFlags( diff --git a/engine/settings/base.py b/engine/settings/base.py index 25056d8d..aa2adca4 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -92,6 +92,9 @@ ONCALL_GATEWAY_URL = os.environ.get("ONCALL_GATEWAY_URL") ONCALL_GATEWAY_API_TOKEN = os.environ.get("ONCALL_GATEWAY_API_TOKEN") ONCALL_BACKEND_REGION = os.environ.get("ONCALL_BACKEND_REGION") +# Prometheus exporter metrics endpoint auth +PROMETHEUS_EXPORTER_SECRET = os.environ.get("PROMETHEUS_EXPORTER_SECRET") + # Database class DatabaseTypes: diff --git a/engine/settings/ci-test.py b/engine/settings/ci-test.py index 9751c49d..fa03e76c 100644 --- a/engine/settings/ci-test.py +++ b/engine/settings/ci-test.py @@ -40,3 +40,5 @@ TWILIO_ACCOUNT_SID = "dummy_twilio_account_sid" TWILIO_AUTH_TOKEN = "dummy_twilio_auth_token" EXTRA_MESSAGING_BACKENDS = [("apps.base.tests.messaging_backend.TestOnlyBackend", 42)] + +FEATURE_PROMETHEUS_EXPORTER_ENABLED = True diff --git a/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.module.css b/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.module.css deleted file mode 100644 index 0a753b7d..00000000 --- a/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.module.css +++ /dev/null @@ -1,19 +0,0 @@ -.hamburger-menu { - cursor: pointer; - color: var(--primary-text-color); -} - -.hamburger-menu-withBackground { - display: inline-flex; - flex-direction: column; - align-items: center; - vertical-align: middle; - justify-content: center; - background-color: rgba(204, 204, 220, 0.16); - border: 1px solid transparent; - height: 32px; - width: 30px; - padding: 4px; - cursor: pointer; - color: var(--primary-text-color); -} diff --git a/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.module.scss b/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.module.scss new file mode 100644 index 00000000..2331838b --- /dev/null +++ b/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.module.scss @@ -0,0 +1,35 @@ +.hamburgerMenu { + cursor: pointer; + color: var(--primary-text-color); + + &--withBackground { + display: inline-flex; + flex-direction: column; + align-items: center; + vertical-align: middle; + justify-content: center; + background-color: rgba(204, 204, 220, 0.16); + border: 1px solid transparent; + height: 32px; + width: 30px; + padding: 4px; + cursor: pointer; + color: var(--primary-text-color); + } + + &--small { + display: inline-flex; + flex-direction: column; + align-items: center; + vertical-align: middle; + justify-content: center; + background-color: rgba(204, 204, 220, 0.16); + color: var(--secondary-background); + border: 1px solid transparent; + height: 24px; + width: 22px; + padding: 4px; + cursor: pointer; + color: var(--primary-text-color); + } +} diff --git a/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.tsx b/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.tsx index 7b7c1b48..02937ad6 100644 --- a/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.tsx +++ b/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.tsx @@ -3,12 +3,13 @@ import React, { useRef } from 'react'; import { Icon } from '@grafana/ui'; import cn from 'classnames/bind'; -import styles from './HamburgerMenu.module.css'; +import styles from './HamburgerMenu.module.scss'; interface HamburgerMenuProps { openMenu: React.MouseEventHandler; listWidth: number; listBorder: number; + stopPropagation?: boolean; withBackground?: boolean; className?: string; } @@ -17,12 +18,16 @@ const cx = cn.bind(styles); const HamburgerMenu: React.FC = (props) => { const ref = useRef(); - const { openMenu, listBorder, listWidth, withBackground, className } = props; + const { openMenu, listBorder, listWidth, withBackground, className, stopPropagation = false } = props; return (
{ + className={withBackground ? cx('hamburgerMenu--withBackground') : cx('hamburgerMenu', className)} + onClick={(e) => { + if (stopPropagation) { + e.stopPropagation(); + } + const boundingRect = ref.current.getBoundingClientRect(); openMenu({ diff --git a/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.module.css b/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.module.css index 988421d5..273537df 100644 --- a/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.module.css +++ b/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.module.css @@ -2,6 +2,10 @@ width: 100%; } +.regexp-template-code-error { + border: var(--error-text-color) 1px solid; +} + .regexp-template-editor-modal { width: 700px; } diff --git a/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.tsx b/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.tsx index c7f2390f..7ad74676 100644 --- a/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.tsx +++ b/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.tsx @@ -12,6 +12,7 @@ import Text from 'components/Text/Text'; import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { ChannelFilter } from 'models/channel_filter/channel_filter.types'; import { useStore } from 'state/useStore'; +import { openErrorNotification } from 'utils'; import styles from './EditRegexpRouteTemplateModal.module.css'; @@ -33,6 +34,7 @@ const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemplateMod const regexpBody = store.alertReceiveChannelStore.channelFilters[channelFilterId]?.filtering_term; const [regexpTemplateBody, setRegexpTemplateBody] = useState(regexpBody); + const [showErrorTemplate, setShowErrorTemplate] = useState(false); const templateJinja2Body = store.alertReceiveChannelStore.channelFilters[channelFilterId]?.filtering_term_as_jinja2; @@ -40,14 +42,20 @@ const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemplateMod const handleRegexpBodyChange = () => { return debounce((value: string) => { + setShowErrorTemplate(false); setRegexpTemplateBody(value); }, 1000); }; const handleSave = useCallback(() => { - onUpdateRoute({ ['route_template']: regexpTemplateBody }, channelFilterId, 0); + if (!regexpTemplateBody) { + setShowErrorTemplate(true); + openErrorNotification('Route template body can not be empty'); + } else { + onUpdateRoute({ ['route_template']: regexpTemplateBody }, channelFilterId, 0); - onHide(); + onHide(); + } }, [regexpTemplateBody]); const handleConvertToJinja2 = useCallback(() => { @@ -87,7 +95,7 @@ const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemplateMod -
+
(undefined); - const [telegramInfo, setTelegramInfo] = useState>([]); useEffect(() => { - (async function () { - const telegram = await telegramChannelStore.getAll(); - setTelegramInfo(telegram); - })(); + telegramChannelStore.updateItems(); }, [channelFilterId]); const channelFilter = alertReceiveChannelStore.channelFilters[channelFilterId]; @@ -70,9 +66,7 @@ const CollapsedIntegrationRouteDisplay: React.FC - {routeWording === 'Default' && ( - All unrouted routes will be served to the default route - )} + {routeWording === 'Default' && Unmatched alerts routed to default route} {routeWording !== 'Default' && channelFilter.filtering_term && ( {channelFilter.filtering_term} @@ -93,7 +87,7 @@ const CollapsedIntegrationRouteDisplay: React.FC - {IntegrationHelper.getChatOpsChannels(channelFilter, telegramInfo, store) + {IntegrationHelper.getChatOpsChannels(channelFilter, store) .filter((it) => it) .map((chatOpsChannel, key) => ( diff --git a/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.module.scss b/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.module.scss index 72c5715b..e28cf7d1 100644 --- a/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.module.scss +++ b/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.module.scss @@ -32,19 +32,3 @@ background: var(--gray-9); } } - -.hamburgerMenu-small { - display: inline-flex; - flex-direction: column; - align-items: center; - vertical-align: middle; - justify-content: center; - background-color: rgba(204, 204, 220, 0.16); - color: var(--secondary-background); - border: 1px solid transparent; - height: 24px; - width: 22px; - padding: 4px; - cursor: pointer; - color: var(--primary-text-color); -} diff --git a/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx b/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx index d57242dc..2569d2f0 100644 --- a/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx +++ b/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx @@ -41,6 +41,9 @@ import { UserActions } from 'utils/authorization'; const cx = cn.bind(styles); +const ACTIONS_LIST_WIDTH = 200; +const ACTIONS_LIST_BORDER = 2; + interface ExpandedIntegrationRouteDisplayProps { alertReceiveChannelId: AlertReceiveChannel['id']; channelFilterId: ChannelFilter['id']; @@ -83,7 +86,7 @@ const ExpandedIntegrationRouteDisplay: React.FC { setIsLoading(true); - Promise.all([escalationChainStore.updateItems(), telegramChannelStore.updateTelegramChannels()]).then(() => + Promise.all([escalationChainStore.updateItems(), telegramChannelStore.updateItems()]).then(() => setIsLoading(false) ); }, []); @@ -168,12 +171,14 @@ const ExpandedIntegrationRouteDisplay: React.FC )} - - - Publish to ChatOps - - - + {IntegrationHelper.hasChatopsInstalled(store) && ( + + + Publish to ChatOps + + + + )} @@ -363,15 +368,20 @@ export const RouteButtonsDisplay: React.FC = ({ )} > {({ openMenu }) => ( - + )} )} ); - function onDelete(e: React.SyntheticEvent) { - e.stopPropagation(); + function onDelete() { setRouteIdForDeletion(); } diff --git a/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.module.css b/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.module.scss similarity index 77% rename from grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.module.css rename to grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.module.scss index e7a7639c..6fdee1ab 100644 --- a/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.module.css +++ b/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.module.scss @@ -32,23 +32,32 @@ min-width: min-content; } +.template-block-list, +.template-block-codeeditor { + overflow-y: hidden; +} + +.template-block-list, +.template-block-codeeditor, +.template-block-result, +.result { + height: 100%; + max-height: 100%; +} + .template-block-list { width: 30%; - height: 100%; } - .template-block-codeeditor { width: 40%; - height: 100%; } - .template-block-result { width: 30%; - height: 100%; + overflow-y: scroll !important; } - .result { padding-left: 16px; + padding-bottom: 60px; } .template-block-codeeditor div[aria-label='Code editor container'] { diff --git a/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.tsx b/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.tsx index 75dc6260..dbdd67d9 100644 --- a/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.tsx +++ b/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.tsx @@ -21,10 +21,11 @@ import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_ import { AlertTemplatesDTO } from 'models/alert_templates'; import { Alert } from 'models/alertgroup/alertgroup.types'; import { ChannelFilter } from 'models/channel_filter/channel_filter.types'; +import { openErrorNotification } from 'utils'; import { waitForElement } from 'utils/DOM'; import LocationHelper from 'utils/LocationHelper'; -import styles from './IntegrationTemplate.module.css'; +import styles from './IntegrationTemplate.module.scss'; const cx = cn.bind(styles); @@ -117,11 +118,17 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => { ); const handleSubmit = useCallback(() => { - template.isRoute - ? onUpdateRoute({ [template.name]: changedTemplateBody }, channelFilterId) - : onUpdateTemplates({ [template.name]: changedTemplateBody }); - - onHide(); + if (template.isRoute) { + if (changedTemplateBody) { + onUpdateRoute({ [template.name]: changedTemplateBody }, channelFilterId); + onHide(); + } else { + openErrorNotification('Route template body can not be empty'); + } + } else { + onUpdateTemplates({ [template.name]: changedTemplateBody }); + onHide(); + } }, [onUpdateTemplates, changedTemplateBody]); const getCheatSheet = (templateName) => { diff --git a/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.helpers.ts b/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.helpers.ts index ca03ff2f..9cc38b07 100644 --- a/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.helpers.ts +++ b/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.helpers.ts @@ -10,12 +10,18 @@ const normalize = (value: any) => { return value; }; -export function parseFilters(query: { [key: string]: any }, filterOptions: FilterOption[]) { - const filters = filterOptions.filter((filterOption: FilterOption) => filterOption.name in query); +export function parseFilters( + data: { [key: string]: any }, + filterOptions: FilterOption[], + query: { [key: string]: any } +) { + const filters = filterOptions.filter((filterOption: FilterOption) => filterOption.name in data); const values = filters.reduce((memo: any, filterOption: FilterOption) => { - const rawValue = query[filterOption.name]; + const rawValue = query[filterOption.name] || data[filterOption.name]; // query takes priority over local storage + let value: any = rawValue; + if (filterOption.type === 'options' || filterOption.type === 'team_select') { if (!Array.isArray(rawValue)) { value = [rawValue]; diff --git a/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.tsx b/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.tsx index 189a4073..6f8b92c6 100644 --- a/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.tsx +++ b/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.tsx @@ -69,17 +69,10 @@ class RemoteFilters extends Component { const filterOptions = await filtersStore.updateOptionsForPage(page); - let { filters, values } = parseFilters({ ...query, ...filtersStore.globalValues }, filterOptions); + let { filters, values } = parseFilters({ ...query, ...filtersStore.globalValues }, filterOptions, query); if (isEmpty(values)) { - let newQuery = defaultFilters || { team: [] }; - /* if (filtersStore.values[page]) { - newQuery = { ...filtersStore.values[page] }; - } else { - newQuery = defaultFilters || { team: [] }; - } */ - - ({ filters, values } = parseFilters(newQuery, filterOptions)); + ({ filters, values } = parseFilters(defaultFilters || { team: [] }, filterOptions, query)); } this.setState({ filterOptions, filters, values }, () => this.onChange(true)); @@ -369,17 +362,20 @@ class RemoteFilters extends Component { store.filtersStore.updateValuesForPage(page, values); - Object.keys({ ...store.filtersStore.globalValues }).forEach((key) => { - if (!(key in values)) { - delete store.filtersStore.globalValues[key]; - } - }); + if (!isOnMount) { + // Skip updating local storage for mounting, this way URL won't overwrite local storage but subsequent actions WILL do + Object.keys({ ...store.filtersStore.globalValues }).forEach((key) => { + if (!(key in values)) { + delete store.filtersStore.globalValues[key]; + } + }); - const newGlobalValues = pickBy(values, (_, key) => - filterOptions.some((option) => option.name === key && option.global) - ); + const newGlobalValues = pickBy(values, (_, key) => + filterOptions.some((option) => option.name === key && option.global) + ); - store.filtersStore.globalValues = newGlobalValues; + store.filtersStore.globalValues = newGlobalValues; + } LocationHelper.update({ ...values }, 'partial'); onChange(values, isOnMount); diff --git a/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.module.css b/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.module.css index e66b1ba4..237640a8 100644 --- a/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.module.css +++ b/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.module.css @@ -7,6 +7,8 @@ .template-block-list { width: 30%; height: 100%; + max-width: 100%; + overflow-y: hidden; } .alert-group-payload-view { @@ -37,7 +39,8 @@ } .alert-groups-editor { - width: 100%; + width: calc(100% + 16px); + margin-left: -16px; } .alert-groups-editor div[aria-label='Code editor container'] { @@ -45,6 +48,13 @@ border-right: none; } +.alert-groups-editor-withBadge div[aria-label='Code editor container'] { + background-color: rgba(10, 10, 10, 0.4); + border-bottom: none; + border-right: none; + padding-top: 42px; +} + .no-alert-groups-badge { display: flex; padding: 8px; @@ -54,3 +64,26 @@ .no-alert-groups-badge > div { margin-right: 8px; } + +.alert-groups-last-payload-badge { + position: fixed; + z-index: 1; + margin: 16px; +} + +.selected-alert-name { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.selected-alert-name-container { + display: flex; + align-items: center; + justify-content: space-between; +} + +.title-action-icons { + display: flex; + align-items: center; +} diff --git a/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx b/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx index b58d1988..3b96fc9a 100644 --- a/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx +++ b/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx @@ -6,6 +6,7 @@ import { debounce } from 'lodash-es'; import MonacoEditor, { MONACO_LANGUAGE } from 'components/MonacoEditor/MonacoEditor'; import Text from 'components/Text/Text'; +import TooltipBadge from 'components/TooltipBadge/TooltipBadge'; import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { AlertTemplatesDTO } from 'models/alert_templates'; import { Alert } from 'models/alertgroup/alertgroup.types'; @@ -15,6 +16,8 @@ import { useStore } from 'state/useStore'; import styles from './TemplatesAlertGroupsList.module.css'; const cx = cn.bind(styles); +const HEADER_OF_CONTAINER_HEIGHT = 59; +const BADGE_WITH_PADDINGS_HEIGHT = 42; interface TemplatesAlertGroupsListProps { templates: AlertTemplatesDTO[]; @@ -38,8 +41,14 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => { }, []); const getCodeEditorHeight = () => { - const mainDiv = document.getElementById('content-container-id'); - const height = mainDiv?.getBoundingClientRect().height - 59; + const mainDiv = document.getElementById('alerts-content-container-id'); + const height = mainDiv?.getBoundingClientRect().height - HEADER_OF_CONTAINER_HEIGHT; + return `${height}px`; + }; + + const getCodeEditorHeightWithBadge = () => { + const mainDiv = document.getElementById('alerts-content-container-id'); + const height = mainDiv?.getBoundingClientRect().height - HEADER_OF_CONTAINER_HEIGHT - BADGE_WITH_PADDINGS_HEIGHT; return `${height}px`; }; @@ -71,14 +80,14 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => { }; return ( -
+
{selectedAlertPayload ? ( <> {isEditMode ? ( <>
- Edit {selectedAlertName} + Edit custom payload returnToListView()} /> @@ -101,24 +110,31 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => { ) : ( <>
- - {selectedAlertName} - - +
+
+ {selectedAlertName} +
+
setIsEditMode(true)} /> returnToListView()} /> - - +
+
-
-
+
+ +
this.items?.[alertReceiveChannelId] ); - - // return { - // count: this.searchResult.count, - // results: - // this.searchResult.results && - // this.searchResult.results.map( - // (alertReceiveChannelId: AlertReceiveChannel['id']) => this.items?.[alertReceiveChannelId] - // ), - // }; } getPaginatedSearchResult(_query = '') { @@ -159,8 +150,7 @@ export class AlertReceiveChannelStore extends BaseStore { async updatePaginatedItems(query: any = '', page = 1) { const filters = typeof query === 'string' ? { search: query } : query; - const { search } = filters; - const { count, results } = await makeRequest(this.path, { params: { search, page } }); + const { count, results } = await makeRequest(this.path, { params: { ...filters, page } }); this.items = { ...this.items, diff --git a/grafana-plugin/src/pages/integration_2/Integration2.helper.ts b/grafana-plugin/src/pages/integration_2/Integration2.helper.ts index ef7c6e53..4fff8ea5 100644 --- a/grafana-plugin/src/pages/integration_2/Integration2.helper.ts +++ b/grafana-plugin/src/pages/integration_2/Integration2.helper.ts @@ -72,23 +72,30 @@ const IntegrationHelper = { return totalDiffString; }, - getChatOpsChannels( - channelFilter: ChannelFilter, - telegramInfo: Array<{ id: string; channel_name: string }>, - store: RootStore - ): Array<{ name: string; icon: IconName }> { - const channels: Array<{ name: string; icon: IconName }> = []; + hasChatopsInstalled(store: RootStore) { + const hasSlack = Boolean(store.teamStore.currentTeam?.slack_team_identity); + const hasTelegram = + store.hasFeature(AppFeature.Telegram) && store.telegramChannelStore.currentTeamToTelegramChannel?.length > 0; + return hasSlack || hasTelegram; + }, - if ( - store.hasFeature(AppFeature.Slack) && - channelFilter.notify_in_slack && - channelFilter.notify_in_slack && - channelFilter.slack_channel?.display_name - ) { - channels.push({ name: channelFilter.slack_channel.display_name, icon: 'slack' }); + getChatOpsChannels(channelFilter: ChannelFilter, store: RootStore): Array<{ name: string; icon: IconName }> { + const channels: Array<{ name: string; icon: IconName }> = []; + const telegram = Object.keys(store.telegramChannelStore.items).map((k) => store.telegramChannelStore.items[k]); + + if (store.hasFeature(AppFeature.Slack) && channelFilter.notify_in_slack) { + const matchingSlackChannel = store.teamStore.currentTeam?.slack_channel?.id + ? store.slackChannelStore.items[store.teamStore.currentTeam.slack_channel?.id] + : undefined; + if (channelFilter.slack_channel?.display_name || matchingSlackChannel?.display_name) { + channels.push({ + name: channelFilter.slack_channel?.display_name || matchingSlackChannel?.display_name, + icon: 'slack', + }); + } } - const matchingTelegram = telegramInfo?.find((t) => t.id === channelFilter.telegram_channel); + const matchingTelegram = telegram.find((t) => t.id === channelFilter.telegram_channel); if ( store.hasFeature(AppFeature.Telegram) && diff --git a/grafana-plugin/src/pages/integration_2/Integration2.tsx b/grafana-plugin/src/pages/integration_2/Integration2.tsx index 1f52a26d..6ce84e0f 100644 --- a/grafana-plugin/src/pages/integration_2/Integration2.tsx +++ b/grafana-plugin/src/pages/integration_2/Integration2.tsx @@ -66,7 +66,7 @@ import { openNotification, openErrorNotification } from 'utils'; import { getVar } from 'utils/DOM'; import LocationHelper from 'utils/LocationHelper'; import { UserActions } from 'utils/authorization'; -import { DATASOURCE_GRAFANA, PLUGIN_ROOT } from 'utils/consts'; +import { PLUGIN_ROOT } from 'utils/consts'; import sanitize from 'utils/sanitize'; const cx = cn.bind(styles); @@ -86,7 +86,7 @@ interface Integration2State extends PageBaseState { const ACTIONS_LIST_WIDTH = 200; const ACTIONS_LIST_BORDER = 2; -const NEW_ROUTE_DEFAULT = '{{ (payload.severity == "foo" and "bar" in payload.region) or True }}'; +const NEW_ROUTE_DEFAULT = '{# (payload.severity == "foo" and "bar" in payload.region) or True #}'; @observer class Integration2 extends React.Component { @@ -346,7 +346,9 @@ class Integration2 extends React.Component this.setState({ isEditTemplateModalOpen: undefined, }); - this.setState({ isTemplateSettingsOpen: true }); + if (selectedTemplate?.name !== 'route_template') { + this.setState({ isTemplateSettingsOpen: true }); + } LocationHelper.update({ template: undefined, routeId: undefined }, 'partial'); }} channelFilterId={channelFilterIdForEdit} @@ -601,9 +603,8 @@ const IntegrationSendDemoPayloadModal: React.FC { const store = useStore(); const { alertReceiveChannelStore } = store; - const stringifiedJson = JSON.stringify(alertReceiveChannel.demo_alert_payload, null, 2); - const initialDemoJSON = stringifiedJson.substring(1, stringifiedJson.length - 1); - const [demoPayload, setDemoPayload] = useState(alertReceiveChannel.demo_alert_payload); + const initialDemoJSON = JSON.stringify(alertReceiveChannel.demo_alert_payload, null, 2); + const [demoPayload, setDemoPayload] = useState(initialDemoJSON); let onPayloadChangeDebounced = debounce(100, onPayloadChange); return ( @@ -914,8 +915,6 @@ const IntegrationActions: React.FC = ({ alertReceiveCha const HowToConnectComponent: React.FC<{ id: AlertReceiveChannel['id'] }> = ({ id }) => { const { alertReceiveChannelStore } = useStore(); const alertReceiveChannelCounter = alertReceiveChannelStore.counters[id]; - const alertReceiveChannel = alertReceiveChannelStore.items[id]; - const isGrafanaDatasource = alertReceiveChannel.integration === DATASOURCE_GRAFANA; const hasAlerts = !!alertReceiveChannelCounter?.alerts_count; return ( @@ -947,7 +946,7 @@ const HowToConnectComponent: React.FC<{ id: AlertReceiveChannel['id'] }> = ({ id
} - content={isGrafanaDatasource || !hasAlerts ? renderContent() : null} + content={hasAlerts ? null : renderContent()} /> ); @@ -961,20 +960,6 @@ const HowToConnectComponent: React.FC<{ id: AlertReceiveChannel['id'] }> = ({ id No alerts yet; try to send a demo alert )} - - {isGrafanaDatasource && ( - - - - Contact Point - - and - - Notification Policy - - created in Grafana Alerting - - )}
); diff --git a/grafana-plugin/src/pages/integrations_2/Integrations2.tsx b/grafana-plugin/src/pages/integrations_2/Integrations2.tsx index 1de27eb1..746d8aa3 100644 --- a/grafana-plugin/src/pages/integrations_2/Integrations2.tsx +++ b/grafana-plugin/src/pages/integrations_2/Integrations2.tsx @@ -414,36 +414,34 @@ class Integrations extends React.Component
- <> -
-
{ - this.setState({ - confirmationModal: { - isOpen: true, - confirmText: 'Delete', - dismissText: 'Cancel', - onConfirm: () => this.handleDeleteAlertReceiveChannel(item.id), - title: 'Delete integration', - body: ( - - Are you sure you want to delete integration? - - ), - }, - }); - }} - style={{ width: '100%' }} - > - - - - Delete Integration - - -
+
+
{ + this.setState({ + confirmationModal: { + isOpen: true, + confirmText: 'Delete', + dismissText: 'Cancel', + onConfirm: () => this.handleDeleteAlertReceiveChannel(item.id), + title: 'Delete integration', + body: ( + + Are you sure you want to delete integration? + + ), + }, + }); + }} + style={{ width: '100%' }} + > + + + + Delete Integration + +
- +
)} @@ -475,9 +473,12 @@ class Integrations extends React.Component applyFilters = () => { const { store } = this.props; const { alertReceiveChannelStore } = store; - const { integrationsFilters, page } = this.state; + const { integrationsFilters } = this.state; - return alertReceiveChannelStore.updatePaginatedItems(integrationsFilters, page); + return alertReceiveChannelStore.updatePaginatedItems(integrationsFilters).then(() => { + this.setState({ page: 1 }); + LocationHelper.update({ p: 1 }, 'partial'); + }); }; debouncedUpdateIntegrations = debounce(this.applyFilters, FILTERS_DEBOUNCE_MS); diff --git a/grafana-plugin/src/utils/consts.ts b/grafana-plugin/src/utils/consts.ts index b7dba9fc..a5951b5c 100644 --- a/grafana-plugin/src/utils/consts.ts +++ b/grafana-plugin/src/utils/consts.ts @@ -29,7 +29,7 @@ export const FARO_ENDPOINT_PROD = 'https://faro-collector-prod-us-central-0.grafana.net/collect/03a11ed03c3af04dcfc3be9755f2b053'; export const DOCS_SLACK_SETUP = 'https://grafana.com/docs/oncall/latest/open-source/#slack-setup'; -export const DOCS_TELEGRAM_SETUP = 'https://grafana.com/docs/oncall/latest/chat-options/configure-telegram/'; +export const DOCS_TELEGRAM_SETUP = 'https://grafana.com/docs/oncall/latest/notify/telegram/'; // Make sure if you chage max-width here you also change it in responsive.css export const TABLE_COLUMN_MAX_WIDTH = 1500; diff --git a/helm/README.md b/helm/README.md index b35fd43f..0e538b25 100644 --- a/helm/README.md +++ b/helm/README.md @@ -20,24 +20,17 @@ ```bash helm install helm-testing \ --wait \ - --timeout 30m \ - --wait-for-jobs \ --values ./simple.yml \ - --values ./values-arm64.yml \ ./oncall ``` 5. Get credentials - - ```bash echo "\n\nOpen Grafana on localhost:30002 with credentials - user: admin, password: $(kubectl get secret --namespace default helm-testing-grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo)" echo "Open Plugins -> Grafana OnCall -> fill form: backend url: http://host.docker.internal:30001" ``` - - 6. Clean up ```bash diff --git a/helm/oncall/Chart.yaml b/helm/oncall/Chart.yaml index 71ae02c5..e2afac05 100644 --- a/helm/oncall/Chart.yaml +++ b/helm/oncall/Chart.yaml @@ -2,6 +2,7 @@ apiVersion: v2 name: oncall description: Developer-friendly incident response with brilliant Slack integration type: application +# version and appVersion are handled by CI, no need to change them manually version: 1.2.41 appVersion: v1.2.41 dependencies: @@ -10,7 +11,7 @@ dependencies: repository: https://charts.jetstack.io condition: cert-manager.enabled - name: mariadb - version: 11.0.10 + version: 12.2.5 repository: https://charts.bitnami.com/bitnami condition: mariadb.enabled - name: postgresql @@ -18,7 +19,7 @@ dependencies: repository: https://charts.bitnami.com/bitnami condition: postgresql.enabled - name: rabbitmq - version: 10.3.9 + version: 12.0.0 repository: https://charts.bitnami.com/bitnami condition: rabbitmq.enabled - name: redis @@ -26,7 +27,7 @@ dependencies: repository: https://charts.bitnami.com/bitnami condition: redis.enabled - name: grafana - version: 6.29.6 + version: 6.57.1 repository: https://grafana.github.io/helm-charts condition: grafana.enabled - name: ingress-nginx diff --git a/helm/oncall/README.md b/helm/oncall/README.md index 58ae20e9..5e49c906 100644 --- a/helm/oncall/README.md +++ b/helm/oncall/README.md @@ -45,8 +45,6 @@ helm install \ Follow the `helm install` output to finish setting up Grafana OnCall backend and Grafana OnCall frontend plugin e.g. - - ```bash 👋 Your Grafana OnCall instance has been successfully deployed @@ -74,8 +72,6 @@ Follow the `helm install` output to finish setting up Grafana OnCall backend and 🎉🎉🎉 Done! 🎉🎉🎉 ``` - - ## Configuration You can edit values.yml to make changes to the helm chart configuration and re-deploy the release with the following command: diff --git a/helm/oncall/charts/grafana-6.29.6.tgz b/helm/oncall/charts/grafana-6.29.6.tgz deleted file mode 100644 index ed0a64c9..00000000 Binary files a/helm/oncall/charts/grafana-6.29.6.tgz and /dev/null differ diff --git a/helm/oncall/charts/grafana-6.57.1.tgz b/helm/oncall/charts/grafana-6.57.1.tgz new file mode 100644 index 00000000..25121669 Binary files /dev/null and b/helm/oncall/charts/grafana-6.57.1.tgz differ diff --git a/helm/oncall/charts/mariadb-11.0.10.tgz b/helm/oncall/charts/mariadb-11.0.10.tgz deleted file mode 100644 index 0e41a301..00000000 Binary files a/helm/oncall/charts/mariadb-11.0.10.tgz and /dev/null differ diff --git a/helm/oncall/charts/mariadb-12.2.5.tgz b/helm/oncall/charts/mariadb-12.2.5.tgz new file mode 100644 index 00000000..d9b8b659 Binary files /dev/null and b/helm/oncall/charts/mariadb-12.2.5.tgz differ diff --git a/helm/oncall/charts/rabbitmq-10.1.1.tgz b/helm/oncall/charts/rabbitmq-10.1.1.tgz deleted file mode 100644 index 955e49d1..00000000 Binary files a/helm/oncall/charts/rabbitmq-10.1.1.tgz and /dev/null differ diff --git a/helm/oncall/charts/rabbitmq-12.0.0.tgz b/helm/oncall/charts/rabbitmq-12.0.0.tgz new file mode 100644 index 00000000..0be84a07 Binary files /dev/null and b/helm/oncall/charts/rabbitmq-12.0.0.tgz differ diff --git a/helm/oncall/charts/redis-16.10.1.tgz b/helm/oncall/charts/redis-16.10.1.tgz deleted file mode 100644 index dfb1e8df..00000000 Binary files a/helm/oncall/charts/redis-16.10.1.tgz and /dev/null differ diff --git a/helm/oncall/charts/redis-16.13.2.tgz b/helm/oncall/charts/redis-16.13.2.tgz new file mode 100644 index 00000000..33b5e967 Binary files /dev/null and b/helm/oncall/charts/redis-16.13.2.tgz differ diff --git a/helm/oncall/templates/_env.tpl b/helm/oncall/templates/_env.tpl index f269f37d..a028907a 100644 --- a/helm/oncall/templates/_env.tpl +++ b/helm/oncall/templates/_env.tpl @@ -257,7 +257,7 @@ http://{{ include "oncall.grafana.fullname" . }} {{- if and (not .Values.mariadb.enabled) .Values.externalMysql.db_name -}} {{- required "externalMysql.db_name is required if not mariadb.enabled" .Values.externalMysql.db_name | quote}} {{- else -}} -"oncall" +{{- .Values.mariadb.auth.database | default "oncall" | quote -}} {{- end -}} {{- end -}} @@ -265,7 +265,7 @@ http://{{ include "oncall.grafana.fullname" . }} {{- if and (not .Values.mariadb.enabled) .Values.externalMysql.user -}} {{- .Values.externalMysql.user | quote }} {{- else -}} -"root" +{{- .Values.mariadb.auth.username | default "root" | quote -}} {{- end -}} {{- end -}} @@ -480,3 +480,19 @@ rabbitmq-password value: {{ .Values.oncall.smtp.enabled | toString | title | quote }} {{- end -}} {{- end }} + +{{- define "snippet.oncall.exporter.env" -}} +{{- if .Values.oncall.exporter.enabled -}} +- name: FEATURE_PROMETHEUS_EXPORTER_ENABLED + value: {{ .Values.oncall.exporter.enabled | toString | title | quote }} +- name: PROMETHEUS_EXPORTER_SECRET + valueFrom: + secretKeyRef: + name: {{ include "oncall.fullname" . }}-exporter + key: exporter-secret + optional: true +{{- else -}} +- name: FEATURE_PROMETHEUS_EXPORTER_ENABLED + value: {{ .Values.oncall.exporter.enabled | toString | title | quote }} +{{- end -}} +{{- end }} diff --git a/helm/oncall/templates/_helpers.tpl b/helm/oncall/templates/_helpers.tpl index 1566deb1..e58f9edc 100644 --- a/helm/oncall/templates/_helpers.tpl +++ b/helm/oncall/templates/_helpers.tpl @@ -97,17 +97,7 @@ Create the name of the service account to use {{- include "snippet.mysql.env" . | nindent 4 }} {{- include "snippet.rabbitmq.env" . | nindent 4 }} {{- include "snippet.redis.env" . | nindent 4 }} - {{- if .Values.env }} - {{- if (kindIs "map" .Values.env) }} - {{- range $key, $value := .Values.env }} - - name: {{ $key }} - value: {{ $value }} - {{- end -}} - {{/* support previous schema */}} - {{- else }} - {{- toYaml .Values.env | nindent 4 }} - {{- end }} - {{- end }} + {{- include "oncall.extraEnvs" . | nindent 4 }} {{- end }} {{- define "oncall.postgresql.wait-for-db" }} @@ -122,7 +112,19 @@ Create the name of the service account to use {{- include "snippet.postgresql.env" . | nindent 4 }} {{- include "snippet.rabbitmq.env" . | nindent 4 }} {{- include "snippet.redis.env" . | nindent 4 }} - {{- if .Values.env }} - {{- toYaml .Values.env | nindent 4 }} - {{- end }} + {{- include "oncall.extraEnvs" . | nindent 4 }} {{- end }} + +{{- define "oncall.extraEnvs" -}} +{{- if .Values.env }} + {{- if (kindIs "map" .Values.env) }} + {{- range $key, $value := .Values.env }} +- name: {{ $key }} + value: {{ $value }} + {{- end -}} + {{/* support previous schema */}} + {{- else }} +{{- toYaml .Values.env }} + {{- end }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/oncall/templates/celery/_deployment.tpl b/helm/oncall/templates/celery/_deployment.tpl index 4e09cb30..7a8361dd 100644 --- a/helm/oncall/templates/celery/_deployment.tpl +++ b/helm/oncall/templates/celery/_deployment.tpl @@ -34,6 +34,10 @@ spec: {{- if eq .Values.database.type "postgresql" }} {{- include "oncall.postgresql.wait-for-db" . | indent 8 }} {{- end }} + {{- with .Values.celery.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} containers: - name: {{ .Chart.Name }} securityContext: @@ -47,6 +51,7 @@ spec: {{- include "snippet.oncall.slack.env" . | nindent 12 }} {{- include "snippet.oncall.telegram.env" . | nindent 12 }} {{- include "snippet.oncall.smtp.env" . | nindent 12 }} + {{- include "snippet.oncall.exporter.env" . | nindent 12 }} {{- if eq .Values.database.type "mysql" }} {{- include "snippet.mysql.env" . | nindent 12 }} {{- end }} @@ -55,17 +60,7 @@ spec: {{- end }} {{- include "snippet.rabbitmq.env" . | nindent 12 }} {{- include "snippet.redis.env" . | nindent 12 }} - {{- if .Values.env }} - {{- if (kindIs "map" .Values.env) }} - {{- range $key, $value := .Values.env }} - - name: {{ $key }} - value: {{ $value }} - {{- end -}} - {{/* support previous schema */}} - {{- else }} - {{- toYaml .Values.env | nindent 12 }} - {{- end }} - {{- end }} + {{- include "oncall.extraEnvs" . | nindent 12 }} {{- if .Values.celery.livenessProbe.enabled }} livenessProbe: exec: diff --git a/helm/oncall/templates/engine/deployment.yaml b/helm/oncall/templates/engine/deployment.yaml index f0848c76..6b91cccf 100644 --- a/helm/oncall/templates/engine/deployment.yaml +++ b/helm/oncall/templates/engine/deployment.yaml @@ -51,6 +51,7 @@ spec: {{- include "snippet.oncall.telegram.env" . | nindent 12 }} {{- include "snippet.oncall.smtp.env" . | nindent 12 }} {{- include "snippet.oncall.twilio.env" . | nindent 12 }} + {{- include "snippet.oncall.exporter.env" . | nindent 12 }} {{- if eq .Values.database.type "mysql" }} {{- include "snippet.mysql.env" . | nindent 12 }} {{- end }} @@ -59,17 +60,7 @@ spec: {{- end }} {{- include "snippet.rabbitmq.env" . | nindent 12 }} {{- include "snippet.redis.env" . | nindent 12 }} - {{- if .Values.env }} - {{- if (kindIs "map" .Values.env) }} - {{- range $key, $value := .Values.env }} - - name: {{ $key }} - value: {{ $value }} - {{- end -}} - {{/* support previous schema */}} - {{- else }} - {{- toYaml .Values.env | nindent 12 }} - {{- end }} - {{- end }} + {{- include "oncall.extraEnvs" . | nindent 12 }} livenessProbe: httpGet: path: /health/ @@ -86,7 +77,7 @@ spec: httpGet: path: /startupprobe/ port: http - periodSeconds: 60 + periodSeconds: 10 timeoutSeconds: 3 resources: {{- toYaml .Values.engine.resources | nindent 12 }} diff --git a/helm/oncall/templates/engine/job-migrate.yaml b/helm/oncall/templates/engine/job-migrate.yaml index acf7fb2e..06353ad4 100644 --- a/helm/oncall/templates/engine/job-migrate.yaml +++ b/helm/oncall/templates/engine/job-migrate.yaml @@ -35,6 +35,10 @@ spec: serviceAccountName: {{ include "oncall.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- with .Values.migrate.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} containers: - name: {{ .Chart.Name }}-migrate securityContext: @@ -62,6 +66,7 @@ spec: env: {{- include "snippet.oncall.env" . | nindent 12 }} {{- include "snippet.oncall.smtp.env" . | nindent 12 }} + {{- include "snippet.oncall.exporter.env" . | nindent 12 }} {{- if eq .Values.database.type "mysql" }} {{- include "snippet.mysql.env" . | nindent 12 }} {{- end }} @@ -70,9 +75,7 @@ spec: {{- end }} {{- include "snippet.rabbitmq.env" . | nindent 12 }} {{- include "snippet.redis.env" . | nindent 12 }} - {{- if .Values.env }} - {{- toYaml .Values.env | nindent 12 }} - {{- end }} + {{- include "oncall.extraEnvs" . | nindent 12 }} resources: {{- toYaml .Values.engine.resources | nindent 12 }} {{- end }} diff --git a/helm/oncall/templates/secrets.yaml b/helm/oncall/templates/secrets.yaml index 8a0dc069..62a97896 100644 --- a/helm/oncall/templates/secrets.yaml +++ b/helm/oncall/templates/secrets.yaml @@ -51,6 +51,16 @@ data: smtp-password: {{ .Values.oncall.smtp.password | b64enc | quote }} {{- end }} --- +{{ if and .Values.oncall.exporter.enabled .Values.oncall.exporter.authToken -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "oncall.fullname" . }}-exporter +type: Opaque +data: + exporter-secret: {{ .Values.oncall.exporter.authToken | b64enc | quote }} +{{- end }} +--- {{ if and (not .Values.postgresql.enabled) (eq .Values.database.type "postgresql") (not .Values.externalPostgresql.existingSecret) -}} apiVersion: v1 kind: Secret diff --git a/helm/oncall/tests/__snapshot__/wait_for_db_test.yaml.snap b/helm/oncall/tests/__snapshot__/wait_for_db_test.yaml.snap new file mode 100644 index 00000000..76d8cace --- /dev/null +++ b/helm/oncall/tests/__snapshot__/wait_for_db_test.yaml.snap @@ -0,0 +1,298 @@ +database.type=mysql -> should create initContainer for MySQL database (default): + 1: | + - command: + - sh + - -c + - until (python manage.py migrate --check); do echo Waiting for database migrations; sleep 2; done + env: + - name: BASE_URL + value: https://example.com + - name: SECRET_KEY + valueFrom: + secretKeyRef: + key: SECRET_KEY + name: oncall + - name: MIRAGE_SECRET_KEY + valueFrom: + secretKeyRef: + key: MIRAGE_SECRET_KEY + name: oncall + - name: MIRAGE_CIPHER_IV + value: 1234567890abcdef + - name: DJANGO_SETTINGS_MODULE + value: settings.helm + - name: AMIXR_DJANGO_ADMIN_PATH + value: admin + - name: OSS + value: "True" + - name: UWSGI_LISTEN + value: "1024" + - name: BROKER_TYPE + value: rabbitmq + - name: GRAFANA_API_URL + value: http://oncall-grafana + - name: MYSQL_HOST + value: oncall-mariadb + - name: MYSQL_PORT + value: "3306" + - name: MYSQL_DB_NAME + value: oncall + - name: MYSQL_USER + value: root + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + key: mariadb-root-password + name: oncall-mariadb + - name: RABBITMQ_USERNAME + value: user + - name: RABBITMQ_PASSWORD + valueFrom: + secretKeyRef: + key: rabbitmq-password + name: oncall-rabbitmq + - name: RABBITMQ_HOST + value: oncall-rabbitmq + - name: RABBITMQ_PORT + value: "5672" + - name: RABBITMQ_PROTOCOL + value: amqp + - name: RABBITMQ_VHOST + value: "" + - name: REDIS_HOST + value: oncall-redis-master + - name: REDIS_PORT + value: "6379" + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + key: redis-password + name: oncall-redis + image: grafana/oncall:v1.2.36 + imagePullPolicy: Always + name: wait-for-db + securityContext: {} + 2: | + - command: + - sh + - -c + - until (python manage.py migrate --check); do echo Waiting for database migrations; sleep 2; done + env: + - name: BASE_URL + value: https://example.com + - name: SECRET_KEY + valueFrom: + secretKeyRef: + key: SECRET_KEY + name: oncall + - name: MIRAGE_SECRET_KEY + valueFrom: + secretKeyRef: + key: MIRAGE_SECRET_KEY + name: oncall + - name: MIRAGE_CIPHER_IV + value: 1234567890abcdef + - name: DJANGO_SETTINGS_MODULE + value: settings.helm + - name: AMIXR_DJANGO_ADMIN_PATH + value: admin + - name: OSS + value: "True" + - name: UWSGI_LISTEN + value: "1024" + - name: BROKER_TYPE + value: rabbitmq + - name: GRAFANA_API_URL + value: http://oncall-grafana + - name: MYSQL_HOST + value: oncall-mariadb + - name: MYSQL_PORT + value: "3306" + - name: MYSQL_DB_NAME + value: oncall + - name: MYSQL_USER + value: root + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + key: mariadb-root-password + name: oncall-mariadb + - name: RABBITMQ_USERNAME + value: user + - name: RABBITMQ_PASSWORD + valueFrom: + secretKeyRef: + key: rabbitmq-password + name: oncall-rabbitmq + - name: RABBITMQ_HOST + value: oncall-rabbitmq + - name: RABBITMQ_PORT + value: "5672" + - name: RABBITMQ_PROTOCOL + value: amqp + - name: RABBITMQ_VHOST + value: "" + - name: REDIS_HOST + value: oncall-redis-master + - name: REDIS_PORT + value: "6379" + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + key: redis-password + name: oncall-redis + image: grafana/oncall:v1.2.36 + imagePullPolicy: Always + name: wait-for-db + securityContext: {} +database.type=postgresql -> should create initContainer for PostgreSQL database: + 1: | + - command: + - sh + - -c + - until (python manage.py migrate --check); do echo Waiting for database migrations; sleep 2; done + env: + - name: BASE_URL + value: https://example.com + - name: SECRET_KEY + valueFrom: + secretKeyRef: + key: SECRET_KEY + name: oncall + - name: MIRAGE_SECRET_KEY + valueFrom: + secretKeyRef: + key: MIRAGE_SECRET_KEY + name: oncall + - name: MIRAGE_CIPHER_IV + value: 1234567890abcdef + - name: DJANGO_SETTINGS_MODULE + value: settings.helm + - name: AMIXR_DJANGO_ADMIN_PATH + value: admin + - name: OSS + value: "True" + - name: UWSGI_LISTEN + value: "1024" + - name: BROKER_TYPE + value: rabbitmq + - name: GRAFANA_API_URL + value: http://oncall-grafana + - name: DATABASE_TYPE + value: postgresql + - name: DATABASE_HOST + value: oncall-postgresql + - name: DATABASE_PORT + value: "5432" + - name: DATABASE_NAME + value: oncall + - name: DATABASE_USER + value: postgres + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + key: postgres-password + name: oncall-postgresql + - name: RABBITMQ_USERNAME + value: user + - name: RABBITMQ_PASSWORD + valueFrom: + secretKeyRef: + key: rabbitmq-password + name: oncall-rabbitmq + - name: RABBITMQ_HOST + value: oncall-rabbitmq + - name: RABBITMQ_PORT + value: "5672" + - name: RABBITMQ_PROTOCOL + value: amqp + - name: RABBITMQ_VHOST + value: "" + - name: REDIS_HOST + value: oncall-redis-master + - name: REDIS_PORT + value: "6379" + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + key: redis-password + name: oncall-redis + image: grafana/oncall:v1.2.36 + imagePullPolicy: Always + name: wait-for-db + securityContext: {} + 2: | + - command: + - sh + - -c + - until (python manage.py migrate --check); do echo Waiting for database migrations; sleep 2; done + env: + - name: BASE_URL + value: https://example.com + - name: SECRET_KEY + valueFrom: + secretKeyRef: + key: SECRET_KEY + name: oncall + - name: MIRAGE_SECRET_KEY + valueFrom: + secretKeyRef: + key: MIRAGE_SECRET_KEY + name: oncall + - name: MIRAGE_CIPHER_IV + value: 1234567890abcdef + - name: DJANGO_SETTINGS_MODULE + value: settings.helm + - name: AMIXR_DJANGO_ADMIN_PATH + value: admin + - name: OSS + value: "True" + - name: UWSGI_LISTEN + value: "1024" + - name: BROKER_TYPE + value: rabbitmq + - name: GRAFANA_API_URL + value: http://oncall-grafana + - name: DATABASE_TYPE + value: postgresql + - name: DATABASE_HOST + value: oncall-postgresql + - name: DATABASE_PORT + value: "5432" + - name: DATABASE_NAME + value: oncall + - name: DATABASE_USER + value: postgres + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + key: postgres-password + name: oncall-postgresql + - name: RABBITMQ_USERNAME + value: user + - name: RABBITMQ_PASSWORD + valueFrom: + secretKeyRef: + key: rabbitmq-password + name: oncall-rabbitmq + - name: RABBITMQ_HOST + value: oncall-rabbitmq + - name: RABBITMQ_PORT + value: "5672" + - name: RABBITMQ_PROTOCOL + value: amqp + - name: RABBITMQ_VHOST + value: "" + - name: REDIS_HOST + value: oncall-redis-master + - name: REDIS_PORT + value: "6379" + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + key: redis-password + name: oncall-redis + image: grafana/oncall:v1.2.36 + imagePullPolicy: Always + name: wait-for-db + securityContext: {} diff --git a/helm/oncall/tests/extra_env_test.yaml b/helm/oncall/tests/extra_env_test.yaml new file mode 100644 index 00000000..3c6e19d0 --- /dev/null +++ b/helm/oncall/tests/extra_env_test.yaml @@ -0,0 +1,108 @@ +suite: test extra envs for deployments +templates: + - engine/deployment.yaml + - engine/job-migrate.yaml + - celery/deployment-celery.yaml +release: + name: oncall +tests: + - it: env=[] -> should support old syntax + set: + env: + - name: SOME_VAR + value: some_value + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: SOME_VAR + value: some_value + + - it: env=map[] -> should set multiple envs + set: + env: + SOME_VAR: some_value + another_var: "another_value" + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: SOME_VAR + value: some_value + - contains: + path: spec.template.spec.containers[0].env + content: + name: another_var + value: "another_value" + + - it: env=[] -> should add envs into initContainer + templates: + - engine/deployment.yaml + - celery/deployment-celery.yaml + set: + env: + - name: SOME_VAR + value: some_value + asserts: + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: SOME_VAR + value: some_value + + - it: env=map[] -> should add envs into initContainer + templates: + - engine/deployment.yaml + - celery/deployment-celery.yaml + set: + env: + SOME_VAR: some_value + another_var: "another_value" + asserts: + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: SOME_VAR + value: some_value + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: another_var + value: "another_value" + + - it: database.type=postgresql and env=map[] -> should add envs into initContainer + templates: + - engine/deployment.yaml + - celery/deployment-celery.yaml + set: + database.type: postgresql + env: + SOME_VAR: some_value + another_var: "another_value" + asserts: + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: SOME_VAR + value: some_value + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: another_var + value: "another_value" + + - it: database.type=postgresql and env=[] -> should support old style for initContainer + templates: + - engine/deployment.yaml + - celery/deployment-celery.yaml + set: + database.type: postgresql + env: + - name: SOME_VAR + value: some_value + asserts: + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: SOME_VAR + value: some_value diff --git a/helm/oncall/tests/image_deployments_test.yaml b/helm/oncall/tests/image_deployments_test.yaml new file mode 100644 index 00000000..1418ae7c --- /dev/null +++ b/helm/oncall/tests/image_deployments_test.yaml @@ -0,0 +1,33 @@ +suite: test image and imagePullPolicy for deployments +templates: + - celery/deployment-celery.yaml + - engine/deployment.yaml + - engine/job-migrate.yaml +release: + name: oncall +chart: + appVersion: 1.2.36 +tests: + - it: image={} -> should use default image tag + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: grafana/oncall:1.2.36 + - equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: Always + + - it: image.repository and image.tag -> should use custom image + set: + image: + repository: custom-oncall + tag: 1.2.36-custom + pullPolicy: IfNotPresent + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: custom-oncall:1.2.36-custom + - equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: IfNotPresent + diff --git a/helm/oncall/tests/image_pull_secrets_test.yaml b/helm/oncall/tests/image_pull_secrets_test.yaml new file mode 100644 index 00000000..e4ffd7cb --- /dev/null +++ b/helm/oncall/tests/image_pull_secrets_test.yaml @@ -0,0 +1,25 @@ +suite: test image pull secrets +templates: + - celery/deployment-celery.yaml + - engine/deployment.yaml + - engine/job-migrate.yaml +release: + name: oncall +tests: + - it: imagePullSecrets=[] -> should not create spec.template.spec.imagePullSecrets + set: + imagePullSecrets: [] + asserts: + - notExists: + path: spec.template.spec.imagePullSecrets + + - it: imagePullSecrets -> should use custom imagePullSecrets + set: + imagePullSecrets: + - name: regcred + asserts: + - contains: + path: spec.template.spec.imagePullSecrets + content: + name: regcred + diff --git a/helm/oncall/tests/mysql_env_test.yaml b/helm/oncall/tests/mysql_env_test.yaml new file mode 100644 index 00000000..b374adbe --- /dev/null +++ b/helm/oncall/tests/mysql_env_test.yaml @@ -0,0 +1,118 @@ +suite: test MySQL envs for deployments +templates: + - engine/deployment.yaml + - engine/job-migrate.yaml + - celery/deployment-celery.yaml +release: + name: oncall +tests: + - it: mariadb.enabled=false -> external MySQL default settings + set: + mariadb.enabled: false + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_TYPE + not: true + - contains: + path: spec.template.spec.containers[0].env + content: + name: MYSQL_DB_NAME + value: oncall + - contains: + path: spec.template.spec.containers[0].env + content: + name: MYSQL_PORT + value: "3306" + - contains: + path: spec.template.spec.containers[0].env + content: + name: MYSQL_USER + value: root + - contains: + path: spec.template.spec.containers[0].env + content: + name: MYSQL_HOST + value: oncall-mariadb + + - it: externalMysql -> use external MySQL custom settings + set: + mariadb.enabled: false + externalMysql: + host: test-host + port: 5555 + db_name: grafana_oncall + user: test_user + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: MYSQL_DB_NAME + value: grafana_oncall + - contains: + path: spec.template.spec.containers[0].env + content: + name: MYSQL_PORT + value: "5555" + - contains: + path: spec.template.spec.containers[0].env + content: + name: MYSQL_USER + value: test_user + - contains: + path: spec.template.spec.containers[0].env + content: + name: MYSQL_HOST + value: test-host + + - it: mariadb.enabled=true -> internal MySQL default settings + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: MYSQL_DB_NAME + value: oncall + - contains: + path: spec.template.spec.containers[0].env + content: + name: MYSQL_PORT + value: "3306" + - contains: + path: spec.template.spec.containers[0].env + content: + name: MYSQL_USER + value: root + - contains: + path: spec.template.spec.containers[0].env + content: + name: MYSQL_HOST + value: oncall-mariadb + + - it: mariadb.auth -> internal MySQL custom settings + set: + mariadb: + auth: + database: grafana_oncall + username: grafana_oncall + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: MYSQL_DB_NAME + value: grafana_oncall + - contains: + path: spec.template.spec.containers[0].env + content: + name: MYSQL_PORT + value: "3306" + - contains: + path: spec.template.spec.containers[0].env + content: + name: MYSQL_USER + value: grafana_oncall + - contains: + path: spec.template.spec.containers[0].env + content: + name: MYSQL_HOST + value: oncall-mariadb diff --git a/helm/oncall/tests/mysql_password_env_test.yaml b/helm/oncall/tests/mysql_password_env_test.yaml new file mode 100644 index 00000000..96aca613 --- /dev/null +++ b/helm/oncall/tests/mysql_password_env_test.yaml @@ -0,0 +1,46 @@ +suite: test MySQL password envs for deployments +release: + name: oncall +templates: + - engine/deployment.yaml + - engine/job-migrate.yaml + - celery/deployment-celery.yaml + - secrets.yaml +tests: + - it: secrets -> should fail if externalMysql.password not set + set: + mariadb.enabled: false + asserts: + - failedTemplate: + errorMessage: externalMysql.password is required if not mariadb.enabled + template: secrets.yaml + + - it: externalMySQL.password -> should create a Secret -mariadb-external + templates: + - engine/deployment.yaml + - engine/job-migrate.yaml + - celery/deployment-celery.yaml + set: + mariadb.enabled: false + externalMysql: + password: abcd123 + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: oncall-mysql-external + key: mariadb-root-password + - containsDocument: + kind: Secret + apiVersion: v1 + name: oncall-mysql-external + template: secrets.yaml + - equal: + path: data.mariadb-root-password + value: abcd123 + decodeBase64: true + documentIndex: 1 + template: secrets.yaml diff --git a/helm/oncall/tests/postgres_env_test.yaml b/helm/oncall/tests/postgres_env_test.yaml index e55648f4..bbacbe6e 100644 --- a/helm/oncall/tests/postgres_env_test.yaml +++ b/helm/oncall/tests/postgres_env_test.yaml @@ -1,4 +1,4 @@ -suite: test postgresql deployment environments +suite: test PostgreSQL envs for deployments templates: - engine/deployment.yaml - engine/job-migrate.yaml @@ -6,7 +6,7 @@ templates: release: name: oncall tests: - - it: external Postgresql default settings + - it: postgresql.enabled=false -> external PostgreSQL default settings set: database.type: postgresql postgresql.enabled: false @@ -37,7 +37,7 @@ tests: name: DATABASE_HOST value: oncall-postgresql - - it: external Postgresql custom settings + - it: externalPostgresql -> should use external PostgreSQL custom settings set: database.type: postgresql postgresql.enabled: false @@ -73,7 +73,7 @@ tests: name: DATABASE_HOST value: test-host - - it: internal Postgresql default settings + - it: postgresql.enabled=true -> internal PostgreSQL default settings set: database.type: postgresql postgresql.enabled: true @@ -104,7 +104,7 @@ tests: name: DATABASE_HOST value: oncall-postgresql - - it: internal Postgresql custom settings + - it: postgresql.auth -> should use internal PostgreSQL custom settings set: database.type: postgresql postgresql: diff --git a/helm/oncall/tests/postgres_password_env_test.yaml b/helm/oncall/tests/postgres_password_env_test.yaml index cd0e5b9a..83c2cc02 100644 --- a/helm/oncall/tests/postgres_password_env_test.yaml +++ b/helm/oncall/tests/postgres_password_env_test.yaml @@ -1,4 +1,4 @@ -suite: test postgresql password deployment environments +suite: test PostgreSQL password envs for deployments release: name: oncall templates: @@ -7,7 +7,7 @@ templates: - celery/deployment-celery.yaml - secrets.yaml tests: - - it: should fail if externalPostgresql.password not set + - it: secrets -> should fail if externalPostgresql.password not set set: database.type: postgresql postgresql.enabled: false @@ -16,7 +16,7 @@ tests: errorMessage: externalPostgresql.password is required if not postgresql.enabled and not externalPostgresql.existingSecret template: secrets.yaml - - it: externalPostgresql.password should create Secret -postgresql-external + - it: externalPostgresql.password -> should create a Secret -postgresql-external templates: - engine/deployment.yaml - engine/job-migrate.yaml @@ -47,7 +47,7 @@ tests: documentIndex: 1 template: secrets.yaml - - it: externalPostgresql.existingSecret should use existing secret + - it: externalPostgresql.existingSecret -> should use existing secret templates: - engine/deployment.yaml - engine/job-migrate.yaml @@ -67,7 +67,7 @@ tests: name: some-postgres-secret key: postgres-password - - it: externalPostgresql.passwordKey should be used for existing secret + - it: externalPostgresql.passwordKey -> should be used for existing secret templates: - engine/deployment.yaml - engine/job-migrate.yaml @@ -88,7 +88,7 @@ tests: name: some-postgres-secret key: postgres.key - - it: internal Postgresql custom settings + - it: postgresql.auth -> should use internal Postgresql custom settings templates: - engine/deployment.yaml - engine/job-migrate.yaml diff --git a/helm/oncall/tests/security_context_deployments_test.yaml b/helm/oncall/tests/security_context_deployments_test.yaml new file mode 100644 index 00000000..9cf6db81 --- /dev/null +++ b/helm/oncall/tests/security_context_deployments_test.yaml @@ -0,0 +1,39 @@ +suite: test security context for deployments +templates: + - celery/deployment-celery.yaml + - engine/deployment.yaml + - engine/job-migrate.yaml +release: + name: oncall +tests: + - it: podSecurityContext={} -> spec.template.spec.securityContext is empty (default) + set: + asserts: + - isNullOrEmpty: + path: spec.template.spec.securityContext + - isNullOrEmpty: + path: spec.template.spec.containers[0].securityContext + + - it: podSecurityContext.runAsNonRoot=true -> should fill securityContext + set: + podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + asserts: + - isSubset: + path: spec.template.spec.securityContext + content: + runAsNonRoot: true + runAsUser: 1000 + + - it: securityContext.runAsNonRoot=true -> should fill securityContext for container + set: + securityContext: + runAsNonRoot: true + runAsUser: 1000 + asserts: + - isSubset: + path: spec.template.spec.containers[0].securityContext + content: + runAsNonRoot: true + runAsUser: 1000 diff --git a/helm/oncall/tests/service_account_deployments_test.yaml b/helm/oncall/tests/service_account_deployments_test.yaml new file mode 100644 index 00000000..9fbe9303 --- /dev/null +++ b/helm/oncall/tests/service_account_deployments_test.yaml @@ -0,0 +1,30 @@ +suite: test service account deployments +templates: + - celery/deployment-celery.yaml + - engine/deployment.yaml + - engine/job-migrate.yaml +release: + name: oncall +tests: + - it: serviceAccount.create=true -> should use created serviceAccount for deployments (default) + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: oncall + + - it: serviceAccount.create=false -> should use default serviceAccount for deployments + set: + serviceAccount.create: false + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: default + + - it: serviceAccount.name=custom -> should use created custom serviceAccount for deployments + set: + serviceAccount.name: custom + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: custom + diff --git a/helm/oncall/tests/service_account_test.yaml b/helm/oncall/tests/service_account_test.yaml new file mode 100644 index 00000000..c60aabc0 --- /dev/null +++ b/helm/oncall/tests/service_account_test.yaml @@ -0,0 +1,44 @@ +suite: test service account +templates: + - serviceaccount.yaml +release: + name: oncall +tests: + - it: serviceAccount.create=true -> should create serviceAccount (default) + asserts: + - containsDocument: + kind: ServiceAccount + apiVersion: v1 + name: oncall + - notExists: + path: metadata.annotations + - isSubset: + path: metadata.labels + content: + app.kubernetes.io/instance: oncall + app.kubernetes.io/name: oncall + + - it: serviceAccount.create=false -> should not create serviceAccount + set: + serviceAccount.create: false + asserts: + - hasDocuments: + count: 0 + + - it: serviceAccount.name=custom -> should create custom serviceAccount + set: + serviceAccount.name: custom + asserts: + - equal: + path: metadata.name + value: custom + + - it: serviceAccount.annotations -> should add annotations to serviceAccount + set: + serviceAccount.annotations: + some-annotation: some-value + asserts: + - isSubset: + path: metadata.annotations + content: + some-annotation: some-value diff --git a/helm/oncall/tests/telegram_env_test.yaml b/helm/oncall/tests/telegram_env_test.yaml new file mode 100644 index 00000000..7f954568 --- /dev/null +++ b/helm/oncall/tests/telegram_env_test.yaml @@ -0,0 +1,55 @@ +suite: test telegram envs for deployments +templates: + - engine/deployment.yaml + - celery/deployment-celery.yaml +release: + name: oncall +tests: + - it: oncall.telegram.enabled=false -> Telegram integration disabled (default) + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: FEATURE_TELEGRAM_INTEGRATION_ENABLED + value: "False" + + - it: oncall.telegram.enabled=true -> should enable Telegram integration + set: + oncall.telegram: + enabled: true + webhookUrl: https://example.com + token: "abcd:123" + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: FEATURE_TELEGRAM_INTEGRATION_ENABLED + value: "True" + - contains: + path: spec.template.spec.containers[0].env + content: + name: TELEGRAM_WEBHOOK_HOST + value: "https://example.com" + - contains: + path: spec.template.spec.containers[0].env + content: + name: TELEGRAM_TOKEN + value: "abcd:123" + + - it: oncall.telegram.existingSecret=some-secret -> should prefer existing secret over oncall.telegram.token + set: + oncall.telegram: + enabled: true + token: "abcd:123" + existingSecret: some-secret + tokenKey: token + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: TELEGRAM_TOKEN + valueFrom: + secretKeyRef: + name: some-secret + key: token + diff --git a/helm/oncall/tests/uwsgi_env_test.yaml b/helm/oncall/tests/uwsgi_env_test.yaml index 7986c361..3ed3bbb0 100644 --- a/helm/oncall/tests/uwsgi_env_test.yaml +++ b/helm/oncall/tests/uwsgi_env_test.yaml @@ -6,7 +6,7 @@ templates: release: name: oncall tests: - - it: uwsgi.listen should overwrite UWSGI_LISTEN env + - it: uwsgi.listen -> should overwrite UWSGI_LISTEN env set: uwsgi.listen: 128 asserts: @@ -15,7 +15,7 @@ tests: content: name: UWSGI_LISTEN value: "128" - - it: uwsgi.envs should set multiple UWSGI_* envs + - it: uwsgi=map[] -> should set multiple UWSGI_* envs set: uwsgi: processes: 3 @@ -36,7 +36,8 @@ tests: content: name: UWSGI_MAX_REQUESTS value: "1000" - - it: uwsgi.null should not set any UWSGI_* variable + + - it: uwsgi=null -> should not set any UWSGI_* variable set: uwsgi: null asserts: diff --git a/helm/oncall/tests/wait_for_db_test.yaml b/helm/oncall/tests/wait_for_db_test.yaml new file mode 100644 index 00000000..a744962e --- /dev/null +++ b/helm/oncall/tests/wait_for_db_test.yaml @@ -0,0 +1,41 @@ +suite: test init container wait-for-db in deployments +templates: + - celery/deployment-celery.yaml + - engine/deployment.yaml +release: + name: oncall +chart: + appVersion: v1.2.36 +tests: + - it: database.type=mysql -> should create initContainer for MySQL database (default) + asserts: + - contains: + path: spec.template.spec.initContainers + content: + name: wait-for-db + any: true + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: MYSQL_DB_NAME + value: oncall + - matchSnapshot: + path: spec.template.spec.initContainers + + - it: database.type=postgresql -> should create initContainer for PostgreSQL database + set: + database.type: postgresql + asserts: + - contains: + path: spec.template.spec.initContainers + content: + name: wait-for-db + any: true + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: DATABASE_TYPE + value: postgresql + - matchSnapshot: + path: spec.template.spec.initContainers + diff --git a/helm/oncall/values.yaml b/helm/oncall/values.yaml index fa97f014..d7333149 100644 --- a/helm/oncall/values.yaml +++ b/helm/oncall/values.yaml @@ -4,6 +4,14 @@ # If you want to install grafana as a part of this release make sure to configure grafana.grafana.ini.server.domain too base_url: example.com +## Optionally specify an array of imagePullSecrets. +## Secrets must be manually created in the namespace. +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +## e.g: +## imagePullSecrets: +## - name: myRegistryKeySecretName +imagePullSecrets: [] + image: # Grafana OnCall docker image repository repository: grafana/oncall @@ -67,6 +75,9 @@ celery: initialDelaySeconds: 30 periodSeconds: 300 timeoutSeconds: 10 + ## Node labels for pod assignment + ## ref: https://kubernetes.io/docs/user-guide/node-selection/ + nodeSelector: {} resources: {} # limits: # cpu: 100m @@ -130,6 +141,9 @@ oncall: password: ~ tls: ~ fromEmail: ~ + exporter: + enabled: false + authToken: ~ twilio: # Twilio account SID/username to allow OnCall to send SMSes and make phone calls accountSid: "" @@ -161,6 +175,9 @@ oncall: # Whether to run django database migrations automatically migrate: enabled: true + ## Node labels for pod assignment + ## ref: https://kubernetes.io/docs/user-guide/node-selection/ + nodeSelector: {} # TTL can be unset by setting ttlSecondsAfterFinished: "" ttlSecondsAfterFinished: 20 # use a helm hook to manage the migration job @@ -321,6 +338,9 @@ grafana: serve_from_sub_path: true persistence: enabled: true + # Disable psp as PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+ + rbac: + pspEnabled: false plugins: - grafana-oncall-app diff --git a/helm/values-arm64.yml b/helm/values-arm64.yml deleted file mode 100644 index 7d59ae5a..00000000 --- a/helm/values-arm64.yml +++ /dev/null @@ -1,16 +0,0 @@ -# Substituting bitnami image with official image -# to be able to run Rabbitmq on arm64 (Mac M1) -# Optional for amd64 systems -rabbitmq: - enabled: true - image: - repository: rabbitmq - tag: 3.10.10 - auth: - username: user - password: user - extraEnvVars: - - name: RABBITMQ_DEFAULT_USER - value: user - - name: RABBITMQ_DEFAULT_PASS - value: user \ No newline at end of file diff --git a/tools/pagerduty-migrator/README.md b/tools/pagerduty-migrator/README.md index e60a7500..143add77 100644 --- a/tools/pagerduty-migrator/README.md +++ b/tools/pagerduty-migrator/README.md @@ -107,11 +107,9 @@ webhook integrations to adjust them for incoming payloads. Configuration is done via environment variables passed to the docker container. - - | Name | Description | Type | Default | -|-----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------|---------| -| `PAGERDUTY_API_TOKEN` | PagerDuty API **user token**. To create a token, refer to [PagerDuty docs](). | String | N/A | +| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------- | ------- | +| `PAGERDUTY_API_TOKEN` | PagerDuty API **user token**. To create a token, refer to [PagerDuty docs](https://support.pagerduty.com/docs/api-access-keys#generate-a-user-token-rest-api-key). | String | N/A | | `ONCALL_API_URL` | Grafana OnCall API URL. This can be found on the "Settings" page of your Grafana OnCall instance. | String | N/A | | `ONCALL_API_TOKEN` | Grafana OnCall API Token. To create a token, navigate to the "Settings" page of your Grafana OnCall instance. | String | N/A | | `MODE` | Migration mode (plan vs actual migration). | String (choices: `plan`, `migrate`) | `plan` | @@ -120,8 +118,6 @@ Configuration is done via environment variables passed to the docker container. | `EXPERIMENTAL_MIGRATE_EVENT_RULES` | Migrate global event rulesets to Grafana OnCall integrations. | Boolean | `false` | | `EXPERIMENTAL_MIGRATE_EVENT_RULES_LONG_NAMES` | Include service & integrations names from PD in migrated integrations (only effective when `EXPERIMENTAL_MIGRATE_EVENT_RULES` is `true`). | Boolean | `false` | - - ## Resources ### User notification rules @@ -144,11 +140,11 @@ The tool is capable of migrating on-call schedules from PagerDuty to Grafana OnC There are two ways to migrate on-call schedules: - Migrate on-call shifts as if they were created in Grafana OnCall web UI. Due to scheduling differences between -PagerDuty and Grafana OnCall, it's sometimes impossible to automatically migrate on-call shifts without manual changes -in PD. Pass `SCHEDULE_MIGRATION_MODE=web` to the tool to enable this mode. + PagerDuty and Grafana OnCall, it's sometimes impossible to automatically migrate on-call shifts without manual changes + in PD. Pass `SCHEDULE_MIGRATION_MODE=web` to the tool to enable this mode. - Using ICalendar file URLs from PagerDuty. This way it's always possible to migrate schedules without any manual -changes in PD, but resulting schedules in Grafana OnCall will be read-only. Pass `SCHEDULE_MIGRATION_MODE=ical` to the tool -to enable this mode. + changes in PD, but resulting schedules in Grafana OnCall will be read-only. Pass `SCHEDULE_MIGRATION_MODE=ical` to + the tool to enable this mode. On-call schedules will be migrated to new Grafana OnCall schedules with the same name as in PD. Any existing schedules with the same name will be deleted before migration. Any on-call schedules that reference unmatched users won't be @@ -197,4 +193,4 @@ but it can also make the names of integrations too long. - Connect integrations (press the "How to connect" button on the integration page) - Make sure users connect their phone numbers, Slack accounts, etc. in their user settings - When using `SCHEDULE_MIGRATION_MODE=ical`, at some point you would probably want to recreate schedules using -Google Calendar or Terraform to be able to modify migrated on-call schedules in Grafana OnCall + Google Calendar or Terraform to be able to modify migrated on-call schedules in Grafana OnCall