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
This commit is contained in:
Marshall Bowers authored and GitHub committed 2026-05-08 13:34:46 +00:00
1 parent 45362c24f1
commit 463beda9ca
7 files changed
+196 -3

No files matched your search

@@ -1,4 +1,5 @@
mod extension;
pub mod internal_api;
mod known_or_unknown;
mod plan;
mod timestamp;
@@ -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<String>,
pub admin: bool,
pub connected_once: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LookUpUsersByLegacyIdBody {
pub legacy_user_ids: Vec<i32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LookUpUsersByLegacyIdResponse {
pub users: Vec<User>,
}
+1
View File
@@ -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"
+5
View File
@@ -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:
+19 -3
View File
@@ -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<String>,
pub kinesis_secret_key: Option<String>,
pub zed_environment: Arc<str>,
pub zed_cloud_internal_api_key: String,
pub zed_client_checksum_seed: Option<String>,
}
@@ -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))
+147
View File
@@ -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<UserId>) -> Result<Vec<User>> {
self.cloud_user_service.get_users_by_ids(ids).await
}
async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
self.database_user_service
.get_user_by_github_login(github_login)
.await
}
async fn fuzzy_search_users(&self, query: &str, limit: u32) -> Result<Vec<User>> {
self.database_user_service
.fuzzy_search_users(query, limit)
.await
}
async fn search_channel_members(
&self,
channel: &Channel,
query: &str,
limit: u32,
) -> Result<(Vec<proto::ChannelMember>, Vec<User>)> {
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<UserId>) -> Result<Vec<User>> {
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<Option<User>> {
let _ = github_login;
unimplemented!("not yet implemented in Cloud")
}
async fn fuzzy_search_users(&self, query: &str, limit: u32) -> Result<Vec<User>> {
let _ = query;
let _ = limit;
unimplemented!("not yet implemented in Cloud")
}
async fn search_channel_members(
&self,
channel: &Channel,
query: &str,
limit: u32,
) -> Result<(Vec<proto::ChannelMember>, Vec<User>)> {
let _ = channel;
let _ = query;
let _ = limit;
unimplemented!("not yet implemented in Cloud")
}
}
impl From<internal_api::User> 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<Database>,
@@ -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,