diff --git a/backend/cmd/gh-updater/main.go b/backend/cmd/gh-updater/main.go index 6568fbc..32ac560 100644 --- a/backend/cmd/gh-updater/main.go +++ b/backend/cmd/gh-updater/main.go @@ -10,7 +10,7 @@ import ( "strconv" "time" - "github.com/bee-ci/bee-ci-system/internal/common/gh_service" + "github.com/bee-ci/bee-ci-system/internal/common/ghservice" "github.com/bee-ci/bee-ci-system/internal/data" "github.com/bee-ci/bee-ci-system/internal/updater" @@ -59,7 +59,7 @@ func main() { userRepo := data.NewPostgresUserRepo(db) repoRepo := data.NewPostgresRepoRepo(db) - githubService := gh_service.NewGithubService(githubAppID, rsaPrivateKey) + githubService := ghservice.NewGithubService(githubAppID, rsaPrivateKey) minReconnectInterval := 10 * time.Second maxReconnectInterval := time.Minute diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index ca64d94..fbf2e29 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -11,7 +11,7 @@ import ( "strconv" "time" - "github.com/bee-ci/bee-ci-system/internal/common/gh_service" + "github.com/bee-ci/bee-ci-system/internal/common/ghservice" "github.com/golang-jwt/jwt/v5" influxdb2 "github.com/influxdata/influxdb-client-go/v2" @@ -94,7 +94,7 @@ func main() { repoRepo := data.NewPostgresRepoRepo(db) logsRepo := data.NewInfluxLogsRepo(influxClient, influxOrg, influxBucket) - githubService := gh_service.NewGithubService(githubAppID, rsaPrivateKey) + githubService := ghservice.NewGithubService(githubAppID, rsaPrivateKey) webhooks := webhook.NewWebhookHandler(userRepo, repoRepo, buildRepo, githubService, mainDomain, redirectURL, githubAppClientID, githubAppClientSecret, githubAppWebhookSecret, jwtSecret) app := api.NewApp(buildRepo, logsRepo, repoRepo, userRepo, jwtSecret) diff --git a/backend/internal/common/auth/bearer_transport.go b/backend/internal/common/auth/bearer_transport.go index 5c2b15e..4c5e464 100644 --- a/backend/internal/common/auth/bearer_transport.go +++ b/backend/internal/common/auth/bearer_transport.go @@ -1,25 +1,3 @@ // Package auth implements a BearerTransport type that implements the // http.RoundTripper interface. package auth - -import ( - "net/http" -) - -type BearerTransport struct { - Token string - Transport http.RoundTripper -} - -// RoundTrip implements http.RoundTripper interface. -func (b *BearerTransport) RoundTrip(r *http.Request) (*http.Response, error) { - clonedRequest := r.Clone(r.Context()) - clonedRequest.Header.Set("Authorization", "Bearer "+b.Token) - clonedRequest.Header.Set("Accept", "application/json") - - if b.Transport == nil { - b.Transport = http.DefaultTransport - } - - return b.Transport.RoundTrip(clonedRequest) -} diff --git a/backend/internal/common/gh_service/gh_service.go b/backend/internal/common/ghservice/ghservice.go similarity index 56% rename from backend/internal/common/gh_service/gh_service.go rename to backend/internal/common/ghservice/ghservice.go index f6c5d2e..c4ccbef 100644 --- a/backend/internal/common/gh_service/gh_service.go +++ b/backend/internal/common/ghservice/ghservice.go @@ -1,5 +1,5 @@ -// Package gh_service exposes a simple service that makes it easy to get app installation tokens. -package gh_service +// Package ghservice exposes a simple service that makes it easy to get app installation tokens. +package ghservice import ( "context" @@ -9,7 +9,6 @@ import ( "net/http" "time" - "github.com/bee-ci/bee-ci-system/internal/common/auth" "github.com/golang-jwt/jwt/v5" "github.com/google/go-github/v64/github" ) @@ -30,20 +29,34 @@ func NewGithubService(githubAppID int64, rsaPrivateKey *rsa.PrivateKey) *GithubS } } +func (g GithubService) GetClientForInstallation(ctx context.Context, installationID int64) (*github.Client, error) { + // token, err := getFromRedis // TODO: Get from Redis + + token, err := g.getInstallationAccessToken(ctx, installationID) + if err != nil { + return nil, fmt.Errorf("get installation access token: %w", err) + } + + client := github.NewClient(&http.Client{ + Transport: &bearerTransport{Token: token}, + }) + + return client, nil +} + // TODO: Cache the token in some KV store, for example Redis. Before returning // it, always check if 1 hour has passed. -// GetInstallationAccessToken returns the installation access token for the -// [installationID]. +// getInstallationAccessToken returns the installation access token for the [installationID]. // // The token returned is short-lived – per GitHub docs, it expires after 1 hour. -func (g GithubService) GetInstallationAccessToken(ctx context.Context, installationID int64) (string, error) { +func (g GithubService) getInstallationAccessToken(ctx context.Context, installationID int64) (string, error) { jwtString, err := g.generateSignedJWT(g.githubAppID, g.rsaPrivateKey) if err != nil { return "", fmt.Errorf("generate signed jwt: %w", err) } - appClient := http.Client{Transport: &auth.BearerTransport{Token: jwtString}} + appClient := http.Client{Transport: &bearerTransport{Token: jwtString}} gh := github.NewClient(&appClient) res, _, err := gh.Apps.CreateInstallationToken(ctx, installationID, nil) if err != nil { @@ -68,3 +81,21 @@ func (g GithubService) generateSignedJWT(githubAppID int64, rsaPrivateKey *rsa.P return tokenStr, nil } + +type bearerTransport struct { + Token string + Transport http.RoundTripper +} + +// RoundTrip implements http.RoundTripper interface. +func (b *bearerTransport) RoundTrip(r *http.Request) (*http.Response, error) { + clonedRequest := r.Clone(r.Context()) + clonedRequest.Header.Set("Authorization", "Bearer "+b.Token) + clonedRequest.Header.Set("Accept", "application/json") + + if b.Transport == nil { + b.Transport = http.DefaultTransport + } + + return b.Transport.RoundTrip(clonedRequest) +} diff --git a/backend/internal/server/webhook/webhook.go b/backend/internal/server/webhook/webhook.go index 1fd298a..027d984 100644 --- a/backend/internal/server/webhook/webhook.go +++ b/backend/internal/server/webhook/webhook.go @@ -12,8 +12,7 @@ import ( "strconv" "time" - "github.com/bee-ci/bee-ci-system/internal/common/auth" - ghs "github.com/bee-ci/bee-ci-system/internal/common/gh_service" + ghs "github.com/bee-ci/bee-ci-system/internal/common/ghservice" "github.com/golang-jwt/jwt/v5" "github.com/google/go-github/v64/github" @@ -254,17 +253,13 @@ func (h WebhookHandler) handleWebhook(w http.ResponseWriter, r *http.Request) { if *event.Action == "created" { // Example response: https://github.com/octokit/webhooks/blob/main/payload-examples/api.github.com/installation/created.payload.json - installationAccessToken, err := h.githubService.GetInstallationAccessToken(r.Context(), *installation.ID) + ghClient, err := h.githubService.GetClientForInstallation(r.Context(), *installation.ID) if err != nil { - logger.Error("get installation access token", slog.Any("error", err)) + logger.Error("get client for installation", slog.Any("error", err)) w.WriteHeader(http.StatusInternalServerError) return } - ghClient := github.NewClient(&http.Client{ - Transport: &auth.BearerTransport{Token: installationAccessToken}, - }) - // The webhook event doesn't contain all repository data we need: // - description // - latest commit SHA @@ -311,10 +306,10 @@ func (h WebhookHandler) handleWebhook(w http.ResponseWriter, r *http.Request) { } case "removed": removedRepositories := event.RepositoriesRemoved - repos := mapRepos(userID, removedRepositories) - repoIDs := make([]int64, 0, len(repos)) - for _, repo := range repos { - repoIDs = append(repoIDs, repo.ID) + + repoIDs := make([]int64, 0, len(removedRepositories)) + for _, removedRepository := range removedRepositories { + repoIDs = append(repoIDs, *removedRepository.ID) } err = h.repoRepo.Delete(r.Context(), repoIDs) @@ -332,7 +327,7 @@ func (h WebhookHandler) handleWebhook(w http.ResponseWriter, r *http.Request) { logger.Debug(fmt.Sprintf("check suite %s", *event.Action), slog.String("owner", *event.Repo.Owner.Login), - slog.String("repo", *event.Repo.Name), + slog.String("removedRepository", *event.Repo.Name), slog.Int64("installation_id", installationID), slog.String("head_sha", headSHA), ) diff --git a/backend/internal/updater/updater.go b/backend/internal/updater/updater.go index 3229cb0..8fb6800 100644 --- a/backend/internal/updater/updater.go +++ b/backend/internal/updater/updater.go @@ -11,9 +11,7 @@ import ( "strconv" "time" - ghs "github.com/bee-ci/bee-ci-system/internal/common/gh_service" - - "github.com/bee-ci/bee-ci-system/internal/common/auth" + ghs "github.com/bee-ci/bee-ci-system/internal/common/ghservice" "github.com/google/go-github/v64/github" "github.com/bee-ci/bee-ci-system/internal/data" @@ -130,15 +128,11 @@ func (u Updater) createCheckRun(ctx context.Context, build data.Build) (checkRun return 0, fmt.Errorf("get user: %w", err) } - installationAccessToken, err := u.githubService.GetInstallationAccessToken(ctx, build.InstallationID) + ghClient, err := u.githubService.GetClientForInstallation(ctx, build.InstallationID) if err != nil { - return 0, fmt.Errorf("get installation access token: %w", err) + return 0, fmt.Errorf("get client for installation: %w", err) } - ghClient := github.NewClient(&http.Client{ - Transport: &auth.BearerTransport{Token: installationAccessToken}, - }) - createCheckRunOptions := github.CreateCheckRunOptions{ // TODO: Get name from the BeeCI config file? Name: build.CommitMsg + ", started at: " + fmt.Sprint(time.Now().Format(time.RFC822Z)), @@ -185,15 +179,11 @@ func (u Updater) updateCheckRun(ctx context.Context, checkRunID int64, build dat return fmt.Errorf("get user: %w", err) } - installationAccessToken, err := u.githubService.GetInstallationAccessToken(ctx, build.InstallationID) + ghClient, err := u.githubService.GetClientForInstallation(ctx, build.InstallationID) if err != nil { - return fmt.Errorf("get installation access token: %w", err) + return fmt.Errorf("get client for installation: %w", err) } - ghClient := github.NewClient(&http.Client{ - Transport: &auth.BearerTransport{Token: installationAccessToken}, - }) - // TODO: Do I need to set these options again, or if I set them to null they will be removed? checkRunUpdateOptions := github.UpdateCheckRunOptions{ Name: build.CommitMsg + ", started at: " + fmt.Sprint(time.Now().Format(time.RFC822Z)),