diff --git a/emailtemplate.go b/emailtemplate.go new file mode 100644 index 0000000..68b2813 --- /dev/null +++ b/emailtemplate.go @@ -0,0 +1,129 @@ +package signalfx + +import ( + "bytes" + "context" + "encoding/json" + "io" + "io/ioutil" + "net/http" + "net/url" + "strconv" + + "github.com/signalfx/signalfx-go/emailtemplate" +) + +// EmailTemplateAPIURL is the base URL for interacting with email templates. +const EmailTemplateAPIURL = "/v2/alert/emailtemplate" + +// CreateEmailTemplate creates an email template. +func (c *Client) CreateEmailTemplate(ctx context.Context, template *emailtemplate.EmailTemplate) (*emailtemplate.EmailTemplate, error) { + payload, err := json.Marshal(template) + if err != nil { + return nil, err + } + + resp, err := c.doRequest(ctx, http.MethodPost, EmailTemplateAPIURL, nil, bytes.NewReader(payload)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err = newResponseError(resp, http.StatusCreated); err != nil { + return nil, err + } + + finalTemplate := &emailtemplate.EmailTemplate{} + err = json.NewDecoder(resp.Body).Decode(finalTemplate) + _, _ = io.Copy(ioutil.Discard, resp.Body) + + return finalTemplate, err +} + +// GetEmailTemplate gets an email template by ID. +func (c *Client) GetEmailTemplate(ctx context.Context, id string) (*emailtemplate.EmailTemplate, error) { + resp, err := c.doRequest(ctx, http.MethodGet, EmailTemplateAPIURL+"/"+id, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err = newResponseError(resp, http.StatusOK); err != nil { + return nil, err + } + + finalTemplate := &emailtemplate.EmailTemplate{} + err = json.NewDecoder(resp.Body).Decode(finalTemplate) + _, _ = io.Copy(ioutil.Discard, resp.Body) + + return finalTemplate, err +} + +// UpdateEmailTemplate updates an email template. +func (c *Client) UpdateEmailTemplate(ctx context.Context, id string, template *emailtemplate.EmailTemplate) (*emailtemplate.EmailTemplate, error) { + payload, err := json.Marshal(template) + if err != nil { + return nil, err + } + + resp, err := c.doRequest(ctx, http.MethodPut, EmailTemplateAPIURL+"/"+id, nil, bytes.NewReader(payload)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err = newResponseError(resp, http.StatusOK); err != nil { + return nil, err + } + + finalTemplate := &emailtemplate.EmailTemplate{} + err = json.NewDecoder(resp.Body).Decode(finalTemplate) + _, _ = io.Copy(ioutil.Discard, resp.Body) + + return finalTemplate, err +} + +// DeleteEmailTemplate deletes an email template. +func (c *Client) DeleteEmailTemplate(ctx context.Context, id string) error { + resp, err := c.doRequest(ctx, http.MethodDelete, EmailTemplateAPIURL+"/"+id, nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + if err = newResponseError(resp, http.StatusNoContent); err != nil { + return err + } + _, _ = io.Copy(ioutil.Discard, resp.Body) + + return nil +} + +// SearchEmailTemplates searches email templates by name with pagination. +func (c *Client) SearchEmailTemplates(ctx context.Context, limit int, name string, offset int, orderBy string) (*emailtemplate.SearchResult, error) { + params := url.Values{} + params.Add("limit", strconv.Itoa(limit)) + params.Add("offset", strconv.Itoa(offset)) + if name != "" { + params.Add("name", name) + } + if orderBy != "" { + params.Add("orderBy", orderBy) + } + + resp, err := c.doRequest(ctx, http.MethodGet, EmailTemplateAPIURL, params, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err = newResponseError(resp, http.StatusOK); err != nil { + return nil, err + } + + finalTemplates := &emailtemplate.SearchResult{} + err = json.NewDecoder(resp.Body).Decode(finalTemplates) + _, _ = io.Copy(ioutil.Discard, resp.Body) + + return finalTemplates, err +} diff --git a/emailtemplate/model_email_template.go b/emailtemplate/model_email_template.go new file mode 100644 index 0000000..2acf1d8 --- /dev/null +++ b/emailtemplate/model_email_template.go @@ -0,0 +1,25 @@ +package emailtemplate + +// EmailTemplate represents a reusable detector alert email template. +type EmailTemplate struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + TriggerSubject string `json:"triggerSubject,omitempty"` + TriggerBody string `json:"triggerBody,omitempty"` + ResolvedSubject string `json:"resolvedSubject,omitempty"` + ResolvedBody string `json:"resolvedBody,omitempty"` + To []string `json:"to,omitempty"` + Cc []string `json:"cc,omitempty"` + Bcc []string `json:"bcc,omitempty"` + CustomHeaders map[string]string `json:"customHeaders,omitempty"` + CreatedOnMs int64 `json:"createdOnMs,omitempty"` + CreatedBy string `json:"createdBy,omitempty"` + UpdatedOnMs int64 `json:"updatedOnMs,omitempty"` + UpdatedBy string `json:"updatedBy,omitempty"` +} + +// SearchResult is the paginated email-template search response. +type SearchResult struct { + Count int `json:"count,omitempty"` + Results []*EmailTemplate `json:"results,omitempty"` +} diff --git a/emailtemplate_test.go b/emailtemplate_test.go new file mode 100644 index 0000000..286a585 --- /dev/null +++ b/emailtemplate_test.go @@ -0,0 +1,113 @@ +package signalfx + +import ( + "context" + "net/http" + "net/url" + "strconv" + "testing" + + "github.com/signalfx/signalfx-go/emailtemplate" + "github.com/stretchr/testify/assert" +) + +const emailTemplateRequestBody = `{ + "name": "Terraform email template", + "triggerSubject": "{{ruleSeverity}} Alert: {{{ruleName}}}", + "triggerBody": "Triggered at {{timestamp}}", + "resolvedSubject": "{{ruleSeverity}} Resolved: {{{ruleName}}}", + "resolvedBody": "Resolved at {{timestamp}}", + "to": ["alerts@example.com"], + "cc": ["oncall@example.com"], + "bcc": ["audit@example.com"], + "customHeaders": { + "X-Test-Header": "terraform" + } +}` + +func testEmailTemplateRequest() *emailtemplate.EmailTemplate { + return &emailtemplate.EmailTemplate{ + Name: "Terraform email template", + TriggerSubject: "{{ruleSeverity}} Alert: {{{ruleName}}}", + TriggerBody: "Triggered at {{timestamp}}", + ResolvedSubject: "{{ruleSeverity}} Resolved: {{{ruleName}}}", + ResolvedBody: "Resolved at {{timestamp}}", + To: []string{"alerts@example.com"}, + Cc: []string{"oncall@example.com"}, + Bcc: []string{"audit@example.com"}, + CustomHeaders: map[string]string{ + "X-Test-Header": "terraform", + }, + } +} + +func TestCreateEmailTemplate(t *testing.T) { + teardown := setup() + defer teardown() + + mux.HandleFunc("/v2/alert/emailtemplate", verifyRequestWithJsonBody(t, "POST", true, http.StatusCreated, nil, emailTemplateRequestBody, "emailtemplate/create_success.json")) + + result, err := client.CreateEmailTemplate(context.Background(), testEmailTemplateRequest()) + assert.NoError(t, err, "Unexpected error creating email template") + assert.Equal(t, "template-id", result.Id, "ID does not match") + assert.Equal(t, "Terraform email template", result.Name, "Name does not match") + assert.Equal(t, []string{"alerts@example.com"}, result.To, "To recipients do not match") + assert.Equal(t, "terraform", result.CustomHeaders["X-Test-Header"], "Custom header does not match") +} + +func TestGetEmailTemplate(t *testing.T) { + teardown := setup() + defer teardown() + + mux.HandleFunc("/v2/alert/emailtemplate/template-id", verifyRequest(t, "GET", true, http.StatusOK, nil, "emailtemplate/create_success.json")) + + result, err := client.GetEmailTemplate(context.Background(), "template-id") + assert.NoError(t, err, "Unexpected error getting email template") + assert.Equal(t, "template-id", result.Id, "ID does not match") + assert.Equal(t, "{{ruleSeverity}} Alert: {{{ruleName}}}", result.TriggerSubject, "Trigger subject does not match") +} + +func TestUpdateEmailTemplate(t *testing.T) { + teardown := setup() + defer teardown() + + mux.HandleFunc("/v2/alert/emailtemplate/template-id", verifyRequestWithJsonBody(t, "PUT", true, http.StatusOK, nil, emailTemplateRequestBody, "emailtemplate/update_success.json")) + + result, err := client.UpdateEmailTemplate(context.Background(), "template-id", testEmailTemplateRequest()) + assert.NoError(t, err, "Unexpected error updating email template") + assert.Equal(t, "Updated Terraform email template", result.Name, "Name does not match") + assert.Equal(t, int64(1710000100000), result.UpdatedOnMs, "Updated timestamp does not match") +} + +func TestDeleteEmailTemplate(t *testing.T) { + teardown := setup() + defer teardown() + + mux.HandleFunc("/v2/alert/emailtemplate/template-id", verifyRequest(t, "DELETE", true, http.StatusNoContent, nil, "")) + + err := client.DeleteEmailTemplate(context.Background(), "template-id") + assert.NoError(t, err, "Unexpected error deleting email template") +} + +func TestSearchEmailTemplates(t *testing.T) { + teardown := setup() + defer teardown() + + limit := 10 + name := "Terraform" + offset := 2 + orderBy := "-updatedOnMs" + params := url.Values{} + params.Add("limit", strconv.Itoa(limit)) + params.Add("offset", strconv.Itoa(offset)) + params.Add("name", name) + params.Add("orderBy", orderBy) + + mux.HandleFunc("/v2/alert/emailtemplate", verifyRequest(t, "GET", true, http.StatusOK, params, "emailtemplate/search_success.json")) + + results, err := client.SearchEmailTemplates(context.Background(), limit, name, offset, orderBy) + assert.NoError(t, err, "Unexpected error searching email templates") + assert.Equal(t, 1, results.Count, "Count does not match") + assert.Equal(t, 1, len(results.Results), "Results length does not match") + assert.Equal(t, "template-id", results.Results[0].Id, "Result ID does not match") +} diff --git a/notification/model_email_template_notification.go b/notification/model_email_template_notification.go new file mode 100644 index 0000000..47b2bac --- /dev/null +++ b/notification/model_email_template_notification.go @@ -0,0 +1,9 @@ +package notification + +// Notification properties for an alert sent through a centralized email template. +type EmailTemplateNotification struct { + // Tells SignalFx which system to use to send the notification. For an email template notification, this is always "EmailTemplate". + Type string `json:"type"` + // The SignalFx ID of the email template to render for this detector notification. + TemplateId string `json:"templateId"` +} diff --git a/notification/model_notification.go b/notification/model_notification.go index 63ed4bd..8166947 100644 --- a/notification/model_notification.go +++ b/notification/model_notification.go @@ -26,6 +26,8 @@ func (n *Notification) UnmarshalJSON(data []byte) error { n.Value = &BigPandaNotification{} case "Email": n.Value = &EmailNotification{} + case "EmailTemplate": + n.Value = &EmailTemplateNotification{} case "Jira": n.Value = &JiraNotification{} case "Office365": diff --git a/notification/model_notification_test.go b/notification/model_notification_test.go index 223dd7f..2602a92 100644 --- a/notification/model_notification_test.go +++ b/notification/model_notification_test.go @@ -48,7 +48,16 @@ func TestNotificationUnmarshaJSON(t *testing.T) { errVal: "", }, { - name: "Email with cc and bcc", + name: "EmailTemplate", + input: `{"type":"EmailTemplate","templateId":"template-id"}`, + expect: &Notification{Type: "EmailTemplate", Value: &EmailTemplateNotification{ + Type: "EmailTemplate", + TemplateId: "template-id", + }}, + errVal: "", + }, + { + name: "Email with cc and bcc", input: `{"type":"Email","email":"alerts@example.com","cc":["oncall@example.com","ops@example.com"],"bcc":["audit@example.com"]}`, expect: &Notification{Type: "Email", Value: &EmailNotification{ Type: "Email", diff --git a/testdata/fixtures/emailtemplate/create_success.json b/testdata/fixtures/emailtemplate/create_success.json new file mode 100644 index 0000000..536b24c --- /dev/null +++ b/testdata/fixtures/emailtemplate/create_success.json @@ -0,0 +1,18 @@ +{ + "id": "template-id", + "name": "Terraform email template", + "triggerSubject": "{{ruleSeverity}} Alert: {{{ruleName}}}", + "triggerBody": "Triggered at {{timestamp}}", + "resolvedSubject": "{{ruleSeverity}} Resolved: {{{ruleName}}}", + "resolvedBody": "Resolved at {{timestamp}}", + "to": ["alerts@example.com"], + "cc": ["oncall@example.com"], + "bcc": ["audit@example.com"], + "customHeaders": { + "X-Test-Header": "terraform" + }, + "createdOnMs": 1710000000000, + "createdBy": "creator-id", + "updatedOnMs": 1710000000000, + "updatedBy": "creator-id" +} diff --git a/testdata/fixtures/emailtemplate/search_success.json b/testdata/fixtures/emailtemplate/search_success.json new file mode 100644 index 0000000..ee1121a --- /dev/null +++ b/testdata/fixtures/emailtemplate/search_success.json @@ -0,0 +1,23 @@ +{ + "count": 1, + "results": [ + { + "id": "template-id", + "name": "Terraform email template", + "triggerSubject": "{{ruleSeverity}} Alert: {{{ruleName}}}", + "triggerBody": "Triggered at {{timestamp}}", + "resolvedSubject": "{{ruleSeverity}} Resolved: {{{ruleName}}}", + "resolvedBody": "Resolved at {{timestamp}}", + "to": ["alerts@example.com"], + "cc": ["oncall@example.com"], + "bcc": ["audit@example.com"], + "customHeaders": { + "X-Test-Header": "terraform" + }, + "createdOnMs": 1710000000000, + "createdBy": "creator-id", + "updatedOnMs": 1710000000000, + "updatedBy": "creator-id" + } + ] +} diff --git a/testdata/fixtures/emailtemplate/update_success.json b/testdata/fixtures/emailtemplate/update_success.json new file mode 100644 index 0000000..acbd71a --- /dev/null +++ b/testdata/fixtures/emailtemplate/update_success.json @@ -0,0 +1,18 @@ +{ + "id": "template-id", + "name": "Updated Terraform email template", + "triggerSubject": "{{ruleSeverity}} Alert: {{{ruleName}}}", + "triggerBody": "Triggered at {{timestamp}}", + "resolvedSubject": "{{ruleSeverity}} Resolved: {{{ruleName}}}", + "resolvedBody": "Resolved at {{timestamp}}", + "to": ["alerts@example.com"], + "cc": ["oncall@example.com"], + "bcc": ["audit@example.com"], + "customHeaders": { + "X-Test-Header": "terraform" + }, + "createdOnMs": 1710000000000, + "createdBy": "creator-id", + "updatedOnMs": 1710000100000, + "updatedBy": "updater-id" +}