[ { "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::::all(client.clone());\n let deployments = Api::::all(client.clone());\n let configmaps = Api::::all(client.clone());\n let services = Api::::all(client.clone());\n let secrets = Api::::all(client.clone());\n let certificates = Api::::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,\n #[serde(default)]\n pub listeners: Vec,\n #[serde(default)]\n pub tls: Option,\n #[serde(default)]\n pub route: Option,\n #[serde(default)]\n pub resources: Option,\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,\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,\n #[serde(default)]\n pub limits: Option,\n}\n\n#[derive(Serialize, Deserialize, Default, Debug, Clone, JsonSchema)]\npub struct ResourceList {\n #[serde(default)]\n pub memory: Option,\n #[serde(default)]\n pub cpu: Option,\n}\n\n#[derive(Serialize, Deserialize, Default, Debug, Clone, JsonSchema)]\npub struct SojuBouncerStatus {\n #[serde(default)]\n pub conditions: Vec,\n #[serde(default)]\n pub observed_generation: Option,\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>),\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,\n ctx: Arc,\n) -> Result {\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 = 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 {\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 = 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 = 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 = 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 = 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 = 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 = 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 {\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,\n error: &Error,\n _ctx: Arc,\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 {\n let secret_api: Api = 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::::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> {\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> {\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 = [\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 = bouncer\n .spec\n .listeners\n .iter()\n .filter_map(|l| {\n l.address\n .rsplit(':')\n .next()\n .and_then(|p| p.parse::().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 =\n [(\"app.kubernetes.io/name\".to_string(), name.clone())].into();\n\n let ports: Vec = 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::().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,\n pub to: RouteTargetReference,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub port: Option,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub tls: Option,\n #[serde(skip_serializing_if = \"Option::is_none\", rename = \"wildcardPolicy\")]\n pub wildcard_policy: Option,\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,\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,\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)\ntotal_tokens: 96123\ntool_uses: 24\nduration_ms: 102608" } ]