From cc338cd3acd7ddfb68ec627c3a8f63218ccc8f44 Mon Sep 17 00:00:00 2001 From: "jason.liao" Date: Mon, 11 May 2026 17:09:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0config=20=E8=83=BD?= =?UTF-8?q?=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 命令行参数优先级高于配置文件(配置文件先加载,flag.Parse() 后执行会覆盖) --- config.go | 118 +++++++++++++++++++++++++++++++ webhook.go | 9 +++ webhook.yaml.example | 160 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 287 insertions(+) create mode 100644 config.go create mode 100644 webhook.yaml.example diff --git a/config.go b/config.go new file mode 100644 index 0000000..cf78a05 --- /dev/null +++ b/config.go @@ -0,0 +1,118 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "gopkg.in/yaml.v2" +) + +// configFile holds the path to the YAML configuration file. +// It is extracted from os.Args before flag.Parse() runs. +var configFile string + +// loadConfigFile reads a YAML configuration file and sets the corresponding +// flags. Flags set via the command line take precedence over the config file +// because this function is called before flag.Parse(). +// +// Supported YAML structure: +// +// ip: 0.0.0.0 +// port: 9000 +// nopanic: true +// hooks: +// - /etc/webhook/hooks.yaml +// header: +// - "X-Custom=value" +func loadConfigFile(path string) error { + data, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read config file %q: %w", path, err) + } + + // Use a generic map so we can handle both scalar and list values. + var cfg map[string]interface{} + if err := yaml.Unmarshal(data, &cfg); err != nil { + return fmt.Errorf("failed to parse config file %q: %w", path, err) + } + + for key, val := range cfg { + f := flag.CommandLine.Lookup(key) + if f == nil { + return fmt.Errorf("unknown option %q in config file %q", key, path) + } + + switch v := val.(type) { + case []interface{}: + // List values: call flag.Set for each element (supports -hooks, -header, etc.) + for _, item := range v { + s := fmt.Sprintf("%v", item) + if err := flag.CommandLine.Set(key, s); err != nil { + return fmt.Errorf("error setting flag %q to %q: %w", key, s, err) + } + } + case bool: + s := "false" + if v { + s = "true" + } + if err := flag.CommandLine.Set(key, s); err != nil { + return fmt.Errorf("error setting flag %q to %q: %w", key, s, err) + } + default: + s := fmt.Sprintf("%v", v) + if err := flag.CommandLine.Set(key, s); err != nil { + return fmt.Errorf("error setting flag %q to %q: %w", key, s, err) + } + } + } + + return nil +} + +// extractConfigFlag scans os.Args for -config / -c / --config and returns the +// path value. It also removes those arguments from os.Args so that flag.Parse() +// does not see them (since -config is not a registered flag). +func extractConfigFlag() string { + var path string + var newArgs []string + + args := os.Args[1:] // skip program name + newArgs = append(newArgs, os.Args[0]) + + for i := 0; i < len(args); i++ { + arg := args[i] + + var matched bool + var value string + + switch { + case arg == "-c" || arg == "-config" || arg == "--config": + matched = true + if i+1 < len(args) { + i++ + value = args[i] + } + case strings.HasPrefix(arg, "-c="): + matched = true + value = strings.TrimPrefix(arg, "-c=") + case strings.HasPrefix(arg, "-config="): + matched = true + value = strings.TrimPrefix(arg, "-config=") + case strings.HasPrefix(arg, "--config="): + matched = true + value = strings.TrimPrefix(arg, "--config=") + } + + if matched { + path = value + } else { + newArgs = append(newArgs, arg) + } + } + + os.Args = newArgs + return path +} diff --git a/webhook.go b/webhook.go index ff64186..7a3787c 100644 --- a/webhook.go +++ b/webhook.go @@ -99,6 +99,15 @@ func main() { // register platform-specific flags platformFlags() + // Extract -config / -c from os.Args before flag.Parse() sees it. + configFile = extractConfigFlag() + if configFile != "" { + if err := loadConfigFile(configFile); err != nil { + fmt.Println("error:", err) + os.Exit(1) + } + } + flag.Parse() if err := initExecutionSettings(); err != nil { diff --git a/webhook.yaml.example b/webhook.yaml.example new file mode 100644 index 0000000..8d0ed9b --- /dev/null +++ b/webhook.yaml.example @@ -0,0 +1,160 @@ +# Webhook 配置文件 +# 用法: webhook -c /etc/webhook/webhook.yaml +# +# 支持所有命令行参数。 +# 列表类参数(hooks、header)使用 YAML 数组语法。 +# 布尔类参数使用 true/false。 +# 命令行参数优先级高于本配置文件。 + +# ============================================================ +# 网络设置 +# ============================================================ + +# 监听地址,默认 0.0.0.0(监听所有网卡) +ip: 0.0.0.0 + +# 监听端口,默认 9000 +port: 1987 + +# URL 前缀,最终 hook 地址为 http://host:port/PREFIX/hook-id +# 默认值为 "hooks" +urlprefix: wh + +# 限制允许的 HTTP 方法,多个方法用逗号分隔(如 "POST,PUT") +# 留空表示允许所有方法 +# http-methods: "POST" + +# ============================================================ +# Hook 定义文件 +# ============================================================ + +# hook 定义文件路径,支持 JSON 和 YAML 格式 +# 可指定多个文件,每个文件中的 hook id 不能重复 +hooks: + - /etc/webhook/admin.yaml + - /etc/webhook/hooks.yaml + +# ============================================================ +# 运行行为 +# ============================================================ + +# 当无法加载任何 hook 时不要 panic 退出(需配合非 verbose 模式) +# 默认 false +nopanic: true + +# 监视 hook 文件变化,自动热重载 +# 默认 false +hotreload: true + +# 将 hook 文件作为 Go template 解析 +# 默认 false +# template: false + +# multipart 表单解析时的最大内存(字节),超出部分写入磁盘临时文件 +# 默认 1048576 (1MB) +# max-multipart-mem: 1048576 + +# ============================================================ +# 日志设置 +# ============================================================ + +# 启用详细日志输出 +# 默认 false +# verbose: true + +# 启用调试输出(会打印完整的请求/响应内容,隐含 verbose=true) +# 默认 false +# debug: false + +# 日志输出到文件(设置后隐含 verbose=true) +# 留空表示输出到 stdout +# logfile: /var/log/webhook.log + +# ============================================================ +# 自定义响应头 +# ============================================================ + +# 所有 hook 响应都会附带这些 header,格式为 "Name=Value" +# header: +# - "X-Custom-Header=value" +# - "Access-Control-Allow-Origin=*" + +# ============================================================ +# Request ID +# ============================================================ + +# 使用客户端传入的 X-Request-Id 头作为请求 ID(用于日志追踪) +# 默认 false +# x-request-id: false + +# 截断 X-Request-Id 的最大长度,0 表示不限制 +# x-request-id-limit: 0 + +# ============================================================ +# TLS / HTTPS 设置 +# ============================================================ + +# 启用 HTTPS 模式 +# 默认 false +# secure: false + +# TLS 证书文件路径 +# cert: /etc/webhook/cert.pem + +# TLS 私钥文件路径 +# key: /etc/webhook/key.pem + +# TLS 最低版本,可选 "1.0", "1.1", "1.2", "1.3" +# 默认 "1.2" +# tls-min-version: "1.2" + +# TLS 加密套件,逗号分隔。留空使用 Go 默认值 +# 可通过 webhook --list-cipher-suites 查看可用列表 +# cipher-suites: "" + +# ============================================================ +# Admin 管理界面 +# ============================================================ + +# 启用 Admin UI 和 API,用于在线管理 hooks +# 默认 false +# admin: false + +# Admin 界面的 URL 前缀,最终地址为 http://host:port/PREFIX/ +# 默认 "admin" +# admin-path: admin + +# Admin 登录使用的 TOTP 密钥(Base32 编码) +# 可用 Google Authenticator 等 APP 扫码登录 +# admin-totp-secret: "" + +# 用于签发 Admin JWT 会话令牌的密钥 +# admin-jwt-secret: "" + +# Admin JWT 会话有效期,支持 Go duration 格式(如 12h、30m、720h) +# 默认 "12h" +# admin-session-ttl: "12h" + +# ============================================================ +# Unix Socket(仅 Linux/macOS) +# ============================================================ + +# 使用 Unix socket 代替 IP:Port 监听 +# 设置后 ip 和 port 选项将被忽略 +# socket: /tmp/webhook.sock + +# ============================================================ +# 权限降级(仅 Linux/macOS) +# ============================================================ + +# 打开监听端口后切换到指定的用户/组 ID 运行 +# setuid 和 setgid 必须同时设置,不能与 socket 同时使用 +# setuid: 1000 +# setgid: 1000 + +# ============================================================ +# PID 文件 +# ============================================================ + +# 创建 PID 文件,用于进程管理 +# pidfile: /var/run/webhook.pid