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;
33#[cfg(any(feature = "cli-tui", feature = "cli-progress"))]
36pub mod livemetrics;
37pub mod merge;
38#[cfg(feature = "notify")]
39pub mod notify;
40pub mod obs;
41pub mod pipeline_test;
42#[cfg(feature = "cli-progress")]
43pub mod progress;
44pub mod registry;
45pub mod registry_index;
46pub mod replication;
47pub mod scaffold;
48#[cfg(feature = "schedule")]
49pub mod schedule;
50pub mod schema_compose;
51pub mod secrets;
52pub mod select;
53#[cfg(feature = "serve")]
54pub mod serve;
55pub mod sla;
56pub mod state;
57pub mod transforms;
58#[cfg(feature = "cli-tui")]
59pub mod tui;
60
61pub use error::{CliError, CliResult};
62
63use crate::cli::{Cli, Command};
64use crate::registry::PluginRegistry;
65
66pub fn run_main(registry: PluginRegistry) -> std::process::ExitCode {
87 use clap::Parser;
88 use std::process::ExitCode;
89
90 if let Err(err) = registry.install() {
91 commands::report(&err);
92 return ExitCode::from(1);
93 }
94
95 clap_complete::env::CompleteEnv::with_factory(<Cli as clap::CommandFactory>::command)
100 .complete();
101
102 let cli = Cli::parse();
103 #[cfg(feature = "serve")]
104 let is_serve = matches!(cli.command, Command::Serve(_));
105 #[cfg(not(feature = "serve"))]
106 let is_serve = false;
107 #[cfg(feature = "cli-tui")]
110 let is_tui = matches!(&cli.command, Command::Run(a) if tui::is_tui_session(a.tui));
111 #[cfg(not(feature = "cli-tui"))]
112 let is_tui = false;
113 if !is_serve && !is_tui {
116 install_tracing(&cli.log_level);
117 }
118 #[cfg(feature = "cli-tui")]
119 if is_tui {
120 tui::install_tui_tracing(&cli.log_level);
121 }
122
123 let runtime = match tokio::runtime::Builder::new_multi_thread()
124 .enable_all()
125 .build()
126 {
127 Ok(rt) => rt,
128 Err(e) => {
129 eprintln!("error: failed to start async runtime: {e}");
130 return ExitCode::from(1);
131 }
132 };
133
134 runtime.block_on(async move {
135 match run_command(cli).await {
136 Ok(()) => ExitCode::SUCCESS,
137 Err(CliError::DoctorFailed { failed }) => ExitCode::from(failed.min(255) as u8),
140 Err(CliError::TestsFailed { failed }) => ExitCode::from(failed.min(255) as u8),
141 Err(CliError::BackfillFailed { failed }) => ExitCode::from(failed.min(255) as u8),
142 Err(err) => {
143 commands::report(&err);
144 ExitCode::from(1)
145 }
146 }
147 })
148}
149
150pub async fn run_command(cli: Cli) -> CliResult<()> {
155 #[cfg(feature = "serve")]
156 let serve_log_level = cli.log_level.clone();
157 match cli.command {
158 Command::Run(args) => commands::run::run(args).await,
159 Command::Backfill(args) => commands::backfill::run(args).await,
160 Command::Replicate(args) => commands::replicate::run(args).await,
161 Command::Discover(args) => commands::discover::run(args).await,
162 Command::Validate(args) => commands::validate::run(args).await,
163 Command::Schema(args) => commands::schema::run(args).await,
164 Command::List(args) => commands::list::run(args).await,
165 Command::Search(args) => commands::search::run(args).await,
166 Command::Conformance(args) => commands::conformance::run(args).await,
167 Command::Install(args) => commands::install::run(args).await,
168 Command::Preview(args) => commands::preview::run(args).await,
169 Command::Plan(args) => commands::plan::run(args).await,
170 #[cfg(feature = "cli-dev")]
171 Command::Dev(args) => commands::dev::run(args).await,
172 Command::Init(args) => commands::init::run(args).await,
173 Command::New(args) => commands::new::run(args).await,
174 Command::Doctor(args) => commands::doctor::run(args).await,
175 Command::Test(args) => commands::test::run(args).await,
176 Command::Dlq(args) => commands::dlq::run(args).await,
177 #[cfg(feature = "contract")]
178 Command::Contract(args) => commands::contract::run(args).await,
179 #[cfg(feature = "masking")]
180 Command::Masking(args) => commands::masking::run(args).await,
181 #[cfg(feature = "schedule")]
182 Command::Schedule(args) => commands::schedule::run(args).await,
183 #[cfg(feature = "serve")]
184 Command::Serve(args) => commands::serve::run(args, serve_log_level).await,
185 #[cfg(feature = "notify")]
186 Command::Notify(args) => commands::notify::run(args).await,
187 #[cfg(feature = "catalog")]
188 Command::Catalog(args) => commands::catalog::run(args).await,
189 Command::Completions(args) => commands::completions::run(args.shell),
190 Command::Migrate(args) => commands::migrate::run(args).await,
191 Command::Fmt(args) => commands::fmt::run(args).await,
192 Command::Explain(args) => commands::explain::run(args).await,
193 #[cfg(feature = "catalog")]
194 Command::History(args) => commands::history::run(args).await,
195 }
196}
197
198#[cfg(feature = "observability")]
199fn install_tracing(level: &str) {
200 use crate::secrets::registry::RedactingMakeWriter;
201 use tracing_subscriber::EnvFilter;
202 let filter = EnvFilter::try_new(level).unwrap_or_else(|_| EnvFilter::new("info"));
203 let _ = tracing_subscriber::fmt()
204 .with_env_filter(filter)
205 .with_writer(RedactingMakeWriter)
206 .try_init();
207}
208
209#[cfg(not(feature = "observability"))]
212fn install_tracing(_level: &str) {}
213
214pub async fn run_from_yaml_str(yaml: &str) -> CliResult<executor::RunSummary> {
221 let mut value: serde_json::Value =
225 serde_yaml::from_str(yaml).map_err(|e| CliError::ParseConfig {
226 path: std::path::PathBuf::from("<yaml-string>"),
227 message: e.to_string(),
228 })?;
229 interpolate::interpolate_value(&mut value)?;
230 let interpolated = serde_yaml::to_string(&value).map_err(|e| CliError::ParseConfig {
231 path: std::path::PathBuf::from("<yaml-string>"),
232 message: e.to_string(),
233 })?;
234 let mut cfg: config::PipelineConfig =
235 serde_yaml::from_str(&interpolated).map_err(|e| CliError::ParseConfig {
236 path: std::path::PathBuf::from("<yaml-string>"),
237 message: e.to_string(),
238 })?;
239 if cfg.version != 1 {
240 return Err(CliError::ParseConfig {
241 path: std::path::PathBuf::from("<yaml-string>"),
242 message: format!(
243 "unsupported pipeline version {}, only version 1 is recognised",
244 cfg.version
245 ),
246 });
247 }
248 crate::secrets::resolve_secrets(&mut cfg).await?;
249 let pipeline_name = cfg.name.clone().unwrap_or_else(|| "unnamed".to_string());
250 let auth = auth_catalog::build_auth_catalog(cfg.auth.as_ref())?;
251 let resilience = match &cfg.resilience {
252 Some(spec) => Some(spec.to_policy()?),
253 None => None,
254 };
255 #[cfg(feature = "catalog")]
256 let catalog = match cfg.catalog.as_ref() {
257 Some(spec) => Some(catalog::connect_from_spec(spec).await?),
258 None => None,
259 };
260 let nodes = expand::expand(&cfg)?;
261 executor::run_expanded(
262 nodes,
263 executor::ExecuteOptions {
264 pipeline_name,
265 execution: cfg.execution.clone(),
266 dry_run: false,
267 limit: None,
268 state_path_override: None,
269 shard: None,
270 auth,
271 clock: chrono::Utc::now().fixed_offset(),
272 cancel: None,
273 resilience,
274 sla: cfg.sla.clone(),
275 #[cfg(feature = "lineage")]
276 lineage: None,
277 #[cfg(feature = "lineage")]
278 lineage_cfg: None,
279 #[cfg(feature = "notify")]
280 notifier: None,
281 #[cfg(feature = "catalog")]
282 catalog,
283 },
284 )
285 .await
286}