强制 Gitlab 在 Go 失败时重试 webhooks

问题描述 投票:0回答:2

我想监视 Gitlab 项目中的每个事件并将它们存储在外部服务中。为此,我使用 Gitlab Webhooks。我在 Go 中创建了一个小型本地 HTTP 服务器,用于侦听 Gitlab 的 POST 并将它们转发到外部服务。 Hooks 包含我需要的所有信息,所以看起来这个架构很好:

Gitlab > HTTPServer > External Service.

我的问题是当外部服务关闭时,我无法让 Gitlab 重试失败的请求。正如文档所说:

  • GitLab 忽略端点返回的 HTTP 状态代码。
  • 您的端点应始终返回有效的 HTTP 响应。如果你不这样做,那么 GitLab 会认为挂钩失败并重试。

令人惊讶的是,Gitlab 没有合适的方法来请求 webhook 重试。我必须明确返回无效的 http 响应。此外,我找不到 API 端点来列出所有失败的 webhook 并要求重新发送。

问题:如何使用标准“net/http”库显式返回无效的 HTTP 响应,以强制 Gitlab 重试 Webhooks?

http go gitlab webhooks
2个回答
1
投票

正如评论中所写,webhook 只是一个事件发生的通知,并且可能会发送一些数据,通常是 JSON 数据。

您有责任保留事件本身以及您想要/需要处理的随事件一起发送的数据。您将在下面找到一个带注释的示例。请注意,这不包括增量退避,但应该很容易添加:

package main

import (
    "encoding/json"
    "flag"
    "io"
    "log"
    "net/http"
    "os"
    "path/filepath"

    "github.com/joncrlsn/dque"
)

var (
    bind        string
    queueDir    string
    segmentSize int
)

// You might want to add request headers and stuff
type webhookContent struct {
    Foo string
    Bar int
}

func init() {
    flag.StringVar(&bind, "bind", ":8080", "The address to bind to")
    flag.StringVar(&queueDir, "path", "./queue", "path to store the queue in")
    flag.IntVar(&segmentSize, "size", 50, "number of entries for the queue")
}

// The "webserver" component
func runserver(q *dque.DQue) {

    http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
        // A new decoder for each call, as we want to have a new LimitReader
        // for each call. This is a simple, albeit a bit crude method to prevent
        // accidental or malicious overload of your server.
        dec := json.NewDecoder(io.LimitReader(r.Body, 4096))

        defer r.Body.Close()

        c := &webhookContent{}
        if err := dec.Decode(c); err != nil {
            log.Printf("reading body: %s", err)
            http.Error(w, "internal error", http.StatusInternalServerError)
            return
        }

        // When the content is successfully decoded, we can persist it into
        // our queue.
        if err := q.Enqueue(c); err != nil {
            log.Printf("enqueueing webhook data: %s", err)
            // PROPER ERROR HANDLING IS MISSING HERE
        }
    })

    http.ListenAndServe(bind, nil)
}

func main() {
    flag.Parse()

    var (
        q   *dque.DQue
        err error
    )

    if !dirExists(queueDir) {
        if err = os.MkdirAll(queueDir, 0750); err != nil {
            log.Fatalf("creating queue dir: %s", err)
        }
    }

    if !dirExists(filepath.Join(queueDir, "webhooks")) {
        q, err = dque.New("webhooks", queueDir, segmentSize, func() interface{} { return &webhookContent{} })
    } else {
        q, err = dque.Open("webhooks", queueDir, segmentSize, func() interface{} { return &webhookContent{} })
    }

    if err != nil {
        log.Fatalf("setting up queue: %s", err)
    }

    defer q.Close()

    go runserver(q)

    var (
        // Placeholder during event loop
        i interface{}
        // Payload
        w *webhookContent
        // Did the type assertion succeed
        ok bool
    )

    for {
        // We peek only. The semantic of this is that
        // you can already access the next item in the queue
        // without removing it from the queue and "mark" it as read.
        // We use PeekBlock since we want to wait for an item in the
        // queue to be available.
        if i, err = q.PeekBlock(); err != nil {
            // If we can not peek, something is SERIOUSLY wrong.
            log.Fatalf("reading from queue: %s", err)
        }

        if w, ok = i.(*webhookContent); !ok {
            // If the type assertion fails, something is seriously wrong, too.
            log.Fatalf("reading from queue: %s", err)
        }

        if err = doSomethingUseful(w); err != nil {
            log.Printf("Something went wrong: %s", err)
            log.Println("I strongly suggest entering an incremental backoff!")
            continue
        }

        // We did something useful, so we can dequeue the item we just processed from the queue.
        q.Dequeue()
    }

}

func doSomethingUseful(w *webhookContent) error {
    log.Printf("Instead of this log message, you can do something useful with: %#v", w)
    return nil
}

func dirExists(path string) bool {
    fileInfo, err := os.Stat(path)
    if err == nil {
        return fileInfo.IsDir()
    }
    return false
}

现在当你做这样的事情时:

$ curl -X POST --data '{"Foo":"Baz","Bar":42}' http://localhost:8080/webhook

你应该得到一个像

这样的日志条目
2020/04/18 11:34:23 Instead of this log message, you can do something useful with: &main.webhookContent{Foo:"Baz", Bar:42}

1
投票

请注意,参见GitLab 15.7(2022 年 12 月)实施了一个相反 方法:

自动禁用失败的webhooks

为了保护 GitLab 和整个系统的用户免受任何潜在的滥用或误用,我们实施了一项功能来禁用持续失败的 webhooks。

  • 5xx
    范围内返回响应代码的 Webhook 被理解为间歇性失败并暂时禁用。这些 webhook 最初被禁用 1 分钟,每次重试时最多可延长 24 小时。
  • 4xx
    错误而失败的 Webhook 将被永久禁用。

应用程序会提醒所有项目所有者和维护者调查并重新启用任何失败的 webhooks。

此功能现在可在 GitLab.com 和自我管理的实例上使用,同时功能增强包括处理冷启动

See EpicDocumentation.

因此,从 GitLab 15.7+ 开始,不仅发回“无效的 HTTP 响应”不起作用,还会导致禁用 webhook。


这已通过 GitLab 15.10(2023 年 3 月)进行了改进

自动禁用失败的组 webhooks

为了保护 GitLab 和整个系统的用户免受任何潜在的滥用或误用,我们实施了一项功能来禁用持续失败的组 webhooks。

  • Group webhooks 返回
    5xx
    范围内的响应代码被理解为间歇性失败并暂时禁用。这些 webhook 最初被禁用 1 分钟,每次重试时最多可延长 24 小时。
  • 4xx
    错误而失败的组 webhook 将被永久禁用。

具有所有者或维护者角色的用户会在应用程序中收到提醒,调查并重新启用任何失败的组 webhooks。

默认情况下,此功能在 GitLab.com 上启用,在自我管理的 GitLab 上禁用。
要为项目或组 webhook 启用自动禁用失败的 webhook,自我管理实例的管理员必须启用

auto_disabling_web_hooks
功能标志

参见文档问题

© www.soinside.com 2019 - 2024. All rights reserved.