Skip to main content

kernel/config/
secret.rs

1use std::borrow::Cow;
2use std::fmt;
3
4use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
5use serde::{Deserialize, Serialize};
6
7use crate::adapter::SECRET_MARKER;
8
9/// A value resolved at **runtime**: either a literal baked into the config or a
10/// reference to an environment variable read when the pipeline runs. Deferring
11/// resolution is what lets a compiled config travel without its secrets — a
12/// literal is carried as-is, an [`Env`](Self::Env) reference carries only the
13/// variable name, and the real value is read in the environment that runs it.
14///
15/// Serialized exactly as it is written in `flusso.toml`: a plain string for a
16/// literal, `{ env = "VAR" }` for a reference. The `Deserialize` is hand-written
17/// (not `#[serde(untagged)]`) so a wrong shape reports what was expected instead
18/// of serde's opaque "data did not match any variant".
19#[derive(Clone, PartialEq, Eq)]
20pub enum Secret {
21    /// A literal value, stored verbatim.
22    Value(String),
23    /// Read from this environment variable at resolution time.
24    Env(String),
25}
26
27impl Secret {
28    /// A reference to the environment variable `var`, read at resolution time.
29    pub fn env(var: impl Into<String>) -> Self {
30        Secret::Env(var.into())
31    }
32
33    /// Read this secret's value from its own source — a literal as-is, an `Env`
34    /// from the environment. Does not consult any reserved variable.
35    fn read(&self) -> Result<String, ResolveError> {
36        match self {
37            Secret::Value(value) => Ok(value.clone()),
38            Secret::Env(var) => {
39                std::env::var(var).map_err(|_| ResolveError::EnvNotSet(var.clone()))
40            }
41        }
42    }
43
44    /// Resolve a **required** secret with `override_var` as its deployment
45    /// override (see [`override_var`](crate::override_var)). Precedence: an
46    /// explicit `Env` reference names its own source and wins; otherwise the
47    /// override variable, if set; otherwise the literal.
48    pub fn resolve(&self, override_var: &str) -> Result<String, ResolveError> {
49        resolve_required(self, override_var)
50    }
51
52    /// Resolve an **optional** secret: same precedence as [`resolve`](Self::resolve),
53    /// and when the config omits it the override variable fills it if set.
54    pub fn resolve_optional(
55        secret: Option<&Secret>,
56        override_var: &str,
57    ) -> Result<Option<String>, ResolveError> {
58        resolve_optional(secret, override_var)
59    }
60}
61
62/// A secret is a string or `{ env = "VAR" }`; the schema says so and plants the
63/// [`SECRET_MARKER`] so an adapter description can list its override variable.
64impl JsonSchema for Secret {
65    fn schema_name() -> Cow<'static, str> {
66        Cow::Borrowed("Secret")
67    }
68
69    fn json_schema(_: &mut SchemaGenerator) -> Schema {
70        let mut schema = json_schema!({
71            "description": "A literal value, or `{ env = \"VAR\" }` to read it from the environment at run time.",
72            "anyOf": [
73                { "type": "string" },
74                {
75                    "type": "object",
76                    "properties": { "env": { "type": "string", "description": "The environment variable to read." } },
77                    "required": ["env"],
78                    "additionalProperties": false
79                }
80            ]
81        });
82        schema.insert(SECRET_MARKER.to_owned(), serde_json::Value::Bool(true));
83        schema
84    }
85}
86
87impl From<&str> for Secret {
88    fn from(value: &str) -> Self {
89        Secret::Value(value.to_owned())
90    }
91}
92
93impl From<String> for Secret {
94    fn from(value: String) -> Self {
95        Secret::Value(value)
96    }
97}
98
99/// The accepted shapes, named once so every error message stays consistent.
100const EXPECTED: &str = "a string value or an env reference `{ env = \"VAR\" }`";
101
102impl Serialize for Secret {
103    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
104        use serde::ser::SerializeMap;
105        match self {
106            Secret::Value(value) => serializer.serialize_str(value),
107            Secret::Env(env) => {
108                let mut map = serializer.serialize_map(Some(1))?;
109                map.serialize_entry("env", env)?;
110                map.end()
111            }
112        }
113    }
114}
115
116struct SecretVisitor;
117
118impl<'de> serde::de::Visitor<'de> for SecretVisitor {
119    type Value = Secret;
120
121    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        f.write_str(EXPECTED)
123    }
124
125    fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
126        Ok(Secret::Value(value.to_owned()))
127    }
128
129    fn visit_string<E: serde::de::Error>(self, value: String) -> Result<Self::Value, E> {
130        Ok(Secret::Value(value))
131    }
132
133    fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
134        use serde::de::Error;
135        let Some(key) = map.next_key::<String>()? else {
136            return Err(A::Error::custom(format!(
137                "expected {EXPECTED}, found an empty table"
138            )));
139        };
140        if key != "env" {
141            return Err(A::Error::custom(format!(
142                "unknown key `{key}` — expected {EXPECTED}"
143            )));
144        }
145        let env: String = map.next_value()?;
146        if let Some(extra) = map.next_key::<String>()? {
147            return Err(A::Error::custom(format!(
148                "unexpected key `{extra}` in env reference — write it as `{{ env = \"VAR\" }}`"
149            )));
150        }
151        Ok(Secret::Env(env))
152    }
153}
154
155impl<'de> Deserialize<'de> for Secret {
156    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
157        deserializer.deserialize_any(SecretVisitor)
158    }
159}
160
161/// Redacted, so a debug-printed config never leaks a literal. An `Env` reference
162/// shows its variable name (not a secret); a literal shows `***`.
163impl fmt::Debug for Secret {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        match self {
166            Secret::Value(_) => write!(f, "Secret(***)"),
167            Secret::Env(var) => write!(f, "Secret(env {var})"),
168        }
169    }
170}
171
172impl fmt::Display for Secret {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        match self {
175            Secret::Value(_) => write!(f, "***"),
176            Secret::Env(var) => write!(f, "${{{var}}}"),
177        }
178    }
179}
180
181fn resolve_required(secret: &Secret, reserved: &str) -> Result<String, ResolveError> {
182    match secret {
183        env @ Secret::Env(_) => env.read(),
184        Secret::Value(literal) => Ok(literal_or_override(literal, reserved)),
185    }
186}
187
188fn resolve_optional(
189    secret: Option<&Secret>,
190    reserved: &str,
191) -> Result<Option<String>, ResolveError> {
192    match secret {
193        Some(env @ Secret::Env(_)) => env.read().map(Some),
194        Some(Secret::Value(literal)) => Ok(Some(literal_or_override(literal, reserved))),
195        None => Ok(std::env::var(reserved).ok()),
196    }
197}
198
199/// The `reserved` variable if set, else the literal.
200fn literal_or_override(literal: &str, reserved: &str) -> String {
201    std::env::var(reserved).unwrap_or_else(|_| literal.to_owned())
202}
203
204/// A [`Secret`] could not be read in the running environment.
205#[derive(thiserror::Error, Debug)]
206pub enum ResolveError {
207    #[error("environment variable '{0}' is not set")]
208    EnvNotSet(String),
209    /// A required value is neither in the config nor in its override variable;
210    /// the message names both.
211    #[error("{0}")]
212    Missing(String),
213    /// A resolved value failed the adapter's own validation (a malformed URL,
214    /// a missing part); the message is the adapter's.
215    #[error("{0}")]
216    Invalid(String),
217}