Skip to content
This repository has been archived by the owner on Mar 2, 2022. It is now read-only.

Automatic hook registration #2

Open
wants to merge 15 commits into
base: master
Choose a base branch
from

Conversation

mattmoyer
Copy link

@mattmoyer mattmoyer commented Oct 6, 2017

I did a little bit of cleanup, but the meat of this is in 185d301 which sets up a background thread that automatically sets up webhooks and branch protection for all DCO repositories in our organizations.

The new README section explains how it works.

@mattmoyer mattmoyer requested review from jbeda and chuckha October 6, 2017 19:42
Copy link
Contributor

@jbeda jbeda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good. Mostly nits.

Would be good to have a TODO or issues filed for a few things. One is graceful shutdown. Another is making sure we have timeouts on everything so that we don't hang around for everything. Getting rid of all of the context.TODO() would get us there.

README.md Outdated

## Running
There are two environment variables that need to be set when running:

* `SHARED_SECRET`: Set this to a random value that you supply as the "secret" when configuring the webhook.
* `GITHUB_TOKEN`: Set this to an personal access token for a github user that has access to the repo in question. The webhook doesn't include details of the commits so we have to fetch them. Unforutnately this requires full read/write `repo` access scope even though we are just reading. Create one of these at https://github.com/settings/tokens.
* `GITHUB_TOKEN`: Set this to a personal access token for a github user that has access to the repo in question. The webhook doesn't include details of the commits so we have to fetch them. Unfortunately this requires full read/write `repo` access scope even though we are just reading. Create one of these at https://github.com/settings/tokens.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "just reading" comment is no longer accurate as we are writing hooks, right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarified.

default:
log.Printf("Unhandled hook type: %v", hooktype)
publicWebhookURL, _ := os.LookupEnv("PUBLIC_WEBHOOK_URL")
if publicWebhookURL == "" && len(autoRegisterOrgs) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After these checks pass might be good to output the config stating that autoregistration is active and the list of orgs and the webhook.


func loggingMiddleware(handler http.Handler) http.Handler {
func loggingMiddleware(log *log.Logger, handler http.Handler) http.Handler {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NB: At some point ~soon we'll want to standardize on the error reporting and logging stuff that @ncdc has been working on. Probably doable after this though.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, I'll do a followup PR to swap this over to logrus and add in graceful shutdown and liveness checks using the other libraries I've been working on.

break
}
}
dryRunString, _ := os.LookupEnv("DRY_RUN")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not documented in README. Also makes me think that we should start moving to command line flags for this stuff. The only reason other params were env variables is that it is bad form to put secrets on the command line.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I'll pull in spf13/cobra + spf13/viper and update the command to use that.

}
for _, hook := range hooks {
// if the hook with our expected URL already exists, we're done
if hook.Config["url"] == url {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm -- the secret may be wrong. But I'm guessing that this doesn't return the secret. At some point we may want to have a one shot mode to force re-registration in the event we rotate the secret. That might be a good roadmap/TODO thing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you can't really validate that the secret matches. You just have to overwrite it if you want to make sure it's correct. I'll file an issue for the force resync command.

}

func addSignOffHook(gh *github.Client, org string, repo *github.Repository, url string, secret string) error {
panic(nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left over debugging?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh. Yes.

EnforceAdmins: true,
RequiredStatusChecks: &github.RequiredStatusChecks{
Strict: false,
Contexts: []string{},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if is an early exit so we don't fall through to the code below where we automatically add the context. It would get done on the next run probably but would be good to get it in one shot.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I refactored this a bit to split apart hasBranchProtection and addBranchProtection and this got dropped. I fixed it so it converges in a single step.

return nil, fmt.Errorf("Error getting repositories for organization %q: %v", org, err)
}
result = append(result, repos...)
if resp.NextPage == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NB: the paging stuff is a total pain. Would be nice for the library to abstract that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What might be really nice is something like client-go SharedInformer that could update repository state based on webhook events but also periodically resync.

Adds a background thread that automatically configures webhooks and branch protection settings for all DCO repositories in a whitelist of organizations.

Signed-off-by: Matt Moyer <[email protected]>
Signed-off-by: Matt Moyer <[email protected]>
Signed-off-by: Matt Moyer <[email protected]>
@mattmoyer
Copy link
Author

@jbeda updated, should be ready for a fresh review when you have a minute (thanks again!).

})
}
// --listen / $LISTEN
rootCmd.Flags().String(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC this returns the flag, so you might be able to pass that to the BindPFlag call on the next line.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It returns a *string that holds the flag's value and I can't see an easy way to define the flag and bind it into viper in a single shot. I am not a fan of the stringly typed interfaces here.

I thought about defining some constants for the viper config keys like const sharedSecretKey = "sharedSecret" so at least I've got a shot at catching the bug if I typo it to "shareSecret" somewhere.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a shot at this in 481c419.

viper.BindEnv("listenAddress", "LISTEN")

// --autoregister / $AUTOREGISTER_ORGANIZATIONS
rootCmd.Flags().StringSlice(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never liked how you have to repeat the flag to get multiple values... For Ark we created our own array/slice flag type that supports comma-separated values. Not sure how much that matters here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not documented well but Cobra also supports comma separated style. This also works with --autoregister Org1,Org2,Org3

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it didn't the last time we tried it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like only StringSlice splits on commas and StringArray doesn't (spf13/pflag#90)?

return serveWebhook(gh)
}

func autoregister(gh *github.Client) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to be able to stop this (stop channel or context)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hoping to add graceful shutdown (using this code: https://github.com/heptio/shutdown/pull/1) in a followup PR.

This should make it a little harder to typo the viper key values and end up with nil somewhere we shouldn't.

Signed-off-by: Matt Moyer <[email protected]>
@mattmoyer mattmoyer removed the request for review from chuckha October 10, 2017 20:24
func hasSignOffHook(gh *github.Client, org string, repo *github.Repository, url string) (bool, error) {
opt := &github.ListOptions{PerPage: 10}
for {
hooks, resp, err := gh.Repositories.ListHooks(context.TODO(), org, repo.GetName(), opt)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should use context.Background() here, context.TODO() blows up when it's used ... which tells me that it isn't actually used (insert rant about context here)

// 404 just means there are no hooks for this repo
return false, nil
}
if err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please handle the error first, then the other values after that.

if err != nil {
return fmt.Errorf("Error registering webhook: %v", err)
}
return err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return nil?


func hasBranchProtection(gh *github.Client, org string, repo *github.Repository) (bool, error) {
contexts, resp, err := gh.Repositories.ListRequiredStatusChecksContexts(
context.TODO(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/TODO()/Background here and everywhere else

// The GH API forces us to get+modify+set which means this could race with other
// concurrent modifications.
func addBranchProtection(gh *github.Client, org string, repo *github.Repository) error {
existing, resp, err := gh.Repositories.GetBranchProtection(context.TODO(), org, repo.GetName(), repo.GetDefaultBranch())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, reading the code for context TODO and Background() do the same thing internally ... and the promises of sufficiently advanced static analysis probably don't apply outside google3. Still, better to use context.Background()

// concurrent modifications.
func addBranchProtection(gh *github.Client, org string, repo *github.Repository) error {
existing, resp, err := gh.Repositories.GetBranchProtection(context.TODO(), org, repo.GetName(), repo.GetDefaultBranch())
if err != nil && resp != nil && resp.StatusCode != 404 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if err was not nil, then you should probably not assume anything about the state of resp, it's not safe to call.

// concurrent modifications.
func addBranchProtection(gh *github.Client, org string, repo *github.Repository) error {
existing, resp, err := gh.Repositories.GetBranchProtection(context.TODO(), org, repo.GetName(), repo.GetDefaultBranch())
if err != nil && resp != nil && resp.StatusCode != 404 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is resp a http.Response object? Does resp.Body need to be closed here and everywhere?


// Package register implements automatic registration of webhooks and branch
// protections on all DCO repositories in a set of organizations.
package register
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: a package's name should be a one word description of what it provides. webhooks might be a better name than register, the verb.

// Register walks the provided organization, finds repositories that use the
// Developer Certificate of Origin (in CONTRIBUTING.md), and registers the
// sign-off-checker webhook and required commit statuses in each repository.
func Register(log *log.Logger, gh *github.Client, dryRun bool, organizations []string, webhookURL string, webhookSecret string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: RegisterWebhooks might be a better name.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants