Skip to content

Commit b0447ea

Browse files
committed
Check required variables are potentially resolvable
Adds new dynamism metadata for Providers. Allow for no default value before runtime if Provider is a Dynamic type. PR also includes unit tests Signed-off-by: Aminu Oluwaseun Joshua <[email protected]>
1 parent d1eb9ac commit b0447ea

File tree

14 files changed

+392
-7
lines changed

14 files changed

+392
-7
lines changed

Cargo.lock

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

crates/expressions/src/lib.rs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
pub mod provider;
22
mod template;
33

4-
use std::{borrow::Cow, collections::HashMap, fmt::Debug};
4+
use std::{borrow::Cow, collections::HashMap, fmt::Debug, vec};
55

66
use spin_locked_app::Variable;
77

@@ -11,6 +11,8 @@ pub use provider::Provider;
1111
use template::Part;
1212
pub use template::Template;
1313

14+
use crate::provider::ProviderVariableKind;
15+
1416
/// A [`ProviderResolver`] that can be shared.
1517
pub type SharedPreparedResolver =
1618
std::sync::Arc<std::sync::OnceLock<std::sync::Arc<PreparedResolver>>>;
@@ -90,6 +92,54 @@ impl ProviderResolver {
9092
Ok(PreparedResolver { variables })
9193
}
9294

95+
/// Ensures that all required variables are resolvable at startup
96+
pub async fn ensure_required_variables_resolvable(&self) -> Result<()> {
97+
// If a dynamic provider is available, skip validation.
98+
if self
99+
.providers
100+
.iter()
101+
.any(|p| p.kind() == ProviderVariableKind::Dynamic)
102+
{
103+
return Ok(());
104+
}
105+
106+
let mut unvalidated_keys = vec![];
107+
for key in self.internal.variables.keys() {
108+
// If default value is provided, skip validation.
109+
if let Some(value) = self.internal.variables.get(key) {
110+
if value.default.is_some() {
111+
continue;
112+
}
113+
}
114+
115+
let mut resolved = false;
116+
117+
for provider in &self.providers {
118+
if provider
119+
.get(&Key(key))
120+
.await
121+
.map_err(Error::Provider)?
122+
.is_some()
123+
{
124+
resolved = true;
125+
break;
126+
}
127+
}
128+
129+
if !resolved {
130+
unvalidated_keys.push(key);
131+
}
132+
}
133+
134+
if unvalidated_keys.is_empty() {
135+
Ok(())
136+
} else {
137+
Err(Error::Provider(anyhow::anyhow!(
138+
"no provider resolved required variable(s): {unvalidated_keys:?}",
139+
)))
140+
}
141+
}
142+
93143
async fn resolve_variable(&self, key: &str) -> Result<String> {
94144
for provider in &self.providers {
95145
if let Some(value) = provider.get(&Key(key)).await.map_err(Error::Provider)? {
@@ -310,6 +360,7 @@ mod tests {
310360
use async_trait::async_trait;
311361

312362
use super::*;
363+
use crate::provider::ProviderVariableKind;
313364

314365
#[derive(Debug)]
315366
struct TestProvider;
@@ -323,6 +374,10 @@ mod tests {
323374
_ => Ok(None),
324375
}
325376
}
377+
378+
fn kind(&self) -> ProviderVariableKind {
379+
ProviderVariableKind::Static
380+
}
326381
}
327382

328383
async fn test_resolve(template: &str) -> Result<String> {

crates/expressions/src/provider.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,15 @@ use crate::Key;
99
pub trait Provider: Debug + Send + Sync {
1010
/// Returns the value at the given config path, if it exists.
1111
async fn get(&self, key: &Key) -> anyhow::Result<Option<String>>;
12+
fn kind(&self) -> ProviderVariableKind;
13+
}
14+
15+
/// The dynamism of a Provider.
16+
#[derive(Clone, Debug, Default, PartialEq)]
17+
pub enum ProviderVariableKind {
18+
/// Variable must be declared on start
19+
Static,
20+
/// Variable can be made available at runtime
21+
#[default]
22+
Dynamic,
1223
}
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
use std::collections::HashMap;
2+
3+
use async_trait::async_trait;
4+
use spin_expressions::{provider::ProviderVariableKind, Key, Provider, ProviderResolver};
5+
use spin_locked_app::Variable;
6+
7+
#[derive(Default)]
8+
struct ResolverTester {
9+
providers: Vec<Box<dyn Provider>>,
10+
variables: HashMap<String, Variable>,
11+
}
12+
13+
impl ResolverTester {
14+
fn new() -> Self {
15+
Self::default()
16+
}
17+
18+
fn with_provider(mut self, provider: impl Provider + 'static) -> Self {
19+
self.providers.push(Box::new(provider));
20+
self
21+
}
22+
23+
fn with_variable(mut self, key: &str, default: Option<&str>) -> Self {
24+
self.variables.insert(
25+
key.to_string(),
26+
Variable {
27+
description: None,
28+
default: default.map(ToString::to_string),
29+
secret: false,
30+
},
31+
);
32+
self
33+
}
34+
35+
fn make_resolver(self) -> anyhow::Result<ProviderResolver> {
36+
let mut provider_resolver = ProviderResolver::new(self.variables)?;
37+
38+
for provider in self.providers {
39+
provider_resolver.add_provider(provider);
40+
}
41+
42+
Ok(provider_resolver)
43+
}
44+
}
45+
46+
#[tokio::test(flavor = "multi_thread")]
47+
async fn if_single_static_provider_with_no_key_to_resolve_is_valid() -> anyhow::Result<()> {
48+
let resolver = ResolverTester::new()
49+
.with_provider(StaticProvider::with_variable(
50+
"database_host",
51+
Some("localhost"),
52+
))
53+
.make_resolver()?;
54+
55+
resolver.ensure_required_variables_resolvable().await?;
56+
57+
Ok(())
58+
}
59+
60+
#[tokio::test(flavor = "multi_thread")]
61+
async fn if_single_static_provider_has_data_for_variable_key_to_resolve_it_succeeds(
62+
) -> anyhow::Result<()> {
63+
let resolver = ResolverTester::new()
64+
.with_provider(StaticProvider::with_variable(
65+
"database_host",
66+
Some("localhost"),
67+
))
68+
.with_variable("database_host", None)
69+
.make_resolver()?;
70+
71+
resolver.ensure_required_variables_resolvable().await?;
72+
73+
Ok(())
74+
}
75+
76+
#[tokio::test(flavor = "multi_thread")]
77+
async fn if_there_is_a_single_static_provider_and_it_does_not_contain_a_required_variable_then_validation_fails(
78+
) -> anyhow::Result<()> {
79+
let resolver = ResolverTester::new()
80+
.with_provider(StaticProvider::with_variable(
81+
"database_host",
82+
Some("localhost"),
83+
))
84+
.with_variable("api_key", None)
85+
.make_resolver()?;
86+
87+
assert!(resolver
88+
.ensure_required_variables_resolvable()
89+
.await
90+
.is_err());
91+
92+
Ok(())
93+
}
94+
95+
#[tokio::test(flavor = "multi_thread")]
96+
async fn if_there_is_a_dynamic_provider_then_validation_succeeds_even_without_default_value_in_play(
97+
) -> anyhow::Result<()> {
98+
let resolver = ResolverTester::new()
99+
.with_provider(DynamicProvider)
100+
.with_variable("api_key", None)
101+
.make_resolver()?;
102+
103+
resolver.ensure_required_variables_resolvable().await?;
104+
105+
Ok(())
106+
}
107+
108+
#[tokio::test(flavor = "multi_thread")]
109+
async fn if_there_is_a_dynamic_provider_and_static_provider_but_the_variable_to_be_resolved_is_not_in_play(
110+
) -> anyhow::Result<()> {
111+
let resolver = ResolverTester::new()
112+
.with_provider(DynamicProvider)
113+
.with_provider(StaticProvider::with_variable(
114+
"database_host",
115+
Some("localhost"),
116+
))
117+
.with_variable("api_key", None)
118+
.make_resolver()?;
119+
120+
resolver.ensure_required_variables_resolvable().await?;
121+
122+
Ok(())
123+
}
124+
125+
#[tokio::test(flavor = "multi_thread")]
126+
async fn if_there_is_a_dynamic_provider_and_a_static_provider_then_validation_succeeds_even_if_a_static_provider_with_the_variable_is_in_play(
127+
) -> anyhow::Result<()> {
128+
let resolver = ResolverTester::new()
129+
.with_provider(DynamicProvider)
130+
.with_provider(StaticProvider::with_variable(
131+
"database_host",
132+
Some("localhost"),
133+
))
134+
.with_variable("api_key", Some("super-secret-key"))
135+
.make_resolver()?;
136+
137+
resolver.ensure_required_variables_resolvable().await?;
138+
139+
Ok(())
140+
}
141+
142+
#[tokio::test(flavor = "multi_thread")]
143+
async fn if_there_are_two_static_providers_where_one_has_data_is_valid() -> anyhow::Result<()> {
144+
let resolver = ResolverTester::new()
145+
.with_provider(StaticProvider::with_variable(
146+
"database_host",
147+
Some("localhost"),
148+
))
149+
.with_provider(StaticProvider::with_variable(
150+
"api_key",
151+
Some("super-secret-key"),
152+
))
153+
.with_variable("database_host", None)
154+
.make_resolver()?;
155+
156+
resolver.ensure_required_variables_resolvable().await?;
157+
158+
Ok(())
159+
}
160+
// Ensure that if there are two or more static providers and the first one does not have data for the variable to be resolved,
161+
// but the second or subsequent one does, then validation still succeeds.
162+
#[tokio::test(flavor = "multi_thread")]
163+
async fn if_there_are_two_static_providers_where_first_provider_does_not_have_data_while_second_provider_does(
164+
) -> anyhow::Result<()> {
165+
let resolver = ResolverTester::new()
166+
.with_provider(StaticProvider::with_variable(
167+
"database_host",
168+
Some("localhost"),
169+
))
170+
.with_provider(StaticProvider::with_variable(
171+
"api_key",
172+
Some("super-secret-key"),
173+
))
174+
.with_variable("api_key", None)
175+
.make_resolver()?;
176+
177+
resolver.ensure_required_variables_resolvable().await?;
178+
179+
Ok(())
180+
}
181+
182+
#[tokio::test(flavor = "multi_thread")]
183+
async fn if_there_is_two_static_providers_neither_having_data_is_invalid() -> anyhow::Result<()> {
184+
let resolver = ResolverTester::new()
185+
.with_provider(StaticProvider::with_variable(
186+
"database_host",
187+
Some("localhost"),
188+
))
189+
.with_provider(StaticProvider::with_variable(
190+
"api_key",
191+
Some("super-secret-key"),
192+
))
193+
.with_variable("hello", None)
194+
.make_resolver()?;
195+
196+
assert!(resolver
197+
.ensure_required_variables_resolvable()
198+
.await
199+
.is_err());
200+
201+
Ok(())
202+
}
203+
204+
#[tokio::test(flavor = "multi_thread")]
205+
async fn no_provider_data_available_but_variable_default_value_needed_is_invalid(
206+
) -> anyhow::Result<()> {
207+
let resolver = ResolverTester::new()
208+
.with_variable("api_key", None)
209+
.make_resolver()?;
210+
211+
assert!(resolver
212+
.ensure_required_variables_resolvable()
213+
.await
214+
.is_err());
215+
216+
Ok(())
217+
}
218+
219+
#[tokio::test(flavor = "multi_thread")]
220+
async fn no_provider_data_available_but_variable_has_default_value_needed_is_valid(
221+
) -> anyhow::Result<()> {
222+
let resolver = ResolverTester::new()
223+
.with_variable("api_key", Some("super-secret-key"))
224+
.make_resolver()?;
225+
226+
resolver.ensure_required_variables_resolvable().await?;
227+
228+
Ok(())
229+
}
230+
231+
#[derive(Debug)]
232+
struct StaticProvider {
233+
variables: HashMap<String, Option<String>>,
234+
}
235+
236+
impl StaticProvider {
237+
fn with_variable(key: &str, value: Option<&str>) -> Self {
238+
Self {
239+
variables: HashMap::from([(key.into(), value.map(|v| v.into()))]),
240+
}
241+
}
242+
}
243+
244+
#[async_trait]
245+
impl Provider for StaticProvider {
246+
async fn get(&self, key: &Key) -> anyhow::Result<Option<String>> {
247+
Ok(self.variables.get(key.as_str()).cloned().flatten())
248+
}
249+
250+
fn kind(&self) -> ProviderVariableKind {
251+
ProviderVariableKind::Static
252+
}
253+
}
254+
255+
#[derive(Debug)]
256+
struct DynamicProvider;
257+
258+
#[async_trait]
259+
impl Provider for DynamicProvider {
260+
async fn get(&self, _key: &Key) -> anyhow::Result<Option<String>> {
261+
panic!("validation should never call get for a dynamic provider")
262+
}
263+
264+
fn kind(&self) -> ProviderVariableKind {
265+
ProviderVariableKind::Dynamic
266+
}
267+
}

crates/factor-variables/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ impl AppState {
8585
let template = Template::new(expr)?;
8686
self.expression_resolver.resolve_template(&template).await
8787
}
88+
89+
pub fn expression_resolver(&self) -> &Arc<ExpressionResolver> {
90+
&self.expression_resolver
91+
}
8892
}
8993

9094
pub struct InstanceState {

0 commit comments

Comments
 (0)