Files
at-container-registry/docs/WEBHOOKS.md
T

238 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Webhooks
Webhooks notify external services when events occur in the registry. Payloads are JSON, signed with HMAC-SHA256 (optional), and delivered with retry (exponential backoff: 0s, 30s, 2m, 8m). Discord and Slack URLs are auto-detected and receive platform-native formatting.
## Current Events
### `push` — Image Push
Fires when a manifest is stored (the "logical push complete" moment). Tagless pushes (e.g., buildx platform manifests) also fire with an empty `tag` field.
**Bitmask:** `0x08` — Free tier
```json
{
"trigger": "push",
"push_data": {
"pushed_at": "2026-02-27T15:30:00Z",
"pusher": "alice.bsky.social",
"pusher_did": "did:plc:abc123",
"tag": "latest",
"digest": "sha256:abc..."
},
"repository": {
"name": "myapp",
"namespace": "alice.bsky.social",
"repo_name": "alice.bsky.social/myapp",
"repo_url": "https://buoy.cr/alice.bsky.social/myapp",
"media_type": "application/vnd.oci.image.manifest.v1+json",
"star_count": 42,
"pull_count": 1337
},
"hold": {
"did": "did:web:hold01.atcr.io",
"endpoint": "https://hold01.atcr.io"
}
}
```
`repo_url` uses `registry_domains[0]` (the pull domain) when configured, otherwise falls back to `base_url`.
### `scan:first` — First Scan
Fires the first time an image is scanned (no previous scan record exists).
**Bitmask:** `0x01` — Free tier
### `scan:all` — Every Scan
Fires on every scan completion.
**Bitmask:** `0x02` — Paid tier
### `scan:changed` — Vulnerability Change
Fires when vulnerability counts change from the previous scan. Includes a `previous` field with the old counts.
**Bitmask:** `0x04` — Paid tier
**Scan payload format** (shared by all scan triggers):
```json
{
"trigger": "scan:first",
"holdDid": "did:web:hold01.atcr.io",
"holdEndpoint": "https://hold01.atcr.io",
"manifest": {
"digest": "sha256:abc...",
"repository": "myapp",
"tag": "latest",
"userDid": "did:plc:abc123",
"userHandle": "alice.bsky.social"
},
"scan": {
"scannedAt": "2026-02-27T16:00:00Z",
"scannerVersion": "atcr-scanner-v1.0.0",
"vulnerabilities": {
"critical": 0,
"high": 2,
"medium": 5,
"low": 12,
"total": 19
}
},
"previous": null
}
```
For `scan:changed`, the `previous` field contains the previous vulnerability counts.
## Billing
| Tier | Max Webhooks | Available Triggers |
|------|-------------|-------------------|
| Free | 1 | `push`, `scan:first` |
| Paid | Per plan | All triggers |
| Captain | Unlimited | All triggers |
Free users can enable both `push` and `scan:first` on their single webhook.
## Security
- **HMAC-SHA256 signing:** If a secret is set, payloads include `X-Webhook-Signature-256: sha256=<hex>`. The signature covers the delivered payload (including platform-specific formatting for Discord/Slack).
- **Retry:** 4 attempts with exponential backoff (0s, 30s, 2m, 8m).
- **Test delivery:** The settings UI supports sending a test payload to verify connectivity.
## Implementation
- Types: `pkg/appview/webhooks/types.go`
- Dispatch + retry: `pkg/appview/webhooks/dispatch.go`
- Discord/Slack formatting: `pkg/appview/webhooks/format.go`
- UI handlers: `pkg/appview/handlers/webhooks.go`
- Settings page SSR: `pkg/appview/handlers/settings.go`
- Template: `pkg/appview/templates/partials/webhooks_list.html`
- Trigger bitmask stored in `webhooks.triggers` column (integer)
---
## Future Events
Inspired by [Harbor's webhook model](https://goharbor.io/docs/working-with-projects/project-configuration/configure-webhooks/). These are not yet implemented but document the intended direction.
### `pull` — Image Pull
**Bitmask:** `0x10` (reserved)
Fires when a manifest is pulled. This is tricky because pulls go through presigned S3 URLs — the appview issues a redirect and never sees the actual blob download. Manifest fetches *are* visible to the appview, so a pull event would fire on manifest GET, not blob download.
**Scalability concern:** Public repos with high pull volume would generate excessive webhook traffic. Would need rate limiting or batching (e.g., "5 pulls in the last minute" digest). Not suitable for free tier without throttling.
**Suggested payload:**
```json
{
"trigger": "pull",
"pull_data": {
"pulled_at": "2026-02-27T15:30:00Z",
"puller": "bob.bsky.social",
"puller_did": "did:plc:def456",
"tag": "latest",
"digest": "sha256:abc..."
},
"repository": {
"name": "myapp",
"namespace": "alice.bsky.social",
"repo_name": "alice.bsky.social/myapp",
"repo_url": "https://buoy.cr/alice.bsky.social/myapp",
"star_count": 42,
"pull_count": 1338
},
"hold": {
"did": "did:web:hold01.atcr.io",
"endpoint": "https://hold01.atcr.io"
}
}
```
Anonymous pulls would have empty `puller` / `puller_did` fields.
### `delete` — Manifest Delete
**Bitmask:** `0x20` (reserved)
Fires when a manifest is deleted from the user's PDS. Lower priority — deletes are uncommon.
**Suggested payload:**
```json
{
"trigger": "delete",
"delete_data": {
"deleted_at": "2026-02-27T15:30:00Z",
"deleted_by": "alice.bsky.social",
"deleted_by_did": "did:plc:abc123",
"tag": "v1.0.0",
"digest": "sha256:abc..."
},
"repository": {
"name": "myapp",
"namespace": "alice.bsky.social",
"repo_name": "alice.bsky.social/myapp",
"repo_url": "https://buoy.cr/alice.bsky.social/myapp",
"star_count": 42,
"pull_count": 1337
}
}
```
No `hold` field — deletion removes the manifest record from the PDS; blob cleanup is handled separately by GC.
### `quota` — Storage Quota Threshold
**Bitmask:** `0x40` — Free tier
Fires when the user's storage on a hold rises across a configurable threshold percent. Each quota webhook carries its own threshold, so a user who can create multiple webhooks (paid tier) can pin alerts at e.g. 75%, 90%, and 100% by creating one webhook per percentage.
**Per-webhook configuration.** The threshold is stored in the same `triggers` integer as the bitmask flags: bits 07 are the flag bits, bits 815 hold the threshold percent (0100). Read flag bits through `webhooks.TriggerFlags(triggers)` and the threshold through `webhooks.ThresholdPct(triggers)`. The packing is invisible to webhook recipients — only the JSON payload's `threshold_percent` matters.
**Edge-triggered dedup.** Each webhook row carries a `last_fired_at TIMESTAMP NULL` (generic, reusable by future triggers). For quota:
- `last_fired_at IS NULL` → armed: dispatcher fires when `current_pct >= threshold` and stamps `last_fired_at`.
- `last_fired_at IS NOT NULL` → already fired and still above threshold: suppressed.
- When `current_pct < threshold` and `last_fired_at IS NOT NULL`, the dispatcher clears `last_fired_at` so the next upward crossing fires again.
Re-arming happens lazily on the next push by that user — there is no background sweep. A user who deletes images to free space and then never pushes again will leave `last_fired_at` set; that is harmless because no further alerts are needed until activity resumes.
**Scope.** Quota webhooks are per-user. They fire for the pusher's storage (whose layer records actually grew), not the repo owner — on a crew push the owner's quota is unaffected. The hold's public `io.atcr.hold.getQuota` endpoint is the source of truth; if the hold is unreachable, dispatch is skipped (push has already succeeded).
**Payload:**
```json
{
"trigger": "quota",
"quota_data": {
"timestamp": "2026-02-27T15:30:00Z",
"usage_bytes": 8589934592,
"limit_bytes": 10737418240,
"usage_percent": 80,
"threshold_percent": 75
},
"hold": {
"did": "did:web:hold01.atcr.io",
"endpoint": "https://hold01.atcr.io"
},
"user": {
"did": "did:plc:abc123",
"handle": "alice.bsky.social"
}
}
```
`usage_percent` is the user's current usage at fire time; `threshold_percent` echoes the per-webhook threshold that triggered this delivery. Captains and unlimited tiers have no `limit` and never fire quota webhooks.
### Events explicitly not planned
- **Scan failed / scan stopped** — Server-side operational issues, not user-actionable. Belongs in ops monitoring (logs, alerting), not user-facing webhooks.
- **Replication** — No replication feature in ATCR.
- **Tag retention** — No retention policies yet.