Skip to content
Open
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
9 changes: 7 additions & 2 deletions docs/services/newrelic.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,22 @@ stringData:
newrelic-apiKey: apiKey
```

3. Copy [Application ID](https://docs.newrelic.com/docs/apis/rest-api-v2/get-started/get-app-other-ids-new-relic-one/#apm)
3. Copy [Application ID](https://docs.newrelic.com/docs/apis/rest-api-v2/get-started/get-app-other-ids-new-relic-one/#apm) or [Application Name](https://docs.newrelic.com/docs/apm/agents/manage-apm-agents/app-naming/name-your-application/#app-alias)
4. Create subscription for your NewRelic integration

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
annotations:
notifications.argoproj.io/subscribe.<trigger-name>.newrelic: <app-id>
notifications.argoproj.io/subscribe.<trigger-name>.newrelic: <app-id> || <app-name>
```

**Notes**

- If you use an application name, `app_id` will be looked up by name.
- If multiple applications matching the application name are returned by NewRelic, then no deployment marker will be created.

## Templates

- `revision` - **optional**, The revision being deployed. Can contain a custom template to extract the revision from your specific application status structure.
Expand Down
80 changes: 76 additions & 4 deletions pkg/services/newrelic.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
texttemplate "text/template"

Expand All @@ -29,8 +31,10 @@ type NewrelicNotification struct {
}

var (
ErrMissingConfig = errors.New("config is missing")
ErrMissingApiKey = errors.New("apiKey is missing")
ErrMissingConfig = errors.New("config is missing")
ErrMissingApiKey = errors.New("apiKey is missing")
ErrAppIdMultipleMatches = errors.New("multiple matches found for application name")
ErrAppIdNoMatches = errors.New("no matches found for application name")
)

func (n *NewrelicNotification) GetTemplater(name string, f texttemplate.FuncMap) (Templater, error) {
Expand Down Expand Up @@ -116,6 +120,52 @@ type newrelicService struct {
type newrelicDeploymentMarkerRequest struct {
Deployment NewrelicNotification `json:"deployment"`
}
type newrelicApplicationsResponse struct {
Applications []struct {
ID json.Number `json:"id"`
} `json:"applications"`
}

func (s newrelicService) getApplicationId(client *http.Client, appName string) (string, error) {
u, err := url.Parse(s.opts.ApiURL)
if err != nil {
log.Errorf("Failed to parse ApiURL: %s", err)
return "", err
}
u.Path = "/v2/applications.json"
q := u.Query()
q.Set("filter[name]", appName)
u.RawQuery = q.Encode()

req, err := http.NewRequest(http.MethodGet, u.String(), http.NoBody)
if err != nil {
return "", fmt.Errorf("failed to create filtered application request: %w", err)
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Api-Key", s.opts.ApiKey)

resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()

var data newrelicApplicationsResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return "", fmt.Errorf("failed to decode applications response: %w", err)
}

if len(data.Applications) == 0 {
return "", ErrAppIdNoMatches
}

if len(data.Applications) > 1 {
return "", ErrAppIdMultipleMatches
}

return data.Applications[0].ID.String(), nil
}

func (s newrelicService) Send(notification Notification, dest Destination) (err error) {
if s.opts.ApiKey == "" {
Expand Down Expand Up @@ -149,8 +199,30 @@ func (s newrelicService) Send(notification Notification, dest Destination) (err
return err
}

markerApi := fmt.Sprintf(s.opts.ApiURL+"/v2/applications/%s/deployments.json", dest.Recipient)
req, err := http.NewRequest(http.MethodPost, markerApi, bytes.NewBuffer(jsonValue))
appId := dest.Recipient
if dest.Recipient != "" {
_, err := strconv.Atoi(dest.Recipient)
if err != nil {
log.Debugf(
"Recipient was provided by application name. Looking up the application id for %s",
dest.Recipient,
)
appId, err = s.getApplicationId(client, dest.Recipient)
if err != nil {
log.Errorf("Failed to lookup application %s by name: %s", dest.Recipient, err)
return err
}
}
}

u, err := url.Parse(s.opts.ApiURL)
if err != nil {
log.Errorf("Failed to parse ApiURL: %s", err)
return err
}
u.Path = fmt.Sprintf("/v2/applications/%s/deployments.json", appId)

req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewBuffer(jsonValue))
if err != nil {
log.Errorf("Failed to create deployment marker request: %s", err)
return err
Expand Down
117 changes: 117 additions & 0 deletions pkg/services/newrelic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,57 @@ func TestSend_Newrelic(t *testing.T) {
require.NoError(t, err)
})

t.Run("recipient is application name", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v2/applications.json" {
_, err := w.Write([]byte(`{
"applications": [
{"id": "123456789"}
]
}`))
if !assert.NoError(t, err) {
t.FailNow()
}
return
}

b, err := io.ReadAll(r.Body)
if !assert.NoError(t, err) {
t.FailNow()
}

assert.Equal(t, "/v2/applications/123456789/deployments.json", r.URL.Path)
assert.Equal(t, []string{"application/json"}, r.Header["Content-Type"])
assert.Equal(t, []string{"NRAK-5F2FIVA5UTA4FFDD11XCXVA7WPJ"}, r.Header["X-Api-Key"])

assert.JSONEq(t, `{
"deployment": {
"revision": "2027ed5",
"description": "message",
"user": "[email protected]"
}
}`, string(b))
}))
defer ts.Close()

service := NewNewrelicService(NewrelicOptions{
ApiKey: "NRAK-5F2FIVA5UTA4FFDD11XCXVA7WPJ",
ApiURL: ts.URL,
})
err := service.Send(Notification{
Message: "message",
Newrelic: &NewrelicNotification{
Revision: "2027ed5",
User: "[email protected]",
},
}, Destination{
Service: "newrelic",
Recipient: "myapp",
})

require.NoError(t, err)
})

t.Run("missing config", func(t *testing.T) {
service := NewNewrelicService(NewrelicOptions{
ApiKey: "NRAK-5F2FIVA5UTA4FFDD11XCXVA7WPJ",
Expand Down Expand Up @@ -202,3 +253,69 @@ func TestSend_Newrelic(t *testing.T) {
}
})
}

func TestGetApplicationId(t *testing.T) {
t.Run("successful lookup by application name", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/v2/applications.json", r.URL.Path)
assert.Equal(t, "GET", r.Method)
assert.Equal(t, "myapp", r.URL.Query().Get("filter[name]"))
assert.Equal(t, []string{"application/json"}, r.Header["Content-Type"])
assert.Equal(t, []string{"NRAK-5F2FIVA5UTA4FFDD11XCXVA7WPJ"}, r.Header["X-Api-Key"])

_, err := w.Write([]byte(`{
"applications": [
{"id": "123456789"}
]
}`))
if !assert.NoError(t, err) {
t.FailNow()
}
}))
defer ts.Close()
service := NewNewrelicService(NewrelicOptions{
ApiKey: "NRAK-5F2FIVA5UTA4FFDD11XCXVA7WPJ",
ApiURL: ts.URL,
}).(*newrelicService)
appId, err := service.getApplicationId(http.DefaultClient, "myapp")
require.NoError(t, err)
assert.Equal(t, "123456789", appId)
})

t.Run("application not found", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte(`{"applications": []}`))
if !assert.NoError(t, err) {
t.FailNow()
}
}))
defer ts.Close()
service := NewNewrelicService(NewrelicOptions{
ApiKey: "NRAK-5F2FIVA5UTA4FFDD11XCXVA7WPJ",
ApiURL: ts.URL,
}).(*newrelicService)
_, err := service.getApplicationId(http.DefaultClient, "myapp")
assert.Equal(t, ErrAppIdNoMatches, err)
})

t.Run("multiple matches for application name", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte(`{
"applications": [
{"id": "123456789"},
{"id": "987654321"}
]
}`))
if !assert.NoError(t, err) {
t.FailNow()
}
}))
defer ts.Close()
service := NewNewrelicService(NewrelicOptions{
ApiKey: "NRAK-5F2FIVA5UTA4FFDD11XCXVA7WPJ",
ApiURL: ts.URL,
}).(*newrelicService)
_, err := service.getApplicationId(http.DefaultClient, "myapp")
assert.Equal(t, ErrAppIdMultipleMatches, err)
})
}
Loading