Live: support a dashboard gitops channel (#33291)

This commit is contained in:
Ryan McKinley
2021-04-23 21:55:31 +02:00
committed by GitHub
parent 788bc2a793
commit 1dd9e9b184
7 changed files with 269 additions and 65 deletions
+151 -25
View File
@@ -3,23 +3,42 @@ package features
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
)
type actionType string
const (
ACTION_SAVED actionType = "saved"
ACTION_DELETED actionType = "deleted"
EDITING_STARTED actionType = "editing-started"
EDITING_FINISHED actionType = "editing-finished"
GITOPS_CHANNEL = "grafana/dashboard/gitops"
)
// DashboardEvent events related to dashboards
type dashboardEvent struct {
UID string `json:"uid"`
Action string `json:"action"` // saved, editing
UserID int64 `json:"userId,omitempty"`
SessionID string `json:"sessionId,omitempty"`
UID string `json:"uid"`
Action actionType `json:"action"` // saved, editing, deleted
User *models.UserDisplayDTO `json:"user,omitempty"`
SessionID string `json:"sessionId,omitempty"`
Message string `json:"message,omitempty"`
Dashboard *models.Dashboard `json:"dashboard,omitempty"`
Error string `json:"error,omitempty"`
}
// DashboardHandler manages all the `grafana/dashboard/*` channels
type DashboardHandler struct {
Publisher models.ChannelPublisher
Publisher models.ChannelPublisher
ClientCount models.ChannelClientCount
}
// GetHandlerForPath called on init
@@ -28,16 +47,95 @@ func (h *DashboardHandler) GetHandlerForPath(path string) (models.ChannelHandler
}
// OnSubscribe for now allows anyone to subscribe to any dashboard
func (h *DashboardHandler) OnSubscribe(ctx context.Context, _ *models.SignedInUser, e models.SubscribeEvent) (models.SubscribeReply, backend.SubscribeStreamStatus, error) {
return models.SubscribeReply{
Presence: true,
JoinLeave: true,
}, backend.SubscribeStreamStatusOK, nil
func (h *DashboardHandler) OnSubscribe(ctx context.Context, user *models.SignedInUser, e models.SubscribeEvent) (models.SubscribeReply, backend.SubscribeStreamStatus, error) {
parts := strings.Split(e.Path, "/")
if parts[0] == "gitops" {
// gitops gets all changes for everything, so lets make sure it is an admin user
if !user.HasRole(models.ROLE_ADMIN) {
return models.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, nil
}
return models.SubscribeReply{
Presence: true,
}, backend.SubscribeStreamStatusOK, nil
}
// make sure can view this dashboard
if len(parts) == 2 && parts[0] == "uid" {
query := models.GetDashboardQuery{Uid: parts[1], OrgId: user.OrgId}
if err := bus.Dispatch(&query); err != nil {
logger.Error("Unknown dashboard", "query", query)
return models.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, nil
}
dash := query.Result
guardian := guardian.New(dash.Id, user.OrgId, user)
if canView, err := guardian.CanView(); err != nil || !canView {
return models.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, nil
}
return models.SubscribeReply{
Presence: true,
JoinLeave: true,
}, backend.SubscribeStreamStatusOK, nil
}
// Unknown path
logger.Error("Unknown dashboard channel", "path", e.Path)
return models.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, nil
}
// OnPublish is called when someone begins to edit a dashboard
func (h *DashboardHandler) OnPublish(ctx context.Context, _ *models.SignedInUser, e models.PublishEvent) (models.PublishReply, backend.PublishStreamStatus, error) {
return models.PublishReply{}, backend.PublishStreamStatusOK, nil
func (h *DashboardHandler) OnPublish(ctx context.Context, user *models.SignedInUser, e models.PublishEvent) (models.PublishReply, backend.PublishStreamStatus, error) {
parts := strings.Split(e.Path, "/")
if parts[0] == "gitops" {
// gitops gets all changes for everything, so lets make sure it is an admin user
if !user.HasRole(models.ROLE_ADMIN) {
return models.PublishReply{}, backend.PublishStreamStatusPermissionDenied, nil
}
// Eventually this could broadcast a message back to the dashboard saying a pull request exists
return models.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("not implemented yet")
}
// make sure can view this dashboard
if len(parts) == 2 && parts[0] == "uid" {
event := dashboardEvent{}
err := json.Unmarshal(e.Data, &event)
if err != nil || event.UID != parts[1] {
return models.PublishReply{}, backend.SubscribeStreamStatusNotFound, fmt.Errorf("bad request")
}
if event.Action != EDITING_STARTED {
// just ignore the event
return models.PublishReply{}, backend.SubscribeStreamStatusNotFound, fmt.Errorf("ignore???")
}
query := models.GetDashboardQuery{Uid: parts[1], OrgId: user.OrgId}
if err := bus.Dispatch(&query); err != nil {
logger.Error("Unknown dashboard", "query", query)
return models.PublishReply{}, backend.SubscribeStreamStatusNotFound, nil
}
guardian := guardian.New(query.Result.Id, user.OrgId, user)
canEdit, err := guardian.CanEdit()
if err != nil {
return models.PublishReply{}, backend.SubscribeStreamStatusNotFound, fmt.Errorf("internal error")
}
// Ignore edit events if the user can not edit
if !canEdit {
return models.PublishReply{}, backend.SubscribeStreamStatusNotFound, nil // NOOP
}
// Tell everyone who is editing
event.User = user.ToUserDisplayDTO()
msg, err := json.Marshal(event)
if err != nil {
return models.PublishReply{}, backend.SubscribeStreamStatusNotFound, fmt.Errorf("internal error")
}
return models.PublishReply{Data: msg}, backend.PublishStreamStatusOK, nil
}
return models.PublishReply{}, backend.SubscribeStreamStatusNotFound, nil
}
// DashboardSaved should broadcast to the appropriate stream
@@ -46,27 +144,55 @@ func (h *DashboardHandler) publish(event dashboardEvent) error {
if err != nil {
return err
}
err = h.Publisher("grafana/dashboard/uid/"+event.UID, msg)
if err != nil {
return err
// Only broadcast non-error events
if event.Error == "" {
err = h.Publisher("grafana/dashboard/uid/"+event.UID, msg)
if err != nil {
return err
}
}
return h.Publisher("grafana/dashboard/changes", msg)
// Send everything to the gitops channel
return h.Publisher(GITOPS_CHANNEL, msg)
}
// DashboardSaved will broadcast to all connected dashboards
func (h *DashboardHandler) DashboardSaved(uid string, userID int64) error {
return h.publish(dashboardEvent{
UID: uid,
Action: "saved",
UserID: userID,
})
func (h *DashboardHandler) DashboardSaved(user *models.UserDisplayDTO, message string, dashboard *models.Dashboard, err error) error {
if err != nil && !h.HasGitOpsObserver() {
return nil // only broadcast if it was OK
}
msg := dashboardEvent{
UID: dashboard.Uid,
Action: ACTION_SAVED,
User: user,
Message: message,
Dashboard: dashboard,
}
if err != nil {
msg.Error = err.Error()
}
return h.publish(msg)
}
// DashboardDeleted will broadcast to all connected dashboards
func (h *DashboardHandler) DashboardDeleted(uid string, userID int64) error {
func (h *DashboardHandler) DashboardDeleted(user *models.UserDisplayDTO, uid string) error {
return h.publish(dashboardEvent{
UID: uid,
Action: "deleted",
UserID: userID,
Action: ACTION_DELETED,
User: user,
})
}
// HasGitOpsObserver will return true if anyone is listening to the `gitops` channel
func (h *DashboardHandler) HasGitOpsObserver() bool {
count, err := h.ClientCount(GITOPS_CHANNEL)
if err != nil {
logger.Error("error getting client count", "error", err)
return false
}
return count > 0
}
+25 -2
View File
@@ -133,7 +133,8 @@ func (g *GrafanaLive) Init() error {
// Initialize the main features
dash := &features.DashboardHandler{
Publisher: g.Publish,
Publisher: g.Publish,
ClientCount: g.ClientCount,
}
g.GrafanaScope.Dashboards = dash
g.GrafanaScope.Features["dashboard"] = dash
@@ -435,9 +436,18 @@ func (g *GrafanaLive) Publish(channel string, data []byte) error {
return err
}
// ClientCount returns the number of clients
func (g *GrafanaLive) ClientCount(channel string) (int, error) {
p, err := g.node.Presence(channel)
if err != nil {
return 0, err
}
return len(p.Presence), nil
}
// IsEnabled returns true if the Grafana Live feature is enabled.
func (g *GrafanaLive) IsEnabled() bool {
return g.Cfg.IsLiveEnabled()
return g != nil && g.Cfg.IsLiveEnabled()
}
func (g *GrafanaLive) HandleHTTPPublish(ctx *models.ReqContext, cmd dtos.LivePublishCmd) response.Response {
@@ -503,3 +513,16 @@ func (g *GrafanaLive) HandleListHTTP(_ *models.ReqContext) response.Response {
info["channels"] = channels
return response.JSONStreaming(200, info)
}
// HandleInfoHTTP special http response for
func (g *GrafanaLive) HandleInfoHTTP(ctx *models.ReqContext) response.Response {
path := ctx.Params("*")
if path == "grafana/dashboards/gitops" {
return response.JSON(200, util.DynMap{
"active": g.GrafanaScope.Dashboards.HasGitOpsObserver(),
})
}
return response.JSONStreaming(404, util.DynMap{
"message": "Info is not supported for this channel",
})
}