//! Variables for OxygenFlow jobs (hub spec §13.2, oxygenhub#102): plain //! `NAME=value` pairs kept per site, organisation and repository. A job //! gets its repository's effective set, inner levels overriding outer, as //! the claim's `env`; Flow sets them under `FLOW_*` and the pipeline's //! own `env`. They are not secrets and are shown in full on the settings //! pages; secrets are mounted by Flow (OxygenFlow spec §9). use crate::db::{now_ms, Pool}; use crate::levels::{self, Scope}; use sqlx::FromRow; use std::collections::BTreeMap; /// How many a level may hold. pub const MAX_PER_LEVEL: usize = 100; /// The longest value. pub const MAX_VALUE: usize = 4096; #[derive(Clone, Debug, FromRow)] pub struct Variable { pub scope: String, pub scope_id: i64, pub name: String, pub value: String, pub updated_ms: i64, } /// A variable's name: a letter or `_`, then letters, digits and `_`, up /// to 64; never `FLOW_*`, which Flow sets itself. pub fn name_ok(name: &str) -> bool { let mut chars = name.chars(); let first = chars.next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_'); first && name.len() <= 64 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_') && !name.starts_with("FLOW_") } /// A value: up to 4096 characters, no control characters but newline and tab. pub fn value_ok(value: &str) -> bool { value.len() <= MAX_VALUE && !value.chars().any(|c| c.is_control() && c != '\n' && c != '\t') } pub async fn list(pool: &Pool, scope: Scope) -> sqlx::Result> { sqlx::query_as("SELECT * FROM flow_variables WHERE scope = ? AND scope_id = ? ORDER BY name") .bind(scope.kind()) .bind(scope.id()) .fetch_all(pool) .await } /// Set a variable at a level, new or changed. pub async fn set(pool: &Pool, scope: Scope, name: &str, value: &str) -> sqlx::Result<()> { sqlx::query( "INSERT INTO flow_variables (scope, scope_id, name, value, updated_ms) VALUES (?, ?, ?, ?, ?) \ ON CONFLICT (scope, scope_id, name) DO UPDATE SET value = excluded.value, updated_ms = excluded.updated_ms", ) .bind(scope.kind()) .bind(scope.id()) .bind(name) .bind(value) .bind(now_ms()) .execute(pool) .await?; Ok(()) } /// Remove a variable from a level; false when it was not there. pub async fn delete(pool: &Pool, scope: Scope, name: &str) -> sqlx::Result { let r = sqlx::query("DELETE FROM flow_variables WHERE scope = ? AND scope_id = ? AND name = ?") .bind(scope.kind()) .bind(scope.id()) .bind(name) .execute(pool) .await?; Ok(r.rows_affected() == 1) } /// Forget a level's variables, when the organisation or repository goes. pub async fn delete_scope(pool: &Pool, scope: Scope) -> sqlx::Result<()> { sqlx::query("DELETE FROM flow_variables WHERE scope = ? AND scope_id = ?").bind(scope.kind()).bind(scope.id()).execute(pool).await?; Ok(()) } /// A variable as a level sees it: where it comes from, and whether the /// level's own value hides it. #[derive(Clone, Debug)] pub struct Seen { pub variable: Variable, /// `the site`, `the acme organisation`, or empty for the level's own. pub from: String, /// An inner level sets the same name, so this value is not the one used. pub overridden: bool, } /// Every variable a level sees, outermost first, inherited ones marked /// overridden where an inner level sets the same name. pub async fn seen(pool: &Pool, scope: Scope, owner: &str) -> sqlx::Result> { let mut lvls = levels::above(pool, scope, owner).await?; lvls.push((scope, String::new())); let mut out: Vec = vec![]; for (level, from) in lvls { for v in list(pool, level).await? { for o in out.iter_mut().filter(|o| o.variable.name == v.name) { o.overridden = true; } out.push(Seen { variable: v, from: from.clone(), overridden: false }); } } Ok(out) } /// The variables a repository's jobs get. pub async fn env_for_repo(pool: &Pool, repo: &crate::db::repos::RepoRow) -> sqlx::Result> { Ok(seen(pool, Scope::Repo(repo.id), &repo.owner) .await? .into_iter() .filter(|s| !s.overridden) .map(|s| (s.variable.name, s.variable.value)) .collect()) } #[cfg(test)] mod tests { use super::*; #[test] fn names_and_values_are_checked() { assert!(name_ok("CC") && name_ok("_x") && name_ok("RUST_LOG")); assert!(!name_ok("") && !name_ok("1A") && !name_ok("A-B") && !name_ok("FLOW_REPO") && !name_ok(&"A".repeat(65))); assert!(value_ok("clang") && value_ok("two\nlines\twith tab") && value_ok("")); assert!(!value_ok("a\u{0}b") && !value_ok(&"x".repeat(MAX_VALUE + 1))); } }