From af4af4cd785e1e30b06a92a57e8198f8afb7d313 Mon Sep 17 00:00:00 2001 From: Jan Mazur Date: Wed, 9 Oct 2024 14:42:09 -0700 Subject: [PATCH] initial git over slapi endpoints behind /slapigit Summary: We want to be able to distinguish if we're using git over edenapi or regular edenapi, but reuse all the code we can at the same time. This puts the info which flavour of the protocol we speak in the `State` which later on handlers can read if they want to. We can also straight away block or allow handlers. Reviewed By: lmvasquezg Differential Revision: D63995106 fbshipit-source-id: 95006537bd30bf71b575af39f9e90bbf3e592d40 --- eden/mononoke/edenapi_service/src/handlers.rs | 53 +++++++++++++++---- .../edenapi_service/src/handlers/handler.rs | 12 +++++ eden/mononoke/gotham_ext/src/handler.rs | 45 +++++++++++++--- eden/mononoke/gotham_ext/src/serve.rs | 9 +++- .../server/repo_listener/src/http_service.rs | 33 +++++++++--- .../tests/integration/edenapi/test-slapigit.t | 26 +++++++++ 6 files changed, 152 insertions(+), 26 deletions(-) create mode 100644 eden/mononoke/tests/integration/edenapi/test-slapigit.t diff --git a/eden/mononoke/edenapi_service/src/handlers.rs b/eden/mononoke/edenapi_service/src/handlers.rs index 092a467d5de6a..b3d186c364a57 100644 --- a/eden/mononoke/edenapi_service/src/handlers.rs +++ b/eden/mononoke/edenapi_service/src/handlers.rs @@ -10,6 +10,7 @@ use std::pin::Pin; use std::time::Duration; use std::time::Instant; +use anyhow::anyhow; use anyhow::Context; use anyhow::Error; use edenapi_types::ToWire; @@ -33,6 +34,8 @@ use gotham::state::State; use gotham_derive::StateData; use gotham_ext::content_encoding::ContentEncoding; use gotham_ext::error::ErrorFormatter; +use gotham_ext::error::HttpError; +use gotham_ext::handler::SlapiCommitIdentityScheme; use gotham_ext::middleware::load::RequestLoad; use gotham_ext::middleware::request_context::RequestContext; use gotham_ext::middleware::scuba::HttpScubaKey; @@ -231,24 +234,45 @@ impl ErrorFormatter for JsonErrorFomatter { /// fn wrapped(mut state: State) -> Pin> /// ``` macro_rules! define_handler { - ($name:ident, $func:path) => { + ($name:ident, $func:path, [$($flavour:ident),*]) => { fn $name(mut state: State) -> Pin> { async move { - let (future_stats, res) = $func(&mut state).timed().await; - ScubaMiddlewareState::try_set_future_stats(&mut state, &future_stats); + let slapi_flavour = SlapiCommitIdentityScheme::borrow_from(&state).clone(); + let supported_flavours = [$(SlapiCommitIdentityScheme::$flavour),*]; + let res = if !supported_flavours + .iter() + .any(|x| *x == slapi_flavour) + { + Err(HttpError::e400(anyhow!( + "Unsupported SaplingRemoteApi flavour" + ))) + } else { + let (future_stats, res) = $func(&mut state).timed().await; + ScubaMiddlewareState::try_set_future_stats(&mut state, &future_stats); + res + }; build_response(res, state, &JsonErrorFomatter) + } .boxed() } }; } -define_handler!(capabilities_handler, capabilities::capabilities_handler); -define_handler!(commit_hash_to_location_handler, commit::hash_to_location); -define_handler!(commit_revlog_data_handler, commit::revlog_data); -define_handler!(repos_handler, repos::repos); -define_handler!(trees_handler, trees::trees); -define_handler!(upload_file_handler, files::upload_file); +define_handler!( + capabilities_handler, + capabilities::capabilities_handler, + [Hg] +); +define_handler!( + commit_hash_to_location_handler, + commit::hash_to_location, + [Hg] +); +define_handler!(commit_revlog_data_handler, commit::revlog_data, [Hg]); +define_handler!(repos_handler, repos::repos, [Hg]); +define_handler!(trees_handler, trees::trees, [Hg]); +define_handler!(upload_file_handler, files::upload_file, [Hg]); static HIGH_LOAD_SIGNAL: &str = "I_AM_OVERLOADED"; static ALIVE: &str = "I_AM_ALIVE"; @@ -291,6 +315,15 @@ where let query = Handler::QueryStringExtractor::take_from(&mut state); let content_encoding = ContentEncoding::from_state(&state); + let slapi_flavour = SlapiCommitIdentityScheme::borrow_from(&state).clone(); + if !Handler::SUPPORTED_FLAVOURS + .iter() + .any(|x| *x == slapi_flavour) + { + return Err(gotham_ext::error::HttpError::e400(anyhow!( + "Unsupported SaplingRemoteApi flavour" + ))); + } state.put(HandlerInfo::new(path.repo(), Handler::API_METHOD)); let rctx = RequestContext::borrow_from(&state).clone(); @@ -308,7 +341,7 @@ where rd.add_request(&request); } - let ectx = SaplingRemoteApiContext::new(rctx, sctx, repo, path, query); + let ectx = SaplingRemoteApiContext::new(rctx, sctx, repo, path, query, slapi_flavour); match Handler::handler(ectx, request).await { Ok(responses) => Ok(encode_response_stream( diff --git a/eden/mononoke/edenapi_service/src/handlers/handler.rs b/eden/mononoke/edenapi_service/src/handlers/handler.rs index f64b0f4dc71d6..5fbdf6b297c4f 100644 --- a/eden/mononoke/edenapi_service/src/handlers/handler.rs +++ b/eden/mononoke/edenapi_service/src/handlers/handler.rs @@ -15,6 +15,7 @@ use gotham::extractor::QueryStringExtractor; use gotham_derive::StateData; use gotham_derive::StaticResponseExtender; use gotham_ext::error::HttpError; +use gotham_ext::handler::SlapiCommitIdentityScheme; use gotham_ext::middleware::request_context::RequestContext; use hyper::body::Body; use mononoke_api::MononokeError; @@ -80,6 +81,7 @@ pub struct SaplingRemoteApiContext { repo: HgRepoContext, path: P, query: Q, + slapi_flavour: SlapiCommitIdentityScheme, } impl SaplingRemoteApiContext { @@ -89,6 +91,7 @@ impl SaplingRemoteApiContext { repo: HgRepoContext, path: P, query: Q, + slapi_flavour: SlapiCommitIdentityScheme, ) -> Self { Self { rctx, @@ -96,6 +99,7 @@ impl SaplingRemoteApiContext { repo, path, query, + slapi_flavour, } } pub fn repo(&self) -> HgRepoContext @@ -105,6 +109,11 @@ impl SaplingRemoteApiContext { self.repo.clone() } + #[allow(unused)] + pub fn slapi_flavour(&self) -> SlapiCommitIdentityScheme { + self.slapi_flavour + } + #[allow(unused)] pub fn path(&self) -> &P { &self.path @@ -140,6 +149,9 @@ pub trait SaplingRemoteApiHandler: 'static { /// Example: "/ephemeral/prepare" const ENDPOINT: &'static str; + const SUPPORTED_FLAVOURS: &'static [SlapiCommitIdentityScheme] = + &[SlapiCommitIdentityScheme::Hg]; + fn sampling_rate(_request: &Self::Request) -> NonZeroU64 { nonzero!(1u64) } diff --git a/eden/mononoke/gotham_ext/src/handler.rs b/eden/mononoke/gotham_ext/src/handler.rs index 04aa92c791fe9..d807eec56e618 100644 --- a/eden/mononoke/gotham_ext/src/handler.rs +++ b/eden/mononoke/gotham_ext/src/handler.rs @@ -18,6 +18,7 @@ use gotham::handler::HandlerFuture; use gotham::handler::IntoResponse; use gotham::handler::NewHandler; use gotham::state::State; +use gotham_derive::StateData; use hyper::service::Service; use hyper::Body; use hyper::Request; @@ -26,6 +27,12 @@ use hyper::Response; use crate::middleware::Middleware; use crate::socket_data::TlsSocketData; +#[derive(StateData, Clone, PartialEq, Copy, Debug)] +pub enum SlapiCommitIdentityScheme { + Hg, + Git, +} + #[derive(Clone)] pub struct MononokeHttpHandler { inner: H, @@ -33,15 +40,30 @@ pub struct MononokeHttpHandler { } impl MononokeHttpHandler { - pub fn into_service( + pub fn into_service( self, addr: SocketAddr, tls_socket_data: Option, - ) -> MononokeHttpHandlerAsService { + ) -> MononokeHttpHandlerAsService { MononokeHttpHandlerAsService { handler: self, addr, tls_socket_data, + state: None, + } + } + + pub fn into_service_with_state( + self, + addr: SocketAddr, + tls_socket_data: Option, + state: T, + ) -> MononokeHttpHandlerAsService { + MononokeHttpHandlerAsService { + handler: self, + addr, + tls_socket_data, + state: Some(state), } } } @@ -146,19 +168,28 @@ impl MononokeHttpHandlerBuilder { /// This is an instance of MononokeHttpHandlerAsService that is connected to a client. We can use /// it to call into Gotham explicitly, or use it as a Hyper service. #[derive(Clone)] -pub struct MononokeHttpHandlerAsService { +pub struct MononokeHttpHandlerAsService { handler: MononokeHttpHandler, addr: SocketAddr, tls_socket_data: Option, + state: Option, } -impl MononokeHttpHandlerAsService { +impl< + H: Handler + Clone + Send + Sync + 'static + RefUnwindSafe, + T: gotham::state::StateData + Clone, +> MononokeHttpHandlerAsService +{ pub async fn call_gotham(self, req: Request) -> Response { let mut state = State::from_request(req, self.addr); if let Some(tls_socket_data) = self.tls_socket_data { tls_socket_data.populate_state(&mut state); } + if let Some(s) = self.state { + state.put(s); + } + match self.handler.handle(state).await { Ok((_state, res)) => res, Err((state, err)) => err.into_response(&state), @@ -166,8 +197,10 @@ impl MononokeHttpHan } } -impl Service> - for MononokeHttpHandlerAsService +impl< + H: Handler + Clone + Send + Sync + 'static + RefUnwindSafe, + T: gotham::state::StateData + Clone, +> Service> for MononokeHttpHandlerAsService { type Response = Response; type Error = anyhow::Error; diff --git a/eden/mononoke/gotham_ext/src/serve.rs b/eden/mononoke/gotham_ext/src/serve.rs index f91e97ccc7e4e..e9c25f669da49 100644 --- a/eden/mononoke/gotham_ext/src/serve.rs +++ b/eden/mononoke/gotham_ext/src/serve.rs @@ -24,8 +24,12 @@ use tokio::net::TcpListener; use tokio_openssl::SslStream; use crate::handler::MononokeHttpHandler; +use crate::handler::MononokeHttpHandlerAsService; use crate::socket_data::TlsSocketData; +#[derive(gotham_derive::StateData, Clone)] +struct Empty {} + pub async fn https( logger: Logger, listener: TcpListener, @@ -66,7 +70,7 @@ where ) .await; - let service = handler + let service: MononokeHttpHandlerAsService<_, Empty> = handler .clone() .into_service(peer_addr, Some(tls_socket_data)); @@ -103,7 +107,8 @@ where cloned!(logger, handler); let task = async move { - let service = handler.clone().into_service(peer_addr, None); + let service: MononokeHttpHandlerAsService<_, Empty> = + handler.clone().into_service(peer_addr, None); let socket = QuietShutdownStream::new(socket); diff --git a/eden/mononoke/server/repo_listener/src/http_service.rs b/eden/mononoke/server/repo_listener/src/http_service.rs index 74643a9330b05..349c74d1ece2f 100644 --- a/eden/mononoke/server/repo_listener/src/http_service.rs +++ b/eden/mononoke/server/repo_listener/src/http_service.rs @@ -25,6 +25,7 @@ use clientinfo::ClientInfo; use clientinfo::CLIENT_INFO_HEADER; use futures::future::BoxFuture; use futures::future::FutureExt; +use gotham_ext::handler::SlapiCommitIdentityScheme; use gotham_ext::middleware::metadata::ingress_request_identities_from_headers; use gotham_ext::socket_data::TlsSocketData; use http::HeaderMap; @@ -197,19 +198,34 @@ where return self.handle_control_request(req.method, path).await; } - let edenapi_path_and_query = req + if let Some((flavour, path_and_query)) = req .uri .path_and_query() .as_ref() - .and_then(|pq| pq.as_str().strip_prefix("/edenapi")); - - if let Some(edenapi_path_and_query) = edenapi_path_and_query { - let pq = http::uri::PathAndQuery::from_str(edenapi_path_and_query) + .and_then(|pq| pq.as_str().strip_prefix("/")) + .and_then(|pq| pq.split_once('/')) + { + let pq = http::uri::PathAndQuery::from_str(&format!("/{}", path_and_query)) .context("Error translating SaplingRemoteAPI request path") .map_err(HttpError::internal)?; - return self.handle_eden_api_request(req, pq, body).await; + match flavour { + "edenapi" | "slapi" => { + return self + .handle_eden_api_request(req, pq, body, SlapiCommitIdentityScheme::Hg) + .await; + } + "slapigit" => { + return self + .handle_eden_api_request(req, pq, body, SlapiCommitIdentityScheme::Git) + .await; + } + _ => { + return Err(HttpError::BadRequest(anyhow!( + "Unknown SaplingRemoteAPI flavour" + ))); + } + } } - Err(HttpError::NotFound) } @@ -355,6 +371,7 @@ where mut req: http::request::Parts, pq: http::uri::PathAndQuery, body: Body, + flavour: SlapiCommitIdentityScheme, ) -> Result, HttpError> { let mut uri_parts = req.uri.into_parts(); @@ -380,7 +397,7 @@ where .acceptor() .edenapi .clone() - .into_service(self.conn.pending.addr, Some(tls_socket_data)) + .into_service_with_state(self.conn.pending.addr, Some(tls_socket_data), flavour) .call_gotham(req) .await; diff --git a/eden/mononoke/tests/integration/edenapi/test-slapigit.t b/eden/mononoke/tests/integration/edenapi/test-slapigit.t new file mode 100644 index 0000000000000..5a1dfd918cac0 --- /dev/null +++ b/eden/mononoke/tests/integration/edenapi/test-slapigit.t @@ -0,0 +1,26 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This software may be used and distributed according to the terms of the +# GNU General Public License found in the LICENSE file in the root +# directory of this source tree. + + $ . "${TEST_FIXTURES}/library.sh" + +Start up SaplingRemoteAPI server. + $ setup_mononoke_config + $ start_and_wait_for_mononoke_server +List repos. + $ sslcurl -s "https://localhost:$MONONOKE_SOCKET/slapigit/repos" + {"message":"Unsupported SaplingRemoteApi flavour","request_id":"*"} (no-eol) (glob) +Test request with a missing mandatory header + $ sslcurl_noclientinfo_test -s "https://localhost:$MONONOKE_SOCKET/slapigit/repos" + {"message:"Error: X-Client-Info header not provided or wrong format (expected json)."} (no-eol) +Test that health check request still passes + $ sslcurl_noclientinfo_test -s "https://localhost:$MONONOKE_SOCKET/edenapi/health_check" + I_AM_ALIVE (no-eol) + $ sslcurl -s "https://localhost:$MONONOKE_SOCKET/slapigit/health_check" + I_AM_ALIVE (no-eol) + $ sslcurl -X POST -s "https://localhost:$MONONOKE_SOCKET/slapigit/repo/trees" + {"message":"Unsupported SaplingRemoteApi flavour","request_id":"*"} (no-eol) (glob) + $ sslcurl -X POST -s "https://localhost:$MONONOKE_SOCKET/slapigit/repo/commit/location_to_hash" + {"message":"Unsupported SaplingRemoteApi flavour","request_id":"*"} (no-eol) (glob)