Add basic tap integration tests (#154)

Signed-off-by: Sean McArthur <sean@buoyant.io>
This commit is contained in:
Sean McArthur 2018-12-04 18:37:26 -08:00 committed by GitHub
parent 68f42c337f
commit 3ac6b72c48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 345 additions and 16 deletions

View File

@ -11,12 +11,15 @@ use self::tokio::{
net::TcpStream,
io::{AsyncRead, AsyncWrite},
};
use support::hyper::body::Payload;
type Request = http::Request<Bytes>;
type Response = http::Response<BodyStream>;
type BodyStream = Box<Stream<Item=Bytes, Error=String> + Send>;
type Response = http::Response<BytesBody>;
type Sender = mpsc::UnboundedSender<(Request, oneshot::Sender<Result<Response, String>>)>;
#[derive(Debug)]
pub struct BytesBody(hyper::Body);
pub fn new<T: Into<String>>(addr: SocketAddr, auth: T) -> Client {
http2(addr, auth.into())
}
@ -84,9 +87,7 @@ impl Client {
}
pub fn request_async(&self, builder: &mut http::request::Builder) -> Box<Future<Item=Response, Error=String> + Send> {
let (tx, rx) = oneshot::channel();
let _ = self.tx.unbounded_send((builder.body(Bytes::new()).unwrap(), tx));
Box::new(rx.then(|oneshot_result| oneshot_result.expect("request canceled")))
self.send_req(builder.body(Bytes::new()).unwrap())
}
pub fn request(&self, builder: &mut http::request::Builder) -> Response {
@ -96,14 +97,15 @@ impl Client {
}
pub fn request_body(&self, req: Request) -> Response {
let (tx, rx) = oneshot::channel();
let _ = self.tx.unbounded_send((req, tx));
rx
.then(|oneshot_result| oneshot_result.expect("request canceled"))
self.send_req(req)
.wait()
.expect("response")
}
pub fn request_body_async(&self, req: Request) -> Box<Future<Item=Response, Error=String> + Send> {
self.send_req(req)
}
pub fn request_builder(&self, path: &str) -> http::request::Builder {
let mut b = ::http::Request::builder();
b.uri(format!("http://{}{}", self.authority, path).as_str())
@ -111,6 +113,16 @@ impl Client {
b
}
fn send_req(&self, mut req: Request) -> Box<Future<Item=Response, Error=String> + Send> {
if req.uri().scheme_part().is_none() {
let absolute = format!("http://{}{}", self.authority, req.uri().path()).parse().unwrap();
*req.uri_mut() = absolute;
}
let (tx, rx) = oneshot::channel();
let _ = self.tx.unbounded_send((req, tx));
Box::new(rx.then(|oneshot_result| oneshot_result.expect("request canceled")))
}
pub fn wait_for_closed(self) {
self.running
.wait()
@ -159,13 +171,7 @@ fn run(addr: SocketAddr, version: Run) -> (Sender, Running) {
let req = req.map(hyper::Body::from);
let fut = client.request(req).then(move |result| {
let result = result
.map(|res| {
let res = http::Response::from(res);
res.map(|body| -> BodyStream {
Box::new(body.map(|chunk| chunk.into())
.map_err(|e| e.to_string()))
})
})
.map(|resp| resp.map(BytesBody))
.map_err(|e| e.to_string());
let _ = cb.send(result);
Ok(())
@ -263,3 +269,25 @@ impl AsyncWrite for RunningIo {
}
}
impl BytesBody {
pub fn poll_data(&mut self) -> Poll<Option<Bytes>, hyper::Error> {
match try_ready!(self.0.poll_data()) {
Some(chunk) => Ok(Async::Ready(Some(chunk.into()))),
None => Ok(Async::Ready(None)),
}
}
pub fn poll_trailers(&mut self) -> Poll<Option<http::HeaderMap>, hyper::Error> {
self.0.poll_trailers()
}
}
impl Stream for BytesBody {
type Item = Bytes;
type Error = hyper::Error;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
self.poll_data()
}
}

View File

@ -148,6 +148,7 @@ pub mod client;
pub mod controller;
pub mod proxy;
pub mod server;
pub mod tap;
pub mod tcp;
pub fn shutdown_signal() -> (Shutdown, ShutdownRx) {

217
tests/support/tap.rs Normal file
View File

@ -0,0 +1,217 @@
use support::*;
use bytes::BytesMut;
use linkerd2_proxy_api::tap as pb;
pub fn client(addr: SocketAddr) -> Client {
let api = pb::client::Tap::new(SyncSvc(client::http2(addr, "localhost")));
Client {
api,
}
}
pub struct Client {
api: pb::client::Tap<SyncSvc>,
}
impl Client {
pub fn observe(&mut self, req: ObserveBuilder) -> impl Stream<Item = pb::TapEvent, Error = tower_grpc::Error> {
let req = tower_grpc::Request::new(req.0);
self.api.observe(req)
.wait()
.expect("tap observe wait")
.into_inner()
}
}
pub fn observe_request() -> ObserveBuilder {
ObserveBuilder(pb::ObserveRequest {
limit: 100,
match_: Some(pb::observe_request::Match {
match_: Some(pb::observe_request::match_::Match::Http(
pb::observe_request::match_::Http {
match_: Some(pb::observe_request::match_::http::Match::Path(
pb::observe_request::match_::http::StringMatch {
match_: Some(pb::observe_request::match_::http::string_match::Match::Prefix(
"/".to_string()
)),
},
)),
},
)),
}),
})
}
#[derive(Debug)]
pub struct ObserveBuilder(pb::ObserveRequest);
impl ObserveBuilder {
pub fn limit(mut self, limit: u32) -> Self {
self.0.limit = limit;
self
}
pub fn ports(mut self, min: u16, max: u16) -> Self {
self.0.match_ = Some(pb::observe_request::Match {
match_: Some(
pb::observe_request::match_::Match::Destination(
pb::observe_request::match_::Tcp {
match_: Some(pb::observe_request::match_::tcp::Match::Ports(
pb::observe_request::match_::tcp::PortRange {
min: min.into(),
max: max.into(),
},
)),
},
),
),
});
self
}
}
pub trait TapEventExt {
fn is_inbound(&self) -> bool;
fn is_outbound(&self) -> bool;
//fn id(&self) -> (u32, u64);
fn event(&self) -> &pb::tap_event::http::Event;
fn request_init_method(&self) -> String;
fn request_init_authority(&self) -> &str;
fn request_init_path(&self) -> &str;
fn response_init_status(&self) -> u16;
fn response_end_bytes(&self) -> u64;
fn response_end_eos_grpc(&self) -> u32;
}
impl TapEventExt for pb::TapEvent {
fn is_inbound(&self) -> bool {
self.proxy_direction == pb::tap_event::ProxyDirection::Inbound as i32
}
fn is_outbound(&self) -> bool {
self.proxy_direction == pb::tap_event::ProxyDirection::Outbound as i32
}
fn event(&self) -> &pb::tap_event::http::Event {
match self.event {
Some(
pb::tap_event::Event::Http(
pb::tap_event::Http {
event: Some(ref ev),
}
)
) => ev,
_ => panic!("unknown event: {:?}", self.event),
}
}
fn request_init_method(&self) -> String {
match self.event() {
pb::tap_event::http::Event::RequestInit(_ev) => {
//TODO: ugh
unimplemented!("method");
},
_ => panic!("not RequestInit event"),
}
}
fn request_init_authority(&self) -> &str {
match self.event() {
pb::tap_event::http::Event::RequestInit(ev) => {
&ev.authority
},
_ => panic!("not RequestInit event"),
}
}
fn request_init_path(&self) -> &str {
match self.event() {
pb::tap_event::http::Event::RequestInit(ev) => {
&ev.path
},
_ => panic!("not RequestInit event"),
}
}
fn response_init_status(&self) -> u16 {
match self.event() {
pb::tap_event::http::Event::ResponseInit(ev) => {
ev.http_status as u16
},
_ => panic!("not ResponseInit event"),
}
}
fn response_end_bytes(&self) -> u64 {
match self.event() {
pb::tap_event::http::Event::ResponseEnd(ev) => {
ev.response_bytes
},
_ => panic!("not ResponseEnd event"),
}
}
fn response_end_eos_grpc(&self) -> u32 {
match self.event() {
pb::tap_event::http::Event::ResponseEnd(ev) => {
match ev.eos {
Some(pb::Eos {
end: Some(pb::eos::End::GrpcStatusCode(code)),
}) => code,
_ => panic!("not Eos GrpcStatusCode: {:?}", ev.eos),
}
},
_ => panic!("not ResponseEnd event"),
}
}
}
struct SyncSvc(client::Client);
impl<B> tower_service::Service<http::Request<B>> for SyncSvc
where
B: tower_grpc::Body<Data = Bytes>,
{
type Response = http::Response<GrpcBody>;
type Error = String;
type Future = Box<Future<Item = Self::Response, Error = Self::Error> + Send>;
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
unreachable!("tap SyncSvc poll_ready");
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
let req = req.map(|mut body| {
let mut buf = BytesMut::new();
while let Some(bytes) = future::poll_fn(|| body.poll_data()).wait().expect("req body") {
buf.extend_from_slice(&bytes);
}
buf.freeze()
});
Box::new(self.0.request_body_async(req)
.map(|res| res.map(GrpcBody)))
}
}
struct GrpcBody(client::BytesBody);
impl tower_grpc::Body for GrpcBody {
type Data = Bytes;
fn poll_data(&mut self) -> Poll<Option<Self::Data>, tower_grpc::Error> {
self.0.poll_data().map_err(|err| {
unimplemented!("grpc poll_data error: {}", err)
})
}
fn poll_metadata(&mut self) -> Poll<Option<http::HeaderMap>, tower_grpc::Error> {
self.0.poll_trailers().map_err(|err| {
unimplemented!("grpc poll_trailers error: {}", err)
})
}
}

83
tests/tap.rs Normal file
View File

@ -0,0 +1,83 @@
#![deny(warnings)]
#[macro_use]
mod support;
use self::support::*;
use support::tap::TapEventExt;
// Flaky: sometimes the admin thread hasn't had a chance to register
// the Taps before the `client.get` is called.
#[test]
#[cfg_attr(not(feature = "flaky_tests"), ignore)]
fn inbound_http1() {
let _ = env_logger_init();
let srv = server::http1()
.route("/", "hello")
.run();
let proxy = proxy::new()
.inbound(srv)
.run();
let mut tap = tap::client(proxy.control);
let events = tap.observe(
tap::observe_request()
);
let authority = "tap.test.svc.cluster.local";
let client = client::http1(proxy.inbound, authority);
assert_eq!(client.get("/"), "hello");
let mut events = events.wait().take(3);
let ev1 = events.next().expect("next1").expect("stream1");
assert!(ev1.is_inbound());
assert_eq!(ev1.request_init_authority(), authority);
assert_eq!(ev1.request_init_path(), "/");
let ev2 = events.next().expect("next2").expect("stream2");
assert!(ev2.is_inbound());
assert_eq!(ev2.response_init_status(), 200);
let ev3 = events.next().expect("next3").expect("stream3");
assert!(ev3.is_inbound());
assert_eq!(ev3.response_end_bytes(), 5);
}
#[test]
#[cfg_attr(not(feature = "flaky_tests"), ignore)]
fn grpc_headers_end() {
let _ = env_logger_init();
let srv = server::http2()
.route_fn("/", |_req| {
Response::builder()
.header("grpc-status", "1")
.body(Default::default())
.unwrap()
})
.run();
let proxy = proxy::new()
.inbound(srv)
.run();
let mut tap = tap::client(proxy.control);
let events = tap.observe(
tap::observe_request()
);
let authority = "tap.test.svc.cluster.local";
let client = client::http2(proxy.inbound, authority);
let res = client.request(client
.request_builder("/")
.header("content-type", "application/grpc+nope")
);
assert_eq!(res.status(), 200);
assert_eq!(res.headers()["grpc-status"], "1");
assert_eq!(res.into_body().concat2().wait().unwrap().len(), 0);
let ev = events.wait().nth(2).expect("nth").expect("stream");
assert_eq!(ev.response_end_eos_grpc(), 1);
}