diff --git a/.gitignore b/.gitignore index e2a09c9..ef2a955 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /.project /.idea +/*.iml /release /jiralert /jiralert*.tar.gz diff --git a/cmd/jiralert/main.go b/cmd/jiralert/main.go index b00c017..5dd13d1 100644 --- a/cmd/jiralert/main.go +++ b/cmd/jiralert/main.go @@ -17,6 +17,7 @@ import ( "encoding/json" "flag" "fmt" + "github.com/pkg/errors" "net/http" "os" "runtime" @@ -83,6 +84,10 @@ func main() { os.Exit(1) } + // We re-use the HTTP client here. Otherwise, in case of cookie-based auth, we might get banned by JIRA for creating + // many new sessions. + var jiraHTTPClient = createHTTPClient(config, logger) + http.HandleFunc("/alert", func(w http.ResponseWriter, req *http.Request) { level.Debug(logger).Log("msg", "handling /alert webhook request") defer func() { _ = req.Body.Close() }() @@ -101,12 +106,7 @@ func main() { } level.Debug(logger).Log("msg", " matched receiver", "receiver", conf.Name) - // TODO: Consider reusing notifiers or just jira clients to reuse connections. - tp := jira.BasicAuthTransport{ - Username: conf.User, - Password: string(conf.Password), - } - client, err := jira.NewClient(tp.Client(), conf.APIURL) + client, err := jira.NewClient(jiraHTTPClient, conf.APIURL) if err != nil { errorHandler(w, http.StatusInternalServerError, err, conf.Name, &data, logger) return @@ -114,12 +114,22 @@ func main() { if retry, err := notify.NewReceiver(logger, conf, tmpl, client.Issue).Notify(&data, *hashJiraLabel); err != nil { var status int + + if errors.Is(err, notify.ErrUnauthorized) && config.Defaults.SessionCookie { + level.Info(logger).Log("msg", "refreshing session cookie") + jiraHTTPClient = createHTTPClient(config, logger) + + // Next time, we start with a fresh session! + retry = true + } + if retry { // Instruct Alertmanager to retry. status = http.StatusServiceUnavailable } else { status = http.StatusInternalServerError } + errorHandler(w, status, err, conf.Name, &data, logger) return } @@ -187,3 +197,22 @@ func setupLogger(lvl string, fmt string) (logger log.Logger) { logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller) return } + +func createHTTPClient(config *config.Config, logger log.Logger) *http.Client { + if config.Defaults.SessionCookie { + level.Debug(logger).Log("msg", "creating new HTTP client using cookie auth") + tp := jira.CookieAuthTransport{ + Username: config.Defaults.User, + Password: string(config.Defaults.Password), + AuthURL: config.Defaults.APIURL + "/rest/auth/1/session", + } + return tp.Client() + } + + level.Debug(logger).Log("msg", "creating new HTTP client using basic auth") + tp := jira.BasicAuthTransport{ + Username: config.Defaults.User, + Password: string(config.Defaults.Password), + } + return tp.Client() +} diff --git a/examples/jiralert.yml b/examples/jiralert.yml index 1ec7563..681cfcb 100644 --- a/examples/jiralert.yml +++ b/examples/jiralert.yml @@ -5,6 +5,7 @@ defaults: api_url: https://jiralert.atlassian.net user: jiralert password: 'JIRAlert' + session_cookie: false # The type of JIRA issue to create. Required. issue_type: Bug diff --git a/pkg/config/config.go b/pkg/config/config.go index 7359bc7..56fcfe1 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -94,9 +94,10 @@ type ReceiverConfig struct { Name string `yaml:"name" json:"name"` // API access fields - APIURL string `yaml:"api_url" json:"api_url"` - User string `yaml:"user" json:"user"` - Password Secret `yaml:"password" json:"password"` + APIURL string `yaml:"api_url" json:"api_url"` + User string `yaml:"user" json:"user"` + Password Secret `yaml:"password" json:"password"` + SessionCookie bool `yaml:"session_cookie" json:"session_cookie"` // Required issue fields Project string `yaml:"project" json:"project"` diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index f6d741e..ce8ccb0 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -29,6 +29,7 @@ defaults: api_url: https://jiralert.atlassian.net user: jiralert password: 'JIRAlert' + session_cookie: false # The type of JIRA issue to create. Required. issue_type: Bug @@ -60,7 +61,7 @@ receivers: # Overrides default. issue_type: Task # JIRA components. Optional. - components: [ 'Operations' ] + components: ['Operations'] # Standard or custom field values to set on created issue. Optional. # # See https://developer.atlassian.com/server/jira/platform/jira-rest-api-examples/#setting-custom-field-data-for-other-field-types for further examples. @@ -68,9 +69,9 @@ receivers: # TextField customfield_10001: "Random text" # SelectList - customfield_10002: { "value": "red" } + customfield_10002: {"value": "red"} # MultiSelect - customfield_10003: [{"value": "red" }, {"value": "blue" }, {"value": "green" }] + customfield_10003: [{"value": "red"}, {"value": "blue"}, {"value": "green"}] # File containing template definitions. Required. template: jiralert.tmpl diff --git a/pkg/notify/notify.go b/pkg/notify/notify.go index 0cd7da3..c9fb21d 100644 --- a/pkg/notify/notify.go +++ b/pkg/notify/notify.go @@ -53,6 +53,8 @@ type Receiver struct { timeNow func() time.Time } +var ErrUnauthorized = errors.New("JIRA returned HTTP status 401 Unauthorized") + // NewReceiver creates a Receiver using the provided configuration, template and jiraIssueService. func NewReceiver(logger log.Logger, c *config.ReceiverConfig, t *template.Template, client jiraIssueService) *Receiver { return &Receiver{logger: logger, conf: c, tmpl: t, client: client, timeNow: time.Now} @@ -385,8 +387,16 @@ func handleJiraErrResponse(api string, resp *jira.Response, err error, logger lo if resp != nil && resp.StatusCode/100 != 2 { retry := resp.StatusCode == 500 || resp.StatusCode == 503 body, _ := ioutil.ReadAll(resp.Body) - // go-jira error message is not particularly helpful, replace it - return retry, errors.Errorf("JIRA request %s returned status %s, body %q", resp.Request.URL, resp.Status, string(body)) + + var responseErr error + if resp.StatusCode == 401 { + responseErr = ErrUnauthorized + } else { + // go-jira error message is not particularly helpful, replace it + responseErr = errors.Errorf("JIRA request %s returned status %s, body %q", resp.Request.URL, resp.Status, string(body)) + } + + return retry, responseErr } return false, errors.Wrapf(err, "JIRA request %s failed", api) }