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