Skip to main content

olai_stack_runtime/
source.rs

1//! Overlay sources: what an instance's environment takes from outside the store.
2
3use std::collections::BTreeMap;
4use std::fmt;
5use std::time::SystemTime;
6
7use async_trait::async_trait;
8use olai_stack_state::{InstanceId, VarSet};
9
10use crate::secrets::SecretValue;
11
12/// What a [`Source`] contributes to an instance's environment.
13#[derive(Clone, Default)]
14pub struct Overlay {
15    /// Plain variables.
16    pub vars: BTreeMap<String, String>,
17    /// Where hosted services are reached (e.g. a function's URL), as variables.
18    pub endpoints: BTreeMap<String, String>,
19    /// Credentials, as variables (e.g. a database password, vended storage keys). Masked
20    /// wherever the environment is shown.
21    pub credentials: BTreeMap<String, SecretValue>,
22    /// When the credentials stop working, if they expire.
23    pub expires_at: Option<SystemTime>,
24}
25
26impl fmt::Debug for Overlay {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        f.debug_struct("Overlay")
29            .field("vars", &self.vars)
30            .field("endpoints", &self.endpoints)
31            .field("credentials", &self.credentials.keys().collect::<Vec<_>>())
32            .field("expires_at", &self.expires_at)
33            .finish()
34    }
35}
36
37impl Overlay {
38    /// Whether the credentials have expired at `now`.
39    pub fn is_expired(&self, now: SystemTime) -> bool {
40        self.expires_at.is_some_and(|t| t <= now)
41    }
42}
43
44/// What a source is asked for.
45#[derive(Clone, Debug)]
46pub struct SourceCtx {
47    /// The instance.
48    pub instance: InstanceId,
49    /// Its workspace's name.
50    pub workspace: String,
51    /// Its environment's name.
52    pub environment: String,
53    /// The revision it runs.
54    pub revision: u32,
55}
56
57/// Why a source failed.
58#[derive(Debug, thiserror::Error)]
59#[error("source `{source_name}`: {reason}")]
60pub struct SourceError {
61    /// The source.
62    pub source_name: String,
63    /// What went wrong.
64    pub reason: String,
65}
66
67/// Something outside the store an instance takes variables, endpoints and credentials from,
68/// as an overlay on its environment (a Neon branch, say: its Postgres URL, its object-storage
69/// endpoint and vended keys, its function URLs). An instance names the sources it takes; they
70/// apply in that order, above the store's vars and below the instance's own overrides.
71#[async_trait]
72pub trait Source: Send + Sync {
73    /// The name instances refer to it by.
74    fn name(&self) -> &str;
75
76    /// The overlay for the instance `ctx` describes.
77    async fn overlay(&self, ctx: &SourceCtx) -> Result<Overlay, SourceError>;
78
79    /// A fresh overlay once `current` has expired. The default asks again.
80    async fn refresh(&self, ctx: &SourceCtx, current: &Overlay) -> Result<Overlay, SourceError> {
81        let _ = current;
82        self.overlay(ctx).await
83    }
84}
85
86/// The user's own variables, as a source named `user`: literal values only (secrets belong in
87/// the store as references).
88#[derive(Clone, Debug, Default)]
89pub struct UserVars {
90    vars: BTreeMap<String, String>,
91}
92
93impl UserVars {
94    /// A source of `vars`.
95    pub fn new(vars: BTreeMap<String, String>) -> Self {
96        UserVars { vars }
97    }
98
99    /// The literal values of `set` (its secret references are skipped).
100    pub fn from_var_set(set: &VarSet) -> Self {
101        UserVars::new(
102            set.0
103                .iter()
104                .filter_map(|(k, v)| match v {
105                    olai_stack_state::VarValue::Literal(l) => Some((k.clone(), l.clone())),
106                    olai_stack_state::VarValue::Secret(_) => None,
107                })
108                .collect(),
109        )
110    }
111}
112
113#[async_trait]
114impl Source for UserVars {
115    fn name(&self) -> &str {
116        "user"
117    }
118
119    async fn overlay(&self, _ctx: &SourceCtx) -> Result<Overlay, SourceError> {
120        Ok(Overlay {
121            vars: self.vars.clone(),
122            ..Overlay::default()
123        })
124    }
125}