Skip to content

Commit d667f9a

Browse files
Merge pull request #97 from galbirk/master
feat: Added GitHub app authentication
2 parents 400a939 + 9878340 commit d667f9a

File tree

6 files changed

+205
-10
lines changed

6 files changed

+205
-10
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.sequence
2-
2+
*.pem
33
.idea/
44
.vscode/
55

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ This exporter is setup to take input from environment variables. All variables a
2121
the format "user1, user2".
2222
* `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.
2323
* `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.
24+
* `GITHUB_APP` If true , authenticates ass GitHub app to the API.
25+
* `GITHUB_APP_ID` The APP ID of the GitHub App.
26+
* `GITHUB_APP_INSTALLATION_ID` The INSTALLATION ID of the GitHub App.
27+
* `GITHUB_APP_KEY_PATH` The path to the github private key.
28+
* `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.
2429
* `API_URL` Github API URL, shouldn't need to change this. Defaults to `https://api.github.com`
2530
* `LISTEN_PORT` The port you wish to run the container on, the Dockerfile defaults this to `9171`
2631
* `METRICS_PATH` the metrics URL path you wish to use, defaults to `/metrics`
@@ -34,10 +39,14 @@ Run manually from Docker Hub:
3439
docker run -d --restart=always -p 9171:9171 -e REPOS="infinityworks/ranch-eye, infinityworks/prom-conf" githubexporter/github-exporter
3540
```
3641

42+
Run manually from Docker Hub (With GitHub App):
43+
```
44+
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>
45+
```
46+
3747
Build a docker image:
3848
```
3949
docker build -t <image-name> .
40-
docker run -d --restart=always -p 9171:9171 -e REPOS="infinityworks/ranch-eye, infinityworks/prom-conf" <image-name>
4150
```
4251

4352
## Docker compose
@@ -54,6 +63,29 @@ github-exporter:
5463
environment:
5564
- REPOS=<REPOS you want to monitor>
5665
- GITHUB_TOKEN=<your github api token>
66+
```
67+
68+
## Docker compose (GitHub App)
69+
70+
```
71+
github-exporter-github-app:
72+
tty: true
73+
stdin_open: true
74+
expose:
75+
- 9171
76+
ports:
77+
- 9171:9171
78+
build: .
79+
environment:
80+
- LOG_LEVEL=debug
81+
- LISTEN_PORT=9171
82+
- GITHUB_APP=true
83+
- GITHUB_APP_ID=
84+
- GITHUB_APP_INSTALLATION_ID=
85+
- GITHUB_APP_KEY_PATH=/key.pem
86+
restart: unless-stopped
87+
volumes:
88+
- "./key.pem:/key.pem:ro"
5789
5890
```
5991

config/config.go

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package config
22

33
import (
4+
"context"
5+
"net/http"
46
"net/url"
57
"path"
8+
"strconv"
69
"strings"
710

11+
"github.com/bradleyfalzon/ghinstallation"
812
log "github.com/sirupsen/logrus"
913

1014
"os"
@@ -15,12 +19,17 @@ import (
1519
// Config struct holds all of the runtime confgiguration for the application
1620
type Config struct {
1721
*cfg.BaseConfig
18-
apiUrl *url.URL
19-
repositories []string
20-
organisations []string
21-
users []string
22-
apiToken string
23-
targetURLs []string
22+
apiUrl *url.URL
23+
repositories []string
24+
organisations []string
25+
users []string
26+
apiToken string
27+
targetURLs []string
28+
gitHubApp bool
29+
gitHubAppKeyPath string
30+
gitHubAppId int64
31+
gitHubAppInstallationId int64
32+
gitHubRateLimit float64
2433
}
2534

2635
// Init populates the Config struct based on environmental runtime configuration
@@ -38,6 +47,11 @@ func Init() Config {
3847
nil,
3948
"",
4049
nil,
50+
false,
51+
"",
52+
0,
53+
0,
54+
15000,
4155
}
4256

4357
err := appConfig.SetAPIURL(cfg.GetEnv("API_URL", "https://api.github.com"))
@@ -56,6 +70,24 @@ func Init() Config {
5670
if users != "" {
5771
appConfig.SetUsers(strings.Split(users, ", "))
5872
}
73+
74+
gitHubApp := strings.ToLower(os.Getenv("GITHUB_APP"))
75+
if gitHubApp == "true" {
76+
gitHubAppKeyPath := os.Getenv("GITHUB_APP_KEY_PATH")
77+
gitHubAppId, _ := strconv.ParseInt(os.Getenv("GITHUB_APP_ID"), 10, 64)
78+
gitHubAppInstalaltionId, _ := strconv.ParseInt(os.Getenv("GITHUB_APP_INSTALLATION_ID"), 10, 64)
79+
gitHubRateLimit, _ := strconv.ParseFloat(cfg.GetEnv("GITHUB_RATE_LIMIT", "15000"), 64)
80+
appConfig.SetGitHubApp(true)
81+
appConfig.SetGitHubAppKeyPath(gitHubAppKeyPath)
82+
appConfig.SetGitHubAppId(gitHubAppId)
83+
appConfig.SetGitHubAppInstallationId(gitHubAppInstalaltionId)
84+
appConfig.SetGitHubRateLimit(gitHubRateLimit)
85+
err = appConfig.SetAPITokenFromGitHubApp()
86+
if err != nil {
87+
log.Errorf("Error initializing Configuration, Error: %v", err)
88+
}
89+
}
90+
5991
tokenEnv := os.Getenv("GITHUB_TOKEN")
6092
tokenFile := os.Getenv("GITHUB_TOKEN_FILE")
6193
if tokenEnv != "" {
@@ -66,7 +98,6 @@ func Init() Config {
6698
log.Errorf("Error initialising Configuration, Error: %v", err)
6799
}
68100
}
69-
70101
return appConfig
71102
}
72103

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

119+
// Returns the GitHub App authentication value
120+
func (c *Config) GitHubApp() bool {
121+
return c.gitHubApp
122+
}
123+
124+
// Returns the GitHub app private key path
125+
func (c *Config) GitHubAppKeyPath() string {
126+
return c.gitHubAppKeyPath
127+
}
128+
129+
// Returns the GitHub app id
130+
func (c *Config) GitHubAppId() int64 {
131+
return c.gitHubAppId
132+
}
133+
134+
// Returns the GitHub app installation id
135+
func (c *Config) GitHubAppInstallationId() int64 {
136+
return c.gitHubAppInstallationId
137+
}
138+
139+
// Returns the GitHub RateLimit
140+
func (c *Config) GitHubRateLimit() float64 {
141+
return c.gitHubRateLimit
142+
}
143+
88144
// Sets the base API URL returning an error if the supplied string is not a valid URL
89145
func (c *Config) SetAPIURL(u string) error {
90146
ur, err := url.Parse(u)
@@ -125,6 +181,45 @@ func (c *Config) SetAPITokenFromFile(tokenFile string) error {
125181
return nil
126182
}
127183

184+
// SetGitHubApp accepts a boolean for GitHub app authentication
185+
func (c *Config) SetGitHubApp(githubApp bool) {
186+
c.gitHubApp = githubApp
187+
}
188+
189+
// SetGitHubAppKeyPath accepts a string for GitHub app private key path
190+
func (c *Config) SetGitHubAppKeyPath(gitHubAppKeyPath string) {
191+
c.gitHubAppKeyPath = gitHubAppKeyPath
192+
}
193+
194+
// SetGitHubAppId accepts a string for GitHub app id
195+
func (c *Config) SetGitHubAppId(gitHubAppId int64) {
196+
c.gitHubAppId = gitHubAppId
197+
}
198+
199+
// SetGitHubAppInstallationId accepts a string for GitHub app installation id
200+
func (c *Config) SetGitHubAppInstallationId(gitHubAppInstallationId int64) {
201+
c.gitHubAppInstallationId = gitHubAppInstallationId
202+
}
203+
204+
// SetGitHubAppRateLimit accepts a string for GitHub RateLimit
205+
func (c *Config) SetGitHubRateLimit(gitHubRateLimit float64) {
206+
c.gitHubRateLimit = gitHubRateLimit
207+
}
208+
209+
// SetAPITokenFromGitHubApp generating api token from github app configuration.
210+
func (c *Config) SetAPITokenFromGitHubApp() error {
211+
itr, err := ghinstallation.NewKeyFromFile(http.DefaultTransport, c.gitHubAppId, c.gitHubAppInstallationId, c.gitHubAppKeyPath)
212+
if err != nil {
213+
return err
214+
}
215+
strToken, err := itr.Token(context.Background())
216+
if err != nil {
217+
return err
218+
}
219+
c.SetAPIToken(strToken)
220+
return nil
221+
}
222+
128223
// Init populates the Config struct based on environmental runtime configuration
129224
// All URL's are added to the TargetURL's string array
130225
func (c *Config) setScrapeURLs() error {
@@ -152,6 +247,7 @@ func (c *Config) setScrapeURLs() error {
152247
}
153248

154249
// Append github orginisations to the array
250+
155251
if len(c.organisations) > 0 {
156252
for _, x := range c.organisations {
157253
y := *c.apiUrl

exporter/prometheus.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package exporter
22

33
import (
4+
"path"
5+
"strconv"
6+
47
"github.com/prometheus/client_golang/prometheus"
58
log "github.com/sirupsen/logrus"
69
)
@@ -17,9 +20,22 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
1720
// Collect function, called on by Prometheus Client library
1821
// This function is called when a scrape is peformed on the /metrics page
1922
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
20-
2123
data := []*Datum{}
2224
var err error
25+
26+
if e.Config.GitHubApp() {
27+
needReAuth, err := e.isTokenExpired()
28+
if err != nil {
29+
log.Errorf("Error checking token expiration status: %v", err)
30+
return
31+
}
32+
if needReAuth {
33+
err = e.Config.SetAPITokenFromGitHubApp()
34+
if err != nil {
35+
log.Errorf("Error authenticating with GitHub app: %v", err)
36+
}
37+
}
38+
}
2339
// Scrape the Data from Github
2440
if len(e.TargetURLs()) > 0 {
2541
data, err = e.gatherData()
@@ -46,3 +62,32 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
4662
log.Info("All Metrics successfully collected")
4763

4864
}
65+
66+
func (e *Exporter) isTokenExpired() (bool, error) {
67+
u := *e.APIURL()
68+
u.Path = path.Join(u.Path, "rate_limit")
69+
70+
resp, err := getHTTPResponse(u.String(), e.APIToken())
71+
72+
if err != nil {
73+
return false, err
74+
}
75+
defer resp.Body.Close()
76+
// Triggers if rate-limiting isn't enabled on private Github Enterprise installations
77+
if resp.StatusCode == 404 {
78+
return false, nil
79+
}
80+
81+
limit, err := strconv.ParseFloat(resp.Header.Get("X-RateLimit-Limit"), 64)
82+
83+
if err != nil {
84+
return false, err
85+
}
86+
87+
defaultRateLimit := e.Config.GitHubRateLimit()
88+
if limit < defaultRateLimit {
89+
return true, nil
90+
}
91+
return false, nil
92+
93+
}

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/githubexporter/github-exporter
33
go 1.22
44

55
require (
6+
github.com/bradleyfalzon/ghinstallation v1.1.1
67
github.com/infinityworks/go-common v0.0.0-20170820165359-7f20a140fd37
78
github.com/prometheus/client_golang v1.14.0
89
github.com/sirupsen/logrus v1.9.0
@@ -14,14 +15,18 @@ require (
1415
github.com/beorn7/perks v1.0.1 // indirect
1516
github.com/cespare/xxhash/v2 v2.2.0 // indirect
1617
github.com/davecgh/go-spew v1.1.1 // indirect
18+
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
1719
github.com/golang/protobuf v1.5.2 // indirect
20+
github.com/google/go-github/v29 v29.0.2 // indirect
21+
github.com/google/go-querystring v1.0.0 // indirect
1822
github.com/kr/pretty v0.3.1 // indirect
1923
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
2024
github.com/pmezard/go-difflib v1.0.0 // indirect
2125
github.com/prometheus/client_model v0.3.0 // indirect
2226
github.com/prometheus/common v0.39.0 // indirect
2327
github.com/prometheus/procfs v0.9.0 // indirect
2428
github.com/stretchr/testify v1.8.0 // indirect
29+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 // indirect
2530
golang.org/x/sys v0.4.0 // indirect
2631
google.golang.org/protobuf v1.28.1 // indirect
2732
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
22
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
3+
github.com/bradleyfalzon/ghinstallation v1.1.1 h1:pmBXkxgM1WeF8QYvDLT5kuQiHMcmf+X015GI0KM/E3I=
4+
github.com/bradleyfalzon/ghinstallation v1.1.1/go.mod h1:vyCmHTciHx/uuyN82Zc3rXN3X2KTK8nUTCrTMwAhcug=
35
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
46
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
57
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
68
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
79
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
810
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
11+
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
12+
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
913
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
14+
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
1015
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
1116
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
1217
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
1318
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
19+
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
1420
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
1521
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
22+
github.com/google/go-github/v29 v29.0.2 h1:opYN6Wc7DOz7Ku3Oh4l7prmkOMwEcQxpFtxdU8N8Pts=
23+
github.com/google/go-github/v29 v29.0.2/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E=
24+
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
25+
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
1626
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
1727
github.com/infinityworks/go-common v0.0.0-20170820165359-7f20a140fd37 h1:Lm6kyC3JBiJQvJrus66He0E4viqDc/m5BdiFNSkIFfU=
1828
github.com/infinityworks/go-common v0.0.0-20170820165359-7f20a140fd37/go.mod h1:+OaHNKQvQ9oOCr+DgkF95PkiDx20fLHpzMp8SmRPQTg=
@@ -48,11 +58,18 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
4858
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
4959
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
5060
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=
61+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
62+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
63+
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
64+
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
5165
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
66+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
5267
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
5368
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
5469
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
70+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
5571
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
72+
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
5673
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
5774
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
5875
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=

0 commit comments

Comments
 (0)