Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support previewing AlertRuleGroup resources in grr serve #526

Merged
merged 1 commit into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading