vitrine-types 0.1.1

Shikumi-style typed configuration for the vitrine pre-merge delivery CLI.
Documentation
//! Typed configuration for the vitrine CLI.
//!
//! Shikumi-style: every config knob is a typed Rust struct with strong
//! validation at parse time. Loaded from TOML (default location:
//! `.vitrine.toml` in the repo root, or `VITRINE_CONFIG` env var).

use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// Root configuration for a vitrine session.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VitrineConfig {
    pub target: TargetEnvironment,
    pub argocd: Option<ArgoCdConfig>,
    pub evidence: EvidenceConfig,
}

/// The target environment a vitrine PR is delivered to.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TargetEnvironment {
    /// kubectl context name for the cluster receiving the change.
    pub kubectl_context: String,

    /// Cloud provider project / subscription / account ID.
    pub cloud_project: String,

    /// Path to the terragrunt root that owns the target's IaC.
    pub terragrunt_root: PathBuf,
}

/// ArgoCD configuration — required for chart-driven vitrine PRs (Pattern A).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArgoCdConfig {
    /// Path to the cluster's argocd_cluster terragrunt module.
    pub cluster_terragrunt: PathBuf,

    /// Namespace where ArgoCD runs (typically "argocd").
    pub namespace: String,

    /// ApplicationSet generator path (for reconciliation watching).
    pub applicationset_path: Option<PathBuf>,
}

/// Evidence-capture configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceConfig {
    /// Output file for the captured evidence Markdown.
    pub output_path: PathBuf,

    /// Whether to include the full plan output or just the summary.
    #[serde(default)]
    pub include_full_plan: bool,

    /// Functional probes to run (curl, kubectl get, etc.).
    #[serde(default)]
    pub functional_probes: Vec<FunctionalProbe>,
}

/// Which of the three vitrine evidence layers a probe belongs to.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProbeLayer {
    /// `terragrunt output -json`, state inspection
    TfState,
    /// `gcloud … describe`, `aws … list`, cloud-provider API
    CloudApi,
    /// `curl`, `kubectl get`, end-to-end probe
    Functional,
}

impl ProbeLayer {
    pub fn label(self) -> &'static str {
        match self {
            ProbeLayer::TfState => "TF state",
            ProbeLayer::CloudApi => "Cloud API",
            ProbeLayer::Functional => "Functional",
        }
    }
}

fn default_layer() -> ProbeLayer {
    ProbeLayer::Functional
}

/// A single probe to run + cite in evidence.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionalProbe {
    /// Human-readable name (e.g., "nginx default 404").
    pub name: String,

    /// Shell command (executed via `sh -c <command>` so shell features work).
    pub command: String,

    /// Which evidence layer this probe belongs to. Defaults to `functional`.
    #[serde(default = "default_layer")]
    pub layer: ProbeLayer,

    /// Expected exit code (None to accept any).
    pub expected_exit: Option<i32>,
}

/// Pattern A annotation operation — set on a cluster's argocd_cluster TF.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatternAOverride {
    /// Annotation key (typically the chart name).
    pub chart_name: String,

    /// Feature branch to point targetRevision at.
    pub feature_branch: String,
}

#[derive(thiserror::Error, Debug)]
pub enum ConfigError {
    #[error("failed to read config file: {0}")]
    Io(#[from] std::io::Error),

    #[error("failed to parse config TOML: {0}")]
    Toml(#[from] toml::de::Error),

    #[error("missing required field: {0}")]
    MissingField(String),
}

impl VitrineConfig {
    /// Load configuration from a TOML file.
    pub fn load(path: &std::path::Path) -> Result<Self, ConfigError> {
        let text = std::fs::read_to_string(path)?;
        let cfg: VitrineConfig = toml::from_str(&text)?;
        Ok(cfg)
    }

    /// Try to find a config file at the conventional locations.
    pub fn discover() -> Result<Option<PathBuf>, ConfigError> {
        if let Ok(p) = std::env::var("VITRINE_CONFIG") {
            return Ok(Some(PathBuf::from(p)));
        }
        let candidates = [".vitrine.toml", "vitrine.toml"];
        for c in &candidates {
            let p = PathBuf::from(c);
            if p.exists() {
                return Ok(Some(p));
            }
        }
        Ok(None)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn minimal_config_parses() {
        let text = r#"
[target]
kubectl_context = "dbk-staging-europe-west3-gke"
cloud_project   = "dbk-staging-314422"
terragrunt_root = "/path/to/terragrunt"

[evidence]
output_path = "evidence.md"
"#;
        let cfg: VitrineConfig = toml::from_str(text).unwrap();
        assert_eq!(cfg.target.kubectl_context, "dbk-staging-europe-west3-gke");
        assert!(cfg.argocd.is_none());
        assert!(cfg.evidence.functional_probes.is_empty());
    }
}