# Whois Hostname Cloaking Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Make soju display `.irc.now` in whois instead of the raw connection IP. **Architecture:** Fork soju with a minimal `hostname-cloak` config directive patch, build a custom container image, update the soju-operator to emit the new directive and use the custom image. **Tech Stack:** Go (soju patch), Rust (operator/web-api changes), OCP builds --- ### Task 1: Create soju patch file **Files:** - Create: `crates/soju-operator/soju/hostname-cloak.patch` **Step 1: Write the patch** The patch modifies two files in soju's source: `config/config.go` -- add `HostnameCloak` field to `BasicServer` and parse it from config: ```go // In BasicServer struct, add after Hostname: HostnameCloak string // In raw config struct inside Load(), add: HostnameCloak string `scfg:"hostname-cloak"` // In Load() body, after the hostname parsing block: if raw.HostnameCloak != "" { srv.HostnameCloak = raw.HostnameCloak } ``` `downstream.go` -- override hostname when cloak is configured: ```go // In newDownstreamConn(), replace: if host, _, err := net.SplitHostPort(remoteAddr); err == nil { dc.hostname = host } else { dc.hostname = remoteAddr } // With: if cloak := dc.srv.Config().HostnameCloak; cloak != "" { dc.hostname = cloak } else if host, _, err := net.SplitHostPort(remoteAddr); err == nil { dc.hostname = host } else { dc.hostname = remoteAddr } ``` Write a unified diff patch file containing both changes. **Step 2: Verify patch format** Run: `cat crates/soju-operator/soju/hostname-cloak.patch` Expected: valid unified diff with `--- a/` and `+++ b/` headers **Step 3: Commit** ```bash git add crates/soju-operator/soju/hostname-cloak.patch git commit -m "feat(soju): add hostname-cloak patch for whois cloaking" ``` --- ### Task 2: Create custom soju Containerfile **Files:** - Create: `crates/soju-operator/soju/Containerfile` **Step 1: Write the Containerfile** ```dockerfile FROM docker.io/golang:1.23 AS build WORKDIR /src RUN git clone https://git.sr.ht/~emersion/soju . && git checkout v0.9.0 COPY hostname-cloak.patch . RUN git apply hostname-cloak.patch RUN CGO_ENABLED=0 go build -o /soju ./cmd/soju RUN CGO_ENABLED=0 go build -o /sojuctl ./cmd/sojuctl FROM registry.access.redhat.com/ubi9-minimal COPY --from=build /soju /usr/local/bin/ COPY --from=build /sojuctl /usr/local/bin/ USER 1000 ENTRYPOINT ["soju"] ``` Pin to a tagged release (v0.9.0 or latest stable). Check `git.sr.ht/~emersion/soju` for the current release tag before writing. **Step 2: Commit** ```bash git add crates/soju-operator/soju/Containerfile git commit -m "feat(soju): add Containerfile for custom soju image" ``` --- ### Task 3: Add OCP BuildConfig for custom soju image **Files:** - Create: `crates/soju-operator/soju/buildconfig.yaml` **Step 1: Write the BuildConfig + ImageStream** ```yaml apiVersion: build.openshift.io/v1 kind: BuildConfig metadata: name: soju namespace: irc-josie-cloud spec: source: type: Binary strategy: type: Docker dockerStrategy: dockerfilePath: Containerfile output: to: kind: ImageStreamTag name: soju:latest --- apiVersion: image.openshift.io/v1 kind: ImageStream metadata: name: soju namespace: irc-josie-cloud ``` **Step 2: Commit** ```bash git add crates/soju-operator/soju/buildconfig.yaml git commit -m "feat(soju): add OCP BuildConfig for custom soju image" ``` --- ### Task 4: Build and push custom soju image **Step 1: Apply the BuildConfig** Run: `oc apply -f crates/soju-operator/soju/buildconfig.yaml` Expected: buildconfig and imagestream created **Step 2: Start the build** Run: `oc start-build soju --from-dir=crates/soju-operator/soju/ -n irc-josie-cloud --follow` Expected: build completes, image pushed to internal registry **Step 3: Verify image** Run: `oc get istag soju:latest -n irc-josie-cloud` Expected: image stream tag exists with recent timestamp --- ### Task 5: Add hostname_cloak to CRD and update tests **Files:** - Modify: `crates/soju-operator/src/types.rs:16-30` - Modify: `crates/soju-operator/src/testutil.rs` **Step 1: Write the failing test** In `crates/soju-operator/src/types.rs`, add to the existing tests: ```rust #[test] fn default_spec_has_no_hostname_cloak() { let spec = SojuBouncerSpec::default(); assert!(spec.hostname_cloak.is_none()); } ``` **Step 2: Run test to verify it fails** Run: `cargo test -p soju-operator default_spec_has_no_hostname_cloak` Expected: FAIL -- no field `hostname_cloak` **Step 3: Add the field** In `SojuBouncerSpec` struct, add after the `hostname` field: ```rust #[serde(default)] pub hostname_cloak: Option, ``` **Step 4: Run test to verify it passes** Run: `cargo test -p soju-operator default_spec_has_no_hostname_cloak` Expected: PASS **Step 5: Update test_bouncer helper** In `crates/soju-operator/src/testutil.rs`, add `hostname_cloak` to `test_bouncer()`: ```rust hostname_cloak: Some("irc.example.com".to_string()), ``` **Step 6: Run all operator tests** Run: `cargo test -p soju-operator` Expected: all pass **Step 7: Commit** ```bash git add crates/soju-operator/src/types.rs crates/soju-operator/src/testutil.rs git commit -m "feat(soju-operator): add hostname_cloak field to CRD spec" ``` --- ### Task 6: Emit hostname-cloak in configmap **Files:** - Modify: `crates/soju-operator/src/resources/configmap.rs:26-54` **Step 1: Write the failing test** In `crates/soju-operator/src/resources/configmap.rs`, add: ```rust #[test] fn generates_hostname_cloak_directive() { let bouncer = test_bouncer(); let cm = build_configmap(&bouncer, "host=localhost dbname=test", None); let data = cm.data.unwrap(); let config = &data["config"]; assert!(config.contains("hostname-cloak irc.example.com")); } ``` **Step 2: Run test to verify it fails** Run: `cargo test -p soju-operator generates_hostname_cloak_directive` Expected: FAIL -- config does not contain `hostname-cloak` **Step 3: Add hostname-cloak emission** In `generate_soju_config()`, after the `hostname` line push: ```rust if let Some(cloak) = &spec.hostname_cloak { lines.push(format!("hostname-cloak {cloak}")); } ``` **Step 4: Run test to verify it passes** Run: `cargo test -p soju-operator generates_hostname_cloak_directive` Expected: PASS **Step 5: Add negative test** ```rust #[test] fn omits_hostname_cloak_when_not_set() { let mut bouncer = test_bouncer(); bouncer.spec.hostname_cloak = None; let cm = build_configmap(&bouncer, "host=localhost dbname=test", None); let data = cm.data.unwrap(); let config = &data["config"]; assert!(!config.contains("hostname-cloak")); } ``` **Step 6: Run all tests** Run: `cargo test -p soju-operator` Expected: all pass **Step 7: Commit** ```bash git add crates/soju-operator/src/resources/configmap.rs git commit -m "feat(soju-operator): emit hostname-cloak directive in soju config" ``` --- ### Task 7: Update deployment to use custom soju image **Files:** - Modify: `crates/soju-operator/src/resources/deployment.rs:15` **Step 1: Write the failing test** In `crates/soju-operator/src/resources/deployment.rs`, add: ```rust #[test] fn deployment_uses_custom_soju_image() { let dep = build_deployment(&test_bouncer(), "testhash"); let containers = dep.spec.unwrap().template.spec.unwrap().containers; let image = containers[0].image.as_ref().unwrap(); assert!(image.contains("soju"), "image should reference soju"); assert!(!image.contains("codeberg.org"), "should not use upstream image"); } ``` **Step 2: Run test to verify it fails** Run: `cargo test -p soju-operator deployment_uses_custom_soju_image` Expected: FAIL -- image contains "codeberg.org" **Step 3: Update the image constant** Change the `SOJU_IMAGE` constant: ```rust const SOJU_IMAGE: &str = "image-registry.openshift-image-registry.svc:5000/irc-josie-cloud/soju:latest"; ``` **Step 4: Run test to verify it passes** Run: `cargo test -p soju-operator deployment_uses_custom_soju_image` Expected: PASS **Step 5: Run all tests** Run: `cargo test -p soju-operator` Expected: all pass **Step 6: Commit** ```bash git add crates/soju-operator/src/resources/deployment.rs git commit -m "feat(soju-operator): use custom soju image with hostname cloaking" ``` --- ### Task 8: Update web-api to set hostname_cloak on bouncer creation **Files:** - Modify: `crates/web-api/src/k8s.rs:24-27` - Modify: `crates/web-api/src/routes/bouncer.rs:64-67` **Step 1: Add hostname_cloak to web-api's SojuBouncerSpec** In `crates/web-api/src/k8s.rs`, add the field: ```rust pub struct SojuBouncerSpec { pub hostname: String, pub owner: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub hostname_cloak: Option, } ``` **Step 2: Set hostname_cloak in bouncer creation** In `crates/web-api/src/routes/bouncer.rs`, update the CR creation: ```rust let bouncer = SojuBouncer::new(&name, SojuBouncerSpec { hostname: format!("{name}.irc.now"), owner: user.sub.clone(), hostname_cloak: Some(format!("{name}.irc.now")), }); ``` **Step 3: Run web-api tests** Run: `cargo test -p web-api` Expected: all pass **Step 4: Commit** ```bash git add crates/web-api/src/k8s.rs crates/web-api/src/routes/bouncer.rs git commit -m "feat(web-api): set hostname_cloak when creating bouncer" ``` --- ### Task 9: Build and deploy operator + update existing bouncers **Step 1: Build and deploy the operator** ```bash tar czf /tmp/build.tar.gz --exclude='target' --exclude='.git' \ --exclude='irc-now-landing-page' --exclude='status' --exclude='design' \ --exclude='./docs' --exclude='notes' Cargo.toml Cargo.lock crates/ oc start-build soju-operator --from-archive=/tmp/build.tar.gz -n irc-josie-cloud --follow ``` Expected: build completes **Step 2: Regenerate and apply the CRD** Run: `cargo run -p soju-operator -- crd | oc apply -f -` (or however the CRD is generated/applied -- check the operator's main.rs) **Step 3: Restart the operator** Run: `oc rollout restart deployment/soju-operator -n irc-josie-cloud` **Step 4: Patch existing bouncer CRs with hostname_cloak** For each existing bouncer, patch it to add hostname_cloak: ```bash oc patch sojubouncer -n irc-josie-cloud --type merge \ -p '{"spec":{"hostnameCloak":".irc.now"}}' ``` **Step 5: Verify soju config** Run: `oc get configmap -config -n irc-josie-cloud -o jsonpath='{.data.config}'` Expected: config includes `hostname-cloak .irc.now` **Step 6: Verify whois** Connect to the bouncer and run `/whois `. Should show `.irc.now` instead of the IP. --- ### Task 10: Build and deploy web-api **Step 1: Build web-api** ```bash tar czf /tmp/build.tar.gz --exclude='target' --exclude='.git' \ --exclude='irc-now-landing-page' --exclude='status' --exclude='design' \ --exclude='./docs' --exclude='notes' Cargo.toml Cargo.lock crates/ oc start-build web-api --from-archive=/tmp/build.tar.gz -n irc-josie-cloud --follow ``` **Step 2: Restart web-api** Run: `oc rollout restart deployment/web-api -n irc-josie-cloud` **Step 3: Verify new bouncers get hostname_cloak** Create a test bouncer via the portal and check the CR: Run: `oc get sojubouncer -n irc-josie-cloud -o yaml` Expected: new bouncers have `hostnameCloak` set