Skip to content

Commit

Permalink
Merge pull request #10 from m-lab/sandbox-soltesz-reporoute
Browse files Browse the repository at this point in the history
Enable routing alerts to multiple repos
  • Loading branch information
stephen-soltesz authored Jun 7, 2018
2 parents 9dfa3d3 + a878452 commit 4e1350b
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 73 deletions.
28 changes: 24 additions & 4 deletions alerts/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//////////////////////////////////////////////////////////////////////////////

package alerts

import (
Expand All @@ -26,18 +27,26 @@ import (
"github.com/prometheus/alertmanager/notify"
)

// ReceiverClient defines all issue operations needed by the ReceiverHandler.
type ReceiverClient interface {
CloseIssue(issue *github.Issue) (*github.Issue, error)
CreateIssue(title, body string) (*github.Issue, error)
CreateIssue(repo, title, body string) (*github.Issue, error)
ListOpenIssues() ([]*github.Issue, error)
}

// ReceiverHandler contains data needed for HTTP handlers.
type ReceiverHandler struct {
// Client is an implementation of the ReceiverClient interface. Client is used to handle requests.
// Client is an implementation of the ReceiverClient interface. Client is used
// to handle requests.
Client ReceiverClient

// AutoClose indicates whether resolved issues that are still open should be closed automatically.
// AutoClose indicates whether resolved issues that are still open should be
// closed automatically.
AutoClose bool

// DefaultRepo is the repository where all alerts without a "repo" label will
// be created. Repo must exist.
DefaultRepo string
}

// ServeHTTP receives and processes alertmanager notifications. If the alert
Expand Down Expand Up @@ -106,7 +115,7 @@ func (rh *ReceiverHandler) processAlert(msg *notify.WebhookMessage) error {
// issue from github, so create a new issue.
if msg.Data.Status == "firing" && foundIssue == nil {
msgBody := formatIssueBody(msg)
_, err := rh.Client.CreateIssue(msgTitle, msgBody)
_, err := rh.Client.CreateIssue(rh.getTargetRepo(msg), msgTitle, msgBody)
return err
}

Expand All @@ -124,3 +133,14 @@ func (rh *ReceiverHandler) processAlert(msg *notify.WebhookMessage) error {
// log.Printf("Unsupported WebhookMessage.Data.Status: %s", msg.Data.Status)
return nil
}

// getTargetRepo returns a suitable github repository for creating an issue for
// the given alert message. If the alert includes a "repo" label, then getTargetRepo
// uses that value. Otherwise, getTargetRepo uses the ReceiverHandler's default repo.
func (rh *ReceiverHandler) getTargetRepo(msg *notify.WebhookMessage) string {
repo := msg.CommonLabels["repo"]
if repo != "" {
return repo
}
return rh.DefaultRepo
}
14 changes: 11 additions & 3 deletions alerts/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (f *fakeClient) ListOpenIssues() ([]*github.Issue, error) {
return f.listIssues, nil
}

func (f *fakeClient) CreateIssue(title, body string) (*github.Issue, error) {
func (f *fakeClient) CreateIssue(repo, title, body string) (*github.Issue, error) {
fmt.Println("create issue")
f.createdIssue = createIssue(title, body)
return f.createdIssue, nil
Expand Down Expand Up @@ -110,7 +110,11 @@ func TestReceiverHandler(t *testing.T) {
createIssue("DiskRunningFull", "body1"),
},
}
handler := alerts.ReceiverHandler{f, true}
handler := alerts.ReceiverHandler{
Client: f,
AutoClose: true,
DefaultRepo: "default",
}
handler.ServeHTTP(rw, req)
resp := rw.Result()

Expand Down Expand Up @@ -143,7 +147,11 @@ func TestReceiverHandler(t *testing.T) {

// No pre-existing issues to close.
f = &fakeClient{}
handler = alerts.ReceiverHandler{f, true}
handler = alerts.ReceiverHandler{
Client: f,
AutoClose: true,
DefaultRepo: "default",
}
handler.ServeHTTP(rw, req)
resp = rw.Result()

Expand Down
17 changes: 11 additions & 6 deletions cmd/github_receiver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ import (

"github.com/m-lab/alertmanager-github-receiver/alerts"
"github.com/m-lab/alertmanager-github-receiver/issues"
// TODO: add prometheus metrics for errors and github api access.
)

var (
authtoken = flag.String("authtoken", "", "Oauth2 token for access to github API.")
githubOwner = flag.String("owner", "", "The github user or organization name.")
githubRepo = flag.String("repo", "", "The repository where issues are created.")
githubOrg = flag.String("org", "", "The github user or organization name where all repos are found.")
githubRepo = flag.String("repo", "", "The default repository for creating issues when alerts do not include a repo label.")
enableAutoClose = flag.Bool("enable-auto-close", false, "Once an alert stops firing, automatically close open issues.")
)

Expand All @@ -52,17 +53,21 @@ func init() {
}

func serveListener(client *issues.Client) {
http.Handle("/", &issues.ListHandler{client})
http.Handle("/v1/receiver", &alerts.ReceiverHandler{client, *enableAutoClose})
http.Handle("/", &issues.ListHandler{ListClient: client})
http.Handle("/v1/receiver", &alerts.ReceiverHandler{
Client: client,
DefaultRepo: *githubRepo,
AutoClose: *enableAutoClose,
})
http.ListenAndServe(":9393", nil)
}

func main() {
flag.Parse()
if *authtoken == "" || *githubOwner == "" || *githubRepo == "" {
if *authtoken == "" || *githubOrg == "" || *githubRepo == "" {
flag.Usage()
os.Exit(1)
}
client := issues.NewClient(*githubOwner, *githubRepo, *authtoken)
client := issues.NewClient(*githubOrg, *authtoken)
serveListener(client)
}
15 changes: 9 additions & 6 deletions issues/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,20 @@ package issues

import (
"fmt"
"github.com/google/go-github/github"
"html/template"
"net/http"

"github.com/google/go-github/github"
)

const (
listRawHTMLTemplate = `
<html><body>
<h1>Open Issues</h1>
<table>
{{range .}}
<tr><td><a href={{.HTMLURL}}>{{.Title}}</a></td></tr>
{{end}}
{{range .}}
<tr><td><a href={{.HTMLURL}}>{{.Title}}</a></td></tr>
{{end}}
</table>
</body></html>`
)
Expand All @@ -38,17 +39,19 @@ var (
listTemplate = template.Must(template.New("list").Parse(listRawHTMLTemplate))
)

// ListClient defines an interface for listing issues.
type ListClient interface {
ListOpenIssues() ([]*github.Issue, error)
}

// ListHandler contains data needed for HTTP handlers.
type ListHandler struct {
Client ListClient
ListClient
}

// ServeHTTP lists open issues from github for view in a browser.
func (lh *ListHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
issues, err := lh.Client.ListOpenIssues()
issues, err := lh.ListOpenIssues()
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(rw, "%s\n", err)
Expand Down
11 changes: 6 additions & 5 deletions issues/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
package issues_test

import (
"github.com/google/go-github/github"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"

"github.com/google/go-github/github"

"github.com/m-lab/alertmanager-github-receiver/issues"
)

Expand All @@ -37,9 +38,9 @@ func TestListHandler(t *testing.T) {
<html><body>
<h1>Open Issues</h1>
<table>
<tr><td><a href=http://foo.bar>issue1 title</a></td></tr>
<tr><td><a href=http://foo.bar>issue1 title</a></td></tr>
</table>
</body></html>`
f := &fakeClient{
Expand All @@ -59,7 +60,7 @@ func TestListHandler(t *testing.T) {
}

// Run the list handler.
handler := issues.ListHandler{f}
handler := issues.ListHandler{ListClient: f}
handler.ServeHTTP(rw, req)
resp := rw.Result()

Expand Down
106 changes: 77 additions & 29 deletions issues/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@
// limitations under the License.
//////////////////////////////////////////////////////////////////////////////

// A client interface wrapping the Github API for creating, listing, and closing
// issues on a single repository.
// Package issues defines a client interface wrapping the Github API for
// creating, listing, and closing issues on a single repository.
package issues

import (
"fmt"
"log"
"net/url"
"strings"
"time"

"github.com/google/go-github/github"
"github.com/kr/pretty"
Expand All @@ -30,40 +34,44 @@ import (
type Client struct {
// githubClient is an authenticated client for accessing the github API.
GithubClient *github.Client
// owner is the github project (e.g. github.com/<owner>/<repo>).
owner string
// repo is the github repository under the above owner.
repo string
// org is the github user or organization name (e.g. github.com/<org>/<repo>).
org string
}

// NewClient creates an Client authenticated using the Github authToken.
// Future operations are only performed on the given github "owner/repo".
func NewClient(owner, repo, authToken string) *Client {
// Future operations are only performed on the given github "org/repo".
func NewClient(org, authToken string) *Client {
ctx := context.Background()
tokenSource := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: authToken},
)
client := &Client{
GithubClient: github.NewClient(oauth2.NewClient(ctx, tokenSource)),
owner: owner,
repo: repo,
org: org,
}
return client
}

// CreateIssue creates a new Github issue. New issues are unassigned.
func (c *Client) CreateIssue(title, body string) (*github.Issue, error) {
// CreateIssue creates a new Github issue. New issues are unassigned. Issues are
// labeled with with an alert named "alert:boom:". Labels are created automatically
// if they do not already exist in a repo.
func (c *Client) CreateIssue(repo, title, body string) (*github.Issue, error) {
// Construct a minimal github issue request.
issueReq := github.IssueRequest{
Title: &title,
Body: &body,
Title: &title,
Body: &body,
Labels: &([]string{"alert:boom:"}), // Search using: label:"alert:boom:"
}

// Enforce a timeout on the issue creation.
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

// Create the issue.
// See also: https://developer.github.com/v3/issues/#create-an-issue
// See also: https://godoc.org/github.com/google/go-github/github#IssuesService.Create
issue, resp, err := c.GithubClient.Issues.Create(
context.Background(), c.owner, c.repo, &issueReq)
ctx, c.org, repo, &issueReq)
if err != nil {
log.Printf("Error in CreateIssue: response: %v\n%s",
err, pretty.Sprint(resp))
Expand All @@ -72,30 +80,42 @@ func (c *Client) CreateIssue(title, body string) (*github.Issue, error) {
return issue, nil
}

// ListOpenIssues returns open issues from github Github issues are either
// "open" or "closed". Closed issues have either been resolved automatically or
// by a person. So, there will be an ever increasing number of "closed" issues.
// By only listing "open" issues we limit the number of issues returned.
// ListOpenIssues returns open issues created by past alerts within the
// client organization. Because ListOpenIssues uses the Github Search API,
// the *github.Issue instances returned will contain partial information.
// See also: https://developer.github.com/v3/search/#search-issues
func (c *Client) ListOpenIssues() ([]*github.Issue, error) {
var allIssues []*github.Issue

opts := &github.IssueListByRepoOptions{State: "open"}
sopts := &github.SearchOptions{}
for {
issues, resp, err := c.GithubClient.Issues.ListByRepo(
context.Background(), c.owner, c.repo, opts)
// Enforce a timeout on the issue listing.
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

// Github issues are either "open" or "closed". Closed issues have either been
// resolved automatically or by a person. So, there will be an ever increasing
// number of "closed" issues. By only listing "open" issues we limit the
// number of issues returned.
//
// The search depends on all relevant issues including the "alert:boom:" label.
issues, resp, err := c.GithubClient.Search.Issues(
ctx, `is:issue in:title is:open org:`+c.org+` label:"alert:boom:"`, sopts)
if err != nil {
log.Printf("Failed to list open github issues: %v\n%s",
err, pretty.Sprint(resp))
log.Printf("Failed to list open github issues: %v\n", err)
return nil, err
}
// Collect 'em all.
allIssues = append(allIssues, issues...)
for i := range issues.Issues {
log.Println("ListOpenIssues:", issues.Issues[i].GetTitle())
allIssues = append(allIssues, &issues.Issues[i])
}

// Continue loading the next page until all issues are received.
if resp.NextPage == 0 {
break
}
opts.ListOptions.Page = resp.NextPage
sopts.ListOptions.Page = resp.NextPage
}
return allIssues, nil
}
Expand All @@ -107,14 +127,42 @@ func (c *Client) CloseIssue(issue *github.Issue) (*github.Issue, error) {
State: github.String("closed"),
}

org, repo, err := getOrgAndRepoFromIssue(issue)
if err != nil {
return nil, err
}
// Enforce a timeout on the issue edit.
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

// Edits the issue to have "closed" state.
// See also: https://developer.github.com/v3/issues/#edit-an-issue
// See also: https://godoc.org/github.com/google/go-github/github#IssuesService.Edit
closedIssue, resp, err := c.GithubClient.Issues.Edit(
context.Background(), c.owner, c.repo, *issue.Number, &issueReq)
closedIssue, _, err := c.GithubClient.Issues.Edit(
ctx, org, repo, *issue.Number, &issueReq)
if err != nil {
log.Printf("Failed to close issue: %v\n%s", err, pretty.Sprint(resp))
log.Printf("Failed to close issue: %v", err)
return nil, err
}
return closedIssue, nil
}

// getOrgAndRepoFromIssue reads the issue RepositoryURL and extracts the
// owner and repo names. Issues returned by the Search API contain partial
// records.
func getOrgAndRepoFromIssue(issue *github.Issue) (string, string, error) {
repoURL := issue.GetRepositoryURL()
if repoURL == "" {
return "", "", fmt.Errorf("Issue has invalid RepositoryURL value")
}
u, err := url.Parse(repoURL)
if err != nil {
return "", "", err
}
fields := strings.Split(u.Path, "/")
if len(fields) != 4 {
return "", "", fmt.Errorf("Issue has invalid RepositoryURL value")
}
return fields[2], fields[3], nil

}
Loading

0 comments on commit 4e1350b

Please sign in to comment.