Adding upstream version 0.8.9.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
3b2c48b5e4
commit
c0c4addb85
285 changed files with 25880 additions and 0 deletions
160
pkg/services/opsgenie/opsgenie.go
Normal file
160
pkg/services/opsgenie/opsgenie.go
Normal file
|
@ -0,0 +1,160 @@
|
|||
package opsgenie
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/format"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/services/standard"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
)
|
||||
|
||||
// alertEndpointTemplate is the OpsGenie API endpoint template for sending alerts.
|
||||
const (
|
||||
alertEndpointTemplate = "https://%s:%d/v2/alerts"
|
||||
MaxMessageLength = 130 // MaxMessageLength is the maximum length of the alert message field in OpsGenie.
|
||||
httpSuccessMax = 299 // httpSuccessMax is the maximum HTTP status code for a successful response.
|
||||
defaultHTTPTimeout = 10 * time.Second // defaultHTTPTimeout is the default timeout for HTTP requests.
|
||||
)
|
||||
|
||||
// ErrUnexpectedStatus indicates that OpsGenie returned an unexpected HTTP status code.
|
||||
var ErrUnexpectedStatus = errors.New("OpsGenie notification returned unexpected HTTP status code")
|
||||
|
||||
// Service provides OpsGenie as a notification service.
|
||||
type Service struct {
|
||||
standard.Standard
|
||||
Config *Config
|
||||
pkr format.PropKeyResolver
|
||||
}
|
||||
|
||||
// sendAlert sends an alert to OpsGenie using the specified URL and API key.
|
||||
func (service *Service) sendAlert(url string, apiKey string, payload AlertPayload) error {
|
||||
jsonBody, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling alert payload to JSON: %w", err)
|
||||
}
|
||||
|
||||
jsonBuffer := bytes.NewBuffer(jsonBody)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultHTTPTimeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, jsonBuffer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating HTTP request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "GenieKey "+apiKey)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send notification to OpsGenie: %w", err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode > httpSuccessMax {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"%w: %d, cannot read body: %w",
|
||||
ErrUnexpectedStatus,
|
||||
resp.StatusCode,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: %d - %s", ErrUnexpectedStatus, resp.StatusCode, body)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize configures the service with a URL and logger.
|
||||
func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) error {
|
||||
service.SetLogger(logger)
|
||||
service.Config = &Config{}
|
||||
service.pkr = format.NewPropKeyResolver(service.Config)
|
||||
|
||||
return service.Config.setURL(&service.pkr, configURL)
|
||||
}
|
||||
|
||||
// GetID returns the service identifier.
|
||||
func (service *Service) GetID() string {
|
||||
return Scheme
|
||||
}
|
||||
|
||||
// Send delivers a notification message to OpsGenie.
|
||||
// See: https://docs.opsgenie.com/docs/alert-api#create-alert
|
||||
func (service *Service) Send(message string, params *types.Params) error {
|
||||
config := service.Config
|
||||
endpointURL := fmt.Sprintf(alertEndpointTemplate, config.Host, config.Port)
|
||||
|
||||
payload, err := service.newAlertPayload(message, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return service.sendAlert(endpointURL, config.APIKey, payload)
|
||||
}
|
||||
|
||||
// newAlertPayload creates a new alert payload for OpsGenie based on the message and parameters.
|
||||
func (service *Service) newAlertPayload(
|
||||
message string,
|
||||
params *types.Params,
|
||||
) (AlertPayload, error) {
|
||||
if params == nil {
|
||||
params = &types.Params{}
|
||||
}
|
||||
|
||||
// Defensive copy
|
||||
payloadFields := *service.Config
|
||||
|
||||
if err := service.pkr.UpdateConfigFromParams(&payloadFields, params); err != nil {
|
||||
return AlertPayload{}, fmt.Errorf("updating payload fields from params: %w", err)
|
||||
}
|
||||
|
||||
// Use `Message` for the title if available, or if the message is too long
|
||||
// Use `Description` for the message in these scenarios
|
||||
title := payloadFields.Title
|
||||
description := message
|
||||
|
||||
if title == "" {
|
||||
if len(message) > MaxMessageLength {
|
||||
title = message[:MaxMessageLength]
|
||||
} else {
|
||||
title = message
|
||||
description = ""
|
||||
}
|
||||
}
|
||||
|
||||
if payloadFields.Description != "" && description != "" {
|
||||
description += "\n"
|
||||
}
|
||||
|
||||
result := AlertPayload{
|
||||
Message: title,
|
||||
Alias: payloadFields.Alias,
|
||||
Description: description + payloadFields.Description,
|
||||
Responders: payloadFields.Responders,
|
||||
VisibleTo: payloadFields.VisibleTo,
|
||||
Actions: payloadFields.Actions,
|
||||
Tags: payloadFields.Tags,
|
||||
Details: payloadFields.Details,
|
||||
Entity: payloadFields.Entity,
|
||||
Source: payloadFields.Source,
|
||||
Priority: payloadFields.Priority,
|
||||
User: payloadFields.User,
|
||||
Note: payloadFields.Note,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
109
pkg/services/opsgenie/opsgenie_config.go
Normal file
109
pkg/services/opsgenie/opsgenie_config.go
Normal file
|
@ -0,0 +1,109 @@
|
|||
package opsgenie
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/format"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPort = 443 // defaultPort is the default port for OpsGenie API connections.
|
||||
Scheme = "opsgenie" // Scheme is the identifying part of this service's configuration URL.
|
||||
)
|
||||
|
||||
// ErrAPIKeyMissing indicates that the API key is missing from the config URL path.
|
||||
var ErrAPIKeyMissing = errors.New("API key missing from config URL path")
|
||||
|
||||
// Config holds the configuration for the OpsGenie service.
|
||||
type Config struct {
|
||||
APIKey string `desc:"The OpsGenie API key" url:"path"`
|
||||
Host string `desc:"The OpsGenie API host. Use 'api.eu.opsgenie.com' for EU instances" url:"host" default:"api.opsgenie.com"`
|
||||
Port uint16 `desc:"The OpsGenie API port." url:"port" default:"443"`
|
||||
Alias string `desc:"Client-defined identifier of the alert" key:"alias" optional:"true"`
|
||||
Description string `desc:"Description field of the alert" key:"description" optional:"true"`
|
||||
Responders []Entity `desc:"Teams, users, escalations and schedules that the alert will be routed to send notifications" key:"responders" optional:"true"`
|
||||
VisibleTo []Entity `desc:"Teams and users that the alert will become visible to without sending any notification" key:"visibleTo" optional:"true"`
|
||||
Actions []string `desc:"Custom actions that will be available for the alert" key:"actions" optional:"true"`
|
||||
Tags []string `desc:"Tags of the alert" key:"tags" optional:"true"`
|
||||
Details map[string]string `desc:"Map of key-value pairs to use as custom properties of the alert" key:"details" optional:"true"`
|
||||
Entity string `desc:"Entity field of the alert that is generally used to specify which domain the Source field of the alert" key:"entity" optional:"true"`
|
||||
Source string `desc:"Source field of the alert" key:"source" optional:"true"`
|
||||
Priority string `desc:"Priority level of the alert. Possible values are P1, P2, P3, P4 and P5" key:"priority" optional:"true"`
|
||||
Note string `desc:"Additional note that will be added while creating the alert" key:"note" optional:"true"`
|
||||
User string `desc:"Display name of the request owner" key:"user" optional:"true"`
|
||||
Title string `desc:"notification title, optionally set by the sender" default:"" key:"title"`
|
||||
}
|
||||
|
||||
// Enums returns an empty map because the OpsGenie service doesn't use Enums.
|
||||
func (config *Config) Enums() map[string]types.EnumFormatter {
|
||||
return map[string]types.EnumFormatter{}
|
||||
}
|
||||
|
||||
// GetURL returns a URL representation of the Config's current field values.
|
||||
func (config *Config) GetURL() *url.URL {
|
||||
resolver := format.NewPropKeyResolver(config)
|
||||
|
||||
return config.getURL(&resolver)
|
||||
}
|
||||
|
||||
// getURL constructs a URL from the Config's fields using the provided resolver.
|
||||
func (config *Config) getURL(resolver types.ConfigQueryResolver) *url.URL {
|
||||
var host string
|
||||
if config.Port > 0 {
|
||||
host = fmt.Sprintf("%s:%d", config.Host, config.Port)
|
||||
} else {
|
||||
host = config.Host
|
||||
}
|
||||
|
||||
result := &url.URL{
|
||||
Host: host,
|
||||
Path: "/" + config.APIKey,
|
||||
Scheme: Scheme,
|
||||
RawQuery: format.BuildQuery(resolver),
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// SetURL updates the Config from a URL representation of its field values.
|
||||
func (config *Config) SetURL(url *url.URL) error {
|
||||
resolver := format.NewPropKeyResolver(config)
|
||||
|
||||
return config.setURL(&resolver, url)
|
||||
}
|
||||
|
||||
// setURL updates the Config from a URL using the provided resolver.
|
||||
func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) error {
|
||||
config.Host = url.Hostname()
|
||||
|
||||
if url.String() != "opsgenie://dummy@dummy.com" {
|
||||
if len(url.Path) > 0 {
|
||||
config.APIKey = url.Path[1:]
|
||||
} else {
|
||||
return ErrAPIKeyMissing
|
||||
}
|
||||
}
|
||||
|
||||
if url.Port() != "" {
|
||||
port, err := strconv.ParseUint(url.Port(), 10, 16)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing port %q: %w", url.Port(), err)
|
||||
}
|
||||
|
||||
config.Port = uint16(port)
|
||||
} else {
|
||||
config.Port = defaultPort
|
||||
}
|
||||
|
||||
for key, vals := range url.Query() {
|
||||
if err := resolver.Set(key, vals[0]); err != nil {
|
||||
return fmt.Errorf("setting query parameter %q to %q: %w", key, vals[0], err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
93
pkg/services/opsgenie/opsgenie_entity.go
Normal file
93
pkg/services/opsgenie/opsgenie_entity.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
package opsgenie
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EntityPartsCount is the expected number of parts in an entity string (type:identifier).
|
||||
const (
|
||||
EntityPartsCount = 2 // Expected number of parts in an entity string (type:identifier)
|
||||
)
|
||||
|
||||
// ErrInvalidEntityFormat indicates that the entity string does not have two elements separated by a colon.
|
||||
var (
|
||||
ErrInvalidEntityFormat = errors.New(
|
||||
"invalid entity, should have two elements separated by colon",
|
||||
)
|
||||
ErrInvalidEntityIDName = errors.New("invalid entity, cannot parse id/name")
|
||||
ErrUnexpectedEntityType = errors.New("invalid entity, unexpected entity type")
|
||||
ErrMissingEntityIdentity = errors.New("invalid entity, should have either ID, name or username")
|
||||
)
|
||||
|
||||
// Entity represents an OpsGenie entity (e.g., user, team) with type and identifier.
|
||||
// Example JSON: { "username":"trinity@opsgenie.com", "type":"user" }.
|
||||
type Entity struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
}
|
||||
|
||||
// SetFromProp deserializes an entity from a string in the format "type:identifier".
|
||||
func (e *Entity) SetFromProp(propValue string) error {
|
||||
elements := strings.Split(propValue, ":")
|
||||
|
||||
if len(elements) != EntityPartsCount {
|
||||
return fmt.Errorf("%w: %q", ErrInvalidEntityFormat, propValue)
|
||||
}
|
||||
|
||||
e.Type = elements[0]
|
||||
identifier := elements[1]
|
||||
|
||||
isID, err := isOpsGenieID(identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %q", ErrInvalidEntityIDName, identifier)
|
||||
}
|
||||
|
||||
switch {
|
||||
case isID:
|
||||
e.ID = identifier
|
||||
case e.Type == "team":
|
||||
e.Name = identifier
|
||||
case e.Type == "user":
|
||||
e.Username = identifier
|
||||
default:
|
||||
return fmt.Errorf("%w: %q", ErrUnexpectedEntityType, e.Type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPropValue serializes an entity back into a string in the format "type:identifier".
|
||||
func (e *Entity) GetPropValue() (string, error) {
|
||||
var identifier string
|
||||
|
||||
switch {
|
||||
case e.ID != "":
|
||||
identifier = e.ID
|
||||
case e.Name != "":
|
||||
identifier = e.Name
|
||||
case e.Username != "":
|
||||
identifier = e.Username
|
||||
default:
|
||||
return "", ErrMissingEntityIdentity
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%s", e.Type, identifier), nil
|
||||
}
|
||||
|
||||
// isOpsGenieID checks if a string matches the OpsGenie ID format (e.g., 4513b7ea-3b91-438f-b7e4-e3e54af9147c).
|
||||
func isOpsGenieID(str string) (bool, error) {
|
||||
matched, err := regexp.MatchString(
|
||||
`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`,
|
||||
str,
|
||||
)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("matching OpsGenie ID format for %q: %w", str, err)
|
||||
}
|
||||
|
||||
return matched, nil
|
||||
}
|
33
pkg/services/opsgenie/opsgenie_json.go
Normal file
33
pkg/services/opsgenie/opsgenie_json.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package opsgenie
|
||||
|
||||
// AlertPayload represents the payload being sent to the OpsGenie API
|
||||
//
|
||||
// See: https://docs.opsgenie.com/docs/alert-api#create-alert
|
||||
//
|
||||
// Some fields contain complex values like arrays and objects.
|
||||
// Because `params` are strings only we cannot pass in slices
|
||||
// or maps. Instead we "preserve" the JSON in those fields. That
|
||||
// way we can pass in complex types as JSON like so:
|
||||
//
|
||||
// service.Send("An example alert message", &types.Params{
|
||||
// "alias": "Life is too short for no alias",
|
||||
// "description": "Every alert needs a description",
|
||||
// "responders": `[{"id":"4513b7ea-3b91-438f-b7e4-e3e54af9147c","type":"team"},{"name":"NOC","type":"team"}]`,
|
||||
// "visibleTo": `[{"id":"4513b7ea-3b91-438f-b7e4-e3e54af9147c","type":"team"},{"name":"rocket_team","type":"team"}]`,
|
||||
// "details": `{"key1": "value1", "key2": "value2"}`,
|
||||
// })
|
||||
type AlertPayload struct {
|
||||
Message string `json:"message"`
|
||||
Alias string `json:"alias,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Responders []Entity `json:"responders,omitempty"`
|
||||
VisibleTo []Entity `json:"visibleTo,omitempty"`
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Details map[string]string `json:"details,omitempty"`
|
||||
Entity string `json:"entity,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Priority string `json:"priority,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
422
pkg/services/opsgenie/opsgenie_test.go
Normal file
422
pkg/services/opsgenie/opsgenie_test.go
Normal file
|
@ -0,0 +1,422 @@
|
|||
package opsgenie
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/onsi/ginkgo/v2"
|
||||
"github.com/onsi/gomega"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
)
|
||||
|
||||
const (
|
||||
mockAPIKey = "eb243592-faa2-4ba2-a551q-1afdf565c889"
|
||||
mockHost = "api.opsgenie.com"
|
||||
)
|
||||
|
||||
func TestOpsGenie(t *testing.T) {
|
||||
gomega.RegisterFailHandler(ginkgo.Fail)
|
||||
ginkgo.RunSpecs(t, "Shoutrrr OpsGenie Suite")
|
||||
}
|
||||
|
||||
var _ = ginkgo.Describe("the OpsGenie service", func() {
|
||||
var (
|
||||
// a simulated http server to mock out OpsGenie itself
|
||||
mockServer *httptest.Server
|
||||
// the host of our mock server
|
||||
mockHost string
|
||||
// function to check if the http request received by the mock server is as expected
|
||||
checkRequest func(body string, header http.Header)
|
||||
// the shoutrrr OpsGenie service
|
||||
service *Service
|
||||
// just a mock logger
|
||||
mockLogger *log.Logger
|
||||
)
|
||||
|
||||
ginkgo.BeforeEach(func() {
|
||||
// Initialize a mock http server
|
||||
httpHandler := func(_ http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
defer r.Body.Close()
|
||||
|
||||
checkRequest(string(body), r.Header)
|
||||
}
|
||||
mockServer = httptest.NewTLSServer(http.HandlerFunc(httpHandler))
|
||||
|
||||
// Our mock server doesn't have a valid cert
|
||||
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
// Determine the host of our mock http server
|
||||
mockServerURL, err := url.Parse(mockServer.URL)
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
mockHost = mockServerURL.Host
|
||||
|
||||
// Initialize a mock logger
|
||||
var buf bytes.Buffer
|
||||
mockLogger = log.New(&buf, "", 0)
|
||||
})
|
||||
|
||||
ginkgo.AfterEach(func() {
|
||||
mockServer.Close()
|
||||
})
|
||||
|
||||
ginkgo.Context("without query parameters", func() {
|
||||
ginkgo.BeforeEach(func() {
|
||||
// Initialize service
|
||||
serviceURL, err := url.Parse(fmt.Sprintf("opsgenie://%s/%s", mockHost, mockAPIKey))
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
service = &Service{}
|
||||
err = service.Initialize(serviceURL, mockLogger)
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
})
|
||||
|
||||
ginkgo.When("sending a simple alert", func() {
|
||||
ginkgo.It("should send a request to our mock OpsGenie server", func() {
|
||||
checkRequest = func(body string, header http.Header) {
|
||||
gomega.Expect(header["Authorization"][0]).
|
||||
To(gomega.Equal("GenieKey " + mockAPIKey))
|
||||
gomega.Expect(header["Content-Type"][0]).To(gomega.Equal("application/json"))
|
||||
gomega.Expect(body).To(gomega.Equal(`{"message":"hello world"}`))
|
||||
}
|
||||
|
||||
err := service.Send("hello world", &types.Params{})
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("sending an alert with runtime parameters", func() {
|
||||
ginkgo.It(
|
||||
"should send a request to our mock OpsGenie server with all fields populated from runtime parameters",
|
||||
func() {
|
||||
checkRequest = func(body string, header http.Header) {
|
||||
gomega.Expect(header["Authorization"][0]).
|
||||
To(gomega.Equal("GenieKey " + mockAPIKey))
|
||||
gomega.Expect(header["Content-Type"][0]).
|
||||
To(gomega.Equal("application/json"))
|
||||
gomega.Expect(body).To(gomega.Equal(`{"` +
|
||||
`message":"An example alert message",` +
|
||||
`"alias":"Life is too short for no alias",` +
|
||||
`"description":"Every alert needs a description",` +
|
||||
`"responders":[{"type":"team","id":"4513b7ea-3b91-438f-b7e4-e3e54af9147c"},{"type":"team","name":"NOC"},{"type":"user","username":"Donald"},{"type":"user","id":"696f0759-3b0f-4a15-b8c8-19d3dfca33f2"}],` +
|
||||
`"visibleTo":[{"type":"team","name":"rocket"}],` +
|
||||
`"actions":["action1","action2"],` +
|
||||
`"tags":["tag1","tag2"],` +
|
||||
`"details":{"key1":"value1","key2":"value2"},` +
|
||||
`"entity":"An example entity",` +
|
||||
`"source":"The source",` +
|
||||
`"priority":"P1",` +
|
||||
`"user":"Dracula",` +
|
||||
`"note":"Here is a note"` +
|
||||
`}`))
|
||||
}
|
||||
|
||||
err := service.Send("An example alert message", &types.Params{
|
||||
"alias": "Life is too short for no alias",
|
||||
"description": "Every alert needs a description",
|
||||
"responders": "team:4513b7ea-3b91-438f-b7e4-e3e54af9147c,team:NOC,user:Donald,user:696f0759-3b0f-4a15-b8c8-19d3dfca33f2",
|
||||
"visibleTo": "team:rocket",
|
||||
"actions": "action1,action2",
|
||||
"tags": "tag1,tag2",
|
||||
"details": "key1:value1,key2:value2",
|
||||
"entity": "An example entity",
|
||||
"source": "The source",
|
||||
"priority": "P1",
|
||||
"user": "Dracula",
|
||||
"note": "Here is a note",
|
||||
})
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Context("with query parameters", func() {
|
||||
ginkgo.BeforeEach(func() {
|
||||
// Initialize service
|
||||
serviceURL, err := url.Parse(
|
||||
fmt.Sprintf(
|
||||
`opsgenie://%s/%s?alias=query-alias&description=query-description&responders=team:query_team&visibleTo=user:query_user&actions=queryAction1,queryAction2&tags=queryTag1,queryTag2&details=queryKey1:queryValue1,queryKey2:queryValue2&entity=query-entity&source=query-source&priority=P2&user=query-user¬e=query-note`,
|
||||
mockHost,
|
||||
mockAPIKey,
|
||||
),
|
||||
)
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
service = &Service{}
|
||||
err = service.Initialize(serviceURL, mockLogger)
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
})
|
||||
|
||||
ginkgo.When("sending a simple alert", func() {
|
||||
ginkgo.It(
|
||||
"should send a request to our mock OpsGenie server with all fields populated from query parameters",
|
||||
func() {
|
||||
checkRequest = func(body string, header http.Header) {
|
||||
gomega.Expect(header["Authorization"][0]).
|
||||
To(gomega.Equal("GenieKey " + mockAPIKey))
|
||||
gomega.Expect(header["Content-Type"][0]).
|
||||
To(gomega.Equal("application/json"))
|
||||
gomega.Expect(body).To(gomega.Equal(`{` +
|
||||
`"message":"An example alert message",` +
|
||||
`"alias":"query-alias",` +
|
||||
`"description":"query-description",` +
|
||||
`"responders":[{"type":"team","name":"query_team"}],` +
|
||||
`"visibleTo":[{"type":"user","username":"query_user"}],` +
|
||||
`"actions":["queryAction1","queryAction2"],` +
|
||||
`"tags":["queryTag1","queryTag2"],` +
|
||||
`"details":{"queryKey1":"queryValue1","queryKey2":"queryValue2"},` +
|
||||
`"entity":"query-entity",` +
|
||||
`"source":"query-source",` +
|
||||
`"priority":"P2",` +
|
||||
`"user":"query-user",` +
|
||||
`"note":"query-note"` +
|
||||
`}`))
|
||||
}
|
||||
|
||||
err := service.Send("An example alert message", &types.Params{})
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
ginkgo.When("sending two alerts", func() {
|
||||
ginkgo.It("should not mix-up the runtime parameters and the query parameters", func() {
|
||||
// Internally the opsgenie service copies runtime parameters into the config struct
|
||||
// before generating the alert payload. This test ensures that none of the parameters
|
||||
// from alert 1 remain in the config struct when sending alert 2
|
||||
// In short: This tests if we clone the config struct
|
||||
|
||||
checkRequest = func(body string, header http.Header) {
|
||||
gomega.Expect(header["Authorization"][0]).
|
||||
To(gomega.Equal("GenieKey " + mockAPIKey))
|
||||
gomega.Expect(header["Content-Type"][0]).To(gomega.Equal("application/json"))
|
||||
gomega.Expect(body).To(gomega.Equal(`{"` +
|
||||
`message":"1",` +
|
||||
`"alias":"1",` +
|
||||
`"description":"1",` +
|
||||
`"responders":[{"type":"team","name":"1"}],` +
|
||||
`"visibleTo":[{"type":"team","name":"1"}],` +
|
||||
`"actions":["action1","action2"],` +
|
||||
`"tags":["tag1","tag2"],` +
|
||||
`"details":{"key1":"value1","key2":"value2"},` +
|
||||
`"entity":"1",` +
|
||||
`"source":"1",` +
|
||||
`"priority":"P1",` +
|
||||
`"user":"1",` +
|
||||
`"note":"1"` +
|
||||
`}`))
|
||||
}
|
||||
|
||||
err := service.Send("1", &types.Params{
|
||||
"alias": "1",
|
||||
"description": "1",
|
||||
"responders": "team:1",
|
||||
"visibleTo": "team:1",
|
||||
"actions": "action1,action2",
|
||||
"tags": "tag1,tag2",
|
||||
"details": "key1:value1,key2:value2",
|
||||
"entity": "1",
|
||||
"source": "1",
|
||||
"priority": "P1",
|
||||
"user": "1",
|
||||
"note": "1",
|
||||
})
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
checkRequest = func(body string, header http.Header) {
|
||||
gomega.Expect(header["Authorization"][0]).
|
||||
To(gomega.Equal("GenieKey " + mockAPIKey))
|
||||
gomega.Expect(header["Content-Type"][0]).To(gomega.Equal("application/json"))
|
||||
gomega.Expect(body).To(gomega.Equal(`{` +
|
||||
`"message":"2",` +
|
||||
`"alias":"query-alias",` +
|
||||
`"description":"query-description",` +
|
||||
`"responders":[{"type":"team","name":"query_team"}],` +
|
||||
`"visibleTo":[{"type":"user","username":"query_user"}],` +
|
||||
`"actions":["queryAction1","queryAction2"],` +
|
||||
`"tags":["queryTag1","queryTag2"],` +
|
||||
`"details":{"queryKey1":"queryValue1","queryKey2":"queryValue2"},` +
|
||||
`"entity":"query-entity",` +
|
||||
`"source":"query-source",` +
|
||||
`"priority":"P2",` +
|
||||
`"user":"query-user",` +
|
||||
`"note":"query-note"` +
|
||||
`}`))
|
||||
}
|
||||
|
||||
err = service.Send("2", nil)
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.It("should return the correct service ID", func() {
|
||||
service := &Service{}
|
||||
gomega.Expect(service.GetID()).To(gomega.Equal("opsgenie"))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = ginkgo.Describe("the OpsGenie Config struct", func() {
|
||||
ginkgo.When("generating a config from a simple URL", func() {
|
||||
ginkgo.It("should populate the config with host and apikey", func() {
|
||||
url, err := url.Parse(fmt.Sprintf("opsgenie://%s/%s", mockHost, mockAPIKey))
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
config := Config{}
|
||||
err = config.SetURL(url)
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
gomega.Expect(config.APIKey).To(gomega.Equal(mockAPIKey))
|
||||
gomega.Expect(config.Host).To(gomega.Equal(mockHost))
|
||||
gomega.Expect(config.Port).To(gomega.Equal(uint16(443)))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("generating a config from a url with port", func() {
|
||||
ginkgo.It("should populate the port field", func() {
|
||||
url, err := url.Parse(
|
||||
fmt.Sprintf("opsgenie://%s/%s", net.JoinHostPort(mockHost, "12345"), mockAPIKey),
|
||||
)
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
config := Config{}
|
||||
err = config.SetURL(url)
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
gomega.Expect(config.Port).To(gomega.Equal(uint16(12345)))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("generating a config from a url with query parameters", func() {
|
||||
ginkgo.It("should populate the config fields with the query parameter values", func() {
|
||||
queryParams := `alias=Life+is+too+short+for+no+alias&description=Every+alert+needs+a+description&actions=An+action&tags=tag1,tag2&details=key:value,key2:value2&entity=An+example+entity&source=The+source&priority=P1&user=Dracula¬e=Here+is+a+note&responders=user:Test,team:NOC&visibleTo=user:A+User`
|
||||
url, err := url.Parse(
|
||||
fmt.Sprintf(
|
||||
"opsgenie://%s/%s?%s",
|
||||
net.JoinHostPort(mockHost, "12345"),
|
||||
mockAPIKey,
|
||||
queryParams,
|
||||
),
|
||||
)
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
config := Config{}
|
||||
err = config.SetURL(url)
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
gomega.Expect(config.Alias).To(gomega.Equal("Life is too short for no alias"))
|
||||
gomega.Expect(config.Description).To(gomega.Equal("Every alert needs a description"))
|
||||
gomega.Expect(config.Responders).To(gomega.Equal([]Entity{
|
||||
{Type: "user", Username: "Test"},
|
||||
{Type: "team", Name: "NOC"},
|
||||
}))
|
||||
gomega.Expect(config.VisibleTo).To(gomega.Equal([]Entity{
|
||||
{Type: "user", Username: "A User"},
|
||||
}))
|
||||
gomega.Expect(config.Actions).To(gomega.Equal([]string{"An action"}))
|
||||
gomega.Expect(config.Tags).To(gomega.Equal([]string{"tag1", "tag2"}))
|
||||
gomega.Expect(config.Details).
|
||||
To(gomega.Equal(map[string]string{"key": "value", "key2": "value2"}))
|
||||
gomega.Expect(config.Entity).To(gomega.Equal("An example entity"))
|
||||
gomega.Expect(config.Source).To(gomega.Equal("The source"))
|
||||
gomega.Expect(config.Priority).To(gomega.Equal("P1"))
|
||||
gomega.Expect(config.User).To(gomega.Equal("Dracula"))
|
||||
gomega.Expect(config.Note).To(gomega.Equal("Here is a note"))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("generating a config from a url with differently escaped spaces", func() {
|
||||
ginkgo.It("should parse the escaped spaces correctly", func() {
|
||||
// Use: '%20', '+' and a normal space
|
||||
queryParams := `alias=Life is+too%20short+for+no+alias`
|
||||
url, err := url.Parse(
|
||||
fmt.Sprintf(
|
||||
"opsgenie://%s/%s?%s",
|
||||
net.JoinHostPort(mockHost, "12345"),
|
||||
mockAPIKey,
|
||||
queryParams,
|
||||
),
|
||||
)
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
config := Config{}
|
||||
err = config.SetURL(url)
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
gomega.Expect(config.Alias).To(gomega.Equal("Life is too short for no alias"))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("generating a url from a simple config", func() {
|
||||
ginkgo.It("should generate a url", func() {
|
||||
config := Config{
|
||||
Host: "api.opsgenie.com",
|
||||
APIKey: "eb243592-faa2-4ba2-a551q-1afdf565c889",
|
||||
}
|
||||
|
||||
url := config.GetURL()
|
||||
|
||||
gomega.Expect(url.String()).
|
||||
To(gomega.Equal("opsgenie://api.opsgenie.com/eb243592-faa2-4ba2-a551q-1afdf565c889"))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("generating a url from a config with a port", func() {
|
||||
ginkgo.It("should generate a url with port", func() {
|
||||
config := Config{
|
||||
Host: "api.opsgenie.com",
|
||||
APIKey: "eb243592-faa2-4ba2-a551q-1afdf565c889",
|
||||
Port: 12345,
|
||||
}
|
||||
|
||||
url := config.GetURL()
|
||||
|
||||
gomega.Expect(url.String()).
|
||||
To(gomega.Equal("opsgenie://api.opsgenie.com:12345/eb243592-faa2-4ba2-a551q-1afdf565c889"))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("generating a url from a config with all optional config fields", func() {
|
||||
ginkgo.It("should generate a url with query parameters", func() {
|
||||
config := Config{
|
||||
Host: "api.opsgenie.com",
|
||||
APIKey: "eb243592-faa2-4ba2-a551q-1afdf565c889",
|
||||
Alias: "Life is too short for no alias",
|
||||
Description: "Every alert needs a description",
|
||||
Responders: []Entity{
|
||||
{Type: "user", Username: "Test"},
|
||||
{Type: "team", Name: "NOC"},
|
||||
{Type: "team", ID: "4513b7ea-3b91-438f-b7e4-e3e54af9147c"},
|
||||
},
|
||||
VisibleTo: []Entity{
|
||||
{Type: "user", Username: "A User"},
|
||||
},
|
||||
Actions: []string{"action1", "action2"},
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
Details: map[string]string{"key": "value"},
|
||||
Entity: "An example entity",
|
||||
Source: "The source",
|
||||
Priority: "P1",
|
||||
User: "Dracula",
|
||||
Note: "Here is a note",
|
||||
}
|
||||
|
||||
url := config.GetURL()
|
||||
gomega.Expect(url.String()).
|
||||
To(gomega.Equal(`opsgenie://api.opsgenie.com/eb243592-faa2-4ba2-a551q-1afdf565c889?actions=action1%2Caction2&alias=Life+is+too+short+for+no+alias&description=Every+alert+needs+a+description&details=key%3Avalue&entity=An+example+entity¬e=Here+is+a+note&priority=P1&responders=user%3ATest%2Cteam%3ANOC%2Cteam%3A4513b7ea-3b91-438f-b7e4-e3e54af9147c&source=The+source&tags=tag1%2Ctag2&user=Dracula&visibleto=user%3AA+User`))
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue