OXYGENHUB

Coverage / crates/hub/src/variables.rs 100.0%

80 of 80 lines run, 26 of 41 functions · run #493 on diffusion-pin-bookmark · run missed

1
//! Variables for OxygenFlow jobs (hub spec §13.2, oxygenhub#102): plain
2
//! `NAME=value` pairs kept per site, organisation and repository. A job
3
//! gets its repository's effective set, inner levels overriding outer, as
4
//! the claim's `env`; Flow sets them under `FLOW_*` and the pipeline's
5
//! own `env`. They are not secrets and are shown in full on the settings
6
//! pages; secrets are mounted by Flow (OxygenFlow spec §9).
7
8
use crate::db::{now_ms, Pool};
9
use crate::levels::{self, Scope};
10
use sqlx::FromRow;
11
use std::collections::BTreeMap;
12
13
/// How many a level may hold.
14
pub const MAX_PER_LEVEL: usize = 100;
15
/// The longest value.
16
pub const MAX_VALUE: usize = 4096;
17
18
#[derive(Clone, Debug, FromRow)]
19
pub struct Variable {
20
    pub scope: String,
21
    pub scope_id: i64,
22
    pub name: String,
23
    pub value: String,
24
    pub updated_ms: i64,
25
}
26
27
/// A variable's name: a letter or `_`, then letters, digits and `_`, up
28
/// to 64; never `FLOW_*`, which Flow sets itself.
2912×
pub fn name_ok(name: &str) -> bool {
3012×
    let mut chars = name.chars();
3112×
    let first = chars.next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
3243×
    first && name.len() <= 64 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_') && !name.starts_with("FLOW_")
3312×
}
34
35
/// A value: up to 4096 characters, no control characters but newline and tab.
367×
pub fn value_ok(value: &str) -> bool {
3735×
    value.len() <= MAX_VALUE && !value.chars().any(|c| c.is_control() && c != '\n' && c != '\t')
387×
}
39
40753×
pub async fn list(pool: &Pool, scope: Scope) -> sqlx::Result<Vec<Variable>> {
41753×
    sqlx::query_as("SELECT * FROM flow_variables WHERE scope = $1 AND scope_id = $2 ORDER BY name")
42753×
        .bind(scope.kind())
43753×
        .bind(scope.id())
44753×
        .fetch_all(pool)
45753×
        .await
46753×
}
47
48
/// Set a variable at a level, new or changed.
495×
pub async fn set(pool: &Pool, scope: Scope, name: &str, value: &str) -> sqlx::Result<()> {
505×
    sqlx::query(
515×
        "INSERT INTO flow_variables (scope, scope_id, name, value, updated_ms) VALUES ($1, $2, $3, $4, $5) \
525×
         ON CONFLICT (scope, scope_id, name) DO UPDATE SET value = excluded.value, updated_ms = excluded.updated_ms",
535×
    )
545×
    .bind(scope.kind())
555×
    .bind(scope.id())
565×
    .bind(name)
575×
    .bind(value)
585×
    .bind(now_ms())
595×
    .execute(pool)
605×
    .await?;
615×
    Ok(())
625×
}
63
64
/// Remove a variable from a level; false when it was not there.
651×
pub async fn delete(pool: &Pool, scope: Scope, name: &str) -> sqlx::Result<bool> {
661×
    let r = sqlx::query("DELETE FROM flow_variables WHERE scope = $1 AND scope_id = $2 AND name = $3")
671×
        .bind(scope.kind())
681×
        .bind(scope.id())
691×
        .bind(name)
701×
        .execute(pool)
711×
        .await?;
721×
    Ok(r.rows_affected() == 1)
731×
}
74
75
/// Forget a level's variables, when the organisation or repository goes.
761×
pub async fn delete_scope(pool: &Pool, scope: Scope) -> sqlx::Result<()> {
771×
    sqlx::query("DELETE FROM flow_variables WHERE scope = $1 AND scope_id = $2").bind(scope.kind()).bind(scope.id()).execute(pool).await?;
781×
    Ok(())
791×
}
80
81
/// A variable as a level sees it: where it comes from, and whether the
82
/// level's own value hides it.
83
#[derive(Clone, Debug)]
84
pub struct Seen {
85
    pub variable: Variable,
86
    /// `the site`, `the acme organisation`, or empty for the level's own.
87
    pub from: String,
88
    /// An inner level sets the same name, so this value is not the one used.
89
    pub overridden: bool,
90
}
91
92
/// Every variable a level sees, outermost first, inherited ones marked
93
/// overridden where an inner level sets the same name.
94417×
pub async fn seen(pool: &Pool, scope: Scope, owner: &str) -> sqlx::Result<Vec<Seen>> {
95417×
    let mut lvls = levels::above(pool, scope, owner).await?;
96417×
    lvls.push((scope, String::new()));
97417×
    let mut out: Vec<Seen> = vec![];
98751×
    for (level, from) in lvls {
99751×
        for v in list(pool, level).await? {
10032×
            for o in out.iter_mut().filter(|o| o.variable.name == v.name) {
1016×
                o.overridden = true;
1026×
            }
10332×
            out.push(Seen { variable: v, from: from.clone(), overridden: false });
104
        }
105
    }
106417×
    Ok(out)
107417×
}
108
109
/// The variables a repository's jobs get.
110157×
pub async fn env_for_repo(pool: &Pool, repo: &crate::db::repos::RepoRow) -> sqlx::Result<BTreeMap<String, String>> {
111157×
    Ok(seen(pool, Scope::Repo(repo.id), &repo.owner)
112157×
        .await?
113157×
        .into_iter()
114157×
        .filter(|s| !s.overridden)
115157×
        .map(|s| (s.variable.name, s.variable.value))
116157×
        .collect())
117157×
}
118
119
#[cfg(test)]
120
mod tests {
121
    use super::*;
122
123
    #[test]
1241×
    fn names_and_values_are_checked() {
1251×
        assert!(name_ok("CC") && name_ok("_x") && name_ok("RUST_LOG"));
1261×
        assert!(!name_ok("") && !name_ok("1A") && !name_ok("A-B") && !name_ok("FLOW_REPO") && !name_ok(&"A".repeat(65)));
1271×
        assert!(value_ok("clang") && value_ok("two\nlines\twith tab") && value_ok(""));
1281×
        assert!(!value_ok("a\u{0}b") && !value_ok(&"x".repeat(MAX_VALUE + 1)));
1291×
    }
130
}