Packages webhook: read CloudEvent type from ce-type header
f795f88 · gmorell · 2026-06-25 21:43
Message
{commit_body(@commit)}
Files changed
modified
lib/git_gud_web/controllers/packages_webhook_controller.ex
+49
−22
@@ -39,9 +39,13 @@ defmodule GitGudWeb.PackagesWebhookController do
| 39 | 39 | |
| 40 | 40 | def notify(conn, params) do |
| 41 | 41 | with :ok <- authenticate(conn) do |
| 42 | − Logger.info("zot webhook: #{inspect(params, limit: 40, printable_limit: 1500)}") | |
| 42 | + ce_type = cloudevent_type(conn) | |
| 43 | 43 | |
| 44 | − case classify(params) do | |
| 44 | + Logger.debug(fn -> | |
| 45 | + "zot webhook: ce-type=#{inspect(ce_type)} name=#{inspect(params["name"])} ref=#{inspect(params["reference"])}" | |
| 46 | + end) | |
| 47 | + | |
| 48 | + case classify(ce_type, params) do | |
| 45 | 49 | {:push, payload} -> |
| 46 | 50 | handle_push(conn, payload) |
| 47 | 51 |
@@ -59,17 +63,14 @@ defmodule GitGudWeb.PackagesWebhookController do
| 59 | 63 | |
| 60 | 64 | # ── dispatch ──────────────────────────────────────────────────────── |
| 61 | 65 | |
| 62 | − # Old zot `notifications` payloads carried `event`/`type` = PUSH/DELETE | |
| 63 | − # at the top level; the v2.1 `events` extension sends a CloudEvent with | |
| 64 | − # `type` like `zotregistry.image.updated` and the image fields nested | |
| 65 | − # under `data`. Handle both: derive the action from whichever action | |
| 66 | − # string is present, and operate on the unwrapped data map. | |
| 67 | − defp classify(%{"event" => event} = p) when is_binary(event), do: classify_action(event, p) | |
| 68 | − defp classify(%{"type" => type} = p) when is_binary(type), do: classify_action(type, p) | |
| 69 | − defp classify(_), do: :ignored | |
| 70 | − | |
| 71 | − defp classify_action(action, p) do | |
| 72 | − data = unwrap_data(p) | |
| 66 | + # zot's v2.1 `events` extension sends CloudEvents in *binary* mode: the | |
| 67 | + # event type (`zotregistry.image.updated` / `.deleted`) is in the | |
| 68 | + # `ce-type` HTTP header, and the image fields (name/reference/digest/…) | |
| 69 | + # are the JSON body. Older `notifications` payloads carried the action | |
| 70 | + # as a body `event`/`type` field — fall back to that. | |
| 71 | + defp classify(ce_type, params) do | |
| 72 | + action = ce_type || params["type"] || params["event"] || "" | |
| 73 | + data = unwrap_data(params) | |
| 73 | 74 | a = String.downcase(action) |
| 74 | 75 | |
| 75 | 76 | cond do |
@@ -84,22 +85,34 @@ defmodule GitGudWeb.PackagesWebhookController do
| 84 | 85 | end |
| 85 | 86 | end |
| 86 | 87 | |
| 88 | + defp cloudevent_type(conn) do | |
| 89 | + case get_req_header(conn, "ce-type") do | |
| 90 | + [t | _] -> t | |
| 91 | + _ -> nil | |
| 92 | + end | |
| 93 | + end | |
| 94 | + | |
| 87 | 95 | defp unwrap_data(%{"data" => %{} = data}), do: data |
| 88 | 96 | defp unwrap_data(p), do: p |
| 89 | 97 | |
| 90 | 98 | defp handle_push(conn, p) do |
| 91 | 99 | with {:ok, repo, image} <- resolve_repo_and_image(repository(p)), |
| 92 | 100 | reference when is_binary(reference) <- reference(p) do |
| 93 | − attrs = %{ | |
| 94 | − version: reference, | |
| 95 | − digest: digest(p) || "", | |
| 96 | − size: parse_size(p), | |
| 97 | − media_type: p["mediaType"] || p["MediaType"], | |
| 98 | − manifest_doc: p["manifest"] || p["Manifest"] || %{}, | |
| 99 | − pushed_at: parse_timestamp(p["timestamp"] || p["time"] || p["Timestamp"]) | |
| 100 | − } | |
| 101 | + # Skip by-digest manifest pushes (the per-arch children of an | |
| 102 | + # image index) — only tags are worth surfacing. | |
| 103 | + unless digest_ref?(reference) do | |
| 104 | + attrs = %{ | |
| 105 | + version: reference, | |
| 106 | + digest: digest(p) || "", | |
| 107 | + size: parse_size(p), | |
| 108 | + media_type: p["mediaType"] || p["MediaType"], | |
| 109 | + manifest_doc: decode_manifest(p["manifest"] || p["Manifest"]), | |
| 110 | + pushed_at: parse_timestamp(p["timestamp"] || p["time"] || p["Timestamp"]) | |
| 111 | + } | |
| 112 | + | |
| 113 | + _ = Packages.record_push(repo, "container", image, attrs) | |
| 114 | + end | |
| 101 | 115 | |
| 102 | − _ = Packages.record_push(repo, "container", image, attrs) | |
| 103 | 116 | send_resp(conn, 204, "") |
| 104 | 117 | else |
| 105 | 118 | _ -> send_resp(conn, 422, "unparseable payload") |
@@ -121,6 +134,20 @@ defmodule GitGudWeb.PackagesWebhookController do
| 121 | 134 | defp reference(p), do: p["reference"] || p["tag"] || p["Reference"] || p["Tag"] |
| 122 | 135 | defp digest(p), do: p["digest"] || p["Digest"] || p["manifestDigest"] |
| 123 | 136 | |
| 137 | + defp digest_ref?(ref), do: String.starts_with?(ref, "sha256:") | |
| 138 | + | |
| 139 | + # The events payload carries `manifest` as a JSON string; the schema | |
| 140 | + # field is a map. | |
| 141 | + defp decode_manifest(m) when is_binary(m) do | |
| 142 | + case Jason.decode(m) do | |
| 143 | + {:ok, map} when is_map(map) -> map | |
| 144 | + _ -> %{} | |
| 145 | + end | |
| 146 | + end | |
| 147 | + | |
| 148 | + defp decode_manifest(m) when is_map(m), do: m | |
| 149 | + defp decode_manifest(_), do: %{} | |
| 150 | + | |
| 124 | 151 | # ── helpers ──────────────────────────────────────────────────────── |
| 125 | 152 | |
| 126 | 153 | defp resolve_repo_and_image(nil), do: :error |
Parents: 34ac246