[IRM] - changes to get incident working in IRM plugin (#4987)

Required for https://github.com/grafana/irm/pull/69
This commit is contained in:
Joey Orlando 2024-09-05 13:50:08 -04:00 committed by GitHub
parent df6f7af8a5
commit a0a5482a85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 65 additions and 49 deletions

View file

@ -4,19 +4,14 @@
[run] [run]
init_cmds = [ init_cmds = [
["mage", "-v", "build:debug"], ["mage", "-v", "build:debug"],
["mage", "-v" , "reloadPlugin"],
] ]
watch_all = true watch_all = true
follow_symlinks = false follow_symlinks = false
ignore = [".git", "node_modules", "dist"] ignore = [".git", "node_modules", "dist"]
ignore_files = ["mage_output_file.go"] ignore_files = ["mage_output_file.go"]
watch_dirs = [ watch_dirs = ["pkg"]
"pkg",
# "src",
]
watch_exts = [".go", ".json"] watch_exts = [".go", ".json"]
build_delay = 2000 build_delay = 2000
cmds = [ cmds = [
["mage", "-v", "build:debug"], ["mage", "-v", "build:debug"],
["mage", "-v" , "reloadPlugin"],
] ]

View file

@ -1,4 +1,4 @@
module github.com/grafana-labs/grafana-oncall-app module github.com/grafana/grafana-oncall-app
go 1.21.5 go 1.21.5

View file

@ -3,7 +3,7 @@ package main
import ( import (
"os" "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/app"
"github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/backend/log"
) )
@ -16,7 +16,7 @@ func main() {
// argument. This factory will be automatically called on incoming request // argument. This factory will be automatically called on incoming request
// from Grafana to create different instances of `App` (per plugin // from Grafana to create different instances of `App` (per plugin
// ID). // 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()) log.DefaultLogger.Error(err.Error())
os.Exit(1) os.Exit(1)
} }

View file

@ -38,15 +38,9 @@ type App struct {
} }
// NewApp creates a new example *App instance. // 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 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.OnCallSyncCache = &OnCallSyncCache{}
app.OnCallSettingsCache = &OnCallSettingsCache{} app.OnCallSettingsCache = &OnCallSettingsCache{}
app.OnCallUserCache = NewOnCallUserCache() app.OnCallUserCache = NewOnCallUserCache()
@ -66,6 +60,25 @@ func NewApp(ctx context.Context, settings backend.AppInstanceSettings) (instance
return &app, nil 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 // Dispose here tells plugin SDK that plugin wants to clean up resources when a new instance
// created. // created.
func (a *App) Dispose() { func (a *App) Dispose() {

View file

@ -2,9 +2,10 @@ package plugin
import ( import (
"encoding/json" "encoding/json"
"net/http"
"github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
"net/http"
) )
type OnCallDebugStats struct { type OnCallDebugStats struct {
@ -47,7 +48,7 @@ func (a *App) handleDebugSync(w http.ResponseWriter, req *http.Request) {
return return
} }
onCallSync, err := a.GetSyncData(req.Context(), onCallPluginSettings) onCallSync, err := a.GetSyncData(onCallPluginSettings)
if err != nil { if err != nil {
log.DefaultLogger.Error("Error getting sync data", "error", err) log.DefaultLogger.Error("Error getting sync data", "error", err)
return return

View file

@ -41,7 +41,7 @@ func (a *App) handleInstall(w http.ResponseWriter, req *http.Request) {
return return
} }
onCallSync, err := a.GetSyncData(req.Context(), onCallPluginSettings) onCallSync, err := a.GetSyncData(onCallPluginSettings)
if err != nil { if err != nil {
log.DefaultLogger.Error("Error getting sync data", "error", err) log.DefaultLogger.Error("Error getting sync data", "error", err)
return return

View file

@ -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/") a.ProxyRequestToOnCall(w, req, "api/internal/v1/")
} }
@ -121,10 +121,10 @@ func (a *App) handleLegacyInstall(w *responseWriter, req *http.Request) {
// registerRoutes takes a *http.ServeMux and registers some HTTP handlers. // registerRoutes takes a *http.ServeMux and registers some HTTP handlers.
func (a *App) registerRoutes(mux *http.ServeMux) { func (a *App) registerRoutes(mux *http.ServeMux) {
mux.HandleFunc("/plugin/install", a.handleInstall) mux.HandleFunc("/plugin/install", a.handleInstall)
mux.HandleFunc("/plugin/status", a.handleStatus) mux.HandleFunc("/plugin/status", a.HandleStatus)
mux.HandleFunc("/plugin/sync", a.handleSync) 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 // Disable debug endpoints
//mux.HandleFunc("/debug/user", a.handleDebugUser) //mux.HandleFunc("/debug/user", a.handleDebugUser)
@ -134,5 +134,5 @@ func (a *App) registerRoutes(mux *http.ServeMux) {
//mux.HandleFunc("/debug/stats", a.handleDebugStats) //mux.HandleFunc("/debug/stats", a.handleDebugStats)
//mux.HandleFunc("/debug/unlock", a.handleDebugUnlock) //mux.HandleFunc("/debug/unlock", a.handleDebugUnlock)
mux.HandleFunc("/", a.handleInternalApi) mux.HandleFunc("/", a.HandleInternalApi)
} }

View file

@ -3,8 +3,9 @@ package plugin
import ( import (
"bytes" "bytes"
"context" "context"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"testing" "testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
) )
// mockCallResourceResponseSender implements backend.CallResourceResponseSender // mockCallResourceResponseSender implements backend.CallResourceResponseSender
@ -23,7 +24,7 @@ func (s *mockCallResourceResponseSender) Send(response *backend.CallResourceResp
// This ensures the httpadapter for CallResource works correctly. // This ensures the httpadapter for CallResource works correctly.
func TestCallResource(t *testing.T) { func TestCallResource(t *testing.T) {
// Initialize app // Initialize app
inst, err := NewApp(context.Background(), backend.AppInstanceSettings{}) inst, err := NewInstance(context.Background(), backend.AppInstanceSettings{})
if err != nil { if err != nil {
t.Fatalf("new app: %s", err) t.Fatalf("new app: %s", err)
} }

View file

@ -317,32 +317,29 @@ func (a *App) SaveOnCallSettings(settings *OnCallPluginSettings) error {
return nil return nil
} }
func (a *App) GetSyncData(ctx context.Context, settings *OnCallPluginSettings) (*OnCallSync, error) { func (a *App) GetSyncData(pluginSettings *OnCallPluginSettings) (*OnCallSync, error) {
var err error
startGetSyncData := time.Now() startGetSyncData := time.Now()
defer func() { defer func() {
elapsed := time.Since(startGetSyncData) elapsed := time.Since(startGetSyncData)
log.DefaultLogger.Info("GetSyncData", "time", elapsed.Milliseconds()) log.DefaultLogger.Info("GetSyncData", "time", elapsed.Milliseconds())
}() }()
onCallPluginSettings, err := a.OnCallSettingsFromContext(ctx)
if err != nil {
return nil, fmt.Errorf("error getting settings from context = %v", err)
}
onCallSync := OnCallSync{ onCallSync := OnCallSync{
Settings: *settings, Settings: *pluginSettings,
} }
onCallSync.Users, err = a.GetAllUsersWithPermissions(onCallPluginSettings) onCallSync.Users, err = a.GetAllUsersWithPermissions(pluginSettings)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting users = %v", err) return nil, fmt.Errorf("error getting users = %v", err)
} }
onCallSync.Teams, err = a.GetAllTeams(onCallPluginSettings) onCallSync.Teams, err = a.GetAllTeams(pluginSettings)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting teams = %v", err) return nil, fmt.Errorf("error getting teams = %v", err)
} }
teamMembers, err := a.GetAllTeamMembers(onCallPluginSettings, onCallSync.Teams) teamMembers, err := a.GetAllTeamMembers(pluginSettings, onCallSync.Teams)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting team members = %v", err) return nil, fmt.Errorf("error getting team members = %v", err)
} }

View file

@ -244,7 +244,7 @@ func (a *App) ValidateOnCallStatus(ctx context.Context, settings *OnCallPluginSe
return &status, nil return &status, nil
} }
func (a *App) handleStatus(w http.ResponseWriter, req *http.Request) { func (a *App) HandleStatus(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet { if req.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return return

View file

@ -7,13 +7,14 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"sync" "sync"
"time" "time"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
) )
type OnCallSyncCache struct { type OnCallSyncCache struct {
@ -38,7 +39,7 @@ func (oc *OnCallSyncCache) UnlockAfterDelay(delay time.Duration) {
}) })
} }
func (a *App) handleSync(w http.ResponseWriter, req *http.Request) { func (a *App) HandleSync(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost { if req.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return return
@ -122,7 +123,7 @@ func (a *App) makeSyncRequest(ctx context.Context, forceSend bool) error {
return fmt.Errorf("error getting settings from context: %v ", err) return fmt.Errorf("error getting settings from context: %v ", err)
} }
onCallSync, err := a.GetSyncData(ctx, onCallPluginSettings) onCallSync, err := a.GetSyncData(onCallPluginSettings)
if err != nil { if err != nil {
return fmt.Errorf("error getting sync data: %v", err) return fmt.Errorf("error getting sync data: %v", err)
} }

View file

@ -1,6 +1,6 @@
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv } from '@grafana/runtime';
import { OnCallPluginMetaJSONData } from 'app-types'; import { OnCallPluginMetaJSONData } from 'app-types';
import { getPluginId } from 'helpers/consts'; import { getPluginId, PluginId } from 'helpers/consts';
import { import {
ApiAuthKeyDTO, ApiAuthKeyDTO,
@ -11,18 +11,26 @@ import {
UpdateGrafanaPluginSettingsProps, UpdateGrafanaPluginSettingsProps,
} from './api.types'; } from './api.types';
const pluginId = getPluginId();
const KEY_NAME = {
[PluginId.OnCall]: 'OnCall',
[PluginId.Irm]: 'IRM',
}[pluginId];
const SERVICE_ACCOUNT_NAME = {
[PluginId.OnCall]: 'sa-autogen-OnCall',
[PluginId.Irm]: 'sa-autogen-IRM',
}[pluginId];
const KEYS_BASE_URL = '/api/auth/keys'; const KEYS_BASE_URL = '/api/auth/keys';
const SERVICE_ACCOUNTS_BASE_URL = '/api/serviceaccounts'; const SERVICE_ACCOUNTS_BASE_URL = '/api/serviceaccounts';
const ONCALL_KEY_NAME = 'OnCall'; const GRAFANA_PLUGIN_SETTINGS_URL = `/api/plugins/${pluginId}/settings`;
const ONCALL_SERVICE_ACCOUNT_NAME = 'sa-autogen-OnCall';
const GRAFANA_PLUGIN_SETTINGS_URL = `/api/plugins/${getPluginId()}/settings`;
export class GrafanaApiClient { export class GrafanaApiClient {
static grafanaBackend = getBackendSrv(); static grafanaBackend = getBackendSrv();
private static getServiceAccount = async () => { private static getServiceAccount = async () => {
const serviceAccounts = await this.grafanaBackend.get<PaginatedServiceAccounts>( const serviceAccounts = await this.grafanaBackend.get<PaginatedServiceAccounts>(
`${SERVICE_ACCOUNTS_BASE_URL}/search?query=${ONCALL_SERVICE_ACCOUNT_NAME}` `${SERVICE_ACCOUNTS_BASE_URL}/search?query=${SERVICE_ACCOUNT_NAME}`
); );
return serviceAccounts.serviceAccounts.length > 0 ? serviceAccounts.serviceAccounts[0] : null; return serviceAccounts.serviceAccounts.length > 0 ? serviceAccounts.serviceAccounts[0] : null;
}; };
@ -34,7 +42,7 @@ export class GrafanaApiClient {
} }
return await this.grafanaBackend.post<ServiceAccountDTO>(SERVICE_ACCOUNTS_BASE_URL, { return await this.grafanaBackend.post<ServiceAccountDTO>(SERVICE_ACCOUNTS_BASE_URL, {
name: ONCALL_SERVICE_ACCOUNT_NAME, name: SERVICE_ACCOUNT_NAME,
role: 'Admin', role: 'Admin',
isDisabled: false, isDisabled: false,
}); });
@ -44,7 +52,7 @@ export class GrafanaApiClient {
const tokens = await this.grafanaBackend.get<TokenDTO[]>( const tokens = await this.grafanaBackend.get<TokenDTO[]>(
`${SERVICE_ACCOUNTS_BASE_URL}/${serviceAccount.id}/tokens` `${SERVICE_ACCOUNTS_BASE_URL}/${serviceAccount.id}/tokens`
); );
return tokens.find(({ name }) => name === ONCALL_KEY_NAME); return tokens.find(({ name }) => name === KEY_NAME);
}; };
private static getGrafanaToken = async () => { private static getGrafanaToken = async () => {
@ -54,7 +62,7 @@ export class GrafanaApiClient {
} }
const keys = await this.grafanaBackend.get<ApiAuthKeyDTO[]>(KEYS_BASE_URL); const keys = await this.grafanaBackend.get<ApiAuthKeyDTO[]>(KEYS_BASE_URL);
return keys.find(({ name }) => name === ONCALL_KEY_NAME); return keys.find(({ name }) => name === KEY_NAME);
}; };
static updateGrafanaPluginSettings = async (data: UpdateGrafanaPluginSettingsProps, enabled = true) => static updateGrafanaPluginSettings = async (data: UpdateGrafanaPluginSettingsProps, enabled = true) =>
@ -79,7 +87,7 @@ export class GrafanaApiClient {
const { key: grafanaToken } = await this.grafanaBackend.post<NewApiKeyResult>( const { key: grafanaToken } = await this.grafanaBackend.post<NewApiKeyResult>(
`${SERVICE_ACCOUNTS_BASE_URL}/${serviceAccount.id}/tokens`, `${SERVICE_ACCOUNTS_BASE_URL}/${serviceAccount.id}/tokens`,
{ {
name: ONCALL_KEY_NAME, name: KEY_NAME,
role: 'Admin', role: 'Admin',
} }
); );