Skip to content

Commit c0b074c

Browse files
committed
feat(aggregator-discovery): add 'HttpConfigAggregatorDiscoverer'
Implementation for 'AggregatorDiscoverer' trait.
1 parent 65f6a90 commit c0b074c

File tree

6 files changed

+215
-0
lines changed

6 files changed

+215
-0
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/mithril-aggregator-discovery/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,23 @@ crate-type = ["lib", "cdylib", "staticlib"]
1717
anyhow = { workspace = true }
1818
async-trait = { workspace = true }
1919
mithril-common = { path = "../../mithril-common" }
20+
reqwest = { workspace = true, features = [
21+
"default",
22+
"gzip",
23+
"zstd",
24+
"deflate",
25+
"brotli"
26+
] }
2027
serde = { workspace = true }
28+
serde_json = { workspace = true }
2129
slog = { workspace = true }
2230
slog-scope = "4.4.0"
2331
thiserror = { workspace = true }
2432
tokio = { workspace = true, features = ["sync"] }
2533

2634
[dev-dependencies]
2735
mockall = { workspace = true }
36+
httpmock = "0.8.1"
2837
slog-async = { workspace = true }
2938
slog-term = { workspace = true }
3039
tokio = { workspace = true, features = ["macros"] }
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
use std::collections::HashMap;
2+
3+
use anyhow::Context;
4+
use reqwest::Client;
5+
use serde::{Deserialize, Serialize};
6+
7+
use mithril_common::StdResult;
8+
9+
use crate::{AggregatorDiscoverer, AggregatorEndpoint, MithrilNetwork};
10+
11+
const DEFAULT_REMOTE_NETWORKS_CONFIG_URL: &str =
12+
"https://raw.githubusercontent.com/input-output-hk/mithril/main/networks.json";
13+
14+
/// Representation of the networks configuration file.
15+
#[derive(Debug, Clone, Serialize, Deserialize)]
16+
struct NetworksConfigMessage {
17+
#[serde(flatten)]
18+
pub networks: HashMap<String, NetworkEnvironmentMessage>,
19+
}
20+
21+
/// Representation of a network environment in the networks configuration file.
22+
#[derive(Debug, Clone, Serialize, Deserialize)]
23+
struct NetworkEnvironmentMessage {
24+
#[serde(rename = "mithril-networks")]
25+
pub mithril_networks: Vec<HashMap<String, MithrilNetworkMessage>>,
26+
}
27+
28+
/// Representation of a Mithril network in the networks configuration file.
29+
#[derive(Debug, Clone, Serialize, Deserialize)]
30+
struct MithrilNetworkMessage {
31+
pub aggregators: Vec<AggregatorMessage>,
32+
}
33+
34+
/// Representation of an aggregator in the networks configuration file.
35+
#[derive(Debug, Clone, Serialize, Deserialize)]
36+
struct AggregatorMessage {
37+
pub url: String,
38+
}
39+
40+
/// An implementation of the [AggregatorDiscoverer] trait which discovers aggregators from remote networks configuration.
41+
///
42+
/// The reference file is the `networks.json` file hosted in the Mithril GitHub repository.
43+
pub struct HttpConfigAggregatorDiscoverer {
44+
configuration_file_url: String,
45+
}
46+
47+
impl HttpConfigAggregatorDiscoverer {
48+
/// Creates a new `HttpConfigAggregatorDiscoverer` instance with the provided results.
49+
pub fn new(configuration_file_url: &str) -> Self {
50+
Self {
51+
configuration_file_url: configuration_file_url.to_string(),
52+
}
53+
}
54+
55+
/// Builds a reqwest HTTP client.
56+
fn build_client(&self) -> StdResult<Client> {
57+
let client_builder = Client::builder();
58+
let client = client_builder.build()?;
59+
60+
Ok(client)
61+
}
62+
}
63+
64+
impl Default for HttpConfigAggregatorDiscoverer {
65+
fn default() -> Self {
66+
Self::new(DEFAULT_REMOTE_NETWORKS_CONFIG_URL)
67+
}
68+
}
69+
70+
#[async_trait::async_trait]
71+
impl AggregatorDiscoverer for HttpConfigAggregatorDiscoverer {
72+
async fn get_available_aggregators(
73+
&self,
74+
network: MithrilNetwork,
75+
) -> StdResult<Vec<AggregatorEndpoint>> {
76+
let client = self.build_client()?;
77+
let networks_configuration_response = client
78+
.get(&self.configuration_file_url)
79+
.send()
80+
.await
81+
.with_context(|| {
82+
format!(
83+
"AggregatorDiscovererHttpConfig failed retrieving configuration file from {}",
84+
&self.configuration_file_url
85+
)
86+
})?
87+
.json::<NetworksConfigMessage>()
88+
.await
89+
.with_context(|| {
90+
format!(
91+
"AggregatorDiscovererHttpConfig failed parsing configuration file from {}",
92+
&self.configuration_file_url
93+
)
94+
})?;
95+
96+
Ok(networks_configuration_response
97+
.networks
98+
.values()
99+
.flat_map(|env| &env.mithril_networks)
100+
.flat_map(|network_map| network_map.iter())
101+
.filter(|(name, _)| *name == network.name())
102+
.flat_map(|(_, network)| &network.aggregators)
103+
.map(|aggregator_msg| AggregatorEndpoint::new(aggregator_msg.url.clone()))
104+
.collect())
105+
}
106+
}
107+
108+
#[cfg(test)]
109+
mod tests {
110+
use httpmock::MockServer;
111+
112+
use super::*;
113+
114+
const TEST_NETWORKS_CONFIG_JSON_SUCCESS: &str = r#"
115+
{
116+
"devnet": {
117+
"mithril-networks": [
118+
{
119+
"release-devnet": {
120+
"aggregators": [
121+
{ "url": "https://release-devnet-aggregator1" },
122+
{ "url": "https://release-devnet-aggregator2" }
123+
]
124+
}
125+
}
126+
]
127+
},
128+
"testnet": {
129+
"mithril-networks": [
130+
{
131+
"preview-testnet": {
132+
"aggregators": [
133+
{ "url": "https://preview-testnet-aggregator1" },
134+
{ "url": "https://preview-testnet-aggregator2" }
135+
]
136+
}
137+
}
138+
]
139+
}
140+
}"#;
141+
142+
const TEST_NETWORKS_CONFIG_JSON_FAILURE: &str = r#"
143+
{
144+
{"}
145+
}"#;
146+
147+
fn create_server_and_discoverer(content: &str) -> (MockServer, HttpConfigAggregatorDiscoverer) {
148+
let size = content.len() as u64;
149+
let server = MockServer::start();
150+
server.mock(|when, then| {
151+
when.method(httpmock::Method::GET).path("/networks.json");
152+
then.status(200)
153+
.body(content)
154+
.header(reqwest::header::CONTENT_LENGTH.as_str(), size.to_string());
155+
});
156+
let configuration_file_url = format!("{}{}", server.url("/"), "networks.json");
157+
let discoverer = HttpConfigAggregatorDiscoverer::new(&configuration_file_url);
158+
159+
(server, discoverer)
160+
}
161+
162+
#[tokio::test]
163+
async fn get_available_aggregators_success() {
164+
let content = TEST_NETWORKS_CONFIG_JSON_SUCCESS;
165+
let (_server, discoverer) = create_server_and_discoverer(content);
166+
let aggregators = discoverer
167+
.get_available_aggregators(MithrilNetwork::new("release-devnet".into()))
168+
.await
169+
.unwrap();
170+
171+
assert_eq!(
172+
vec![
173+
AggregatorEndpoint::new("https://release-devnet-aggregator1".into()),
174+
AggregatorEndpoint::new("https://release-devnet-aggregator2".into()),
175+
],
176+
aggregators
177+
);
178+
179+
let aggregators = discoverer
180+
.get_available_aggregators(MithrilNetwork::new("unknown".into()))
181+
.await
182+
.unwrap();
183+
184+
assert!(aggregators.is_empty());
185+
}
186+
187+
#[tokio::test]
188+
async fn get_available_aggregators_failure() {
189+
let content = TEST_NETWORKS_CONFIG_JSON_FAILURE;
190+
let (_server, discoverer) = create_server_and_discoverer(content);
191+
discoverer
192+
.get_available_aggregators(MithrilNetwork::new("release-devnet".into()))
193+
.await
194+
.expect_err("The retrieval of the aggregators should fail");
195+
}
196+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
#![warn(missing_docs)]
22
//! This crate provides mechanisms to discover aggregators in a Mithril network.
33
4+
mod http_config;
45
mod interface;
56
mod model;
67
pub mod test;
78

9+
pub use http_config_discoverer::HttpConfigAggregatorDiscoverer;
810
pub use interface::AggregatorDiscoverer;
911
pub use model::{AggregatorEndpoint, MithrilNetwork};

internal/mithril-aggregator-discovery/src/model.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ impl MithrilNetwork {
1515
pub fn dummy() -> Self {
1616
Self("dummy".to_string())
1717
}
18+
19+
/// Retrieve the name of the Mithril network
20+
pub fn name(&self) -> &str {
21+
&self.0
22+
}
1823
}
1924

2025
/// Representation of an aggregator endpoint

mithril-client/src/aggregator_discovery.rs

Whitespace-only changes.

0 commit comments

Comments
 (0)