Skip to content

Commit 810535b

Browse files
authored
fix: Add custom deserializer to handle APOLLO_UPLINK_ENDPOINTS environment variable parsing (#220)
* fix: Add custom deserializer to handle APOLLO_UPLINK_ENDPOINTS environment variable parsing * Add changeset * Add an error message to url decoding * Change how we map the error * rustfmt * Use insta snapshot test * Only import for test
1 parent a2286e5 commit 810535b

File tree

3 files changed

+169
-1
lines changed

3 files changed

+169
-1
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
### fix: Add custom deserializer to handle APOLLO_UPLINK_ENDPOINTS environment variable parsing - @swcollard PR #220
2+
3+
The APOLLO_UPLINK_ENDPOINTS environment variables has historically been a comma separated list of URL strings.
4+
The move to yaml configuration allows us to more directly define the endpoints as a Vec.
5+
This fix introduces a custom deserializer for the `apollo_uplink_endpoints` config field that can handle both the environment variable comma separated string, and the yaml-based list.

crates/apollo-mcp-server/src/runtime.rs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,136 @@ mod test {
124124
Ok(())
125125
});
126126
}
127+
128+
#[test]
129+
fn it_merges_env_and_file_with_uplink_endpoints() {
130+
let config = "
131+
endpoint: http://from_file:4000/
132+
";
133+
134+
figment::Jail::expect_with(move |jail| {
135+
let path = "config.yaml";
136+
137+
jail.create_file(path, config)?;
138+
jail.set_env(
139+
"APOLLO_UPLINK_ENDPOINTS",
140+
"http://from_env:4000/,http://from_env2:4000/",
141+
);
142+
143+
let config = read_config(path)?;
144+
145+
insta::assert_debug_snapshot!(config, @r#"
146+
Config {
147+
custom_scalars: None,
148+
endpoint: Endpoint(
149+
Url {
150+
scheme: "http",
151+
cannot_be_a_base: false,
152+
username: "",
153+
password: None,
154+
host: Some(
155+
Domain(
156+
"from_file",
157+
),
158+
),
159+
port: Some(
160+
4000,
161+
),
162+
path: "/",
163+
query: None,
164+
fragment: None,
165+
},
166+
),
167+
graphos: GraphOSConfig {
168+
apollo_key: None,
169+
apollo_graph_ref: None,
170+
apollo_registry_url: None,
171+
apollo_uplink_endpoints: [
172+
Url {
173+
scheme: "http",
174+
cannot_be_a_base: false,
175+
username: "",
176+
password: None,
177+
host: Some(
178+
Domain(
179+
"from_env",
180+
),
181+
),
182+
port: Some(
183+
4000,
184+
),
185+
path: "/",
186+
query: None,
187+
fragment: None,
188+
},
189+
Url {
190+
scheme: "http",
191+
cannot_be_a_base: false,
192+
username: "",
193+
password: None,
194+
host: Some(
195+
Domain(
196+
"from_env2",
197+
),
198+
),
199+
port: Some(
200+
4000,
201+
),
202+
path: "/",
203+
query: None,
204+
fragment: None,
205+
},
206+
],
207+
},
208+
headers: {},
209+
health_check: HealthCheckConfig {
210+
enabled: false,
211+
path: "/health",
212+
readiness: ReadinessConfig {
213+
interval: ReadinessIntervalConfig {
214+
sampling: 5s,
215+
unready: None,
216+
},
217+
allowed: 100,
218+
},
219+
},
220+
introspection: Introspection {
221+
execute: ExecuteConfig {
222+
enabled: false,
223+
},
224+
introspect: IntrospectConfig {
225+
enabled: false,
226+
minify: false,
227+
},
228+
search: SearchConfig {
229+
enabled: false,
230+
index_memory_bytes: 50000000,
231+
leaf_depth: 1,
232+
minify: false,
233+
},
234+
validate: ValidateConfig {
235+
enabled: false,
236+
},
237+
},
238+
logging: Logging {
239+
level: Level(
240+
Info,
241+
),
242+
path: None,
243+
rotation: Hourly,
244+
},
245+
operations: Infer,
246+
overrides: Overrides {
247+
disable_type_description: false,
248+
disable_schema_description: false,
249+
enable_explorer: false,
250+
mutation_mode: None,
251+
},
252+
schema: Uplink,
253+
transport: Stdio,
254+
}
255+
"#);
256+
Ok(())
257+
});
258+
}
127259
}

crates/apollo-mcp-server/src/runtime/graphos.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,48 @@ use apollo_mcp_registry::{
66
};
77
use apollo_mcp_server::errors::ServerError;
88
use schemars::JsonSchema;
9-
use serde::Deserialize;
9+
use serde::de::Error;
10+
use serde::{Deserialize, Deserializer};
1011
use url::Url;
1112

13+
#[cfg(test)]
14+
use serde::Serialize;
15+
1216
const APOLLO_GRAPH_REF_ENV: &str = "APOLLO_GRAPH_REF";
1317
const APOLLO_KEY_ENV: &str = "APOLLO_KEY";
1418

19+
fn apollo_uplink_endpoints_deserializer<'de, D>(deserializer: D) -> Result<Vec<Url>, D::Error>
20+
where
21+
D: Deserializer<'de>,
22+
{
23+
#[derive(Deserialize)]
24+
#[serde(untagged)]
25+
enum UrlListOrString {
26+
List(Vec<Url>),
27+
String(String),
28+
}
29+
30+
match UrlListOrString::deserialize(deserializer)? {
31+
UrlListOrString::List(urls) => Ok(urls),
32+
UrlListOrString::String(s) => s
33+
.split(',')
34+
.map(|v| {
35+
Url::parse(v.trim()).map_err(|e| {
36+
D::Error::custom(format!("Could not parse uplink endpoint URL: {e}"))
37+
})
38+
})
39+
.collect(),
40+
}
41+
}
42+
1543
/// Credentials to use with GraphOS
1644
#[derive(Debug, Deserialize, Default, JsonSchema)]
45+
#[cfg_attr(test, derive(Serialize))]
1746
#[serde(default)]
1847
pub struct GraphOSConfig {
1948
/// The apollo key
2049
#[schemars(with = "Option<String>")]
50+
#[cfg_attr(test, serde(skip_serializing))]
2151
apollo_key: Option<SecretString>,
2252

2353
/// The graph reference
@@ -27,6 +57,7 @@ pub struct GraphOSConfig {
2757
apollo_registry_url: Option<Url>,
2858

2959
/// List of uplink URL overrides
60+
#[serde(deserialize_with = "apollo_uplink_endpoints_deserializer")]
3061
apollo_uplink_endpoints: Vec<Url>,
3162
}
3263

0 commit comments

Comments
 (0)