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    /// What the instance's binding tells the source (e.g. which branch), merged over the
56    /// workspace's config for it: `null` when neither sets any.
57    pub config: serde_json::Value,
58}
59
60/// Why a source failed.
61#[derive(Debug, thiserror::Error)]
62#[error("source `{source_name}`: {reason}")]
63pub struct SourceError {
64    /// The source.
65    pub source_name: String,
66    /// What went wrong.
67    pub reason: String,
68}
69
70/// Something outside the store an instance takes variables, endpoints and credentials from,
71/// as an overlay on its environment (a Neon branch, say: its Postgres URL, its object-storage
72/// endpoint and vended keys, its function URLs). An instance binds the sources it takes; they
73/// apply in that order, above the store's vars and below the instance's own overrides.
74#[async_trait]
75pub trait Source: Send + Sync {
76    /// The name instances refer to it by.
77    fn name(&self) -> &str;
78
79    /// The overlay for the instance `ctx` describes.
80    async fn overlay(&self, ctx: &SourceCtx) -> Result<Overlay, SourceError>;
81
82    /// A fresh overlay once `current` has expired. The default asks again.
83    async fn refresh(&self, ctx: &SourceCtx, current: &Overlay) -> Result<Overlay, SourceError> {
84        let _ = current;
85        self.overlay(ctx).await
86    }
87}
88
89/// The user's own variables, as a source named `user`: literal values only (secrets belong in
90/// the store as references).
91#[derive(Clone, Debug, Default)]
92pub struct UserVars {
93    vars: BTreeMap<String, String>,
94}
95
96impl UserVars {
97    /// A source of `vars`.
98    pub fn new(vars: BTreeMap<String, String>) -> Self {
99        UserVars { vars }
100    }
101
102    /// The literal values of `set` (its secret references are skipped).
103    pub fn from_var_set(set: &VarSet) -> Self {
104        UserVars::new(
105            set.0
106                .iter()
107                .filter_map(|(k, v)| match v {
108                    olai_stack_state::VarValue::Literal(l) => Some((k.clone(), l.clone())),
109                    olai_stack_state::VarValue::Secret(_) => None,
110                })
111                .collect(),
112        )
113    }
114}
115
116#[async_trait]
117impl Source for UserVars {
118    fn name(&self) -> &str {
119        "user"
120    }
121
122    async fn overlay(&self, _ctx: &SourceCtx) -> Result<Overlay, SourceError> {
123        Ok(Overlay {
124            vars: self.vars.clone(),
125            ..Overlay::default()
126        })
127    }
128}