Skip to content

Commit b6b6a83

Browse files
committed
refactor: optimize GHApp installation ID retrieval
- Removed listing of all installations. - Fetched the installation directly using the repository URL. - Added fallback to find the organization installation if the repository installation is not found. - Enhanced error handling for token retrieval. - Updated tests to reflect the changes. - Eliminated the matchRepos method from Install struct as it was unused
1 parent 542f1a0 commit b6b6a83

File tree

2 files changed

+43
-109
lines changed

2 files changed

+43
-109
lines changed

pkg/provider/github/app/token.go

Lines changed: 34 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7+
"net/url"
8+
"strings"
79
"time"
810

911
"github.com/golang-jwt/jwt/v4"
10-
gt "github.com/google/go-github/v71/github"
1112
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
1213
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
1314
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider/github"
@@ -20,8 +21,6 @@ type Install struct {
2021
repo *v1alpha1.Repository
2122
ghClient *github.Provider
2223
namespace string
23-
24-
repoList []string
2524
}
2625

2726
func NewInstallation(req *http.Request, run *params.Run, repo *v1alpha1.Repository, gh *github.Provider, namespace string) *Install {
@@ -38,91 +37,60 @@ func NewInstallation(req *http.Request, run *params.Run, repo *v1alpha1.Reposito
3837
}
3938

4039
// GetAndUpdateInstallationID retrieves and updates the installation ID for the GitHub App.
41-
// It generates a JWT token, lists all installations, and matches repositories to their installation IDs.
42-
// If a matching repository is found, it returns the enterprise host, token, and installation ID.
40+
// It generates a JWT token, and directly fetches the installation for the
41+
// repository.
4342
func (ip *Install) GetAndUpdateInstallationID(ctx context.Context) (string, string, int64, error) {
44-
var (
45-
enterpriseHost, token string
46-
installationID int64
47-
)
43+
logger := logging.FromContext(ctx)
4844

4945
// Generate a JWT token for authentication
5046
jwtToken, err := ip.GenerateJWT(ctx)
5147
if err != nil {
5248
return "", "", 0, err
5349
}
5450

51+
// Get owner and repo from the repository URL
52+
repoURL, err := url.Parse(ip.repo.Spec.URL)
53+
if err != nil {
54+
return "", "", 0, fmt.Errorf("failed to parse repository URL: %w", err)
55+
}
56+
pathParts := strings.Split(strings.Trim(repoURL.Path, "/"), "/")
57+
if len(pathParts) < 2 {
58+
return "", "", 0, fmt.Errorf("invalid repository URL path: %s", repoURL.Path)
59+
}
60+
owner := pathParts[0]
61+
repoName := pathParts[1]
62+
5563
apiURL := *ip.ghClient.APIURL
56-
enterpriseHost = ip.request.Header.Get("X-GitHub-Enterprise-Host")
64+
enterpriseHost := ip.request.Header.Get("X-GitHub-Enterprise-Host")
5765
if enterpriseHost != "" {
58-
// NOTE: Hopefully this works even when the GHE URL is on another host than the API URL
5966
apiURL = "https://" + enterpriseHost + "/api/v3"
6067
}
6168

62-
logger := logging.FromContext(ctx)
63-
opt := &gt.ListOptions{PerPage: ip.ghClient.PaginedNumber}
6469
client, _, _ := github.MakeClient(ctx, apiURL, jwtToken)
65-
installationData := []*gt.Installation{}
66-
67-
// List all installations
68-
for {
69-
installationSet, resp, err := client.Apps.ListInstallations(ctx, opt)
70+
// Directly get the installation for the repository
71+
installation, _, err := client.Apps.FindRepositoryInstallation(ctx, owner, repoName)
72+
if err != nil {
73+
// Fallback to finding organization installation if repository installation is not found
74+
installation, _, err = client.Apps.FindOrganizationInstallation(ctx, owner)
7075
if err != nil {
71-
return "", "", 0, err
76+
return "", "", 0, fmt.Errorf("could not find repository or organization installation for %s/%s: %w", owner, repoName, err)
7277
}
73-
installationData = append(installationData, installationSet...)
74-
if resp.NextPage == 0 {
75-
break
76-
}
77-
opt.Page = resp.NextPage
7878
}
7979

80-
// Iterate through each installation to find a matching repository
81-
for i := range installationData {
82-
if installationData[i].ID == nil {
83-
return "", "", 0, fmt.Errorf("installation ID is nil")
84-
}
85-
if *installationData[i].ID != 0 {
86-
token, err = ip.ghClient.GetAppToken(ctx, ip.run.Clients.Kube, enterpriseHost, *installationData[i].ID, ip.namespace)
87-
// While looping on the list of installations, there could be cases where we can't
88-
// obtain a token for installation. In a test I did for GitHub App with ~400
89-
// installations, there were 3 failing consistently with:
90-
// "could not refresh installation id XXX's token: received non 2xx response status "403 Forbidden".
91-
// If there is a matching installation after the failure, we miss it. So instead of
92-
// failing, we just log the error and continue. Token is "".
93-
if err != nil {
94-
logger.Warn(err)
95-
continue
96-
}
97-
}
98-
exist, err := ip.matchRepos(ctx)
99-
if err != nil {
100-
return "", "", 0, err
101-
}
102-
if exist {
103-
installationID = *installationData[i].ID
104-
break
105-
}
80+
if installation.ID == nil {
81+
return "", "", 0, fmt.Errorf("installation ID is nil")
10682
}
107-
return enterpriseHost, token, installationID, nil
108-
}
10983

110-
// matchRepos matches GitHub repositories to their installation IDs.
111-
// It lists all repositories accessible to the app installation and checks if
112-
// any match the repository URL in the spec.
113-
func (ip *Install) matchRepos(ctx context.Context) (bool, error) {
114-
installationRepoList, err := github.ListRepos(ctx, ip.ghClient)
84+
installationID := *installation.ID
85+
token, err := ip.ghClient.GetAppToken(ctx, ip.run.Clients.Kube, enterpriseHost, installationID, ip.namespace)
11586
if err != nil {
116-
return false, err
117-
}
118-
ip.repoList = append(ip.repoList, installationRepoList...)
119-
for i := range installationRepoList {
120-
// If URL matches with repo spec URL then we can break the loop
121-
if installationRepoList[i] == ip.repo.Spec.URL {
122-
return true, nil
123-
}
87+
logger.Warnf("Could not get a token for installation ID %d: %v", installationID, err)
88+
// Return with the installation ID even if token generation fails,
89+
// as some operations might only need the ID.
90+
return enterpriseHost, "", installationID, nil
12491
}
125-
return false, nil
92+
93+
return enterpriseHost, token, installationID, nil
12694
}
12795

12896
// JWTClaim represents the JWT claims for the GitHub App.

pkg/provider/github/app/token_test.go

Lines changed: 9 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ func Test_GetAndUpdateInstallationID(t *testing.T) {
170170
badToken := "BADTOKEN"
171171
badID := 666
172172
missingID := 111
173+
orgName := "org"
173174

174175
fakeghclient, mux, serverURL, teardown := ghtesthelper.SetupGH()
175176
defer teardown()
@@ -214,7 +215,7 @@ func Test_GetAndUpdateInstallationID(t *testing.T) {
214215
Name: "repo",
215216
},
216217
Spec: v1alpha1.RepositorySpec{
217-
URL: "https://matched/by/incoming",
218+
URL: fmt.Sprintf("https://matched/%s/incoming", orgName),
218219
Incomings: &[]v1alpha1.Incoming{
219220
{
220221
Targets: []string{"main"},
@@ -235,6 +236,10 @@ func Test_GetAndUpdateInstallationID(t *testing.T) {
235236
_, _ = fmt.Fprintf(w, `{"token": "%s"}`, wantToken)
236237
})
237238

239+
mux.HandleFunc(fmt.Sprintf("/orgs/%s/installation", orgName), func(w http.ResponseWriter, _ *http.Request) {
240+
_, _ = fmt.Fprintf(w, `{"id": %d}`, wantID)
241+
})
242+
238243
mux.HandleFunc(fmt.Sprintf("/app/installations/%d/access_tokens", badID), func(w http.ResponseWriter, r *http.Request) {
239244
testMethod(t, r, "POST")
240245
w.Header().Set("Authorization", "Bearer "+jwtToken)
@@ -247,7 +252,9 @@ func Test_GetAndUpdateInstallationID(t *testing.T) {
247252
mux.HandleFunc("/installation/repositories", func(w http.ResponseWriter, _ *http.Request) {
248253
w.Header().Set("Authorization", "Bearer 12345")
249254
w.Header().Set("Accept", "application/vnd.github+json")
250-
_, _ = fmt.Fprint(w, `{"total_count": 2,"repositories": [{"id":1,"html_url": "https://matched/by/incoming"},{"id":2,"html_url": "https://anotherrepo/that/would/failit"}]}`)
255+
_, _ = fmt.Fprintf(w,
256+
`{"total_count": 2,"repositories": [{"id":1,"html_url": "https://matched/%s/incoming"},{"id":2,"html_url": "https://anotherrepo/that/would/failit"}]}`,
257+
orgName)
251258
})
252259
ip = NewInstallation(req, run, repo, gprovider, testNamespace.GetName())
253260
_, token, installationID, err := ip.GetAndUpdateInstallationID(ctx)
@@ -263,44 +270,3 @@ func testMethod(t *testing.T, r *http.Request, want string) {
263270
t.Errorf("Request method: %v, want %v", got, want)
264271
}
265272
}
266-
267-
func Test_ListRepos(t *testing.T) {
268-
fakeclient, mux, _, teardown := ghtesthelper.SetupGH()
269-
defer teardown()
270-
271-
mux.HandleFunc("user/installations/1/repositories/2", func(rw http.ResponseWriter, _ *http.Request) {
272-
fmt.Fprint(rw)
273-
})
274-
275-
mux.HandleFunc("/installation/repositories", func(w http.ResponseWriter, _ *http.Request) {
276-
w.Header().Set("Authorization", "Bearer 12345")
277-
w.Header().Set("Accept", "application/vnd.github+json")
278-
_, _ = fmt.Fprint(w, `{"total_count": 1,"repositories": [{"id":1,"html_url": "https://matched/by/incoming"}]}`)
279-
})
280-
281-
repo := &v1alpha1.Repository{
282-
ObjectMeta: metav1.ObjectMeta{
283-
Name: "repo",
284-
},
285-
Spec: v1alpha1.RepositorySpec{
286-
URL: "https://matched/by/incoming",
287-
Incomings: &[]v1alpha1.Incoming{
288-
{
289-
Targets: []string{"main"},
290-
Secret: v1alpha1.Secret{
291-
Name: "secret",
292-
},
293-
},
294-
},
295-
},
296-
}
297-
298-
ctx, _ := rtesting.SetupFakeContext(t)
299-
gprovider := &github.Provider{}
300-
gprovider.SetGithubClient(fakeclient)
301-
ip := NewInstallation(httptest.NewRequest(http.MethodGet, "http://localhost", strings.NewReader("")),
302-
&params.Run{}, repo, gprovider, testNamespace.GetName())
303-
exist, err := ip.matchRepos(ctx)
304-
assert.NilError(t, err)
305-
assert.Equal(t, exist, true)
306-
}

0 commit comments

Comments
 (0)