1use std::collections::{BTreeMap, BTreeSet};
11use std::future::Future;
12
13use std::path::PathBuf;
14use std::str::FromStr;
15use std::sync::LazyLock;
16use std::time::Duration;
17use std::{env, fs};
18
19use anyhow::{Context, anyhow, bail};
20use async_trait::async_trait;
21use aws_credential_types::provider::ProvideCredentials;
22use aws_types::SdkConfig;
23use futures::future::FutureExt;
24use itertools::Itertools;
25use mz_adapter::catalog::{Catalog, ConnCatalog, DebugAwsContext};
26use mz_adapter::session::Session;
27use mz_build_info::BuildInfo;
28use mz_catalog::config::ClusterReplicaSizeMap;
29use mz_catalog::durable::BootstrapArgs;
30use mz_ccsr::SubjectVersion;
31use mz_kafka_util::client::{
32 MzClientContext, create_new_client_config_simple, create_with_context,
33};
34use mz_ore::error::ErrorExt;
35use mz_ore::metrics::MetricsRegistry;
36use mz_ore::now::SYSTEM_TIME;
37use mz_ore::retry::Retry;
38use mz_ore::secure::Zeroizing;
39use mz_ore::task;
40use mz_ore::url::SensitiveUrl;
41use mz_persist_client::cache::PersistClientCache;
42use mz_persist_client::cfg::PersistConfig;
43use mz_persist_client::rpc::PubSubClientConnection;
44use mz_persist_client::{PersistClient, PersistLocation};
45use mz_postgres_util::{
46 Sql, batch_execute as pg_batch_execute, query as pg_query, query_one as pg_query_one,
47 sql as pg_sql,
48};
49use mz_sql::catalog::EnvironmentId;
50use mz_tls_util::make_tls;
51use openssl::pkcs12::Pkcs12;
52use rdkafka::ClientConfig;
53use rdkafka::producer::Producer;
54use regex::{Captures, Regex};
55use semver::Version;
56use tokio_postgres::CancelToken;
57use tokio_postgres::error::{DbError, SqlState};
58use tracing::info;
59use url::Url;
60
61use crate::error::PosError;
62use crate::parser::{
63 Command, PosCommand, SqlExpectedError, SqlOutput, VersionConstraint, validate_ident,
64};
65use crate::util;
66use crate::util::postgres::postgres_client;
67
68pub mod consistency;
69
70mod duckdb;
71mod file;
72mod fivetran;
73mod glue;
74mod http;
75mod kafka;
76mod mysql;
77mod nop;
78mod persist;
79mod postgres;
80mod protobuf;
81mod psql;
82mod s3;
83mod schema_registry;
84mod set;
85mod skip_end;
86mod skip_if;
87mod sleep;
88mod sql;
89mod sql_server;
90mod version_check;
91mod webhook;
92
93pub(crate) async fn verify_kafka_topics_exhausted(state: &State) -> Result<(), anyhow::Error> {
94 kafka::verify_topics_exhausted(state).await
95}
96
97#[derive(Debug, Clone)]
99pub struct Config {
100 pub arg_vars: BTreeMap<String, String>,
106 pub seed: Option<String>,
108 pub reset: bool,
111 pub temp_dir: Option<String>,
116 pub source: Option<String>,
118 pub default_timeout: Duration,
120 pub default_max_tries: usize,
122 pub initial_backoff: Duration,
126 pub backoff_factor: f64,
130 pub consistency_checks: consistency::Level,
132 pub consistency_check_timeout: Duration,
134 pub check_statement_logging: bool,
137 pub rewrite_results: bool,
139
140 pub materialize_pgconfig: tokio_postgres::Config,
144 pub materialize_internal_pgconfig: tokio_postgres::Config,
147 pub materialize_use_https: bool,
149 pub materialize_http_port: u16,
152 pub materialize_internal_http_port: u16,
155 pub materialize_password_sql_port: u16,
158 pub materialize_sasl_sql_port: u16,
161 pub materialize_params: Vec<(String, String)>,
163 pub materialize_catalog_config: Option<CatalogConfig>,
165 pub build_info: &'static BuildInfo,
167 pub materialize_cluster_replica_sizes: ClusterReplicaSizeMap,
169
170 pub persist_consensus_url: Option<SensitiveUrl>,
173 pub persist_blob_url: Option<SensitiveUrl>,
175
176 pub kafka_addr: String,
179 pub kafka_default_partitions: usize,
181 pub kafka_opts: Vec<(String, String)>,
184 pub schema_registry_url: Url,
186 pub cert_path: Option<String>,
191 pub cert_password: Option<String>,
193 pub ccsr_username: Option<String>,
196 pub ccsr_password: Option<String>,
199
200 pub aws_config: SdkConfig,
203 pub aws_account: String,
205
206 pub fivetran_destination_url: String,
209 pub fivetran_destination_files_path: String,
211}
212
213pub struct MaterializeState {
214 catalog_config: Option<CatalogConfig>,
215
216 sql_addr: String,
217 use_https: bool,
218 http_addr: String,
219 internal_sql_addr: String,
220 internal_http_addr: String,
221 password_sql_addr: String,
222 sasl_sql_addr: String,
223 user: String,
224 pgclient: tokio_postgres::Client,
225 environment_id: EnvironmentId,
226 bootstrap_args: BootstrapArgs,
227 aws_account_id: Option<String>,
235 aws_external_id_prefix: Option<String>,
236 aws_connection_role_arn: Option<String>,
237}
238
239pub struct State {
240 pub config: Config,
242
243 arg_vars: BTreeMap<String, String>,
245 cmd_vars: BTreeMap<String, String>,
246 seed: String,
247 temp_path: PathBuf,
248 _tempfile: Option<tempfile::TempDir>,
249 default_timeout: Duration,
250 timeout: Duration,
251 max_tries: usize,
252 initial_backoff: Duration,
253 backoff_factor: f64,
254 consistency_checks: consistency::Level,
255 consistency_check_timeout: Duration,
256 check_statement_logging: bool,
257 consistency_checks_adhoc_skip: bool,
258 regex: Option<Regex>,
259 regex_replacement: String,
260 error_line_count: usize,
261 error_string: String,
262
263 materialize: MaterializeState,
265
266 persist_consensus_url: Option<SensitiveUrl>,
268 persist_blob_url: Option<SensitiveUrl>,
269 build_info: &'static BuildInfo,
270 persist_clients: PersistClientCache,
271
272 schema_registry_url: Url,
274 ccsr_client: mz_ccsr::Client,
275 kafka_addr: String,
276 kafka_admin: rdkafka::admin::AdminClient<MzClientContext>,
277 kafka_admin_opts: rdkafka::admin::AdminOptions,
278 kafka_config: ClientConfig,
279 kafka_default_partitions: usize,
280 kafka_producer: rdkafka::producer::FutureProducer<MzClientContext>,
281 kafka_topics: BTreeMap<String, usize>,
282 kafka_verify_topics: BTreeSet<String>,
284
285 aws_account: String,
287 aws_config: SdkConfig,
288
289 pub duckdb_clients: BTreeMap<String, std::sync::Arc<std::sync::Mutex<::duckdb::Connection>>>,
291 mysql_clients: BTreeMap<String, mysql_async::Conn>,
292 postgres_clients: BTreeMap<String, tokio_postgres::Client>,
293 sql_server_clients: BTreeMap<String, mz_sql_server_util::Client>,
294 background_tasks: Vec<BackgroundTask>,
297
298 fivetran_destination_url: String,
300 fivetran_destination_files_path: String,
301
302 rewrite_results: bool,
304 pub rewrites: Vec<Rewrite>,
306 pub rewrite_pos_start: usize,
308 pub rewrite_pos_end: usize,
310}
311
312pub struct Rewrite {
313 pub content: String,
314 pub start: usize,
315 pub end: usize,
316}
317
318pub(crate) struct BackgroundTask {
329 desc: String,
330 handle: task::JoinHandle<Result<(), anyhow::Error>>,
331 cancel_token: CancelToken,
332 url: String,
335}
336
337async fn cancel_background_query(cancel_token: &CancelToken, url: &str, timeout: Duration) {
342 let tls = match tokio_postgres::Config::from_str(url)
343 .map_err(anyhow::Error::from)
344 .and_then(|config| make_tls(&config).map_err(anyhow::Error::from))
345 {
346 Ok(tls) => tls,
347 Err(e) => {
348 tracing::warn!("could not build TLS connector to cancel background query: {e}");
349 return;
350 }
351 };
352 match tokio::time::timeout(timeout, cancel_token.cancel_query(tls)).await {
353 Ok(Ok(())) => {}
354 Ok(Err(e)) => tracing::warn!("cancel request for background query failed: {e}"),
355 Err(_) => tracing::warn!("cancel request for background query timed out"),
356 }
357}
358
359impl State {
360 pub async fn initialize_cmd_vars(&mut self) -> Result<(), anyhow::Error> {
361 self.cmd_vars
362 .insert("testdrive.kafka-addr".into(), self.kafka_addr.clone());
363 self.cmd_vars.insert(
364 "testdrive.schema-registry-url".into(),
365 self.schema_registry_url.to_string(),
366 );
367 self.cmd_vars
368 .insert("testdrive.seed".into(), self.seed.clone());
369 self.cmd_vars.insert(
370 "testdrive.temp-dir".into(),
371 self.temp_path.display().to_string(),
372 );
373 self.cmd_vars
374 .insert("testdrive.aws-region".into(), self.aws_region().into());
375 self.cmd_vars
376 .insert("testdrive.aws-endpoint".into(), self.aws_endpoint().into());
377 self.cmd_vars
378 .insert("testdrive.aws-account".into(), self.aws_account.clone());
379 {
380 let aws_credentials = self
381 .aws_config
382 .credentials_provider()
383 .ok_or_else(|| anyhow!("no AWS credentials provider configured"))?
384 .provide_credentials()
385 .await
386 .context("fetching AWS credentials")?;
387 self.cmd_vars.insert(
388 "testdrive.aws-access-key-id".into(),
389 aws_credentials.access_key_id().to_owned(),
390 );
391 self.cmd_vars.insert(
392 "testdrive.aws-secret-access-key".into(),
393 aws_credentials.secret_access_key().to_owned(),
394 );
395 self.cmd_vars.insert(
396 "testdrive.aws-token".into(),
397 aws_credentials
398 .session_token()
399 .map(|token| token.to_owned())
400 .unwrap_or_else(String::new),
401 );
402 }
403 self.cmd_vars.insert(
404 "testdrive.materialize-environment-id".into(),
405 self.materialize.environment_id.to_string(),
406 );
407 self.cmd_vars.insert(
408 "testdrive.materialize-sql-addr".into(),
409 self.materialize.sql_addr.clone(),
410 );
411 self.cmd_vars.insert(
412 "testdrive.materialize-internal-sql-addr".into(),
413 self.materialize.internal_sql_addr.clone(),
414 );
415 self.cmd_vars.insert(
416 "testdrive.materialize-password-sql-addr".into(),
417 self.materialize.password_sql_addr.clone(),
418 );
419 self.cmd_vars.insert(
420 "testdrive.materialize-sasl-sql-addr".into(),
421 self.materialize.sasl_sql_addr.clone(),
422 );
423 self.cmd_vars.insert(
424 "testdrive.materialize-http-addr".into(),
425 self.materialize.http_addr.clone(),
426 );
427 self.cmd_vars.insert(
428 "testdrive.materialize-user".into(),
429 self.materialize.user.clone(),
430 );
431 self.cmd_vars.insert(
432 "testdrive.fivetran-destination-url".into(),
433 self.fivetran_destination_url.clone(),
434 );
435 self.cmd_vars.insert(
436 "testdrive.fivetran-destination-files-path".into(),
437 self.fivetran_destination_files_path.clone(),
438 );
439
440 for (key, value) in env::vars() {
441 self.cmd_vars.insert(format!("env.{}", key), value);
442 }
443
444 for (key, value) in &self.arg_vars {
445 validate_ident(key)?;
446 self.cmd_vars
447 .insert(format!("arg.{}", key), value.to_string());
448 }
449
450 Ok(())
451 }
452 pub async fn with_catalog_copy<F, T>(
455 &self,
456 system_parameter_defaults: BTreeMap<String, String>,
457 build_info: &'static BuildInfo,
458 bootstrap_args: &BootstrapArgs,
459 enable_expression_cache_override: Option<bool>,
460 f: F,
461 ) -> Result<Option<T>, anyhow::Error>
462 where
463 F: FnOnce(ConnCatalog) -> T,
464 {
465 async fn persist_client(
466 persist_consensus_url: SensitiveUrl,
467 persist_blob_url: SensitiveUrl,
468 persist_clients: &PersistClientCache,
469 ) -> Result<PersistClient, anyhow::Error> {
470 let persist_location = PersistLocation {
471 blob_uri: persist_blob_url,
472 consensus_uri: persist_consensus_url,
473 };
474 Ok(persist_clients.open(persist_location).await?)
475 }
476
477 if let Some(CatalogConfig {
478 persist_consensus_url,
479 persist_blob_url,
480 }) = &self.materialize.catalog_config
481 {
482 let persist_client = persist_client(
483 persist_consensus_url.clone(),
484 persist_blob_url.clone(),
485 &self.persist_clients,
486 )
487 .await?;
488 let aws_context = DebugAwsContext {
489 aws_account_id: self.materialize.aws_account_id.clone(),
490 aws_external_id_prefix: self.materialize.aws_external_id_prefix.clone(),
491 aws_connection_role_arn: self.materialize.aws_connection_role_arn.clone(),
492 };
493 let catalog = Catalog::open_debug_read_only_persist_catalog_config(
494 persist_client,
495 SYSTEM_TIME.clone(),
496 self.materialize.environment_id.clone(),
497 system_parameter_defaults,
498 build_info,
499 bootstrap_args,
500 enable_expression_cache_override,
501 Some(aws_context),
502 )
503 .await?;
504 let res = f(catalog.for_session(&Session::dummy()));
505 catalog.expire().await;
506 Ok(Some(res))
507 } else {
508 Ok(None)
509 }
510 }
511
512 pub fn aws_endpoint(&self) -> &str {
513 self.aws_config.endpoint_url().unwrap_or("")
514 }
515
516 pub fn aws_region(&self) -> &str {
517 self.aws_config.region().map(|r| r.as_ref()).unwrap_or("")
518 }
519
520 pub fn clear_skip_consistency_checks(&mut self) -> bool {
523 std::mem::replace(&mut self.consistency_checks_adhoc_skip, false)
524 }
525
526 pub(crate) async fn join_background_tasks(&mut self) -> Vec<anyhow::Error> {
531 let mut errors = Vec::new();
532 for BackgroundTask {
533 desc,
534 mut handle,
535 cancel_token,
536 url,
537 } in self.background_tasks.drain(..)
538 {
539 match tokio::time::timeout(self.default_timeout, &mut handle).await {
544 Ok(Ok(())) => {}
545 Ok(Err(e)) => errors.push(e.context(format!("background query failed: {desc}"))),
546 Err(_) => {
547 cancel_background_query(&cancel_token, &url, self.default_timeout).await;
551 handle.abort_and_wait().await;
552 errors.push(anyhow!(
553 "background query did not complete before the end of the file: {desc}"
554 ));
555 }
556 }
557 }
558 errors
559 }
560
561 pub async fn reset_materialize(&self) -> Result<(), anyhow::Error> {
562 let (inner_client, _) = postgres_client(
563 &format!(
564 "postgres://mz_system:materialize@{}",
565 self.materialize.internal_sql_addr
566 ),
567 self.default_timeout,
568 )
569 .await?;
570
571 let version = pg_query_one(&inner_client, pg_sql!("SELECT mz_version_num()"), &[])
572 .await
573 .context("getting version of materialize")
574 .map(|row| row.get::<_, i32>(0))?;
575
576 let semver = pg_query_one(
577 &inner_client,
578 pg_sql!("SELECT right(split_part(mz_version(), ' ', 1), -1)"),
579 &[],
580 )
581 .await
582 .context("getting semver of materialize")
583 .map(|row| row.get::<_, String>(0))?
584 .parse::<semver::Version>()
585 .context("parsing semver of materialize")?;
586
587 pg_batch_execute(&inner_client, pg_sql!("ALTER SYSTEM RESET ALL"))
588 .await
589 .context("resetting materialize state: ALTER SYSTEM RESET ALL")?;
590
591 {
593 let rename_version = Version::parse("0.128.0-dev.1").expect("known to be valid");
594 let enable_unsafe_functions = if semver >= rename_version {
595 "unsafe_enable_unsafe_functions"
596 } else {
597 "enable_unsafe_functions"
598 };
599 let res = pg_batch_execute(
600 &inner_client,
601 pg_sql!(
602 "ALTER SYSTEM SET {} = on",
603 Sql::ident(enable_unsafe_functions)
604 ),
605 )
606 .await
607 .context("enabling dangerous functions");
608 if let Err(e) = res {
609 match e.root_cause().downcast_ref::<DbError>() {
610 Some(e) if *e.code() == SqlState::CANT_CHANGE_RUNTIME_PARAM => {
611 info!(
612 "can't enable unsafe functions because the server is safe mode; \
613 testdrive scripts will fail if they use unsafe functions",
614 );
615 }
616 _ => return Err(e),
617 }
618 }
619 }
620
621 for row in pg_query(&inner_client, pg_sql!("SHOW DATABASES"), &[])
622 .await
623 .context("resetting materialize state: SHOW DATABASES")?
624 {
625 let db_name: String = row.get(0);
626 if db_name.starts_with("testdrive_no_reset_") {
627 continue;
628 }
629 let drop_database = pg_sql!("DROP DATABASE {}", Sql::ident(&db_name));
630 sql::print_query(drop_database.as_str(), None);
631 pg_batch_execute(&inner_client, drop_database)
632 .await
633 .context(format!(
634 "resetting materialize state: DROP DATABASE {}",
635 db_name,
636 ))?;
637 }
638
639 let inactive_user_clusters = "
641 WITH
642 active_user_clusters AS
643 (
644 SELECT DISTINCT cluster_id, object_id
645 FROM
646 (
647 SELECT cluster_id, id FROM mz_catalog.mz_sources
648 UNION ALL SELECT cluster_id, id FROM mz_catalog.mz_sinks
649 UNION ALL
650 SELECT cluster_id, id
651 FROM mz_catalog.mz_materialized_views
652 UNION ALL
653 SELECT cluster_id, id FROM mz_catalog.mz_indexes
654 UNION ALL
655 SELECT cluster_id, id
656 FROM mz_internal.mz_subscriptions
657 )
658 AS t (cluster_id, object_id)
659 WHERE cluster_id IS NOT NULL AND object_id LIKE 'u%'
660 )
661 SELECT name
662 FROM mz_catalog.mz_clusters
663 WHERE
664 id NOT IN ( SELECT cluster_id FROM active_user_clusters ) AND id LIKE 'u%'
665 AND
666 owner_id LIKE 'u%';";
667
668 let inactive_clusters = pg_query(&inner_client, Sql::new(inactive_user_clusters), &[])
669 .await
670 .context("resetting materialize state: inactive_user_clusters")?;
671
672 if !inactive_clusters.is_empty() {
673 println!("cleaning up user clusters from previous tests...")
674 }
675
676 for cluster_name in inactive_clusters {
677 let cluster_name: String = cluster_name.get(0);
678 if cluster_name.starts_with("testdrive_no_reset_") {
679 continue;
680 }
681 let drop_cluster = pg_sql!("DROP CLUSTER {}", Sql::ident(&cluster_name));
682 sql::print_query(drop_cluster.as_str(), None);
683 pg_batch_execute(&inner_client, drop_cluster)
684 .await
685 .context(format!(
686 "resetting materialize state: DROP CLUSTER {}",
687 cluster_name,
688 ))?;
689 }
690
691 pg_batch_execute(&inner_client, pg_sql!("CREATE DATABASE materialize"))
692 .await
693 .context("resetting materialize state: CREATE DATABASE materialize")?;
694
695 if let Ok(rows) = pg_query(&inner_client, pg_sql!("SELECT name FROM mz_roles"), &[]).await {
699 for row in rows {
700 let role_name: String = row.get(0);
701 if role_name == self.materialize.user || role_name.starts_with("mz_") {
702 continue;
703 }
704 let drop_role = pg_sql!("DROP ROLE {}", Sql::ident(&role_name));
705 sql::print_query(drop_role.as_str(), None);
706 pg_batch_execute(&inner_client, drop_role)
707 .await
708 .context(format!(
709 "resetting materialize state: DROP ROLE {}",
710 role_name,
711 ))?;
712 }
713 }
714
715 pg_batch_execute(
717 &inner_client,
718 pg_sql!(
719 "GRANT ALL PRIVILEGES ON SYSTEM TO {}",
720 Sql::ident(&self.materialize.user)
721 ),
722 )
723 .await?;
724
725 pg_batch_execute(
727 &inner_client,
728 pg_sql!("GRANT USAGE ON DATABASE materialize TO PUBLIC"),
729 )
730 .await?;
731 pg_batch_execute(
732 &inner_client,
733 pg_sql!(
734 "GRANT ALL PRIVILEGES ON DATABASE materialize TO {}",
735 Sql::ident(&self.materialize.user)
736 ),
737 )
738 .await?;
739 pg_batch_execute(
740 &inner_client,
741 pg_sql!(
742 "GRANT ALL PRIVILEGES ON SCHEMA materialize.public TO {}",
743 Sql::ident(&self.materialize.user)
744 ),
745 )
746 .await?;
747
748 let cluster = match version {
749 ..=8199 => "default",
750 8200.. => "quickstart",
751 };
752 pg_batch_execute(
753 &inner_client,
754 pg_sql!("GRANT USAGE ON CLUSTER {} TO PUBLIC", Sql::ident(cluster)),
755 )
756 .await?;
757 pg_batch_execute(
758 &inner_client,
759 pg_sql!(
760 "GRANT ALL PRIVILEGES ON CLUSTER {} TO {}",
761 Sql::ident(cluster),
762 Sql::ident(&self.materialize.user)
763 ),
764 )
765 .await?;
766
767 Ok(())
768 }
769
770 pub async fn reset_kafka(&self) -> Result<(), anyhow::Error> {
772 use rdkafka::types::RDKafkaErrorCode;
773 let mut errors: Vec<anyhow::Error> = Vec::new();
774
775 let metadata = self.kafka_producer.client().fetch_metadata(
776 None,
777 Some(std::cmp::max(Duration::from_secs(1), self.default_timeout)),
778 )?;
779
780 let testdrive_topics: Vec<_> = metadata
781 .topics()
782 .iter()
783 .filter_map(|t| {
784 if t.name().starts_with("testdrive-") {
785 Some(t.name())
786 } else {
787 None
788 }
789 })
790 .collect();
791
792 if !testdrive_topics.is_empty() {
793 match self
794 .kafka_admin
795 .delete_topics(&testdrive_topics, &self.kafka_admin_opts)
796 .await
797 {
798 Ok(res) => {
799 if res.len() != testdrive_topics.len() {
800 errors.push(anyhow!(
801 "kafka topic deletion returned {} results, but exactly {} expected",
802 res.len(),
803 testdrive_topics.len()
804 ));
805 }
806 for (res, topic) in res.iter().zip_eq(testdrive_topics.iter()) {
807 match res {
808 Ok(_) | Err((_, RDKafkaErrorCode::UnknownTopicOrPartition)) => (),
809 Err((_, err)) => {
810 errors.push(anyhow!("unable to delete {}: {}", topic, err));
811 }
812 }
813 }
814 }
815 Err(e) => {
816 errors.push(e.into());
817 }
818 };
819 }
820
821 let schema_registry_errors = self.reset_schema_registry().await;
822
823 errors.extend(schema_registry_errors);
824 if errors.is_empty() {
825 Ok(())
826 } else {
827 bail!(
828 "deleting Kafka topics: {} errors: {}",
829 errors.len(),
830 errors
831 .into_iter()
832 .map(|e| e.to_string_with_causes())
833 .join("\n")
834 );
835 }
836 }
837
838 #[allow(clippy::disallowed_types)]
839 async fn reset_schema_registry(&self) -> Vec<anyhow::Error> {
840 use std::collections::HashMap;
841
842 let mut errors = Vec::new();
843 match self
844 .ccsr_client
845 .list_subjects()
846 .await
847 .context("listing schema registry subjects")
848 {
849 Ok(subjects) => {
850 let testdrive_subjects: Vec<_> = subjects
851 .into_iter()
852 .filter(|s| s.starts_with("testdrive-"))
853 .collect();
854
855 let mut graphs: HashMap<SubjectVersion, Vec<SubjectVersion>> = HashMap::new();
857
858 for subject in &testdrive_subjects {
859 match self.ccsr_client.get_subject_with_references(subject).await {
860 Ok((subj, refs)) => {
861 let refs: Vec<_> = refs
863 .into_iter()
864 .filter(|r| r.subject.starts_with("testdrive-"))
865 .collect();
866 graphs.insert(
867 SubjectVersion {
868 subject: subj.name,
869 version: subj.version,
870 },
871 refs,
872 );
873 }
874 Err(mz_ccsr::GetBySubjectError::SubjectNotFound) => {
875 }
877 Err(e) => {
878 errors.push(anyhow::anyhow!(
879 "failed to get references for subject {}: {}",
880 subject,
881 e
882 ));
883 }
884 }
885 }
886
887 let subjects_to_delete: Vec<_> = match mz_ccsr::topological_sort(&graphs) {
890 Ok(ordered) => {
891 let mut subjects: Vec<_> = ordered.into_iter().collect();
892 subjects.sort_by(|a, b| a.1.cmp(&b.1));
893 subjects.into_iter().map(|(s, _)| s.clone()).collect()
894 }
895 Err(_) => {
896 tracing::info!("Cycle detected, attempting to delete anyway");
897 graphs.into_keys().collect()
899 }
900 };
901
902 for subject in subjects_to_delete {
903 match self.ccsr_client.delete_subject(&subject.subject).await {
904 Ok(()) | Err(mz_ccsr::DeleteError::SubjectNotFound) => (),
905 Err(e) => errors.push(e.into()),
906 }
907 }
908 }
909 Err(e) => {
910 errors.push(e);
911 }
912 }
913 errors
914 }
915}
916
917#[derive(Debug, Clone)]
919pub struct CatalogConfig {
920 pub persist_consensus_url: SensitiveUrl,
922 pub persist_blob_url: SensitiveUrl,
924}
925
926pub enum ControlFlow {
927 Continue,
928 SkipBegin,
929 SkipEnd,
930}
931
932#[async_trait]
933pub(crate) trait Run {
934 async fn run(self, state: &mut State) -> Result<ControlFlow, PosError>;
935}
936
937#[async_trait]
938impl Run for PosCommand {
939 async fn run(self, state: &mut State) -> Result<ControlFlow, PosError> {
940 macro_rules! handle_version {
941 ($version_constraint:expr) => {
942 match $version_constraint {
943 Some(VersionConstraint { min, max }) => {
944 match version_check::run_version_check(min, max, state).await {
945 Ok(true) => return Ok(ControlFlow::Continue),
946 Ok(false) => {}
947 Err(err) => return Err(PosError::new(err, self.pos)),
948 }
949 }
950 None => {}
951 }
952 };
953 }
954
955 let wrap_err = |e| PosError::new(e, self.pos);
956 let ignore_prefix = match &self.command {
959 Command::Builtin(builtin, _) => Some(builtin.name.clone()),
960 _ => None,
961 };
962 let subst = |msg: &str, vars: &BTreeMap<String, String>| {
963 substitute_vars(msg, vars, &ignore_prefix, false).map_err(wrap_err)
964 };
965 let subst_re = |msg: &str, vars: &BTreeMap<String, String>| {
966 substitute_vars(msg, vars, &ignore_prefix, true).map_err(wrap_err)
967 };
968
969 let r = match self.command {
970 Command::Builtin(mut builtin, version_constraint) => {
971 handle_version!(version_constraint);
972 for val in builtin.args.values_mut() {
973 *val = subst(val, &state.cmd_vars)?;
974 }
975 for line in &mut builtin.input {
976 *line = subst(line, &state.cmd_vars)?;
977 }
978 match builtin.name.as_ref() {
979 "check-consistency" => consistency::run_consistency_checks(state).await,
980 "skip-consistency-checks" => {
981 consistency::skip_consistency_checks(builtin, state)
982 }
983 "check-shard-tombstone" => {
984 consistency::run_check_shard_tombstone(builtin, state).await
985 }
986 "duckdb-execute" => duckdb::run_execute(builtin, state).await,
987 "duckdb-query" => duckdb::run_query(builtin, state).await,
988 "fivetran-destination" => {
989 fivetran::run_destination_command(builtin, state).await
990 }
991 "file-append" => file::run_append(builtin, state).await,
992 "file-delete" => file::run_delete(builtin, state).await,
993 "glue-create-schema" => glue::run_create_schema(builtin, state).await,
994 "glue-verify-compatibility" => {
995 glue::run_verify_compatibility(builtin, state).await
996 }
997 "http-request" => http::run_request(builtin, state).await,
998 "kafka-add-partitions" => kafka::run_add_partitions(builtin, state).await,
999 "kafka-create-topic" => kafka::run_create_topic(builtin, state).await,
1000 "kafka-wait-topic" => kafka::run_wait_topic(builtin, state).await,
1001 "kafka-delete-records" => kafka::run_delete_records(builtin, state).await,
1002 "kafka-delete-topic-flaky" => kafka::run_delete_topic(builtin, state).await,
1003 "kafka-ingest" => kafka::run_ingest(builtin, state).await,
1004 "kafka-verify-data" => kafka::run_verify_data(builtin, state).await,
1005 "kafka-verify-commit" => kafka::run_verify_commit(builtin, state).await,
1006 "kafka-verify-topic" => kafka::run_verify_topic(builtin, state).await,
1007 "mysql-connect" => mysql::run_connect(builtin, state).await,
1008 "mysql-execute" => mysql::run_execute(builtin, state).await,
1009 "nop" => nop::run_nop(),
1010 "postgres-connect" => postgres::run_connect(builtin, state).await,
1011 "postgres-execute" => postgres::run_execute(builtin, state).await,
1012 "postgres-verify-slot" => postgres::run_verify_slot(builtin, state).await,
1013 "protobuf-compile-descriptors" => {
1014 protobuf::run_compile_descriptors(builtin, state).await
1015 }
1016 "psql-execute" => psql::run_execute(builtin, state).await,
1017 "s3-verify-data" => s3::run_verify_data(builtin, state).await,
1018 "s3-verify-keys" => s3::run_verify_keys(builtin, state).await,
1019 "s3-file-upload" => s3::run_upload(builtin, state).await,
1020 "s3-set-presigned-url" => s3::run_set_presigned_url(builtin, state).await,
1021 "s3-upload-parquet-types" => s3::run_upload_parquet_types(builtin, state).await,
1022 "s3-upload-parquet-unsorted-map" => {
1023 s3::run_upload_parquet_unsorted_map(builtin, state).await
1024 }
1025 "schema-registry-publish" => schema_registry::run_publish(builtin, state).await,
1026 "schema-registry-verify" => schema_registry::run_verify(builtin, state).await,
1027 "schema-registry-wait" => schema_registry::run_wait(builtin, state).await,
1028 "skip-if" => skip_if::run_skip_if(builtin, state).await,
1029 "skip-end" => skip_end::run_skip_end(),
1030 "sql-server-connect" => sql_server::run_connect(builtin, state).await,
1031 "sql-server-execute" => sql_server::run_execute(builtin, state).await,
1032 "sql-server-set-from-sql" => sql_server::run_set_from_sql(builtin, state).await,
1033 "persist-force-compaction" => {
1034 persist::run_force_compaction(builtin, state).await
1035 }
1036 "random-sleep" => sleep::run_random_sleep(builtin),
1037 "set-regex" => set::run_regex_set(builtin, state),
1038 "unset-regex" => set::run_regex_unset(builtin, state),
1039 "set-sql-timeout" => set::run_sql_timeout(builtin, state),
1040 "set-max-tries" => set::run_max_tries(builtin, state),
1041 "sleep-is-probably-flaky-i-have-justified-my-need-with-a-comment" => {
1042 sleep::run_sleep(builtin)
1043 }
1044 "set" => set::set_vars(builtin, state),
1045 "set-arg-default" => set::run_set_arg_default(builtin, state),
1046 "set-from-sql" => set::run_set_from_sql(builtin, state).await,
1047 "set-from-file" => set::run_set_from_file(builtin, state).await,
1048 "webhook-append" => webhook::run_append(builtin, state).await,
1049 _ => {
1050 return Err(PosError::new(
1051 anyhow!("unknown built-in command {}", builtin.name),
1052 self.pos,
1053 ));
1054 }
1055 }
1056 }
1057 Command::Sql(mut sql, version_constraint) => {
1058 handle_version!(version_constraint);
1059 sql.query = subst(&sql.query, &state.cmd_vars)?;
1060 if let SqlOutput::Full { expected_rows, .. } = &mut sql.expected_output {
1061 for row in expected_rows {
1062 for col in row {
1063 *col = subst(col, &state.cmd_vars)?;
1064 }
1065 }
1066 }
1067 sql::run_sql(sql, state).await
1068 }
1069 Command::FailSql(mut sql, version_constraint) => {
1070 handle_version!(version_constraint);
1071 sql.query = subst(&sql.query, &state.cmd_vars)?;
1072 sql.expected_error = match &sql.expected_error {
1073 SqlExpectedError::Contains(s) => {
1074 SqlExpectedError::Contains(subst(s, &state.cmd_vars)?)
1075 }
1076 SqlExpectedError::Exact(s) => {
1077 SqlExpectedError::Exact(subst(s, &state.cmd_vars)?)
1078 }
1079 SqlExpectedError::Regex(s) => {
1080 SqlExpectedError::Regex(subst_re(s, &state.cmd_vars)?)
1081 }
1082 SqlExpectedError::Timeout => SqlExpectedError::Timeout,
1083 };
1084 sql.expected_detail = match sql.expected_detail {
1085 Some(s) => Some(subst(&s, &state.cmd_vars)?),
1086 None => None,
1087 };
1088 sql.expected_hint = match sql.expected_hint {
1089 Some(s) => Some(subst(&s, &state.cmd_vars)?),
1090 None => None,
1091 };
1092 sql::run_fail_sql(sql, state).await
1093 }
1094 };
1095
1096 r.map_err(wrap_err)
1097 }
1098}
1099
1100fn substitute_vars(
1102 msg: &str,
1103 vars: &BTreeMap<String, String>,
1104 ignore_prefix: &Option<String>,
1105 regex_escape: bool,
1106) -> Result<String, anyhow::Error> {
1107 static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\{([^}]+)\}").unwrap());
1108 let mut err = None;
1109 let out = RE.replace_all(msg, |caps: &Captures| {
1110 let name = &caps[1];
1111 if let Some(ignore_prefix) = &ignore_prefix {
1112 if name.starts_with(format!("{}.", ignore_prefix).as_str()) {
1113 return caps.get(0).unwrap().as_str().to_string();
1115 }
1116 }
1117
1118 if let Some(val) = vars.get(name) {
1119 if regex_escape {
1120 regex::escape(val)
1121 } else {
1122 val.to_string()
1123 }
1124 } else {
1125 err = Some(anyhow!("unknown variable: {}", name));
1126 "#VAR-MISSING#".to_string()
1127 }
1128 });
1129 match err {
1130 Some(err) => Err(err),
1131 None => Ok(out.into_owned()),
1132 }
1133}
1134
1135pub async fn create_state(
1143 config: &Config,
1144) -> Result<(State, impl Future<Output = Result<(), anyhow::Error>>), anyhow::Error> {
1145 let seed = config
1146 .seed
1147 .clone()
1148 .unwrap_or_else(|| format!("{:010}", rand::random::<u32>()));
1149
1150 let (_tempfile, temp_path) = match &config.temp_dir {
1151 Some(temp_dir) => {
1152 fs::create_dir_all(temp_dir).context("creating temporary directory")?;
1153 (None, PathBuf::from(&temp_dir))
1154 }
1155 _ => {
1156 let tempfile_handle = tempfile::tempdir().context("creating temporary directory")?;
1159 let temp_path = tempfile_handle.path().to_path_buf();
1160 (Some(tempfile_handle), temp_path)
1161 }
1162 };
1163
1164 let materialize_catalog_config = config.materialize_catalog_config.clone();
1165
1166 let materialize_url = util::postgres::config_url(&config.materialize_pgconfig)?;
1167 info!("Connecting to {}", materialize_url.as_str());
1168 let (pgclient, pgconn) = Retry::default()
1169 .max_duration(config.default_timeout)
1170 .retry_async_canceling(|_| async move {
1171 let mut pgconfig = config.materialize_pgconfig.clone();
1172 pgconfig.connect_timeout(config.default_timeout);
1173 let tls = make_tls(&pgconfig)?;
1174 pgconfig.connect(tls).await.map_err(|e| anyhow!(e))
1175 })
1176 .await?;
1177
1178 let pgconn_task =
1179 task::spawn(|| "pgconn_task", pgconn).map(|join| join.context("running SQL connection"));
1180
1181 let materialize_state =
1182 create_materialize_state(&config, materialize_catalog_config, pgclient).await?;
1183
1184 let schema_registry_url = config.schema_registry_url.to_owned();
1185
1186 let ccsr_client = {
1187 let mut ccsr_config = mz_ccsr::ClientConfig::new(schema_registry_url.clone());
1188
1189 if let Some(cert_path) = &config.cert_path {
1190 let keystore = Zeroizing::new(fs::read(cert_path).context("reading cert")?);
1191 let pass = config.cert_password.as_deref().unwrap_or("");
1192 let ident = identity_from_pkcs12_der(&keystore, pass)
1193 .context("reading keystore file as pkcs12")?;
1194 ccsr_config = ccsr_config.identity(ident);
1195 }
1196
1197 if let Some(ccsr_username) = &config.ccsr_username {
1198 ccsr_config = ccsr_config.auth(ccsr_username.clone(), config.ccsr_password.clone());
1199 }
1200
1201 ccsr_config.build().context("Creating CCSR client")?
1202 };
1203
1204 let (kafka_addr, kafka_admin, kafka_admin_opts, kafka_producer, kafka_topics, kafka_config) = {
1205 use rdkafka::admin::{AdminClient, AdminOptions};
1206 use rdkafka::producer::FutureProducer;
1207
1208 let mut kafka_config = create_new_client_config_simple();
1209 kafka_config.set("bootstrap.servers", &config.kafka_addr);
1210 kafka_config.set("group.id", "materialize-testdrive");
1211 kafka_config.set("auto.offset.reset", "earliest");
1212 kafka_config.set("isolation.level", "read_committed");
1213 if let Some(cert_path) = &config.cert_path {
1214 kafka_config.set("security.protocol", "ssl");
1215 kafka_config.set("ssl.keystore.location", cert_path);
1216 if let Some(cert_password) = &config.cert_password {
1217 kafka_config.set("ssl.keystore.password", cert_password);
1218 }
1219 }
1220 kafka_config.set("message.max.bytes", "15728640");
1221
1222 for (key, value) in &config.kafka_opts {
1223 kafka_config.set(key, value);
1224 }
1225
1226 let admin: AdminClient<_> = create_with_context(&kafka_config, MzClientContext::default())
1227 .with_context(|| format!("opening Kafka connection: {}", config.kafka_addr))?;
1228
1229 let admin_opts = AdminOptions::new().operation_timeout(Some(config.default_timeout));
1230
1231 let producer: FutureProducer<_> =
1232 create_with_context(&kafka_config, MzClientContext::default()).with_context(|| {
1233 format!("opening Kafka producer connection: {}", config.kafka_addr)
1234 })?;
1235
1236 let topics = BTreeMap::new();
1237
1238 (
1239 config.kafka_addr.to_owned(),
1240 admin,
1241 admin_opts,
1242 producer,
1243 topics,
1244 kafka_config,
1245 )
1246 };
1247
1248 let mut state = State {
1249 config: config.clone(),
1250
1251 arg_vars: config.arg_vars.clone(),
1253 cmd_vars: BTreeMap::new(),
1254 seed,
1255 temp_path,
1256 _tempfile,
1257 default_timeout: config.default_timeout,
1258 timeout: config.default_timeout,
1259 max_tries: config.default_max_tries,
1260 initial_backoff: config.initial_backoff,
1261 backoff_factor: config.backoff_factor,
1262 consistency_checks: config.consistency_checks,
1263 consistency_check_timeout: config.consistency_check_timeout,
1264 check_statement_logging: config.check_statement_logging,
1265 consistency_checks_adhoc_skip: false,
1266 regex: None,
1267 regex_replacement: set::DEFAULT_REGEX_REPLACEMENT.into(),
1268 rewrite_results: config.rewrite_results,
1269 error_line_count: 0,
1270 error_string: "".to_string(),
1271
1272 materialize: materialize_state,
1274
1275 persist_consensus_url: config.persist_consensus_url.clone(),
1277 persist_blob_url: config.persist_blob_url.clone(),
1278 build_info: config.build_info,
1279 persist_clients: PersistClientCache::new(
1280 PersistConfig::new_default_configs(config.build_info, SYSTEM_TIME.clone()),
1281 &MetricsRegistry::new(),
1282 |_, _| PubSubClientConnection::noop(),
1283 ),
1284
1285 schema_registry_url,
1287 ccsr_client,
1288 kafka_addr,
1289 kafka_admin,
1290 kafka_admin_opts,
1291 kafka_config,
1292 kafka_default_partitions: config.kafka_default_partitions,
1293 kafka_producer,
1294 kafka_topics,
1295 kafka_verify_topics: BTreeSet::new(),
1296
1297 aws_account: config.aws_account.clone(),
1299 aws_config: config.aws_config.clone(),
1300
1301 duckdb_clients: BTreeMap::new(),
1303 mysql_clients: BTreeMap::new(),
1304 postgres_clients: BTreeMap::new(),
1305 sql_server_clients: BTreeMap::new(),
1306 background_tasks: Vec::new(),
1307
1308 fivetran_destination_url: config.fivetran_destination_url.clone(),
1310 fivetran_destination_files_path: config.fivetran_destination_files_path.clone(),
1311
1312 rewrites: Vec::new(),
1313 rewrite_pos_start: 0,
1314 rewrite_pos_end: 0,
1315 };
1316 state.initialize_cmd_vars().await?;
1317 Ok((state, pgconn_task))
1318}
1319
1320fn identity_from_pkcs12_der(der: &[u8], pass: &str) -> anyhow::Result<mz_ccsr::tls::Identity> {
1325 let parsed = Pkcs12::from_der(der)?.parse2(pass)?;
1326 let key = Zeroizing::new(
1327 parsed
1328 .pkey
1329 .context("keystore has no private key")?
1330 .private_key_to_pem_pkcs8()?,
1331 );
1332 let mut chain = parsed
1333 .cert
1334 .context("keystore has no certificate")?
1335 .to_pem()?;
1336 for ca in parsed.ca.into_iter().flatten() {
1337 chain.extend(ca.to_pem()?);
1338 }
1339 Ok(mz_ccsr::tls::Identity::from_pem(&key, &chain)?)
1340}
1341
1342async fn create_materialize_state(
1343 config: &&Config,
1344 materialize_catalog_config: Option<CatalogConfig>,
1345 pgclient: tokio_postgres::Client,
1346) -> Result<MaterializeState, anyhow::Error> {
1347 let materialize_url = util::postgres::config_url(&config.materialize_pgconfig)?;
1348 let materialize_internal_url =
1349 util::postgres::config_url(&config.materialize_internal_pgconfig)?;
1350
1351 for (key, value) in &config.materialize_params {
1352 #[allow(clippy::disallowed_methods)]
1354 pgclient
1355 .batch_execute(&format!("SET {key} = {value}"))
1356 .await
1357 .context("setting session parameter")?;
1358 }
1359
1360 let materialize_user = config
1361 .materialize_pgconfig
1362 .get_user()
1363 .expect("testdrive URL must contain user")
1364 .to_string();
1365
1366 let materialize_sql_addr = format!(
1367 "{}:{}",
1368 materialize_url.host_str().unwrap(),
1369 materialize_url.port().unwrap()
1370 );
1371 let materialize_http_addr = format!(
1372 "{}:{}",
1373 materialize_url.host_str().unwrap(),
1374 config.materialize_http_port
1375 );
1376 let materialize_internal_sql_addr = format!(
1377 "{}:{}",
1378 materialize_internal_url.host_str().unwrap(),
1379 materialize_internal_url.port().unwrap()
1380 );
1381 let materialize_password_sql_addr = format!(
1382 "{}:{}",
1383 materialize_url.host_str().unwrap(),
1384 config.materialize_password_sql_port
1385 );
1386 let materialize_sasl_sql_addr = format!(
1387 "{}:{}",
1388 materialize_url.host_str().unwrap(),
1389 config.materialize_sasl_sql_port
1390 );
1391 let materialize_internal_http_addr = format!(
1392 "{}:{}",
1393 materialize_internal_url.host_str().unwrap(),
1394 config.materialize_internal_http_port
1395 );
1396 let environment_id = pg_query_one(&pgclient, pg_sql!("SELECT mz_environment_id()"), &[])
1397 .await?
1398 .get::<_, String>(0)
1399 .parse()
1400 .context("parsing environment ID")?;
1401
1402 let (aws_account_id, aws_external_id_prefix, aws_connection_role_arn) = match pg_query_one(
1406 &pgclient,
1407 pg_sql!(
1408 "SELECT mz_aws_account_id(), mz_aws_external_id_prefix(), \
1409 mz_aws_connection_role_arn()"
1410 ),
1411 &[],
1412 )
1413 .await
1414 {
1415 Ok(row) => (
1416 row.get::<_, Option<String>>(0),
1417 row.get::<_, Option<String>>(1),
1418 row.get::<_, Option<String>>(2),
1419 ),
1420 Err(_) => (None, None, None),
1421 };
1422
1423 let bootstrap_args = BootstrapArgs {
1424 cluster_replica_size_map: config.materialize_cluster_replica_sizes.clone(),
1425 default_cluster_replica_size: "ABC".to_string(),
1426 default_cluster_replication_factor: 1,
1427 bootstrap_role: None,
1428 };
1429
1430 let materialize_state = MaterializeState {
1431 catalog_config: materialize_catalog_config,
1432 sql_addr: materialize_sql_addr,
1433 use_https: config.materialize_use_https,
1434 http_addr: materialize_http_addr,
1435 internal_sql_addr: materialize_internal_sql_addr,
1436 internal_http_addr: materialize_internal_http_addr,
1437 password_sql_addr: materialize_password_sql_addr,
1438 sasl_sql_addr: materialize_sasl_sql_addr,
1439 user: materialize_user,
1440 pgclient,
1441 environment_id,
1442 bootstrap_args,
1443 aws_account_id,
1444 aws_external_id_prefix,
1445 aws_connection_role_arn,
1446 };
1447
1448 Ok(materialize_state)
1449}