OXYGENHUB
# 554ccacfc123 oxygenhub / crates / hub / src / variables.rs
4.7 KiB · Rust · last changed Variables for pipelines at the site, organisation and repository 1 month ago source blame history raw
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.
29
pub fn name_ok(name: &str) -> bool {
30
    let mut chars = name.chars();
31
    let first = chars.next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
32
    first && name.len() <= 64 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_') && !name.starts_with("FLOW_")
33
}
34
35
/// A value: up to 4096 characters, no control characters but newline and tab.
36
pub fn value_ok(value: &str) -> bool {
37
    value.len() <= MAX_VALUE && !value.chars().any(|c| c.is_control() && c != '\n' && c != '\t')
38
}
39
40
pub async fn list(pool: &Pool, scope: Scope) -> sqlx::Result<Vec<Variable>> {
41
    sqlx::query_as("SELECT * FROM flow_variables WHERE scope = ? AND scope_id = ? ORDER BY name")
42
        .bind(scope.kind())
43
        .bind(scope.id())
44
        .fetch_all(pool)
45
        .await
46
}
47
48
/// Set a variable at a level, new or changed.
49
pub async fn set(pool: &Pool, scope: Scope, name: &str, value: &str) -> sqlx::Result<()> {
50
    sqlx::query(
51
        "INSERT INTO flow_variables (scope, scope_id, name, value, updated_ms) VALUES (?, ?, ?, ?, ?) \
52
         ON CONFLICT (scope, scope_id, name) DO UPDATE SET value = excluded.value, updated_ms = excluded.updated_ms",
53
    )
54
    .bind(scope.kind())
55
    .bind(scope.id())
56
    .bind(name)
57
    .bind(value)
58
    .bind(now_ms())
59
    .execute(pool)
60
    .await?;
61
    Ok(())
62
}
63
64
/// Remove a variable from a level; false when it was not there.
65
pub async fn delete(pool: &Pool, scope: Scope, name: &str) -> sqlx::Result<bool> {
66
    let r = sqlx::query("DELETE FROM flow_variables WHERE scope = ? AND scope_id = ? AND name = ?")
67
        .bind(scope.kind())
68
        .bind(scope.id())
69
        .bind(name)
70
        .execute(pool)
71
        .await?;
72
    Ok(r.rows_affected() == 1)
73
}
74
75
/// Forget a level's variables, when the organisation or repository goes.
76
pub async fn delete_scope(pool: &Pool, scope: Scope) -> sqlx::Result<()> {
77
    sqlx::query("DELETE FROM flow_variables WHERE scope = ? AND scope_id = ?").bind(scope.kind()).bind(scope.id()).execute(pool).await?;
78
    Ok(())
79
}
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.
94
pub async fn seen(pool: &Pool, scope: Scope, owner: &str) -> sqlx::Result<Vec<Seen>> {
95
    let mut lvls = levels::above(pool, scope, owner).await?;
96
    lvls.push((scope, String::new()));
97
    let mut out: Vec<Seen> = vec![];
98
    for (level, from) in lvls {
99
        for v in list(pool, level).await? {
100
            for o in out.iter_mut().filter(|o| o.variable.name == v.name) {
101
                o.overridden = true;
102
            }
103
            out.push(Seen { variable: v, from: from.clone(), overridden: false });
104
        }
105
    }
106
    Ok(out)
107
}
108
109
/// The variables a repository's jobs get.
110
pub async fn env_for_repo(pool: &Pool, repo: &crate::db::repos::RepoRow) -> sqlx::Result<BTreeMap<String, String>> {
111
    Ok(seen(pool, Scope::Repo(repo.id), &repo.owner)
112
        .await?
113
        .into_iter()
114
        .filter(|s| !s.overridden)
115
        .map(|s| (s.variable.name, s.variable.value))
116
        .collect())
117
}
118
119
#[cfg(test)]
120
mod tests {
121
    use super::*;
122
123
    #[test]
124
    fn names_and_values_are_checked() {
125
        assert!(name_ok("CC") && name_ok("_x") && name_ok("RUST_LOG"));
126
        assert!(!name_ok("") && !name_ok("1A") && !name_ok("A-B") && !name_ok("FLOW_REPO") && !name_ok(&"A".repeat(65)));
127
        assert!(value_ok("clang") && value_ok("two\nlines\twith tab") && value_ok(""));
128
        assert!(!value_ok("a\u{0}b") && !value_ok(&"x".repeat(MAX_VALUE + 1)));
129
    }
130
}