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 15, 2024
1 parent e9b18b5 commit 913665b
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 14 deletions.
121 changes: 120 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"

"github.com/davecgh/go-spew/spew"
"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,120 @@ 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: "/alerting/{rule_uid}/edit",
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
}

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)))
}

writeJSONOrLog(w, map[string]any{
"name": ruleGroup.Name(),
"interval": "1m",
"rules": formattedRules,
})
}
}

func toGrafanaAlert(rule map[string]any) map[string]any {
grafanaAlert := rule
grafanaAlert["intervalSeconds"] = 60 // TODO: "rule.for" converted to int (seconds) ?
grafanaAlert["version"] = 3
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": "1m",
"grafana_alert": grafanaAlert,
}
}

func (h *AlertRuleGroupHandler) AlertRuleJSONGetHandler(s grizzly.Server) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
spew.Dump("getting rule")

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
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)
}

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
}

writeJSONOrLog(w, toGrafanaAlert(rule))
}
}

// getRemoteAlertRuleGroup retrieves a alertRuleGroup object from Grafana
func (h *AlertRuleGroupHandler) getRemoteAlertRuleGroup(uid string) (*grizzly.Resource, error) {
folder, group := h.splitUID(uid)
Expand Down
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": true,
"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
26 changes: 15 additions & 11 deletions pkg/grizzly/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,21 @@ var mustProxyGET = []string{
"/api/instance/plugins",
"/api/instance/provisioned-plugins",
"/api/usage/datasource/*",
"/api/search",
"/api/v1/ngalert",
"/avatar/*",
}
var mustProxyPOST = []string{
"/api/datasources/proxy/*",
"/api/ds/query",
"/api/v1/eval",
}
var blockJSONget = map[string]string{
"/api/ma/events": "[]",
"/api/live/publish": "[]",
"/api/live/list": "[]",
"/api/user/orgs": "[]",
"/api/annotations": "[]",
"/api/search": "[]",
"/api/usage/*": "[]",
"/api/frontend/assets": "{}",
"/api/org/preferences": "{}",
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 913665b

Please sign in to comment.