Skip to content

Commit

Permalink
Merge pull request #97 from galbirk/master
Browse files Browse the repository at this point in the history
feat: Added GitHub app authentication
  • Loading branch information
henrymcconville authored Apr 23, 2024
2 parents 400a939 + 9878340 commit d667f9a
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.sequence

*.pem
.idea/
.vscode/

Expand Down
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ This exporter is setup to take input from environment variables. All variables a
the format "user1, user2".
* `GITHUB_TOKEN` If supplied, enables the user to supply a github authentication token that allows the API to be queried more often. Optional, but recommended.
* `GITHUB_TOKEN_FILE` If supplied _instead of_ `GITHUB_TOKEN`, enables the user to supply a path to a file containing a github authentication token that allows the API to be queried more often. Optional, but recommended.
* `GITHUB_APP` If true , authenticates ass GitHub app to the API.
* `GITHUB_APP_ID` The APP ID of the GitHub App.
* `GITHUB_APP_INSTALLATION_ID` The INSTALLATION ID of the GitHub App.
* `GITHUB_APP_KEY_PATH` The path to the github private key.
* `GITHUB_RATE_LIMIT` The RATE LIMIT that suppose to be for github app (default is 15,000). If the exporter sees the value is below this variable it generating new token for the app.
* `API_URL` Github API URL, shouldn't need to change this. Defaults to `https://api.github.com`
* `LISTEN_PORT` The port you wish to run the container on, the Dockerfile defaults this to `9171`
* `METRICS_PATH` the metrics URL path you wish to use, defaults to `/metrics`
Expand All @@ -34,10 +39,14 @@ Run manually from Docker Hub:
docker run -d --restart=always -p 9171:9171 -e REPOS="infinityworks/ranch-eye, infinityworks/prom-conf" githubexporter/github-exporter
```

Run manually from Docker Hub (With GitHub App):
```
docker run -d --restart=always -p 9171:9171 --read-only -v ./key.pem:/key.pem -e GITHUB_APP=true -e GITHUB_APP_ID= -e GITHUB_APP_INSTALLATION_ID= -e GITHUB_APP_KEY_PATH=/key.pem <IMAGE_NAME>
```

Build a docker image:
```
docker build -t <image-name> .
docker run -d --restart=always -p 9171:9171 -e REPOS="infinityworks/ranch-eye, infinityworks/prom-conf" <image-name>
```

## Docker compose
Expand All @@ -54,6 +63,29 @@ github-exporter:
environment:
- REPOS=<REPOS you want to monitor>
- GITHUB_TOKEN=<your github api token>
```

## Docker compose (GitHub App)

```
github-exporter-github-app:
tty: true
stdin_open: true
expose:
- 9171
ports:
- 9171:9171
build: .
environment:
- LOG_LEVEL=debug
- LISTEN_PORT=9171
- GITHUB_APP=true
- GITHUB_APP_ID=
- GITHUB_APP_INSTALLATION_ID=
- GITHUB_APP_KEY_PATH=/key.pem
restart: unless-stopped
volumes:
- "./key.pem:/key.pem:ro"
```

Expand Down
110 changes: 103 additions & 7 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package config

import (
"context"
"net/http"
"net/url"
"path"
"strconv"
"strings"

"github.com/bradleyfalzon/ghinstallation"
log "github.com/sirupsen/logrus"

"os"
Expand All @@ -15,12 +19,17 @@ import (
// Config struct holds all of the runtime confgiguration for the application
type Config struct {
*cfg.BaseConfig
apiUrl *url.URL
repositories []string
organisations []string
users []string
apiToken string
targetURLs []string
apiUrl *url.URL
repositories []string
organisations []string
users []string
apiToken string
targetURLs []string
gitHubApp bool
gitHubAppKeyPath string
gitHubAppId int64
gitHubAppInstallationId int64
gitHubRateLimit float64
}

// Init populates the Config struct based on environmental runtime configuration
Expand All @@ -38,6 +47,11 @@ func Init() Config {
nil,
"",
nil,
false,
"",
0,
0,
15000,
}

err := appConfig.SetAPIURL(cfg.GetEnv("API_URL", "https://api.github.com"))
Expand All @@ -56,6 +70,24 @@ func Init() Config {
if users != "" {
appConfig.SetUsers(strings.Split(users, ", "))
}

gitHubApp := strings.ToLower(os.Getenv("GITHUB_APP"))
if gitHubApp == "true" {
gitHubAppKeyPath := os.Getenv("GITHUB_APP_KEY_PATH")
gitHubAppId, _ := strconv.ParseInt(os.Getenv("GITHUB_APP_ID"), 10, 64)
gitHubAppInstalaltionId, _ := strconv.ParseInt(os.Getenv("GITHUB_APP_INSTALLATION_ID"), 10, 64)
gitHubRateLimit, _ := strconv.ParseFloat(cfg.GetEnv("GITHUB_RATE_LIMIT", "15000"), 64)
appConfig.SetGitHubApp(true)
appConfig.SetGitHubAppKeyPath(gitHubAppKeyPath)
appConfig.SetGitHubAppId(gitHubAppId)
appConfig.SetGitHubAppInstallationId(gitHubAppInstalaltionId)
appConfig.SetGitHubRateLimit(gitHubRateLimit)
err = appConfig.SetAPITokenFromGitHubApp()
if err != nil {
log.Errorf("Error initializing Configuration, Error: %v", err)
}
}

tokenEnv := os.Getenv("GITHUB_TOKEN")
tokenFile := os.Getenv("GITHUB_TOKEN_FILE")
if tokenEnv != "" {
Expand All @@ -66,7 +98,6 @@ func Init() Config {
log.Errorf("Error initialising Configuration, Error: %v", err)
}
}

return appConfig
}

Expand All @@ -85,6 +116,31 @@ func (c *Config) APIToken() string {
return c.apiToken
}

// Returns the GitHub App authentication value
func (c *Config) GitHubApp() bool {
return c.gitHubApp
}

// Returns the GitHub app private key path
func (c *Config) GitHubAppKeyPath() string {
return c.gitHubAppKeyPath
}

// Returns the GitHub app id
func (c *Config) GitHubAppId() int64 {
return c.gitHubAppId
}

// Returns the GitHub app installation id
func (c *Config) GitHubAppInstallationId() int64 {
return c.gitHubAppInstallationId
}

// Returns the GitHub RateLimit
func (c *Config) GitHubRateLimit() float64 {
return c.gitHubRateLimit
}

// Sets the base API URL returning an error if the supplied string is not a valid URL
func (c *Config) SetAPIURL(u string) error {
ur, err := url.Parse(u)
Expand Down Expand Up @@ -125,6 +181,45 @@ func (c *Config) SetAPITokenFromFile(tokenFile string) error {
return nil
}

// SetGitHubApp accepts a boolean for GitHub app authentication
func (c *Config) SetGitHubApp(githubApp bool) {
c.gitHubApp = githubApp
}

// SetGitHubAppKeyPath accepts a string for GitHub app private key path
func (c *Config) SetGitHubAppKeyPath(gitHubAppKeyPath string) {
c.gitHubAppKeyPath = gitHubAppKeyPath
}

// SetGitHubAppId accepts a string for GitHub app id
func (c *Config) SetGitHubAppId(gitHubAppId int64) {
c.gitHubAppId = gitHubAppId
}

// SetGitHubAppInstallationId accepts a string for GitHub app installation id
func (c *Config) SetGitHubAppInstallationId(gitHubAppInstallationId int64) {
c.gitHubAppInstallationId = gitHubAppInstallationId
}

// SetGitHubAppRateLimit accepts a string for GitHub RateLimit
func (c *Config) SetGitHubRateLimit(gitHubRateLimit float64) {
c.gitHubRateLimit = gitHubRateLimit
}

// SetAPITokenFromGitHubApp generating api token from github app configuration.
func (c *Config) SetAPITokenFromGitHubApp() error {
itr, err := ghinstallation.NewKeyFromFile(http.DefaultTransport, c.gitHubAppId, c.gitHubAppInstallationId, c.gitHubAppKeyPath)
if err != nil {
return err
}
strToken, err := itr.Token(context.Background())
if err != nil {
return err
}
c.SetAPIToken(strToken)
return nil
}

// Init populates the Config struct based on environmental runtime configuration
// All URL's are added to the TargetURL's string array
func (c *Config) setScrapeURLs() error {
Expand Down Expand Up @@ -152,6 +247,7 @@ func (c *Config) setScrapeURLs() error {
}

// Append github orginisations to the array

if len(c.organisations) > 0 {
for _, x := range c.organisations {
y := *c.apiUrl
Expand Down
47 changes: 46 additions & 1 deletion exporter/prometheus.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package exporter

import (
"path"
"strconv"

"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
)
Expand All @@ -17,9 +20,22 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
// Collect function, called on by Prometheus Client library
// This function is called when a scrape is peformed on the /metrics page
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {

data := []*Datum{}
var err error

if e.Config.GitHubApp() {
needReAuth, err := e.isTokenExpired()
if err != nil {
log.Errorf("Error checking token expiration status: %v", err)
return
}
if needReAuth {
err = e.Config.SetAPITokenFromGitHubApp()
if err != nil {
log.Errorf("Error authenticating with GitHub app: %v", err)
}
}
}
// Scrape the Data from Github
if len(e.TargetURLs()) > 0 {
data, err = e.gatherData()
Expand All @@ -46,3 +62,32 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
log.Info("All Metrics successfully collected")

}

func (e *Exporter) isTokenExpired() (bool, error) {
u := *e.APIURL()
u.Path = path.Join(u.Path, "rate_limit")

resp, err := getHTTPResponse(u.String(), e.APIToken())

if err != nil {
return false, err
}
defer resp.Body.Close()
// Triggers if rate-limiting isn't enabled on private Github Enterprise installations
if resp.StatusCode == 404 {
return false, nil
}

limit, err := strconv.ParseFloat(resp.Header.Get("X-RateLimit-Limit"), 64)

if err != nil {
return false, err
}

defaultRateLimit := e.Config.GitHubRateLimit()
if limit < defaultRateLimit {
return true, nil
}
return false, nil

}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/githubexporter/github-exporter
go 1.22

require (
github.com/bradleyfalzon/ghinstallation v1.1.1
github.com/infinityworks/go-common v0.0.0-20170820165359-7f20a140fd37
github.com/prometheus/client_golang v1.14.0
github.com/sirupsen/logrus v1.9.0
Expand All @@ -14,14 +15,18 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-github/v29 v29.0.2 // indirect
github.com/google/go-querystring v1.0.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.39.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/stretchr/testify v1.8.0 // indirect
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 // indirect
golang.org/x/sys v0.4.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
17 changes: 17 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
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 v1.1.1 h1:pmBXkxgM1WeF8QYvDLT5kuQiHMcmf+X015GI0KM/E3I=
github.com/bradleyfalzon/ghinstallation v1.1.1/go.mod h1:vyCmHTciHx/uuyN82Zc3rXN3X2KTK8nUTCrTMwAhcug=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-github/v29 v29.0.2 h1:opYN6Wc7DOz7Ku3Oh4l7prmkOMwEcQxpFtxdU8N8Pts=
github.com/google/go-github/v29 v29.0.2/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/infinityworks/go-common v0.0.0-20170820165359-7f20a140fd37 h1:Lm6kyC3JBiJQvJrus66He0E4viqDc/m5BdiFNSkIFfU=
github.com/infinityworks/go-common v0.0.0-20170820165359-7f20a140fd37/go.mod h1:+OaHNKQvQ9oOCr+DgkF95PkiDx20fLHpzMp8SmRPQTg=
Expand Down Expand Up @@ -48,11 +58,18 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
Expand Down

0 comments on commit d667f9a

Please sign in to comment.