Skip to content

Commit 85a9829

Browse files
committed
worker: Implement SyncAdmins background job
1 parent 7247bae commit 85a9829

File tree

5 files changed

+180
-0
lines changed

5 files changed

+180
-0
lines changed

src/tests/worker/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
mod git;
2+
mod sync_admins;

src/tests/worker/sync_admins.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use crate::util::TestApp;
2+
use crates_io::schema::users;
3+
use crates_io::team_repo::{Member, MockTeamRepo, Team};
4+
use crates_io::worker::jobs::SyncAdmins;
5+
use crates_io_worker::BackgroundJob;
6+
use diesel::prelude::*;
7+
use diesel::{PgConnection, QueryResult, RunQueryDsl};
8+
9+
#[test]
10+
fn test_sync_admins_job() {
11+
let mock_response = mock_team(
12+
"crates-io",
13+
vec![
14+
mock_member("existing-admin", 1),
15+
mock_member("new-admin", 3),
16+
],
17+
);
18+
19+
let mut team_repo = MockTeamRepo::new();
20+
team_repo
21+
.expect_get_team()
22+
.with(mockall::predicate::eq("crates-io-admins"))
23+
.returning(move |_| Ok(mock_response.clone()));
24+
25+
let (app, _) = TestApp::full().with_team_repo(team_repo).empty();
26+
27+
app.db(|conn| create_user("existing-admin", 1, true, conn).unwrap());
28+
app.db(|conn| create_user("obsolete-admin", 2, true, conn).unwrap());
29+
app.db(|conn| create_user("new-admin", 3, false, conn).unwrap());
30+
app.db(|conn| create_user("unrelated-user", 42, false, conn).unwrap());
31+
32+
let admins = app.db(|conn| get_admins(conn).unwrap());
33+
let expected_admins = vec![("existing-admin".into(), 1), ("obsolete-admin".into(), 2)];
34+
assert_eq!(admins, expected_admins);
35+
36+
app.db(|conn| SyncAdmins.enqueue(conn).unwrap());
37+
app.run_pending_background_jobs();
38+
39+
let admins = app.db(|conn| get_admins(conn).unwrap());
40+
let expected_admins = vec![("existing-admin".into(), 1), ("new-admin".into(), 3)];
41+
assert_eq!(admins, expected_admins);
42+
}
43+
44+
fn mock_team(name: impl Into<String>, members: Vec<Member>) -> Team {
45+
Team {
46+
name: name.into(),
47+
kind: "marker-team".to_string(),
48+
members,
49+
}
50+
}
51+
52+
fn mock_member(name: impl Into<String>, github_id: i32) -> Member {
53+
let name = name.into();
54+
let github = name.clone();
55+
Member {
56+
name,
57+
github,
58+
github_id,
59+
is_lead: false,
60+
}
61+
}
62+
63+
fn create_user(name: &str, gh_id: i32, is_admin: bool, conn: &mut PgConnection) -> QueryResult<()> {
64+
diesel::insert_into(users::table)
65+
.values((
66+
users::name.eq(name),
67+
users::gh_login.eq(name),
68+
users::gh_id.eq(gh_id),
69+
users::gh_access_token.eq("some random token"),
70+
users::is_admin.eq(is_admin),
71+
))
72+
.execute(conn)?;
73+
74+
Ok(())
75+
}
76+
77+
fn get_admins(conn: &mut PgConnection) -> QueryResult<Vec<(String, i32)>> {
78+
users::table
79+
.select((users::gh_login, users::gh_id))
80+
.filter(users::is_admin.eq(true))
81+
.order(users::gh_id.asc())
82+
.get_results(conn)
83+
}

src/worker/jobs/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ mod daily_db_maintenance;
99
pub mod dump_db;
1010
mod git;
1111
mod readmes;
12+
mod sync_admins;
1213
mod typosquat;
1314
mod update_downloads;
1415

1516
pub use self::daily_db_maintenance::DailyDbMaintenance;
1617
pub use self::dump_db::DumpDb;
1718
pub use self::git::{NormalizeIndex, SquashIndex, SyncToGitIndex, SyncToSparseIndex};
1819
pub use self::readmes::RenderAndUploadReadme;
20+
pub use self::sync_admins::SyncAdmins;
1921
pub use self::typosquat::CheckTyposquat;
2022
pub use self::update_downloads::UpdateDownloads;
2123

src/worker/jobs/sync_admins.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use crate::schema::users;
2+
use crate::tasks::spawn_blocking;
3+
use crate::worker::Environment;
4+
use crates_io_worker::BackgroundJob;
5+
use diesel::prelude::*;
6+
use diesel::RunQueryDsl;
7+
use std::collections::HashSet;
8+
use std::sync::Arc;
9+
10+
/// See <https://github.com/rust-lang/team/blob/master/teams/crates-io-admins.toml>.
11+
const TEAM_NAME: &str = "crates-io-admins";
12+
13+
#[derive(Serialize, Deserialize)]
14+
pub struct SyncAdmins;
15+
16+
impl BackgroundJob for SyncAdmins {
17+
const JOB_NAME: &'static str = "sync_admins";
18+
19+
type Context = Arc<Environment>;
20+
21+
async fn run(&self, ctx: Self::Context) -> anyhow::Result<()> {
22+
info!("Syncing admins from rust-lang/team repo…");
23+
24+
let repo_admins = ctx.team_repo.get_team(TEAM_NAME).await?.members;
25+
let repo_admin_ids = repo_admins
26+
.iter()
27+
.map(|m| m.github_id)
28+
.collect::<HashSet<_>>();
29+
30+
spawn_blocking::<_, _, anyhow::Error>(move || {
31+
let mut conn = ctx.connection_pool.get()?;
32+
33+
let database_admins = users::table
34+
.select((users::gh_id, users::gh_login))
35+
.filter(users::is_admin.eq(true))
36+
.get_results::<(i32, String)>(&mut conn)?;
37+
38+
let database_admin_ids = database_admins
39+
.iter()
40+
.map(|(gh_id, _)| *gh_id)
41+
.collect::<HashSet<_>>();
42+
43+
let new_admin_ids = repo_admin_ids
44+
.difference(&database_admin_ids)
45+
.collect::<HashSet<_>>();
46+
47+
if new_admin_ids.is_empty() {
48+
debug!("No new admins to add");
49+
} else {
50+
let new_admins = repo_admins
51+
.iter()
52+
.filter(|m| new_admin_ids.contains(&&m.github_id))
53+
.map(|m| format!("{} (github_id: {})", m.github, m.github_id))
54+
.collect::<Vec<_>>()
55+
.join(", ");
56+
57+
info!("Adding new admins: {}", new_admins);
58+
59+
diesel::update(users::table)
60+
.filter(users::gh_id.eq_any(new_admin_ids))
61+
.set(users::is_admin.eq(true))
62+
.execute(&mut conn)?;
63+
}
64+
65+
let obsolete_admin_ids = database_admin_ids
66+
.difference(&repo_admin_ids)
67+
.collect::<HashSet<_>>();
68+
69+
if obsolete_admin_ids.is_empty() {
70+
debug!("No obsolete admins to remove");
71+
} else {
72+
let obsolete_admins = database_admins
73+
.iter()
74+
.filter(|(gh_id, _)| obsolete_admin_ids.contains(&gh_id))
75+
.map(|(gh_id, login)| format!("{} (github_id: {})", login, gh_id))
76+
.collect::<Vec<_>>()
77+
.join(", ");
78+
79+
info!("Removing obsolete admins: {}", obsolete_admins);
80+
81+
diesel::update(users::table)
82+
.filter(users::gh_id.eq_any(obsolete_admin_ids))
83+
.set(users::is_admin.eq(false))
84+
.execute(&mut conn)?;
85+
}
86+
87+
Ok(())
88+
})
89+
.await?;
90+
91+
Ok(())
92+
}
93+
}

src/worker/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ impl RunnerExt for Runner<Arc<Environment>> {
2525
.register_job_type::<jobs::NormalizeIndex>()
2626
.register_job_type::<jobs::RenderAndUploadReadme>()
2727
.register_job_type::<jobs::SquashIndex>()
28+
.register_job_type::<jobs::SyncAdmins>()
2829
.register_job_type::<jobs::SyncToGitIndex>()
2930
.register_job_type::<jobs::SyncToSparseIndex>()
3031
.register_job_type::<jobs::UpdateDownloads>()

0 commit comments

Comments
 (0)