Skip to content

Commit

Permalink
initial git over slapi endpoints behind /slapigit
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mzr authored and facebook-github-bot committed Oct 9, 2024
1 parent 0312b19 commit af4af4c
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 26 deletions.
53 changes: 43 additions & 10 deletions eden/mononoke/edenapi_service/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -231,24 +234,45 @@ impl ErrorFormatter for JsonErrorFomatter {
/// fn wrapped(mut state: State) -> Pin<Box<HandlerFuture>>
/// ```
macro_rules! define_handler {
($name:ident, $func:path) => {
($name:ident, $func:path, [$($flavour:ident),*]) => {
fn $name(mut state: State) -> Pin<Box<HandlerFuture>> {
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";
Expand Down Expand Up @@ -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();
Expand All @@ -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(
Expand Down
12 changes: 12 additions & 0 deletions eden/mononoke/edenapi_service/src/handlers/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -80,6 +81,7 @@ pub struct SaplingRemoteApiContext<P, Q, R: Send + Sync + 'static> {
repo: HgRepoContext<R>,
path: P,
query: Q,
slapi_flavour: SlapiCommitIdentityScheme,
}

impl<P, Q, R: Send + Sync + 'static> SaplingRemoteApiContext<P, Q, R> {
Expand All @@ -89,13 +91,15 @@ impl<P, Q, R: Send + Sync + 'static> SaplingRemoteApiContext<P, Q, R> {
repo: HgRepoContext<R>,
path: P,
query: Q,
slapi_flavour: SlapiCommitIdentityScheme,
) -> Self {
Self {
rctx,
sctx,
repo,
path,
query,
slapi_flavour,
}
}
pub fn repo(&self) -> HgRepoContext<R>
Expand All @@ -105,6 +109,11 @@ impl<P, Q, R: Send + Sync + 'static> SaplingRemoteApiContext<P, Q, R> {
self.repo.clone()
}

#[allow(unused)]
pub fn slapi_flavour(&self) -> SlapiCommitIdentityScheme {
self.slapi_flavour
}

#[allow(unused)]
pub fn path(&self) -> &P {
&self.path
Expand Down Expand Up @@ -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)
}
Expand Down
45 changes: 39 additions & 6 deletions eden/mononoke/gotham_ext/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,22 +27,43 @@ 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<H> {
inner: H,
middleware: Arc<Vec<Box<dyn Middleware>>>,
}

impl<H> MononokeHttpHandler<H> {
pub fn into_service(
pub fn into_service<T>(
self,
addr: SocketAddr,
tls_socket_data: Option<TlsSocketData>,
) -> MononokeHttpHandlerAsService<H> {
) -> MononokeHttpHandlerAsService<H, T> {
MononokeHttpHandlerAsService {
handler: self,
addr,
tls_socket_data,
state: None,
}
}

pub fn into_service_with_state<T: gotham::state::StateData>(
self,
addr: SocketAddr,
tls_socket_data: Option<TlsSocketData>,
state: T,
) -> MononokeHttpHandlerAsService<H, T> {
MononokeHttpHandlerAsService {
handler: self,
addr,
tls_socket_data,
state: Some(state),
}
}
}
Expand Down Expand Up @@ -146,28 +168,39 @@ 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<H> {
pub struct MononokeHttpHandlerAsService<H, T> {
handler: MononokeHttpHandler<H>,
addr: SocketAddr,
tls_socket_data: Option<TlsSocketData>,
state: Option<T>,
}

impl<H: Handler + Clone + Send + Sync + 'static + RefUnwindSafe> MononokeHttpHandlerAsService<H> {
impl<
H: Handler + Clone + Send + Sync + 'static + RefUnwindSafe,
T: gotham::state::StateData + Clone,
> MononokeHttpHandlerAsService<H, T>
{
pub async fn call_gotham(self, req: Request<Body>) -> Response<Body> {
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),
}
}
}

impl<H: Handler + Clone + Send + Sync + 'static + RefUnwindSafe> Service<Request<Body>>
for MononokeHttpHandlerAsService<H>
impl<
H: Handler + Clone + Send + Sync + 'static + RefUnwindSafe,
T: gotham::state::StateData + Clone,
> Service<Request<Body>> for MononokeHttpHandlerAsService<H, T>
{
type Response = Response<Body>;
type Error = anyhow::Error;
Expand Down
9 changes: 7 additions & 2 deletions eden/mononoke/gotham_ext/src/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<H>(
logger: Logger,
listener: TcpListener,
Expand Down Expand Up @@ -66,7 +70,7 @@ where
)
.await;

let service = handler
let service: MononokeHttpHandlerAsService<_, Empty> = handler
.clone()
.into_service(peer_addr, Some(tls_socket_data));

Expand Down Expand Up @@ -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);

Expand Down
33 changes: 25 additions & 8 deletions eden/mononoke/server/repo_listener/src/http_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -355,6 +371,7 @@ where
mut req: http::request::Parts,
pq: http::uri::PathAndQuery,
body: Body,
flavour: SlapiCommitIdentityScheme,
) -> Result<Response<Body>, HttpError> {
let mut uri_parts = req.uri.into_parts();

Expand All @@ -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;

Expand Down
26 changes: 26 additions & 0 deletions eden/mononoke/tests/integration/edenapi/test-slapigit.t
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit af4af4c

Please sign in to comment.