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

feat: add support for opsgenie #73

Closed
wants to merge 37 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2da7a9c
feat: add support for opsgenie
JonasPf Nov 9, 2020
3530a13
Fix for issue https://github.com/containrrr/shoutrrr/issues/71 (#75)
Nov 15, 2020
c78b769
docs: add atighineanu as a contributor (#81)
allcontributors[bot] Nov 15, 2020
b67f9b1
added custom port option for rocketchat (#80)
Nov 15, 2020
d3c987d
fix for https://github.com/containrrr/shoutrrr/issues/70 (#74)
ellisab Nov 15, 2020
ca5e1f2
docs: add ellisab as a contributor (#82)
allcontributors[bot] Nov 15, 2020
bdbdcf4
do not rewrite channel name without hashes (#85)
ellisab Nov 17, 2020
0f4ed7c
Added few testcases for rawURL passed as path/#####channel and path/…
Nov 24, 2020
54f0f63
refactor: rename JSON to AlertPayload
JonasPf Nov 27, 2020
c4ba3b9
refactor: rename `plugin` to `service`
JonasPf Nov 27, 2020
c108686
feat: add support for setting parameters via query string
JonasPf Dec 10, 2020
88d19ae
Fix query parameter documentation
JonasPf Dec 28, 2020
5277afa
Implement GetURL
JonasPf Dec 28, 2020
6bee1e0
Merge remote-tracking branch 'upstream/master' into opsgenie
JonasPf Jan 2, 2021
3a4d5c6
Add opsgenie to servicemap
JonasPf Jan 6, 2021
993bce8
Support generating and verifying opsgenie URLs
JonasPf Jan 6, 2021
cddfbf8
Use OpsGenie convention for URL params instead of json
JonasPf Jan 25, 2021
453c63c
Merge branch 'main' into opsgenie
JonasPf Jan 25, 2021
5cb1ad3
Use 'key:value,key2:value2' format for details and overall code cleanup
JonasPf Jan 26, 2021
584deee
Remove comment about spaces in URL
JonasPf Feb 2, 2021
b168211
Remove comment about spaces; Add test instead
JonasPf Feb 2, 2021
33fddea
Merge branch 'main' into opsgenie
JonasPf Feb 2, 2021
59819e7
Fix IFTTT test by removing obsolete values; Add another test for values
JonasPf Feb 4, 2021
a511157
Merge branch 'main' into opsgenie
JonasPf Feb 4, 2021
d87e5d4
Fix teams test by removing obsolete key without value
JonasPf Feb 4, 2021
3145f46
Fix OpsGenie test by using URL encoded strings for space, comma, colon
JonasPf Feb 4, 2021
ec73129
Simplify `getURL` by using query.Set/Encode instead of diy-ing it
JonasPf Feb 4, 2021
cc9d75d
Fix test after simplifying `getURL`, it encodes commas etc. now
JonasPf Feb 4, 2021
6390b12
Move `newAlertPayload` to `Service` struct
JonasPf Feb 4, 2021
8f3aac4
Preparation for implementing an interface for de-serializing structs
JonasPf Feb 4, 2021
3305147
Fix linting issues
JonasPf Feb 4, 2021
7bd4ba0
feat: add support for struct and map fields
piksel Feb 7, 2021
2fc0dbc
Fix: Use default port if no port was specified
JonasPf Feb 16, 2021
7e19f85
Merge branch 'feature/format-struct-map-fields' into opsgenie
JonasPf Feb 16, 2021
26bd837
Remove custom serialization logic; use formatters functionality instead
JonasPf Feb 16, 2021
38c1f10
Fix: Expect default port (443) in test
JonasPf Feb 16, 2021
f0c460d
Fix test: formatter lowercases keys
JonasPf Feb 16, 2021
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
59 changes: 59 additions & 0 deletions docs/services/opsgenie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# OpsGenie

## URL Format

## Creating a REST API endpoint in OpsGenie

1. Open up the Integration List page by clicking on *Settings => Integration List* within the menu
![Screenshot 1](opsgenie/1.png)

2. Click *API => Add*

3. Make sure *Create and Update Access* and *Enabled* are checked and click *Save Integration*
![Screenshot 2](opsgenie/2.png)

4. Copy the *API Key*

5. Format the service URL

The host can be either api.opsgenie.com or api.eu.opsgenie.com depending on the location of your instance. See
the [OpsGenie documentation](https://docs.opsgenie.com/docs/alert-api) for details.

```
opsgenie://api.opsgenie.com/eb243592-faa2-4ba2-a551q-1afdf565c889
└───────────────────────────────────┘
token
```

## Passing parameters via code

If you want to, you can pass additional parameters to the `send` function.
<br/>
The following example contains all parameters that are currently supported.

```gotemplate
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"}]`,
"actions": "An action",
"tags": "tag1 tag2",
"details": `{"key1": "value1", "key2": "value2"}`,
"entity": "An example entity",
"source": "The source",
"priority": "P1",
"user": "Dracula",
"note": "Here is a note",
})
```

# Optional parameters

You can optionally specify the parameters in the URL:
opsgenie://api.opsgenie.com/eb243592-faa2-4ba2-a551q-1afdf565c889?alias=Life+is+too+short+for+no+alias&description=Every+alert+needs+a+description&actions=An+action&tags=["tag1","tag2"]&entity=An+example+entity&source=The+source&priority=P1&user=Dracula&note=Here+is+a+note

Example using the command line:

shoutrrr send -u 'opsgenie://api.eu.opsgenie.com/token?tags=["tag1","tag2"]&description=testing&responders=[{"username":"superuser", "type": "user"}]&entity=Example Entity&source=Example Source&actions=["asdf", "bcde"]' -m "Hello World6"

Binary file added docs/services/opsgenie/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/services/opsgenie/2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 2 additions & 18 deletions pkg/format/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,12 @@ import (
"strings"
)

// NotifyFormat describes the format used in the notification body
type NotifyFormat int

const (
// Markdown is the default notification format
Markdown NotifyFormat = 0
)

// ParseBool returns true for "1","true","yes" or false for "0","false","no" or defaultValue for any other value
func ParseBool(value string, defaultValue bool) (parsedValue bool, ok bool) {
switch strings.ToLower(value) {
case "true":
fallthrough
case "1":
fallthrough
case "yes":
case "true", "1", "yes":
return true, true
case "false":
fallthrough
case "0":
fallthrough
case "no":
case "false", "0", "no":
return false, true
default:
return defaultValue, false
Expand Down
2 changes: 1 addition & 1 deletion pkg/format/format_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func BuildQuery(cqr types.ConfigQueryResolver) string {
}
value, err := cqr.Get(key)

if err == nil {
if err == nil && value != "" {
JonasPf marked this conversation as resolved.
Show resolved Hide resolved
query.Set(key, value)
}
}
Expand Down
273 changes: 273 additions & 0 deletions pkg/format/format_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
package format

import (
"errors"
"github.com/containrrr/shoutrrr/pkg/types"
"reflect"

"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var (
// logger *log.Logger
f = formatter{
EnumFormatters: map[string]types.EnumFormatter{
"TestEnum": testEnum,
},
MaxDepth: 2,
}
ts *testStruct
)

func TestFormat(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Shoutrrr Discord Suite")
}

var _ = Describe("the format package", func() {
BeforeSuite(func() {
// logger = log.New(GinkgoWriter, "Test", log.LstdFlags)
})

Describe("SetConfigField", func() {
var (
tv reflect.Value
)
tt := reflect.TypeOf(testStruct{})
fields := f.getStructFieldInfo(tt)

fieldMap := make(map[string]FieldInfo, len(fields))
for _, fi := range fields {
fieldMap[fi.Name] = fi
}
When("updating a struct", func() {

BeforeEach(func() {
tsPtr := reflect.New(tt)
tv = tsPtr.Elem()
ts = tsPtr.Interface().(*testStruct)
})

When("setting an integer value", func() {
When("the value is valid", func() {
It("should set it", func() {
valid, err := SetConfigField(tv, fieldMap["Signed"], "3")
Expect(valid).To(BeTrue())
Expect(err).NotTo(HaveOccurred())

Expect(ts.Signed).To(Equal(3))
})
})
When("the value is invalid", func() {
It("should return an error", func() {
ts.Signed = 2
valid, err := SetConfigField(tv, fieldMap["Signed"], "z7")
Expect(valid).To(BeFalse())
Expect(err).To(HaveOccurred())

Expect(ts.Signed).To(Equal(2))
})
})
})

When("setting an unsigned integer value", func() {
When("the value is valid", func() {
It("should set it", func() {
valid, err := SetConfigField(tv, fieldMap["Unsigned"], "6")
Expect(valid).To(BeTrue())
Expect(err).NotTo(HaveOccurred())

Expect(ts.Unsigned).To(Equal(uint(6)))
})
})
When("the value is invalid", func() {
It("should return an error", func() {
ts.Unsigned = 2
valid, err := SetConfigField(tv, fieldMap["Unsigned"], "-3")

Expect(ts.Unsigned).To(Equal(uint(2)))
Expect(valid).To(BeFalse())
Expect(err).To(HaveOccurred())
})
})
})

When("setting a string slice value", func() {
When("the value is valid", func() {
It("should set it", func() {
valid, err := SetConfigField(tv, fieldMap["StrSlice"], "meawannowalkalitabitalleh,meawannofeelalitabitstrongah")
Expect(valid).To(BeTrue())
Expect(err).NotTo(HaveOccurred())

Expect(ts.StrSlice).To(HaveLen(2))
})
})
})

When("setting a string array value", func() {
When("the value is valid", func() {
It("should set it", func() {
valid, err := SetConfigField(tv, fieldMap["StrArray"], "meawannowalkalitabitalleh,meawannofeelalitabitstrongah,meawannothinkalitabitsmartah")
Expect(valid).To(BeTrue())
Expect(err).NotTo(HaveOccurred())
})
})
When("the value has too many elements", func() {
It("should return an error", func() {
valid, err := SetConfigField(tv, fieldMap["StrArray"], "one,two,three,four?")
Expect(valid).To(BeFalse())
Expect(err).To(HaveOccurred())
})
})
When("the value has too few elements", func() {
It("should return an error", func() {
valid, err := SetConfigField(tv, fieldMap["StrArray"], "one,two")
Expect(valid).To(BeFalse())
Expect(err).To(HaveOccurred())
})
})
})

When("setting a struct value", func() {
When("it implements ConfigProp", func() {
It("should return an error", func() {
valid, err := SetConfigField(tv, fieldMap["Sub"], "@awol")
Expect(err).To(HaveOccurred())
Expect(valid).NotTo(BeTrue())
})
})
When("it implements ConfigProp", func() {
When("the value is valid", func() {
It("should set it", func() {
valid, err := SetConfigField(tv, fieldMap["SubProp"], "@awol")
Expect(err).NotTo(HaveOccurred())
Expect(valid).To(BeTrue())

Expect(ts.SubProp.Value).To(Equal("awol"))
})
})
When("the value is invalid", func() {
It("should return an error", func() {
valid, err := SetConfigField(tv, fieldMap["SubProp"], "missing initial at symbol")
Expect(err).To(HaveOccurred())
Expect(valid).NotTo(BeTrue())
})
})
})
})

When("setting a struct slice value", func() {
When("the value is valid", func() {
It("should set it", func() {
valid, err := SetConfigField(tv, fieldMap["SubPropSlice"], "@alice,@merton")
Expect(err).NotTo(HaveOccurred())
Expect(valid).To(BeTrue())

Expect(ts.SubPropSlice).To(HaveLen(2))
})
})
})

When("setting a struct pointer slice value", func() {
When("the value is valid", func() {
It("should set it", func() {
valid, err := SetConfigField(tv, fieldMap["SubPropPtrSlice"], "@the,@best")
Expect(err).NotTo(HaveOccurred())
Expect(valid).To(BeTrue())

Expect(ts.SubPropPtrSlice).To(HaveLen(2))
})
})
})
})

When("formatting stuct values", func() {
BeforeEach(func() {
tsPtr := reflect.New(tt)
tv = tsPtr.Elem()
})
When("setting and formatting", func() {
It("should format signed integers identical to input", func() {
testSetAndFormat(tv, fieldMap["Signed"], "-45", "-45")
})
It("should format unsigned integers identical to input", func() {
testSetAndFormat(tv, fieldMap["Unsigned"], "5", "5")
})
It("should format structs identical to input", func() {
testSetAndFormat(tv, fieldMap["SubProp"], "@whoa", "@whoa")
})
It("should format enums identical to input", func() {
testSetAndFormat(tv, fieldMap["TestEnum"], "Foo", "Foo")
})
It("should format string slices identical to input", func() {
testSetAndFormat(tv, fieldMap["StrSlice"], "one,two,three,four", "[ one, two, three, four ]")
})
It("should format string arrays identical to input", func() {
testSetAndFormat(tv, fieldMap["StrArray"], "one,two,three", "[ one, two, three ]")
})
It("should format prop struct slices identical to input", func() {
testSetAndFormat(tv, fieldMap["SubPropSlice"], "@be,@the,@best", "[ @be, @the, @best ]")
})
It("should format prop struct slices identical to input", func() {
testSetAndFormat(tv, fieldMap["SubPropPtrSlice"], "@diet,@glue", "[ @diet, @glue ]")
})
It("should format prop struct slices identical to input", func() {
testSetAndFormat(tv, fieldMap["StrMap"], "one:1,two:2", "{ one: 1, two: 2 }")
})
})
})
})
})

func testSetAndFormat(tv reflect.Value, field FieldInfo, value string, prettyFormat string) {
_, _ = SetConfigField(tv, field, value)
fieldValue := tv.FieldByName(field.Name)

// Used for de-/serializing configuration
formatted, err := GetConfigFieldString(tv, field)
Expect(err).NotTo(HaveOccurred())
Expect(formatted).To(Equal(value))

// Used for pretty printing output, coloring etc.
formatted, _ = f.getStructFieldValueString(fieldValue, field, 0)
Expect(formatted).To(Equal(prettyFormat))
}

type testStruct struct {
Signed int
Unsigned uint
Str string
StrSlice []string
StrArray [3]string
Sub subStruct
TestEnum int
SubProp subPropStruct
SubSlice []subStruct
SubPropSlice []subPropStruct
SubPropPtrSlice []*subPropStruct
StrMap map[string]string
}

type subStruct struct {
Value string
}

type subPropStruct struct {
Value string
}

func (s *subPropStruct) SetFromProp(propValue string) error {
if len(propValue) < 1 || propValue[0] != '@' {
return errors.New("invalid value")
}
s.Value = propValue[1:]
return nil
}
func (s *subPropStruct) GetPropValue() (string, error) {
return "@" + s.Value, nil
}

var testEnum = CreateEnumFormatter([]string{"None", "Foo", "Bar"})
Loading