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

Add option to authenticate with GitHub App #33

Merged
merged 4 commits into from
Aug 24, 2023
Merged
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
35 changes: 25 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,25 @@ go get github.com/pfnet-research/alertmanager-to-github

## Usage

Start webhook server:
Set GitHub API credentials to environment variables:

```shell
$ read ATG_GITHUB_APP_PRIVATE_KEY
(GitHub App's private key)
$ export ATG_GITHUB_APP_PRIVATE_KEY
```

or,

```shell
$ read ATG_GITHUB_TOKEN
(Personal Access Token)
$ export ATG_GITHUB_TOKEN
```

Start webhook server:

```
$ alertmanager-to-github start
```

Expand Down Expand Up @@ -63,15 +75,18 @@ USAGE:
alertmanager-to-github start [command options] [arguments...]

OPTIONS:
--listen value HTTP listen on (default: ":8080") [$ATG_LISTEN]
--github-url value GitHub Enterprise URL (e.g. https://github.example.com) [$ATG_GITHUB_URL]
--labels value Issue labels [$ATG_LABELS]
--body-template-file value Body template file [$ATG_BODY_TEMPLATE_FILE]
--title-template-file value Title template file [$ATG_TITLE_TEMPLATE_FILE]
--alert-id-template value Alert ID template (default: "{{.Payload.GroupKey}}") [$ATG_ALERT_ID_TEMPLATE]
--github-token value GitHub API token (command line argument is not recommended) [$ATG_GITHUB_TOKEN]
--auto-close-resolved-issues Close resolved issues automatically (default: true) [$ATG_AUTO_CLOSE_RESOLVED_ISSUES]
--help, -h show help (default: false)
--listen value HTTP listen on (default: ":8080") [$ATG_LISTEN]
--github-url value GitHub Enterprise URL (e.g. https://github.example.com) [$ATG_GITHUB_URL]
--labels value [ --labels value ] Issue labels [$ATG_LABELS]
--body-template-file value Body template file [$ATG_BODY_TEMPLATE_FILE]
--title-template-file value Title template file [$ATG_TITLE_TEMPLATE_FILE]
--alert-id-template value Alert ID template (default: "{{.Payload.GroupKey}}") [$ATG_ALERT_ID_TEMPLATE]
--github-app-id value GitHub App ID (default: 0) [$ATG_GITHUB_APP_ID]
--github-app-installation-id value GitHub App installation ID (default: 0) [$ATG_GITHUB_APP_INSTALLATION_ID]
--github-app-private-key value GitHub App private key (command line argument is not recommended) [$ATG_GITHUB_APP_PRIVATE_KEY]
--github-token value GitHub API token (command line argument is not recommended) [$ATG_GITHUB_TOKEN]
--auto-close-resolved-issues Should issues be automatically closed when resolved (default: true) [$ATG_AUTO_CLOSE_RESOLVED_ISSUES]
--help, -h show help
```

### GitHub Enterprise
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/pfnet-research/alertmanager-to-github
go 1.20

require (
github.com/bradleyfalzon/ghinstallation/v2 v2.0.4
github.com/gin-gonic/gin v1.9.1
github.com/google/go-github/v43 v43.0.0
github.com/prometheus/client_golang v1.16.0
Expand All @@ -25,7 +26,9 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt/v4 v4.0.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-github/v41 v41.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
Expand Down
15 changes: 15 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradleyfalzon/ghinstallation/v2 v2.0.4 h1:tXKVfhE7FcSkhkv0UwkLvPDeZ4kz6OXd0PKPlFqf81M=
github.com/bradleyfalzon/ghinstallation/v2 v2.0.4/go.mod h1:B40qPqJxWE0jDZgOR1JmaMy+4AY1eBP+IByOvqyAKp0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
Expand Down Expand Up @@ -31,14 +33,20 @@ github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QX
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-github/v41 v41.0.0 h1:HseJrM2JFf2vfiZJ8anY2hqBjdfY1Vlj/K27ueww4gg=
github.com/google/go-github/v41 v41.0.0/go.mod h1:XgmCA5H323A9rtgExdTcnDkcqp6S30AVACCBDOonIxg=
github.com/google/go-github/v43 v43.0.0 h1:y+GL7LIsAIF2NZlJ46ZoC/D1W1ivZasT0lnWHMYPZ+U=
github.com/google/go-github/v43 v43.0.0/go.mod h1:ZkTvvmCXBvsfPpTHXnH/d2hP9Y0cTbvN9kr5xqyXOIc=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
Expand Down Expand Up @@ -110,24 +118,31 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs=
golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
Expand Down
81 changes: 68 additions & 13 deletions pkg/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import (
"context"
"embed"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"

"github.com/bradleyfalzon/ghinstallation/v2"
"github.com/google/go-github/v43/github"
"github.com/pfnet-research/alertmanager-to-github/pkg/notifier"
"github.com/pfnet-research/alertmanager-to-github/pkg/server"
Expand All @@ -23,6 +26,9 @@ const flagGitHubURL = "github-url"
const flagLabels = "labels"
const flagBodyTemplateFile = "body-template-file"
const flagTitleTemplateFile = "title-template-file"
const flagGitHubAppID = "github-app-id"
const flagGitHubAppInstallationID = "github-app-installation-id"
const flagGitHubAppPrivateKey = "github-app-private-key"
const flagGitHubToken = "github-token"
const flagAlertIDTemplate = "alert-id-template"
const flagTemplateFile = "template-file"
Expand Down Expand Up @@ -141,16 +147,34 @@ func App() *cli.App {
Usage: "Alert ID template",
EnvVars: []string{"ATG_ALERT_ID_TEMPLATE"},
},
&cli.Int64Flag{
Name: flagGitHubAppID,
Required: false,
Usage: "GitHub App ID",
EnvVars: []string{"ATG_GITHUB_APP_ID"},
},
&cli.Int64Flag{
Name: flagGitHubAppInstallationID,
Required: false,
Usage: "GitHub App installation ID",
EnvVars: []string{"ATG_GITHUB_APP_INSTALLATION_ID"},
},
&cli.StringFlag{
Name: flagGitHubAppPrivateKey,
Required: false,
Usage: "GitHub App private key (command line argument is not recommended)",
EnvVars: []string{"ATG_GITHUB_APP_PRIVATE_KEY"},
},
&cli.StringFlag{
Name: flagGitHubToken,
Required: true,
Required: false,
Usage: "GitHub API token (command line argument is not recommended)",
EnvVars: []string{"ATG_GITHUB_TOKEN"},
},
&cli.BoolFlag{
Name: flagAutoCloseResolvedIssues,
Required: false,
Value: true,
Value: true,
Usage: "Should issues be automatically closed when resolved",
EnvVars: []string{"ATG_AUTO_CLOSE_RESOLVED_ISSUES"},
},
Expand Down Expand Up @@ -181,9 +205,29 @@ func App() *cli.App {
}
}

func buildGitHubClient(githubURL string, token string) (*github.Client, error) {
var err error
var client *github.Client
func buildGitHubClientWithAppCredentials(
githubURL string, appID int64, installationID int64, privateKey []byte,
) (*github.Client, error) {
fmt.Printf(
"Building a GitHub client with GitHub App credentials (app ID: %d, installation ID: %d)...\n",
appID, installationID,
)

tr, err := ghinstallation.New(http.DefaultTransport, appID, installationID, privateKey)
if err != nil {
return nil, err
}

if githubURL == "" {
return github.NewClient(&http.Client{Transport: tr}), nil
}

tr.BaseURL = githubURL
return github.NewEnterpriseClient(githubURL, githubURL, &http.Client{Transport: tr})
}

func buildGitHubClientWithToken(githubURL string, token string) (*github.Client, error) {
fmt.Println("Building a GitHub client with token...")

ctx := context.TODO()
ts := oauth2.StaticTokenSource(
Expand All @@ -192,15 +236,10 @@ func buildGitHubClient(githubURL string, token string) (*github.Client, error) {
tc := oauth2.NewClient(ctx, ts)

if githubURL == "" {
client = github.NewClient(tc)
} else {
client, err = github.NewEnterpriseClient(githubURL, githubURL, tc)
if err != nil {
return nil, err
}
return github.NewClient(tc), nil
}

return client, nil
return github.NewEnterpriseClient(githubURL, githubURL, tc)
}

func templateFromReader(r io.Reader) (*template.Template, error) {
Expand Down Expand Up @@ -228,7 +267,20 @@ func templateFromString(s string) (*template.Template, error) {
}

func actionStart(c *cli.Context) error {
githubClient, err := buildGitHubClient(c.String(flagGitHubURL), c.String(flagGitHubToken))
githubClient, err := func() (*github.Client, error) {
appID := c.Int64(flagGitHubAppID)
installationID := c.Int64(flagGitHubAppInstallationID)
appKey := c.String(flagGitHubAppPrivateKey)
if appID != 0 && installationID != 0 && appKey != "" {
return buildGitHubClientWithAppCredentials(c.String(flagGitHubURL), appID, installationID, []byte(appKey))
}

if token := c.String(flagGitHubToken); token != "" {
return buildGitHubClientWithToken(c.String(flagGitHubURL), token)
}

return nil, errors.New("GitHub credentials must be specified")
}()
if err != nil {
return err
}
Expand Down Expand Up @@ -264,6 +316,9 @@ func actionStart(c *cli.Context) error {
}
nt.GitHubClient = githubClient
nt.Labels = c.StringSlice(flagLabels)
if nt.Labels == nil {
nt.Labels = []string{}
}
nt.BodyTemplate = bodyTemplate
nt.TitleTemplate = titleTemplate
nt.AlertIDTemplate = alertIDTemplate
Expand Down
18 changes: 11 additions & 7 deletions pkg/notifier/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,11 @@ func (n *GitHubNotifier) Notify(ctx context.Context, payload *types.WebhookPaylo
searchResult, response, err := n.GitHubClient.Search.Issues(ctx, query, &github.SearchOptions{
TextMatch: true,
})
updateGithubApiMetrics("search", response, err)
if err != nil {
return err
}

updateGithubApiMetrics("search", response)
if err = checkSearchResponse(response); err != nil {
return err
}
Expand Down Expand Up @@ -154,10 +155,11 @@ func (n *GitHubNotifier) Notify(ctx context.Context, payload *types.WebhookPaylo

if issue == nil {
issue, response, err = n.GitHubClient.Issues.Create(ctx, owner, repo, req)
updateGithubApiMetrics("issues", response, err)
if err != nil {
return err
}

updateGithubApiMetrics("issues", response)
log.Info().Msgf("created an issue: %s", issue.GetURL())
} else {
// we have to merge existing labels because Edit api replaces its labels
Expand All @@ -178,10 +180,11 @@ func (n *GitHubNotifier) Notify(ctx context.Context, payload *types.WebhookPaylo
}
req.Labels = &mergedLabels
issue, _, err = n.GitHubClient.Issues.Edit(ctx, owner, repo, issue.GetNumber(), req)
updateGithubApiMetrics("issues", response, err)
if err != nil {
return err
}

updateGithubApiMetrics("issues", response)
log.Info().Msgf("edited an issue: %s", issue.GetURL())
}

Expand All @@ -203,11 +206,11 @@ func (n *GitHubNotifier) Notify(ctx context.Context, payload *types.WebhookPaylo
State: github.String(desiredState),
}
issue, response, err = n.GitHubClient.Issues.Edit(ctx, owner, repo, issue.GetNumber(), req)
updateGithubApiMetrics("issues", response, err)
if err != nil {
return err
}

updateGithubApiMetrics("issues", response)
log.Info().Str("state", desiredState).Msgf("updated state of the issue: %s", issue.GetURL())
}

Expand All @@ -223,10 +226,11 @@ func (n *GitHubNotifier) cleanupIssues(ctx context.Context, owner, repo, alertID
searchResult, response, err := n.GitHubClient.Search.Issues(ctx, query, &github.SearchOptions{
TextMatch: true,
})
updateGithubApiMetrics("search", response, err)
if err != nil {
return err
}

updateGithubApiMetrics("search", response)
if err = checkSearchResponse(response); err != nil {
return err
}
Expand All @@ -248,11 +252,11 @@ func (n *GitHubNotifier) cleanupIssues(ctx context.Context, owner, repo, alertID
State: github.String("closed"),
}
issue, response, err = n.GitHubClient.Issues.Edit(ctx, owner, repo, issue.GetNumber(), req)
updateGithubApiMetrics("issues", response, err)
if err != nil {
return err
}

updateGithubApiMetrics("issues", response)
log.Info().Msgf("closed an issue: %s", issue.GetURL())
}

Expand All @@ -275,7 +279,7 @@ func checkSearchResponse(response *github.Response) error {
return nil
}

func updateGithubApiMetrics(apiName string, resp *github.Response, err error) {
func updateGithubApiMetrics(apiName string, resp *github.Response) {
rateLimit.WithLabelValues(apiName).Set(float64(resp.Rate.Limit))
rateRemaining.WithLabelValues(apiName).Set(float64(resp.Rate.Remaining))
rateResetTime.WithLabelValues(apiName).Set(float64(resp.Rate.Reset.UTC().Unix()))
Expand Down