Dev to main (#1129)
Release v1.1.16 Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> Co-authored-by: Joey Orlando <joey.orlando@grafana.com> Co-authored-by: Vadim Stepanov <vadimkerr@gmail.com> Co-authored-by: Rares Mardare <40542072+teodosii@users.noreply.github.com> Co-authored-by: Tom Mitchell <klaue@live.com>
This commit is contained in:
parent
49789098d8
commit
6d4e6f0108
14 changed files with 224 additions and 48 deletions
|
|
@ -5,6 +5,15 @@ 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).
|
||||
|
||||
## v1.1.16 (2023-01-12)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Minor bug fix in how the value of `Organization.is_rbac_permissions_enabled` is determined
|
||||
|
||||
- Helm chart: default values file and documentation now reflect the correct key to set for the Slack
|
||||
slash command name, `oncall.slack.commandName`.
|
||||
|
||||
## v1.1.15 (2023-01-10)
|
||||
|
||||
### Changed
|
||||
|
|
|
|||
|
|
@ -30,6 +30,14 @@ class GrafanaUserWithPermissions(GrafanaUser):
|
|||
permissions: List[GrafanaAPIPermission]
|
||||
|
||||
|
||||
class GCOMInstanceInfoConfigFeatureToggles(TypedDict):
|
||||
accessControlOnCall: str
|
||||
|
||||
|
||||
class GCOMInstanceInfoConfig(TypedDict):
|
||||
feature_toggles: GCOMInstanceInfoConfigFeatureToggles
|
||||
|
||||
|
||||
class GCOMInstanceInfo(TypedDict):
|
||||
id: int
|
||||
orgId: int
|
||||
|
|
@ -38,6 +46,7 @@ class GCOMInstanceInfo(TypedDict):
|
|||
orgName: str
|
||||
url: str
|
||||
status: str
|
||||
config: Optional[GCOMInstanceInfoConfig]
|
||||
|
||||
|
||||
class APIClient:
|
||||
|
|
@ -190,10 +199,26 @@ class GcomAPIClient(APIClient):
|
|||
def __init__(self, api_token: str):
|
||||
super().__init__(settings.GRAFANA_COM_API_URL, api_token)
|
||||
|
||||
def get_instance_info(self, stack_id: str) -> Optional[GCOMInstanceInfo]:
|
||||
data, _ = self.api_get(f"instances/{stack_id}")
|
||||
def get_instance_info(self, stack_id: str, include_config_query_param: bool = False) -> Optional[GCOMInstanceInfo]:
|
||||
"""
|
||||
NOTE: in order to use ?config=true, an "Admin" GCOM token must be used to make the API call
|
||||
"""
|
||||
url = f"instances/{stack_id}"
|
||||
if include_config_query_param:
|
||||
url += "?config=true"
|
||||
|
||||
data, _ = self.api_get(url)
|
||||
return data
|
||||
|
||||
def is_rbac_enabled_for_stack(self, stack_id: str) -> bool:
|
||||
"""
|
||||
NOTE: must use an "Admin" GCOM token when calling this method
|
||||
"""
|
||||
instance_info = self.get_instance_info(stack_id, True)
|
||||
if not instance_info:
|
||||
return False
|
||||
return instance_info.get("config", {}).get("feature_toggles", {}).get("accessControlOnCall", "false") == "true"
|
||||
|
||||
def get_instances(self, query: str):
|
||||
return self.api_get(query)
|
||||
|
||||
|
|
|
|||
29
engine/apps/grafana_plugin/tests/test_gcom_api_client.py
Normal file
29
engine/apps/grafana_plugin/tests/test_gcom_api_client.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from apps.grafana_plugin.helpers.client import GcomAPIClient
|
||||
|
||||
|
||||
class TestIsRbacEnabledForStack:
|
||||
@pytest.mark.parametrize(
|
||||
"gcom_api_response,expected",
|
||||
[
|
||||
(None, False),
|
||||
({}, False),
|
||||
({"config": {}}, False),
|
||||
({"config": {"feature_toggles": {}}}, False),
|
||||
({"config": {"feature_toggles": {"accessControlOnCall": "false"}}}, False),
|
||||
({"config": {"feature_toggles": {"accessControlOnCall": "true"}}}, True),
|
||||
],
|
||||
)
|
||||
@patch("apps.grafana_plugin.helpers.client.GcomAPIClient.api_get")
|
||||
def test_it_returns_based_on_feature_toggle_value(
|
||||
self, mocked_gcom_api_client_api_get, gcom_api_response, expected
|
||||
):
|
||||
stack_id = 5
|
||||
mocked_gcom_api_client_api_get.return_value = (gcom_api_response, {"status_code": 200})
|
||||
|
||||
api_client = GcomAPIClient("someFakeApiToken")
|
||||
assert api_client.is_rbac_enabled_for_stack(stack_id) == expected
|
||||
assert mocked_gcom_api_client_api_get.called_once_with(f"instances/{stack_id}?config=true")
|
||||
|
|
@ -15,7 +15,7 @@ class TestGetUsersPermissions:
|
|||
permissions = api_client.get_users_permissions(False)
|
||||
assert len(permissions.keys()) == 0
|
||||
|
||||
@patch("apps.grafana_plugin.views.self_hosted_install.GrafanaAPIClient.api_get")
|
||||
@patch("apps.grafana_plugin.helpers.client.GrafanaAPIClient.api_get")
|
||||
def test_api_call_returns_none(self, mocked_grafana_api_client_api_get):
|
||||
mocked_grafana_api_client_api_get.return_value = (None, "dfkjfdkj")
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ class TestGetUsersPermissions:
|
|||
permissions = api_client.get_users_permissions(True)
|
||||
assert len(permissions.keys()) == 0
|
||||
|
||||
@patch("apps.grafana_plugin.views.self_hosted_install.GrafanaAPIClient.api_get")
|
||||
@patch("apps.grafana_plugin.helpers.client.GrafanaAPIClient.api_get")
|
||||
def test_it_properly_transforms_the_data(self, mocked_grafana_api_client_api_get):
|
||||
mocked_grafana_api_client_api_get.return_value = (
|
||||
{"1": {"grafana-oncall-app.alert-groups:read": [""], "grafana-oncall-app.alert-groups:write": [""]}},
|
||||
|
|
@ -50,7 +50,7 @@ class TestIsRbacEnabledForOrganization:
|
|||
(status.HTTP_404_NOT_FOUND, False),
|
||||
],
|
||||
)
|
||||
@patch("apps.grafana_plugin.views.self_hosted_install.GrafanaAPIClient.api_head")
|
||||
@patch("apps.grafana_plugin.helpers.client.GrafanaAPIClient.api_head")
|
||||
def test_it_returns_based_on_status_code_of_head_call(
|
||||
self, mocked_grafana_api_client_api_head, grafana_api_status_code, expected
|
||||
):
|
||||
|
|
|
|||
|
|
@ -1,29 +1,72 @@
|
|||
# from firebase_admin.messaging import Message
|
||||
# from fcm_django.models import FCMDevice
|
||||
import logging
|
||||
|
||||
from fcm_django.models import FCMDevice
|
||||
from firebase_admin.messaging import APNSConfig, APNSPayload, Aps, ApsAlert, CriticalSound, Message
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
REQUIRED_FIELDS = {"registration_ids", "notification", "data"}
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
# TODO: update thie
|
||||
class FCMRelayView(APIView):
|
||||
# TODO: use public API authentication (then it would be required to connect to a cloud instance to use the app)
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
This view accepts requests from OSS instances of Grafana OnCall and forwards these requests to FCM.
|
||||
Requests will be sent with the FCM_API_KEY configured in server settings
|
||||
(see PUSH_NOTIFICATIONS_SETTINGS in settings/base.py)
|
||||
This view accepts push notifications from OSS instances and forwards these requests to FCM.
|
||||
Requests to this endpoint come from OSS instances: apps.mobile_app.tasks.send_push_notification_to_fcm_relay
|
||||
"""
|
||||
|
||||
if not REQUIRED_FIELDS.issubset(request.data.keys()):
|
||||
try:
|
||||
token = request.data["token"]
|
||||
data = request.data["data"]
|
||||
except KeyError:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# registration_ids = request.data["registration_ids"]
|
||||
# data = {
|
||||
# **request.data["data"],
|
||||
# **request.data["notification"],
|
||||
# }
|
||||
message = Message(token=token, data=data, apns=get_apns(request.data))
|
||||
|
||||
# return FCMDevice.objects.send_message(Message(), False, ["registration_ids"])
|
||||
return "TODO:"
|
||||
logger.debug(f"Sending message to FCM: {message}")
|
||||
result = FCMDevice(registration_id=token).send_message(message)
|
||||
logger.debug(f"FCM response: {result}")
|
||||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
def get_apns(data):
|
||||
"""
|
||||
Create APNSConfig object from JSON payload from OSS instance.
|
||||
"""
|
||||
aps = data.get("apns", {}).get("payload", {}).get("aps", {})
|
||||
if not aps:
|
||||
return None
|
||||
|
||||
thread_id = aps.get("thread-id")
|
||||
badge = aps.get("badge")
|
||||
|
||||
alert = aps.get("alert")
|
||||
if isinstance(alert, dict):
|
||||
alert = ApsAlert(**alert)
|
||||
|
||||
sound = aps.get("sound")
|
||||
if isinstance(sound, dict):
|
||||
sound = CriticalSound(**sound)
|
||||
|
||||
# remove all keys from "aps" so it can be used for custom_data
|
||||
for key in ["thread-id", "badge", "alert", "sound"]:
|
||||
aps.pop(key, None)
|
||||
|
||||
return APNSConfig(
|
||||
payload=APNSPayload(
|
||||
aps=Aps(
|
||||
thread_id=thread_id,
|
||||
badge=badge,
|
||||
alert=alert,
|
||||
sound=sound,
|
||||
custom_data=aps,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from celery.utils.log import get_task_logger
|
||||
from django.conf import settings
|
||||
from fcm_django.models import FCMDevice
|
||||
|
|
@ -6,10 +10,12 @@ from firebase_admin.messaging import APNSConfig, APNSPayload, Aps, ApsAlert, Cri
|
|||
from apps.alerts.models import AlertGroup
|
||||
from apps.mobile_app.alert_rendering import get_push_notification_message
|
||||
from apps.user_management.models import User
|
||||
from common.api_helpers.utils import create_engine_url
|
||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||
|
||||
MAX_RETRIES = 1 if settings.DEBUG else 10
|
||||
logger = get_task_logger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@shared_dedicated_queue_retry_task(autoretry_for=(Exception,), retry_backoff=True, max_retries=MAX_RETRIES)
|
||||
|
|
@ -71,8 +77,6 @@ def notify_user_async(user_pk, alert_group_pk, notification_policy_pk, critical)
|
|||
|
||||
alert_body = f"Status: {status_verbose}, alerts: {alerts_count_str}"
|
||||
|
||||
# TODO: we should update this to check if FCM_RELAY is set and conditionally make a call here..
|
||||
|
||||
message = Message(
|
||||
token=device_to_notify.registration_id,
|
||||
data={
|
||||
|
|
@ -109,10 +113,25 @@ def notify_user_async(user_pk, alert_group_pk, notification_policy_pk, critical)
|
|||
),
|
||||
)
|
||||
|
||||
logger.info(f"Sending push notification with message: {message}; thread-id: {thread_id};")
|
||||
logger.debug(f"Sending push notification with message: {message}; thread-id: {thread_id};")
|
||||
|
||||
fcm_response = device_to_notify.send_message(message)
|
||||
if settings.LICENSE == settings.OPEN_SOURCE_LICENSE_NAME:
|
||||
response = send_push_notification_to_fcm_relay(message)
|
||||
logger.debug(f"FCM relay response: {response}")
|
||||
else:
|
||||
response = device_to_notify.send_message(message)
|
||||
# NOTE: we may want to further handle the response from FCM, but for now lets simply log it out
|
||||
# https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream
|
||||
logger.debug(f"FCM response: {response}")
|
||||
|
||||
# NOTE: we may want to further handle the response from FCM, but for now lets simply log it out
|
||||
# https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream
|
||||
logger.info(f"FCM response was: {fcm_response}")
|
||||
|
||||
def send_push_notification_to_fcm_relay(message):
|
||||
"""
|
||||
Send push notification to FCM relay on cloud instance: apps.mobile_app.fcm_relay.FCMRelayView
|
||||
"""
|
||||
url = create_engine_url("mobile_app/v1/fcm_relay", override_base=settings.GRAFANA_CLOUD_ONCALL_API_URL)
|
||||
|
||||
response = requests.post(url, json=json.loads(str(message)))
|
||||
response.raise_for_status()
|
||||
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from apps.alerts.tasks import disable_maintenance
|
|||
from apps.slack.utils import post_message_to_channel
|
||||
from apps.user_management.subscription_strategy import FreePublicBetaSubscriptionStrategy
|
||||
from common.insight_log import ChatOpsEvent, ChatOpsType, write_chatops_insight_log
|
||||
from common.oncall_gateway import create_oncall_connector, delete_oncall_connector_async
|
||||
from common.oncall_gateway import create_oncall_connector, delete_oncall_connector_async, delete_slack_connector_async
|
||||
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -51,6 +51,7 @@ class OrganizationQuerySet(models.QuerySet):
|
|||
return instance
|
||||
|
||||
def delete(self):
|
||||
# Be careful with deleting via queryset - it doesn't delete chatops-proxy connectors.
|
||||
self.update(deleted_at=timezone.now())
|
||||
|
||||
def hard_delete(self):
|
||||
|
|
@ -72,10 +73,12 @@ class Organization(MaintainableObject):
|
|||
self.subscription_strategy = self._get_subscription_strategy()
|
||||
|
||||
def delete(self):
|
||||
self.deleted_at = timezone.now()
|
||||
self.save(update_fields=["deleted_at"])
|
||||
if settings.FEATURE_MULTIREGION_ENABLED:
|
||||
delete_oncall_connector_async.apply_async((self.public_primary_key,))
|
||||
if self.slack_team_identity:
|
||||
delete_slack_connector_async.apply_async((self.slack_team_identity.slack_id,))
|
||||
self.deleted_at = timezone.now()
|
||||
self.save(update_fields=["deleted_at"])
|
||||
|
||||
def hard_delete(self):
|
||||
super().delete()
|
||||
|
|
|
|||
|
|
@ -12,18 +12,29 @@ logger.setLevel(logging.DEBUG)
|
|||
|
||||
|
||||
def sync_organization(organization):
|
||||
client = GrafanaAPIClient(api_url=organization.grafana_url, api_token=organization.api_token)
|
||||
grafana_api_client = GrafanaAPIClient(api_url=organization.grafana_url, api_token=organization.api_token)
|
||||
|
||||
# NOTE: checking whether or not RBAC is enabled depends on whether we are dealing with an open-source or cloud
|
||||
# stack. For Cloud we should make a call to the GCOM API, using an admin API token, and get the list of
|
||||
# feature_toggles enabled for the stack. For open-source, simply make a HEAD request to the grafana instance's API
|
||||
# and consider RBAC enabled if the list RBAC permissions endpoint returns 200. We cannot simply rely on the HEAD
|
||||
# call in cloud because if an instance is not active, the grafana gateway will still return 200 for the
|
||||
# HEAD request.
|
||||
if settings.LICENSE == settings.CLOUD_LICENSE_NAME:
|
||||
gcom_client = GcomAPIClient(settings.GRAFANA_COM_ADMIN_API_TOKEN)
|
||||
rbac_is_enabled = gcom_client.is_rbac_enabled_for_stack(organization.stack_id)
|
||||
else:
|
||||
rbac_is_enabled = grafana_api_client.is_rbac_enabled_for_organization()
|
||||
|
||||
rbac_is_enabled = client.is_rbac_enabled_for_organization()
|
||||
organization.is_rbac_permissions_enabled = rbac_is_enabled
|
||||
|
||||
_sync_instance_info(organization)
|
||||
|
||||
api_users = client.get_users(rbac_is_enabled)
|
||||
api_users = grafana_api_client.get_users(rbac_is_enabled)
|
||||
|
||||
if api_users:
|
||||
organization.api_token_status = Organization.API_TOKEN_STATUS_OK
|
||||
sync_users_and_teams(client, api_users, organization)
|
||||
sync_users_and_teams(grafana_api_client, api_users, organization)
|
||||
else:
|
||||
organization.api_token_status = Organization.API_TOKEN_STATUS_FAILED
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.test import override_settings
|
||||
|
||||
from apps.grafana_plugin.helpers.client import GcomAPIClient, GrafanaAPIClient
|
||||
from apps.user_management.models import Team, User
|
||||
from apps.user_management.sync import cleanup_organization, sync_organization
|
||||
from conftest import IS_RBAC_ENABLED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -133,7 +134,7 @@ def test_sync_organization(make_organization, make_team, make_user_for_organizat
|
|||
},
|
||||
)
|
||||
|
||||
with patch.object(GrafanaAPIClient, "is_rbac_enabled_for_organization", return_value=IS_RBAC_ENABLED):
|
||||
with patch.object(GrafanaAPIClient, "is_rbac_enabled_for_organization", return_value=False):
|
||||
with patch.object(GrafanaAPIClient, "get_users", return_value=api_users_response):
|
||||
with patch.object(GrafanaAPIClient, "get_teams", return_value=(api_teams_response, None)):
|
||||
with patch.object(GrafanaAPIClient, "get_team_members", return_value=(api_members_response, None)):
|
||||
|
|
@ -153,8 +154,40 @@ def test_sync_organization(make_organization, make_team, make_user_for_organizat
|
|||
assert team.users.count() == 1
|
||||
assert team.users.get() == user
|
||||
|
||||
# check that the rbac flag is properly set on the org
|
||||
assert organization.is_rbac_permissions_enabled == IS_RBAC_ENABLED
|
||||
|
||||
@pytest.mark.parametrize("grafana_api_response", [False, True])
|
||||
@override_settings(LICENSE=settings.OPEN_SOURCE_LICENSE_NAME)
|
||||
@pytest.mark.django_db
|
||||
def test_sync_organization_is_rbac_permissions_enabled_open_source(make_organization, grafana_api_response):
|
||||
organization = make_organization()
|
||||
|
||||
with patch.object(GrafanaAPIClient, "is_rbac_enabled_for_organization", return_value=grafana_api_response):
|
||||
with patch.object(GrafanaAPIClient, "get_users", return_value=[]):
|
||||
sync_organization(organization)
|
||||
|
||||
organization.refresh_from_db()
|
||||
assert organization.is_rbac_permissions_enabled == grafana_api_response
|
||||
|
||||
|
||||
@pytest.mark.parametrize("gcom_api_response", [False, True])
|
||||
@patch("apps.user_management.sync.GcomAPIClient")
|
||||
@override_settings(LICENSE=settings.CLOUD_LICENSE_NAME)
|
||||
@override_settings(GRAFANA_COM_ADMIN_API_TOKEN="mockedToken")
|
||||
@pytest.mark.django_db
|
||||
def test_sync_organization_is_rbac_permissions_enabled_cloud(mocked_gcom_client, make_organization, gcom_api_response):
|
||||
stack_id = 5
|
||||
organization = make_organization(stack_id=stack_id)
|
||||
|
||||
mocked_gcom_client.return_value.is_rbac_enabled_for_stack.return_value = gcom_api_response
|
||||
|
||||
with patch.object(GrafanaAPIClient, "get_users", return_value=[]):
|
||||
sync_organization(organization)
|
||||
|
||||
organization.refresh_from_db()
|
||||
|
||||
assert mocked_gcom_client.return_value.called_once_with("mockedToken")
|
||||
assert mocked_gcom_client.return_value.is_rbac_enabled_for_stack.called_once_with(stack_id)
|
||||
assert organization.is_rbac_permissions_enabled == gcom_api_response
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const SlackTab = () => {
|
|||
<VerticalGroup align="center" spacing="lg">
|
||||
<SlackNewIcon />
|
||||
<Text>
|
||||
Personal Slack connection will allow you to manage alert grouops in your connected team Internal Slack
|
||||
Personal Slack connection will allow you to manage alert groups in your connected team's Internal Slack
|
||||
workspace.
|
||||
</Text>
|
||||
<Text>To setup personal Slack click the button below, choose workspace and click Allow.</Text>
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@
|
|||
{
|
||||
"role": {
|
||||
"name": "Editor",
|
||||
"description": "Similar to the Admin role, minus the abilities to: create Integrations, create Escalation Chains, create Schedules, create Outgoing Webhooks, update ChatOps settings, update other user's settings, and update general OnCall setings.",
|
||||
"description": "Similar to the Admin role, minus the abilities to: create Integrations, create Escalation Chains, create Outgoing Webhooks, update ChatOps settings, update other user's settings, and update general OnCall setings.",
|
||||
"permissions": [
|
||||
{ "action": "plugins.app:access", "scope": "plugins:id:grafana-oncall-app" },
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ type: application
|
|||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 1.1.1
|
||||
version: 1.1.2
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
|
|
|
|||
|
|
@ -98,15 +98,19 @@ You can set up Slack connection via following variables:
|
|||
oncall:
|
||||
slack:
|
||||
enabled: true
|
||||
command: ~
|
||||
commandName: oncall
|
||||
clientId: ~
|
||||
clientSecret: ~
|
||||
apiToken: ~
|
||||
apiTokenCommon: ~
|
||||
signingSecret: ~
|
||||
existingSecret: ""
|
||||
clientIdKey: ""
|
||||
clientSecretKey: ""
|
||||
signingSecretKey: ""
|
||||
redirectHost: ~
|
||||
```
|
||||
|
||||
`oncall.slack.command` is used for changing default bot slash command,
|
||||
`oncall`. In slack, it could be called via `/<oncall.slack.command>`.
|
||||
`oncall.slack.commandName` is used for changing default bot slash command,
|
||||
`oncall`. In slack, it could be called via `/<oncall.slack.commandName>`.
|
||||
|
||||
To set up Telegram tokem and webhook url use:
|
||||
|
||||
|
|
|
|||
|
|
@ -91,8 +91,8 @@ oncall:
|
|||
slack:
|
||||
# enabled enable the Slack ChatOps integration for the Oncall Engine.
|
||||
enabled: false
|
||||
# command sets the Slack bot slash-command
|
||||
command: oncall
|
||||
# commandName sets the Slack bot slash-command
|
||||
commandName: oncall
|
||||
# clientId configures the Slack app OAuth2 client ID.
|
||||
# api.slack.com/apps/<yourApp> -> Basic Information -> App Credentials -> Client ID
|
||||
clientId: ~
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue