1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3pub mod auth_catalog;
15pub mod backfill;
16#[cfg(feature = "catalog")]
17pub mod catalog;
18pub mod cli;
19pub mod commands;
20pub mod compose;
21pub mod config;
22pub mod conformance;
23pub mod dlq_replay;
24pub mod env_config;
25pub mod env_loader;
26pub mod error;
27pub mod executor;
28pub mod expand;
29pub mod init_template;
30pub mod interpolate;
31#[cfg(feature = "lineage")]
32pub mod lineage_glue;
33pub mod merge;
34#[cfg(feature = "notify")]
35pub mod notify;
36pub mod obs;
37pub mod pipeline_test;
38pub mod registry;
39pub mod registry_index;
40pub mod replication;
41pub mod scaffold;
42#[cfg(feature = "schedule")]
43pub mod schedule;
44pub mod schema_compose;
45pub mod secrets;
46#[cfg(feature = "serve")]
47pub mod serve;
48pub mod sla;
49pub mod state;
50pub mod transforms;
51#[cfg(feature = "cli-tui")]
52pub mod tui;
53
54pub use error::{CliError, CliResult};
55
56use crate::cli::{Cli, Command};
57use crate::registry::PluginRegistry;
58
59pub fn run_main(registry: PluginRegistry) -> std::process::ExitCode {
80 use clap::Parser;
81 use std::process::ExitCode;
82
83 if let Err(err) = registry.install() {
84 commands::report(&err);
85 return ExitCode::from(1);
86 }
87 let cli = Cli::parse();
88 #[cfg(feature = "serve")]
89 let is_serve = matches!(cli.command, Command::Serve(_));
90 #[cfg(not(feature = "serve"))]
91 let is_serve = false;
92 #[cfg(feature = "cli-tui")]
95 let is_tui = matches!(&cli.command, Command::Run(a) if tui::is_tui_session(a.tui));
96 #[cfg(not(feature = "cli-tui"))]
97 let is_tui = false;
98 if !is_serve && !is_tui {
101 install_tracing(&cli.log_level);
102 }
103 #[cfg(feature = "cli-tui")]
104 if is_tui {
105 tui::install_tui_tracing(&cli.log_level);
106 }
107
108 let runtime = match tokio::runtime::Builder::new_multi_thread()
109 .enable_all()
110 .build()
111 {
112 Ok(rt) => rt,
113 Err(e) => {
114 eprintln!("error: failed to start async runtime: {e}");
115 return ExitCode::from(1);
116 }
117 };
118
119 runtime.block_on(async move {
120 match run_command(cli).await {
121 Ok(()) => ExitCode::SUCCESS,
122 Err(CliError::DoctorFailed { failed }) => ExitCode::from(failed.min(255) as u8),
125 Err(CliError::TestsFailed { failed }) => ExitCode::from(failed.min(255) as u8),
126 Err(CliError::BackfillFailed { failed }) => ExitCode::from(failed.min(255) as u8),
127 Err(err) => {
128 commands::report(&err);
129 ExitCode::from(1)
130 }
131 }
132 })
133}
134
135pub async fn run_command(cli: Cli) -> CliResult<()> {
140 #[cfg(feature = "serve")]
141 let serve_log_level = cli.log_level.clone();
142 match cli.command {
143 Command::Run(args) => commands::run::run(args).await,
144 Command::Backfill(args) => commands::backfill::run(args).await,
145 Command::Replicate(args) => commands::replicate::run(args).await,
146 Command::Discover(args) => commands::discover::run(args).await,
147 Command::Validate(args) => commands::validate::run(args).await,
148 Command::Schema(args) => commands::schema::run(args).await,
149 Command::List(args) => commands::list::run(args).await,
150 Command::Search(args) => commands::search::run(args).await,
151 Command::Conformance(args) => commands::conformance::run(args).await,
152 Command::Install(args) => commands::install::run(args).await,
153 Command::Preview(args) => commands::preview::run(args).await,
154 Command::Plan(args) => commands::plan::run(args).await,
155 #[cfg(feature = "cli-dev")]
156 Command::Dev(args) => commands::dev::run(args).await,
157 Command::Init(args) => commands::init::run(args).await,
158 Command::New(args) => commands::new::run(args).await,
159 Command::Doctor(args) => commands::doctor::run(args).await,
160 Command::Test(args) => commands::test::run(args).await,
161 Command::Dlq(args) => commands::dlq::run(args).await,
162 #[cfg(feature = "contract")]
163 Command::Contract(args) => commands::contract::run(args).await,
164 #[cfg(feature = "masking")]
165 Command::Masking(args) => commands::masking::run(args).await,
166 #[cfg(feature = "schedule")]
167 Command::Schedule(args) => commands::schedule::run(args).await,
168 #[cfg(feature = "serve")]
169 Command::Serve(args) => commands::serve::run(args, serve_log_level).await,
170 #[cfg(feature = "notify")]
171 Command::Notify(args) => commands::notify::run(args).await,
172 #[cfg(feature = "catalog")]
173 Command::Catalog(args) => commands::catalog::run(args).await,
174 }
175}
176
177#[cfg(feature = "observability")]
178fn install_tracing(level: &str) {
179 use crate::secrets::registry::RedactingMakeWriter;
180 use tracing_subscriber::EnvFilter;
181 let filter = EnvFilter::try_new(level).unwrap_or_else(|_| EnvFilter::new("info"));
182 let _ = tracing_subscriber::fmt()
183 .with_env_filter(filter)
184 .with_writer(RedactingMakeWriter)
185 .try_init();
186}
187
188#[cfg(not(feature = "observability"))]
191fn install_tracing(_level: &str) {}
192
193pub async fn run_from_yaml_str(yaml: &str) -> CliResult<executor::RunSummary> {
200 let mut value: serde_json::Value =
204 serde_yaml::from_str(yaml).map_err(|e| CliError::ParseConfig {
205 path: std::path::PathBuf::from("<yaml-string>"),
206 message: e.to_string(),
207 })?;
208 interpolate::interpolate_value(&mut value)?;
209 let interpolated = serde_yaml::to_string(&value).map_err(|e| CliError::ParseConfig {
210 path: std::path::PathBuf::from("<yaml-string>"),
211 message: e.to_string(),
212 })?;
213 let mut cfg: config::PipelineConfig =
214 serde_yaml::from_str(&interpolated).map_err(|e| CliError::ParseConfig {
215 path: std::path::PathBuf::from("<yaml-string>"),
216 message: e.to_string(),
217 })?;
218 if cfg.version != 1 {
219 return Err(CliError::ParseConfig {
220 path: std::path::PathBuf::from("<yaml-string>"),
221 message: format!(
222 "unsupported pipeline version {}, only version 1 is recognised",
223 cfg.version
224 ),
225 });
226 }
227 crate::secrets::resolve_secrets(&mut cfg).await?;
228 let pipeline_name = cfg.name.clone().unwrap_or_else(|| "unnamed".to_string());
229 let auth = auth_catalog::build_auth_catalog(cfg.auth.as_ref())?;
230 let resilience = match &cfg.resilience {
231 Some(spec) => Some(spec.to_policy()?),
232 None => None,
233 };
234 #[cfg(feature = "catalog")]
235 let catalog = match cfg.catalog.as_ref() {
236 Some(spec) => Some(catalog::connect_from_spec(spec).await?),
237 None => None,
238 };
239 let nodes = expand::expand(&cfg)?;
240 executor::run_expanded(
241 nodes,
242 executor::ExecuteOptions {
243 pipeline_name,
244 execution: cfg.execution.clone(),
245 dry_run: false,
246 limit: None,
247 state_path_override: None,
248 shard: None,
249 auth,
250 clock: chrono::Utc::now().fixed_offset(),
251 cancel: None,
252 resilience,
253 sla: cfg.sla.clone(),
254 #[cfg(feature = "lineage")]
255 lineage: None,
256 #[cfg(feature = "lineage")]
257 lineage_cfg: None,
258 #[cfg(feature = "notify")]
259 notifier: None,
260 #[cfg(feature = "catalog")]
261 catalog,
262 },
263 )
264 .await
265}