use askama::Template; use askama_web::WebTemplate; use axum::extract::State; use axum::response::Redirect; use axum::Form; use k8s_openapi::api::core::v1::Secret; use kube::api::{ListParams, PostParams}; use kube::Api; use serde::Deserialize; use crate::auth_guard::AuthUser; use crate::k8s::{SojuBouncer, SojuBouncerSpec, sanitize_bouncer_name}; use crate::state::AppState; #[derive(Template, WebTemplate)] #[template(path = "bouncers.html")] pub struct BouncersTemplate { pub bouncers: Vec, } pub struct BouncerInfo { pub name: String, pub hostname: String, } pub async fn list( State(state): State, AuthUser(user): AuthUser, ) -> BouncersTemplate { let api: Api = Api::namespaced(state.kube, &state.namespace); let lp = ListParams::default().labels(&format!("irc.josie.cloud/owner={}", user.sub)); let bouncers = api .list(&lp) .await .map(|list| { list.items .into_iter() .map(|b| BouncerInfo { name: b.metadata.name.unwrap_or_default(), hostname: b.spec.hostname, }) .collect() }) .unwrap_or_default(); BouncersTemplate { bouncers } } #[derive(Deserialize)] pub struct CreateForm { pub name: String, } pub async fn create( State(state): State, AuthUser(user): AuthUser, Form(form): Form, ) -> Result { let name = sanitize_bouncer_name(&form.name); if name.is_empty() { return Err(Redirect::to("/bouncers")); } let bouncer = SojuBouncer::new(&name, SojuBouncerSpec { hostname: format!("{name}.irc.now"), owner: user.sub.clone(), hostname_cloak: Some(format!("{name}.irc.now")), }); let mut bouncer = bouncer; bouncer .metadata .labels .get_or_insert_with(Default::default) .insert( "irc.josie.cloud/owner".to_string(), user.sub, ); let api: Api = Api::namespaced(state.kube.clone(), &state.namespace); api.create(&PostParams::default(), &bouncer) .await .map_err(|e| { tracing::error!("failed to create bouncer: {e}"); Redirect::to("/bouncers") })?; if let Err(e) = add_default_upstream(&state, &name).await { tracing::warn!("failed to add default upstream: {e}"); } Ok(Redirect::to("/bouncers")) } async fn add_default_upstream( state: &AppState, bouncer_name: &str, ) -> Result<(), Box> { let secret_api: Api = Api::namespaced(state.kube.clone(), &state.namespace); let secret = secret_api.get(&format!("{bouncer_name}-db")).await?; let uri = secret .data .as_ref() .and_then(|d| d.get("uri")) .map(|b| String::from_utf8_lossy(&b.0).to_string()) .ok_or("missing uri in db secret")?; let (client, connection) = tokio_postgres::connect(&uri, tokio_postgres::NoTls).await?; tokio::spawn(async move { if let Err(e) = connection.await { tracing::warn!("soju db connection error: {e}"); } }); client .execute( r#"INSERT INTO "Network" ("user", name, addr, nick, enabled, connect_commands) SELECT id, 'irc.now', 'irc-now-net:6697', username, true, '' FROM "User" LIMIT 1 ON CONFLICT DO NOTHING"#, &[], ) .await?; Ok(()) }