//! Terraform and OpenTofu states (migration 0079, oxygenhub#252). use super::{now_ms, Pool}; use sqlx::FromRow; #[derive(Clone, Debug, FromRow)] pub struct TfState { pub repo_id: i64, pub name: String, /// Sealed with the hub's secrets key; empty once deleted. pub body: Vec, pub serial: i64, pub lock_id: String, pub lock_info: String, pub locked_by: String, pub locked_ms: Option, pub updated_by: String, pub updated_ms: i64, } pub async fn get(pool: &Pool, repo_id: i64, name: &str) -> sqlx::Result> { sqlx::query_as("SELECT * FROM terraform_states WHERE repo_id = $1 AND name = $2").bind(repo_id).bind(name).fetch_optional(pool).await } pub async fn of_repo(pool: &Pool, repo_id: i64) -> sqlx::Result> { sqlx::query_as("SELECT * FROM terraform_states WHERE repo_id = $1 ORDER BY name").bind(repo_id).fetch_all(pool).await } /// How many earlier versions a state keeps. pub async fn versions(pool: &Pool, repo_id: i64, name: &str) -> sqlx::Result { sqlx::query_scalar("SELECT COUNT(*) FROM terraform_state_versions WHERE repo_id = $1 AND name = $2") .bind(repo_id) .bind(name) .fetch_one(pool) .await } /// A kept version of a state (#255), without its body. #[derive(Clone, Debug, FromRow)] pub struct TfVersion { pub id: i64, pub serial: i64, pub created_by: String, pub created_ms: i64, } /// A state's kept versions, newest first. pub async fn version_list(pool: &Pool, repo_id: i64, name: &str) -> sqlx::Result> { sqlx::query_as( "SELECT id, serial, created_by, created_ms FROM terraform_state_versions WHERE repo_id = $1 AND name = $2 ORDER BY id DESC", ) .bind(repo_id) .bind(name) .fetch_all(pool) .await } /// One kept version's sealed body and serial. pub async fn version_body(pool: &Pool, repo_id: i64, name: &str, id: i64) -> sqlx::Result, i64)>> { sqlx::query_as("SELECT body, serial FROM terraform_state_versions WHERE repo_id = $1 AND name = $2 AND id = $3") .bind(repo_id) .bind(name) .bind(id) .fetch_optional(pool) .await } /// Store a new body; the one it replaces becomes a version, and only the /// last `keep` versions stay. pub async fn put(pool: &Pool, repo_id: i64, name: &str, body: &[u8], serial: i64, by: &str, keep: i64) -> sqlx::Result<()> { let now = now_ms(); let mut tx = pool.begin().await?; sqlx::query( "INSERT INTO terraform_state_versions (repo_id, name, body, serial, created_by, created_ms) \ SELECT repo_id, name, body, serial, updated_by, updated_ms FROM terraform_states \ WHERE repo_id = $1 AND name = $2 AND length(body) > 0", ) .bind(repo_id) .bind(name) .execute(&mut *tx) .await?; sqlx::query( "INSERT INTO terraform_states (repo_id, name, body, serial, updated_by, updated_ms) VALUES ($1, $2, $3, $4, $5, $6) \ ON CONFLICT (repo_id, name) DO UPDATE SET body = $3, serial = $4, updated_by = $5, updated_ms = $6", ) .bind(repo_id) .bind(name) .bind(body) .bind(serial) .bind(by) .bind(now) .execute(&mut *tx) .await?; sqlx::query( "DELETE FROM terraform_state_versions WHERE repo_id = $1 AND name = $2 AND id NOT IN \ (SELECT id FROM terraform_state_versions WHERE repo_id = $1 AND name = $2 ORDER BY id DESC LIMIT $3)", ) .bind(repo_id) .bind(name) .bind(keep) .execute(&mut *tx) .await?; tx.commit().await } /// Delete a state: its body goes (its versions stay, for a mistake). pub async fn delete(pool: &Pool, repo_id: i64, name: &str) -> sqlx::Result<()> { sqlx::query("DELETE FROM terraform_states WHERE repo_id = $1 AND name = $2").bind(repo_id).bind(name).execute(pool).await?; Ok(()) } /// Take the lock; false when someone holds it. pub async fn lock(pool: &Pool, repo_id: i64, name: &str, id: &str, info: &str, by: &str) -> sqlx::Result { let now = now_ms(); let r = sqlx::query( "INSERT INTO terraform_states (repo_id, name, body, lock_id, lock_info, locked_by, locked_ms, updated_ms) \ VALUES ($1, $2, $3, $4, $5, $6, $7, $7) \ ON CONFLICT (repo_id, name) DO UPDATE SET lock_id = $4, lock_info = $5, locked_by = $6, locked_ms = $7 \ WHERE terraform_states.lock_id = ''", ) .bind(repo_id) .bind(name) .bind(Vec::::new()) .bind(id) .bind(info) .bind(by) .bind(now) .execute(pool) .await?; Ok(r.rows_affected() == 1) } pub async fn unlock(pool: &Pool, repo_id: i64, name: &str) -> sqlx::Result<()> { sqlx::query( "UPDATE terraform_states SET lock_id = '', lock_info = '', locked_by = '', locked_ms = NULL WHERE repo_id = $1 AND name = $2", ) .bind(repo_id) .bind(name) .execute(pool) .await?; Ok(()) }