Skip to content

Commit 04a3e8e

Browse files
Allow providing static variables via the cli (#3216)
Signed-off-by: Brian Hardock <[email protected]>
1 parent 0a4c05a commit 04a3e8e

File tree

7 files changed

+198
-2
lines changed

7 files changed

+198
-2
lines changed

Cargo.lock

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

crates/runtime-factors/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ spin-factors = { path = "../factors" }
3232
spin-factors-executor = { path = "../factors-executor" }
3333
spin-runtime-config = { path = "../runtime-config" }
3434
spin-trigger = { path = "../trigger" }
35+
spin-variables-static = { path = "../variables-static" }
3536
terminal = { path = "../terminal" }
3637
tracing = { workspace = true }
3738

crates/runtime-factors/src/build.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use spin_trigger::cli::{
1010
RuntimeFactorsBuilder, SqlStatementExecutorHook, SqliteDefaultStoreSummaryHook,
1111
StdioLoggingExecutorHooks,
1212
};
13+
use spin_variables_static::StaticVariablesProvider;
1314

1415
/// A [`RuntimeFactorsBuilder`] for [`TriggerFactors`].
1516
pub struct FactorsBuilder;
@@ -23,13 +24,26 @@ impl RuntimeFactorsBuilder for FactorsBuilder {
2324
config: &FactorsConfig,
2425
args: &Self::CliArgs,
2526
) -> anyhow::Result<(Self::Factors, Self::RuntimeConfig)> {
26-
let runtime_config = ResolvedRuntimeConfig::<TriggerFactorsRuntimeConfig>::from_file(
27+
let mut runtime_config = ResolvedRuntimeConfig::<TriggerFactorsRuntimeConfig>::from_file(
2728
config.runtime_config_file.clone().as_deref(),
2829
config.local_app_dir.clone().map(PathBuf::from),
2930
config.state_dir.clone(),
3031
config.log_dir.clone(),
3132
)?;
3233

34+
let cli_static_variables = args.get_variables()?.clone();
35+
let cli_static_variables_provider = StaticVariablesProvider::new(cli_static_variables);
36+
37+
// Insert the parsed static variables provided via cli arguments
38+
// into the set of variable providers with highest precedence.
39+
runtime_config
40+
.runtime_config
41+
.variables
42+
.as_mut()
43+
.unwrap()
44+
.providers
45+
.insert(0, Box::new(cli_static_variables_provider));
46+
3347
runtime_config.summarize(config.runtime_config_file.as_deref());
3448

3549
let factors = TriggerFactors::new(

crates/runtime-factors/src/lib.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ mod build;
22

33
pub use build::FactorsBuilder;
44

5+
use std::cell::OnceCell;
6+
use std::collections::HashMap;
57
use std::path::PathBuf;
68

79
use anyhow::Context as _;
@@ -19,6 +21,7 @@ use spin_factor_variables::VariablesFactor;
1921
use spin_factor_wasi::{spin::SpinFilesMounter, WasiFactor};
2022
use spin_factors::RuntimeFactors;
2123
use spin_runtime_config::{ResolvedRuntimeConfig, TomlRuntimeConfigSource};
24+
use spin_variables_static::VariableSource;
2225

2326
#[derive(RuntimeFactors)]
2427
pub struct TriggerFactors {
@@ -104,6 +107,22 @@ pub struct TriggerAppArgs {
104107
/// Sets the maxmimum memory allocation limit for an instance in bytes.
105108
#[clap(long, env = "SPIN_MAX_INSTANCE_MEMORY")]
106109
pub max_instance_memory: Option<usize>,
110+
111+
/// Variable(s) to be passed to the app
112+
///
113+
/// A single key-value pair can be passed as `key=value`. Alternatively, the
114+
/// path to a JSON or TOML file may be given as `@file.json` or
115+
/// `@file.toml`.
116+
///
117+
/// This option may be repeated. If the same key is specified multiple times
118+
/// the last value will be used.
119+
#[clap(long, multiple = true, value_parser = clap::value_parser!(VariableSource),
120+
value_name = "KEY=VALUE | @FILE.json | @FILE.toml")]
121+
pub variable: Vec<VariableSource>,
122+
123+
/// Cache variables to avoid reading files twice
124+
#[clap(skip)]
125+
variables_cache: OnceCell<HashMap<String, String>>,
107126
}
108127

109128
impl From<ResolvedRuntimeConfig<TriggerFactorsRuntimeConfig>> for TriggerFactorsRuntimeConfig {
@@ -119,3 +138,17 @@ impl TryFrom<TomlRuntimeConfigSource<'_, '_>> for TriggerFactorsRuntimeConfig {
119138
Self::from_source(value)
120139
}
121140
}
141+
142+
impl TriggerAppArgs {
143+
/// Parse all variable sources into a single merged map.
144+
pub fn get_variables(&self) -> anyhow::Result<&HashMap<String, String>> {
145+
if self.variables_cache.get().is_none() {
146+
let mut variables = HashMap::new();
147+
for source in &self.variable {
148+
variables.extend(source.get_variables()?);
149+
}
150+
self.variables_cache.set(variables).unwrap();
151+
}
152+
Ok(self.variables_cache.get().unwrap())
153+
}
154+
}

crates/variables-static/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@ rust-version.workspace = true
1010

1111
[dependencies]
1212
serde = { workspace = true }
13+
serde_json = { workspace = true }
14+
spin-common = { path = "../common" }
1315
spin-expressions = { path = "../expressions" }
1416
spin-factors = { path = "../factors" }
17+
toml = { workspace = true }
1518

1619
[lints]
1720
workspace = true
21+
22+
[dev-dependencies]
23+
tempfile = { workspace = true }

crates/variables-static/src/lib.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
use std::{collections::HashMap, sync::Arc};
1+
use std::{collections::HashMap, hash::Hash, sync::Arc};
22

33
use serde::Deserialize;
44
use spin_expressions::{async_trait::async_trait, Key, Provider};
55
use spin_factors::anyhow;
66

7+
pub use source::*;
8+
mod source;
9+
710
/// A [`Provider`] that reads variables from an static map.
811
#[derive(Debug, Deserialize, Clone)]
912
pub struct StaticVariablesProvider {
@@ -16,3 +19,20 @@ impl Provider for StaticVariablesProvider {
1619
Ok(self.values.get(key.as_str()).cloned())
1720
}
1821
}
22+
23+
impl StaticVariablesProvider {
24+
/// Creates a new `StaticVariablesProvider` with the given key-value pairs.
25+
pub fn new<K, V>(values: impl IntoIterator<Item = (K, V)>) -> Self
26+
where
27+
K: Into<String> + Eq + Hash,
28+
V: Into<String>,
29+
{
30+
let values = values
31+
.into_iter()
32+
.map(|(k, v)| (k.into(), v.into()))
33+
.collect();
34+
Self {
35+
values: Arc::new(values),
36+
}
37+
}
38+
}

crates/variables-static/src/source.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use spin_common::ui::quoted_path;
2+
use spin_factors::anyhow::{self, bail, Context as _};
3+
use std::{collections::HashMap, path::PathBuf, str::FromStr};
4+
5+
#[derive(Clone, Debug)]
6+
pub enum VariableSource {
7+
Literal(String, String),
8+
JsonFile(PathBuf),
9+
TomlFile(PathBuf),
10+
}
11+
12+
impl VariableSource {
13+
pub fn get_variables(&self) -> anyhow::Result<HashMap<String, String>> {
14+
match self {
15+
VariableSource::Literal(key, val) => Ok([(key.to_string(), val.to_string())].into()),
16+
VariableSource::JsonFile(path) => {
17+
let json_bytes = std::fs::read(path)
18+
.with_context(|| format!("Failed to read {}.", quoted_path(path)))?;
19+
let json_vars: HashMap<String, String> = serde_json::from_slice(&json_bytes)
20+
.with_context(|| format!("Failed to parse JSON from {}.", quoted_path(path)))?;
21+
Ok(json_vars)
22+
}
23+
VariableSource::TomlFile(path) => {
24+
let toml_str = std::fs::read_to_string(path)
25+
.with_context(|| format!("Failed to read {}.", quoted_path(path)))?;
26+
let toml_vars: HashMap<String, String> = toml::from_str(&toml_str)
27+
.with_context(|| format!("Failed to parse TOML from {}.", quoted_path(path)))?;
28+
Ok(toml_vars)
29+
}
30+
}
31+
}
32+
}
33+
34+
impl FromStr for VariableSource {
35+
type Err = anyhow::Error;
36+
37+
fn from_str(s: &str) -> Result<Self, Self::Err> {
38+
if let Some(path) = s.strip_prefix('@') {
39+
let path = PathBuf::from(path);
40+
match path.extension().and_then(|s| s.to_str()) {
41+
Some("json") => Ok(VariableSource::JsonFile(path)),
42+
Some("toml") => Ok(VariableSource::TomlFile(path)),
43+
_ => bail!("variable files must end in .json or .toml"),
44+
}
45+
} else if let Some((key, val)) = s.split_once('=') {
46+
Ok(VariableSource::Literal(key.to_string(), val.to_string()))
47+
} else {
48+
bail!("variables must be in the form 'key=value' or '@file'")
49+
}
50+
}
51+
}
52+
53+
#[cfg(test)]
54+
mod tests {
55+
use std::io::Write;
56+
57+
use super::*;
58+
59+
#[test]
60+
fn source_from_str() {
61+
match "k=v".parse() {
62+
Ok(VariableSource::Literal(key, val)) => {
63+
assert_eq!(key, "k");
64+
assert_eq!(val, "v");
65+
}
66+
Ok(other) => panic!("wrong variant {other:?}"),
67+
Err(err) => panic!("{err:?}"),
68+
}
69+
match "@file.json".parse() {
70+
Ok(VariableSource::JsonFile(_)) => {}
71+
Ok(other) => panic!("wrong variant {other:?}"),
72+
Err(err) => panic!("{err:?}"),
73+
}
74+
match "@file.toml".parse() {
75+
Ok(VariableSource::TomlFile(_)) => {}
76+
Ok(other) => panic!("wrong variant {other:?}"),
77+
Err(err) => panic!("{err:?}"),
78+
}
79+
}
80+
81+
#[test]
82+
fn source_from_str_errors() {
83+
assert!(VariableSource::from_str("nope").is_err());
84+
assert!(VariableSource::from_str("@whatami").is_err());
85+
assert!(VariableSource::from_str("@wrong.kind").is_err());
86+
}
87+
88+
#[test]
89+
fn literal_get_variables() {
90+
let vars = VariableSource::Literal("k".to_string(), "v".to_string())
91+
.get_variables()
92+
.unwrap();
93+
assert_eq!(vars["k"], "v");
94+
}
95+
96+
#[test]
97+
fn json_get_variables() {
98+
let mut json_file = tempfile::NamedTempFile::with_suffix(".json").unwrap();
99+
json_file.write_all(br#"{"k": "v"}"#).unwrap();
100+
let json_path = json_file.into_temp_path();
101+
let vars = VariableSource::JsonFile(json_path.to_path_buf())
102+
.get_variables()
103+
.unwrap();
104+
assert_eq!(vars["k"], "v");
105+
}
106+
107+
#[test]
108+
fn toml() {
109+
let mut toml_file = tempfile::NamedTempFile::with_suffix(".toml").unwrap();
110+
toml_file.write_all(br#"k = "v""#).unwrap();
111+
let toml_path = toml_file.into_temp_path();
112+
let vars = VariableSource::TomlFile(toml_path.to_path_buf())
113+
.get_variables()
114+
.unwrap();
115+
assert_eq!(vars["k"], "v");
116+
}
117+
}

0 commit comments

Comments
 (0)