Skip to content

Commit

Permalink
feat: support installation via Github app
Browse files Browse the repository at this point in the history
  • Loading branch information
vrivellino committed Jan 29, 2024
1 parent c9c58f9 commit 72bec95
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 34 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ go.work
git-rev.txt
/argo-diff
/.env.sh
*.pem
14 changes: 12 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/rs/zerolog/log"

"github.com/vince-riv/argo-diff/internal/argocd"
"github.com/vince-riv/argo-diff/internal/github"
"github.com/vince-riv/argo-diff/internal/server"
)

Expand Down Expand Up @@ -75,11 +76,20 @@ func main() {
log.Fatal().Msg("ARGOCD_SERVER_ADDR environment variable not set")
}
if os.Getenv("GITHUB_PERSONAL_ACCESS_TOKEN") == "" {
log.Fatal().Msg("GITHUB_PERSONAL_ACCESS_TOKEN environment variable not set")
log.Info().Msg("GITHUB_PERSONAL_ACCESS_TOKEN environment variable not set - assuming Github App installation")
for _, e := range []string{"GITHUB_APP_ID", "GITHUB_INSTALLATION_ID", "GITHUB_PRIVATE_KEY_FILE"} {
if os.Getenv(e) == "" {
log.Fatal().Msgf("%s environment variable is not set for Github App installations", e)
}
}
}

if err := argocd.ConnectivityCheck(); err != nil {
log.Fatal().Err(err).Msg("Connectivity check failed")
log.Fatal().Err(err).Msg("Connectivity check to ArgoCD failed")
}

if err := github.ConnectivityCheck(); err != nil {
log.Fatal().Err(err).Msg("Connectivity check to Github API failed")
}

server.StartWebhookProcessor("", 8080, githubWebhookSecret, devMode)
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.21.3
require (
github.com/argoproj/argo-cd/v2 v2.9.5
github.com/argoproj/gitops-engine v0.7.1-0.20230906152414-b0fffe419a0f
github.com/bradleyfalzon/ghinstallation/v2 v2.9.0
github.com/google/go-github/v58 v58.0.0
github.com/hexops/gotextdiff v1.0.3
github.com/rs/zerolog v1.31.0
Expand All @@ -25,7 +26,6 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect
github.com/bombsimon/logrusr/v2 v2.0.1 // indirect
github.com/bradleyfalzon/ghinstallation/v2 v2.6.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
Expand Down Expand Up @@ -61,6 +61,7 @@ require (
github.com/google/gnostic v0.6.9 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-github/v53 v53.2.0 // indirect
github.com/google/go-github/v57 v57.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ github.com/bombsimon/logrusr/v2 v2.0.1 h1:1VgxVNQMCvjirZIYaT9JYn6sAVGVEcNtRE0y4m
github.com/bombsimon/logrusr/v2 v2.0.1/go.mod h1:ByVAX+vHdLGAfdroiMg6q0zgq2FODY2lc5YJvzmOJio=
github.com/bradleyfalzon/ghinstallation/v2 v2.6.0 h1:IRY7Xy588KylkoycsUhFpW7cdGpy5Y5BPsz4IfuJtGk=
github.com/bradleyfalzon/ghinstallation/v2 v2.6.0/go.mod h1:oQ3etOwN3TRH4EwgW5/7MxSVMGlMlzG/O8TU7eYdoSk=
github.com/bradleyfalzon/ghinstallation/v2 v2.9.0 h1:HmxIYqnxubRYcYGRc5v3wUekmo5Wv2uX3gukmWJ0AFk=
github.com/bradleyfalzon/ghinstallation/v2 v2.9.0/go.mod h1:wmkTDJf8CmVypxE8ijIStFnKoTa6solK5QfdmJrP9KI=
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
Expand Down Expand Up @@ -227,6 +229,7 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
Expand Down Expand Up @@ -290,6 +293,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v53 v53.2.0 h1:wvz3FyF53v4BK+AsnvCmeNhf8AkTaeh2SoYu/XUvTtI=
github.com/google/go-github/v53 v53.2.0/go.mod h1:XhFRObz+m/l+UCm9b7KSIC3lT3NWSXGt7mOsAWEloao=
github.com/google/go-github/v57 v57.0.0 h1:L+Y3UPTY8ALM8x+TV0lg+IEBI+upibemtBD8Q9u7zHs=
github.com/google/go-github/v57 v57.0.0/go.mod h1:s0omdnye0hvK/ecLvpsGfJMiRt85PimQh4oygmLIxHw=
github.com/google/go-github/v58 v58.0.0 h1:Una7GGERlF/37XfkPwpzYJe0Vp4dt2k1kCjlxwjIvzw=
github.com/google/go-github/v58 v58.0.0/go.mod h1:k4hxDKEfoWpSqFlc8LTpGd9fu2KrV1YAa6Hi6FmDNY4=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
Expand Down
109 changes: 84 additions & 25 deletions internal/github/comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,119 @@ package github

import (
"context"
"errors"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"

ghinstallation "github.com/bradleyfalzon/ghinstallation/v2"
"github.com/google/go-github/v58/github"
"github.com/rs/zerolog/log"
)

var (
commentClient *github.Client
commentUser *github.User
mux *sync.RWMutex
commentClient *github.Client
appsClient *github.Client
commentClientIsApp bool
commentLogin string
mux *sync.RWMutex
)

const commentIdentifier = "<!-- comment produced by argo-diff -->"

func init() {
githubPAT := os.Getenv("GITHUB_PERSONAL_ACCESS_TOKEN")
if githubPAT == "" {
log.Error().Msg("Cannot create github client - GITHUB_PERSONAL_ACCESS_TOKEN is empty")
} else {
commentClientIsApp = false
mux = &sync.RWMutex{}
// Create Github API client
if githubPAT := os.Getenv("GITHUB_PERSONAL_ACCESS_TOKEN"); githubPAT != "" {
commentClient = github.NewClient(nil).WithAuthToken(githubPAT)
} else {
tr := http.DefaultTransport
appId, err := strconv.ParseInt(os.Getenv("GITHUB_APP_ID"), 10, 64)
if err != nil {
log.Error().Err(err).Msgf("Unable to parse %s", os.Getenv("GITHUB_APP_ID"))
return
}
installId, err := strconv.ParseInt(os.Getenv("GITHUB_INSTALLATION_ID"), 10, 64)
if err != nil {
log.Error().Err(err).Msgf("Unable to parse %s", os.Getenv("GITHUB_INSTALLATION_ID"))
return
}
privKeyFile := os.Getenv("GITHUB_PRIVATE_KEY_FILE")
atr, err := ghinstallation.NewAppsTransportKeyFromFile(tr, appId, privKeyFile)
if err != nil {
log.Error().Err(err).Msgf("Failed to create jwt transport: appId %d, privKeyFile %s", appId, privKeyFile)
return
}
itr := ghinstallation.NewFromAppsTransport(atr, installId)
commentClient = github.NewClient(&http.Client{Transport: itr})
appsClient = github.NewClient(&http.Client{Transport: atr}) // /app endpoints need separate client
commentClientIsApp = true
}
mux = &sync.RWMutex{}
}

// Populates commentUser singleton with the Github user associated with our github client
func ConnectivityCheck() error {
if commentClient == nil {
return errors.New("github client is not initialized")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
log.Info().Msg("Calling Github API for a connectivity test")
return getCommentUser(ctx)
}

// Populates commentLogin singleton with the Github user associated with our github client
func getCommentUser(ctx context.Context) error {
if commentClient == nil {
log.Error().Msg("Cannot call github API - I don't have a client set")
return fmt.Errorf("no github commenter client")
}
log.Debug().Msg("Calling Github API to determine comment user")
mux.RLock()
if commentUser != nil {
if commentLogin != "" {
mux.RUnlock()
return nil
}
mux.RUnlock()
user, resp, err := commentClient.Users.Get(ctx, "")
if resp != nil {
log.Info().Msgf("%s received when calling client.Users.Get() via go-github", resp.Status)
}
if err != nil {
log.Error().Err(err).Msg("Unable to determine get my github user")
return err
}
if user == nil {
log.Error().Msg("Empty user returned - not sure how I got here")
return nil
if commentClientIsApp {
app, resp, err := appsClient.Apps.Get(ctx, "")
if resp != nil {
log.Info().Msgf("%s received when calling client.Apps.Get() via go-github", resp.Status)
}
if err != nil {
log.Error().Err(err).Msg("Unable to determine get my github app")
return err
}
log.Trace().Msgf("Github App: %+v", app)
if app == nil || app.Name == nil {
log.Error().Msg("Empty user returned - not sure how I got here")
return fmt.Errorf("empty app info")
}
mux.Lock()
commentLogin = *app.Name + "[bot]"
mux.Unlock()
} else {
user, resp, err := commentClient.Users.Get(ctx, "")
if resp != nil {
log.Info().Msgf("%s received when calling client.Users.Get() via go-github", resp.Status)
}
if err != nil {
log.Error().Err(err).Msg("Unable to determine get my github user")
return err
}
if user == nil || user.Login == nil {
log.Error().Msg("Empty user returned - not sure how I got here")
return fmt.Errorf("empty user info")
}
mux.Lock()
commentLogin = *user.Login
mux.Unlock()
}
mux.Lock()
commentUser = user
mux.Unlock()
log.Info().Msgf("Github Comment user name: %s", commentLogin)
return nil
}

Expand Down Expand Up @@ -139,7 +198,7 @@ func getExistingComments(ctx context.Context, owner, repo string, prNum int) ([]
}
log.Debug().Msgf("Checking %d comments in %s/%s#%d", len(comments), owner, repo, prNum)
for _, c := range comments {
if *c.User.Login == *commentUser.Login && strings.Contains(*c.Body, commentIdentifier) {
if *c.User.Login == commentLogin && strings.Contains(*c.Body, commentIdentifier) {
res = append(res, c)
}
}
Expand Down
32 changes: 26 additions & 6 deletions internal/github/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package github
import (
"context"
"fmt"
"net/http"
"os"
"strconv"

"github.com/bradleyfalzon/ghinstallation"

Check failure on line 10 in internal/github/status.go

View workflow job for this annotation

GitHub Actions / test

no required module provides package github.com/bradleyfalzon/ghinstallation; to add it:

Check failure on line 10 in internal/github/status.go

View workflow job for this annotation

GitHub Actions / build

no required module provides package github.com/bradleyfalzon/ghinstallation; to add it:
"github.com/google/go-github/v58/github"
"github.com/rs/zerolog/log"
)
Expand All @@ -22,16 +25,33 @@ const StatusFailure = "failure"
const StatusError = "error"

func init() {
githubPAT := os.Getenv("GITHUB_PERSONAL_ACCESS_TOKEN")
if githubPAT == "" {
log.Error().Msg("Cannot create github client - GITHUB_PERSONAL_ACCESS_TOKEN is empty")
} else {
statusClient = github.NewClient(nil).WithAuthToken(githubPAT)
}
statusContextEnv := os.Getenv("GITHUB_STATUS_CONTEXT_STR")
if statusContextEnv != "" {
statusContextStr = statusContextEnv
}
// Create Github API client
if githubPAT := os.Getenv("GITHUB_PERSONAL_ACCESS_TOKEN"); githubPAT != "" {
statusClient = github.NewClient(nil).WithAuthToken(githubPAT)
return
}
tr := http.DefaultTransport
appId, err := strconv.ParseInt(os.Getenv("GITHUB_APP_ID"), 10, 64)
if err != nil {
log.Error().Err(err).Msgf("Unable to parse %s", os.Getenv("GITHUB_APP_ID"))
return
}
installId, err := strconv.ParseInt(os.Getenv("GITHUB_INSTALLATION_ID"), 10, 64)
if err != nil {
log.Error().Err(err).Msgf("Unable to parse %s", os.Getenv("GITHUB_INSTALLATION_ID"))
return
}
privKeyFile := os.Getenv("GITHUB_PRIVATE_KEY_FILE")
itr, err := ghinstallation.NewKeyFromFile(tr, appId, installId, privKeyFile)
if err != nil {
log.Error().Err(err).Msgf("Failed to create github client: appId %d, installId %d, privKeyFile %s", appId, installId, privKeyFile)
return
}
statusClient = github.NewClient(&http.Client{Transport: itr})
}

// Helper that sets commit status for the request commit sha
Expand Down

0 comments on commit 72bec95

Please sign in to comment.