OXYGENHUB
◆ v0.6.1 oxygenhub / crates / hub / src / db / terraform.rs
4.8 KiB · Rust · last changed Terraform state: list, download and restore kept versions 18 days ago source blame history raw
8dcaeeb8 Andy McCall ↶ 1
//! Terraform and OpenTofu states (migration 0079, oxygenhub#252).
2
3
use super::{now_ms, Pool};
4
use sqlx::FromRow;
5
6
#[derive(Clone, Debug, FromRow)]
7
pub struct TfState {
8
    pub repo_id: i64,
9
    pub name: String,
10
    /// Sealed with the hub's secrets key; empty once deleted.
11
    pub body: Vec<u8>,
12
    pub serial: i64,
13
    pub lock_id: String,
14
    pub lock_info: String,
15
    pub locked_by: String,
16
    pub locked_ms: Option<i64>,
17
    pub updated_by: String,
18
    pub updated_ms: i64,
19
}
20
21
pub async fn get(pool: &Pool, repo_id: i64, name: &str) -> sqlx::Result<Option<TfState>> {
22
    sqlx::query_as("SELECT * FROM terraform_states WHERE repo_id = $1 AND name = $2").bind(repo_id).bind(name).fetch_optional(pool).await
23
}
24
25
pub async fn of_repo(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<TfState>> {
26
    sqlx::query_as("SELECT * FROM terraform_states WHERE repo_id = $1 ORDER BY name").bind(repo_id).fetch_all(pool).await
27
}
28
29
/// How many earlier versions a state keeps.
30
pub async fn versions(pool: &Pool, repo_id: i64, name: &str) -> sqlx::Result<i64> {
31
    sqlx::query_scalar("SELECT COUNT(*) FROM terraform_state_versions WHERE repo_id = $1 AND name = $2")
32
        .bind(repo_id)
33
        .bind(name)
34
        .fetch_one(pool)
35
        .await
36
}
37
67523e06 Andy McCall ↶ 38
/// A kept version of a state (#255), without its body.
39
#[derive(Clone, Debug, FromRow)]
40
pub struct TfVersion {
41
    pub id: i64,
42
    pub serial: i64,
43
    pub created_by: String,
44
    pub created_ms: i64,
45
}
46
47
/// A state's kept versions, newest first.
48
pub async fn version_list(pool: &Pool, repo_id: i64, name: &str) -> sqlx::Result<Vec<TfVersion>> {
49
    sqlx::query_as(
50
        "SELECT id, serial, created_by, created_ms FROM terraform_state_versions WHERE repo_id = $1 AND name = $2 ORDER BY id DESC",
51
    )
52
    .bind(repo_id)
53
    .bind(name)
54
    .fetch_all(pool)
55
    .await
56
}
57
58
/// One kept version's sealed body and serial.
59
pub async fn version_body(pool: &Pool, repo_id: i64, name: &str, id: i64) -> sqlx::Result<Option<(Vec<u8>, i64)>> {
60
    sqlx::query_as("SELECT body, serial FROM terraform_state_versions WHERE repo_id = $1 AND name = $2 AND id = $3")
61
        .bind(repo_id)
62
        .bind(name)
63
        .bind(id)
64
        .fetch_optional(pool)
65
        .await
66
}
67
8dcaeeb8 Andy McCall ↶ 68
/// Store a new body; the one it replaces becomes a version, and only the
69
/// last `keep` versions stay.
70
pub async fn put(pool: &Pool, repo_id: i64, name: &str, body: &[u8], serial: i64, by: &str, keep: i64) -> sqlx::Result<()> {
71
    let now = now_ms();
72
    let mut tx = pool.begin().await?;
73
    sqlx::query(
74
        "INSERT INTO terraform_state_versions (repo_id, name, body, serial, created_by, created_ms) \
75
         SELECT repo_id, name, body, serial, updated_by, updated_ms FROM terraform_states \
76
         WHERE repo_id = $1 AND name = $2 AND length(body) > 0",
77
    )
78
    .bind(repo_id)
79
    .bind(name)
80
    .execute(&mut *tx)
81
    .await?;
82
    sqlx::query(
83
        "INSERT INTO terraform_states (repo_id, name, body, serial, updated_by, updated_ms) VALUES ($1, $2, $3, $4, $5, $6) \
84
         ON CONFLICT (repo_id, name) DO UPDATE SET body = $3, serial = $4, updated_by = $5, updated_ms = $6",
85
    )
86
    .bind(repo_id)
87
    .bind(name)
88
    .bind(body)
89
    .bind(serial)
90
    .bind(by)
91
    .bind(now)
92
    .execute(&mut *tx)
93
    .await?;
94
    sqlx::query(
95
        "DELETE FROM terraform_state_versions WHERE repo_id = $1 AND name = $2 AND id NOT IN \
96
         (SELECT id FROM terraform_state_versions WHERE repo_id = $1 AND name = $2 ORDER BY id DESC LIMIT $3)",
97
    )
98
    .bind(repo_id)
99
    .bind(name)
100
    .bind(keep)
101
    .execute(&mut *tx)
102
    .await?;
103
    tx.commit().await
104
}
105
106
/// Delete a state: its body goes (its versions stay, for a mistake).
107
pub async fn delete(pool: &Pool, repo_id: i64, name: &str) -> sqlx::Result<()> {
108
    sqlx::query("DELETE FROM terraform_states WHERE repo_id = $1 AND name = $2").bind(repo_id).bind(name).execute(pool).await?;
109
    Ok(())
110
}
111
112
/// Take the lock; false when someone holds it.
113
pub async fn lock(pool: &Pool, repo_id: i64, name: &str, id: &str, info: &str, by: &str) -> sqlx::Result<bool> {
114
    let now = now_ms();
115
    let r = sqlx::query(
116
        "INSERT INTO terraform_states (repo_id, name, body, lock_id, lock_info, locked_by, locked_ms, updated_ms) \
117
         VALUES ($1, $2, $3, $4, $5, $6, $7, $7) \
118
         ON CONFLICT (repo_id, name) DO UPDATE SET lock_id = $4, lock_info = $5, locked_by = $6, locked_ms = $7 \
119
         WHERE terraform_states.lock_id = ''",
120
    )
121
    .bind(repo_id)
122
    .bind(name)
123
    .bind(Vec::<u8>::new())
124
    .bind(id)
125
    .bind(info)
126
    .bind(by)
127
    .bind(now)
128
    .execute(pool)
129
    .await?;
130
    Ok(r.rows_affected() == 1)
131
}
132
133
pub async fn unlock(pool: &Pool, repo_id: i64, name: &str) -> sqlx::Result<()> {
134
    sqlx::query(
135
        "UPDATE terraform_states SET lock_id = '', lock_info = '', locked_by = '', locked_ms = NULL WHERE repo_id = $1 AND name = $2",
136
    )
137
    .bind(repo_id)
138
    .bind(name)
139
    .execute(pool)
140
    .await?;
141
    Ok(())
142
}