Skip to content

Commit

Permalink
add support for github app auth (#288)
Browse files Browse the repository at this point in the history
* add github app support

* add github app support

* add github app support

* add github app support

* add missing docs

* rebase

Signed-off-by: drfaust92 <[email protected]>

* refactor client creation

* avoid setting email/username for app purposes

* gracefully handle a failure to post a message

Signed-off-by: Joe Lombrozo <[email protected]>

* only push images on zapier/kubechecks

* remove extra step

---------

Signed-off-by: drfaust92 <[email protected]>
Signed-off-by: Joe Lombrozo <[email protected]>
Co-authored-by: drfaust92 <[email protected]>
  • Loading branch information
djeebus and DrFaust92 authored Nov 13, 2024
1 parent 4e317be commit c08c70b
Show file tree
Hide file tree
Showing 10 changed files with 94 additions and 44 deletions.
2 changes: 1 addition & 1 deletion .github/actions/build-image/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ runs:

- name: Build and push the Docker image
shell: bash
run: >-
run: >-
./earthly.sh
${{ inputs.push == 'true' && '--push' || '' }}
+docker-multiarch
Expand Down
12 changes: 10 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func init() {
zerolog.LevelDebugValue,
zerolog.LevelTraceValue,
).
withDefault("info").
withDefault(zerolog.LevelInfoValue).
withShortHand("l"),
)
boolFlag(flags, "persist-log-level", "Persists the set log level down to other module loggers.")
Expand All @@ -56,6 +56,11 @@ func init() {
withChoices("github", "gitlab").
withDefault("gitlab"))
stringFlag(flags, "vcs-token", "VCS API token.")
stringFlag(flags, "vcs-username", "VCS Username.")
stringFlag(flags, "vcs-email", "VCS Email.")
stringFlag(flags, "github-private-key", "Github App Private Key.")
int64Flag(flags, "github-app-id", "Github App ID.")
int64Flag(flags, "github-installation-id", "Github Installation ID.")
stringFlag(flags, "argocd-api-token", "ArgoCD API token.")
stringFlag(flags, "argocd-api-server-addr", "ArgoCD API Server Address.",
newStringOpts().
Expand Down Expand Up @@ -121,7 +126,10 @@ func setupLogOutput() {

// Default level is info, unless debug flag is present
levelFlag := viper.GetString("log-level")
level, _ := zerolog.ParseLevel(levelFlag)
level, err := zerolog.ParseLevel(levelFlag)
if err != nil {
log.Error().Err(err).Msg("Invalid log level")
}

zerolog.SetGlobalLevel(level)
log.Debug().Msg("Debug level logging enabled.")
Expand Down
5 changes: 5 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ The full list of supported environment variables is described below:
|`KUBECHECKS_ENABLE_PREUPGRADE`|Enable preupgrade checks.|`true`|
|`KUBECHECKS_ENSURE_WEBHOOKS`|Ensure that webhooks are created in repositories referenced by argo.|`false`|
|`KUBECHECKS_FALLBACK_K8S_VERSION`|Fallback target Kubernetes version for schema / upgrade checks.|`1.23.0`|
|`KUBECHECKS_GITHUB_APP_ID`|Github App ID.|`0`|
|`KUBECHECKS_GITHUB_INSTALLATION_ID`|Github Installation ID.|`0`|
|`KUBECHECKS_GITHUB_PRIVATE_KEY`|Github App Private Key.||
|`KUBECHECKS_KUBERNETES_CLUSTERID`|Kubernetes Cluster ID, must be specified if kubernetes-type is eks.||
|`KUBECHECKS_KUBERNETES_CONFIG`|Path to your kubernetes config file, used to monitor applications.||
|`KUBECHECKS_KUBERNETES_TYPE`|Kubernetes Type One of eks, or local.|`local`|
Expand All @@ -66,9 +69,11 @@ The full list of supported environment variables is described below:
|`KUBECHECKS_SHOW_DEBUG_INFO`|Set to true to print debug info to the footer of MR comments.|`false`|
|`KUBECHECKS_TIDY_OUTDATED_COMMENTS_MODE`|Sets the mode to use when tidying outdated comments. One of hide, delete.|`hide`|
|`KUBECHECKS_VCS_BASE_URL`|VCS base url, useful if self hosting gitlab, enterprise github, etc.||
|`KUBECHECKS_VCS_EMAIL`|VCS Email.||
|`KUBECHECKS_VCS_TOKEN`|VCS API token.||
|`KUBECHECKS_VCS_TYPE`|VCS type. One of gitlab or github.|`gitlab`|
|`KUBECHECKS_VCS_UPLOAD_URL`|VCS upload url, required for enterprise github.||
|`KUBECHECKS_VCS_USERNAME`|VCS Username.||
|`KUBECHECKS_WEBHOOK_SECRET`|Optional secret key for validating the source of incoming webhooks.||
|`KUBECHECKS_WEBHOOK_URL_BASE`|The endpoint to listen on for incoming PR/MR event webhooks. For example, 'https://checker.mycompany.com'.||
|`KUBECHECKS_WEBHOOK_URL_PREFIX`|If your application is running behind a proxy that uses path based routing, set this value to match the path prefix. For example, '/hello/world'.||
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/eks v1.46.0
github.com/aws/aws-sdk-go-v2/service/sts v1.30.1
github.com/aws/smithy-go v1.20.3
github.com/bradleyfalzon/ghinstallation/v2 v2.6.0
github.com/cenkalti/backoff/v4 v4.3.0
github.com/chainguard-dev/git-urls v1.0.2
github.com/creasty/defaults v1.7.0
Expand Down Expand Up @@ -105,7 +106,6 @@ require (
github.com/blang/semver/v4 v4.0.0 // 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/bufbuild/protocompile v0.6.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
Expand Down
7 changes: 7 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,18 @@ type ServerConfig struct {
OtelCollectorPort string `mapstructure:"otel-collector-port"`

// vcs
VcsUsername string `mapstructure:"vcs-username"`
VcsEmail string `mapstructure:"vcs-email"`
VcsBaseUrl string `mapstructure:"vcs-base-url"`
VcsUploadUrl string `mapstructure:"vcs-upload-url"` // github enterprise upload URL
VcsToken string `mapstructure:"vcs-token"`
VcsType string `mapstructure:"vcs-type"`

//github
GithubPrivateKey string `mapstructure:"github-private-key"`
GithubAppID int64 `mapstructure:"github-app-id"`
GithubInstallationID int64 `mapstructure:"github-installation-id"`

// webhooks
EnsureWebhooks bool `mapstructure:"ensure-webhooks"`
WebhookSecret string `mapstructure:"webhook-secret"`
Expand Down
15 changes: 10 additions & 5 deletions pkg/events/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ func (ce *CheckEvent) getRepo(ctx context.Context, vcsClient hasUsername, cloneU
ce.clonedRepos[reposKey] = repo

// if we cloned 'HEAD', figure out its original branch and store a copy of the repo there
if branchName == "" || branchName == "HEAD" {
if branchName == "HEAD" {
remoteHeadBranchName, err := repo.GetRemoteHead()
if err != nil {
return repo, errors.Wrap(err, "failed to determine remote head")
Expand Down Expand Up @@ -260,12 +260,17 @@ func (ce *CheckEvent) Process(ctx context.Context) error {

if len(ce.affectedItems.Applications) <= 0 && len(ce.affectedItems.ApplicationSets) <= 0 {
ce.logger.Info().Msg("No affected apps or appsets, skipping")
ce.ctr.VcsClient.PostMessage(ctx, ce.pullRequest, "No changes")
if _, err := ce.ctr.VcsClient.PostMessage(ctx, ce.pullRequest, "No changes"); err != nil {
return errors.Wrap(err, "failed to post changes")
}
return nil
}

// We make one comment per run, containing output for all the apps
ce.vcsNote = ce.createNote(ctx)
ce.vcsNote, err = ce.createNote(ctx)
if err != nil {
return errors.Wrap(err, "failed to create note")
}

for num := 0; num <= ce.ctr.Config.MaxConcurrenctChecks; num++ {

Expand Down Expand Up @@ -375,8 +380,8 @@ Check kubechecks application logs for more information.
`
)

// Creates a generic Note struct that we can write into across all worker threads
func (ce *CheckEvent) createNote(ctx context.Context) *msg.Message {
// createNote creates a generic Note struct that we can write into across all worker threads
func (ce *CheckEvent) createNote(ctx context.Context) (*msg.Message, error) {
ctx, span := otel.Tracer("check").Start(ctx, "createNote")
defer span.End()

Expand Down
77 changes: 50 additions & 27 deletions pkg/vcs/github_client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import (
"strconv"
"strings"

"github.com/chainguard-dev/git-urls"
"github.com/bradleyfalzon/ghinstallation/v2"
giturls "github.com/chainguard-dev/git-urls"
"github.com/google/go-github/v62/github"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
Expand Down Expand Up @@ -48,37 +49,26 @@ func CreateGithubClient(cfg config.ServerConfig) (*Client, error) {
shurcoolClient *githubv4.Client
)

// Initialize the GitLab client with access token
t := cfg.VcsToken
if t == "" {
log.Fatal().Msg("github token needs to be set")
}
log.Debug().Msgf("Token Length - %d", len(t))
ctx := context.Background()
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: t},
)
tc := oauth2.NewClient(ctx, ts)

githubClient, err := createHttpClient(ctx, cfg)
if err != nil {
return nil, errors.Wrap(err, "failed to create github http client")
}

githubUrl := cfg.VcsBaseUrl
githubUploadUrl := cfg.VcsUploadUrl
// we need both urls to be set for github enterprise
if githubUrl == "" || githubUploadUrl == "" {
googleClient = github.NewClient(tc) // If this has failed, we'll catch it on first call
googleClient = github.NewClient(githubClient) // If this has failed, we'll catch it on first call

// Use the client from shurcooL's githubv4 library for queries.
shurcoolClient = githubv4.NewClient(tc)
shurcoolClient = githubv4.NewClient(githubClient)
} else {
googleClient, err = github.NewClient(tc).WithEnterpriseURLs(githubUrl, githubUploadUrl)
googleClient, err = github.NewClient(githubClient).WithEnterpriseURLs(githubUrl, githubUploadUrl)
if err != nil {
log.Fatal().Err(err).Msg("failed to create github enterprise client")
}
shurcoolClient = githubv4.NewEnterpriseClient(githubUrl, tc)
}

user, _, err := googleClient.Users.Get(ctx, "")
if err != nil {
return nil, errors.Wrap(err, "failed to get user")
shurcoolClient = githubv4.NewEnterpriseClient(githubUrl, githubClient)
}

client := &Client{
Expand All @@ -89,13 +79,20 @@ func CreateGithubClient(cfg config.ServerConfig) (*Client, error) {
Issues: IssuesService{googleClient.Issues},
},
shurcoolClient: shurcoolClient,
username: cfg.VcsUsername,
email: cfg.VcsEmail,
}
if user != nil {
if user.Login != nil {
client.username = *user.Login
}
if user.Email != nil {
client.email = *user.Email

if client.username == "" || client.email == "" {
user, _, err := googleClient.Users.Get(ctx, "")
if err == nil {
if user.Login != nil {
client.username = *user.Login
}

if user.Email != nil {
client.email = *user.Email
}
}
}

Expand All @@ -108,6 +105,32 @@ func CreateGithubClient(cfg config.ServerConfig) (*Client, error) {
return client, nil
}

func createHttpClient(ctx context.Context, cfg config.ServerConfig) (*http.Client, error) {
// Initialize the GitHub client with app key if provided
if cfg.GithubAppID != 0 && cfg.GithubInstallationID != 0 && cfg.GithubPrivateKey != "" {
appTransport, err := ghinstallation.New(
http.DefaultTransport, cfg.GithubAppID, cfg.GithubInstallationID, []byte(cfg.GithubPrivateKey),
)
if err != nil {
return nil, errors.Wrap(err, "failed to create github app transport")
}

return &http.Client{Transport: appTransport}, nil
}

// Initialize the GitHub client with access token if app key is not provided
vcsToken := cfg.VcsToken
if vcsToken != "" {
log.Debug().Msgf("Token Length - %d", len(vcsToken))
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: vcsToken},
)
return oauth2.NewClient(ctx, ts), nil
}

return nil, errors.New("Either GitHub token or GitHub App credentials (App ID, Installation ID, Private Key) must be set")
}

func (c *Client) Username() string { return c.username }
func (c *Client) Email() string { return c.email }
func (c *Client) GetName() string {
Expand Down
7 changes: 4 additions & 3 deletions pkg/vcs/github_client/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"

"github.com/google/go-github/v62/github"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/shurcooL/githubv4"

Expand All @@ -17,7 +18,7 @@ import (

const MaxCommentLength = 64 * 1024

func (c *Client) PostMessage(ctx context.Context, pr vcs.PullRequest, message string) *msg.Message {
func (c *Client) PostMessage(ctx context.Context, pr vcs.PullRequest, message string) (*msg.Message, error) {
_, span := tracer.Start(ctx, "PostMessageToMergeRequest")
defer span.End()

Expand All @@ -37,10 +38,10 @@ func (c *Client) PostMessage(ctx context.Context, pr vcs.PullRequest, message st

if err != nil {
telemetry.SetError(span, err, "Create Pull Request comment")
log.Error().Err(err).Msg("could not post message to PR")
return nil, errors.Wrap(err, "could not post message to PR")
}

return msg.NewMessage(pr.FullName, pr.CheckID, int(*comment.ID), c)
return msg.NewMessage(pr.FullName, pr.CheckID, int(*comment.ID), c), nil
}

func (c *Client) UpdateMessage(ctx context.Context, m *msg.Message, msg string) error {
Expand Down
9 changes: 5 additions & 4 deletions pkg/vcs/gitlab_client/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"strings"

"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/xanzy/go-gitlab"

Expand All @@ -16,8 +17,8 @@ import (

const MaxCommentLength = 1_000_000

func (c *Client) PostMessage(ctx context.Context, pr vcs.PullRequest, message string) *msg.Message {
_, span := tracer.Start(ctx, "PostMessageToMergeRequest")
func (c *Client) PostMessage(ctx context.Context, pr vcs.PullRequest, message string) (*msg.Message, error) {
_, span := tracer.Start(ctx, "PostMessage")
defer span.End()

if len(message) > MaxCommentLength {
Expand All @@ -32,10 +33,10 @@ func (c *Client) PostMessage(ctx context.Context, pr vcs.PullRequest, message st
})
if err != nil {
telemetry.SetError(span, err, "Create Merge Request Note")
log.Error().Err(err).Msg("could not post message to MR")
return nil, errors.Wrap(err, "could not post message to MR")
}

return msg.NewMessage(pr.FullName, pr.CheckID, n.ID, c)
return msg.NewMessage(pr.FullName, pr.CheckID, n.ID, c), nil
}

func (c *Client) hideOutdatedMessages(ctx context.Context, projectName string, mergeRequestID int, notes []*gitlab.Note) error {
Expand Down
2 changes: 1 addition & 1 deletion pkg/vcs/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type WebHookConfig struct {
// Client represents a VCS client
type Client interface {
// PostMessage takes in project name in form "owner/repo" (ie zapier/kubechecks), the PR/MR id, and the actual message
PostMessage(context.Context, PullRequest, string) *msg.Message
PostMessage(context.Context, PullRequest, string) (*msg.Message, error)
// UpdateMessage update a message with new content
UpdateMessage(context.Context, *msg.Message, string) error
// VerifyHook validates a webhook secret and return the body; must be called even if no secret
Expand Down

0 comments on commit c08c70b

Please sign in to comment.