-
Notifications
You must be signed in to change notification settings - Fork 7
Automatic hook registration #2
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clarified.
cmd/sign-off-checker/main.go
Outdated
default: | ||
log.Printf("Unhandled hook type: %v", hooktype) | ||
publicWebhookURL, _ := os.LookupEnv("PUBLIC_WEBHOOK_URL") | ||
if publicWebhookURL == "" && len(autoRegisterOrgs) > 0 { |
There was a problem hiding this comment.
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.
cmd/sign-off-checker/main.go
Outdated
|
||
func loggingMiddleware(handler http.Handler) http.Handler { | ||
func loggingMiddleware(log *log.Logger, handler http.Handler) http.Handler { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
cmd/sign-off-checker/main.go
Outdated
break | ||
} | ||
} | ||
dryRunString, _ := os.LookupEnv("DRY_RUN") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
pkg/register/hook.go
Outdated
} | ||
|
||
func addSignOffHook(gh *github.Client, org string, repo *github.Repository, url string, secret string) error { | ||
panic(nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left over debugging?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doh. Yes.
pkg/register/protection.go
Outdated
EnforceAdmins: true, | ||
RequiredStatusChecks: &github.RequiredStatusChecks{ | ||
Strict: false, | ||
Contexts: []string{}, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Signed-off-by: Matt Moyer <[email protected]>
Signed-off-by: Matt Moyer <[email protected]>
Signed-off-by: Matt Moyer <[email protected]>
Signed-off-by: Matt Moyer <[email protected]>
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]>
Signed-off-by: Matt Moyer <[email protected]>
Signed-off-by: Matt Moyer <[email protected]>
Signed-off-by: Matt Moyer <[email protected]>
Signed-off-by: Matt Moyer <[email protected]>
2ecd318
to
7e6eabf
Compare
@jbeda updated, should be ready for a fresh review when you have a minute (thanks again!). |
Signed-off-by: Matt Moyer <[email protected]>
Signed-off-by: Matt Moyer <[email protected]>
cmd/sign-off-checker/main.go
Outdated
}) | ||
} | ||
// --listen / $LISTEN | ||
rootCmd.Flags().String( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
cmd/sign-off-checker/main.go
Outdated
viper.BindEnv("listenAddress", "LISTEN") | ||
|
||
// --autoregister / $AUTOREGISTER_ORGANIZATIONS | ||
rootCmd.Flags().StringSlice( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)?
cmd/sign-off-checker/main.go
Outdated
return serveWebhook(gh) | ||
} | ||
|
||
func autoregister(gh *github.Client) { |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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.
Signed-off-by: Matt Moyer <[email protected]>
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]>
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) |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
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.