cellos-ctl 0.5.1

cellctl — kubectl-style CLI for CellOS execution cells and formations. Thin HTTP client over cellos-server with apply/get/describe/logs/events/webui.
Documentation
//! Wire-level types for the cellos-server HTTP API.
//!
//! These mirror the projector's response shape. cellctl is a thin client — these
//! types exist only to deserialize responses and re-serialize for `--output json`.
//! No client-side derivations, no state caching.
//!
//! Fields are flexible (`#[serde(default)]`) so that adding fields server-side
//! does not break older clients. This is the same compatibility contract kubectl
//! has with the kube-apiserver.

use serde::{Deserialize, Serialize};
use serde_json::Value;

/// Cell view returned by `GET /v1/cells` / `GET /v1/cells/<id>`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cell {
    #[serde(default)]
    pub id: String,
    #[serde(default)]
    pub name: String,
    #[serde(default)]
    pub formation_id: Option<String>,
    /// PENDING | ADMITTED | RUNNING | COMPLETED | FAILED | KILLED
    #[serde(default)]
    pub state: String,
    #[serde(default)]
    pub image: Option<String>,
    #[serde(default)]
    pub critical: Option<bool>,
    #[serde(default)]
    pub created_at: Option<String>,
    #[serde(default)]
    pub started_at: Option<String>,
    #[serde(default)]
    pub finished_at: Option<String>,
    /// Most-recent CloudEvent outcome, if known.
    #[serde(default)]
    pub outcome: Option<String>,
    /// Arbitrary extra fields the server may attach.
    #[serde(flatten, default)]
    pub extra: serde_json::Map<String, Value>,
}

/// Formation view returned by `GET /v1/formations` / `GET /v1/formations/<id>`.
///
/// Wire contract drift note (red-team wave 2, HIGH-W2D-2): the server's
/// [`cellos_server::state::FormationRecord`] serialises its lifecycle
/// field as `status` (UPPERCASE enum: `PENDING` / `RUNNING` / `SUCCEEDED`
/// / `FAILED` / `CANCELLED`), while older clients and the table-render
/// code in `output.rs` expect `state` (PascalCase string:
/// `PENDING` / `LAUNCHING` / …). The `state` field below carries
/// `#[serde(alias = "status")]` so cellctl deserialises both shapes
/// without a wire break — until cellos-server is renamed to expose
/// `state` directly, this alias is load-bearing.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Formation {
    #[serde(default)]
    pub id: String,
    #[serde(default)]
    pub name: String,
    /// PENDING | LAUNCHING | RUNNING | DEGRADED | COMPLETED | FAILED
    ///
    /// Accepts both `state` (forward-looking canonical) and `status` (the
    /// shape cellos-server emits today) on the wire.
    #[serde(default, alias = "status")]
    pub state: String,
    #[serde(default)]
    pub tenant: Option<String>,
    #[serde(default)]
    pub cells: Vec<Cell>,
    #[serde(default)]
    pub created_at: Option<String>,
    #[serde(default)]
    pub updated_at: Option<String>,
    #[serde(flatten, default)]
    pub extra: serde_json::Map<String, Value>,
}

/// CloudEvent envelope — projector emits these on the stream and on
/// `GET /v1/cells/<id>/events` plus the `/ws/events` socket.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloudEvent {
    #[serde(default, alias = "specversion")]
    pub spec_version: Option<String>,
    #[serde(default)]
    pub id: Option<String>,
    #[serde(default)]
    pub source: Option<String>,
    #[serde(default, alias = "type")]
    pub event_type: Option<String>,
    #[serde(default)]
    pub time: Option<String>,
    #[serde(default)]
    pub subject: Option<String>,
    #[serde(default)]
    pub data: Option<Value>,
    #[serde(flatten, default)]
    pub extra: serde_json::Map<String, Value>,
}

/// Server response envelope for list endpoints. Some endpoints return a bare
/// array; others wrap in `{"items": [...]}`. `List<T>` accepts either via custom deserialize.
#[derive(Debug, Clone, Serialize)]
pub struct List<T> {
    pub items: Vec<T>,
}

impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for List<T> {
    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum Wire<T> {
            Bare(Vec<T>),
            Wrapped { items: Vec<T> },
        }
        Ok(match Wire::<T>::deserialize(d)? {
            Wire::Bare(items) => List { items },
            Wire::Wrapped { items } => List { items },
        })
    }
}