[
  {
    "type": "text",
    "text": "Perfect! Now I have all the content. Let me compile a comprehensive report with all the source files.\n\n## Complete soju-operator Crate Source Files\n\nHere is the full content of every file in the soju-operator crate:\n\n### Root Workspace Configuration\n\n**File: `/home/josie/development/irc-now/Cargo.toml`**\n```toml\n[workspace]\nresolver = \"3\"\nmembers = [\n    \"crates/bot-landing\",\n    \"crates/common\",\n    \"crates/soju-operator\",\n    \"crates/web-api\",\n    \"crates/web-landing\",\n    \"crates/paste\",\n    \"crates/pics\",\n]\n\n[workspace.package]\nedition = \"2024\"\nlicense = \"MIT\"\n\n[workspace.dependencies]\naxum = \"0.8\"\ntokio = { version = \"1\", features = [\"macros\", \"rt-multi-thread\"] }\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\nsqlx = { version = \"0.8\", features = [\"runtime-tokio\", \"postgres\"] }\ntracing = \"0.1\"\ntracing-subscriber = { version = \"0.3\", features = [\"env-filter\"] }\nthiserror = \"2\"\nanyhow = \"1\"\nnanoid = \"0.4\"\ntime = { version = \"0.3\", features = [\"serde\"] }\n```\n\n### Soju-Operator Package Files\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/Cargo.toml`**\n```toml\n[package]\nname = \"soju-operator\"\nversion = \"0.1.0\"\nedition.workspace = true\n\n[[bin]]\nname = \"soju-operator\"\npath = \"src/main.rs\"\n\n[[bin]]\nname = \"crdgen\"\npath = \"src/crdgen.rs\"\n\n[dependencies]\nkube = { version = \"3\", features = [\"runtime\", \"client\", \"derive\"] }\nk8s-openapi = { version = \"0.27\", features = [\"latest\"] }\nk8s-crds-cert-manager = \"1.19\"\nschemars = \"1\"\nserde.workspace = true\nserde_json.workspace = true\nserde_yaml = \"0.9\"\ntokio.workspace = true\ntokio-postgres = \"0.7\"\ndeadpool-postgres = { version = \"0.14\", features = [\"rt_tokio_1\"] }\nchrono = \"0.4\"\nfutures = \"0.3\"\nthiserror.workspace = true\nanyhow.workspace = true\ntracing.workspace = true\ntracing-subscriber.workspace = true\nrand = \"0.9\"\n\n[dev-dependencies]\ntower-test = \"0.4\"\nhttp = \"1\"\nhyper = \"1\"\n```\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/Containerfile`**\n```dockerfile\nFROM rust:1.88 AS builder\nWORKDIR /build\nCOPY Cargo.toml Cargo.lock ./\nCOPY src/ src/\nRUN cargo build --release --bin soju-operator\n\nFROM registry.access.redhat.com/ubi9-minimal:latest\nCOPY --from=builder /build/target/release/soju-operator /usr/local/bin/soju-operator\nENTRYPOINT [\"soju-operator\"]\n```\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/src/main.rs`**\n```rust\nuse std::sync::Arc;\n\nuse futures::StreamExt;\nuse k8s_crds_cert_manager::certificates::Certificate;\nuse k8s_openapi::api::apps::v1::Deployment;\nuse k8s_openapi::api::core::v1::{ConfigMap, Secret, Service};\nuse kube::api::Api;\nuse kube::runtime::controller::Controller;\nuse kube::runtime::watcher;\nuse kube::Client;\n\nuse soju_operator::controller::{self, Context};\nuse soju_operator::SojuBouncer;\n\n#[tokio::main]\nasync fn main() -> anyhow::Result<()> {\n    tracing_subscriber::fmt()\n        .with_env_filter(\n            tracing_subscriber::EnvFilter::from_default_env()\n                .add_directive(\"soju_operator=info\".parse()?),\n        )\n        .init();\n\n    let client = Client::try_default().await?;\n\n    let db_master_uri =\n        std::env::var(\"SOJU_DB_MASTER_URI\").expect(\"SOJU_DB_MASTER_URI must be set\");\n    let db_host = std::env::var(\"SOJU_DB_HOST\").expect(\"SOJU_DB_HOST must be set\");\n    let db_port: u16 = std::env::var(\"SOJU_DB_PORT\")\n        .unwrap_or_else(|_| \"5432\".to_string())\n        .parse()?;\n\n    let mut db_config = deadpool_postgres::Config::new();\n    db_config.url = Some(db_master_uri);\n    let db_pool = db_config\n        .create_pool(Some(deadpool_postgres::Runtime::Tokio1), tokio_postgres::NoTls)?;\n\n    let ctx = Arc::new(Context {\n        client: client.clone(),\n        db_host,\n        db_port,\n        db_pool,\n    });\n\n    let bouncers = Api::<SojuBouncer>::all(client.clone());\n    let deployments = Api::<Deployment>::all(client.clone());\n    let configmaps = Api::<ConfigMap>::all(client.clone());\n    let services = Api::<Service>::all(client.clone());\n    let secrets = Api::<Secret>::all(client.clone());\n    let certificates = Api::<Certificate>::all(client.clone());\n\n    tracing::info!(\"starting soju-operator\");\n\n    Controller::new(bouncers, watcher::Config::default())\n        .owns(deployments, watcher::Config::default())\n        .owns(configmaps, watcher::Config::default())\n        .owns(services, watcher::Config::default())\n        .owns(secrets, watcher::Config::default())\n        .owns(certificates, watcher::Config::default())\n        .shutdown_on_signal()\n        .run(controller::reconcile, controller::error_policy, ctx)\n        .for_each(|res| async move {\n            match res {\n                Ok(o) => tracing::info!(\"reconciled {:?}\", o),\n                Err(e) => tracing::warn!(\"reconcile failed: {:?}\", e),\n            }\n        })\n        .await;\n\n    Ok(())\n}\n```\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/src/lib.rs`**\n```rust\npub mod types;\npub mod controller;\npub mod resources;\npub mod db;\n\n#[cfg(test)]\npub(crate) mod testutil;\n\npub use types::SojuBouncer;\n```\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/src/crdgen.rs`**\n```rust\nuse kube::CustomResourceExt;\nuse soju_operator::SojuBouncer;\n\nfn main() {\n    print!(\"{}\", serde_yaml::to_string(&SojuBouncer::crd()).unwrap());\n}\n```\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/src/types.rs`**\n```rust\nuse kube::CustomResource;\nuse schemars::JsonSchema;\nuse serde::{Deserialize, Serialize};\n\n#[derive(CustomResource, Serialize, Deserialize, Default, Debug, Clone, JsonSchema)]\n#[kube(\n    group = \"irc.josie.cloud\",\n    version = \"v1alpha1\",\n    kind = \"SojuBouncer\",\n    namespaced,\n    status = \"SojuBouncerStatus\",\n    shortname = \"soju\",\n    printcolumn = r#\"{\"name\":\"Hostname\", \"type\":\"string\", \"jsonPath\":\".spec.hostname\"}\"#,\n    printcolumn = r#\"{\"name\":\"Ready\", \"type\":\"string\", \"jsonPath\":\".status.conditions[?(@.type==\\\"Ready\\\")].status\"}\"#\n)]\npub struct SojuBouncerSpec {\n    pub hostname: String,\n    #[serde(default)]\n    pub title: Option<String>,\n    #[serde(default)]\n    pub listeners: Vec<Listener>,\n    #[serde(default)]\n    pub tls: Option<TlsSpec>,\n    #[serde(default)]\n    pub route: Option<RouteSpec>,\n    #[serde(default)]\n    pub resources: Option<ResourceRequirements>,\n}\n\n#[derive(Serialize, Deserialize, Default, Debug, Clone, JsonSchema)]\npub struct Listener {\n    pub address: String,\n    #[serde(default)]\n    pub tls: bool,\n}\n\n#[derive(Serialize, Deserialize, Default, Debug, Clone, JsonSchema)]\npub struct TlsSpec {\n    pub issuer_ref: IssuerRef,\n    pub dns_names: Vec<String>,\n}\n\n#[derive(Serialize, Deserialize, Default, Debug, Clone, JsonSchema)]\npub struct IssuerRef {\n    pub name: String,\n    pub kind: String,\n}\n\n#[derive(Serialize, Deserialize, Default, Debug, Clone, JsonSchema)]\npub struct RouteSpec {\n    pub host: String,\n}\n\n#[derive(Serialize, Deserialize, Default, Debug, Clone, JsonSchema)]\npub struct ResourceRequirements {\n    #[serde(default)]\n    pub requests: Option<ResourceList>,\n    #[serde(default)]\n    pub limits: Option<ResourceList>,\n}\n\n#[derive(Serialize, Deserialize, Default, Debug, Clone, JsonSchema)]\npub struct ResourceList {\n    #[serde(default)]\n    pub memory: Option<String>,\n    #[serde(default)]\n    pub cpu: Option<String>,\n}\n\n#[derive(Serialize, Deserialize, Default, Debug, Clone, JsonSchema)]\npub struct SojuBouncerStatus {\n    #[serde(default)]\n    pub conditions: Vec<Condition>,\n    #[serde(default)]\n    pub observed_generation: Option<i64>,\n}\n\n#[derive(Serialize, Deserialize, Default, Debug, Clone, JsonSchema)]\npub struct Condition {\n    #[serde(rename = \"type\")]\n    pub type_: String,\n    pub status: String,\n    pub reason: String,\n    pub message: String,\n    pub last_transition_time: String,\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use kube::CustomResourceExt;\n\n    #[test]\n    fn crd_generates_valid_schema() {\n        let crd = SojuBouncer::crd();\n        let name = crd.metadata.name.unwrap();\n        assert_eq!(name, \"sojubouncers.irc.josie.cloud\");\n    }\n\n    #[test]\n    fn default_spec_has_sane_values() {\n        let spec = SojuBouncerSpec::default();\n        assert!(spec.listeners.is_empty());\n        assert!(spec.tls.is_none());\n        assert!(spec.route.is_none());\n        assert!(spec.resources.is_none());\n    }\n}\n```\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/src/controller.rs`**\n```rust\nuse std::sync::Arc;\nuse std::time::Duration;\n\nuse k8s_openapi::api::apps::v1::Deployment;\nuse k8s_openapi::api::core::v1::{ConfigMap, Secret, Service};\nuse k8s_crds_cert_manager::certificates::Certificate;\nuse kube::api::{Api, DynamicObject, Patch, PatchParams};\nuse kube::core::{ApiResource, GroupVersionKind};\nuse kube::runtime::controller::Action;\nuse kube::runtime::finalizer::{finalizer, Event};\n\nuse crate::db;\nuse crate::resources::certificate::build_certificate;\nuse crate::resources::configmap::build_configmap;\nuse crate::resources::deployment::build_deployment;\nuse crate::resources::route::build_route;\nuse crate::resources::secret::build_db_secret;\nuse crate::resources::service::build_service;\nuse crate::types::{Condition, SojuBouncer, SojuBouncerStatus};\n\n#[derive(Debug, thiserror::Error)]\npub enum Error {\n    #[error(\"Kube error: {0}\")]\n    Kube(#[source] kube::Error),\n\n    #[error(\"Finalizer error: {0}\")]\n    Finalizer(#[source] Box<kube::runtime::finalizer::Error<Error>>),\n\n    #[error(\"Database error: {0}\")]\n    Db(String),\n\n    #[error(\"Missing field: {0}\")]\n    MissingField(String),\n\n    #[error(\"Serialization error: {0}\")]\n    Serialization(#[source] serde_json::Error),\n}\n\npub struct Context {\n    pub client: kube::Client,\n    pub db_host: String,\n    pub db_port: u16,\n    pub db_pool: deadpool_postgres::Pool,\n}\n\nstatic FINALIZER_NAME: &str = \"irc.josie.cloud/db-cleanup\";\nstatic FIELD_MANAGER: &str = \"soju-operator\";\n\npub async fn reconcile(\n    bouncer: Arc<SojuBouncer>,\n    ctx: Arc<Context>,\n) -> Result<Action, Error> {\n    let client = &ctx.client;\n    let ns = bouncer\n        .metadata\n        .namespace\n        .as_deref()\n        .ok_or_else(|| Error::MissingField(\"metadata.namespace\".to_string()))?;\n\n    let api: Api<SojuBouncer> = Api::namespaced(client.clone(), ns);\n\n    finalizer(&api, FINALIZER_NAME, bouncer, |event| async {\n        match event {\n            Event::Apply(bouncer) => {\n                let result = reconcile_apply(&bouncer, &ctx).await;\n                if let Err(ref e) = result {\n                    let _ = update_status(&ctx.client, &bouncer, false, &e.to_string()).await;\n                }\n                result\n            }\n            Event::Cleanup(bouncer) => reconcile_cleanup(&bouncer, &ctx).await,\n        }\n    })\n    .await\n    .map_err(|e| Error::Finalizer(Box::new(e)))\n}\n\nasync fn reconcile_apply(bouncer: &SojuBouncer, ctx: &Context) -> Result<Action, Error> {\n    let client = &ctx.client;\n    let ns = bouncer\n        .metadata\n        .namespace\n        .as_deref()\n        .ok_or_else(|| Error::MissingField(\"metadata.namespace\".to_string()))?;\n    let name = bouncer\n        .metadata\n        .name\n        .as_deref()\n        .ok_or_else(|| Error::MissingField(\"metadata.name\".to_string()))?;\n\n    tracing::info!(bouncer = name, namespace = ns, \"reconciling\");\n\n    let password = get_or_create_password(client, ns, name).await?;\n    let tenant_uri = db::build_tenant_uri(&ctx.db_host, ctx.db_port, name, &password);\n\n    let db_client = ctx.db_pool.get().await.map_err(|e| Error::Db(e.to_string()))?;\n    db::provision_tenant_db(&db_client, name, &password)\n        .await\n        .map_err(|e| Error::Db(e.to_string()))?;\n\n    let secret = build_db_secret(bouncer, &tenant_uri, &password);\n    let secret_api: Api<Secret> = Api::namespaced(client.clone(), ns);\n    let secret_name = secret.metadata.name.as_deref().unwrap();\n    secret_api\n        .patch(\n            secret_name,\n            &PatchParams::apply(FIELD_MANAGER).force(),\n            &Patch::Apply(&secret),\n        )\n        .await\n        .map_err(Error::Kube)?;\n    tracing::info!(bouncer = name, \"applied db secret\");\n\n    if bouncer.spec.tls.is_some() {\n        let cert = build_certificate(bouncer);\n        let cert_api: Api<Certificate> = Api::namespaced(client.clone(), ns);\n        let cert_name = cert.metadata.name.as_deref().unwrap();\n        cert_api\n            .patch(\n                cert_name,\n                &PatchParams::apply(FIELD_MANAGER).force(),\n                &Patch::Apply(&cert),\n            )\n            .await\n            .map_err(Error::Kube)?;\n        tracing::info!(bouncer = name, \"applied certificate\");\n    }\n\n    let cm = build_configmap(bouncer, &tenant_uri);\n    let cm_api: Api<ConfigMap> = Api::namespaced(client.clone(), ns);\n    let cm_name = cm.metadata.name.as_deref().unwrap();\n    cm_api\n        .patch(\n            cm_name,\n            &PatchParams::apply(FIELD_MANAGER).force(),\n            &Patch::Apply(&cm),\n        )\n        .await\n        .map_err(Error::Kube)?;\n    tracing::info!(bouncer = name, \"applied configmap\");\n\n    let dep = build_deployment(bouncer);\n    let dep_api: Api<Deployment> = Api::namespaced(client.clone(), ns);\n    dep_api\n        .patch(\n            name,\n            &PatchParams::apply(FIELD_MANAGER).force(),\n            &Patch::Apply(&dep),\n        )\n        .await\n        .map_err(Error::Kube)?;\n    tracing::info!(bouncer = name, \"applied deployment\");\n\n    let svc = build_service(bouncer);\n    let svc_api: Api<Service> = Api::namespaced(client.clone(), ns);\n    svc_api\n        .patch(\n            name,\n            &PatchParams::apply(FIELD_MANAGER).force(),\n            &Patch::Apply(&svc),\n        )\n        .await\n        .map_err(Error::Kube)?;\n    tracing::info!(bouncer = name, \"applied service\");\n\n    if bouncer.spec.route.is_some() {\n        let route = build_route(bouncer);\n        let gvk = GroupVersionKind::gvk(\"route.openshift.io\", \"v1\", \"Route\");\n        let ar = ApiResource::from_gvk_with_plural(&gvk, \"routes\");\n        let route_api: Api<DynamicObject> = Api::namespaced_with(client.clone(), ns, &ar);\n        let patch_value = serde_json::json!({\n            \"apiVersion\": \"route.openshift.io/v1\",\n            \"kind\": \"Route\",\n            \"metadata\": route.metadata,\n            \"spec\": route.spec,\n        });\n        route_api\n            .patch(\n                name,\n                &PatchParams::apply(FIELD_MANAGER).force(),\n                &Patch::Apply(&patch_value),\n            )\n            .await\n            .map_err(Error::Kube)?;\n        tracing::info!(bouncer = name, \"applied route\");\n    }\n\n    update_status(client, bouncer, true, \"all resources applied\").await?;\n    tracing::info!(bouncer = name, \"reconciled\");\n\n    Ok(Action::requeue(Duration::from_secs(300)))\n}\n\nasync fn reconcile_cleanup(bouncer: &SojuBouncer, ctx: &Context) -> Result<Action, Error> {\n    let name = bouncer\n        .metadata\n        .name\n        .as_deref()\n        .ok_or_else(|| Error::MissingField(\"metadata.name\".to_string()))?;\n\n    tracing::info!(bouncer = name, \"cleaning up\");\n\n    let db_client = ctx.db_pool.get().await.map_err(|e| Error::Db(e.to_string()))?;\n    db::deprovision_tenant_db(&db_client, name)\n        .await\n        .map_err(|e| Error::Db(e.to_string()))?;\n\n    tracing::info!(bouncer = name, \"deprovisioned tenant database\");\n    Ok(Action::await_change())\n}\n\npub fn error_policy(\n    _bouncer: Arc<SojuBouncer>,\n    error: &Error,\n    _ctx: Arc<Context>,\n) -> Action {\n    tracing::warn!(%error, \"reconcile failed\");\n    Action::requeue(Duration::from_secs(60))\n}\n\nasync fn get_or_create_password(\n    client: &kube::Client,\n    ns: &str,\n    bouncer_name: &str,\n) -> Result<String, Error> {\n    let secret_api: Api<Secret> = Api::namespaced(client.clone(), ns);\n    let secret_name = format!(\"{bouncer_name}-db\");\n\n    match secret_api.get_opt(&secret_name).await.map_err(Error::Kube)? {\n        Some(existing) => {\n            if let Some(string_data) = &existing.string_data {\n                if let Some(pw) = string_data.get(\"password\") {\n                    return Ok(pw.clone());\n                }\n            }\n            if let Some(data) = &existing.data {\n                if let Some(pw_bytes) = data.get(\"password\") {\n                    return Ok(String::from_utf8_lossy(&pw_bytes.0).to_string());\n                }\n            }\n            Err(Error::MissingField(\"password key not found in existing db secret\".to_string()))\n        }\n        None => Ok(db::generate_password()),\n    }\n}\n\nasync fn update_status(\n    client: &kube::Client,\n    bouncer: &SojuBouncer,\n    ready: bool,\n    message: &str,\n) -> Result<(), Error> {\n    let ns = bouncer.metadata.namespace.as_deref().unwrap_or(\"default\");\n    let api = Api::<SojuBouncer>::namespaced(client.clone(), ns);\n    let name = bouncer.metadata.name.as_deref().unwrap();\n\n    let now = chrono::Utc::now().to_rfc3339();\n\n    let status = SojuBouncerStatus {\n        conditions: vec![Condition {\n            type_: \"Ready\".to_string(),\n            status: if ready { \"True\" } else { \"False\" }.to_string(),\n            reason: if ready { \"Reconciled\" } else { \"Error\" }.to_string(),\n            message: message.to_string(),\n            last_transition_time: now,\n        }],\n        observed_generation: bouncer.metadata.generation,\n    };\n\n    let patch = serde_json::json!({\n        \"apiVersion\": \"irc.josie.cloud/v1alpha1\",\n        \"kind\": \"SojuBouncer\",\n        \"status\": status,\n    });\n\n    api.patch_status(name, &PatchParams::apply(FIELD_MANAGER).force(), &Patch::Apply(&patch))\n        .await\n        .map_err(Error::Kube)?;\n\n    Ok(())\n}\n```\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/src/testutil.rs`**\n```rust\nuse crate::types::*;\n\npub fn test_bouncer() -> SojuBouncer {\n    SojuBouncer {\n        metadata: kube::api::ObjectMeta {\n            name: Some(\"test-bouncer\".to_string()),\n            namespace: Some(\"test-ns\".to_string()),\n            uid: Some(\"test-uid\".to_string()),\n            ..Default::default()\n        },\n        spec: SojuBouncerSpec {\n            hostname: \"irc.example.com\".to_string(),\n            title: Some(\"Test Bouncer\".to_string()),\n            listeners: vec![\n                Listener {\n                    address: \":6697\".to_string(),\n                    tls: true,\n                },\n                Listener {\n                    address: \":6667\".to_string(),\n                    tls: false,\n                },\n            ],\n            tls: Some(TlsSpec {\n                issuer_ref: IssuerRef {\n                    name: \"letsencrypt-prod\".to_string(),\n                    kind: \"ClusterIssuer\".to_string(),\n                },\n                dns_names: vec![\"irc.example.com\".to_string()],\n            }),\n            ..Default::default()\n        },\n        status: None,\n    }\n}\n```\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/src/db.rs`**\n```rust\nuse tokio_postgres::Client;\n\npub fn tenant_role_name(bouncer_name: &str) -> String {\n    format!(\"soju_{}\", bouncer_name.replace('-', \"_\"))\n}\n\npub fn tenant_db_name(bouncer_name: &str) -> String {\n    format!(\"soju_{}\", bouncer_name.replace('-', \"_\"))\n}\n\nfn quote_literal(s: &str) -> String {\n    format!(\"'{}'\", s.replace('\\'', \"''\"))\n}\n\npub fn create_role_sql(bouncer_name: &str, password: &str) -> String {\n    let role = tenant_role_name(bouncer_name);\n    format!(\"CREATE ROLE \\\"{role}\\\" WITH LOGIN PASSWORD {}\", quote_literal(password))\n}\n\npub fn create_database_sql(bouncer_name: &str) -> String {\n    let role = tenant_role_name(bouncer_name);\n    let db = tenant_db_name(bouncer_name);\n    format!(\"CREATE DATABASE \\\"{db}\\\" OWNER \\\"{role}\\\"\")\n}\n\npub fn drop_database_sql(bouncer_name: &str) -> String {\n    let db = tenant_db_name(bouncer_name);\n    format!(\"DROP DATABASE IF EXISTS \\\"{db}\\\"\")\n}\n\npub fn drop_role_sql(bouncer_name: &str) -> String {\n    let role = tenant_role_name(bouncer_name);\n    format!(\"DROP ROLE IF EXISTS \\\"{role}\\\"\")\n}\n\npub fn terminate_connections_sql(bouncer_name: &str) -> String {\n    let db = tenant_db_name(bouncer_name);\n    format!(\n        \"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = {}\",\n        quote_literal(&db)\n    )\n}\n\npub fn build_tenant_uri(host: &str, port: u16, bouncer_name: &str, password: &str) -> String {\n    let role = tenant_role_name(bouncer_name);\n    let db = tenant_db_name(bouncer_name);\n    format!(\"host={host} port={port} user={role} password={password} dbname={db} sslmode=disable\")\n}\n\npub fn generate_password() -> String {\n    use rand::Rng;\n    use rand::distr::Alphanumeric;\n    rand::rng()\n        .sample_iter(&Alphanumeric)\n        .take(32)\n        .map(char::from)\n        .collect()\n}\n\npub async fn provision_tenant_db(\n    client: &Client,\n    bouncer_name: &str,\n    password: &str,\n) -> Result<(), Box<dyn std::error::Error>> {\n    let role = tenant_role_name(bouncer_name);\n    let row = client\n        .query_one(\n            \"SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname = $1)\",\n            &[&role],\n        )\n        .await?;\n    let exists: bool = row.get(0);\n\n    if !exists {\n        client.batch_execute(&create_role_sql(bouncer_name, password)).await?;\n    }\n\n    let db = tenant_db_name(bouncer_name);\n    let row = client\n        .query_one(\n            \"SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = $1)\",\n            &[&db],\n        )\n        .await?;\n    let exists: bool = row.get(0);\n\n    if !exists {\n        client.batch_execute(&create_database_sql(bouncer_name)).await?;\n    }\n\n    Ok(())\n}\n\npub async fn deprovision_tenant_db(\n    client: &Client,\n    bouncer_name: &str,\n) -> Result<(), Box<dyn std::error::Error>> {\n    client.batch_execute(&terminate_connections_sql(bouncer_name)).await?;\n    client.batch_execute(&drop_database_sql(bouncer_name)).await?;\n    client.batch_execute(&drop_role_sql(bouncer_name)).await?;\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn tenant_role_name_replaces_hyphens() {\n        assert_eq!(tenant_role_name(\"my-bouncer\"), \"soju_my_bouncer\");\n    }\n\n    #[test]\n    fn tenant_db_name_replaces_hyphens() {\n        assert_eq!(tenant_db_name(\"my-bouncer\"), \"soju_my_bouncer\");\n    }\n\n    #[test]\n    fn create_role_sql_uses_quoted_identifier() {\n        let sql = create_role_sql(\"my-bouncer\", \"secretpass\");\n        assert!(sql.contains(\"\\\"soju_my_bouncer\\\"\"));\n        assert!(sql.contains(\"PASSWORD\"));\n    }\n\n    #[test]\n    fn create_database_sql_sets_owner() {\n        let sql = create_database_sql(\"my-bouncer\");\n        assert!(sql.contains(\"OWNER \\\"soju_my_bouncer\\\"\"));\n    }\n\n    #[test]\n    fn drop_database_sql_uses_if_exists() {\n        let sql = drop_database_sql(\"my-bouncer\");\n        assert!(sql.contains(\"DROP DATABASE IF EXISTS \\\"soju_my_bouncer\\\"\"));\n    }\n\n    #[test]\n    fn drop_role_sql_uses_if_exists() {\n        let sql = drop_role_sql(\"my-bouncer\");\n        assert!(sql.contains(\"DROP ROLE IF EXISTS \\\"soju_my_bouncer\\\"\"));\n    }\n\n    #[test]\n    fn terminate_connections_sql_targets_correct_db() {\n        let sql = terminate_connections_sql(\"my-bouncer\");\n        assert!(sql.contains(\"soju_my_bouncer\"));\n        assert!(sql.contains(\"pg_terminate_backend\"));\n    }\n\n    #[test]\n    fn build_tenant_uri_has_all_fields() {\n        let uri = build_tenant_uri(\"db.svc\", 5432, \"my-bouncer\", \"generated-pass\");\n        assert!(uri.contains(\"user=soju_my_bouncer\"));\n        assert!(uri.contains(\"dbname=soju_my_bouncer\"));\n        assert!(uri.contains(\"password=generated-pass\"));\n        assert!(uri.contains(\"host=db.svc\"));\n        assert!(uri.contains(\"port=5432\"));\n        assert!(uri.contains(\"sslmode=disable\"));\n    }\n}\n```\n\n### Resources Module Files\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/src/resources/mod.rs`**\n```rust\npub mod certificate;\npub mod configmap;\npub mod deployment;\npub mod route;\npub mod secret;\npub mod service;\n```\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/src/resources/configmap.rs`**\n```rust\nuse k8s_openapi::api::core::v1::ConfigMap;\nuse kube::api::ObjectMeta;\nuse kube::Resource;\n\nuse crate::types::{Listener, SojuBouncer, SojuBouncerSpec};\n\npub fn build_configmap(bouncer: &SojuBouncer, db_uri: &str) -> ConfigMap {\n    let name = bouncer.metadata.name.clone().unwrap();\n    let ns = bouncer.metadata.namespace.clone().unwrap();\n    let oref = bouncer.controller_owner_ref(&()).unwrap();\n\n    let config = generate_soju_config(&bouncer.spec, db_uri);\n\n    ConfigMap {\n        metadata: ObjectMeta {\n            name: Some(format!(\"{name}-config\")),\n            namespace: Some(ns),\n            owner_references: Some(vec![oref]),\n            ..Default::default()\n        },\n        data: Some([(\"config\".to_string(), config)].into()),\n        ..Default::default()\n    }\n}\n\nfn generate_soju_config(spec: &SojuBouncerSpec, db_uri: &str) -> String {\n    let mut lines = Vec::new();\n\n    lines.push(format!(\"hostname {}\", spec.hostname));\n\n    if let Some(title) = &spec.title {\n        lines.push(format!(\"title \\\"{title}\\\"\"));\n    }\n\n    if spec.tls.is_some() {\n        lines.push(\"tls /etc/soju/tls/tls.crt /etc/soju/tls/tls.key\".to_string());\n    }\n\n    for listener in &spec.listeners {\n        lines.push(format!(\"listen {}\", listener_uri(listener)));\n    }\n\n    lines.push(format!(\"db postgres \\\"{db_uri}\\\"\"));\n    lines.push(\"message-store db\".to_string());\n    lines.push(\"auth internal\".to_string());\n\n    lines.join(\"\\n\")\n}\n\nfn listener_uri(listener: &Listener) -> String {\n    let addr = &listener.address;\n    let host_port = if addr.starts_with(':') {\n        format!(\"0.0.0.0{addr}\")\n    } else {\n        addr.clone()\n    };\n\n    if listener.tls {\n        format!(\"ircs://{host_port}\")\n    } else {\n        format!(\"irc+insecure://{host_port}\")\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::testutil::test_bouncer;\n\n    #[test]\n    fn generates_soju_config_with_listeners() {\n        let db_uri = \"host=db.svc port=5432 user=test password=secret dbname=soju_test sslmode=disable\";\n        let cm = build_configmap(&test_bouncer(), db_uri);\n        let data = cm.data.unwrap();\n        let config = &data[\"config\"];\n        assert!(config.contains(\"listen ircs://0.0.0.0:6697\"));\n        assert!(config.contains(\"listen irc+insecure://0.0.0.0:6667\"));\n        assert!(config.contains(\"hostname irc.example.com\"));\n        assert!(config.contains(\"title \\\"Test Bouncer\\\"\"));\n        assert!(config.contains(\"tls /etc/soju/tls/tls.crt /etc/soju/tls/tls.key\"));\n        assert!(config.contains(\"db postgres\"));\n    }\n\n    #[test]\n    fn configmap_has_owner_reference() {\n        let cm = build_configmap(&test_bouncer(), \"host=localhost dbname=test\");\n        let orefs = cm.metadata.owner_references.unwrap();\n        assert_eq!(orefs.len(), 1);\n        assert_eq!(orefs[0].name, \"test-bouncer\");\n    }\n\n    #[test]\n    fn configmap_named_correctly() {\n        let cm = build_configmap(&test_bouncer(), \"host=localhost dbname=test\");\n        assert_eq!(cm.metadata.name.unwrap(), \"test-bouncer-config\");\n    }\n}\n```\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/src/resources/deployment.rs`**\n```rust\nuse std::collections::BTreeMap;\n\nuse k8s_openapi::api::apps::v1::{Deployment, DeploymentSpec};\nuse k8s_openapi::api::core::v1::{\n    ConfigMapVolumeSource, Container, ContainerPort, PodSpec, PodTemplateSpec,\n    SecretVolumeSource, SecurityContext, Volume, VolumeMount,\n};\nuse k8s_openapi::apimachinery::pkg::api::resource::Quantity;\nuse k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;\nuse kube::api::ObjectMeta;\nuse kube::Resource;\n\nuse crate::types::SojuBouncer;\n\nconst SOJU_IMAGE: &str = \"codeberg.org/emersion/soju:latest\";\n\npub fn build_deployment(bouncer: &SojuBouncer) -> Deployment {\n    let name = bouncer.metadata.name.clone().unwrap();\n    let ns = bouncer.metadata.namespace.clone().unwrap();\n    let oref = bouncer.controller_owner_ref(&()).unwrap();\n\n    let labels: BTreeMap<String, String> = [\n        (\"app.kubernetes.io/name\".to_string(), name.clone()),\n        (\n            \"app.kubernetes.io/managed-by\".to_string(),\n            \"soju-operator\".to_string(),\n        ),\n    ]\n    .into();\n\n    let mut volumes = vec![\n        Volume {\n            name: \"config\".to_string(),\n            config_map: Some(ConfigMapVolumeSource {\n                name: format!(\"{name}-config\"),\n                ..Default::default()\n            }),\n            ..Default::default()\n        },\n        Volume {\n            name: \"db\".to_string(),\n            secret: Some(SecretVolumeSource {\n                secret_name: Some(format!(\"{name}-db\")),\n                ..Default::default()\n            }),\n            ..Default::default()\n        },\n    ];\n\n    let mut volume_mounts = vec![\n        VolumeMount {\n            name: \"config\".to_string(),\n            mount_path: \"/etc/soju/\".to_string(),\n            ..Default::default()\n        },\n        VolumeMount {\n            name: \"db\".to_string(),\n            mount_path: \"/etc/soju/db/\".to_string(),\n            ..Default::default()\n        },\n    ];\n\n    if bouncer.spec.tls.is_some() {\n        volumes.push(Volume {\n            name: \"tls\".to_string(),\n            secret: Some(SecretVolumeSource {\n                secret_name: Some(format!(\"{name}-tls\")),\n                ..Default::default()\n            }),\n            ..Default::default()\n        });\n        volume_mounts.push(VolumeMount {\n            name: \"tls\".to_string(),\n            mount_path: \"/etc/soju/tls/\".to_string(),\n            ..Default::default()\n        });\n    }\n\n    let ports: Vec<ContainerPort> = bouncer\n        .spec\n        .listeners\n        .iter()\n        .filter_map(|l| {\n            l.address\n                .rsplit(':')\n                .next()\n                .and_then(|p| p.parse::<i32>().ok())\n                .map(|port| ContainerPort {\n                    container_port: port,\n                    ..Default::default()\n                })\n        })\n        .collect();\n\n    let resources = bouncer.spec.resources.as_ref().map(|r| {\n        let mut k8s_req = k8s_openapi::api::core::v1::ResourceRequirements::default();\n        if let Some(requests) = &r.requests {\n            let mut map = BTreeMap::new();\n            if let Some(cpu) = &requests.cpu {\n                map.insert(\"cpu\".to_string(), Quantity(cpu.clone()));\n            }\n            if let Some(memory) = &requests.memory {\n                map.insert(\"memory\".to_string(), Quantity(memory.clone()));\n            }\n            if !map.is_empty() {\n                k8s_req.requests = Some(map);\n            }\n        }\n        if let Some(limits) = &r.limits {\n            let mut map = BTreeMap::new();\n            if let Some(cpu) = &limits.cpu {\n                map.insert(\"cpu\".to_string(), Quantity(cpu.clone()));\n            }\n            if let Some(memory) = &limits.memory {\n                map.insert(\"memory\".to_string(), Quantity(memory.clone()));\n            }\n            if !map.is_empty() {\n                k8s_req.limits = Some(map);\n            }\n        }\n        k8s_req\n    });\n\n    let container = Container {\n        name: \"soju\".to_string(),\n        image: Some(SOJU_IMAGE.to_string()),\n        args: Some(vec![\"-config\".to_string(), \"/etc/soju/config\".to_string()]),\n        ports: Some(ports),\n        volume_mounts: Some(volume_mounts),\n        resources,\n        security_context: Some(SecurityContext {\n            run_as_non_root: Some(true),\n            allow_privilege_escalation: Some(false),\n            ..Default::default()\n        }),\n        ..Default::default()\n    };\n\n    Deployment {\n        metadata: ObjectMeta {\n            name: Some(name.clone()),\n            namespace: Some(ns),\n            labels: Some(labels.clone()),\n            owner_references: Some(vec![oref]),\n            ..Default::default()\n        },\n        spec: Some(DeploymentSpec {\n            replicas: Some(1),\n            selector: LabelSelector {\n                match_labels: Some(labels.clone()),\n                ..Default::default()\n            },\n            template: PodTemplateSpec {\n                metadata: Some(ObjectMeta {\n                    labels: Some(labels),\n                    ..Default::default()\n                }),\n                spec: Some(PodSpec {\n                    containers: vec![container],\n                    volumes: Some(volumes),\n                    ..Default::default()\n                }),\n            },\n            ..Default::default()\n        }),\n        ..Default::default()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::testutil::test_bouncer;\n\n    #[test]\n    fn deployment_mounts_configmap() {\n        let dep = build_deployment(&test_bouncer());\n        let spec = dep.spec.unwrap();\n        let pod_spec = spec.template.spec.unwrap();\n        let volumes = pod_spec.volumes.unwrap();\n        assert!(volumes.iter().any(|v| v.name == \"config\"));\n    }\n\n    #[test]\n    fn deployment_mounts_tls_when_configured() {\n        let dep = build_deployment(&test_bouncer());\n        let spec = dep.spec.unwrap();\n        let pod_spec = spec.template.spec.unwrap();\n        let volumes = pod_spec.volumes.unwrap();\n        assert!(volumes.iter().any(|v| v.name == \"tls\"));\n    }\n\n    #[test]\n    fn deployment_has_owner_reference() {\n        let dep = build_deployment(&test_bouncer());\n        let orefs = dep.metadata.owner_references.unwrap();\n        assert_eq!(orefs[0].name, \"test-bouncer\");\n    }\n\n    #[test]\n    fn deployment_exposes_listener_ports() {\n        let dep = build_deployment(&test_bouncer());\n        let containers = dep.spec.unwrap().template.spec.unwrap().containers;\n        let ports = containers[0].ports.as_ref().unwrap();\n        assert_eq!(ports.len(), 2);\n        assert_eq!(ports[0].container_port, 6697);\n        assert_eq!(ports[1].container_port, 6667);\n    }\n\n    #[test]\n    fn deployment_mounts_db_secret() {\n        let dep = build_deployment(&test_bouncer());\n        let spec = dep.spec.unwrap();\n        let pod_spec = spec.template.spec.unwrap();\n        let volumes = pod_spec.volumes.unwrap();\n        assert!(volumes.iter().any(|v| v.name == \"db\"));\n    }\n}\n```\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/src/resources/service.rs`**\n```rust\nuse std::collections::BTreeMap;\n\nuse k8s_openapi::api::core::v1::{Service, ServicePort, ServiceSpec};\nuse kube::api::ObjectMeta;\nuse kube::Resource;\n\nuse crate::types::SojuBouncer;\n\npub fn build_service(bouncer: &SojuBouncer) -> Service {\n    let name = bouncer.metadata.name.clone().unwrap();\n    let ns = bouncer.metadata.namespace.clone().unwrap();\n    let oref = bouncer.controller_owner_ref(&()).unwrap();\n\n    let selector: BTreeMap<String, String> =\n        [(\"app.kubernetes.io/name\".to_string(), name.clone())].into();\n\n    let ports: Vec<ServicePort> = bouncer\n        .spec\n        .listeners\n        .iter()\n        .enumerate()\n        .filter_map(|(i, l)| {\n            l.address\n                .rsplit(':')\n                .next()\n                .and_then(|p| p.parse::<i32>().ok())\n                .map(|port| ServicePort {\n                    name: Some(format!(\"listener-{i}\")),\n                    port,\n                    ..Default::default()\n                })\n        })\n        .collect();\n\n    Service {\n        metadata: ObjectMeta {\n            name: Some(name),\n            namespace: Some(ns),\n            owner_references: Some(vec![oref]),\n            ..Default::default()\n        },\n        spec: Some(ServiceSpec {\n            selector: Some(selector),\n            ports: Some(ports),\n            ..Default::default()\n        }),\n        ..Default::default()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::testutil::test_bouncer;\n\n    #[test]\n    fn service_named_correctly() {\n        let svc = build_service(&test_bouncer());\n        assert_eq!(svc.metadata.name.unwrap(), \"test-bouncer\");\n    }\n\n    #[test]\n    fn service_has_correct_selector() {\n        let svc = build_service(&test_bouncer());\n        let selector = svc.spec.unwrap().selector.unwrap();\n        assert_eq!(selector[\"app.kubernetes.io/name\"], \"test-bouncer\");\n    }\n\n    #[test]\n    fn service_has_ports_for_each_listener() {\n        let svc = build_service(&test_bouncer());\n        let ports = svc.spec.unwrap().ports.unwrap();\n        assert_eq!(ports.len(), 2);\n        assert_eq!(ports[0].port, 6697);\n        assert_eq!(ports[1].port, 6667);\n    }\n\n    #[test]\n    fn service_has_owner_reference() {\n        let svc = build_service(&test_bouncer());\n        let orefs = svc.metadata.owner_references.unwrap();\n        assert_eq!(orefs.len(), 1);\n        assert_eq!(orefs[0].name, \"test-bouncer\");\n    }\n}\n```\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/src/resources/certificate.rs`**\n```rust\nuse k8s_crds_cert_manager::certificates::{\n    Certificate, CertificateIssuerRef, CertificateSpec,\n};\nuse kube::api::ObjectMeta;\nuse kube::Resource;\n\nuse crate::types::SojuBouncer;\n\npub fn build_certificate(bouncer: &SojuBouncer) -> Certificate {\n    let name = bouncer.metadata.name.clone().unwrap();\n    let ns = bouncer.metadata.namespace.clone().unwrap();\n    let tls = bouncer.spec.tls.as_ref().unwrap();\n    let oref = bouncer.controller_owner_ref(&()).unwrap();\n\n    Certificate {\n        metadata: ObjectMeta {\n            name: Some(name.clone()),\n            namespace: Some(ns),\n            owner_references: Some(vec![oref]),\n            ..Default::default()\n        },\n        spec: CertificateSpec {\n            secret_name: format!(\"{name}-tls\"),\n            issuer_ref: CertificateIssuerRef {\n                name: tls.issuer_ref.name.clone(),\n                kind: Some(tls.issuer_ref.kind.clone()),\n                group: Some(\"cert-manager.io\".to_string()),\n            },\n            dns_names: Some(tls.dns_names.clone()),\n            ..Default::default()\n        },\n        status: None,\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::testutil::test_bouncer;\n\n    #[test]\n    fn certificate_uses_issuer_from_spec() {\n        let cert = build_certificate(&test_bouncer());\n        let spec = &cert.spec;\n        assert_eq!(spec.issuer_ref.name, \"letsencrypt-prod\");\n        assert_eq!(spec.issuer_ref.kind, Some(\"ClusterIssuer\".to_string()));\n    }\n\n    #[test]\n    fn certificate_has_correct_dns_names() {\n        let cert = build_certificate(&test_bouncer());\n        let dns = cert.spec.dns_names.as_ref().unwrap();\n        assert_eq!(dns, &vec![\"irc.example.com\".to_string()]);\n    }\n\n    #[test]\n    fn certificate_secret_name_matches_convention() {\n        let cert = build_certificate(&test_bouncer());\n        assert_eq!(cert.spec.secret_name, \"test-bouncer-tls\");\n    }\n\n    #[test]\n    fn certificate_has_owner_reference() {\n        let cert = build_certificate(&test_bouncer());\n        let orefs = cert.metadata.owner_references.unwrap();\n        assert_eq!(orefs[0].name, \"test-bouncer\");\n    }\n}\n```\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/src/resources/route.rs`**\n```rust\nuse kube::api::ObjectMeta;\nuse kube::Resource;\nuse serde::{Deserialize, Serialize};\n\nuse crate::types::SojuBouncer;\n\n#[derive(Clone, Debug, Deserialize, Serialize)]\npub struct OcpRoute {\n    pub metadata: ObjectMeta,\n    pub spec: OcpRouteSpec,\n}\n\n#[derive(Clone, Debug, Deserialize, Serialize)]\npub struct OcpRouteSpec {\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub host: Option<String>,\n    pub to: RouteTargetReference,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub port: Option<RoutePort>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub tls: Option<RouteTlsConfig>,\n    #[serde(skip_serializing_if = \"Option::is_none\", rename = \"wildcardPolicy\")]\n    pub wildcard_policy: Option<String>,\n}\n\n#[derive(Clone, Debug, Deserialize, Serialize)]\npub struct RouteTargetReference {\n    pub kind: String,\n    pub name: String,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub weight: Option<i32>,\n}\n\n#[derive(Clone, Debug, Deserialize, Serialize)]\npub struct RoutePort {\n    #[serde(rename = \"targetPort\")]\n    pub target_port: String,\n}\n\n#[derive(Clone, Debug, Deserialize, Serialize)]\npub struct RouteTlsConfig {\n    pub termination: String,\n    #[serde(skip_serializing_if = \"Option::is_none\", rename = \"insecureEdgeTerminationPolicy\")]\n    pub insecure_edge_termination_policy: Option<String>,\n}\n\npub fn build_route(bouncer: &SojuBouncer) -> OcpRoute {\n    let name = bouncer.metadata.name.clone().unwrap();\n    let ns = bouncer.metadata.namespace.clone().unwrap();\n    let oref = bouncer.controller_owner_ref(&()).unwrap();\n\n    let host = bouncer\n        .spec\n        .route\n        .as_ref()\n        .map(|r| r.host.clone())\n        .unwrap_or_else(|| bouncer.spec.hostname.clone());\n\n    OcpRoute {\n        metadata: ObjectMeta {\n            name: Some(name.clone()),\n            namespace: Some(ns),\n            owner_references: Some(vec![oref]),\n            ..Default::default()\n        },\n        spec: OcpRouteSpec {\n            host: Some(host),\n            to: RouteTargetReference {\n                kind: \"Service\".to_string(),\n                name,\n                weight: Some(100),\n            },\n            port: Some(RoutePort {\n                target_port: \"listener-0\".to_string(),\n            }),\n            tls: Some(RouteTlsConfig {\n                termination: \"passthrough\".to_string(),\n                insecure_edge_termination_policy: Some(\"Redirect\".to_string()),\n            }),\n            wildcard_policy: None,\n        },\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::testutil::test_bouncer;\n\n    #[test]\n    fn route_uses_passthrough_tls() {\n        let route = build_route(&test_bouncer());\n        let tls = route.spec.tls.unwrap();\n        assert_eq!(tls.termination, \"passthrough\");\n    }\n\n    #[test]\n    fn route_points_at_service() {\n        let route = build_route(&test_bouncer());\n        assert_eq!(route.spec.to.kind, \"Service\");\n        assert_eq!(route.spec.to.name, \"test-bouncer\");\n    }\n\n    #[test]\n    fn route_has_correct_host() {\n        let mut bouncer = test_bouncer();\n        bouncer.spec.route = Some(crate::types::RouteSpec {\n            host: \"irc.josie.cloud\".to_string(),\n        });\n        let route = build_route(&bouncer);\n        assert_eq!(route.spec.host.unwrap(), \"irc.josie.cloud\");\n    }\n\n    #[test]\n    fn route_has_owner_reference() {\n        let route = build_route(&test_bouncer());\n        let orefs = route.metadata.owner_references.unwrap();\n        assert_eq!(orefs[0].name, \"test-bouncer\");\n    }\n\n    #[test]\n    fn route_falls_back_to_hostname_when_no_route_spec() {\n        let bouncer = test_bouncer();\n        let route = build_route(&bouncer);\n        assert_eq!(route.spec.host.unwrap(), \"irc.example.com\");\n    }\n}\n```\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/src/resources/secret.rs`**\n```rust\nuse k8s_openapi::api::core::v1::Secret;\nuse kube::api::ObjectMeta;\nuse kube::Resource;\nuse std::collections::BTreeMap;\n\nuse crate::types::SojuBouncer;\n\npub fn build_db_secret(bouncer: &SojuBouncer, connection_uri: &str, password: &str) -> Secret {\n    let name = bouncer.metadata.name.clone().unwrap();\n    let ns = bouncer.metadata.namespace.clone().unwrap();\n    let oref = bouncer.controller_owner_ref(&()).unwrap();\n\n    Secret {\n        metadata: ObjectMeta {\n            name: Some(format!(\"{name}-db\")),\n            namespace: Some(ns),\n            owner_references: Some(vec![oref]),\n            ..Default::default()\n        },\n        string_data: Some(BTreeMap::from([\n            (\"uri\".to_string(), connection_uri.to_string()),\n            (\"password\".to_string(), password.to_string()),\n        ])),\n        ..Default::default()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::testutil::test_bouncer;\n\n    #[test]\n    fn secret_named_correctly() {\n        let s = build_db_secret(&test_bouncer(), \"host=db dbname=test\", \"mypass\");\n        assert_eq!(s.metadata.name.unwrap(), \"test-bouncer-db\");\n    }\n\n    #[test]\n    fn secret_contains_uri_and_password_keys() {\n        let s = build_db_secret(&test_bouncer(), \"host=db dbname=test\", \"mypass\");\n        let data = s.string_data.unwrap();\n        assert_eq!(data[\"uri\"], \"host=db dbname=test\");\n        assert_eq!(data[\"password\"], \"mypass\");\n    }\n\n    #[test]\n    fn secret_has_owner_reference() {\n        let s = build_db_secret(&test_bouncer(), \"host=db dbname=test\", \"mypass\");\n        let orefs = s.metadata.owner_references.unwrap();\n        assert_eq!(orefs[0].name, \"test-bouncer\");\n    }\n}\n```\n\n### Deployment Files\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/deploy/crd.yaml`**\n```yaml\napiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\nmetadata:\n  name: sojubouncers.irc.josie.cloud\nspec:\n  group: irc.josie.cloud\n  names:\n    categories: []\n    kind: SojuBouncer\n    plural: sojubouncers\n    shortNames:\n    - soju\n    singular: sojubouncer\n  scope: Namespaced\n  versions:\n  - additionalPrinterColumns:\n    - jsonPath: .spec.hostname\n      name: Hostname\n      type: string\n    - jsonPath: .status.conditions[?(@.type==\"Ready\")].status\n      name: Ready\n      type: string\n    name: v1alpha1\n    schema:\n      openAPIV3Schema:\n        description: Auto-generated derived type for SojuBouncerSpec via `CustomResource`\n        properties:\n          spec:\n            properties:\n              hostname:\n                type: string\n              listeners:\n                default: []\n                items:\n                  properties:\n                    address:\n                      type: string\n                    tls:\n                      default: false\n                      type: boolean\n                  required:\n                  - address\n                  type: object\n                type: array\n              resources:\n                nullable: true\n                properties:\n                  limits:\n                    nullable: true\n                    properties:\n                      cpu:\n                        nullable: true\n                        type: string\n                      memory:\n                        nullable: true\n                        type: string\n                    type: object\n                  requests:\n                    nullable: true\n                    properties:\n                      cpu:\n                        nullable: true\n                        type: string\n                      memory:\n                        nullable: true\n                        type: string\n                    type: object\n                type: object\n              route:\n                nullable: true\n                properties:\n                  host:\n                    type: string\n                required:\n                - host\n                type: object\n              title:\n                nullable: true\n                type: string\n              tls:\n                nullable: true\n                properties:\n                  dns_names:\n                    items:\n                      type: string\n                    type: array\n                  issuer_ref:\n                    properties:\n                      kind:\n                        type: string\n                      name:\n                        type: string\n                    required:\n                    - kind\n                    - name\n                    type: object\n                required:\n                - dns_names\n                - issuer_ref\n                type: object\n            required:\n            - hostname\n            type: object\n          status:\n            nullable: true\n            properties:\n              conditions:\n                default: []\n                items:\n                  properties:\n                    last_transition_time:\n                      type: string\n                    message:\n                      type: string\n                    reason:\n                      type: string\n                    status:\n                      type: string\n                    type:\n                      type: string\n                  required:\n                  - last_transition_time\n                  - message\n                  - reason\n                  - status\n                  - type\n                  type: object\n                type: array\n              observed_generation:\n                format: int64\n                nullable: true\n                type: integer\n            type: object\n        required:\n        - spec\n        title: SojuBouncer\n        type: object\n    served: true\n    storage: true\n    subresources:\n      status: {}\n```\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/deploy/operator.yaml`**\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: soju-operator\n  namespace: irc-josie-cloud\n  labels:\n    app.kubernetes.io/name: soju-operator\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app.kubernetes.io/name: soju-operator\n  template:\n    metadata:\n      labels:\n        app.kubernetes.io/name: soju-operator\n    spec:\n      serviceAccountName: soju-operator\n      containers:\n        - name: operator\n          image: image-registry.openshift-image-registry.svc:5000/irc-josie-cloud/soju-operator:latest\n          env:\n            - name: SOJU_DB_MASTER_URI\n              valueFrom:\n                secretKeyRef:\n                  name: soju-operator-db\n                  key: uri\n            - name: SOJU_DB_HOST\n              valueFrom:\n                secretKeyRef:\n                  name: soju-operator-db\n                  key: host\n            - name: SOJU_DB_PORT\n              valueFrom:\n                secretKeyRef:\n                  name: soju-operator-db\n                  key: port\n          resources:\n            requests:\n              memory: \"64Mi\"\n              cpu: \"50m\"\n            limits:\n              memory: \"128Mi\"\n              cpu: \"200m\"\n```\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/deploy/buildconfig.yaml`**\n```yaml\napiVersion: build.openshift.io/v1\nkind: BuildConfig\nmetadata:\n  name: soju-operator\n  namespace: irc-josie-cloud\nspec:\n  source:\n    type: Binary\n  strategy:\n    type: Docker\n    dockerStrategy:\n      dockerfilePath: Containerfile\n  output:\n    to:\n      kind: ImageStreamTag\n      name: soju-operator:latest\n---\napiVersion: image.openshift.io/v1\nkind: ImageStream\nmetadata:\n  name: soju-operator\n  namespace: irc-josie-cloud\n```\n\n**File: `/home/josie/development/irc-now/crates/soju-operator/deploy/rbac.yaml`**\n```yaml\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: soju-operator\n  namespace: irc-josie-cloud\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n  name: soju-operator\nrules:\n  - apiGroups: [\"irc.josie.cloud\"]\n    resources: [\"sojubouncers\"]\n    verbs: [\"get\", \"list\", \"watch\", \"patch\", \"update\"]\n  - apiGroups: [\"irc.josie.cloud\"]\n    resources: [\"sojubouncers/status\"]\n    verbs: [\"get\", \"patch\", \"update\"]\n  - apiGroups: [\"\"]\n    resources: [\"configmaps\", \"secrets\", \"services\"]\n    verbs: [\"get\", \"list\", \"watch\", \"create\", \"update\", \"patch\", \"delete\"]\n  - apiGroups: [\"apps\"]\n    resources: [\"deployments\"]\n    verbs: [\"get\", \"list\", \"watch\", \"create\", \"update\", \"patch\", \"delete\"]\n  - apiGroups: [\"route.openshift.io\"]\n    resources: [\"routes\"]\n    verbs: [\"get\", \"list\", \"watch\", \"create\", \"update\", \"patch\", \"delete\"]\n  - apiGroups: [\"route.openshift.io\"]\n    resources: [\"routes/custom-host\"]\n    verbs: [\"create\", \"update\"]\n  - apiGroups: [\"cert-manager.io\"]\n    resources: [\"certificates\"]\n    verbs: [\"get\", \"list\", \"watch\", \"create\", \"update\", \"patch\", \"delete\"]\n  - apiGroups: [\"coordination.k8s.io\"]\n    resources: [\"leases\"]\n    verbs: [\"get\", \"list\", \"watch\", \"create\", \"update\", \"patch\", \"delete\"]\n  - apiGroups: [\"\"]\n    resources: [\"events\"]\n    verbs: [\"create\", \"patch\"]\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: soju-operator\nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: ClusterRole\n  name: soju-operator\nsubjects:\n  - kind: ServiceAccount\n    name: soju-operator\n    namespace: irc-josie-cloud\n```\n\n---\n\n## Summary\n\nThe soju-operator is a Kubernetes operator written in Rust using kube.rs 3.0 and Axum. It watches SojuBouncer custom resources and reconciles them by:\n\n1. Creating/managing a PostgreSQL role and database per bouncer\n2. Building a ConfigMap with the soju configuration\n3. Creating a Deployment running the soju IRC bouncer image\n4. Exposing services and routes (OpenShift-specific)\n5. Managing TLS certificates via cert-manager\n\nKey patterns:\n- Finalizer-based cleanup for safe database teardown\n- Client-side apply patches with field manager\n- Multi-tenant database provisioning with password generation\n- OCP Route support via DynamicObject patching\n- Comprehensive unit tests for all resource builders"
  },
  {
    "type": "text",
    "text": "agentId: a0724cac142dccca6 (for resuming to continue this agent's work if needed)\n<usage>total_tokens: 96123\ntool_uses: 24\nduration_ms: 102608</usage>"
  }
]