Skip to content

Commit 51ecbdd

Browse files
dblinkhornguicauladakalgurn
authored
Allow app configuration using org or repo name in lieu of installation id (#98)
* Allow app configuration using GITHUB_ORG_NAME and optional GITHUB_REPO_NAME instead of INSTALLATION_ID. This enables operators to configure the app without needing the installation ID, using the App ID and Private Key for retrieval. Previous authentication with GITHUB_INSTALLATION_ID still works normally. * matching go-version to the updated code * fixed failing test TestAppConfig_InitClient, handled nil httpClient in initAppClient() * fixed failing test TestInitConfigAppWithoutInstallationID, InstallationID should be 0 in testAuth * unset env GITHUB_INSTALLATION_ID that was being set in TestInitConfigApp() --------- Co-authored-by: Guilherme Caulada <[email protected]> Co-authored-by: Kostiantyn Kulbachnyi <[email protected]>
1 parent 4fdf417 commit 51ecbdd

File tree

7 files changed

+347
-77
lines changed

7 files changed

+347
-77
lines changed

.github/workflows/pull_request.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
- amd64
2222
- arm64
2323
go-version:
24-
- '1.17'
24+
- '1.22'
2525
steps:
2626

2727
- uses: actions/checkout@v3
@@ -59,7 +59,7 @@ jobs:
5959
strategy:
6060
matrix:
6161
go-version:
62-
- '1.17'
62+
- '1.22'
6363
needs:
6464
- build
6565
steps:

.github/workflows/release.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
strategy:
1919
matrix:
2020
go-version:
21-
- '1.17'
21+
- '1.22'
2222
steps:
2323

2424
- uses: actions/checkout@v3
@@ -84,7 +84,7 @@ jobs:
8484
- amd64
8585
- arm64
8686
go-version:
87-
- '1.17'
87+
- '1.22'
8888
steps:
8989

9090
- uses: actions/checkout@v3

go.mod

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,38 @@
11
module github.com/kalgurn/github-rate-limits-prometheus-exporter
22

3-
go 1.17
3+
go 1.21
4+
5+
toolchain go1.21.3
46

57
require (
6-
github.com/bradleyfalzon/ghinstallation/v2 v2.0.4
7-
github.com/google/go-github v17.0.0+incompatible
8-
github.com/migueleliasweb/go-github-mock v0.0.8
9-
github.com/prometheus/client_golang v1.12.2
10-
github.com/stretchr/testify v1.7.1
11-
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
8+
github.com/bradleyfalzon/ghinstallation/v2 v2.2.0
9+
github.com/google/go-github/v65 v65.0.0
10+
github.com/migueleliasweb/go-github-mock v0.0.16
11+
github.com/prometheus/client_golang v1.14.0
12+
github.com/stretchr/testify v1.8.2
13+
golang.org/x/oauth2 v0.6.0
1214
)
1315

16+
require github.com/golang-jwt/jwt/v5 v5.2.1
17+
1418
require (
1519
github.com/beorn7/perks v1.0.1 // indirect
1620
github.com/cespare/xxhash/v2 v2.1.2 // indirect
1721
github.com/davecgh/go-spew v1.1.1 // indirect
18-
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
22+
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
1923
github.com/golang/protobuf v1.5.2 // indirect
20-
github.com/google/go-github/v41 v41.0.0 // indirect
24+
github.com/google/go-github/v50 v50.1.0 // indirect
2125
github.com/google/go-querystring v1.1.0 // indirect
2226
github.com/gorilla/mux v1.8.0 // indirect
2327
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
2428
github.com/pmezard/go-difflib v1.0.0 // indirect
25-
github.com/prometheus/client_model v0.2.0 // indirect
26-
github.com/prometheus/common v0.32.1 // indirect
27-
github.com/prometheus/procfs v0.7.3 // indirect
29+
github.com/prometheus/client_model v0.3.0 // indirect
30+
github.com/prometheus/common v0.37.0 // indirect
31+
github.com/prometheus/procfs v0.8.0 // indirect
2832
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b // indirect
29-
golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect
30-
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
33+
golang.org/x/net v0.8.0 // indirect
34+
golang.org/x/sys v0.6.0 // indirect
3135
google.golang.org/appengine v1.6.7 // indirect
32-
google.golang.org/protobuf v1.27.1 // indirect
33-
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
36+
google.golang.org/protobuf v1.28.1 // indirect
37+
gopkg.in/yaml.v3 v3.0.1 // indirect
3438
)

go.sum

Lines changed: 46 additions & 30 deletions
Large diffs are not rendered by default.

internal/github_client/github_client.go

Lines changed: 97 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,21 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7+
"os"
78
"strconv"
89
"time"
910

1011
"github.com/bradleyfalzon/ghinstallation/v2"
11-
"github.com/google/go-github/github"
12+
"github.com/golang-jwt/jwt/v5"
13+
"github.com/google/go-github/v65/github"
1214
"github.com/kalgurn/github-rate-limits-prometheus-exporter/internal/utils"
1315
"golang.org/x/oauth2"
1416
)
1517

1618
func GetRemainingLimits(c *github.Client) RateLimits {
1719
ctx := context.Background()
1820

19-
limits, _, err := c.RateLimits(ctx)
21+
limits, _, err := c.RateLimit.Get(ctx)
2022
if err != nil {
2123
utils.RespError(err)
2224
}
@@ -29,44 +31,37 @@ func GetRemainingLimits(c *github.Client) RateLimits {
2931
}
3032
}
3133

32-
func (c TokenConfig) InitClient() *github.Client {
33-
ctx := context.Background()
34-
ts := oauth2.StaticTokenSource(
35-
&oauth2.Token{AccessToken: c.Token},
36-
)
37-
tc := oauth2.NewClient(ctx, ts)
38-
return github.NewClient(tc)
39-
34+
func (c *TokenConfig) InitClient() *github.Client {
35+
return initTokenClient(c, http.DefaultClient)
4036
}
4137

42-
func (c AppConfig) InitClient() *github.Client {
43-
// Shared transport to reuse TCP connections.
44-
tr := http.DefaultTransport
45-
46-
// Wrap the shared transport for use with the app ID 1 authenticating with installation ID 99.
47-
itr, err := ghinstallation.NewKeyFromFile(tr, c.AppID, c.InstallationID, c.PrivateKeyPath)
48-
utils.RespError(err)
49-
50-
// Use installation transport with github.com/google/go-github
51-
return github.NewClient(&http.Client{Transport: itr})
38+
func (c *AppConfig) InitClient() *github.Client {
39+
return initAppClient(c, http.DefaultClient)
5240
}
5341

5442
func InitConfig() GithubClient {
5543
// determine type (app or pat)
5644
var auth GithubClient
5745
authType := utils.GetOSVar("GITHUB_AUTH_TYPE")
5846
if authType == "PAT" {
59-
auth = TokenConfig{
47+
auth = &TokenConfig{
6048
Token: utils.GetOSVar("GITHUB_TOKEN"),
6149
}
6250

6351
} else if authType == "APP" {
6452
appID, _ := strconv.ParseInt(utils.GetOSVar("GITHUB_APP_ID"), 10, 64)
65-
installationID, _ := strconv.ParseInt(utils.GetOSVar("GITHUB_INSTALLATION_ID"), 10, 64)
6653

67-
auth = AppConfig{
54+
var installationID int64
55+
envInstallationID := utils.GetOSVar("GITHUB_INSTALLATION_ID")
56+
if envInstallationID != "" {
57+
installationID, _ = strconv.ParseInt(envInstallationID, 10, 64)
58+
}
59+
60+
auth = &AppConfig{
6861
AppID: appID,
6962
InstallationID: installationID,
63+
OrgName: utils.GetOSVar("GITHUB_ORG_NAME"),
64+
RepoName: utils.GetOSVar("GITHUB_REPO_NAME"),
7065
PrivateKeyPath: utils.GetOSVar("GITHUB_PRIVATE_KEY_PATH"),
7166
}
7267
} else {
@@ -78,3 +73,82 @@ func InitConfig() GithubClient {
7873
return auth
7974

8075
}
76+
77+
// Helper function to allow testing client initialization with custom http clients
78+
func initTokenClient(c *TokenConfig, httpClient *http.Client) *github.Client {
79+
if httpClient == http.DefaultClient {
80+
ctx := context.Background()
81+
ts := oauth2.StaticTokenSource(
82+
&oauth2.Token{AccessToken: c.Token},
83+
)
84+
httpClient = oauth2.NewClient(ctx, ts)
85+
}
86+
return github.NewClient(httpClient)
87+
}
88+
89+
// Helper function to allow testing client initialization with custom http clients
90+
func initAppClient(c *AppConfig, httpClient *http.Client) *github.Client {
91+
if c.InstallationID == 0 && c.OrgName != "" {
92+
// Retrieve the installation ID if not provided
93+
auth := &TokenConfig{
94+
Token: generateJWT(c.AppID, c.PrivateKeyPath),
95+
}
96+
client := initTokenClient(auth, httpClient)
97+
98+
var err error
99+
var installation *github.Installation
100+
ctx := context.Background()
101+
if c.RepoName != "" {
102+
installation, _, err = client.Apps.FindRepositoryInstallation(ctx, c.OrgName, c.RepoName)
103+
} else {
104+
installation, _, err = client.Apps.FindOrganizationInstallation(ctx, c.OrgName)
105+
}
106+
utils.RespError(err)
107+
108+
c.InstallationID = installation.GetID()
109+
}
110+
111+
if httpClient == nil {
112+
httpClient = &http.Client{}
113+
}
114+
115+
if httpClient == http.DefaultClient {
116+
tr := http.DefaultTransport
117+
itr, err := ghinstallation.NewKeyFromFile(tr, c.AppID, c.InstallationID, c.PrivateKeyPath)
118+
utils.RespError(err)
119+
httpClient = &http.Client{Transport: itr}
120+
} else {
121+
// Wrap the existing transport
122+
tr := httpClient.Transport
123+
if tr == nil {
124+
tr = http.DefaultTransport
125+
}
126+
itr, err := ghinstallation.NewKeyFromFile(tr, c.AppID, c.InstallationID, c.PrivateKeyPath)
127+
utils.RespError(err)
128+
httpClient.Transport = itr
129+
}
130+
131+
return github.NewClient(httpClient)
132+
}
133+
134+
// Helper function to generate JWT for GitHub App
135+
func generateJWT(appID int64, privateKeyPath string) string {
136+
privateKey, err := os.ReadFile(privateKeyPath)
137+
utils.RespError(err)
138+
139+
parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(privateKey)
140+
utils.RespError(err)
141+
142+
now := time.Now()
143+
claims := jwt.RegisteredClaims{
144+
Issuer: fmt.Sprintf("%d", appID),
145+
IssuedAt: jwt.NewNumericDate(now),
146+
ExpiresAt: jwt.NewNumericDate(now.Add(10 * time.Minute)),
147+
}
148+
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
149+
150+
signedToken, err := token.SignedString(parsedKey)
151+
utils.RespError(err)
152+
153+
return signedToken
154+
}

0 commit comments

Comments
 (0)