Packages webhook: parse zot v2.1 events CloudEvent payload
1276b61 · gmorell · 2026-06-25 21:11
Message
{commit_body(@commit)}
Files changed
modified
lib/git_gud_web/controllers/packages_webhook_controller.ex
+41
−20
@@ -35,8 +35,12 @@ defmodule GitGudWeb.PackagesWebhookController do
| 35 | 35 | alias GitGud.Packages |
| 36 | 36 | alias GitGud.Repositories |
| 37 | 37 | |
| 38 | + require Logger | |
| 39 | + | |
| 38 | 40 | def notify(conn, params) do |
| 39 | 41 | with :ok <- authenticate(conn) do |
| 42 | + Logger.info("zot webhook: #{inspect(params, limit: 40, printable_limit: 1500)}") | |
| 43 | + | |
| 40 | 44 | case classify(params) do |
| 41 | 45 | {:push, payload} -> |
| 42 | 46 | handle_push(conn, payload) |
@@ -55,32 +59,44 @@ defmodule GitGudWeb.PackagesWebhookController do
| 55 | 59 | |
| 56 | 60 | # ── dispatch ──────────────────────────────────────────────────────── |
| 57 | 61 | |
| 58 | − defp classify(%{"event" => event} = p) when is_binary(event) do | |
| 59 | − case String.upcase(event) do | |
| 60 | − "PUSH" -> {:push, p} | |
| 61 | − "DELETE" -> {:delete, p} | |
| 62 | − _ -> :ignored | |
| 63 | − end | |
| 64 | − end | |
| 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) | |
| 73 | + a = String.downcase(action) | |
| 65 | 74 | |
| 66 | − defp classify(%{"type" => type} = p) when is_binary(type) do | |
| 67 | − # Some zot versions use `type` instead of `event`. | |
| 68 | − classify(Map.put(p, "event", type)) | |
| 75 | + cond do | |
| 76 | + a == "push" or String.contains?(a, "updated") or String.contains?(a, "created") -> | |
| 77 | + {:push, data} | |
| 78 | + | |
| 79 | + a == "delete" or String.contains?(a, "deleted") -> | |
| 80 | + {:delete, data} | |
| 81 | + | |
| 82 | + true -> | |
| 83 | + :ignored | |
| 84 | + end | |
| 69 | 85 | end |
| 70 | 86 | |
| 71 | − defp classify(_), do: :ignored | |
| 87 | + defp unwrap_data(%{"data" => %{} = data}), do: data | |
| 88 | + defp unwrap_data(p), do: p | |
| 72 | 89 | |
| 73 | 90 | defp handle_push(conn, p) do |
| 74 | − with {:ok, repo, image} <- resolve_repo_and_image(p["repository"]), | |
| 75 | − reference when is_binary(reference) <- p["reference"] || p["tag"], | |
| 76 | − digest when is_binary(digest) <- p["digest"] do | |
| 91 | + with {:ok, repo, image} <- resolve_repo_and_image(repository(p)), | |
| 92 | + reference when is_binary(reference) <- reference(p) do | |
| 77 | 93 | attrs = %{ |
| 78 | 94 | version: reference, |
| 79 | − digest: digest, | |
| 95 | + digest: digest(p) || "", | |
| 80 | 96 | size: parse_size(p), |
| 81 | − media_type: p["mediaType"], | |
| 82 | − manifest_doc: p["manifest"] || %{}, | |
| 83 | − pushed_at: parse_timestamp(p["timestamp"]) | |
| 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"]) | |
| 84 | 100 | } |
| 85 | 101 | |
| 86 | 102 | _ = Packages.record_push(repo, "container", image, attrs) |
@@ -91,8 +107,8 @@ defmodule GitGudWeb.PackagesWebhookController do
| 91 | 107 | end |
| 92 | 108 | |
| 93 | 109 | defp handle_delete(conn, p) do |
| 94 | − with {:ok, repo, image} <- resolve_repo_and_image(p["repository"]), | |
| 95 | − reference when is_binary(reference) <- p["reference"] || p["tag"] do | |
| 110 | + with {:ok, repo, image} <- resolve_repo_and_image(repository(p)), | |
| 111 | + reference when is_binary(reference) <- reference(p) do | |
| 96 | 112 | _ = Packages.delete_version(repo, "container", image, reference) |
| 97 | 113 | send_resp(conn, 204, "") |
| 98 | 114 | else |
@@ -100,6 +116,11 @@ defmodule GitGudWeb.PackagesWebhookController do
| 100 | 116 | end |
| 101 | 117 | end |
| 102 | 118 | |
| 119 | + # zot's field naming varies across the notifications/events payloads. | |
| 120 | + defp repository(p), do: p["repository"] || p["name"] || p["Name"] || p["Repository"] | |
| 121 | + defp reference(p), do: p["reference"] || p["tag"] || p["Reference"] || p["Tag"] | |
| 122 | + defp digest(p), do: p["digest"] || p["Digest"] || p["manifestDigest"] | |
| 123 | + | |
| 103 | 124 | # ── helpers ──────────────────────────────────────────────────────── |
| 104 | 125 | |
| 105 | 126 | defp resolve_repo_and_image(nil), do: :error |
Parents: a4513e0