Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge instructions to main #4996

Merged
merged 5 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading