# Implement ergo-operator crate ## Context The platform roadmap (Phase 4) calls for IRC network hosting via ergo (MIT-licensed IRC server). Users will be able to spin up their own IRC network at `.irc.now`. This requires a Kubernetes operator that manages `ErgoNetwork` CRDs, following the same kube.rs pattern as the existing soju-operator. Key difference from soju-operator: ergo uses a file-based datastore (`ircd.db`), not PostgreSQL. This means no database provisioning, no DB finalizer, and a PVC per network instead of per-tenant DB credentials. The operator is simpler as a result. ## CRD: ErgoNetwork (`irc.josie.cloud/v1alpha1`) ```yaml apiVersion: irc.josie.cloud/v1alpha1 kind: ErgoNetwork metadata: name: my-network spec: hostname: my-network.irc.now networkName: "My Network" operCredentials: secretName: my-network-oper listeners: - address: ":6697" tls: true - address: ":6667" tls: issuerRef: name: letsencrypt-prod kind: ClusterIssuer dnsNames: - my-network.irc.now route: host: my-network.irc.now storage: size: 1Gi storageClassName: customer-workload-storage resources: requests: memory: "64Mi" cpu: "50m" limits: memory: "256Mi" cpu: "200m" ``` ## Reconciliation No finalizer needed -- all child resources have owner references for cascading deletion. ``` reconcile_apply: 1. Read oper password hash from referenced Secret (if operCredentials set) 2. Create PVC if it doesn't exist (immutable after creation, skip if present) 3. Apply Certificate (if TLS configured) 4. Apply ConfigMap (ergo YAML config with oper hash injected) 5. Apply Deployment (ergo:stable, mounts config + PVC + TLS) 6. Apply Service (one port per listener) 7. Apply Route (if route configured, passthrough TLS) 8. Update status -> Ready=True ``` ## Differences from soju-operator | Aspect | soju-operator | ergo-operator | |--------|--------------|---------------| | CRD kind | SojuBouncer | ErgoNetwork | | Extra fields | title | networkName, operCredentials, storage | | Config format | Line-based text | YAML | | Image | codeberg.org/emersion/soju:latest | ghcr.io/ergochat/ergo:stable | | Command | soju -config /etc/soju/config | ergo run --conf /etc/ergo/ircd.yaml | | Datastore | PostgreSQL per-tenant | File-based /ircd/ircd.db | | DB provisioning | Yes (db.rs, deadpool-postgres) | No | | PVC | No | Yes (per-network for /ircd/) | | Finalizer | Yes (DB cleanup) | No (owner refs suffice) | | Context | client + db pool + db host/port | client only | ## File Structure ``` crates/ergo-operator/ Cargo.toml Containerfile deploy/ buildconfig.yaml crd.yaml (generated) operator.yaml rbac.yaml src/ main.rs lib.rs crdgen.rs types.rs controller.rs testutil.rs resources/ mod.rs configmap.rs (generates ergo YAML config) deployment.rs pvc.rs (new -- replaces soju's db secret) service.rs certificate.rs route.rs ``` ## Tasks ### 1. Project scaffolding **Create:** - `crates/ergo-operator/Cargo.toml` -- kube 3, k8s-openapi 0.27, k8s-crds-cert-manager, schemars, serde_yaml. No tokio-postgres/deadpool-postgres/rand (no DB provisioning). Binary targets: `ergo-operator` and `ergo-crdgen`. - `crates/ergo-operator/src/{main,lib,crdgen}.rs` -- placeholders - Empty module files: `types.rs`, `controller.rs`, `testutil.rs`, `resources/mod.rs` **Modify:** - Root `Cargo.toml` -- add `"crates/ergo-operator"` to workspace members **Verify:** `cargo check -p ergo-operator` ### 2. CRD types (`src/types.rs`) Define `ErgoNetworkSpec` with: `hostname`, `network_name`, `oper_credentials` (secret ref), `listeners`, `tls`, `route`, `resources`, `storage` (size + optional storageClassName). Shared sub-types duplicated from soju-operator (small, stable, operators stay independent): Listener, TlsSpec, IssuerRef, RouteSpec, ResourceRequirements, ResourceList, Condition. New types: `OperCredentials { secret_name }`, `StorageSpec { size, storage_class_name }`. Print columns: Hostname, Network, Ready. **Tests:** CRD schema name is `ergonetworks.irc.josie.cloud`, default spec has sane values. ### 3. Test utility (`src/testutil.rs`) `test_network()` function returning an ErgoNetwork with all fields populated. Reused by all resource builder tests. ### 4. ConfigMap builder (`src/resources/configmap.rs`) Generates ergo YAML config from spec. Takes `&ErgoNetwork` + `Option<&str>` oper hash. Config structure: ```yaml server: name: network: name: listeners: ":6697": tls: cert: /etc/ergo/tls/tls.crt key: /etc/ergo/tls/tls.key ":6667": {} datastore: path: /ircd/ircd.db autoupgrade: true opers: # only if oper hash provided admin: class: "server-admin" password: "" ``` Build via `serde_json::json!` -> `serde_yaml::to_string`. ConfigMap key: `ircd.yaml`. **Tests:** Config contains hostname, network name, listeners, TLS paths, oper hash. Config without oper omits opers section. Network name defaults to hostname when unset. ### 5. PVC builder (`src/resources/pvc.rs`) New resource builder (no soju-operator equivalent). Creates a ReadWriteOnce PVC named `{name}-data` with size from `spec.storage` (default 1Gi), optional storageClassName. Owner reference for cascading deletion. **Tests:** Name, size, default size, storage class, owner reference. ### 6. Deployment builder (`src/resources/deployment.rs`) Image: `ghcr.io/ergochat/ergo:stable` Args: `["ergo", "run", "--conf", "/etc/ergo/ircd.yaml"]` Volumes: ConfigMap `{name}-config` at `/etc/ergo/`, PVC `{name}-data` at `/ircd/`, TLS Secret `{name}-tls` at `/etc/ergo/tls/` (conditional). Labels: `app.kubernetes.io/managed-by: ergo-operator` **Tests:** Image, command, config mount, PVC mount, TLS mount, ports, owner ref. ### 7. Service builder (`src/resources/service.rs`) Same pattern as soju-operator. ClusterIP, one port per listener. ### 8. Certificate builder (`src/resources/certificate.rs`) Same pattern as soju-operator. cert-manager Certificate, secret `{name}-tls`. ### 9. Route builder (`src/resources/route.rs`) Same pattern as soju-operator. OcpRoute types (duplicated), passthrough TLS. ### 10. Controller (`src/controller.rs`) Context: just `client: kube::Client` (no DB pool). `reconcile_apply`: 1. Read oper password from Secret (if operCredentials set) 2. Check if PVC exists; create only if missing (immutable field) 3. Server-side apply: Certificate, ConfigMap, Deployment, Service, Route 4. Update status No finalizer. Requeue every 300s. Error policy requeues after 60s. ### 11. Main entry point (`src/main.rs`) Wire Controller with `.owns()` for Deployment, ConfigMap, Service, PVC, Certificate. No `.owns(secrets)` -- oper Secret is user-managed, not operator-owned. No env vars needed (no DB credentials). ### 12. CRD generator (`src/crdgen.rs`) Print `ErgoNetwork::crd()` as YAML. ### 13. Deploy manifests - `deploy/crd.yaml` -- generated from crdgen - `deploy/operator.yaml` -- Deployment in irc-josie-cloud, no env vars - `deploy/rbac.yaml` -- ClusterRole with ergonetworks, configmaps, secrets, services, persistentvolumeclaims, deployments, routes, certificates, leases, events - `deploy/buildconfig.yaml` -- ImageStream + BuildConfig, `dockerfilePath: crates/ergo-operator/Containerfile` - `Containerfile` -- workspace multi-stage build (`cargo build --release -p ergo-operator`) ### 14. Integration smoke test 1. Build and push via `oc start-build` 2. Apply CRD, RBAC, operator Deployment 3. Create oper Secret with bcrypt hash 4. Create test ErgoNetwork CR 5. Verify child resources: Deployment, ConfigMap, Service, Route, Certificate, PVC 6. Check ergo config content: `oc get configmap smoke-test-config -o jsonpath='{.data.ircd\.yaml}'` 7. Delete CR, verify cascading cleanup via owner references ## Reference Files - `crates/soju-operator/src/types.rs` -- template for CRD types, shared sub-types - `crates/soju-operator/src/controller.rs` -- template for reconciler, status updates, SSA - `crates/soju-operator/src/resources/` -- template for all resource builders - `crates/soju-operator/src/testutil.rs` -- template for test fixture - `crates/soju-operator/deploy/` -- template for all deploy manifests - `docs/plans/2026-03-03-irc-now-platform-design.md:169-211` -- ErgoNetwork CRD spec ## Follow-up (not in this plan) - Portal integration: add ErgoNetworkSpec to web-api k8s.rs, network management routes - DNS: wildcard `*.irc.now` or per-tenant subdomain provisioning - Ergo default config tuning (channel limits, history settings, MOTD)