From 4b23564f36230c88549ec7aa7ca369eec797b997 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 8 May 2026 09:34:46 -0400 Subject: [PATCH] collab: Route `get_users_by_ids` through Cloud (#56105) This PR makes it so we route the `UserService::get_users_by_ids` call through Cloud instead of hitting the database. We've introduced a new `CloudUserService` that will fetch the users from Cloud using the internal API. Note that we've only implemented the `get_users_by_ids` method on this service, as the endpoints for the other methods don't yet exist. We have also introduced a `TransitionalUserService` for the purposes of gradually transitioning these calls over to Cloud. Right now it uses the `CloudUserService` for the `get_users_by_ids` implementation, but then uses the `DatabaseUserService` for the other methods. Closes CLO-740. Release Notes: - N/A --- crates/cloud_api_types/src/cloud_api_types.rs | 1 + crates/cloud_api_types/src/internal_api.rs | 22 +++ crates/collab/.env.toml | 1 + crates/collab/k8s/collab.template.yml | 5 + crates/collab/src/lib.rs | 22 ++- crates/collab/src/services/user_service.rs | 147 ++++++++++++++++++ .../collab/tests/integration/test_server.rs | 1 + 7 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 crates/cloud_api_types/src/internal_api.rs diff --git a/crates/cloud_api_types/src/cloud_api_types.rs b/crates/cloud_api_types/src/cloud_api_types.rs index 439ed5b2e822382aebcc7dfc18f5887d7a389038..67836cc5e56878074ee2c0d5bfa9fba129715302 100644 --- a/crates/cloud_api_types/src/cloud_api_types.rs +++ b/crates/cloud_api_types/src/cloud_api_types.rs @@ -1,4 +1,5 @@ mod extension; +pub mod internal_api; mod known_or_unknown; mod plan; mod timestamp; diff --git a/crates/cloud_api_types/src/internal_api.rs b/crates/cloud_api_types/src/internal_api.rs new file mode 100644 index 0000000000000000000000000000000000000000..954dcdad42036f7af0e1e7c833580a5ac0ef7163 --- /dev/null +++ b/crates/cloud_api_types/src/internal_api.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct User { + pub id: String, + pub legacy_user_id: i32, + pub github_login: String, + pub github_user_id: i32, + pub name: Option, + pub admin: bool, + pub connected_once: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct LookUpUsersByLegacyIdBody { + pub legacy_user_ids: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct LookUpUsersByLegacyIdResponse { + pub users: Vec, +} diff --git a/crates/collab/.env.toml b/crates/collab/.env.toml index 1d20a14e2d5fd2d461bb4c9af93321cf7f73d402..fc0d899822d735b0548c326d18f392cdfebdc8da 100644 --- a/crates/collab/.env.toml +++ b/crates/collab/.env.toml @@ -3,6 +3,7 @@ DATABASE_URL = "postgres://postgres@localhost/zed" DATABASE_MAX_CONNECTIONS = 5 HTTP_PORT = 8080 ZED_ENVIRONMENT = "development" +ZED_CLOUD_INTERNAL_API_KEY = "internal-api-key-secret" LIVEKIT_SERVER = "http://localhost:7880" LIVEKIT_KEY = "devkey" LIVEKIT_SECRET = "secret" diff --git a/crates/collab/k8s/collab.template.yml b/crates/collab/k8s/collab.template.yml index f2c2c6ebb487ac90a30132979c60a70f2099569a..d37840218c2cef9a6b3c4e7cd49417c36b89410a 100644 --- a/crates/collab/k8s/collab.template.yml +++ b/crates/collab/k8s/collab.template.yml @@ -92,6 +92,11 @@ spec: secretKeyRef: name: zed-client key: checksum-seed + - name: ZED_CLOUD_INTERNAL_API_KEY + valueFrom: + secretKeyRef: + name: zed-cloud + key: internal-api-key - name: LIVEKIT_SERVER valueFrom: secretKeyRef: diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index 041e7461725bc8193a270b0c13b58abb37620235..d1948d15749d1407c50063c0c31e1e3779829442 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -20,7 +20,9 @@ use serde::Deserialize; use std::{path::PathBuf, sync::Arc}; use util::ResultExt; -use crate::services::{DatabaseUserService, UserService}; +use crate::services::{ + CloudUserService, DatabaseUserService, TransitionalUserService, UserService, +}; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub const REVISION: Option<&'static str> = option_env!("GITHUB_SHA"); @@ -139,6 +141,7 @@ pub struct Config { pub kinesis_access_key: Option, pub kinesis_secret_key: Option, pub zed_environment: Arc, + pub zed_cloud_internal_api_key: String, pub zed_client_checksum_seed: Option, } @@ -176,6 +179,7 @@ impl Config { rust_log: None, log_json: None, zed_environment: "test".into(), + zed_cloud_internal_api_key: "test-internal-api-key".into(), blob_store_url: None, blob_store_region: None, blob_store_access_key: None, @@ -252,7 +256,7 @@ impl AppState { let db = Arc::new(db); let this = Self { db: db.clone(), - http_client: Some(http_client), + http_client: Some(http_client.clone()), livekit_client, blob_store_client: build_blob_store_client(&config).await.log_err(), executor, @@ -261,7 +265,19 @@ impl AppState { } else { None }, - user_service: Arc::new(DatabaseUserService::new(db)), + user_service: { + let database_user_service = DatabaseUserService::new(db); + let cloud_user_service = CloudUserService::new( + http_client, + config.zed_cloud_url().to_string(), + config.zed_cloud_internal_api_key.clone(), + ); + + Arc::new(TransitionalUserService::new( + cloud_user_service, + database_user_service, + )) + }, config, }; Ok(Arc::new(this)) diff --git a/crates/collab/src/services/user_service.rs b/crates/collab/src/services/user_service.rs index e2696e99ff2610e49d603648a26416029ae50ee2..f2fb968b231de22e0742ce7c038724e56b432239 100644 --- a/crates/collab/src/services/user_service.rs +++ b/crates/collab/src/services/user_service.rs @@ -1,6 +1,10 @@ use std::sync::Arc; +use anyhow::{Context as _, anyhow}; use async_trait::async_trait; +use cloud_api_types::internal_api::{ + self, LookUpUsersByLegacyIdBody, LookUpUsersByLegacyIdResponse, +}; use rpc::proto; use crate::Result; @@ -36,6 +40,149 @@ pub trait UserService: Send + Sync + 'static { } } +/// A [`UserService`] implementation for transitioning from reading from the database to reading from Cloud. +pub struct TransitionalUserService { + cloud_user_service: CloudUserService, + database_user_service: DatabaseUserService, +} + +impl TransitionalUserService { + pub fn new( + cloud_user_service: CloudUserService, + database_user_service: DatabaseUserService, + ) -> Self { + Self { + cloud_user_service, + database_user_service, + } + } +} + +#[async_trait] +impl UserService for TransitionalUserService { + async fn get_users_by_ids(&self, ids: Vec) -> Result> { + self.cloud_user_service.get_users_by_ids(ids).await + } + + async fn get_user_by_github_login(&self, github_login: &str) -> Result> { + self.database_user_service + .get_user_by_github_login(github_login) + .await + } + + async fn fuzzy_search_users(&self, query: &str, limit: u32) -> Result> { + self.database_user_service + .fuzzy_search_users(query, limit) + .await + } + + async fn search_channel_members( + &self, + channel: &Channel, + query: &str, + limit: u32, + ) -> Result<(Vec, Vec)> { + self.database_user_service + .search_channel_members(channel, query, limit) + .await + } +} + +/// A [`UserService`] implementation backed by Cloud. +pub struct CloudUserService { + http_client: reqwest::Client, + zed_cloud_url: String, + internal_api_key: String, +} + +impl CloudUserService { + pub fn new( + http_client: reqwest::Client, + zed_cloud_url: String, + internal_api_key: String, + ) -> Self { + Self { + http_client, + zed_cloud_url, + internal_api_key, + } + } +} + +#[async_trait] +impl UserService for CloudUserService { + async fn get_users_by_ids(&self, ids: Vec) -> Result> { + let response = self + .http_client + .post(format!( + "{}/internal/users/look_up_by_legacy_id", + &self.zed_cloud_url + )) + .header("Content-Type", "application/json") + .header( + "Authorization", + format!("Bearer {}", &self.internal_api_key), + ) + .json(&LookUpUsersByLegacyIdBody { + legacy_user_ids: ids.into_iter().map(|id| id.0).collect(), + }) + .send() + .await + .context("failed to get users by legacy IDs")?; + + match response.error_for_status() { + Ok(response) => { + let response_body: LookUpUsersByLegacyIdResponse = response + .json() + .await + .context("failed to parse response body")?; + + Ok(response_body.users.into_iter().map(User::from).collect()) + } + Err(_err) => Err(anyhow!("failed to get users by legacy IDs"))?, + } + } + + async fn get_user_by_github_login(&self, github_login: &str) -> Result> { + let _ = github_login; + + unimplemented!("not yet implemented in Cloud") + } + + async fn fuzzy_search_users(&self, query: &str, limit: u32) -> Result> { + let _ = query; + let _ = limit; + + unimplemented!("not yet implemented in Cloud") + } + + async fn search_channel_members( + &self, + channel: &Channel, + query: &str, + limit: u32, + ) -> Result<(Vec, Vec)> { + let _ = channel; + let _ = query; + let _ = limit; + + unimplemented!("not yet implemented in Cloud") + } +} + +impl From for User { + fn from(user: internal_api::User) -> Self { + Self { + id: UserId(user.legacy_user_id), + github_login: user.github_login, + github_user_id: user.github_user_id, + name: user.name, + admin: user.admin, + connected_once: user.connected_once, + } + } +} + /// A [`UserService`] implementation backed by the database. pub struct DatabaseUserService { database: Arc, diff --git a/crates/collab/tests/integration/test_server.rs b/crates/collab/tests/integration/test_server.rs index 9bfddbca754d28fef5ada27247c7d43cb6fc3ddc..d177f63ef8eb0593aa57e2507340c8753a7127ea 100644 --- a/crates/collab/tests/integration/test_server.rs +++ b/crates/collab/tests/integration/test_server.rs @@ -592,6 +592,7 @@ impl TestServer { rust_log: None, log_json: None, zed_environment: "test".into(), + zed_cloud_internal_api_key: "test-internal-api-key".into(), blob_store_url: None, blob_store_region: None, blob_store_access_key: None,