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

Split proxy configuration out of resource handlers #527

Merged
merged 2 commits 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
29 changes: 29 additions & 0 deletions internal/httputils/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package httputils

import (
"encoding/json"
"net/http"

log "github.com/sirupsen/logrus"
)

func Error(w http.ResponseWriter, msg string, err error, code int) {
log.Warnf("%d - %s: %s", code, msg, err.Error())
http.Error(w, msg, code)
}

func Write(w http.ResponseWriter, content []byte) {
if _, err := w.Write(content); err != nil {
log.Errorf("error writing response: %v", err)
}
}

func WriteJSON(w http.ResponseWriter, content any) {
responseJSON, err := json.Marshal(content)
if err != nil {
log.Errorf("error marshalling response to JSON: %v", err)
}

w.Header().Set("Content-Type", "application/json")
Write(w, responseJSON)
}
144 changes: 13 additions & 131 deletions pkg/grafana/alertgroup-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +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"

var _ grizzly.Handler = &AlertRuleGroupHandler{}
var _ grizzly.ProxyConfiguratorProvider = &AlertRuleGroupHandler{}

// AlertRuleGroupHandler is a Grizzly Handler for Grafana alertRuleGroups
type AlertRuleGroupHandler struct {
grizzly.BaseHandler
Expand All @@ -33,6 +33,13 @@ const (
alertRuleGroupPattern = "alert-rules/alertRuleGroup-%s.%s"
)

// ProxyConfigurator provides a configurator object describing how to proxy alert rule groups.
func (h *AlertRuleGroupHandler) ProxyConfigurator() grizzly.ProxyConfigurator {
return &alertRuleProxyConfigurator{
provider: h.Provider,
}
}

// ResourceFilePath returns the location on disk where a resource should be updated
func (h *AlertRuleGroupHandler) ResourceFilePath(resource grizzly.Resource, filetype string) string {
filename := strings.ReplaceAll(resource.Name(), string(os.PathSeparator), "-")
Expand Down Expand Up @@ -90,99 +97,6 @@ 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 @@ -228,7 +142,7 @@ func (h *AlertRuleGroupHandler) getRemoteAlertRuleGroupList() ([]string, error)

uidmap := make(map[string]struct{})
for _, alert := range alerts {
uid := h.joinUID(*alert.FolderUID, *alert.RuleGroup)
uid := joinAlertRuleGroupUID(*alert.FolderUID, *alert.RuleGroup)
uidmap[uid] = struct{}{}
}
uids := make([]string, len(uidmap))
Expand Down Expand Up @@ -377,46 +291,14 @@ func (h *AlertRuleGroupHandler) putAlertRuleGroup(existing, resource grizzly.Res
}

func (h *AlertRuleGroupHandler) getUID(group models.AlertRuleGroup) string {
return h.joinUID(group.FolderUID, group.Title)
return joinAlertRuleGroupUID(group.FolderUID, group.Title)
}

func (h *AlertRuleGroupHandler) joinUID(folder, title string) string {
func joinAlertRuleGroupUID(folder, title string) string {
return fmt.Sprintf("%s.%s", folder, title)
}

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,
}
}
143 changes: 143 additions & 0 deletions pkg/grafana/alertgroup-proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package grafana

import (
"fmt"
"net/http"
"time"

"github.com/go-chi/chi"
"github.com/grafana/grizzly/internal/httputils"
"github.com/grafana/grizzly/pkg/grizzly"
)

var _ grizzly.ProxyConfigurator = &alertRuleProxyConfigurator{}

// alertRuleProxyConfigurator describes how to proxy AlertRuleGroup resources.
type alertRuleProxyConfigurator struct {
provider grizzly.Provider
}

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

func (c *alertRuleProxyConfigurator) GetProxyEndpoints(s grizzly.Server) []grizzly.HTTPEndpoint {
return []grizzly.HTTPEndpoint{
{
Method: http.MethodGet,
URL: "/alerting/grafana/{rule_uid}/view",
Handler: authenticateAndProxyHandler(s, c.provider),
},
{
Method: http.MethodGet,
URL: "/api/ruler/grafana/api/v1/rule/{rule_uid}",
Handler: c.alertRuleJSONGetHandler(s),
},
{
Method: http.MethodGet,
URL: "/api/ruler/grafana/api/v1/rules/{folder_uid}/{rule_group_uid}",
Handler: c.alertRuleGroupJSONGetHandler(s),
},
}
}

func (c *alertRuleProxyConfigurator) 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 := joinAlertRuleGroupUID(folderUID, ruleGroupUID)

ruleGroup, found := s.Resources.Find(grizzly.NewResourceRef(AlertRuleGroupKind, fullUID))
if !found {
httputils.Error(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))
}

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

func (c *alertRuleProxyConfigurator) alertRuleJSONGetHandler(s grizzly.Server) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ruleUID := chi.URLParam(r, "rule_uid")
if ruleUID == "" {
httputils.Error(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 {
httputils.Error(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

httputils.WriteJSON(w, toGrafanaAlert(rule, interval))
}
}

// 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,
}
}
6 changes: 5 additions & 1 deletion pkg/grafana/contactpoint-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import (
"github.com/grafana/grizzly/pkg/grizzly"
)

const AlertContactPointKind = "AlertContactPoint"

var _ grizzly.Handler = &AlertContactPointHandler{}

// AlertContactPointHandler is a Grizzly Handler for Grafana contactPoints
type AlertContactPointHandler struct {
grizzly.BaseHandler
Expand All @@ -17,7 +21,7 @@ type AlertContactPointHandler struct {
// NewAlertContactPointHandler returns a new Grizzly Handler for Grafana contactPoints
func NewAlertContactPointHandler(provider grizzly.Provider) *AlertContactPointHandler {
return &AlertContactPointHandler{
BaseHandler: grizzly.NewBaseHandler(provider, "AlertContactPoint", false),
BaseHandler: grizzly.NewBaseHandler(provider, AlertContactPointKind, false),
}
}

Expand Down
Loading
Loading