-
Notifications
You must be signed in to change notification settings - Fork 14
Implement interoperation test API #375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
c48f135 to
a3793a7
Compare
a3793a7 to
740078e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really nice; I like the container definitions. Have you done any (possibly manual/one-off) testing to make sure the networking etc works out for these containers to allow a realistic test to be done?
I was working on containerizing the functionality in monolithic_integration_test; I'm probably going to refactor that work to use these binaries instead, and it'd be fantastic if I could run them in a container. (Unfortunately, I think until Daphne provides an implementation of the test API too, we'll need a separate interface for the monolithic_integration_test tests, even if Daphne itself is containerized. I guess the cleanest way to implement that is to phrase the monolithic_integration_test functionality in terms of these official-test-API containers, so at least the bulk of the functionality is only written once.)
| ), | ||
| (1, _) => ( | ||
| Role::Helper, | ||
| // TODO(issue #370): Task::new() requires that we have a collector authentication |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix in this PR?
janus/janus_server/src/task.rs
Lines 235 to 237 in 2940c07
| if collector_auth_tokens.is_empty() { | |
| return Err(Error::InvalidParameter("collector_auth_tokens")); | |
| } |
| .default_value("8080") | ||
| .help("Port number to listen on."), | ||
| ) | ||
| .arg( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we remove this flag entirely and assume 5432? The fact that Postgres exists is an implementation detail of the container, right?
If we want to keep this flag for e.g. out-of-container local testing, make this a full DB connection string?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is for the end_to_end test. I'll make that change.
| { | ||
| let task_id_bytes = base64::decode_config(request.task_id, URL_SAFE_NO_PAD) | ||
| .context("Invalid base64url content in \"taskId\"")?; | ||
| let task_id = TaskId::get_decoded(&task_id_bytes).context("Invalid length of TaskId")?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| let task_id = TaskId::get_decoded(&task_id_bytes).context("Invalid length of TaskId")?; | |
| let task_id = TaskId::get_decoded(&task_id_bytes).context("invalid length of TaskId")?; |
lowercase errors (throughout)
| // We use std::process instead of tokio::process so that we can kill the child processes from | ||
| // a Drop implementation. tokio::process::Child::kill() is async, and could not be called from | ||
| // there. | ||
| let mut client_command = Command::new(env!("CARGO_BIN_EXE_janus_interop_client")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do these need to be written as multiple statements? I think e.g. arg is meant to be called in a chained fashion, so this could be written as:
let client_command = Command::new(...)
.arg("--port")
.arg(format!("{}", client_port))
.stout(Stdio::piped())
.stderr(Stdio::piped())
.spawn();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The builder methods each return &mut Command, so at minimum I need the let binding, since I'm not spawning from a temporary value. Beyond that, I pair arguments here for aesthetics, and then chain the rest of the builder methods at once within the for loop.
interop_binaries/tests/end_to_end.rs
Outdated
| impl Drop for DropGuard { | ||
| fn drop(&mut self) { | ||
| let mut any_error = false; | ||
| for mut process in self.0.drain(..) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This type is complicated by having to deal with multiple Children IMO. It would be more easily stated if each DropGuard owned a single Child and implemented the desired drop logic for it.
interop_binaries/tests/end_to_end.rs
Outdated
| } | ||
|
|
||
| /// RAII guard to ensure that child processes are cleaned up during test failures. | ||
| struct DropGuard(Vec<Child>); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: creating a DropGuard could also set up forwarding of its stdout/stderr. This would simplify setup later, but also merges mostly-unrelated concerns into a single type. But you could write the process creation as a one-liner e.g.:
let my_child = DropGuard::new(Command::new(...)
.arg(...)
.stdin(...)
.etc()
.spawn());
If you take this suggestion, maybe name the type something like TestProcessChild, reflecting the more-general purpose of this type?
interop_binaries/tests/end_to_end.rs
Outdated
| "Ports selected for HTTP servers were not unique", | ||
| ); | ||
|
|
||
| // Step 1: Create and start containers. (here, we just run the binaries instead) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q: is the reason we start binaries instead of containers "container build is not automated"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, plus Cargo ensures that binary targets are up-to-date before running this integration test.
janus_core/src/hpke.rs
Outdated
| } | ||
| /// Generate a new HPKE keypair and return it as an HpkeConfig (public portion) and | ||
| /// HpkePrivateKey (private portion). | ||
| pub fn generate_hpke_config_and_private_key() -> (HpkeConfig, HpkePrivateKey) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this function should either remain gated by the test-util feature, or be generalized to allow different Kem, Kdf, Aead selections & allow the config ID to be specified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. Eventually we'll need something like this function in support of automated onboarding of tasks, but for now, is it so bad for interop_binaries to have to enable test-util on its Janus dependencies?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to avoid it, because turning on test-util there would have the effect of turning it on for every build in the workspace. This was half of the cause of our last run-in with openssl-dev build-time dependencies.
| /// be controlled by a controller retrieved from any of the clones. | ||
| #[derive(Clone, Debug)] | ||
| #[non_exhaustive] | ||
| pub struct MockClock { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think MockClock should remain guarded by the test-util feature, since it is intended for use only in tests. (What's the technical reason you'd like to drop this feature guard?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need this functionality to allow submitting reports with crafted timestamps. If I were to turn on the janus_core/test-util feature from the interop_binaries dependencies, then that would result in it being enabled for all builds throughout the workspace, and we'd be building kube from our janus_server release builds, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's unfortunate. I suppose this is another artifact of the interplay between workspaces & features/how feature unification is implemented.
I think one of the solutions at https://nickb.dev/blog/cargo-workspace-and-the-feature-unification-pitfall might help us. I think our release builds also effectively use solution #2 as well since we specify the --bin flag to cargo build (but maybe specifying -p is also required?). Are you sure our release builds would encounter this pitfall?
In any case, I don't think we need to hold this PR up over this issue, but I'd definitely like to resolve it somehow. Would you mind filing an issue? I'd like to be able to use features w/o dependency resolution changing what features are enabled on a per-package basis; hopefully one of the blog solutions will work, though I'd be happier if we didn't have to go as far as introducing sub-workspaces.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tested cargo --bin aggregator with and without -p janus_server, and the package specification is necessary to keep the feature from leaking over.
|
Yes, I rigged up a quick and dirty test of the containers themselves with bash, curl, and jq. |
janus_core/src/hpke.rs
Outdated
| } | ||
| /// Generate a new HPKE keypair and return it as an HpkeConfig (public portion) and | ||
| /// HpkePrivateKey (private portion). | ||
| pub fn generate_hpke_config_and_private_key() -> (HpkeConfig, HpkePrivateKey) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. Eventually we'll need something like this function in support of automated onboarding of tasks, but for now, is it so bad for interop_binaries to have to enable test-util on its Janus dependencies?
| use warp::{hyper::StatusCode, reply::Response, Filter, Reply}; | ||
|
|
||
| #[derive(Debug, Deserialize)] | ||
| struct EndpointRequest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're going to ignore every field of the request, then why parse it at all? Just to detect if the test runner is sending invalid messages?
| .context("Invalid base64url content in \"taskId\"")?; | ||
| let task_id = TaskId::get_decoded(&task_id_bytes).context("Invalid length of TaskId")?; | ||
| let leader_url = Url::parse(&request.leader).context("Bad leader URL")?; | ||
| let helper_url = Url::parse(&request.helper).context("Bad helper URL")?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you opt into the serde feature on url, then url::Url is Serialize/Deserialize and so you can have leader: Url, helper: Url in UploadRequest.
| use tokio::sync::Mutex; | ||
| use warp::{hyper::StatusCode, reply::Response, Filter, Reply}; | ||
|
|
||
| static DAP_AUTH_TOKEN: &str = "DAP-Auth-Token"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's janus_server::task::DAP_AUTH_HEADER for this
| Number(u64), | ||
| NumberArray(Vec<u64>), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is u64 big enough? Prio3Aes128Sum uses Field128, so in the end, the result type is u128. Making this generic over prio::vdaf::Vdaf would be kind of a pain but since this struct is just used for serializing to JSON, maybe using u128 instead of u64 suffices.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I specified that the result should be encoded as a JSON number, and serde_json's support for u128 is off by default, and has some warts (due to the limitations of JSON). I'll open an issue on the document to consider changing result types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JS numbers are only safe up to 53 bits, so even u64 is not necessarily safe.
The most common solution to this I've seen is to roundtrip numbers to JSON by encoding/decoding to a JSON string representing the number, i.e. 12345 might be encoded into JSON as "12345".
I was hoping that serde might have something as easy as tagging the field, but it looks like implementing this would require some custom code today: serde-rs/json#329 & serde-rs/json#833 have examples.
In any case, I think using strings instead of numbers would (probably?) need a spec update.
| .into_iter() | ||
| .map(|counter| { | ||
| u64::try_from(counter).context( | ||
| "entry in aggregate result was too large to represent natively in JSON", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JSON numbers are floating point, which should be big enough to represent u128. I'm not sure if u128 is big enough that loss of precision can occur, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JSON numbers can only "safely" represent integers up to about 53 bits[1]. If we care to fix this (maybe not for now, since this is test code & can simply arrange to not use such large numbers), the standard method I've seen is to represent the integer as a JSON string like "12345".
| @@ -0,0 +1,503 @@ | |||
| use anyhow::Context; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should consider factoring some of this logic out into a standalone collector library that handles making collect requests, polling collect URIs and combining aggregate shares into aggregate results. We could distribute it in janus_client. Not needed in this PR, but a future enhancement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, but maybe it should go in a separate crate, to keep janus_client slim for WASM users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, it would be good to have a janus_collector implementation eventually (we've got several partial implementations sitting in several different tests, now), but IMO it should go into a separate crate from janus_client.
| }; | ||
|
|
||
| let (hpke_config, private_key) = generate_hpke_config_and_private_key( | ||
| HpkeConfigId::from(0u8), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Teleporting over from a comment I made on the collector code... This works fine if we assume that HPKE config IDs are unique per-task, but I think that won't be true anymore once ietf-wg-ppm/draft-ietf-ppm-dap#289 lands in DAP.
tgeoghegan
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more nits but I think this is a sound implementation of https://divergentdave.github.io/draft-dcook-ppm-dap-interop-test-design/draft-dcook-ppm-dap-interop-test-design.html
interop_binaries/tests/end_to_end.rs
Outdated
| fn drop(&mut self) { | ||
| if self.0.try_wait().unwrap().is_none() { | ||
| self.0.kill().unwrap(); | ||
| self.0.wait().unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the call to Child::wait is unnecessary after Child::kill. The docs aren't really clear, but empirically I've found that no zombie is left behind after calling Child::kill, and the sentence "This is equivalent to sending a SIGKILL on Unix platforms." implies you don't need to wait.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, that way we rely on init to clean up the process once the test exits.
interop_binaries/tests/end_to_end.rs
Outdated
| .header(CONTENT_TYPE, JSON_MEDIA_TYPE) | ||
| .json(&json!({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| .header(CONTENT_TYPE, JSON_MEDIA_TYPE) | |
| .json(&json!({ | |
| .json(&json!({ |
RequestBuilder::json automatically sets Content-Type: application/json so I think quite a few calls to RequestBuilder::header can be deleted.
| @@ -0,0 +1,503 @@ | |||
| use anyhow::Context; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, it would be good to have a janus_collector implementation eventually (we've got several partial implementations sitting in several different tests, now), but IMO it should go into a separate crate from janus_client.
| Number(u64), | ||
| NumberArray(Vec<u64>), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JS numbers are only safe up to 53 bits, so even u64 is not necessarily safe.
The most common solution to this I've seen is to roundtrip numbers to JSON by encoding/decoding to a JSON string representing the number, i.e. 12345 might be encoded into JSON as "12345".
I was hoping that serde might have something as easy as tagging the field, but it looks like implementing this would require some custom code today: serde-rs/json#329 & serde-rs/json#833 have examples.
In any case, I think using strings instead of numbers would (probably?) need a spec update.
interop_binaries/tests/end_to_end.rs
Outdated
|
|
||
| impl Drop for ChildProcessCleanupDropGuard { | ||
| fn drop(&mut self) { | ||
| if self.0.try_wait().unwrap().is_none() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is racy: if the child process has not exited when the try_wait() call occurs, but exits before the call to kill(), the unwrap() on the return value to kill() will panic since kill() will return Err(InvalidInput).
I think the intent is to kill the process if it's still running. If that's accurate, I recommend calling kill() unconditionally and ignoring the error if it is InvalidInput.
interop_binaries/tests/end_to_end.rs
Outdated
|
|
||
| /// Pass output from a child process's stderr pipe to eprint!(), so that it can be captured and | ||
| /// stored by the test harness. | ||
| async fn forward_stderr(stdout: ChildStderr) -> io::Result<()> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| async fn forward_stderr(stdout: ChildStderr) -> io::Result<()> { | |
| async fn forward_stderr(stderr: ChildStderr) -> io::Result<()> { |
(mega-nit)
| /// be controlled by a controller retrieved from any of the clones. | ||
| #[derive(Clone, Debug)] | ||
| #[non_exhaustive] | ||
| pub struct MockClock { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's unfortunate. I suppose this is another artifact of the interplay between workspaces & features/how feature unification is implemented.
I think one of the solutions at https://nickb.dev/blog/cargo-workspace-and-the-feature-unification-pitfall might help us. I think our release builds also effectively use solution #2 as well since we specify the --bin flag to cargo build (but maybe specifying -p is also required?). Are you sure our release builds would encounter this pitfall?
In any case, I don't think we need to hold this PR up over this issue, but I'd definitely like to resolve it somehow. Would you mind filing an issue? I'd like to be able to use features w/o dependency resolution changing what features are enabled on a per-package basis; hopefully one of the blog solutions will work, though I'd be happier if we didn't have to go as far as introducing sub-workspaces.
This adds three new binaries (and container images) to support draft-dcook-ppm-dap-interop-test-design. It includes an end-to-end test that exercises the binaries using the test APIs.
The following items are made public or moved out from behind "test-util" features to support this:
janus_server::aggregator::aggregator_filter()janus_core::hpke::generate_hpke_config_and_private_key()janus_core::time::MockClock