diff --git a/cmd/git-sync/main.go b/cmd/git-sync/main.go index ec815df..373234c 100644 --- a/cmd/git-sync/main.go +++ b/cmd/git-sync/main.go @@ -269,16 +269,17 @@ func main() { log.V(0).Info("starting up", "args", os.Args) // Startup webhooks goroutine - webhookTriggerChan := make(chan webhookRepoInfo, 1) + var webhook *Webhook if *flWebhookURL != "" { - webhook := Webhook{ + webhook = &Webhook{ URL: *flWebhookURL, Method: *flWebhookMethod, Success: *flWebhookStatusSuccess, Timeout: *flWebhookTimeout, Backoff: *flWebhookBackoff, + Data: NewWebhookData(), } - go webhook.run(webhookTriggerChan) + go webhook.run() } initialSync := true @@ -301,8 +302,8 @@ func main() { cancel() time.Sleep(waitTime(*flWait)) continue - } else if changed { - err := triggerWebhook(ctx, webhookTriggerChan, *flRev, *flRoot) + } else if changed && webhook != nil { + err := triggerWebhook(ctx, webhook, *flRev, *flRoot, *flDest) if err != nil { log.Error(err, "triggering webhook failed") } @@ -692,24 +693,19 @@ func setupGitCookieFile() error { return nil } -func triggerWebhook(ctx context.Context, ch chan webhookRepoInfo, rev, gitRoot string) error { - info := webhookRepoInfo{} +func triggerWebhook(ctx context.Context, webhook *Webhook, rev, gitRoot, dest string) error { + target := path.Join(gitRoot, dest) - hash, err := hashForRev(ctx, rev, gitRoot) + hash, err := hashForRev(ctx, rev, target) if err != nil { return err } - info.Hash = hash - // Trigger webhooks to be called. We do a non-blocking write to the channel as we // don't want to backup the syncing locally because we can't complete a webhook call. - // Since the channel has a buffer of 1 we ensure that it is called for a change, but - // this allows us to de-dupe calls if they happen before the webhook call completes. - select { - case ch <- info: - default: - } + // Since the channel has a buffer of 1 we ensure that it is called for a change. Only + // the event of the last git hash will be send in case the webhook is not fast enough. + webhook.Data.UpdateAndTrigger(hash) return nil } diff --git a/cmd/git-sync/webhook.go b/cmd/git-sync/webhook.go index 516be14..35ce964 100644 --- a/cmd/git-sync/webhook.go +++ b/cmd/git-sync/webhook.go @@ -20,15 +20,68 @@ type Webhook struct { Timeout time.Duration // Backoff for failed webhook calls Backoff time.Duration + + Data *webhookData } -type webhookRepoInfo struct { - Hash string +type webhookData struct { + ch chan struct{} + + newHash string + curHash string } -func (w *Webhook) Do(info webhookRepoInfo) error { +func NewWebhookData() *webhookData { + return &webhookData{ + ch: make(chan struct{}, 1), + } +} + +func (d *webhookData) UpdateAndTrigger(newHash string) { + d.newHash = newHash + + select { + case d.ch <- struct{}{}: + default: + } +} + +func (d *webhookData) updateState() bool { + newHash := d.newHash + if newHash != d.curHash { + d.curHash = newHash + return true + } + return false +} + +func (d *webhookData) Hash() string { + d.updateState() + return d.curHash +} + +func (d *webhookData) Wait() bool { + // wait for message from UpdateAndTrigger + <-d.ch + + changed := d.updateState() + + return changed +} + +func (d *webhookData) WaitForChange() { + for { + changed := d.Wait() + + if changed { + return + } + } +} + +func (w *Webhook) Do(hash string) error { req, err := http.NewRequest(w.Method, w.URL, nil) - req.Header.Set("Gitsync-Hash", info.Hash) + req.Header.Set("Gitsync-Hash", hash) if err != nil { return err } @@ -52,13 +105,14 @@ func (w *Webhook) Do(info webhookRepoInfo) error { } // Wait for trigger events from the channel, and send webhooks when triggered -func (w *Webhook) run(ch chan webhookRepoInfo) { +func (w *Webhook) run() { for { - // Wait for trigger - info := <-ch + // Wait for trigger and changed hash value + w.Data.WaitForChange() for { - if err := w.Do(info); err != nil { + hash := w.Data.Hash() + if err := w.Do(hash); err != nil { log.Error(err, "error calling webhook", "url", w.URL) time.Sleep(w.Backoff) } else { diff --git a/cmd/git-sync/webhook_test.go b/cmd/git-sync/webhook_test.go new file mode 100644 index 0000000..c9e21e1 --- /dev/null +++ b/cmd/git-sync/webhook_test.go @@ -0,0 +1,61 @@ +package main + +import ( + "testing" +) + +const ( + hash1 = "1111111111111111111111111111111111111111" + hash2 = "2222222222222222222222222222222222222222" + hash3 = "3333333333333333333333333333333333333333" +) + +func TestWebhookData(t *testing.T) { + t.Run("webhhook consumes first hash value", func(t *testing.T) { + whd := NewWebhookData() + + whd.UpdateAndTrigger(hash1) + whd.WaitForChange() + + hash := whd.Hash() + if hash1 != hash { + t.Fatalf("expected hash %s but got %s", hash1, hash) + } + }) + + t.Run("second update wins", func(t *testing.T) { + whd := NewWebhookData() + + whd.UpdateAndTrigger(hash1) + whd.UpdateAndTrigger(hash2) + whd.WaitForChange() + + hash := whd.Hash() + if hash2 != hash { + t.Fatalf("expected hash %s but got %s", hash2, hash) + } + }) + + t.Run("same hash value does not lead to an update", func(t *testing.T) { + whd := NewWebhookData() + + whd.UpdateAndTrigger(hash1) + whd.WaitForChange() + hash := whd.Hash() + if hash1 != hash { + t.Fatalf("expected hash %s but got %s", hash1, hash) + } + + whd.UpdateAndTrigger(hash1) + changed := whd.Wait() + + if changed { + t.Fatalf("no change expected") + } + + hash = whd.Hash() + if hash1 != hash { + t.Fatalf("expected hash %s but got %s", hash1, hash) + } + }) +}