Skip to main content

vitrine_types/
lib.rs

1//! Typed configuration for the vitrine CLI.
2//!
3//! Shikumi-style: every config knob is a typed Rust struct with strong
4//! validation at parse time. Loaded from TOML (default location:
5//! `.vitrine.toml` in the repo root, or `VITRINE_CONFIG` env var).
6
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9
10/// Root configuration for a vitrine session.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct VitrineConfig {
13    pub target: TargetEnvironment,
14    pub argocd: Option<ArgoCdConfig>,
15    pub evidence: EvidenceConfig,
16}
17
18/// The target environment a vitrine PR is delivered to.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct TargetEnvironment {
21    /// kubectl context name for the cluster receiving the change.
22    pub kubectl_context: String,
23
24    /// Cloud provider project / subscription / account ID.
25    pub cloud_project: String,
26
27    /// Path to the terragrunt root that owns the target's IaC.
28    pub terragrunt_root: PathBuf,
29}
30
31/// ArgoCD configuration — required for chart-driven vitrine PRs (Pattern A).
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct ArgoCdConfig {
34    /// Path to the cluster's argocd_cluster terragrunt module.
35    pub cluster_terragrunt: PathBuf,
36
37    /// Namespace where ArgoCD runs (typically "argocd").
38    pub namespace: String,
39
40    /// ApplicationSet generator path (for reconciliation watching).
41    pub applicationset_path: Option<PathBuf>,
42}
43
44/// Evidence-capture configuration.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct EvidenceConfig {
47    /// Output file for the captured evidence Markdown.
48    pub output_path: PathBuf,
49
50    /// Whether to include the full plan output or just the summary.
51    #[serde(default)]
52    pub include_full_plan: bool,
53
54    /// Functional probes to run (curl, kubectl get, etc.).
55    #[serde(default)]
56    pub functional_probes: Vec<FunctionalProbe>,
57}
58
59/// Which of the three vitrine evidence layers a probe belongs to.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum ProbeLayer {
63    /// `terragrunt output -json`, state inspection
64    TfState,
65    /// `gcloud … describe`, `aws … list`, cloud-provider API
66    CloudApi,
67    /// `curl`, `kubectl get`, end-to-end probe
68    Functional,
69}
70
71impl ProbeLayer {
72    pub fn label(self) -> &'static str {
73        match self {
74            ProbeLayer::TfState => "TF state",
75            ProbeLayer::CloudApi => "Cloud API",
76            ProbeLayer::Functional => "Functional",
77        }
78    }
79}
80
81fn default_layer() -> ProbeLayer {
82    ProbeLayer::Functional
83}
84
85/// A single probe to run + cite in evidence.
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct FunctionalProbe {
88    /// Human-readable name (e.g., "nginx default 404").
89    pub name: String,
90
91    /// Shell command (executed via `sh -c <command>` so shell features work).
92    pub command: String,
93
94    /// Which evidence layer this probe belongs to. Defaults to `functional`.
95    #[serde(default = "default_layer")]
96    pub layer: ProbeLayer,
97
98    /// Expected exit code (None to accept any).
99    pub expected_exit: Option<i32>,
100}
101
102/// Pattern A annotation operation — set on a cluster's argocd_cluster TF.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct PatternAOverride {
105    /// Annotation key (typically the chart name).
106    pub chart_name: String,
107
108    /// Feature branch to point targetRevision at.
109    pub feature_branch: String,
110}
111
112#[derive(thiserror::Error, Debug)]
113pub enum ConfigError {
114    #[error("failed to read config file: {0}")]
115    Io(#[from] std::io::Error),
116
117    #[error("failed to parse config TOML: {0}")]
118    Toml(#[from] toml::de::Error),
119
120    #[error("missing required field: {0}")]
121    MissingField(String),
122}
123
124impl VitrineConfig {
125    /// Load configuration from a TOML file.
126    pub fn load(path: &std::path::Path) -> Result<Self, ConfigError> {
127        let text = std::fs::read_to_string(path)?;
128        let cfg: VitrineConfig = toml::from_str(&text)?;
129        Ok(cfg)
130    }
131
132    /// Try to find a config file at the conventional locations.
133    pub fn discover() -> Result<Option<PathBuf>, ConfigError> {
134        if let Ok(p) = std::env::var("VITRINE_CONFIG") {
135            return Ok(Some(PathBuf::from(p)));
136        }
137        let candidates = [".vitrine.toml", "vitrine.toml"];
138        for c in &candidates {
139            let p = PathBuf::from(c);
140            if p.exists() {
141                return Ok(Some(p));
142            }
143        }
144        Ok(None)
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn minimal_config_parses() {
154        let text = r#"
155[target]
156kubectl_context = "dbk-staging-europe-west3-gke"
157cloud_project   = "dbk-staging-314422"
158terragrunt_root = "/path/to/terragrunt"
159
160[evidence]
161output_path = "evidence.md"
162"#;
163        let cfg: VitrineConfig = toml::from_str(text).unwrap();
164        assert_eq!(cfg.target.kubectl_context, "dbk-staging-europe-west3-gke");
165        assert!(cfg.argocd.is_none());
166        assert!(cfg.evidence.functional_probes.is_empty());
167    }
168}