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;
Marshall Bowers created
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 ++++++++++++++++++++
crates/collab/tests/integration/test_server.rs | 1
7 files changed, 196 insertions(+), 3 deletions(-)
@@ -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>,
+}
@@ -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"
@@ -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:
@@ -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))
@@ -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,