commit
7d3e6f2a25
8 changed files with 143 additions and 16 deletions
|
|
@ -166,6 +166,8 @@ class GrafanaAPIClient(APIClient):
|
|||
|
||||
USER_PERMISSION_ENDPOINT = f"api/access-control/users/permissions/search?actionPrefix={ACTION_PREFIX}"
|
||||
|
||||
MIN_GRAFANA_TOKEN_LENGTH = 16
|
||||
|
||||
class Types:
|
||||
class _BaseGrafanaAPIResponse(typing.TypedDict):
|
||||
totalCount: int
|
||||
|
|
@ -330,6 +332,14 @@ class GrafanaAPIClient(APIClient):
|
|||
def sync(self) -> APIClientResponse:
|
||||
return self.api_post("api/plugins/grafana-oncall-app/resources/plugin/sync")
|
||||
|
||||
@staticmethod
|
||||
def validate_grafana_token_format(grafana_token: str) -> bool:
|
||||
if not grafana_token or not isinstance(grafana_token, str):
|
||||
return False
|
||||
if len(grafana_token) < GrafanaAPIClient.MIN_GRAFANA_TOKEN_LENGTH:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class GcomAPIClient(APIClient):
|
||||
ACTIVE_INSTANCE_QUERY = "instances?status=active"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from django.utils import timezone
|
|||
|
||||
from apps.auth_token.exceptions import InvalidToken
|
||||
from apps.auth_token.models import PluginAuthToken
|
||||
from apps.grafana_plugin.helpers import GcomAPIClient
|
||||
from apps.grafana_plugin.helpers import GcomAPIClient, GrafanaAPIClient
|
||||
from apps.user_management.models import Organization
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -45,6 +45,8 @@ def check_gcom_permission(token_string: str, context) -> GcomToken:
|
|||
if not instance_info or str(instance_info["orgId"]) != org_id:
|
||||
raise InvalidToken
|
||||
|
||||
grafana_token_format_is_valid = GrafanaAPIClient.validate_grafana_token_format(grafana_token)
|
||||
|
||||
if not organization:
|
||||
from apps.base.models import DynamicSetting
|
||||
|
||||
|
|
@ -52,6 +54,11 @@ def check_gcom_permission(token_string: str, context) -> GcomToken:
|
|||
name="allow_plugin_organization_signup", defaults={"boolean_value": True}
|
||||
)[0].boolean_value
|
||||
if allow_signup:
|
||||
if not grafana_token_format_is_valid:
|
||||
logger.debug(
|
||||
f"grafana token sent when creating stack_id={stack_id} was invalid format. api_token will still be written to DB"
|
||||
)
|
||||
|
||||
# Get org from db or create a new one
|
||||
organization, _ = Organization.objects.get_or_create(
|
||||
stack_id=instance_info["id"],
|
||||
|
|
@ -74,8 +81,13 @@ def check_gcom_permission(token_string: str, context) -> GcomToken:
|
|||
organization.grafana_url = instance_info["url"]
|
||||
organization.cluster_slug = instance_info["clusterSlug"]
|
||||
organization.gcom_token = token_string
|
||||
organization.api_token = grafana_token
|
||||
organization.gcom_token_org_last_time_synced = timezone.now()
|
||||
if not grafana_token_format_is_valid:
|
||||
logger.debug(
|
||||
f"grafana token sent when updating stack_id={stack_id} was invalid, api_token in DB will be unchanged"
|
||||
)
|
||||
else:
|
||||
organization.api_token = grafana_token
|
||||
organization.save(
|
||||
update_fields=[
|
||||
"stack_slug",
|
||||
|
|
@ -86,6 +98,7 @@ def check_gcom_permission(token_string: str, context) -> GcomToken:
|
|||
"gcom_token",
|
||||
"gcom_token_org_last_time_synced",
|
||||
"cluster_slug",
|
||||
"api_token",
|
||||
]
|
||||
)
|
||||
logger.debug(f"Finish authenticate by making request to gcom api for org={org_id}, stack_id={stack_id}")
|
||||
|
|
|
|||
|
|
@ -41,14 +41,17 @@ def sync_organizations_v2(org_ids=None):
|
|||
orgs_per_second = math.ceil(len(organization_qs) / SYNC_PERIOD.seconds)
|
||||
logger.info(f"Syncing {len(organization_qs)} organizations @ {orgs_per_second} per 1s pause")
|
||||
for idx, org in enumerate(organization_qs):
|
||||
client = GrafanaAPIClient(api_url=org.grafana_url, api_token=org.api_token)
|
||||
_, status = client.sync()
|
||||
if status["status_code"] != 200:
|
||||
logger.error(
|
||||
f"Failed to request sync stack_slug={org.stack_slug} status_code={status['status_code']} url={status['url']} message={status['message']}"
|
||||
)
|
||||
if idx % orgs_per_second == 0:
|
||||
logger.info(f"Sleep 1s after {idx + 1} organizations processed")
|
||||
sleep(1)
|
||||
if GrafanaAPIClient.validate_grafana_token_format(org.api_token):
|
||||
client = GrafanaAPIClient(api_url=org.grafana_url, api_token=org.api_token)
|
||||
_, status = client.sync()
|
||||
if status["status_code"] != 200:
|
||||
logger.error(
|
||||
f"Failed to request sync stack_slug={org.stack_slug} status_code={status['status_code']} url={status['url']} message={status['message']}"
|
||||
)
|
||||
if idx % orgs_per_second == 0:
|
||||
logger.info(f"Sleep 1s after {idx + 1} organizations processed")
|
||||
sleep(1)
|
||||
else:
|
||||
logger.info(f"Skipping stack_slug={org.stack_slug}, api_token format is invalid or not set")
|
||||
else:
|
||||
logger.info(f"Issuing sync requests already in progress lock_id={lock_id}, check slow outgoing requests")
|
||||
|
|
|
|||
61
engine/apps/grafana_plugin/tests/test_gcom.py
Normal file
61
engine/apps/grafana_plugin/tests/test_gcom.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from apps.grafana_plugin.helpers.gcom import check_gcom_permission
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"api_token, api_token_updated",
|
||||
[
|
||||
("glsa_abcdefghijklmnopqrztuvwxyz", True),
|
||||
("abcdefghijklmnopqrztuvwxyz", True),
|
||||
("abc", False),
|
||||
("", False),
|
||||
("<no_value>", False),
|
||||
(None, False),
|
||||
(24, False),
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_check_gcom_permission_updates_fields(make_organization, api_token, api_token_updated):
|
||||
gcom_token = "gcom:test_token"
|
||||
broken_token = "broken_token"
|
||||
instance_info = {
|
||||
"id": 324534,
|
||||
"slug": "testinstance",
|
||||
"url": "http://example.com",
|
||||
"orgId": 5671,
|
||||
"orgSlug": "testorg",
|
||||
"orgName": "Test Org",
|
||||
"regionSlug": "us",
|
||||
"clusterSlug": "us-test",
|
||||
}
|
||||
context = {
|
||||
"stack_id": str(instance_info["id"]),
|
||||
"org_id": str(instance_info["orgId"]),
|
||||
"grafana_token": api_token,
|
||||
}
|
||||
|
||||
org = make_organization(stack_id=instance_info["id"], org_id=instance_info["orgId"], api_token=broken_token)
|
||||
last_time_gcom_synced = org.gcom_token_org_last_time_synced
|
||||
|
||||
with patch(
|
||||
"apps.grafana_plugin.helpers.GcomAPIClient.get_instance_info",
|
||||
return_value=instance_info,
|
||||
) as mock_instance_info:
|
||||
check_gcom_permission(gcom_token, context)
|
||||
mock_instance_info.assert_called()
|
||||
|
||||
org.refresh_from_db()
|
||||
assert org.stack_id == instance_info["id"]
|
||||
assert org.stack_slug == instance_info["slug"]
|
||||
assert org.grafana_url == instance_info["url"]
|
||||
assert org.org_id == instance_info["orgId"]
|
||||
assert org.org_slug == instance_info["orgSlug"]
|
||||
assert org.org_title == instance_info["orgName"]
|
||||
assert org.region_slug == instance_info["regionSlug"]
|
||||
assert org.cluster_slug == instance_info["clusterSlug"]
|
||||
assert org.api_token == api_token if api_token_updated else broken_token
|
||||
assert org.gcom_token == gcom_token
|
||||
assert org.gcom_token_org_last_time_synced != last_time_gcom_synced
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from unittest.mock import patch
|
||||
from unittest.mock import ANY, patch
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
|
|
@ -58,8 +58,8 @@ def test_start_sync_organization_filter(make_organization):
|
|||
with patch("apps.grafana_plugin.tasks.sync.sync_organization_async.apply_async") as mock_sync:
|
||||
start_sync_organizations()
|
||||
assert mock_sync.call_count == 2
|
||||
mock_sync.assert_any_call((org2.pk,), countdown=0)
|
||||
mock_sync.assert_any_call((org3.pk,), countdown=1)
|
||||
mock_sync.assert_any_call((org2.pk,), countdown=ANY)
|
||||
mock_sync.assert_any_call((org3.pk,), countdown=ANY)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from rest_framework import status
|
|||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.api.permissions import LegacyAccessControlRole
|
||||
from apps.grafana_plugin.tasks import sync_organizations_v2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -44,3 +45,31 @@ def test_invalid_auth(make_organization_and_user_with_plugin_token, make_user_au
|
|||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert not mock_sync.called
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"api_token, sync_called",
|
||||
[
|
||||
("", False),
|
||||
("abc", False),
|
||||
("glsa_abcdefghijklmnopqrstuvwxyz", True),
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_skip_org_without_api_token(make_organization, api_token, sync_called):
|
||||
organization = make_organization(api_token=api_token)
|
||||
|
||||
with patch(
|
||||
"apps.grafana_plugin.helpers.GrafanaAPIClient.sync",
|
||||
return_value=(
|
||||
None,
|
||||
{
|
||||
"url": "",
|
||||
"connected": True,
|
||||
"status_code": status.HTTP_200_OK,
|
||||
"message": "",
|
||||
},
|
||||
),
|
||||
) as mock_sync:
|
||||
sync_organizations_v2(org_ids=[organization.id])
|
||||
assert mock_sync.called == sync_called
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
module github.com/grafana-labs/grafana-oncall-app
|
||||
|
||||
go 1.21
|
||||
go 1.21.5
|
||||
|
||||
require github.com/grafana/grafana-plugin-sdk-go v0.228.0
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,18 @@ export const getProcessEnvVarSafely = (name: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const getOnCallApiPath = (subpath = '') => `/api/plugins/${PLUGIN_ID}/resources${subpath}`;
|
||||
const getGrafanaSubUrl = () => {
|
||||
try {
|
||||
return window.grafanaBootData.settings.appSubUrl || '';
|
||||
} catch (_err) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const getOnCallApiPath = (subpath = '') => {
|
||||
// We need to consider the grafanaSubUrl in case Grafana is served from subpath, e.g. http://localhost:3000/grafana
|
||||
return `${getGrafanaSubUrl()}/api/plugins/${PLUGIN_ID}/resources${subpath}`;
|
||||
};
|
||||
|
||||
// Faro
|
||||
export const FARO_ENDPOINT_DEV =
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue