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

Add support for alert notification templates #531

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
143 changes: 143 additions & 0 deletions pkg/grafana/notificationtemplate-handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package grafana

import (
"errors"
"fmt"
"os"
"strings"

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

const KindAlertNotificationTemplate = "AlertNotificationTemplate"

const notificationTemplatePattern = "alert-notification-templates/notificationTemplate-%s.%s"

// AlertNotificationTemplateHandler is a Grizzly Handler for Grafana contactPoints
type AlertNotificationTemplateHandler struct {
grizzly.BaseHandler
}

// NewAlertNotificationTemplateHandler returns a new Grizzly Handler for Grafana contactPoints
func NewAlertNotificationTemplateHandler(provider grizzly.Provider) *AlertNotificationTemplateHandler {
return &AlertNotificationTemplateHandler{
BaseHandler: grizzly.NewBaseHandler(provider, KindAlertNotificationTemplate, false),
}
}

// ProxyConfigurator provides a configurator object describing how to proxy folders.
func (h *AlertNotificationTemplateHandler) ProxyConfigurator() grizzly.ProxyConfigurator {
return &alertNotificationTemplateProxyConfigurator{
provider: h.Provider,
}
}

// ResourceFilePath returns the location on disk where a resource should be updated
func (h *AlertNotificationTemplateHandler) ResourceFilePath(resource grizzly.Resource, filetype string) string {
filename := strings.ReplaceAll(resource.Name(), string(os.PathSeparator), "-")
return fmt.Sprintf(notificationTemplatePattern, filename, filetype)
}

// Prepare gets a resource ready for dispatch to the remote endpoint
func (h *AlertNotificationTemplateHandler) Prepare(existing *grizzly.Resource, resource grizzly.Resource) *grizzly.Resource {
if !resource.HasSpecString("name") {
resource.SetSpecString("name", resource.Name())
}

return &resource
}

func (h *AlertNotificationTemplateHandler) Validate(resource grizzly.Resource) error {
name, exist := resource.GetSpecString("name")
if resource.Name() != name && exist {
return fmt.Errorf("spec.name '%s' and metadata.name '%s', don't match", name, resource.Name())
}
return nil
}

func (h *AlertNotificationTemplateHandler) GetSpecUID(resource grizzly.Resource) (string, error) {
name, ok := resource.GetSpecString("name")
if !ok {
return "", fmt.Errorf("name not specified")
}
return name, nil
}

func (h *AlertNotificationTemplateHandler) GetByUID(uid string) (*grizzly.Resource, error) {
client, err := h.Provider.(ClientProvider).Client()
if err != nil {
return nil, err
}

response, err := client.Provisioning.GetTemplate(uid)
if err != nil {
var gErr *provisioning.GetTemplateNotFound
if errors.As(err, &gErr) {
return nil, grizzly.ErrNotFound
}
return nil, err
}

spec, err := structToMap(response.GetPayload())
if err != nil {
return nil, err
}

resource, err := grizzly.NewResource(h.APIVersion(), h.Kind(), uid, spec)
if err != nil {
return nil, err
}

return &resource, nil
}

// GetRemote retrieves a contactPoint as a Resource
func (h *AlertNotificationTemplateHandler) GetRemote(resource grizzly.Resource) (*grizzly.Resource, error) {
return h.GetByUID(resource.Name())
}

// ListRemote retrieves as list of UIDs of all remote resources
func (h *AlertNotificationTemplateHandler) ListRemote() ([]string, error) {
client, err := h.Provider.(ClientProvider).Client()
if err != nil {
return nil, err
}

response, err := client.Provisioning.GetTemplates()
if err != nil {
return nil, err
}
templates := response.GetPayload()
uids := make([]string, 0, len(templates))
for _, template := range templates {
uids = append(uids, template.Name)
}
return uids, nil
}

// Add pushes a contactPoint to Grafana via the API
func (h *AlertNotificationTemplateHandler) Add(resource grizzly.Resource) error {
client, err := h.Provider.(ClientProvider).Client()
if err != nil {
return err
}

templateBody, _ := resource.GetSpecString("template")

params := provisioning.NewPutTemplateParams().
WithName(resource.Name()).
WithBody(&models.NotificationTemplateContent{
Template: templateBody,
}).
WithXDisableProvenance(&stringtrue)
_, err = client.Provisioning.PutTemplate(params)
return err
}

// Update pushes a contactPoint to Grafana via the API
func (h *AlertNotificationTemplateHandler) Update(existing, resource grizzly.Resource) error {
// Add calls the "PUT" endpoint, allowing us to create or update a template.
return h.Add(resource)
}
93 changes: 93 additions & 0 deletions pkg/grafana/notificationtemplate-proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package grafana

import (
"fmt"
"net/http"

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

var _ grizzly.ProxyConfigurator = &alertNotificationTemplateProxyConfigurator{}

// alertNotificationTemplateProxyConfigurator describes how to proxy AlertNotificationTemplate resources.
type alertNotificationTemplateProxyConfigurator struct {
provider grizzly.Provider
}

func (c *alertNotificationTemplateProxyConfigurator) ProxyURL(uid string) string {
return fmt.Sprintf("/alerting/notifications/templates/%s/edit", uid)
}

func (c *alertNotificationTemplateProxyConfigurator) Endpoints(s grizzly.Server) []grizzly.HTTPEndpoint {
return []grizzly.HTTPEndpoint{
{
Method: http.MethodGet,
URL: "/alerting/notifications/templates/{template_uid}/edit",
Handler: authenticateAndProxyHandler(s, c.provider),
},
// Depending on the Grafana version, the frontend can call either of these endpoints
{
Method: http.MethodGet,
URL: "/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/{namespace}/templategroups/{template_uid}",
Handler: c.templateGetAsK8S(s),
},
{
Method: http.MethodGet,
URL: "/api/alertmanager/grafana/config/api/v1/alerts",
Handler: c.alertManagerConfigGet(s),
},
}
}

func (c *alertNotificationTemplateProxyConfigurator) StaticEndpoints() grizzly.StaticProxyConfig {
return grizzly.StaticProxyConfig{
ProxyPost: []string{
"/api/alertmanager/grafana/config/api/v1/templates/test",
},
}
}

func (c *alertNotificationTemplateProxyConfigurator) alertManagerConfigGet(s grizzly.Server) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
templates := s.Resources.OfKind(KindAlertNotificationTemplate).AsList()

templatesMap := make(map[string]any, len(templates))
for _, template := range templates {
templatesMap[template.Name()] = template.GetSpecValue("template")
}

httputils.WriteJSON(w, map[string]any{
"template_files": templatesMap,
"alertmanager_config": map[string]any{},
})
}
}

func (c *alertNotificationTemplateProxyConfigurator) templateGetAsK8S(s grizzly.Server) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
templateUID := chi.URLParam(r, "template_uid")

template, found := s.Resources.Find(grizzly.NewResourceRef(KindAlertNotificationTemplate, templateUID))
if !found {
httputils.Error(w, fmt.Sprintf("Alert notification template with UID %s not found", templateUID), fmt.Errorf("alert notification template with UID %s not found", templateUID), http.StatusNotFound)
return
}

httputils.WriteJSON(w, map[string]any{
"kind": "TemplateGroup",
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"metadata": map[string]any{
"name": templateUID,
"uid": templateUID,
"namespace": chi.URLParam(r, "namespace"),
"resourceVersion": "resource-version",
},
"spec": map[string]any{
"title": templateUID,
"content": template.GetSpecValue("template"),
},
})
}
}
1 change: 1 addition & 0 deletions pkg/grafana/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ func (p *Provider) GetHandlers() []grizzly.Handler {
NewAlertRuleGroupHandler(p),
NewAlertNotificationPolicyHandler(p),
NewAlertContactPointHandler(p),
NewAlertNotificationTemplateHandler(p),
}
}

Expand Down
18 changes: 15 additions & 3 deletions pkg/grizzly/embed/templates/proxy/index.html.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,25 @@
<li>
{{ .Spec.title }}
<ul>
{{ range .Spec.rules }}
<li><a href="/grizzly/AlertRuleGroup/{{ .uid }}">{{ .title }}</a></li>
{{ end }}
{{ range .Spec.rules }}
<li><a href="/grizzly/AlertRuleGroup/{{ .uid }}">{{ .title }}</a></li>
{{ end }}
</ul>
</li>
{{ end }}
</ul>

<h1>Alert notification templates</h1>

<ul>
{{ range (.Resources.OfKind "AlertNotificationTemplate").AsList }}
<li>
<a href="/grizzly/{{ .Kind }}/{{ .Name }}">{{ .Spec.name }}</a>
</li>
{{ else }}
<li>No notification templates.</li>
{{ end }}
</ul>
</div>
</main>
</body>
Expand Down
1 change: 1 addition & 0 deletions pkg/grizzly/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ func (s *Server) staticProxyConfig() StaticProxyConfig {
"/api/usage/*": "[]",
"/api/frontend/assets": "{}",
"/api/org/preferences": "{}",
"/api/org/users": "[]",

"/api/prometheus/grafana/api/v1/rules": `{
"status": "success",
Expand Down
Loading