Webhook
Webhook 会在仓库发生变化时通知你的系统,这样它就不必轮询。Arkvory 的 worker 会针对仓库变更订阅源的每个事件,向你的 URL 发送一个 HTTP POST。可用它在构建发布时启动部署,或在某个路径有了新版本时刷新缓存。
订阅由管理员在文件中设置,目前还没有相应的 API 或控制台页面。你也可以自己读取变更订阅源:GET /api/v1/repositories/<repository>/changes。
投递如何工作
- 一个订阅跟随一个仓库,并发送到一个 URL。
- 事件按顺序逐个到达。 下一个事件要等接收方返回
2xx状态后才发送。 - 至少投递一次。 崩溃或响应丢失后,同一事件可能再次到达。每个事件都有稳定的
id:请按它去重。 - 新订阅只收到新事件。 创建之前的事件不会发送。
- 接收方宕机只会延迟它自己的订阅。 事件会等待。Arkvory 在 12 秒后重试,把间隔翻倍直至一小时,并在接收方重新响应后按顺序发送事件。上传和下载从不等待 webhook。
设置订阅
你需要能访问服务器文件,并有权重启 worker。参见配置。
- 创建一个包含签名密钥的文件:至少 16 个随机的可打印字符,例如
/root/ci.secret。接收方需要使用相同的密钥。 - 用订阅信息运行
configure。该命令会检查你的输入,把密钥复制到config/webhooks,写入订阅文件和设置,重启服务,并在服务未就绪时恢复之前的配置。它不会联系接收方。
sudo arkvory configure --root /opt/proanima-arkvory \
--webhook ci --webhook-repository releases \
--webhook-url https://ci.example.com/hooks/arkvory \
--webhook-secret-file /root/ci.secret \
--webhook-actions artifact.publish- 在 worker 日志中查找
webhook.started,发布一个文件,并观察你的接收方。
该命令会写入 config/webhooks/webhooks.json,你也可以手工编写并在 ARKVORY_WEBHOOKS_FILE 中指定。要修改订阅,请用相同的名称再次运行该命令。--webhook-detach ci 会删除订阅:其文件被删除,其位置被遗忘。一次调用只修改一个订阅,不能与其他 configure 选项混用。
文件包含以下字段:
{
"webhooks": [
{
"id": "ci",
"repository": "releases",
"url": "https://ci.example.com/hooks/arkvory",
"secretFile": "/opt/proanima-arkvory/config/webhooks/ci.secret",
"actions": ["artifact.publish"]
}
]
}| 字段 | 含义 |
|---|---|
id | 订阅名称:1 到 64 个字符,可用 a-z、0-9、_ 和 -。其进度按此名称保存。 |
repository | 要发送其变更订阅源的仓库。 |
url | 接收方。使用 HTTPS,不含用户名、query 或 fragment,最长 2048 个字符。仅 localhost、127.0.0.1 和 [::1] 允许使用明文 HTTP。 |
secretFile | 签名密钥文件的绝对路径。 |
nextSecretFile | 可选的第二个密钥,用于轮换。 |
actions | 可选的要发送的订阅源操作列表,例如 artifact.publish、artifact.delete、asset.replace、stage.added 和 package.register。不设置则发送所有操作。 |
文件有误会使 worker 在启动时以 worker.unavailable 停止。最多允许 16 个订阅。在 Compose 安装中,该命令还会把 config/webhooks 挂载到 worker 容器中。
请求
| 请求头 | 值 |
|---|---|
Content-Type | application/json |
X-Arkvory-Delivery | 事件的 id。同一事件的每次重复都相同。 |
X-Arkvory-Event | 订阅源操作,例如 artifact.publish。 |
X-Arkvory-Timestamp | 请求签名时的 Unix 时间(秒)。 |
X-Arkvory-Signature | sha256=<hex>。轮换期间有两个值,以逗号分隔。 |
请求体是 JSON:
{
"id": "releases:128",
"repository": "releases",
"sequence": "128",
"action": "artifact.publish",
"artifactId": "00000000-0000-4000-8000-000000000001",
"detail": null
}sequence 是在订阅源中的位置,为十进制字符串。对于带有路径或阶段的操作,detail 就是文件路径或阶段。请求体不含作者、文件内容和元数据:请用 artifactId 通过 API 读取当前状态。
请在 10 秒内返回任意 2xx 状态。3xx(不会跟随重定向)、4xx、5xx、连接错误或超时都算失败。Arkvory 最多读取响应的 4 KiB,并忽略其内容。
验证签名
签名是用你的密钥对文本 <timestamp>.<body> 计算的 HMAC-SHA256,写作 sha256= 加十六进制摘要。信任请求之前先检查它:
- 在解析 JSON 之前读取原始请求体。
- 如果时间戳与你的时钟相差超过 5 分钟,拒绝该请求。
- 计算签名并用恒定时间函数比较。如果请求头有两个值,接受其中任意一个。
Node.js:
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(secret, headers, rawBody) {
const timestamp = headers['x-arkvory-timestamp'];
if (!/^\d{1,12}$/.test(timestamp ?? '')) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = Buffer.from(
'sha256=' + createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex'),
);
return String(headers['x-arkvory-signature'] ?? '')
.split(',')
.some((given) => {
const actual = Buffer.from(given.trim());
return actual.length === expected.length && timingSafeEqual(actual, expected);
});
}Python:
import hashlib, hmac, time
def verify(secret: bytes, headers, raw_body: bytes) -> bool:
timestamp = headers.get("X-Arkvory-Timestamp", "")
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False
digest = hmac.new(secret, timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
expected = "sha256=" + digest
given = headers.get("X-Arkvory-Signature", "").split(",")
return any(hmac.compare_digest(part.strip(), expected) for part in given)处理重复投递
- 保存每个已处理事件的
id,并忽略重复。 - 快速响应:把工作放入队列并返回
204。接收方过慢会延迟其订阅之后的所有事件。 - 重复并不意味着操作发生了两次。请把事件当作提示,并从 API 读取当前状态。
轮换密钥
- 再次运行同一命令:用
--webhook-secret-file指定当前密钥,用--webhook-next-secret-file指定新密钥。此后每个请求都带有两个签名。 - 把接收方切换到新密钥。在此期间,接受任一签名的接收方可继续工作。
- 再次运行该命令,把新密钥文件作为
--webhook-secret-file,并去掉--webhook-next-secret-file。
内部接收方与证书
- Arkvory 会拒绝位于环回、私有、链路本地和云元数据地址上的接收方,以及解析到这些地址的名称。这样服务器就不能被用来访问内部服务。
- 要向你网络内的接收方发送,请用
--webhook-allow-private添加该网络,例如--webhook-allow-private 10.20.0.0/16(ARKVORY_WEBHOOKS_ALLOW_PRIVATE)。它会一直保留,直到删除最后一个订阅。 - 如果接收方的证书来自你自己的证书颁发机构,请用
--webhook-ca-file添加包含该机构的 PEM 文件(ARKVORY_WEBHOOKS_CA_FILE)。证书始终会被验证。
监控 webhook
投递失败时 worker 会写入带有 errorCode 的 webhook.step_failed,恢复后写入 webhook.recovered。指标 arkvory_webhook_failing 和 arkvory_webhook_last_success_timestamp_seconds 以及告警 ArkvoryWebhookFailing 见监控。webhook.stopped 表示订阅循环结束:在停止时,或 worker 失去存储所有权时。webhook.prune_failed 表示 worker 启动时未能忘记已删除的订阅;下次启动时会再试。代码 unavailable 和 internal 只出现在日志中,不反映在 arkvory_webhook_failing 中。
订阅停滞不前
Arkvory 从不跳过事件:投递的可靠性正建立在这一点上。因此,接收方如果一再拒绝同一个事件,例如因无法解析而返回 400,就会让整个订阅停下来。该订阅之后的事件都不会发送;其他订阅照常工作。worker 写入带有 http_4xx 的 webhook.step_failed,并以最长一小时的间隔重试,arkvory_webhook_failing 为 1,告警 ArkvoryWebhookFailing 在 15 分钟后触发。
- 修复接收方,使其对该事件返回
2xx。Arkvory 会在下一次重试时发送它(最晚约一小时后),然后按顺序发送所有等待中的事件。用相同的名称和仓库再次运行configure会保留位置。 - 如果该事件永远无法被接受,用
--webhook-detach删除订阅后重新添加。它从订阅源末尾开始:等待中的事件不会发送。请从变更订阅源中读取接收方处理过的最后一个sequence之后的事件。
故障排除
errorCode | 原因与处理方法 |
|---|---|
blocked | 接收方地址不被允许(环回、私有、链路本地或元数据)。请使用公网地址,或把该网络加入 ARKVORY_WEBHOOKS_ALLOW_PRIVATE。 |
timeout | 10 秒内没有响应。请更快响应,并把工作放入队列。 |
network | 连接被拒绝、找不到名称或连接被重置。请从服务器检查 URL、DNS 和防火墙。 |
tls | 证书不受信任、已过期或名称不符。请修正,或设置 ARKVORY_WEBHOOKS_CA_FILE。 |
redirect | 接收方返回了 3xx。不会跟随重定向:请使用最终 URL。 |
http_4xx | 接收方拒绝了请求。请检查它的签名校验、路径和密钥。 |
http_5xx | 接收方出错。Arkvory 会持续重试,间隔最长一小时。 |
secret | 密钥文件不存在、无法读取或短于 16 字节。 |
unavailable | worker 无法从数据库读取订阅源或订阅状态。没有发送任何内容;5 秒后重试。请检查数据库。 |
internal | worker 中出现意外错误。没有发送任何内容;5 秒后重试。请附上 worker 日志报告此问题。 |
什么都没收到?请检查 worker 日志中是否有 webhook.started,ARKVORY_WEBHOOKS_FILE 是否已设置,仓库名称以及 actions 过滤器。新订阅只发送其第一步之后的事件。