polyc-controller 2026.8.3

Conversation CRD + kube reconciler for the polychrome control plane.
//! Control-plane addressing shared by the CLI (`polychrome send`) and the TUI
//! cockpit (`polyc-tui`).
//!
//! Centralising the namespace, pod label selector, served ports, and the
//! "pick a Running control-plane pod" resolution means both tools target the
//! same pod the same way, and a manifest namespace/label/port change is a single
//! edit instead of a hunt across crates that the compiler cannot keep in sync.

use anyhow::{Context as _, Result, anyhow};
use k8s_openapi::api::core::v1::Pod;
use kube::{Api, Client, api::ListParams};

/// The namespace every polychrome resource is pinned to.
///
/// Set in manifests `base/kustomization.yaml`; the default operating namespace
/// for all tools. Resolution deliberately does NOT fall back to the kubeconfig
/// context's namespace, which would point an unrelated context at the wrong
/// place.
pub const NAMESPACE: &str = "polychrome";

/// Label selector identifying control-plane pods.
pub const CONTROL_PLANE_SELECTOR: &str = "app.kubernetes.io/component=control-plane";

/// The control-plane gRPC port (`AgentService`/`ApprovalService`). Matches the
/// `containerPort` baked into the kustomize manifests.
pub const AGENT_PORT: u16 = 8080;

/// The control-plane read-only forensics HTTP port.
pub const FORENSICS_PORT: u16 = 8090;

/// Pick the first `Running` control-plane pod in `namespace`.
///
/// Returns a clear, operator-facing error when none is available (cluster down,
/// wrong kube context) rather than treating it as a transient.
///
/// # Errors
/// Returns an error if the list call fails or no `Running` pod is found.
pub async fn pick_control_plane_pod(client: &Client, namespace: &str) -> Result<String> {
    let pods: Api<Pod> = Api::namespaced(client.clone(), namespace);
    let list = pods
        .list(&ListParams::default().labels(CONTROL_PLANE_SELECTOR))
        .await
        .context("list control-plane pods")?;

    for p in list.items {
        let phase = p
            .status
            .as_ref()
            .and_then(|s| s.phase.as_deref())
            .unwrap_or("");
        if phase == "Running"
            && let Some(name) = p.metadata.name
        {
            return Ok(name);
        }
    }
    Err(anyhow!(
        "no Running control-plane pod in namespace {namespace:?} (selector {CONTROL_PLANE_SELECTOR}); is the cluster up and is your kube context right?"
    ))
}