Skip to content

Commit

Permalink
Merge pull request #4996 from grafana/dev
Browse files Browse the repository at this point in the history
Merge instructions to main
  • Loading branch information
mderynck authored Sep 6, 2024
2 parents 0feb182 + fc07a22 commit 64913ac
Show file tree
Hide file tree
Showing 243 changed files with 594 additions and 480 deletions.
41 changes: 35 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ Developer-friendly incident response with brilliant Slack integration.

## Getting Started

> [!IMPORTANT]
> These instructions are for using Grafana 11 or newer. You must enable the feature toggle for
> `externalServiceAccounts`. This is already done for the docker files and helm charts. If you are running Grafana
> separately see the Grafana documentation on how to enable this.
We prepared multiple environments:

- [production](https://grafana.com/docs/oncall/latest/open-source/#production-environment)
Expand Down Expand Up @@ -82,17 +87,41 @@ We prepared multiple environments:
docker-compose pull && docker-compose up -d
```

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_:
5. Provision the plugin (If you run Grafana outside the included docker files install the plugin before these steps):

If you are using the included docker compose file use `admin`/`admin` credentials and `localhost:3000` to
perform this task. If you have configured Grafana differently adjust your credentials and hostnames accordingly.

```text
OnCall backend URL: http://engine:8080
```bash
# Note: onCallApiUrl 'engine' and grafanaUrl 'grafana' use the name from the docker compose file. If you are
# running your grafana or oncall engine instance with another hostname adjust accordingly.
curl -X POST 'http://admin:admin@localhost:3000/api/plugins/grafana-oncall-app/settings' -H "Content-Type: application/json" -d '{"enabled":true, "jsonData":{"stackId":5, "orgId":100, "onCallApiUrl":"http://engine:8080", "grafanaUrl":"http://grafana:3000"}}'
curl -X POST 'http://admin:admin@localhost:3000/api/plugins/grafana-oncall-app/resources/plugin/install'
```

6. Enjoy! Check our [OSS docs](https://grafana.com/docs/oncall/latest/open-source/) if you want to set up
6. Start using OnCall, log in to Grafana with credentials
as defined above: `admin`/`admin`

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

## Troubleshooting

Here are some API calls that can be made to help if you are having difficulty connecting Grafana and OnCall.
(Modify parameters to match your credentials and environment)

```bash
# Use this to get more information about the connection between Grafana and OnCall
curl -X GET 'http://admin:admin@localhost:3000/api/plugins/grafana-oncall-app/resources/plugin/status'
```

```bash
# If you added a user or changed permissions and don't see it show up in OnCall you can manually trigger sync.
# Note: This is called automatically when the app is loaded (page load/refresh) but there is a 5 min timeout so
# that it does not generate unnecessary activity.
curl -X POST 'http://admin:admin@localhost:3000/api/plugins/grafana-oncall-app/resources/plugin/sync'
```

## Update version

To update your Grafana OnCall hobby environment:
Expand Down
1 change: 1 addition & 0 deletions docker-compose-mysql-rabbitmq.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ services:
GF_DATABASE_HOST: ${MYSQL_HOST:-mysql}
GF_DATABASE_USER: ${MYSQL_USER:-root}
GF_DATABASE_PASSWORD: ${MYSQL_PASSWORD:?err}
GF_FEATURE_TOGGLES_ENABLE: externalServiceAccounts
GF_SECURITY_ADMIN_USER: ${GRAFANA_USER:-admin}
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:-admin}
GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: grafana-oncall-app
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ services:
ports:
- "3000:3000"
environment:
GF_FEATURE_TOGGLES_ENABLE: externalServiceAccounts
GF_SECURITY_ADMIN_USER: ${GRAFANA_USER:-admin}
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:-admin}
GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: grafana-oncall-app
Expand Down
2 changes: 1 addition & 1 deletion engine/apps/auth_token/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def authenticate(self, request):
auth = get_authorization_header(request).decode("utf-8")
user, auth_token = self.authenticate_credentials(auth)

if not user_is_authorized(user, [RBACPermission.Permissions.API_KEYS_WRITE]):
if not user.is_active or not user_is_authorized(user, [RBACPermission.Permissions.API_KEYS_WRITE]):
raise exceptions.AuthenticationFailed(
"Only users with Admin permissions are allowed to perform this action."
)
Expand Down
20 changes: 20 additions & 0 deletions engine/apps/auth_token/tests/test_plugin_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,26 @@ def test_plugin_authentication_fail(authorization, instance_context):
PluginAuthentication().authenticate(request)


@pytest.mark.django_db
def test_plugin_authentication_inactive_user(make_organization, make_user, make_token_for_organization):
organization = make_organization(stack_id=42, org_id=24)
token, token_string = make_token_for_organization(organization)
user = make_user(organization=organization, user_id=12)
# user is set to inactive if deleted via queryset (ie. during sync)
user.is_active = False
user.save()

headers = {
"HTTP_AUTHORIZATION": token_string,
"HTTP_X-Instance-Context": INSTANCE_CONTEXT,
"HTTP_X-Grafana-Context": '{"UserId": 12}',
}
request = APIRequestFactory().get("/", **headers)

with pytest.raises(AuthenticationFailed):
PluginAuthentication().authenticate(request)


@pytest.mark.django_db
def test_plugin_authentication_gcom_setup_new_user(make_organization):
# Setting gcom_token_org_last_time_synced to now, so it doesn't try to sync with gcom
Expand Down
2 changes: 1 addition & 1 deletion engine/apps/mobile_app/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class MobileAppAuthTokenAuthentication(BaseAuthentication):
def authenticate(self, request) -> Optional[Tuple[User, MobileAppAuthToken]]:
auth = get_authorization_header(request).decode("utf-8")
user, auth_token = self.authenticate_credentials(auth)
if user is None:
if user is None or not user.is_active:
return None
return user, auth_token

Expand Down
15 changes: 15 additions & 0 deletions engine/apps/mobile_app/tests/test_mobile_app_auth_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rest_framework.test import APIClient

from apps.mobile_app.models import MobileAppAuthToken
from apps.user_management.models import User


@pytest.mark.django_db
Expand Down Expand Up @@ -76,3 +77,17 @@ def test_mobile_app_auth_token(

response = client.get(url, HTTP_AUTHORIZATION=verification_token)
assert response.status_code == status.HTTP_404_NOT_FOUND


@pytest.mark.django_db
def test_mobile_app_auth_token_deleted_user(
make_organization_and_user_with_mobile_app_auth_token,
):
_, user, auth_token = make_organization_and_user_with_mobile_app_auth_token()
# user is deleted via queryset (ie. setting it to inactive, during sync)
User.objects.filter(id=user.id).delete()

client = APIClient()
url = reverse("api-internal:alertgroup-list")
response = client.get(url, HTTP_AUTHORIZATION=auth_token)
assert response.status_code == status.HTTP_403_FORBIDDEN
14 changes: 14 additions & 0 deletions engine/apps/public_api/tests/test_alert_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,20 @@ def test_get_alert_groups(alert_group_public_api_setup):
assert response.json() == expected_response


@pytest.mark.django_db
def test_get_alert_groups_inactive_user(make_organization_and_user_with_token):
_, user, token = make_organization_and_user_with_token()
# user is set to inactive if deleted via queryset (ie. during sync)
user.is_active = False
user.save()

client = APIClient()
url = reverse("api-public:alert_groups-list")
response = client.get(url, format="json", HTTP_AUTHORIZATION=token)

assert response.status_code == status.HTTP_403_FORBIDDEN


@pytest.mark.django_db
def test_get_alert_groups_include_labels(alert_group_public_api_setup, make_alert_group_label_association):
token, _, _, _ = alert_group_public_api_setup
Expand Down
7 changes: 1 addition & 6 deletions grafana-plugin/.bra.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,14 @@
[run]
init_cmds = [
["mage", "-v", "build:debug"],
["mage", "-v" , "reloadPlugin"],
]
watch_all = true
follow_symlinks = false
ignore = [".git", "node_modules", "dist"]
ignore_files = ["mage_output_file.go"]
watch_dirs = [
"pkg",
# "src",
]
watch_dirs = ["pkg"]
watch_exts = [".go", ".json"]
build_delay = 2000
cmds = [
["mage", "-v", "build:debug"],
["mage", "-v" , "reloadPlugin"],
]
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PLUGIN_CONFIG } from 'utils/consts';
import { PLUGIN_CONFIG } from 'helpers/consts';

import { test, expect } from '../fixtures';
import { goToGrafanaPage } from '../utils/navigation';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { waitInMs } from 'utils/async';
import { waitInMs } from 'helpers/async';

import { test, expect, Page } from '../fixtures';
import { OrgRole, isGrafanaVersionLowerThan } from '../utils/constants';
Expand Down
3 changes: 2 additions & 1 deletion grafana-plugin/e2e-tests/schedules/scheduleView.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HTML_ID } from 'helpers/DOM';

import { scheduleViewToDaysInOneRow } from 'models/schedule/schedule.helpers';
import { ScheduleView } from 'models/schedule/schedule.types';
import { HTML_ID } from 'utils/DOM';

import { expect, Page, test } from '../fixtures';
import { isGrafanaVersionLowerThan } from '../utils/constants';
Expand Down
2 changes: 1 addition & 1 deletion grafana-plugin/e2e-tests/utils/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { KeyValue } from '@grafana/data';
import type { Page } from '@playwright/test';
import { getPluginId } from 'helpers/consts';
import qs from 'query-string';

import { getPluginId } from 'utils/consts';

import { BASE_URL } from './constants';

Expand Down
2 changes: 1 addition & 1 deletion grafana-plugin/go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/grafana-labs/grafana-oncall-app
module github.com/grafana/grafana-oncall-app

go 1.21.5

Expand Down
4 changes: 2 additions & 2 deletions grafana-plugin/pkg/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package main
import (
"os"

"github.com/grafana-labs/grafana-oncall-app/pkg/plugin"
"github.com/grafana/grafana-oncall-app/pkg/plugin"
"github.com/grafana/grafana-plugin-sdk-go/backend/app"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
)
Expand All @@ -16,7 +16,7 @@ func main() {
// argument. This factory will be automatically called on incoming request
// from Grafana to create different instances of `App` (per plugin
// ID).
if err := app.Manage("grafana-oncall-app", plugin.NewApp, app.ManageOpts{}); err != nil {
if err := app.Manage("grafana-oncall-app", plugin.NewInstance, app.ManageOpts{}); err != nil {
log.DefaultLogger.Error(err.Error())
os.Exit(1)
}
Expand Down
27 changes: 20 additions & 7 deletions grafana-plugin/pkg/plugin/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,9 @@ type App struct {
}

// NewApp creates a new example *App instance.
func NewApp(ctx context.Context, settings backend.AppInstanceSettings) (instancemgmt.Instance, error) {
func NewApp(ctx context.Context, settings backend.AppInstanceSettings) (*App, error) {
var app App

// Use a httpadapter (provided by the SDK) for resource calls. This allows us
// to use a *http.ServeMux for resource calls, so we can map multiple routes
// to CallResource without having to implement extra logic.
mux := http.NewServeMux()
app.registerRoutes(mux)
app.CallResourceHandler = httpadapter.New(mux)
app.OnCallSyncCache = &OnCallSyncCache{}
app.OnCallSettingsCache = &OnCallSettingsCache{}
app.OnCallUserCache = NewOnCallUserCache()
Expand All @@ -66,6 +60,25 @@ func NewApp(ctx context.Context, settings backend.AppInstanceSettings) (instance
return &app, nil
}

// NewInstance creates a new example *Instance instance.
func NewInstance(ctx context.Context, settings backend.AppInstanceSettings) (instancemgmt.Instance, error) {
app, err := NewApp(ctx, settings)

if err != nil {
log.DefaultLogger.Error("Error creating new app", "error", err)
return nil, err
}

// Use a httpadapter (provided by the SDK) for resource calls. This allows us
// to use a *http.ServeMux for resource calls, so we can map multiple routes
// to CallResource without having to implement extra logic.
mux := http.NewServeMux()
app.registerRoutes(mux)
app.CallResourceHandler = httpadapter.New(mux)

return app, nil
}

// Dispose here tells plugin SDK that plugin wants to clean up resources when a new instance
// created.
func (a *App) Dispose() {
Expand Down
5 changes: 3 additions & 2 deletions grafana-plugin/pkg/plugin/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package plugin

import (
"encoding/json"
"net/http"

"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
"net/http"
)

type OnCallDebugStats struct {
Expand Down Expand Up @@ -47,7 +48,7 @@ func (a *App) handleDebugSync(w http.ResponseWriter, req *http.Request) {
return
}

onCallSync, err := a.GetSyncData(req.Context(), onCallPluginSettings)
onCallSync, err := a.GetSyncData(onCallPluginSettings)
if err != nil {
log.DefaultLogger.Error("Error getting sync data", "error", err)
return
Expand Down
2 changes: 1 addition & 1 deletion grafana-plugin/pkg/plugin/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (a *App) handleInstall(w http.ResponseWriter, req *http.Request) {
return
}

onCallSync, err := a.GetSyncData(req.Context(), onCallPluginSettings)
onCallSync, err := a.GetSyncData(onCallPluginSettings)
if err != nil {
log.DefaultLogger.Error("Error getting sync data", "error", err)
return
Expand Down
10 changes: 5 additions & 5 deletions grafana-plugin/pkg/plugin/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func afterRequest(handler http.Handler, afterFunc func(*responseWriter, *http.Re
})
}

func (a *App) handleInternalApi(w http.ResponseWriter, req *http.Request) {
func (a *App) HandleInternalApi(w http.ResponseWriter, req *http.Request) {
a.ProxyRequestToOnCall(w, req, "api/internal/v1/")
}

Expand Down Expand Up @@ -121,10 +121,10 @@ func (a *App) handleLegacyInstall(w *responseWriter, req *http.Request) {
// registerRoutes takes a *http.ServeMux and registers some HTTP handlers.
func (a *App) registerRoutes(mux *http.ServeMux) {
mux.HandleFunc("/plugin/install", a.handleInstall)
mux.HandleFunc("/plugin/status", a.handleStatus)
mux.HandleFunc("/plugin/sync", a.handleSync)
mux.HandleFunc("/plugin/status", a.HandleStatus)
mux.HandleFunc("/plugin/sync", a.HandleSync)

mux.Handle("/plugin/self-hosted/install", afterRequest(http.HandlerFunc(a.handleInternalApi), a.handleLegacyInstall))
mux.Handle("/plugin/self-hosted/install", afterRequest(http.HandlerFunc(a.HandleInternalApi), a.handleLegacyInstall))

// Disable debug endpoints
//mux.HandleFunc("/debug/user", a.handleDebugUser)
Expand All @@ -134,5 +134,5 @@ func (a *App) registerRoutes(mux *http.ServeMux) {
//mux.HandleFunc("/debug/stats", a.handleDebugStats)
//mux.HandleFunc("/debug/unlock", a.handleDebugUnlock)

mux.HandleFunc("/", a.handleInternalApi)
mux.HandleFunc("/", a.HandleInternalApi)
}
5 changes: 3 additions & 2 deletions grafana-plugin/pkg/plugin/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package plugin
import (
"bytes"
"context"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"testing"

"github.com/grafana/grafana-plugin-sdk-go/backend"
)

// mockCallResourceResponseSender implements backend.CallResourceResponseSender
Expand All @@ -23,7 +24,7 @@ func (s *mockCallResourceResponseSender) Send(response *backend.CallResourceResp
// This ensures the httpadapter for CallResource works correctly.
func TestCallResource(t *testing.T) {
// Initialize app
inst, err := NewApp(context.Background(), backend.AppInstanceSettings{})
inst, err := NewInstance(context.Background(), backend.AppInstanceSettings{})
if err != nil {
t.Fatalf("new app: %s", err)
}
Expand Down
Loading

0 comments on commit 64913ac

Please sign in to comment.