Rework db-seeding, so that it doesn't depend on a github auth token

Max Brunsfeld created

Instead, admins are specified using a JSON file, 'admins.json'. This file is
gitignored. If it is not present, there is a default list of admins in
'admins.default.json'.

Change summary

.gitignore                            |   1 
crates/collab/.admins.default.json    |   1 
crates/collab/src/bin/seed.rs         | 121 +++++++++++++---------------
crates/collab/src/db/queries/users.rs |   6 +
script/seed-db                        |   4 
script/zed-local                      |  39 +++++----
6 files changed, 84 insertions(+), 88 deletions(-)

Detailed changes

.gitignore 🔗

@@ -9,6 +9,7 @@
 /styles/src/types/zed.ts
 /crates/theme/schemas/theme.json
 /crates/collab/static/styles.css
+/crates/collab/.admins.json
 /vendor/bin
 /assets/themes/*.json
 /assets/*licenses.md

crates/collab/src/bin/seed.rs 🔗

@@ -1,7 +1,11 @@
-use collab::{db, executor::Executor};
+use collab::{
+    db::{self, NewUserParams},
+    env::load_dotenv,
+    executor::Executor,
+};
 use db::{ConnectOptions, Database};
 use serde::{de::DeserializeOwned, Deserialize};
-use std::fmt::Write;
+use std::{fmt::Write, fs};
 
 #[derive(Debug, Deserialize)]
 struct GitHubUser {
@@ -12,90 +16,75 @@ struct GitHubUser {
 
 #[tokio::main]
 async fn main() {
+    load_dotenv().expect("failed to load .env.toml file");
+
+    let mut admin_logins =
+        load_admins("./.admins.default.json").expect("failed to load default admins file");
+    if let Ok(other_admins) = load_admins("./.admins.json") {
+        admin_logins.extend(other_admins);
+    }
+
     let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var");
     let db = Database::new(ConnectOptions::new(database_url), Executor::Production)
         .await
         .expect("failed to connect to postgres database");
-    let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
     let client = reqwest::Client::new();
 
-    let mut current_user =
-        fetch_github::<GitHubUser>(&client, &github_token, "https://api.github.com/user").await;
-    current_user
-        .email
-        .get_or_insert_with(|| "placeholder@example.com".to_string());
-    let staff_users = fetch_github::<Vec<GitHubUser>>(
-        &client,
-        &github_token,
-        "https://api.github.com/orgs/zed-industries/teams/staff/members",
-    )
-    .await;
-
-    let mut zed_users = Vec::new();
-    zed_users.push((current_user, true));
-    zed_users.extend(staff_users.into_iter().map(|user| (user, true)));
+    // Create admin users for all of the users in `.admins.toml` or `.admins.default.toml`.
+    for admin_login in admin_logins {
+        let user = fetch_github::<GitHubUser>(
+            &client,
+            &format!("https://api.github.com/users/{admin_login}"),
+        )
+        .await;
+        db.create_user(
+            &user.email.unwrap_or(format!("{admin_login}@example.com")),
+            true,
+            NewUserParams {
+                github_login: user.login,
+                github_user_id: user.id,
+            },
+        )
+        .await
+        .expect("failed to create admin user");
+    }
 
-    let user_count = db
+    // Fetch 100 other random users from GitHub and insert them into the database.
+    let mut user_count = db
         .get_all_users(0, 200)
         .await
         .expect("failed to load users from db")
         .len();
-    if user_count < 100 {
-        let mut last_user_id = None;
-        for _ in 0..10 {
-            let mut uri = "https://api.github.com/users?per_page=100".to_string();
-            if let Some(last_user_id) = last_user_id {
-                write!(&mut uri, "&since={}", last_user_id).unwrap();
-            }
-            let users = fetch_github::<Vec<GitHubUser>>(&client, &github_token, &uri).await;
-            if let Some(last_user) = users.last() {
-                last_user_id = Some(last_user.id);
-                zed_users.extend(users.into_iter().map(|user| (user, false)));
-            } else {
-                break;
-            }
+    let mut last_user_id = None;
+    while user_count < 100 {
+        let mut uri = "https://api.github.com/users?per_page=100".to_string();
+        if let Some(last_user_id) = last_user_id {
+            write!(&mut uri, "&since={}", last_user_id).unwrap();
         }
-    }
+        let users = fetch_github::<Vec<GitHubUser>>(&client, &uri).await;
 
-    for (github_user, admin) in zed_users {
-        if db
-            .get_user_by_github_login(&github_user.login)
+        for github_user in users {
+            last_user_id = Some(github_user.id);
+            user_count += 1;
+            db.get_or_create_user_by_github_account(
+                &github_user.login,
+                Some(github_user.id),
+                github_user.email.as_deref(),
+            )
             .await
-            .expect("failed to fetch user")
-            .is_none()
-        {
-            if admin {
-                db.create_user(
-                    &format!("{}@zed.dev", github_user.login),
-                    admin,
-                    db::NewUserParams {
-                        github_login: github_user.login,
-                        github_user_id: github_user.id,
-                    },
-                )
-                .await
-                .expect("failed to insert user");
-            } else {
-                db.get_or_create_user_by_github_account(
-                    &github_user.login,
-                    Some(github_user.id),
-                    github_user.email.as_deref(),
-                )
-                .await
-                .expect("failed to insert user");
-            }
+            .expect("failed to insert user");
         }
     }
 }
 
-async fn fetch_github<T: DeserializeOwned>(
-    client: &reqwest::Client,
-    access_token: &str,
-    url: &str,
-) -> T {
+fn load_admins(path: &str) -> anyhow::Result<Vec<String>> {
+    let file_content = fs::read_to_string(path)?;
+    Ok(serde_json::from_str(&file_content)?)
+}
+
+async fn fetch_github<T: DeserializeOwned>(client: &reqwest::Client, url: &str) -> T {
     let response = client
         .get(url)
-        .bearer_auth(&access_token)
         .header("user-agent", "zed")
         .send()
         .await

crates/collab/src/db/queries/users.rs 🔗

@@ -20,7 +20,11 @@ impl Database {
             })
             .on_conflict(
                 OnConflict::column(user::Column::GithubLogin)
-                    .update_column(user::Column::GithubLogin)
+                    .update_columns([
+                        user::Column::Admin,
+                        user::Column::EmailAddress,
+                        user::Column::GithubUserId,
+                    ])
                     .to_owned(),
             )
             .exec_with_returning(&*tx)

script/seed-db 🔗

@@ -2,8 +2,4 @@
 set -e
 
 cd crates/collab
-
-# Export contents of .env.toml
-eval "$(cargo run --quiet --bin dotenv)"
-
 cargo run --quiet --package=collab --features seed-support --bin seed -- $@

script/zed-local 🔗

@@ -4,6 +4,11 @@ const HELP = `
 USAGE
   zed-local  [options]  [zed args]
 
+SUMMARY
+  Runs 1-4 instances of Zed using a locally-running collaboration server.
+  Each instance of Zed will be signed in as a different user specified in
+  either \`.admins.json\` or \`.admins.default.json\`.
+
 OPTIONS
   --help        Print this help message
   --release     Build Zed in release mode
@@ -12,6 +17,16 @@ OPTIONS
 `.trim();
 
 const { spawn, execFileSync } = require("child_process");
+const assert = require("assert");
+
+const defaultUsers = require("../crates/collab/.admins.default.json");
+let users = defaultUsers;
+try {
+  const customUsers = require("../crates/collab/.admins.json");
+  assert(customUsers.length > 0);
+  assert(customUsers.every((user) => typeof user === "string"));
+  users.splice(0, 0, ...customUsers);
+} catch (_) {}
 
 const RESOLUTION_REGEX = /(\d+) x (\d+)/;
 const DIGIT_FLAG_REGEX = /^--?(\d+)$/;
@@ -71,10 +86,6 @@ if (instanceCount > 1) {
   }
 }
 
-let users = ["nathansobo", "as-cii", "maxbrunsfeld", "iamnbutler"];
-
-const RUST_LOG = process.env.RUST_LOG || "info";
-
 // If a user is specified, make sure it's first in the list
 const user = process.env.ZED_IMPERSONATE;
 if (user) {
@@ -88,18 +99,12 @@ const positions = [
   `${instanceWidth},${instanceHeight}`,
 ];
 
-const buildArgs = (() => {
-  const buildArgs = ["build"];
-  if (isReleaseMode) {
-    buildArgs.push("--release");
-  }
-
-  return buildArgs;
-})();
-const zedBinary = (() => {
-  const target = isReleaseMode ? "release" : "debug";
-  return `target/${target}/Zed`;
-})();
+let buildArgs = ["build"];
+let zedBinary = "target/debug/Zed";
+if (isReleaseMode) {
+  buildArgs.push("--release");
+  zedBinary = "target/release/Zed";
+}
 
 execFileSync("cargo", buildArgs, { stdio: "inherit" });
 setTimeout(() => {
@@ -115,7 +120,7 @@ setTimeout(() => {
         ZED_ADMIN_API_TOKEN: "secret",
         ZED_WINDOW_SIZE: `${instanceWidth},${instanceHeight}`,
         PATH: process.env.PATH,
-        RUST_LOG,
+        RUST_LOG: process.env.RUST_LOG || "info",
       },
     });
   }