Expose extensions API from api.zed.dev (#8307)

Conrad Irwin and Marshall created

This avoids the need to pay for bandwidth

Co-Authored-By: Marshall <marshall@zed.dev>



Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>

Change summary

crates/collab/src/api.rs                |  3 +--
crates/collab/src/main.rs               | 21 ++++++++++++++++-----
crates/extension/src/extension_store.rs | 25 ++++++++++---------------
3 files changed, 27 insertions(+), 22 deletions(-)

Detailed changes

crates/collab/src/api.rs 🔗

@@ -1,5 +1,5 @@
 pub mod events;
-mod extensions;
+pub mod extensions;
 
 use crate::{
     auth,
@@ -33,7 +33,6 @@ pub fn routes(rpc_server: Option<Arc<rpc::Server>>, state: Arc<AppState>) -> Rou
         .route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
         .route("/contributors", get(get_contributors).post(add_contributor))
         .route("/contributor", get(check_is_contributor))
-        .merge(extensions::router())
         .layer(
             ServiceBuilder::new()
                 .layer(Extension(state))

crates/collab/src/main.rs 🔗

@@ -40,7 +40,16 @@ async fn main() -> Result<()> {
             run_migrations().await?;
         }
         Some("serve") => {
-            let is_api_only = args.next().is_some_and(|arg| arg == "api");
+            let (is_api, is_collab) = if let Some(next) = args.next() {
+                (next == "api", next == "collab")
+            } else {
+                (true, true)
+            };
+            if !is_api && !is_collab {
+                Err(anyhow!(
+                    "usage: collab <version | migrate | serve [api|collab]>"
+                ))?;
+            }
 
             let config = envy::from_env::<Config>().expect("error loading config");
             init_tracing(&config);
@@ -52,7 +61,7 @@ async fn main() -> Result<()> {
             let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port))
                 .expect("failed to bind TCP listener");
 
-            let rpc_server = if !is_api_only {
+            let rpc_server = if is_collab {
                 let epoch = state
                     .db
                     .create_server(&state.config.zed_environment)
@@ -66,8 +75,7 @@ async fn main() -> Result<()> {
                 None
             };
 
-            // TODO: Once we move the extensions endpoints to run inside `api` service, move the background task as well.
-            if !is_api_only {
+            if is_api {
                 fetch_extensions_from_blob_store_periodically(state.clone(), Executor::Production);
             }
 
@@ -80,6 +88,7 @@ async fn main() -> Result<()> {
                     Router::new()
                         .route("/", get(handle_root))
                         .route("/healthz", get(handle_liveness_probe))
+                        .merge(collab::api::extensions::router())
                         .merge(collab::api::events::router())
                         .layer(Extension(state.clone())),
                 )
@@ -120,7 +129,9 @@ async fn main() -> Result<()> {
                 .await?;
         }
         _ => {
-            Err(anyhow!("usage: collab <version | migrate | serve>"))?;
+            Err(anyhow!(
+                "usage: collab <version | migrate | serve [api|collab]>"
+            ))?;
         }
     }
     Ok(())

crates/extension/src/extension_store.rs 🔗

@@ -1,7 +1,6 @@
 use anyhow::{anyhow, bail, Context as _, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
-use client::ClientSettings;
 use collections::{BTreeMap, HashSet};
 use fs::{Fs, RemoveOptions};
 use futures::channel::mpsc::unbounded;
@@ -13,7 +12,6 @@ use language::{
 };
 use parking_lot::RwLock;
 use serde::{Deserialize, Serialize};
-use settings::Settings as _;
 use std::cmp::Ordering;
 use std::{
     ffi::OsStr,
@@ -22,7 +20,7 @@ use std::{
     time::Duration,
 };
 use theme::{ThemeRegistry, ThemeSettings};
-use util::http::AsyncBody;
+use util::http::{AsyncBody, ZedHttpClient};
 use util::TryFutureExt;
 use util::{http::HttpClient, paths::EXTENSIONS_DIR, ResultExt};
 
@@ -57,7 +55,7 @@ pub enum ExtensionStatus {
 pub struct ExtensionStore {
     manifest: Arc<RwLock<Manifest>>,
     fs: Arc<dyn Fs>,
-    http_client: Arc<dyn HttpClient>,
+    http_client: Arc<ZedHttpClient>,
     extensions_dir: PathBuf,
     extensions_being_installed: HashSet<Arc<str>>,
     extensions_being_uninstalled: HashSet<Arc<str>>,
@@ -113,7 +111,7 @@ actions!(zed, [ReloadExtensions]);
 
 pub fn init(
     fs: Arc<fs::RealFs>,
-    http_client: Arc<dyn HttpClient>,
+    http_client: Arc<ZedHttpClient>,
     language_registry: Arc<LanguageRegistry>,
     theme_registry: Arc<ThemeRegistry>,
     cx: &mut AppContext,
@@ -145,7 +143,7 @@ impl ExtensionStore {
     pub fn new(
         extensions_dir: PathBuf,
         fs: Arc<dyn Fs>,
-        http_client: Arc<dyn HttpClient>,
+        http_client: Arc<ZedHttpClient>,
         language_registry: Arc<LanguageRegistry>,
         theme_registry: Arc<ThemeRegistry>,
         cx: &mut ModelContext<Self>,
@@ -224,14 +222,12 @@ impl ExtensionStore {
         search: Option<&str>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<Extension>>> {
-        let url = format!(
-            "{}/{}{query}",
-            ClientSettings::get_global(cx).server_url,
-            "api/extensions",
+        let url = self.http_client.zed_api_url(&format!(
+            "/extensions{query}",
             query = search
                 .map(|search| format!("?filter={search}"))
                 .unwrap_or_default()
-        );
+        ));
         let http_client = self.http_client.clone();
         cx.spawn(move |_, _| async move {
             let mut response = http_client.get(&url, AsyncBody::empty(), true).await?;
@@ -264,10 +260,9 @@ impl ExtensionStore {
         cx: &mut ModelContext<Self>,
     ) {
         log::info!("installing extension {extension_id} {version}");
-        let url = format!(
-            "{}/api/extensions/{extension_id}/{version}/download",
-            ClientSettings::get_global(cx).server_url
-        );
+        let url = self
+            .http_client
+            .zed_api_url(&format!("/extensions/{extension_id}/{version}/download"));
 
         let extensions_dir = self.extensions_dir();
         let http_client = self.http_client.clone();