Merge pull request #4920 from grafana/dev

v1.9.13
This commit is contained in:
Michael Derynck 2024-08-23 16:18:13 -06:00 committed by GitHub
commit 7d3e6f2a25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 143 additions and 16 deletions

View file

@ -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"

View file

@ -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}")

View file

@ -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")

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 =