Live: Move dashboard events from the raw http server to the apiserver (#115066)
This commit is contained in:
@@ -380,13 +380,6 @@ func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Respo
|
||||
return dashboardErrResponse(err, "Failed to delete dashboard")
|
||||
}
|
||||
|
||||
if hs.Live != nil {
|
||||
err := hs.Live.GrafanaScope.Dashboards.DashboardDeleted(c.GetOrgID(), c.SignedInUser, dash.UID)
|
||||
if err != nil {
|
||||
hs.log.Error("Failed to broadcast delete info", "dashboard", dash.UID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, util.DynMap{
|
||||
"title": dash.Title,
|
||||
"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
|
||||
@@ -482,31 +475,6 @@ func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.S
|
||||
}
|
||||
|
||||
dashboard, saveErr := hs.DashboardService.SaveDashboard(ctx, dashItem, allowUiUpdate)
|
||||
|
||||
if hs.Live != nil {
|
||||
// Tell everyone listening that the dashboard changed
|
||||
if dashboard == nil {
|
||||
dashboard = dash // the original request
|
||||
}
|
||||
|
||||
// This will broadcast all save requests only if a `gitops` observer exists.
|
||||
// gitops is useful when trying to save dashboards in an environment where the user can not save
|
||||
channel := hs.Live.GrafanaScope.Dashboards
|
||||
liveerr := channel.DashboardSaved(c.GetOrgID(), c.SignedInUser, cmd.Message, dashboard, saveErr)
|
||||
|
||||
// When an error exists, but the value broadcast to a gitops listener return 202
|
||||
if liveerr == nil && saveErr != nil && channel.HasGitOpsObserver(c.GetOrgID()) {
|
||||
return response.JSON(http.StatusAccepted, util.DynMap{
|
||||
"status": "pending",
|
||||
"message": "changes were broadcast to the gitops listener",
|
||||
})
|
||||
}
|
||||
|
||||
if liveerr != nil {
|
||||
hs.log.Warn("Unable to broadcast save event", "uid", dashboard.UID, "error", liveerr)
|
||||
}
|
||||
}
|
||||
|
||||
if saveErr != nil {
|
||||
return apierrors.ToDashboardErrorResponse(ctx, hs.pluginStore, saveErr)
|
||||
}
|
||||
|
||||
@@ -7,20 +7,41 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/live"
|
||||
)
|
||||
|
||||
// dashboardStoragePermissionWrapper is a wrapper around the grafanarest.Storage that adds dashboard permissions handling
|
||||
// when dual writing is enabled.
|
||||
type dashboardStoragePermissionWrapper struct {
|
||||
dashboardPermissionsSvc accesscontrol.DashboardPermissionsService
|
||||
// dashboardStorageWrapper is a wrapper around the grafanarest.Storage so it will:
|
||||
// 1. support adds dashboard permissions handling
|
||||
// 2. broadcast changes to grafana live
|
||||
// when running in single tenant mode
|
||||
type dashboardStorageWrapper struct {
|
||||
grafanarest.Storage
|
||||
|
||||
dashboardPermissionsSvc accesscontrol.DashboardPermissionsService
|
||||
live live.DashboardActivityChannel
|
||||
}
|
||||
|
||||
func (d dashboardStoragePermissionWrapper) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
func (d dashboardStorageWrapper) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||
ns, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
obj, created, err := d.Storage.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
||||
if err == nil && ns.OrgID > 0 && d.live != nil {
|
||||
if err := d.live.DashboardSaved(ns.OrgID, name); err != nil {
|
||||
logging.FromContext(ctx).Info("live dashboard update failed", "err", err)
|
||||
}
|
||||
}
|
||||
return obj, created, err
|
||||
}
|
||||
|
||||
func (d dashboardStorageWrapper) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
ns, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -28,7 +49,12 @@ func (d dashboardStoragePermissionWrapper) Delete(ctx context.Context, name stri
|
||||
if err != nil {
|
||||
return obj, async, err
|
||||
}
|
||||
if accessErr := d.dashboardPermissionsSvc.DeleteResourcePermissions(ctx, info.OrgID, name); accessErr != nil {
|
||||
if ns.OrgID > 0 && d.live != nil {
|
||||
if err := d.live.DashboardDeleted(ns.OrgID, name); err != nil {
|
||||
logging.FromContext(ctx).Info("live dashboard update failed", "err", err)
|
||||
}
|
||||
}
|
||||
if accessErr := d.dashboardPermissionsSvc.DeleteResourcePermissions(ctx, ns.OrgID, name); accessErr != nil {
|
||||
return obj, async, accessErr
|
||||
}
|
||||
return obj, async, nil
|
||||
|
||||
@@ -56,6 +56,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||
"github.com/grafana/grafana/pkg/services/librarypanels"
|
||||
"github.com/grafana/grafana/pkg/services/live"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
@@ -122,6 +123,7 @@ type DashboardsAPIBuilder struct {
|
||||
snapshotService dashboardsnapshots.Service
|
||||
snapshotOptions dashv0.SnapshotSharingOptions
|
||||
namespacer request.NamespaceMapper
|
||||
dashboardActivityChannel live.DashboardActivityChannel
|
||||
isStandalone bool // skips any handling including anything to do with legacy storage
|
||||
}
|
||||
|
||||
@@ -151,6 +153,7 @@ func RegisterAPIService(
|
||||
libraryPanels libraryelements.Service,
|
||||
publicDashboardService publicdashboards.Service,
|
||||
snapshotService dashboardsnapshots.Service,
|
||||
dashboardActivityChannel live.DashboardActivityChannel,
|
||||
) *DashboardsAPIBuilder {
|
||||
dbp := legacysql.NewDatabaseProvider(sql)
|
||||
namespacer := request.GetNamespaceMapper(cfg)
|
||||
@@ -185,6 +188,7 @@ func RegisterAPIService(
|
||||
snapshotService: snapshotService,
|
||||
snapshotOptions: snapshotOptions,
|
||||
namespacer: namespacer,
|
||||
dashboardActivityChannel: dashboardActivityChannel,
|
||||
legacy: &DashboardStorage{
|
||||
Access: legacy.NewDashboardSQLAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, dashboardPermissionsSvc, accessControl, features),
|
||||
DashboardService: dashboardService,
|
||||
@@ -696,9 +700,10 @@ func (b *DashboardsAPIBuilder) storageForVersion(
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
storage[dashboards.StoragePath()] = dashboardStoragePermissionWrapper{
|
||||
dashboardPermissionsSvc: b.dashboardPermissionsSvc,
|
||||
storage[dashboards.StoragePath()] = dashboardStorageWrapper{
|
||||
Storage: dw,
|
||||
dashboardPermissionsSvc: b.dashboardPermissionsSvc,
|
||||
live: b.dashboardActivityChannel,
|
||||
}
|
||||
|
||||
// Register the DTO endpoint that will consolidate all dashboard bits
|
||||
|
||||
@@ -280,6 +280,7 @@ var wireBasicSet = wire.NewSet(
|
||||
store.ProvideService,
|
||||
store.ProvideSystemUsersService,
|
||||
live.ProvideService,
|
||||
live.ProvideDashboardActivityChannel,
|
||||
pushhttp.ProvideService,
|
||||
contexthandler.ProvideService,
|
||||
ldapservice.ProvideService,
|
||||
|
||||
Generated
+5
-3
File diff suppressed because one or more lines are too long
@@ -22,39 +22,13 @@ const (
|
||||
ActionDeleted actionType = "deleted"
|
||||
EditingStarted actionType = "editing-started"
|
||||
//EditingFinished actionType = "editing-finished"
|
||||
|
||||
GitopsChannel = "grafana/dashboard/gitops"
|
||||
)
|
||||
|
||||
type userDisplayDTO struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
UID string `json:"uid,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Login string `json:"login,omitempty"`
|
||||
AvatarURL string `json:"avatarUrl"`
|
||||
}
|
||||
|
||||
// Static function to parse a requester into a userDisplayDTO
|
||||
func newUserDisplayDTOFromRequester(requester identity.Requester) *userDisplayDTO {
|
||||
// nolint:staticcheck
|
||||
userID, _ := requester.GetInternalID()
|
||||
return &userDisplayDTO{
|
||||
ID: userID,
|
||||
UID: requester.GetRawIdentifier(),
|
||||
Login: requester.GetLogin(),
|
||||
Name: requester.GetName(),
|
||||
}
|
||||
}
|
||||
|
||||
// DashboardEvent events related to dashboards
|
||||
type dashboardEvent struct {
|
||||
UID string `json:"uid"`
|
||||
Action actionType `json:"action"` // saved, editing, deleted
|
||||
User *userDisplayDTO `json:"user,omitempty"`
|
||||
SessionID string `json:"sessionId,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Dashboard *dashboards.Dashboard `json:"dashboard,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
UID string `json:"uid"`
|
||||
Action actionType `json:"action"` // saved, editing, deleted
|
||||
SessionID string `json:"sessionId,omitempty"`
|
||||
}
|
||||
|
||||
// DashboardHandler manages all the `grafana/dashboard/*` channels
|
||||
@@ -134,9 +108,6 @@ func (h *DashboardHandler) OnPublish(ctx context.Context, requester identity.Req
|
||||
return model.PublishReply{}, backend.PublishStreamStatusNotFound, nil // NOOP
|
||||
}
|
||||
|
||||
// Tell everyone who is editing
|
||||
event.User = newUserDisplayDTOFromRequester(requester)
|
||||
|
||||
msg, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error")
|
||||
@@ -153,55 +124,21 @@ func (h *DashboardHandler) publish(orgID int64, event dashboardEvent) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only broadcast non-error events
|
||||
if event.Error == "" {
|
||||
err = h.Publisher(orgID, "grafana/dashboard/uid/"+event.UID, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Send everything to the gitops channel
|
||||
return h.Publisher(orgID, GitopsChannel, msg)
|
||||
return h.Publisher(orgID, "grafana/dashboard/uid/"+event.UID, msg)
|
||||
}
|
||||
|
||||
// DashboardSaved will broadcast to all connected dashboards
|
||||
func (h *DashboardHandler) DashboardSaved(orgID int64, requester identity.Requester, message string, dashboard *dashboards.Dashboard, err error) error {
|
||||
if err != nil && !h.HasGitOpsObserver(orgID) {
|
||||
return nil // only broadcast if it was OK
|
||||
}
|
||||
|
||||
msg := dashboardEvent{
|
||||
UID: dashboard.UID,
|
||||
Action: ActionSaved,
|
||||
User: newUserDisplayDTOFromRequester(requester),
|
||||
Message: message,
|
||||
Dashboard: dashboard,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
msg.Error = err.Error()
|
||||
}
|
||||
|
||||
return h.publish(orgID, msg)
|
||||
}
|
||||
|
||||
// DashboardDeleted will broadcast to all connected dashboards
|
||||
func (h *DashboardHandler) DashboardDeleted(orgID int64, requester identity.Requester, uid string) error {
|
||||
func (h *DashboardHandler) DashboardSaved(orgID int64, uid string) error {
|
||||
return h.publish(orgID, dashboardEvent{
|
||||
UID: uid,
|
||||
Action: ActionDeleted,
|
||||
User: newUserDisplayDTOFromRequester(requester),
|
||||
Action: ActionSaved,
|
||||
})
|
||||
}
|
||||
|
||||
// HasGitOpsObserver will return true if anyone is listening to the `gitops` channel
|
||||
func (h *DashboardHandler) HasGitOpsObserver(orgID int64) bool {
|
||||
count, err := h.ClientCount(orgID, GitopsChannel)
|
||||
if err != nil {
|
||||
logger.Error("Error getting client count", "error", err)
|
||||
return false
|
||||
}
|
||||
return count > 0
|
||||
// DashboardDeleted will broadcast to all connected dashboards
|
||||
func (h *DashboardHandler) DashboardDeleted(orgID int64, uid string) error {
|
||||
return h.publish(orgID, dashboardEvent{
|
||||
UID: uid,
|
||||
Action: ActionDeleted,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -513,18 +513,17 @@ type GrafanaLive struct {
|
||||
|
||||
// DashboardActivityChannel is a service to advertise dashboard activity
|
||||
type DashboardActivityChannel interface {
|
||||
// Called when a dashboard is saved -- this includes the error so we can support a
|
||||
// gitops workflow that knows if the value was saved to the local database or not
|
||||
// in many cases all direct save requests will fail, but the request should be forwarded
|
||||
// to any gitops observers
|
||||
DashboardSaved(orgID int64, requester identity.Requester, message string, dashboard *dashboards.Dashboard, err error) error
|
||||
// Called when a dashboard is saved
|
||||
DashboardSaved(orgID int64, uid string) error
|
||||
|
||||
// Called when a dashboard is deleted
|
||||
DashboardDeleted(orgID int64, requester identity.Requester, uid string) error
|
||||
DashboardDeleted(orgID int64, uid string) error
|
||||
}
|
||||
|
||||
// Experimental! Indicate is GitOps is active. This really means
|
||||
// someone is subscribed to the `grafana/dashboards/gitops` channel
|
||||
HasGitOpsObserver(orgID int64) bool
|
||||
// ProvideDashboardActivityChannel extracts the DashboardActivityChannel from GrafanaLive.
|
||||
// This is used by wire to inject the channel into the dashboard API service.
|
||||
func ProvideDashboardActivityChannel(live *GrafanaLive) DashboardActivityChannel {
|
||||
return live.GrafanaScope.Dashboards
|
||||
}
|
||||
|
||||
func (g *GrafanaLive) getStreamPlugin(ctx context.Context, pluginID string) (backend.StreamHandler, error) {
|
||||
@@ -1178,12 +1177,6 @@ func (g *GrafanaLive) HandleListHTTP(c *contextmodel.ReqContext) response.Respon
|
||||
|
||||
// HandleInfoHTTP special http response for
|
||||
func (g *GrafanaLive) HandleInfoHTTP(ctx *contextmodel.ReqContext) response.Response {
|
||||
path := web.Params(ctx.Req)["*"]
|
||||
if path == "grafana/dashboards/gitops" {
|
||||
return response.JSON(http.StatusOK, util.DynMap{
|
||||
"active": g.GrafanaScope.Dashboards.HasGitOpsObserver(ctx.GetOrgID()),
|
||||
})
|
||||
}
|
||||
return response.JSONStreaming(http.StatusNotFound, util.DynMap{
|
||||
"message": "Info is not supported for this channel",
|
||||
})
|
||||
|
||||
@@ -8,7 +8,6 @@ export enum DashboardEventAction {
|
||||
export interface DashboardEvent {
|
||||
uid: string;
|
||||
action: DashboardEventAction;
|
||||
userId?: number;
|
||||
message?: string;
|
||||
sessionId?: string;
|
||||
timestamp?: number;
|
||||
|
||||
Reference in New Issue
Block a user