faucet_cli/commands/
run.rs1use crate::cli::RunArgs;
5use crate::config::PipelineConfig;
6use crate::error::{CliError, CliResult};
7use crate::executor::{ExecuteOptions, run_expanded};
8use crate::expand::expand;
9
10fn resolve_run_clock(flag: Option<&str>) -> CliResult<chrono::DateTime<chrono::FixedOffset>> {
13 use chrono::{DateTime, NaiveDate, TimeZone, Utc};
14 match flag {
15 None => Ok(Utc::now().fixed_offset()),
16 Some(s) => {
17 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
18 return Ok(dt);
19 }
20 if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
21 let ndt = d.and_hms_opt(0, 0, 0).expect("00:00:00 is valid");
22 return Ok(Utc.from_utc_datetime(&ndt).fixed_offset());
23 }
24 Err(CliError::Config(format!(
25 "--clock '{s}' is not RFC3339 (2026-01-31T00:00:00Z) or a date (2026-01-31)"
26 )))
27 }
28 }
29}
30
31pub async fn run(args: RunArgs) -> CliResult<()> {
33 let cwd = std::env::current_dir()?;
34 let env_path =
35 crate::env_loader::resolve_env_file(args.env_file.as_deref(), args.no_env_file, &cwd)?;
36 crate::env_loader::load_env_file_if_present(env_path.as_deref())?;
37
38 let resolved_config_path: Option<std::path::PathBuf> = if args.from_env {
39 None
40 } else {
41 Some(match args.config.as_ref() {
42 Some(p) => p.clone(),
43 None => {
44 crate::env_loader::discover_config_path(&cwd).ok_or(CliError::NoConfigOrFromEnv)?
45 }
46 })
47 };
48
49 let cfg = if args.from_env {
50 if args.profile.is_some() {
51 tracing::warn!(
52 "--profile / FAUCET_PROFILE has no effect in --from-env mode (no config file to compose); ignoring"
53 );
54 }
55 crate::env_config::from_process_env()?
56 } else {
57 PipelineConfig::from_path_async(
58 resolved_config_path
59 .as_ref()
60 .expect("YAML mode always resolves a path above"),
61 args.profile.as_deref(),
62 )
63 .await?
64 };
65
66 crate::obs::install(&cfg)?;
67
68 let pipeline_name = cfg.name.clone().unwrap_or_else(|| {
69 resolved_config_path
70 .as_ref()
71 .and_then(|p| p.file_stem())
72 .and_then(|s| s.to_str())
73 .unwrap_or("pipeline")
74 .to_owned()
75 });
76
77 let auth = crate::auth_catalog::build_auth_catalog(cfg.auth.as_ref())?;
78 #[cfg(feature = "lineage")]
79 let lineage = crate::lineage_glue::build_emitter(cfg.lineage.as_ref())
80 .map_err(|e| CliError::Config(format!("lineage: {e}")))?;
81 let resilience = match &cfg.resilience {
82 Some(spec) => Some(spec.to_policy()?),
83 None => None,
84 };
85 let nodes = expand(&cfg)?;
86 let summary = run_expanded(
87 nodes,
88 ExecuteOptions {
89 pipeline_name: pipeline_name.clone(),
90 execution: cfg.execution.clone(),
91 dry_run: args.dry_run,
92 limit: args.limit,
93 state_path_override: args.state_path.clone(),
94 shard: None,
95 auth,
96 clock: resolve_run_clock(args.clock.as_deref())?,
97 cancel: None,
100 resilience,
101 #[cfg(feature = "lineage")]
102 lineage,
103 #[cfg(feature = "lineage")]
104 lineage_cfg: cfg.lineage.clone(),
105 },
106 )
107 .await?;
108
109 let total_written: usize = summary.invocations.iter().map(|i| i.records_written).sum();
110 let success = summary
111 .invocations
112 .iter()
113 .filter(|i| i.error.is_none())
114 .count();
115 let failed = summary.failure_count();
116
117 tracing::info!(
118 pipeline = %pipeline_name,
119 invocations = summary.invocations.len(),
120 succeeded = success,
121 failed,
122 records_written = total_written,
123 "pipeline completed"
124 );
125 println!(
126 "{}: {} invocation{}, {} ok, {} failed, wrote {} record{}",
127 pipeline_name,
128 summary.invocations.len(),
129 if summary.invocations.len() == 1 {
130 ""
131 } else {
132 "s"
133 },
134 success,
135 failed,
136 total_written,
137 if total_written == 1 { "" } else { "s" }
138 );
139
140 faucet_core::shutdown_otel();
143
144 if summary.had_failures() {
145 return Err(CliError::PipelineHadFailures { count: failed });
146 }
147 Ok(())
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn run_clock_parses_rfc3339_date_and_defaults() {
156 let c = resolve_run_clock(Some("2026-01-31T12:00:00Z")).unwrap();
158 assert_eq!(c.format("%Y-%m-%d %H:%M").to_string(), "2026-01-31 12:00");
159 let c = resolve_run_clock(Some("2026-01-31")).unwrap();
161 assert_eq!(c.format("%Y-%m-%d %H:%M").to_string(), "2026-01-31 00:00");
162 let c = resolve_run_clock(None).unwrap();
164 assert!(c.format("%Y").to_string().parse::<i32>().unwrap() >= 2025);
165 assert!(resolve_run_clock(Some("not-a-date")).is_err());
167 }
168}