Skip to content

Commit

Permalink
Support AlertRuleGroup in grr serve
Browse files Browse the repository at this point in the history
  • Loading branch information
K-Phoen committed Nov 26, 2024
1 parent 487374f commit 049d02e
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 13 deletions.
132 changes: 131 additions & 1 deletion pkg/grafana/alertgroup-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"strings"
"time"

"github.com/go-chi/chi"
"github.com/grafana/grafana-openapi-client-go/client/provisioning"
"github.com/grafana/grafana-openapi-client-go/models"
"github.com/grafana/grizzly/pkg/grizzly"
)

const AlertRuleGroupKind = "AlertRuleGroup"

// AlertRuleGroupHandler is a Grizzly Handler for Grafana alertRuleGroups
type AlertRuleGroupHandler struct {
grizzly.BaseHandler
Expand All @@ -20,7 +25,7 @@ type AlertRuleGroupHandler struct {
// NewAlertRuleGroupHandler returns a new Grizzly Handler for Grafana alertRuleGroups
func NewAlertRuleGroupHandler(provider grizzly.Provider) *AlertRuleGroupHandler {
return &AlertRuleGroupHandler{
BaseHandler: grizzly.NewBaseHandler(provider, "AlertRuleGroup", false),
BaseHandler: grizzly.NewBaseHandler(provider, AlertRuleGroupKind, false),
}
}

Expand Down Expand Up @@ -85,6 +90,99 @@ func (h *AlertRuleGroupHandler) Update(existing, resource grizzly.Resource) erro
return h.putAlertRuleGroup(existing, resource)
}

func (h *AlertRuleGroupHandler) GetProxyEndpoints(s grizzly.Server) []grizzly.HTTPEndpoint {
return []grizzly.HTTPEndpoint{
{
Method: http.MethodGet,
URL: "/alerting/grafana/{rule_uid}/view",
Handler: authenticateAndProxyHandler(s, h.Provider),
},
{
Method: http.MethodGet,
URL: "/api/ruler/grafana/api/v1/rule/{rule_uid}",
Handler: h.AlertRuleJSONGetHandler(s),
},
{
Method: http.MethodGet,
URL: "/api/ruler/grafana/api/v1/rules/{folder_uid}/{rule_group_uid}",
Handler: h.AlertRuleGroupJSONGetHandler(s),
},
}
}

func (h *AlertRuleGroupHandler) ProxyURL(uid string) string {
return fmt.Sprintf("/alerting/grafana/%s/view", uid)
}

func (h *AlertRuleGroupHandler) AlertRuleGroupJSONGetHandler(s grizzly.Server) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
folderUID := chi.URLParam(r, "folder_uid")
ruleGroupUID := chi.URLParam(r, "rule_group_uid")
fullUID := h.joinUID(folderUID, ruleGroupUID)

ruleGroup, found := s.Resources.Find(grizzly.NewResourceRef(AlertRuleGroupKind, fullUID))
if !found {
grizzly.SendError(w, fmt.Sprintf("Alert rule group with UID %s not found", fullUID), fmt.Errorf("alert rule group with UID %s not found", fullUID), http.StatusNotFound)
return
}

interval := time.Duration(ruleGroup.GetSpecValue("interval").(int)) * time.Second

rules := ruleGroup.GetSpecValue("rules").([]any)
formattedRules := make([]map[string]any, 0, len(rules))
for _, rule := range rules {
formattedRules = append(formattedRules, toGrafanaAlert(rule.(map[string]any), interval))
}

writeJSONOrLog(w, map[string]any{
"name": ruleGroup.GetSpecValue("title"),
"interval": interval.String(),
"rules": formattedRules,
})
}
}

func (h *AlertRuleGroupHandler) AlertRuleJSONGetHandler(s grizzly.Server) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ruleUID := chi.URLParam(r, "rule_uid")
if ruleUID == "" {
grizzly.SendError(w, "No alert rule UID specified", fmt.Errorf("no alert rule UID specified within the URL"), http.StatusBadRequest)
return
}

var rule map[string]any
var ruleGroup grizzly.Resource
ruleFound := false
_ = s.Resources.OfKind(AlertRuleGroupKind).ForEach(func(candidate grizzly.Resource) error {
if ruleFound {
return nil
}

rules := candidate.GetSpecValue("rules").([]any)
for _, candidateRule := range rules {
candidateUID := candidateRule.(map[string]any)["uid"].(string)
if candidateUID != ruleUID {
continue
}

ruleFound = true
rule = candidateRule.(map[string]any)
ruleGroup = candidate
}

return nil
})
if !ruleFound {
grizzly.SendError(w, fmt.Sprintf("Alert rule with UID %s not found", ruleUID), fmt.Errorf("rule group with UID %s not found", ruleUID), http.StatusNotFound)
return
}

interval := time.Duration(ruleGroup.GetSpecValue("interval").(int)) * time.Second

writeJSONOrLog(w, toGrafanaAlert(rule, interval))
}
}

// getRemoteAlertRuleGroup retrieves a alertRuleGroup object from Grafana
func (h *AlertRuleGroupHandler) getRemoteAlertRuleGroup(uid string) (*grizzly.Resource, error) {
folder, group := h.splitUID(uid)
Expand Down Expand Up @@ -290,3 +388,35 @@ func (h *AlertRuleGroupHandler) splitUID(uid string) (string, string) {
spl := strings.SplitN(uid, ".", 2)
return spl[0], spl[1]
}

// See GettableGrafanaRule model in grafana-openapi-client-go
func toGrafanaAlert(rule map[string]any, ruleGroupInterval time.Duration) map[string]any {
var version any = 1
if v, ok := rule["version"]; ok {
version = v
}

intervalSeconds := 0
interval, err := time.ParseDuration(rule["for"].(string))
if err == nil {
intervalSeconds = int(interval.Seconds())
}

grafanaAlert := rule
grafanaAlert["intervalSeconds"] = intervalSeconds
grafanaAlert["version"] = version
grafanaAlert["namespace_uid"] = rule["folderUID"]
grafanaAlert["rule_group"] = rule["ruleGroup"]
grafanaAlert["no_data_state"] = rule["noDataState"]
grafanaAlert["exec_err_state"] = rule["execErrState"]
grafanaAlert["is_paused"] = false
grafanaAlert["metadata"] = map[string]any{
"editor_settings": map[string]any{},
}

return map[string]any{
"expr": "",
"for": ruleGroupInterval.String(),
"grafana_alert": grafanaAlert,
}
}
82 changes: 81 additions & 1 deletion pkg/grafana/folder-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"

"github.com/go-chi/chi"
gclient "github.com/grafana/grafana-openapi-client-go/client"
"github.com/grafana/grafana-openapi-client-go/client/folders"
"github.com/grafana/grafana-openapi-client-go/client/search"
Expand All @@ -14,6 +16,7 @@ import (
)

const DefaultFolder = "General"
const DashboardFolderKind = "DashboardFolder"

// FolderHandler is a Grizzly Handler for Grafana dashboard folders
type FolderHandler struct {
Expand All @@ -23,7 +26,7 @@ type FolderHandler struct {
// NewFolderHandler returns configuration defining a new Grafana Folder Handler
func NewFolderHandler(provider grizzly.Provider) *FolderHandler {
return &FolderHandler{
BaseHandler: grizzly.NewBaseHandler(provider, "DashboardFolder", false),
BaseHandler: grizzly.NewBaseHandler(provider, DashboardFolderKind, false),
}
}

Expand Down Expand Up @@ -147,6 +150,83 @@ func (h *FolderHandler) Update(existing, resource grizzly.Resource) error {
return h.putFolder(resource)
}

func (h *FolderHandler) ProxyURL(uid string) string {
return fmt.Sprintf("/dashboards/f/%s/", uid)
}

func (h *FolderHandler) GetProxyEndpoints(s grizzly.Server) []grizzly.HTTPEndpoint {
return []grizzly.HTTPEndpoint{
{
Method: http.MethodGet,
URL: "/alerting/{rule_uid}/edit",
Handler: authenticateAndProxyHandler(s, h.Provider),
},
{
Method: http.MethodGet,
URL: "/api/folders/{folder_uid}",
Handler: h.FolderJSONGetHandler(s),
},
}
}

func (h *FolderHandler) FolderJSONGetHandler(s grizzly.Server) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
folderUID := chi.URLParam(r, "folder_uid")
withAccessControl := r.URL.Query().Get("accesscontrol")

folder, found := s.Resources.Find(grizzly.NewResourceRef(DashboardFolderKind, folderUID))
if !found {
grizzly.SendError(w, fmt.Sprintf("Folder with UID %s not found", folderUID), fmt.Errorf("folder with UID %s not found", folderUID), http.StatusNotFound)
return
}

// These values are required for the page to load properly.
if folder.GetSpecValue("version") == nil {
folder.SetSpecValue("version", 1)
}
if folder.GetSpecValue("id") == nil {
folder.SetSpecValue("id", 1)
}

response := folder.Spec()

if withAccessControl == "true" {
// TODO: can we omit stuff from this list?
response["accessControl"] = map[string]any{
"alert.rules:create": false,
"alert.rules:delete": false,
"alert.rules:read": true,
"alert.rules:write": false,
"alert.silences:create": false,
"alert.silences:read": true,
"alert.silences:write": false,
"annotations:create": false,
"annotations:delete": false,
"annotations:read": true,
"annotations:write": false,
"dashboards.permissions:read": true,
"dashboards.permissions:write": false,
"dashboards:create": true,
"dashboards:delete": false,
"dashboards:read": true,
"dashboards:write": true,
"folders.permissions:read": true,
"folders.permissions:write": false,
"folders:create": false,
"folders:delete": false,
"folders:read": true,
"folders:write": false,
"library.panels:create": false,
"library.panels:delete": false,
"library.panels:read": true,
"library.panels:write": false,
}
}

writeJSONOrLog(w, response)
}
}

// getRemoteFolder retrieves a folder object from Grafana
func (h *FolderHandler) getRemoteFolder(uid string) (*grizzly.Resource, error) {
if uid == "" {
Expand Down
8 changes: 7 additions & 1 deletion pkg/grafana/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

gclient "github.com/grafana/grafana-openapi-client-go/client"
"github.com/grafana/grafana-openapi-client-go/models"
"github.com/grafana/grizzly/internal/httputils"
"github.com/grafana/grizzly/pkg/grizzly"
)

Expand Down Expand Up @@ -74,7 +75,12 @@ func authenticateAndProxyHandler(s grizzly.Server, provider grizzly.Provider) ht

req.Header.Set("User-Agent", s.UserAgent)

client := &http.Client{}
client, err := httputils.NewHTTPClient()
if err != nil {
grizzly.SendError(w, http.StatusText(http.StatusInternalServerError), err, http.StatusInternalServerError)
return
}

resp, err := client.Do(req)

if err == nil {
Expand Down
15 changes: 15 additions & 0 deletions pkg/grizzly/embed/templates/proxy/index.html.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@
<li>No datasources.</li>
{{ end }}
</ul>

<h1>Alert rule groups</h1>

<ul>
{{ range (.Resources.OfKind "AlertRuleGroup").AsList }}
<li>
{{ .Spec.title }}
<ul>
{{ range .Spec.rules }}
<li><a href="/grizzly/AlertRuleGroup/{{ .uid }}">{{ .title }}</a></li>
{{ end }}
</ul>
</li>
{{ end }}
</ul>
</div>
</main>
</body>
Expand Down
24 changes: 14 additions & 10 deletions pkg/grizzly/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,13 @@ var mustProxyGET = []string{
"/api/instance/plugins",
"/api/instance/provisioned-plugins",
"/api/usage/datasource/*",
"/api/v1/ngalert",
"/avatar/*",
}
var mustProxyPOST = []string{
"/api/datasources/proxy/*",
"/api/ds/query",
"/api/v1/eval",
}
var blockJSONget = map[string]string{
"/api/ma/events": "[]",
Expand Down Expand Up @@ -171,16 +173,18 @@ func (s *Server) Start() error {

for _, handler := range s.Registry.Handlers {
proxyHandler, ok := handler.(ProxyHandler)
if ok {
for _, endpoint := range proxyHandler.GetProxyEndpoints(*s) {
switch endpoint.Method {
case "GET":
r.Get(endpoint.URL, endpoint.Handler)
case "POST":
r.Post(endpoint.URL, endpoint.Handler)
default:
return fmt.Errorf("unknown endpoint method %s for handler %s", endpoint.Method, handler.Kind())
}
if !ok {
continue
}

for _, endpoint := range proxyHandler.GetProxyEndpoints(*s) {
switch endpoint.Method {
case http.MethodGet:
r.Get(endpoint.URL, endpoint.Handler)
case http.MethodPost:
r.Post(endpoint.URL, endpoint.Handler)
default:
return fmt.Errorf("unknown endpoint method %s for handler %s", endpoint.Method, handler.Kind())
}
}
}
Expand Down

0 comments on commit 049d02e

Please sign in to comment.