xbp-deploy 10.57.0

Service-centric declarative deploy engine for XBP.
Documentation
use serde::{Deserialize, Serialize};

use crate::types::{DeployTarget, ProjectConfig};

/// Exactly one deploy mode (CLI must enforce exclusivity).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DeployMode {
    Plan,
    Run,
    Verify,
    Promote,
    Status,
    History,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeployFlags {
    pub dry_run: bool,
    pub skip_oci: bool,
    /// Skip docker build/push even when `services[].oci.dockerfile` is set.
    #[serde(default)]
    pub skip_build: bool,
    /// Build images into the local Docker engine only (no registry push).
    /// Preferred for docker-desktop / kind / minikube / k3d local clusters.
    #[serde(default)]
    pub local_image: bool,
    /// When true, Cloudflare container verification fails if the image digest/tag
    /// did not change after deploy. Default is false (config-only deploys allowed).
    #[serde(default)]
    pub require_new_container_image: bool,
    /// When true (default), if a k8s service has a workload but no manifest paths,
    /// generate and apply a minimal Deployment/Service (first-time bootstrap).
    /// Disable with CLI `--no-bootstrap-workload`.
    #[serde(default = "default_true")]
    pub bootstrap_workload: bool,
    /// Run full preflight doctor before apply (cluster, namespaces, CF builds, OCI).
    #[serde(default)]
    pub doctor: bool,
    /// Skip deploy preflight doctor even when `doctor` would run automatically.
    #[serde(default)]
    pub skip_doctor: bool,
    pub skip_apply: bool,
    pub skip_rollout: bool,
    /// Skip post-rollout port-forward / hosts DNS expose phase.
    #[serde(default)]
    pub skip_expose: bool,
    pub skip_health: bool,
    pub yes: bool,
    pub json: bool,
    pub namespace: Option<String>,
    pub context: Option<String>,
    pub promote_tag: Option<String>,
    pub history_limit: usize,
    pub output: Option<std::path::PathBuf>,
    /// Selected deploy destinations from CLI (`--to cloudflare,kubernetes`).
    /// Empty means: use each service's default `deploy.provider` (or its default destination).
    #[serde(default)]
    pub destinations: Vec<String>,
    /// Keep only these service names after planning (`--only`). Empty = no only-filter.
    #[serde(default)]
    pub only_services: Vec<String>,
    /// Drop these service names after planning (`--exclude`).
    #[serde(default)]
    pub exclude_services: Vec<String>,
}

/// Everything the engine needs; built by CLI, consumed by planner/runner/verifier.
#[derive(Debug, Clone)]
pub struct DeployContext {
    pub env: String,
    pub target: DeployTarget,
    pub config: ProjectConfig,
    pub flags: DeployFlags,
    pub mode: DeployMode,
}

fn default_true() -> bool {
    true
}

impl Default for DeployFlags {
    fn default() -> Self {
        Self {
            dry_run: false,
            skip_oci: false,
            skip_build: false,
            local_image: false,
            require_new_container_image: false,
            bootstrap_workload: true,
            doctor: false,
            skip_doctor: false,
            skip_apply: false,
            skip_rollout: false,
            skip_expose: false,
            skip_health: false,
            yes: false,
            json: false,
            namespace: None,
            context: None,
            promote_tag: None,
            history_limit: 20,
            output: None,
            destinations: Vec::new(),
            only_services: Vec::new(),
            exclude_services: Vec::new(),
        }
    }
}

impl DeployContext {
    pub fn require_confirmation(&self) -> bool {
        if self.flags.yes || self.flags.dry_run {
            return false;
        }
        self.config
            .kubernetes
            .as_ref()
            .map(|k| k.require_context_confirmation_for_apply)
            .unwrap_or(true)
    }

    /// Preflight doctor: explicit `--doctor`, or auto on Run when not skipped.
    pub fn should_run_doctor(&self) -> bool {
        if self.flags.skip_doctor || self.flags.dry_run {
            return false;
        }
        self.flags.doctor || matches!(self.mode, DeployMode::Run)
    }
}