Skip to content

Commit

Permalink
Merge pull request #4920 from grafana/dev
Browse files Browse the repository at this point in the history
v1.9.13
  • Loading branch information
mderynck authored Aug 23, 2024
2 parents 86940ef + a25d44d commit 7d3e6f2
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 16 deletions.
10 changes: 10 additions & 0 deletions engine/apps/grafana_plugin/helpers/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -330,6 +332,14 @@ def get_service_account_token_permissions(self) -> APIClientResponse[typing.Dict
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"
Expand Down
17 changes: 15 additions & 2 deletions engine/apps/grafana_plugin/helpers/gcom.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

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__)
Expand Down Expand Up @@ -45,13 +45,20 @@ 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

allow_signup = DynamicSetting.objects.get_or_create(
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"],
Expand All @@ -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",
Expand All @@ -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}")
Expand Down
21 changes: 12 additions & 9 deletions engine/apps/grafana_plugin/tasks/sync_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 61 additions & 0 deletions engine/apps/grafana_plugin/tests/test_gcom.py
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions engine/apps/grafana_plugin/tests/test_sync.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from unittest.mock import patch
from unittest.mock import ANY, patch

import pytest
from django.conf import settings
Expand Down Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions engine/apps/grafana_plugin/tests/test_sync_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion grafana-plugin/go.mod
Original file line number Diff line number Diff line change
@@ -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

Expand Down
13 changes: 12 additions & 1 deletion grafana-plugin/src/utils/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down

0 comments on commit 7d3e6f2

Please sign in to comment.