commit
130410ccfe
79 changed files with 1372 additions and 349 deletions
1
.github/workflows/linting-and-tests.yml
vendored
1
.github/workflows/linting-and-tests.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"default": true,
|
||||
"MD013": {
|
||||
"line_length": "120"
|
||||
"line_length": "120",
|
||||
"code_blocks": false,
|
||||
"tables": false
|
||||
},
|
||||
"MD024": {
|
||||
"siblings_only": true
|
||||
|
|
|
|||
12
CHANGELOG.md
12
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)
|
||||
|
||||
|
|
|
|||
36
README.md
36
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
|
||||
|
|
|
|||
|
|
@ -269,26 +269,18 @@ ERROR: Failed building wheel for cryptography
|
|||
|
||||
**Solution:**
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
|
||||
```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`
|
||||
```
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
### django.db.utils.OperationalError: (1366, "Incorrect string value")
|
||||
|
||||
**Problem:**
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
|
||||
```bash
|
||||
django.db.utils.OperationalError: (1366, "Incorrect string value: '\\xF0\\x9F\\x98\\x8A\\xF0\\x9F...' for column 'cached_name' at row 1")
|
||||
```
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
**Solution:**
|
||||
|
||||
Recreate the database with the correct encoding.
|
||||
|
|
@ -321,15 +313,11 @@ $ CDPATH="" make init
|
|||
|
||||
When running `make init start`:
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
|
||||
```bash
|
||||
Error response from daemon: open /var/lib/docker/overlay2/ac57b871108ee1b98ff4455e36d2175eae90cbc7d4c9a54608c0b45cfb7c6da5/committed: is a directory
|
||||
make: *** [start] Error 1
|
||||
```
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
**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.
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
|
||||
```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 <module>
|
|||
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'
|
||||
```
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
**Solution:**
|
||||
|
||||
[This solution](https://github.com/grpc/grpc/issues/15510#issuecomment-392012594) posted in a GitHub issue thread for
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -63,14 +63,10 @@ curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/" \
|
|||
}'
|
||||
```
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
|
||||
| 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. |
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
> **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.
|
||||
|
||||
|
|
|
|||
|
|
@ -30,8 +30,6 @@ The above command returns JSON structured in the following way:
|
|||
}
|
||||
```
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
|
||||
| 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`. |
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
**HTTP request**
|
||||
|
||||
`POST {{API_URL}}/api/v1/escalation_policies/`
|
||||
|
|
|
|||
|
|
@ -41,8 +41,6 @@ The above command returns JSON structured in the following way:
|
|||
}
|
||||
```
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
|
||||
| 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` |
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
Please see [RFC 5545](https://tools.ietf.org/html/rfc5545#section-3.3.10) for more information about recurrence rules.
|
||||
|
||||
**HTTP request**
|
||||
|
|
|
|||
|
|
@ -29,8 +29,6 @@ The above command returns JSON structured in the following way:
|
|||
}
|
||||
```
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
|
||||
| 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`. |
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
**HTTP request**
|
||||
|
||||
`POST {{API_URL}}/api/v1/personal_notification_rules/`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
|
||||
| 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. |
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
**HTTP request**
|
||||
|
||||
`POST {{API_URL}}/api/v1/routes/`
|
||||
|
|
|
|||
|
|
@ -39,8 +39,6 @@ The above command returns JSON structured in the following way:
|
|||
}
|
||||
```
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
|
||||
| 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`. |
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
**HTTP request**
|
||||
|
||||
`POST {{API_URL}}/api/v1/schedules/`
|
||||
|
|
|
|||
|
|
@ -36,16 +36,12 @@ The above command returns JSON structured in the following way:
|
|||
}
|
||||
```
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
|
||||
| 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. |
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
**HTTP request**
|
||||
|
||||
`GET {{API_URL}}/api/v1/user_groups/`
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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__,
|
||||
|
|
|
|||
24
engine/apps/metrics_exporter/tests/test_views.py
Normal file
24
engine/apps/metrics_exporter/tests/test_views.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<HTMLElement>;
|
||||
listWidth: number;
|
||||
listBorder: number;
|
||||
stopPropagation?: boolean;
|
||||
withBackground?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
|
@ -17,12 +18,16 @@ const cx = cn.bind(styles);
|
|||
|
||||
const HamburgerMenu: React.FC<HamburgerMenuProps> = (props) => {
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
const { openMenu, listBorder, listWidth, withBackground, className } = props;
|
||||
const { openMenu, listBorder, listWidth, withBackground, className, stopPropagation = false } = props;
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={withBackground ? cx('hamburger-menu-withBackground') : cx('hamburger-menu', className)}
|
||||
onClick={() => {
|
||||
className={withBackground ? cx('hamburgerMenu--withBackground') : cx('hamburgerMenu', className)}
|
||||
onClick={(e) => {
|
||||
if (stopPropagation) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
const boundingRect = ref.current.getBoundingClientRect();
|
||||
|
||||
openMenu({
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.regexp-template-code-error {
|
||||
border: var(--error-text-color) 1px solid;
|
||||
}
|
||||
|
||||
.regexp-template-editor-modal {
|
||||
width: 700px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string>(regexpBody);
|
||||
const [showErrorTemplate, setShowErrorTemplate] = useState<boolean>(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
|
|||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
|
||||
<div className={cx('regexp-template-code')}>
|
||||
<div className={cx('regexp-template-code', { 'regexp-template-code-error': showErrorTemplate })}>
|
||||
<MonacoEditor
|
||||
value={regexpTemplateBody}
|
||||
height={'200px'}
|
||||
|
|
|
|||
|
|
@ -29,13 +29,9 @@ const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDispla
|
|||
const store = useStore();
|
||||
const { escalationChainStore, alertReceiveChannelStore, telegramChannelStore } = store;
|
||||
const [routeIdForDeletion, setRouteIdForDeletion] = useState<ChannelFilter['id']>(undefined);
|
||||
const [telegramInfo, setTelegramInfo] = useState<Array<{ id: string; channel_name: string }>>([]);
|
||||
|
||||
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<CollapsedIntegrationRouteDispla
|
|||
)}
|
||||
tooltipContent={undefined}
|
||||
/>
|
||||
{routeWording === 'Default' && (
|
||||
<Text type="primary">All unrouted routes will be served to the default route</Text>
|
||||
)}
|
||||
{routeWording === 'Default' && <Text type="secondary">Unmatched alerts routed to default route</Text>}
|
||||
{routeWording !== 'Default' && channelFilter.filtering_term && (
|
||||
<Text type="primary" className={cx('heading-container__text')}>
|
||||
{channelFilter.filtering_term}
|
||||
|
|
@ -93,7 +87,7 @@ const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDispla
|
|||
content={
|
||||
<div className={cx('spacing')}>
|
||||
<VerticalGroup>
|
||||
{IntegrationHelper.getChatOpsChannels(channelFilter, telegramInfo, store)
|
||||
{IntegrationHelper.getChatOpsChannels(channelFilter, store)
|
||||
.filter((it) => it)
|
||||
.map((chatOpsChannel, key) => (
|
||||
<HorizontalGroup key={key}>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ExpandedIntegrationRouteDisplayP
|
|||
|
||||
useEffect(() => {
|
||||
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<ExpandedIntegrationRouteDisplayP
|
|||
</IntegrationBlockItem>
|
||||
)}
|
||||
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup spacing="md">
|
||||
<Text type="primary">Publish to ChatOps</Text>
|
||||
<ChatOpsConnectors channelFilterId={channelFilterId} showLineNumber={false} />
|
||||
</VerticalGroup>
|
||||
</IntegrationBlockItem>
|
||||
{IntegrationHelper.hasChatopsInstalled(store) && (
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup spacing="md">
|
||||
<Text type="primary">Publish to ChatOps</Text>
|
||||
<ChatOpsConnectors channelFilterId={channelFilterId} showLineNumber={false} />
|
||||
</VerticalGroup>
|
||||
</IntegrationBlockItem>
|
||||
)}
|
||||
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup>
|
||||
|
|
@ -363,15 +368,20 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
|
|||
)}
|
||||
>
|
||||
{({ openMenu }) => (
|
||||
<HamburgerMenu openMenu={openMenu} listBorder={2} listWidth={200} className={cx('hamburgerMenu-small')} />
|
||||
<HamburgerMenu
|
||||
openMenu={openMenu}
|
||||
listBorder={ACTIONS_LIST_BORDER}
|
||||
listWidth={ACTIONS_LIST_WIDTH}
|
||||
className={'hamburgerMenu--small'}
|
||||
stopPropagation={true}
|
||||
/>
|
||||
)}
|
||||
</WithContextMenu>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
);
|
||||
|
||||
function onDelete(e: React.SyntheticEvent) {
|
||||
e.stopPropagation();
|
||||
function onDelete() {
|
||||
setRouteIdForDeletion();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'] {
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -69,17 +69,10 @@ class RemoteFilters extends Component<RemoteFiltersProps, RemoteFiltersState> {
|
|||
|
||||
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<RemoteFiltersProps, RemoteFiltersState> {
|
|||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={cx('template-block-list')} id="content-container-id">
|
||||
<div className={cx('template-block-list')} id="alerts-content-container-id">
|
||||
{selectedAlertPayload ? (
|
||||
<>
|
||||
{isEditMode ? (
|
||||
<>
|
||||
<div className={cx('template-block-title')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text>Edit {selectedAlertName}</Text>
|
||||
<Text>Edit custom payload</Text>
|
||||
|
||||
<HorizontalGroup>
|
||||
<IconButton name="times" onClick={() => returnToListView()} />
|
||||
|
|
@ -101,24 +110,31 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
) : (
|
||||
<>
|
||||
<div className={cx('template-block-title')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text>{selectedAlertName}</Text>
|
||||
|
||||
<HorizontalGroup>
|
||||
<div className={cx('selected-alert-name-container')}>
|
||||
<div className={cx('selected-alert-name')}>
|
||||
<Text>{selectedAlertName}</Text>
|
||||
</div>
|
||||
<div className={cx('title-action-icons')}>
|
||||
<IconButton name="edit" onClick={() => setIsEditMode(true)} />
|
||||
<IconButton name="times" onClick={() => returnToListView()} />
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={cx('alert-groups-editor')}>
|
||||
<div className={cx('alert-groups-editor')}>
|
||||
<TooltipBadge
|
||||
borderType="primary"
|
||||
text="Last alert payload"
|
||||
tooltipTitle=""
|
||||
tooltipContent=""
|
||||
className={cx('alert-groups-last-payload-badge')}
|
||||
/>
|
||||
<div className={cx('alert-groups-editor-withBadge')}>
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(selectedAlertPayload, null, 4)}
|
||||
data={undefined}
|
||||
disabled
|
||||
height={getCodeEditorHeight()}
|
||||
height={getCodeEditorHeightWithBadge()}
|
||||
onChange={getChangeHandler()}
|
||||
showLineNumbers
|
||||
useAutoCompleteList={false}
|
||||
language={MONACO_LANGUAGE.json}
|
||||
monacoOptions={{
|
||||
|
|
|
|||
|
|
@ -69,15 +69,6 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
return this.searchResult.map(
|
||||
(alertReceiveChannelId: AlertReceiveChannel['id']) => 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,
|
||||
|
|
|
|||
|
|
@ -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) &&
|
||||
|
|
|
|||
|
|
@ -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<Integration2Props, Integration2State> {
|
||||
|
|
@ -346,7 +346,9 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
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<IntegrationSendDemoPayloadModalP
|
|||
}) => {
|
||||
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<string>(alertReceiveChannel.demo_alert_payload);
|
||||
const initialDemoJSON = JSON.stringify(alertReceiveChannel.demo_alert_payload, null, 2);
|
||||
const [demoPayload, setDemoPayload] = useState<string>(initialDemoJSON);
|
||||
let onPayloadChangeDebounced = debounce(100, onPayloadChange);
|
||||
|
||||
return (
|
||||
|
|
@ -914,8 +915,6 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({ 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
|
|||
</a>
|
||||
</div>
|
||||
}
|
||||
content={isGrafanaDatasource || !hasAlerts ? renderContent() : null}
|
||||
content={hasAlerts ? null : renderContent()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -961,20 +960,6 @@ const HowToConnectComponent: React.FC<{ id: AlertReceiveChannel['id'] }> = ({ id
|
|||
<Text type={'primary'}>No alerts yet; try to send a demo alert</Text>
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
|
||||
{isGrafanaDatasource && (
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Icon name="list-ui-alt" size="md" />
|
||||
<a href={`/alerting/notifications?alertmanager=grafana`} target="_blank" rel="noreferrer">
|
||||
<Text type={'link'}>Contact Point</Text>
|
||||
</a>
|
||||
<Text type={'secondary'}>and</Text>
|
||||
<a href="/alerting/routes?alertmanager=grafana" target="_blank">
|
||||
<Text type={'link'}>Notification Policy</Text>
|
||||
</a>
|
||||
<Text type={'secondary'}>created in Grafana Alerting</Text>
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -414,36 +414,34 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
<div className="thin-line-break" />
|
||||
|
||||
<WithPermissionControlTooltip key="delete" userAction={UserActions.IntegrationsWrite}>
|
||||
<>
|
||||
<div className={cx('integrations-actionItem')}>
|
||||
<div
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
confirmationModal: {
|
||||
isOpen: true,
|
||||
confirmText: 'Delete',
|
||||
dismissText: 'Cancel',
|
||||
onConfirm: () => this.handleDeleteAlertReceiveChannel(item.id),
|
||||
title: 'Delete integration',
|
||||
body: (
|
||||
<Text type="primary">
|
||||
Are you sure you want to delete <Emoji text={item.verbal_name} /> integration?
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
});
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Text type="danger">
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Icon name="trash-alt" />
|
||||
<span>Delete Integration</span>
|
||||
</HorizontalGroup>
|
||||
</Text>
|
||||
</div>
|
||||
<div className={cx('integrations-actionItem')}>
|
||||
<div
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
confirmationModal: {
|
||||
isOpen: true,
|
||||
confirmText: 'Delete',
|
||||
dismissText: 'Cancel',
|
||||
onConfirm: () => this.handleDeleteAlertReceiveChannel(item.id),
|
||||
title: 'Delete integration',
|
||||
body: (
|
||||
<Text type="primary">
|
||||
Are you sure you want to delete <Emoji text={item.verbal_name} /> integration?
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
});
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Text type="danger">
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Icon name="trash-alt" />
|
||||
<span>Delete Integration</span>
|
||||
</HorizontalGroup>
|
||||
</Text>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -475,9 +473,12 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
|
||||
```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"
|
||||
```
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
6. Clean up
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
|
||||
```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! 🎉🎉🎉
|
||||
```
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
## Configuration
|
||||
|
||||
You can edit values.yml to make changes to the helm chart configuration and re-deploy the release with the following command:
|
||||
|
|
|
|||
Binary file not shown.
BIN
helm/oncall/charts/grafana-6.57.1.tgz
Normal file
BIN
helm/oncall/charts/grafana-6.57.1.tgz
Normal file
Binary file not shown.
Binary file not shown.
BIN
helm/oncall/charts/mariadb-12.2.5.tgz
Normal file
BIN
helm/oncall/charts/mariadb-12.2.5.tgz
Normal file
Binary file not shown.
Binary file not shown.
BIN
helm/oncall/charts/rabbitmq-12.0.0.tgz
Normal file
BIN
helm/oncall/charts/rabbitmq-12.0.0.tgz
Normal file
Binary file not shown.
Binary file not shown.
BIN
helm/oncall/charts/redis-16.13.2.tgz
Normal file
BIN
helm/oncall/charts/redis-16.13.2.tgz
Normal file
Binary file not shown.
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
298
helm/oncall/tests/__snapshot__/wait_for_db_test.yaml.snap
Normal file
298
helm/oncall/tests/__snapshot__/wait_for_db_test.yaml.snap
Normal file
|
|
@ -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: {}
|
||||
108
helm/oncall/tests/extra_env_test.yaml
Normal file
108
helm/oncall/tests/extra_env_test.yaml
Normal file
|
|
@ -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
|
||||
33
helm/oncall/tests/image_deployments_test.yaml
Normal file
33
helm/oncall/tests/image_deployments_test.yaml
Normal file
|
|
@ -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
|
||||
|
||||
25
helm/oncall/tests/image_pull_secrets_test.yaml
Normal file
25
helm/oncall/tests/image_pull_secrets_test.yaml
Normal file
|
|
@ -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
|
||||
|
||||
118
helm/oncall/tests/mysql_env_test.yaml
Normal file
118
helm/oncall/tests/mysql_env_test.yaml
Normal file
|
|
@ -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
|
||||
46
helm/oncall/tests/mysql_password_env_test.yaml
Normal file
46
helm/oncall/tests/mysql_password_env_test.yaml
Normal file
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
39
helm/oncall/tests/security_context_deployments_test.yaml
Normal file
39
helm/oncall/tests/security_context_deployments_test.yaml
Normal file
|
|
@ -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
|
||||
30
helm/oncall/tests/service_account_deployments_test.yaml
Normal file
30
helm/oncall/tests/service_account_deployments_test.yaml
Normal file
|
|
@ -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
|
||||
|
||||
44
helm/oncall/tests/service_account_test.yaml
Normal file
44
helm/oncall/tests/service_account_test.yaml
Normal file
|
|
@ -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
|
||||
55
helm/oncall/tests/telegram_env_test.yaml
Normal file
55
helm/oncall/tests/telegram_env_test.yaml
Normal file
|
|
@ -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
|
||||
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
41
helm/oncall/tests/wait_for_db_test.yaml
Normal file
41
helm/oncall/tests/wait_for_db_test.yaml
Normal file
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -107,11 +107,9 @@ webhook integrations to adjust them for incoming payloads.
|
|||
|
||||
Configuration is done via environment variables passed to the docker container.
|
||||
|
||||
<!-- markdownlint-disable MD013 -->
|
||||
|
||||
| Name | Description | Type | Default |
|
||||
|-----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------|---------|
|
||||
| `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 |
|
||||
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------- | ------- |
|
||||
| `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` |
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
|
||||
## 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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue