diff --git a/pkg/grafana/notificationtemplate-handler.go b/pkg/grafana/notificationtemplate-handler.go new file mode 100644 index 00000000..3d90e5af --- /dev/null +++ b/pkg/grafana/notificationtemplate-handler.go @@ -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) +} diff --git a/pkg/grafana/notificationtemplate-proxy.go b/pkg/grafana/notificationtemplate-proxy.go new file mode 100644 index 00000000..1e59ec7f --- /dev/null +++ b/pkg/grafana/notificationtemplate-proxy.go @@ -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"), + }, + }) + } +} diff --git a/pkg/grafana/provider.go b/pkg/grafana/provider.go index 374eac2a..1c2e22a6 100644 --- a/pkg/grafana/provider.go +++ b/pkg/grafana/provider.go @@ -140,6 +140,7 @@ func (p *Provider) GetHandlers() []grizzly.Handler { NewAlertRuleGroupHandler(p), NewAlertNotificationPolicyHandler(p), NewAlertContactPointHandler(p), + NewAlertNotificationTemplateHandler(p), } } diff --git a/pkg/grizzly/embed/templates/proxy/index.html.tmpl b/pkg/grizzly/embed/templates/proxy/index.html.tmpl index fb87d0e9..7fda44a7 100644 --- a/pkg/grizzly/embed/templates/proxy/index.html.tmpl +++ b/pkg/grizzly/embed/templates/proxy/index.html.tmpl @@ -54,13 +54,25 @@
  • {{ .Spec.title }}
  • {{ end }} + +

    Alert notification templates

    + + diff --git a/pkg/grizzly/server.go b/pkg/grizzly/server.go index b0355a39..22f019f8 100644 --- a/pkg/grizzly/server.go +++ b/pkg/grizzly/server.go @@ -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",