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
10pub(crate) fn resolve_run_clock(
14 flag: Option<&str>,
15) -> CliResult<chrono::DateTime<chrono::FixedOffset>> {
16 use chrono::{DateTime, NaiveDate, TimeZone, Utc};
17 match flag {
18 None => Ok(Utc::now().fixed_offset()),
19 Some(s) => {
20 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
21 return Ok(dt);
22 }
23 if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
24 let ndt = d.and_hms_opt(0, 0, 0).expect("00:00:00 is valid");
25 return Ok(Utc.from_utc_datetime(&ndt).fixed_offset());
26 }
27 Err(CliError::Config(format!(
28 "--clock '{s}' is not RFC3339 (2026-01-31T00:00:00Z) or a date (2026-01-31)"
29 )))
30 }
31 }
32}
33
34pub async fn run(args: RunArgs) -> CliResult<()> {
36 let cwd = std::env::current_dir()?;
37 let env_path =
38 crate::env_loader::resolve_env_file(args.env_file.as_deref(), args.no_env_file, &cwd)?;
39 crate::env_loader::load_env_file_if_present(env_path.as_deref())?;
40
41 let resolved_config_path: Option<std::path::PathBuf> = if args.from_env {
42 None
43 } else {
44 Some(match args.config.as_ref() {
45 Some(p) => p.clone(),
46 None => {
47 crate::env_loader::discover_config_path(&cwd).ok_or(CliError::NoConfigOrFromEnv)?
48 }
49 })
50 };
51
52 let cfg = if args.from_env {
53 if args.profile.is_some() {
54 tracing::warn!(
55 "--profile / FAUCET_PROFILE has no effect in --from-env mode (no config file to compose); ignoring"
56 );
57 }
58 crate::env_config::from_process_env()?
59 } else {
60 PipelineConfig::from_path_async(
61 resolved_config_path
62 .as_ref()
63 .expect("YAML mode always resolves a path above"),
64 args.profile.as_deref(),
65 )
66 .await?
67 };
68
69 #[cfg(not(feature = "cli-tui"))]
70 if args.tui {
71 return Err(CliError::Config(
72 "--tui requires a binary built with the `cli-tui` feature \
73 (e.g. `cargo install faucet-cli --features cli-tui`)"
74 .into(),
75 ));
76 }
77 #[cfg(feature = "cli-tui")]
78 let tui_active = crate::tui::is_tui_session(args.tui);
79 #[cfg(feature = "cli-tui")]
80 let tui_handle = if tui_active {
81 Some(crate::tui::setup_observability(&cfg)?)
82 } else {
83 if args.tui {
84 tracing::info!("--tui: stdout is not a terminal; running without the TUI");
85 }
86 crate::obs::install(&cfg)?;
87 None
88 };
89 #[cfg(not(feature = "cli-tui"))]
90 crate::obs::install(&cfg)?;
91
92 let pipeline_name = cfg.name.clone().unwrap_or_else(|| {
93 resolved_config_path
94 .as_ref()
95 .and_then(|p| p.file_stem())
96 .and_then(|s| s.to_str())
97 .unwrap_or("pipeline")
98 .to_owned()
99 });
100
101 let auth = crate::auth_catalog::build_auth_catalog(cfg.auth.as_ref())?;
102 #[cfg(feature = "lineage")]
103 let lineage = crate::lineage_glue::build_emitter(cfg.lineage.as_ref())
104 .map_err(|e| CliError::Config(format!("lineage: {e}")))?;
105 let resilience = match &cfg.resilience {
106 Some(spec) => Some(spec.to_policy()?),
107 None => None,
108 };
109 #[cfg(feature = "notify")]
110 let notifier = crate::notify::Notifier::from_specs(&cfg.notifications)?;
111 #[cfg(feature = "catalog")]
112 let catalog = match cfg.catalog.as_ref() {
113 Some(spec) => Some(crate::catalog::connect_from_spec(spec).await?),
114 None => None,
115 };
116 let nodes = expand(&cfg)?;
117 #[cfg(feature = "cli-tui")]
120 let tui_cancel = tui_active.then(faucet_core::CancellationToken::new);
121 #[cfg(not(feature = "cli-tui"))]
122 let tui_cancel: Option<faucet_core::CancellationToken> = None;
123 let run_fut = run_expanded(
124 nodes,
125 ExecuteOptions {
126 pipeline_name: pipeline_name.clone(),
127 execution: cfg.execution.clone(),
128 dry_run: args.dry_run,
129 limit: args.limit,
130 state_path_override: args.state_path.clone(),
131 shard: None,
132 auth,
133 clock: resolve_run_clock(args.clock.as_deref())?,
134 cancel: tui_cancel.clone(),
138 resilience,
139 sla: cfg.sla.clone(),
140 #[cfg(feature = "lineage")]
141 lineage,
142 #[cfg(feature = "lineage")]
143 lineage_cfg: cfg.lineage.clone(),
144 #[cfg(feature = "notify")]
145 notifier,
146 #[cfg(feature = "catalog")]
147 catalog,
148 },
149 );
150 #[cfg(feature = "cli-tui")]
151 let summary = match (tui_handle, tui_cancel) {
152 (Some(handle), Some(cancel)) => {
153 let result = crate::tui::drive(run_fut, &pipeline_name, handle, cancel).await;
154 if result
155 .as_ref()
156 .map(|s| s.failure_count() > 0)
157 .unwrap_or(true)
158 {
159 crate::tui::flush_logs_to_stderr(25);
162 }
163 result?
164 }
165 _ => run_fut.await?,
166 };
167 #[cfg(not(feature = "cli-tui"))]
168 let summary = run_fut.await?;
169
170 let total_written: usize = summary.invocations.iter().map(|i| i.records_written).sum();
171 let success = summary
172 .invocations
173 .iter()
174 .filter(|i| i.error.is_none())
175 .count();
176 let failed = summary.failure_count();
177
178 tracing::info!(
179 pipeline = %pipeline_name,
180 invocations = summary.invocations.len(),
181 succeeded = success,
182 failed,
183 records_written = total_written,
184 "pipeline completed"
185 );
186 println!(
187 "{}: {} invocation{}, {} ok, {} failed, wrote {} record{}",
188 pipeline_name,
189 summary.invocations.len(),
190 if summary.invocations.len() == 1 {
191 ""
192 } else {
193 "s"
194 },
195 success,
196 failed,
197 total_written,
198 if total_written == 1 { "" } else { "s" }
199 );
200
201 faucet_core::shutdown_otel();
204
205 if summary.had_failures() {
206 return Err(CliError::PipelineHadFailures { count: failed });
207 }
208 Ok(())
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn run_clock_parses_rfc3339_date_and_defaults() {
217 let c = resolve_run_clock(Some("2026-01-31T12:00:00Z")).unwrap();
219 assert_eq!(c.format("%Y-%m-%d %H:%M").to_string(), "2026-01-31 12:00");
220 let c = resolve_run_clock(Some("2026-01-31")).unwrap();
222 assert_eq!(c.format("%Y-%m-%d %H:%M").to_string(), "2026-01-31 00:00");
223 let c = resolve_run_clock(None).unwrap();
225 assert!(c.format("%Y").to_string().parse::<i32>().unwrap() >= 2025);
226 assert!(resolve_run_clock(Some("not-a-date")).is_err());
228 }
229}