Jump to…
snowinitial commitqoxwzsukwmkx1mo
1//! Keeping the pre-receive hook installed (spec §4, §9).
2//!
3//! > Hooks are the compiled `dogfood-hook` binary, installed into each repo's
4//! > `hooks/` directory at creation.
5//!
6//! Installing at creation is necessary but not sufficient. A repository created
7//! before the hook existed, restored from a backup taken before it, or created
8//! by a code path that forgot the call, has no hook — and a *missing* hook fails
9//! open. Nothing errors; pushes simply stop being validated. Protected bookmarks
10//! silently become unprotected and the ref-name allowlist silently stops
11//! applying.
12//!
13//! That is exactly the shape of bug this sweep exists to make impossible: on
14//! every boot, every repository gets the hook reinstalled. The write is
15//! idempotent and costs one small file per repository, which is nothing next to
16//! a validation control that is off without anybody noticing.
17
18use std::sync::Arc;
19
20use df_db::PgPool;
21use df_store::{RepoId, RepoStore};
22use uuid::Uuid;
23
24/// Install the hook into one repository.
25///
26/// Called at creation, and by the boot sweep. Errors are the caller's to
27/// report: at creation a failure should be loud, during the sweep it should not
28/// stop the process from starting.
29pub async fn install(store: &dyn RepoStore, repo_id: Uuid, hook_binary: &str) -> anyhow::Result<()> {
30 store
31 .configure_receive_validation(RepoId(repo_id), hook_binary)
32 .await
33 .map_err(|e| anyhow::anyhow!("installing the pre-receive hook: {e}"))
34}
35
36/// Reinstall the hook on every repository, at startup.
37///
38/// Runs in the background so a slow filesystem cannot delay serving, and never
39/// fails the process — an instance that cannot install hooks should still come
40/// up, loudly complaining, rather than refuse to start.
41pub fn sweep_in_background(db: PgPool, store: Arc<dyn RepoStore>, hook_binary: String) {
42 tokio::spawn(async move {
43 let repos: Vec<(Uuid,)> = match sqlx::query_as("SELECT id FROM repos").fetch_all(&db).await {
44 Ok(r) => r,
45 Err(e) => {
46 tracing::error!("could not list repositories to install hooks: {e}");
47 return;
48 }
49 };
50
51 let total = repos.len();
52 let mut failed = 0usize;
53
54 for (id,) in repos {
55 // A repository row whose storage is missing is a real state after a
56 // partial restore (spec §10). It is reported by the reindex path,
57 // not here, so this only counts hook failures.
58 if !store.exists(RepoId(id)).await {
59 continue;
60 }
61 if let Err(e) = install(store.as_ref(), id, &hook_binary).await {
62 failed += 1;
63 tracing::error!(repo = %id, "{e:#}");
64 }
65 }
66
67 if failed > 0 {
68 tracing::error!(
69 total,
70 failed,
71 "some repositories are accepting pushes WITHOUT validation"
72 );
73 } else {
74 tracing::info!(total, "pre-receive hooks verified on every repository");
75 }
76 });
77}

77 lines · Rust