1use std::collections::BTreeMap;
11use std::error::Error;
12use std::future::IntoFuture;
13use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream};
14use std::path::{Path, PathBuf};
15use std::pin::Pin;
16use std::str::FromStr;
17use std::sync::Arc;
18use std::sync::LazyLock;
19use std::time::Duration;
20use std::{env, fs, iter};
21
22use anyhow::anyhow;
23use futures::Future;
24use futures::future::{BoxFuture, LocalBoxFuture};
25use headers::{Header, HeaderMapExt};
26use http::Uri;
27use hyper::http::header::HeaderMap;
28use maplit::btreemap;
29use mz_adapter::TimestampExplanation;
30use mz_adapter_types::bootstrap_builtin_cluster_config::{
31 ANALYTICS_CLUSTER_DEFAULT_REPLICATION_FACTOR, BootstrapBuiltinClusterConfig,
32 CATALOG_SERVER_CLUSTER_DEFAULT_REPLICATION_FACTOR, PROBE_CLUSTER_DEFAULT_REPLICATION_FACTOR,
33 SUPPORT_CLUSTER_DEFAULT_REPLICATION_FACTOR, SYSTEM_CLUSTER_DEFAULT_REPLICATION_FACTOR,
34};
35
36use mz_auth::password::Password;
37use mz_catalog::config::ClusterReplicaSizeMap;
38use mz_controller::ControllerConfig;
39use mz_dyncfg::ConfigUpdates;
40use mz_license_keys::ValidatedLicenseKey;
41use mz_orchestrator_process::{ProcessOrchestrator, ProcessOrchestratorConfig};
42use mz_orchestrator_tracing::{TracingCliArgs, TracingOrchestrator};
43use mz_ore::cast::CastLossy;
44use mz_ore::metrics::MetricsRegistry;
45use mz_ore::now::{EpochMillis, NowFn, SYSTEM_TIME};
46use mz_ore::retry::Retry;
47use mz_ore::task;
48use mz_ore::tracing::{
49 OpenTelemetryConfig, StderrLogConfig, StderrLogFormat, TracingConfig, TracingHandle,
50};
51use mz_persist_client::PersistLocation;
52use mz_persist_client::cache::PersistClientCache;
53use mz_persist_client::cfg::{CONSENSUS_CONNECTION_POOL_MAX_SIZE, PersistConfig};
54use mz_persist_client::rpc::PersistGrpcPubSubServer;
55use mz_postgres_util::{
56 Sql, batch_execute as pg_batch_execute, execute as pg_execute, query_one as pg_query_one, sql,
57};
58use mz_secrets::SecretsController;
59use mz_server_core::listeners::v26_32_0::ListenersConfig;
60use mz_server_core::listeners::{
61 AllowedRoles, AuthenticatorKind, HttpListenerConfig, HttpRoutesEnabled, RouteGroup,
62};
63use mz_server_core::{ReloadTrigger, TlsCertConfig};
64use mz_sql::catalog::EnvironmentId;
65use mz_storage_types::connections::ConnectionContext;
66use mz_tracing::CloneableEnvFilter;
67use openssl::asn1::Asn1Time;
68use openssl::error::ErrorStack;
69use openssl::hash::MessageDigest;
70use openssl::nid::Nid;
71use openssl::pkey::{PKey, Private};
72use openssl::rsa::Rsa;
73use openssl::ssl::{SslConnector, SslConnectorBuilder, SslMethod, SslOptions};
74use openssl::x509::extension::{BasicConstraints, SubjectAlternativeName};
75use openssl::x509::{X509, X509Name, X509NameBuilder};
76use postgres::error::DbError;
77use postgres::tls::{MakeTlsConnect, TlsConnect};
78use postgres::types::{FromSql, Type};
79use postgres::{NoTls, Socket};
80use postgres_openssl::MakeTlsConnector;
81use tempfile::TempDir;
82use tokio::net::TcpListener;
83use tokio::runtime::Runtime;
84use tokio_postgres::config::{Host, SslMode};
85use tokio_postgres::{AsyncMessage, Client};
86use tokio_stream::wrappers::TcpListenerStream;
87use tower_http::cors::AllowOrigin;
88use tracing::Level;
89use tracing_capture::SharedStorage;
90use tracing_subscriber::EnvFilter;
91use tungstenite::stream::MaybeTlsStream;
92use tungstenite::{Message, WebSocket};
93
94use crate::{
95 CatalogConfig, FronteggAuthenticator, SqlListenerConfig, WebSocketAuth, WebSocketResponse,
96};
97
98pub static KAFKA_ADDRS: LazyLock<String> =
99 LazyLock::new(|| env::var("KAFKA_ADDRS").unwrap_or_else(|_| "localhost:9092".into()));
100
101#[derive(Clone)]
103pub struct TestHarness {
104 data_directory: Option<PathBuf>,
105 tls: Option<TlsCertConfig>,
106 frontegg: Option<FronteggAuthenticator>,
107 external_login_password_mz_system: Option<Password>,
108 listeners_config: ListenersConfig,
109 unsafe_mode: bool,
110 aws_connection_context: bool,
116 workers: usize,
117 now: NowFn,
118 seed: u32,
119 storage_usage_collection_interval: Duration,
120 storage_usage_retention_period: Option<Duration>,
121 default_cluster_replica_size: String,
122 default_cluster_replication_factor: u32,
123 builtin_system_cluster_config: BootstrapBuiltinClusterConfig,
124 builtin_catalog_server_cluster_config: BootstrapBuiltinClusterConfig,
125 builtin_probe_cluster_config: BootstrapBuiltinClusterConfig,
126 builtin_support_cluster_config: BootstrapBuiltinClusterConfig,
127 builtin_analytics_cluster_config: BootstrapBuiltinClusterConfig,
128
129 propagate_crashes: bool,
130 enable_tracing: bool,
131 orchestrator_tracing_cli_args: TracingCliArgs,
134 bootstrap_role: Option<String>,
135 deploy_generation: u64,
136 system_parameter_defaults: BTreeMap<String, String>,
137 internal_console_redirect_url: Option<String>,
138 metrics_registry: Option<MetricsRegistry>,
139 code_version: semver::Version,
140 capture: Option<SharedStorage>,
141 pub environment_id: EnvironmentId,
142}
143
144impl Default for TestHarness {
145 fn default() -> TestHarness {
146 TestHarness {
147 data_directory: None,
148 tls: None,
149 frontegg: None,
150 external_login_password_mz_system: None,
151 listeners_config: ListenersConfig {
152 sql: btreemap![
153 "external".to_owned() => SqlListenerConfig {
154 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
155 authenticator_kind: AuthenticatorKind::None,
156 allowed_roles: AllowedRoles::Normal,
157 enable_tls: false,
158 },
159 "internal".to_owned() => SqlListenerConfig {
160 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
161 authenticator_kind: AuthenticatorKind::None,
162 allowed_roles: AllowedRoles::NormalAndInternal,
163 enable_tls: false,
164 },
165 ],
166 http: btreemap![
167 "external".to_owned() => HttpListenerConfig {
168 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
169 authenticator_kind: AuthenticatorKind::None,
170 enable_tls: false,
171 routes: HttpRoutesEnabled {
172 base: RouteGroup::Enabled(AllowedRoles::Normal),
173 webhook: RouteGroup::Enabled(AllowedRoles::Normal),
174 internal: RouteGroup::Disabled,
175 metrics: RouteGroup::Disabled,
176 profiling: RouteGroup::Disabled,
177 mcp_agent: RouteGroup::Disabled,
178 mcp_developer: RouteGroup::Disabled,
179 console_config: RouteGroup::Enabled(AllowedRoles::Normal),
180 },
181 },
182 "internal".to_owned() => HttpListenerConfig {
183 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
184 authenticator_kind: AuthenticatorKind::None,
185 enable_tls: false,
186 routes: HttpRoutesEnabled {
187 base: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
188 webhook: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
189 internal: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
190 metrics: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
191 profiling: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
192 mcp_agent: RouteGroup::Disabled,
193 mcp_developer: RouteGroup::Disabled,
194 console_config: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
195 },
196 },
197 ],
198 },
199 unsafe_mode: false,
200 aws_connection_context: true,
201 workers: 1,
202 now: SYSTEM_TIME.clone(),
203 seed: rand::random(),
204 storage_usage_collection_interval: Duration::from_secs(3600),
205 storage_usage_retention_period: None,
206 default_cluster_replica_size: "scale=1,workers=1".to_string(),
207 default_cluster_replication_factor: 1,
208 builtin_system_cluster_config: BootstrapBuiltinClusterConfig {
209 size: "scale=1,workers=1".to_string(),
210 replication_factor: SYSTEM_CLUSTER_DEFAULT_REPLICATION_FACTOR,
211 },
212 builtin_catalog_server_cluster_config: BootstrapBuiltinClusterConfig {
213 size: "scale=1,workers=1".to_string(),
214 replication_factor: CATALOG_SERVER_CLUSTER_DEFAULT_REPLICATION_FACTOR,
215 },
216 builtin_probe_cluster_config: BootstrapBuiltinClusterConfig {
217 size: "scale=1,workers=1".to_string(),
218 replication_factor: PROBE_CLUSTER_DEFAULT_REPLICATION_FACTOR,
219 },
220 builtin_support_cluster_config: BootstrapBuiltinClusterConfig {
221 size: "scale=1,workers=1".to_string(),
222 replication_factor: SUPPORT_CLUSTER_DEFAULT_REPLICATION_FACTOR,
223 },
224 builtin_analytics_cluster_config: BootstrapBuiltinClusterConfig {
225 size: "scale=1,workers=1".to_string(),
226 replication_factor: ANALYTICS_CLUSTER_DEFAULT_REPLICATION_FACTOR,
227 },
228 propagate_crashes: false,
229 enable_tracing: false,
230 bootstrap_role: Some("materialize".into()),
231 deploy_generation: 0,
232 system_parameter_defaults: BTreeMap::from([(
235 "log_filter".to_string(),
236 "error".to_string(),
237 )]),
238 internal_console_redirect_url: None,
239 metrics_registry: None,
240 orchestrator_tracing_cli_args: TracingCliArgs {
241 startup_log_filter: CloneableEnvFilter::from_str("error").expect("must parse"),
242 ..Default::default()
243 },
244 code_version: crate::BUILD_INFO.semver_version(),
245 environment_id: EnvironmentId::for_tests(),
246 capture: None,
247 }
248 }
249}
250
251impl TestHarness {
252 pub async fn start(self) -> TestServer {
256 self.try_start().await.expect("Failed to start test Server")
257 }
258
259 pub async fn start_with_trigger(self, tls_reload_certs: ReloadTrigger) -> TestServer {
261 self.try_start_with_trigger(tls_reload_certs)
262 .await
263 .expect("Failed to start test Server")
264 }
265
266 pub async fn try_start(self) -> Result<TestServer, anyhow::Error> {
268 self.try_start_with_trigger(mz_server_core::cert_reload_never_reload())
269 .await
270 }
271
272 pub async fn try_start_with_trigger(
274 self,
275 tls_reload_certs: ReloadTrigger,
276 ) -> Result<TestServer, anyhow::Error> {
277 let listeners = Listeners::new(&self).await?;
278 listeners.serve_with_trigger(self, tls_reload_certs).await
279 }
280
281 pub fn start_blocking(self) -> TestServerWithRuntime {
283 let runtime = tokio::runtime::Builder::new_multi_thread()
284 .enable_all()
285 .thread_stack_size(mz_ore::stack::STACK_SIZE)
286 .build()
287 .expect("failed to spawn runtime for test");
288 let runtime = Arc::new(runtime);
289 let server = runtime.block_on(self.start());
290 TestServerWithRuntime { runtime, server }
291 }
292
293 pub fn data_directory(mut self, data_directory: impl Into<PathBuf>) -> Self {
294 self.data_directory = Some(data_directory.into());
295 self
296 }
297
298 pub fn with_tls(mut self, cert_path: impl Into<PathBuf>, key_path: impl Into<PathBuf>) -> Self {
299 self.tls = Some(TlsCertConfig {
300 cert: cert_path.into(),
301 key: key_path.into(),
302 });
303 for (_, listener) in &mut self.listeners_config.sql {
304 listener.enable_tls = true;
305 }
306 for (_, listener) in &mut self.listeners_config.http {
307 listener.enable_tls = true;
308 }
309 self
310 }
311
312 pub fn unsafe_mode(mut self) -> Self {
313 self.unsafe_mode = true;
314 self
315 }
316
317 pub fn without_aws_connection_context(mut self) -> Self {
321 self.aws_connection_context = false;
322 self
323 }
324
325 pub fn workers(mut self, workers: usize) -> Self {
326 self.workers = workers;
327 self
328 }
329
330 pub fn with_frontegg_auth(mut self, frontegg: &FronteggAuthenticator) -> Self {
331 self.frontegg = Some(frontegg.clone());
332 let enable_tls = self.tls.is_some();
333 self.listeners_config = ListenersConfig {
334 sql: btreemap! {
335 "external".to_owned() => SqlListenerConfig {
336 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
337 authenticator_kind: AuthenticatorKind::Frontegg,
338 allowed_roles: AllowedRoles::Normal,
339 enable_tls,
340 },
341 "internal".to_owned() => SqlListenerConfig {
342 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
343 authenticator_kind: AuthenticatorKind::None,
344 allowed_roles: AllowedRoles::NormalAndInternal,
345 enable_tls: false,
346 },
347 },
348 http: btreemap! {
349 "external".to_owned() => HttpListenerConfig {
350 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
351 authenticator_kind: AuthenticatorKind::Frontegg,
352 enable_tls,
353 routes: HttpRoutesEnabled {
354 base: RouteGroup::Enabled(AllowedRoles::Normal),
355 webhook: RouteGroup::Enabled(AllowedRoles::Normal),
356 internal: RouteGroup::Disabled,
357 metrics: RouteGroup::Disabled,
358 profiling: RouteGroup::Disabled,
359 mcp_agent: RouteGroup::Disabled,
360 mcp_developer: RouteGroup::Disabled,
361 console_config: RouteGroup::Enabled(AllowedRoles::Normal),
362 },
363 },
364 "internal".to_owned() => HttpListenerConfig {
365 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
366 authenticator_kind: AuthenticatorKind::None,
367 enable_tls: false,
368 routes: HttpRoutesEnabled {
369 base: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
370 webhook: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
371 internal: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
372 metrics: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
373 profiling: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
374 mcp_agent: RouteGroup::Disabled,
375 mcp_developer: RouteGroup::Disabled,
376 console_config: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
377 },
378 },
379 },
380 };
381 self
382 }
383
384 pub fn with_oidc_auth(
385 mut self,
386 issuer: Option<String>,
387 authentication_claim: Option<String>,
388 expected_audiences: Option<Vec<String>>,
389 external_login_password_mz_system: Option<Password>,
390 ) -> Self {
391 let enable_tls = self.tls.is_some();
392 self.listeners_config = ListenersConfig {
393 sql: btreemap! {
394 "external".to_owned() => SqlListenerConfig {
395 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
396 authenticator_kind: AuthenticatorKind::Oidc,
397 allowed_roles: AllowedRoles::NormalAndInternal,
398 enable_tls,
399 },
400 "internal".to_owned() => SqlListenerConfig {
401 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
402 authenticator_kind: AuthenticatorKind::None,
403 allowed_roles: AllowedRoles::NormalAndInternal,
404 enable_tls: false,
405 },
406 },
407 http: btreemap! {
408 "external".to_owned() => HttpListenerConfig {
409 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
410 authenticator_kind: AuthenticatorKind::Oidc,
411 enable_tls,
412 routes: HttpRoutesEnabled {
413 base: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
414 webhook: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
415 internal: RouteGroup::Disabled,
416 metrics: RouteGroup::Disabled,
417 profiling: RouteGroup::Disabled,
418 mcp_agent: RouteGroup::Disabled,
419 mcp_developer: RouteGroup::Disabled,
420 console_config: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
421 },
422 },
423 "internal".to_owned() => HttpListenerConfig {
424 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
425 authenticator_kind: AuthenticatorKind::None,
426 enable_tls: false,
427 routes: HttpRoutesEnabled {
428 base: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
429 webhook: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
430 internal: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
431 metrics: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
432 profiling: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
433 mcp_agent: RouteGroup::Disabled,
434 mcp_developer: RouteGroup::Disabled,
435 console_config: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
436 },
437 },
438 },
439 };
440
441 if let Some(issuer) = issuer {
442 self.system_parameter_defaults
443 .insert("oidc_issuer".to_string(), issuer);
444 }
445
446 if let Some(authentication_claim) = authentication_claim {
447 self.system_parameter_defaults.insert(
448 "oidc_authentication_claim".to_string(),
449 authentication_claim,
450 );
451 }
452
453 if let Some(expected_audiences) = expected_audiences {
454 self.system_parameter_defaults.insert(
455 "oidc_audience".to_string(),
456 serde_json::to_string(&expected_audiences).unwrap(),
457 );
458 }
459
460 if let Some(external_login_password_mz_system) = external_login_password_mz_system {
461 self.external_login_password_mz_system = Some(external_login_password_mz_system);
462 self.system_parameter_defaults
463 .insert("enable_password_auth".to_string(), "true".to_string());
464 }
465
466 self
467 }
468
469 pub fn with_password_auth(mut self, mz_system_password: Password) -> Self {
470 self.external_login_password_mz_system = Some(mz_system_password);
471 let enable_tls = self.tls.is_some();
472 self.listeners_config = ListenersConfig {
473 sql: btreemap! {
474 "external".to_owned() => SqlListenerConfig {
475 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
476 authenticator_kind: AuthenticatorKind::Password,
477 allowed_roles: AllowedRoles::NormalAndInternal,
478 enable_tls,
479 },
480 },
481 http: btreemap! {
482 "external".to_owned() => HttpListenerConfig {
483 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
484 authenticator_kind: AuthenticatorKind::Password,
485 enable_tls,
486 routes: HttpRoutesEnabled {
487 base: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
488 webhook: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
489 internal: RouteGroup::Enabled(AllowedRoles::Internal),
490 metrics: RouteGroup::Disabled,
491 profiling: RouteGroup::Enabled(AllowedRoles::Internal),
492 mcp_agent: RouteGroup::Disabled,
493 mcp_developer: RouteGroup::Disabled,
494 console_config: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
495 },
496 },
497 "metrics".to_owned() => HttpListenerConfig {
498 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
499 authenticator_kind: AuthenticatorKind::None,
500 enable_tls: false,
501 routes: HttpRoutesEnabled {
502 base: RouteGroup::Disabled,
503 webhook: RouteGroup::Disabled,
504 internal: RouteGroup::Disabled,
505 metrics: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
506 profiling: RouteGroup::Disabled,
507 mcp_agent: RouteGroup::Disabled,
508 mcp_developer: RouteGroup::Disabled,
509 console_config: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
510 },
511 },
512 },
513 };
514 self
515 }
516
517 pub fn with_sasl_scram_auth(mut self, mz_system_password: Password) -> Self {
518 self.external_login_password_mz_system = Some(mz_system_password);
519 let enable_tls = self.tls.is_some();
520 self.listeners_config = ListenersConfig {
521 sql: btreemap! {
522 "external".to_owned() => SqlListenerConfig {
523 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
524 authenticator_kind: AuthenticatorKind::Sasl,
525 allowed_roles: AllowedRoles::NormalAndInternal,
526 enable_tls,
527 },
528 },
529 http: btreemap! {
530 "external".to_owned() => HttpListenerConfig {
531 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
532 authenticator_kind: AuthenticatorKind::Password,
533 enable_tls,
534 routes: HttpRoutesEnabled {
535 base: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
536 webhook: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
537 internal: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
538 metrics: RouteGroup::Disabled,
539 profiling: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
540 mcp_agent: RouteGroup::Disabled,
541 mcp_developer: RouteGroup::Disabled,
542 console_config: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
543 },
544 },
545 "metrics".to_owned() => HttpListenerConfig {
546 addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
547 authenticator_kind: AuthenticatorKind::None,
548 enable_tls: false,
549 routes: HttpRoutesEnabled {
550 base: RouteGroup::Disabled,
551 webhook: RouteGroup::Disabled,
552 internal: RouteGroup::Disabled,
553 metrics: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
554 profiling: RouteGroup::Disabled,
555 mcp_agent: RouteGroup::Disabled,
556 mcp_developer: RouteGroup::Disabled,
557 console_config: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
558 },
559 },
560 },
561 };
562 self
563 }
564
565 pub fn with_now(mut self, now: NowFn) -> Self {
566 self.now = now;
567 self
568 }
569
570 pub fn with_storage_usage_collection_interval(
571 mut self,
572 storage_usage_collection_interval: Duration,
573 ) -> Self {
574 self.storage_usage_collection_interval = storage_usage_collection_interval;
575 self
576 }
577
578 pub fn with_storage_usage_retention_period(
579 mut self,
580 storage_usage_retention_period: Duration,
581 ) -> Self {
582 self.storage_usage_retention_period = Some(storage_usage_retention_period);
583 self
584 }
585
586 pub fn with_default_cluster_replica_size(
587 mut self,
588 default_cluster_replica_size: String,
589 ) -> Self {
590 self.default_cluster_replica_size = default_cluster_replica_size;
591 self
592 }
593
594 pub fn with_builtin_system_cluster_replica_size(
595 mut self,
596 builtin_system_cluster_replica_size: String,
597 ) -> Self {
598 self.builtin_system_cluster_config.size = builtin_system_cluster_replica_size;
599 self
600 }
601
602 pub fn with_builtin_system_cluster_replication_factor(
603 mut self,
604 builtin_system_cluster_replication_factor: u32,
605 ) -> Self {
606 self.builtin_system_cluster_config.replication_factor =
607 builtin_system_cluster_replication_factor;
608 self
609 }
610
611 pub fn with_builtin_support_cluster_replication_factor(
612 mut self,
613 builtin_support_cluster_replication_factor: u32,
614 ) -> Self {
615 self.builtin_support_cluster_config.replication_factor =
616 builtin_support_cluster_replication_factor;
617 self
618 }
619
620 pub fn with_builtin_catalog_server_cluster_replica_size(
621 mut self,
622 builtin_catalog_server_cluster_replica_size: String,
623 ) -> Self {
624 self.builtin_catalog_server_cluster_config.size =
625 builtin_catalog_server_cluster_replica_size;
626 self
627 }
628
629 pub fn with_propagate_crashes(mut self, propagate_crashes: bool) -> Self {
630 self.propagate_crashes = propagate_crashes;
631 self
632 }
633
634 pub fn with_enable_tracing(mut self, enable_tracing: bool) -> Self {
635 self.enable_tracing = enable_tracing;
636 self
637 }
638
639 pub fn with_bootstrap_role(mut self, bootstrap_role: Option<String>) -> Self {
640 self.bootstrap_role = bootstrap_role;
641 self
642 }
643
644 pub fn with_deploy_generation(mut self, deploy_generation: u64) -> Self {
645 self.deploy_generation = deploy_generation;
646 self
647 }
648
649 pub fn with_system_parameter_default(mut self, param: String, value: String) -> Self {
650 self.system_parameter_defaults.insert(param, value);
651 self
652 }
653
654 pub fn with_mcp_routes(mut self, agent: bool, developer: bool) -> Self {
655 for config in self.listeners_config.http.values_mut() {
656 let roles = config
660 .routes
661 .base
662 .allowed_roles()
663 .unwrap_or(AllowedRoles::NormalAndInternal);
664 let group = |enabled| {
665 if enabled {
666 RouteGroup::Enabled(roles)
667 } else {
668 RouteGroup::Disabled
669 }
670 };
671 config.routes.mcp_agent = group(agent);
672 config.routes.mcp_developer = group(developer);
673 }
674 self
675 }
676
677 pub fn with_internal_console_redirect_url(
678 mut self,
679 internal_console_redirect_url: Option<String>,
680 ) -> Self {
681 self.internal_console_redirect_url = internal_console_redirect_url;
682 self
683 }
684
685 pub fn with_metrics_registry(mut self, registry: MetricsRegistry) -> Self {
686 self.metrics_registry = Some(registry);
687 self
688 }
689
690 pub fn with_code_version(mut self, version: semver::Version) -> Self {
691 self.code_version = version;
692 self
693 }
694
695 pub fn with_capture(mut self, storage: SharedStorage) -> Self {
696 self.capture = Some(storage);
697 self
698 }
699}
700
701pub struct Listeners {
702 pub inner: crate::Listeners,
703}
704
705impl Listeners {
706 pub async fn new(config: &TestHarness) -> Result<Listeners, anyhow::Error> {
707 let inner = crate::Listeners::bind(config.listeners_config.clone()).await?;
708 Ok(Listeners { inner })
709 }
710
711 pub async fn serve(self, config: TestHarness) -> Result<TestServer, anyhow::Error> {
712 self.serve_with_trigger(config, mz_server_core::cert_reload_never_reload())
713 .await
714 }
715
716 pub async fn serve_with_trigger(
717 self,
718 config: TestHarness,
719 tls_reload_certs: ReloadTrigger,
720 ) -> Result<TestServer, anyhow::Error> {
721 let (data_directory, temp_dir) = match config.data_directory {
722 None => {
723 let temp_dir = tempfile::tempdir()?;
728 (temp_dir.path().to_path_buf(), Some(temp_dir))
729 }
730 Some(data_directory) => (data_directory, None),
731 };
732 let scratch_dir = tempfile::tempdir()?;
733 let (consensus_uri, timestamp_oracle_url) = {
734 let seed = config.seed;
735 let cockroach_url = env::var("METADATA_BACKEND_URL")
736 .map_err(|_| anyhow!("METADATA_BACKEND_URL environment variable is not set"))?;
737 let (client, conn) = tokio_postgres::connect(&cockroach_url, NoTls).await?;
738 mz_ore::task::spawn(|| "startup-postgres-conn", async move {
739 if let Err(err) = conn.await {
740 panic!("connection error: {}", err);
741 };
742 });
743 let consensus_schema = sql!("consensus_{}", seed);
744 let tsoracle_schema = sql!("tsoracle_{}", seed);
745 pg_batch_execute(
746 &client,
747 sql!(
748 "CREATE SCHEMA IF NOT EXISTS {};
749 CREATE SCHEMA IF NOT EXISTS {};",
750 consensus_schema,
751 tsoracle_schema,
752 ),
753 )
754 .await?;
755 (
756 format!("{cockroach_url}?options=--search_path=consensus_{seed}")
757 .parse()
758 .expect("invalid consensus URI"),
759 format!("{cockroach_url}?options=--search_path=tsoracle_{seed}")
760 .parse()
761 .expect("invalid timestamp oracle URI"),
762 )
763 };
764 let metrics_registry = config.metrics_registry.unwrap_or_else(MetricsRegistry::new);
765 let orchestrator = ProcessOrchestrator::new(ProcessOrchestratorConfig {
766 image_dir: env::current_exe()?
767 .parent()
768 .unwrap()
769 .parent()
770 .unwrap()
771 .to_path_buf(),
772 suppress_output: false,
773 environment_id: config.environment_id.to_string(),
774 secrets_dir: data_directory.join("secrets"),
775 command_wrapper: vec![],
776 propagate_crashes: config.propagate_crashes,
777 tcp_proxy: None,
778 scratch_directory: scratch_dir.path().to_path_buf(),
779 })
780 .await?;
781 let orchestrator = Arc::new(orchestrator);
782 let persist_now = SYSTEM_TIME.clone();
785 let dyncfgs = mz_dyncfgs::all_dyncfgs();
786
787 let mut updates = ConfigUpdates::default();
788 updates.add(&CONSENSUS_CONNECTION_POOL_MAX_SIZE, 1);
791 updates.apply(&dyncfgs);
792
793 let mut persist_cfg = PersistConfig::new(&crate::BUILD_INFO, persist_now.clone(), dyncfgs);
794 persist_cfg.build_version = config.code_version;
795 persist_cfg.set_rollup_threshold(5);
797
798 let persist_pubsub_server = PersistGrpcPubSubServer::new(&persist_cfg, &metrics_registry);
799 let persist_pubsub_client = persist_pubsub_server.new_same_process_connection();
800 let persist_pubsub_tcp_listener =
801 TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0))
802 .await
803 .expect("pubsub addr binding");
804 let persist_pubsub_server_port = persist_pubsub_tcp_listener
805 .local_addr()
806 .expect("pubsub addr has local addr")
807 .port();
808
809 mz_ore::task::spawn(|| "persist_pubsub_server", async move {
811 persist_pubsub_server
812 .serve_with_stream(TcpListenerStream::new(persist_pubsub_tcp_listener))
813 .await
814 .expect("success")
815 });
816 let persist_clients =
817 PersistClientCache::new(persist_cfg, &metrics_registry, |_, _| persist_pubsub_client);
818 let persist_clients = Arc::new(persist_clients);
819 let system_dyncfgs = Arc::clone(&persist_clients.cfg().configs);
820
821 let secrets_controller = Arc::clone(&orchestrator);
822 let mut connection_context = ConnectionContext::for_tests(orchestrator.reader());
823 if !config.aws_connection_context {
824 connection_context.aws_external_id_prefix = None;
825 connection_context.aws_connection_role_arn = None;
826 }
827 let orchestrator = Arc::new(TracingOrchestrator::new(
828 orchestrator,
829 config.orchestrator_tracing_cli_args,
830 ));
831 let tracing_handle = if config.enable_tracing {
832 let config = TracingConfig::<fn(&tracing::Metadata) -> sentry_tracing::EventFilter> {
833 service_name: "environmentd",
834 stderr_log: StderrLogConfig {
835 format: StderrLogFormat::Json,
836 filter: EnvFilter::default(),
837 },
838 opentelemetry: Some(OpenTelemetryConfig {
839 endpoint: "http://fake_address_for_testing:8080".to_string(),
840 headers: http::HeaderMap::new(),
841 filter: EnvFilter::default().add_directive(Level::DEBUG.into()),
842 resource: opentelemetry_sdk::resource::Resource::builder().build(),
843 max_batch_queue_size: 2048,
844 max_export_batch_size: 512,
845 max_concurrent_exports: 1,
846 batch_scheduled_delay: Duration::from_millis(5000),
847 max_export_timeout: Duration::from_secs(30),
848 }),
849 tokio_console: None,
850 sentry: None,
851 build_version: crate::BUILD_INFO.version,
852 build_sha: crate::BUILD_INFO.sha,
853 registry: metrics_registry.clone(),
854 capture: config.capture,
855 };
856 mz_ore::tracing::configure(config).await?
857 } else {
858 TracingHandle::disabled()
859 };
860 let host_name = format!(
861 "localhost:{}",
862 self.inner.http["external"].handle.local_addr.port()
863 );
864 let catalog_config = CatalogConfig {
865 persist_clients: Arc::clone(&persist_clients),
866 metrics: Arc::new(mz_catalog::durable::Metrics::new(&MetricsRegistry::new())),
867 };
868
869 let inner = self
870 .inner
871 .serve(crate::Config {
872 catalog_config,
873 timestamp_oracle_url: Some(timestamp_oracle_url),
874 controller: ControllerConfig {
875 build_info: &crate::BUILD_INFO,
876 orchestrator,
877 clusterd_image: "clusterd".into(),
878 init_container_image: None,
879 deploy_generation: config.deploy_generation,
880 persist_location: PersistLocation {
881 blob_uri: format!("file://{}/persist/blob", data_directory.display())
882 .parse()
883 .expect("invalid blob URI"),
884 consensus_uri,
885 },
886 persist_clients,
887 now: config.now.clone(),
888 metrics_registry: metrics_registry.clone(),
889 persist_pubsub_url: format!("http://localhost:{}", persist_pubsub_server_port),
890 secrets_args: mz_service::secrets::SecretsReaderCliArgs {
891 secrets_reader: mz_service::secrets::SecretsControllerKind::LocalFile,
892 secrets_reader_local_file_dir: Some(data_directory.join("secrets")),
893 secrets_reader_kubernetes_context: None,
894 secrets_reader_aws_prefix: None,
895 secrets_reader_name_prefix: None,
896 },
897 connection_context,
898 replica_http_locator: Default::default(),
899 },
900 secrets_controller,
901 cloud_resource_controller: None,
902 system_dyncfgs,
903 tls: config.tls,
904 frontegg: config.frontegg,
905 frontegg_oauth_issuer_url: None,
906 unsafe_mode: config.unsafe_mode,
907 all_features: false,
908 metrics_registry: metrics_registry.clone(),
909 now: config.now,
910 environment_id: config.environment_id,
911 cors_allowed_origin: AllowOrigin::list([]),
912 cors_allowed_origin_list: Vec::new(),
913 cluster_replica_sizes: ClusterReplicaSizeMap::for_tests(),
914 bootstrap_default_cluster_replica_size: config.default_cluster_replica_size,
915 bootstrap_default_cluster_replication_factor: config
916 .default_cluster_replication_factor,
917 bootstrap_builtin_system_cluster_config: config.builtin_system_cluster_config,
918 bootstrap_builtin_catalog_server_cluster_config: config
919 .builtin_catalog_server_cluster_config,
920 bootstrap_builtin_probe_cluster_config: config.builtin_probe_cluster_config,
921 bootstrap_builtin_support_cluster_config: config.builtin_support_cluster_config,
922 bootstrap_builtin_analytics_cluster_config: config.builtin_analytics_cluster_config,
923 system_parameter_defaults: config.system_parameter_defaults,
924 availability_zones: Default::default(),
925 tracing_handle,
926 storage_usage_collection_interval: config.storage_usage_collection_interval,
927 storage_usage_retention_period: config.storage_usage_retention_period,
928 segment_api_key: None,
929 segment_client_side: false,
930 test_only_dummy_segment_client: false,
931 egress_addresses: vec![],
932 aws_account_id: None,
933 aws_privatelink_availability_zones: None,
934 launchdarkly_sdk_key: None,
935 launchdarkly_base_uri: None,
936 launchdarkly_key_map: Default::default(),
937 config_sync_file_path: None,
938 config_sync_timeout: Duration::from_secs(30),
939 config_sync_loop_interval: None,
940 bootstrap_role: config.bootstrap_role,
941 http_host_name: Some(host_name),
942 internal_console_redirect_url: config.internal_console_redirect_url,
943 tls_reload_certs,
944 helm_chart_version: None,
945 license_key: ValidatedLicenseKey::for_tests(),
946 external_login_password_mz_system: config.external_login_password_mz_system,
947 force_builtin_schema_migration: None,
948 })
949 .await?;
950
951 Ok(TestServer {
952 inner,
953 metrics_registry,
954 _temp_dir: temp_dir,
955 _scratch_dir: scratch_dir,
956 })
957 }
958}
959
960pub struct TestServer {
962 pub inner: crate::Server,
963 pub metrics_registry: MetricsRegistry,
964 _temp_dir: Option<TempDir>,
966 _scratch_dir: TempDir,
967}
968
969impl TestServer {
970 pub fn connect(&self) -> ConnectBuilder<'_, postgres::NoTls, NoHandle> {
971 ConnectBuilder::new(self).no_tls()
972 }
973
974 pub async fn enable_feature_flags(&self, flags: &[&'static str]) {
975 let internal_client = self.connect().internal().await.unwrap();
976
977 for flag in flags {
978 let query = sql!("ALTER SYSTEM SET {} = true;", Sql::ident(flag));
979 pg_batch_execute(&internal_client, query).await.unwrap();
980 }
981 }
982
983 pub async fn disable_feature_flags(&self, flags: &[&'static str]) {
984 let internal_client = self.connect().internal().await.unwrap();
985
986 for flag in flags {
987 let query = sql!("ALTER SYSTEM SET {} = false;", Sql::ident(flag));
988 pg_batch_execute(&internal_client, query).await.unwrap();
989 }
990 }
991
992 pub fn ws_addr(&self) -> Uri {
993 format!(
994 "ws://{}/api/experimental/sql",
995 self.inner.http_listener_handles["external"].local_addr
996 )
997 .parse()
998 .unwrap()
999 }
1000
1001 pub fn internal_ws_addr(&self) -> Uri {
1002 format!(
1003 "ws://{}/api/experimental/sql",
1004 self.inner.http_listener_handles["internal"].local_addr
1005 )
1006 .parse()
1007 .unwrap()
1008 }
1009
1010 pub fn http_local_addr(&self) -> SocketAddr {
1011 self.inner.http_listener_handles["external"].local_addr
1012 }
1013
1014 pub fn internal_http_local_addr(&self) -> SocketAddr {
1015 self.inner.http_listener_handles["internal"].local_addr
1016 }
1017
1018 pub fn sql_local_addr(&self) -> SocketAddr {
1019 self.inner.sql_listener_handles["external"].local_addr
1020 }
1021
1022 pub fn internal_sql_local_addr(&self) -> SocketAddr {
1023 self.inner.sql_listener_handles["internal"].local_addr
1024 }
1025}
1026
1027pub struct ConnectBuilder<'s, T, H> {
1031 server: &'s TestServer,
1033
1034 pg_config: tokio_postgres::Config,
1036 port: u16,
1038 tls: T,
1040
1041 notice_callback: Option<Box<dyn FnMut(tokio_postgres::error::DbError) + Send + 'static>>,
1043
1044 _with_handle: H,
1046}
1047
1048impl<'s> ConnectBuilder<'s, (), NoHandle> {
1049 fn new(server: &'s TestServer) -> Self {
1050 let mut pg_config = tokio_postgres::Config::new();
1051 pg_config
1052 .host(&Ipv4Addr::LOCALHOST.to_string())
1053 .user("materialize")
1054 .options("--welcome_message=off")
1055 .application_name("environmentd_test_framework");
1056
1057 ConnectBuilder {
1058 server,
1059 pg_config,
1060 port: server.sql_local_addr().port(),
1061 tls: (),
1062 notice_callback: None,
1063 _with_handle: NoHandle,
1064 }
1065 }
1066}
1067
1068impl<'s, T, H> ConnectBuilder<'s, T, H> {
1069 pub fn no_tls(self) -> ConnectBuilder<'s, postgres::NoTls, H> {
1073 ConnectBuilder {
1074 server: self.server,
1075 pg_config: self.pg_config,
1076 port: self.port,
1077 tls: postgres::NoTls,
1078 notice_callback: self.notice_callback,
1079 _with_handle: self._with_handle,
1080 }
1081 }
1082
1083 pub fn with_tls<Tls>(self, tls: Tls) -> ConnectBuilder<'s, Tls, H>
1085 where
1086 Tls: MakeTlsConnect<Socket> + Send + 'static,
1087 Tls::TlsConnect: Send,
1088 Tls::Stream: Send,
1089 <Tls::TlsConnect as TlsConnect<Socket>>::Future: Send,
1090 {
1091 ConnectBuilder {
1092 server: self.server,
1093 pg_config: self.pg_config,
1094 port: self.port,
1095 tls,
1096 notice_callback: self.notice_callback,
1097 _with_handle: self._with_handle,
1098 }
1099 }
1100
1101 pub fn with_config(mut self, pg_config: tokio_postgres::Config) -> Self {
1103 self.pg_config = pg_config;
1104 self
1105 }
1106
1107 pub fn ssl_mode(mut self, mode: SslMode) -> Self {
1109 self.pg_config.ssl_mode(mode);
1110 self
1111 }
1112
1113 pub fn user(mut self, user: &str) -> Self {
1115 self.pg_config.user(user);
1116 self
1117 }
1118
1119 pub fn password(mut self, password: &str) -> Self {
1121 self.pg_config.password(password);
1122 self
1123 }
1124
1125 pub fn application_name(mut self, application_name: &str) -> Self {
1127 self.pg_config.application_name(application_name);
1128 self
1129 }
1130
1131 pub fn dbname(mut self, dbname: &str) -> Self {
1133 self.pg_config.dbname(dbname);
1134 self
1135 }
1136
1137 pub fn options(mut self, options: &str) -> Self {
1139 self.pg_config.options(options);
1140 self
1141 }
1142
1143 pub fn internal(mut self) -> Self {
1148 self.port = self.server.internal_sql_local_addr().port();
1149 self.pg_config.user(mz_sql::session::user::SYSTEM_USER_NAME);
1150 self
1151 }
1152
1153 pub fn notice_callback(self, callback: impl FnMut(DbError) + Send + 'static) -> Self {
1155 ConnectBuilder {
1156 notice_callback: Some(Box::new(callback)),
1157 ..self
1158 }
1159 }
1160
1161 pub fn with_handle(self) -> ConnectBuilder<'s, T, WithHandle> {
1164 ConnectBuilder {
1165 server: self.server,
1166 pg_config: self.pg_config,
1167 port: self.port,
1168 tls: self.tls,
1169 notice_callback: self.notice_callback,
1170 _with_handle: WithHandle,
1171 }
1172 }
1173
1174 pub fn as_pg_config(&self) -> &tokio_postgres::Config {
1176 &self.pg_config
1177 }
1178}
1179
1180pub trait IncludeHandle: Send {
1183 type Output;
1184 fn transform_result(
1185 client: tokio_postgres::Client,
1186 handle: mz_ore::task::JoinHandle<()>,
1187 ) -> Self::Output;
1188}
1189
1190pub struct NoHandle;
1193impl IncludeHandle for NoHandle {
1194 type Output = tokio_postgres::Client;
1195 fn transform_result(
1196 client: tokio_postgres::Client,
1197 _handle: mz_ore::task::JoinHandle<()>,
1198 ) -> Self::Output {
1199 client
1200 }
1201}
1202
1203pub struct WithHandle;
1206impl IncludeHandle for WithHandle {
1207 type Output = (tokio_postgres::Client, mz_ore::task::JoinHandle<()>);
1208 fn transform_result(
1209 client: tokio_postgres::Client,
1210 handle: mz_ore::task::JoinHandle<()>,
1211 ) -> Self::Output {
1212 (client, handle)
1213 }
1214}
1215
1216impl<'s, T, H> IntoFuture for ConnectBuilder<'s, T, H>
1217where
1218 T: MakeTlsConnect<Socket> + Send + 'static,
1219 T::TlsConnect: Send,
1220 T::Stream: Send,
1221 <T::TlsConnect as TlsConnect<Socket>>::Future: Send,
1222 H: IncludeHandle,
1223{
1224 type Output = Result<H::Output, postgres::Error>;
1225 type IntoFuture = BoxFuture<'static, Self::Output>;
1226
1227 fn into_future(mut self) -> Self::IntoFuture {
1228 Box::pin(async move {
1229 assert!(
1230 self.pg_config.get_ports().is_empty(),
1231 "specifying multiple ports is not supported"
1232 );
1233 self.pg_config.port(self.port);
1234
1235 let (client, mut conn) = self.pg_config.connect(self.tls).await?;
1236 let mut notice_callback = self.notice_callback.take();
1237
1238 let handle = task::spawn(|| "connect", async move {
1239 while let Some(msg) = std::future::poll_fn(|cx| conn.poll_message(cx)).await {
1240 match msg {
1241 Ok(AsyncMessage::Notice(notice)) => {
1242 if let Some(callback) = notice_callback.as_mut() {
1243 callback(notice);
1244 }
1245 }
1246 Ok(msg) => {
1247 tracing::debug!(?msg, "Dropping message from database");
1248 }
1249 Err(e) => {
1250 tracing::info!("connection error: {e}");
1255 break;
1256 }
1257 }
1258 }
1259 tracing::info!("connection closed");
1260 });
1261
1262 let output = H::transform_result(client, handle);
1263 Ok(output)
1264 })
1265 }
1266}
1267
1268pub struct TestServerWithRuntime {
1273 server: TestServer,
1274 runtime: Arc<Runtime>,
1275}
1276
1277impl TestServerWithRuntime {
1278 pub fn runtime(&self) -> &Arc<Runtime> {
1282 &self.runtime
1283 }
1284
1285 pub fn inner(&self) -> &crate::Server {
1287 &self.server.inner
1288 }
1289
1290 pub fn connect<T>(&self, tls: T) -> Result<postgres::Client, postgres::Error>
1292 where
1293 T: MakeTlsConnect<Socket> + Send + 'static,
1294 T::TlsConnect: Send,
1295 T::Stream: Send,
1296 <T::TlsConnect as TlsConnect<Socket>>::Future: Send,
1297 {
1298 self.pg_config().connect(tls)
1299 }
1300
1301 pub fn connect_internal<T>(&self, tls: T) -> Result<postgres::Client, anyhow::Error>
1303 where
1304 T: MakeTlsConnect<Socket> + Send + 'static,
1305 T::TlsConnect: Send,
1306 T::Stream: Send,
1307 <T::TlsConnect as TlsConnect<Socket>>::Future: Send,
1308 {
1309 Ok(self.pg_config_internal().connect(tls)?)
1310 }
1311
1312 pub fn enable_feature_flags(&self, flags: &[&'static str]) {
1314 let mut internal_client = self.connect_internal(postgres::NoTls).unwrap();
1315
1316 for flag in flags {
1317 let query = sql!("ALTER SYSTEM SET {} = true;", Sql::ident(flag));
1318 #[allow(clippy::disallowed_methods)]
1321 internal_client.batch_execute(query.as_str()).unwrap();
1322 }
1323 }
1324
1325 pub fn disable_feature_flags(&self, flags: &[&'static str]) {
1327 let mut internal_client = self.connect_internal(postgres::NoTls).unwrap();
1328
1329 for flag in flags {
1330 let query = sql!("ALTER SYSTEM SET {} = false;", Sql::ident(flag));
1331 #[allow(clippy::disallowed_methods)]
1334 internal_client.batch_execute(query.as_str()).unwrap();
1335 }
1336 }
1337
1338 pub fn pg_config(&self) -> postgres::Config {
1341 let local_addr = self.server.sql_local_addr();
1342 let mut config = postgres::Config::new();
1343 config
1344 .host(&Ipv4Addr::LOCALHOST.to_string())
1345 .port(local_addr.port())
1346 .user("materialize")
1347 .options("--welcome_message=off");
1348 config
1349 }
1350
1351 pub fn pg_config_internal(&self) -> postgres::Config {
1354 let local_addr = self.server.internal_sql_local_addr();
1355 let mut config = postgres::Config::new();
1356 config
1357 .host(&Ipv4Addr::LOCALHOST.to_string())
1358 .port(local_addr.port())
1359 .user("mz_system")
1360 .options("--welcome_message=off");
1361 config
1362 }
1363
1364 pub fn ws_addr(&self) -> Uri {
1365 self.server.ws_addr()
1366 }
1367
1368 pub fn internal_ws_addr(&self) -> Uri {
1369 self.server.internal_ws_addr()
1370 }
1371
1372 pub fn http_local_addr(&self) -> SocketAddr {
1373 self.server.http_local_addr()
1374 }
1375
1376 pub fn internal_http_local_addr(&self) -> SocketAddr {
1377 self.server.internal_http_local_addr()
1378 }
1379
1380 pub fn sql_local_addr(&self) -> SocketAddr {
1381 self.server.sql_local_addr()
1382 }
1383
1384 pub fn internal_sql_local_addr(&self) -> SocketAddr {
1385 self.server.internal_sql_local_addr()
1386 }
1387
1388 pub fn metrics_registry(&self) -> &MetricsRegistry {
1390 &self.server.metrics_registry
1391 }
1392}
1393
1394#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
1395pub struct MzTimestamp(pub u64);
1396
1397impl<'a> FromSql<'a> for MzTimestamp {
1398 fn from_sql(ty: &Type, raw: &'a [u8]) -> Result<MzTimestamp, Box<dyn Error + Sync + Send>> {
1399 let n = mz_pgrepr::Numeric::from_sql(ty, raw)?;
1400 Ok(MzTimestamp(u64::try_from(n.0.0)?))
1401 }
1402
1403 fn accepts(ty: &Type) -> bool {
1404 mz_pgrepr::Numeric::accepts(ty)
1405 }
1406}
1407
1408pub trait PostgresErrorExt {
1409 fn unwrap_db_error(self) -> DbError;
1410}
1411
1412impl PostgresErrorExt for postgres::Error {
1413 fn unwrap_db_error(self) -> DbError {
1414 match self.source().and_then(|e| e.downcast_ref::<DbError>()) {
1415 Some(e) => e.clone(),
1416 None => panic!("expected DbError, but got: {:?}", self),
1417 }
1418 }
1419}
1420
1421impl<T, E> PostgresErrorExt for Result<T, E>
1422where
1423 E: PostgresErrorExt,
1424{
1425 fn unwrap_db_error(self) -> DbError {
1426 match self {
1427 Ok(_) => panic!("expected Err(DbError), but got Ok(_)"),
1428 Err(e) => e.unwrap_db_error(),
1429 }
1430 }
1431}
1432
1433pub async fn insert_with_deterministic_timestamps(
1437 table: &'static str,
1438 values: &'static str,
1439 server: &TestServer,
1440 now: Arc<std::sync::Mutex<EpochMillis>>,
1441) -> Result<(), Box<dyn Error>> {
1442 let client_write = server.connect().await?;
1443 let client_read = server.connect().await?;
1444
1445 let mut current_ts = get_explain_timestamp(table, &client_read).await;
1446
1447 let insert_query = format!("INSERT INTO {} VALUES {values}", Sql::ident(table));
1448
1449 #[allow(clippy::disallowed_methods)]
1452 let write_future = client_write.execute(&insert_query, &[]);
1453 let timestamp_interval = tokio::time::interval(Duration::from_millis(1));
1454
1455 let mut write_future = std::pin::pin!(write_future);
1456 let mut timestamp_interval = std::pin::pin!(timestamp_interval);
1457
1458 loop {
1461 tokio::select! {
1462 _ = (&mut write_future) => return Ok(()),
1463 _ = timestamp_interval.tick() => {
1464 current_ts += 1;
1465 *now.lock().expect("lock poisoned") = current_ts;
1466 }
1467 };
1468 }
1469}
1470
1471pub async fn get_explain_timestamp(from_suffix: &str, client: &Client) -> EpochMillis {
1472 try_get_explain_timestamp(from_suffix, client)
1473 .await
1474 .unwrap()
1475}
1476
1477pub async fn try_get_explain_timestamp(
1478 from_suffix: &str,
1479 client: &Client,
1480) -> Result<EpochMillis, anyhow::Error> {
1481 let det = get_explain_timestamp_determination(from_suffix, client).await?;
1482 let ts = det.determination.timestamp_context.timestamp_or_default();
1483 Ok(ts.into())
1484}
1485
1486pub async fn get_explain_timestamp_determination(
1487 from_suffix: &str,
1488 client: &Client,
1489) -> Result<TimestampExplanation, anyhow::Error> {
1490 #[allow(clippy::disallowed_methods)]
1493 let row = client
1494 .query_one(
1495 &format!("EXPLAIN TIMESTAMP AS JSON FOR SELECT * FROM {from_suffix}"),
1496 &[],
1497 )
1498 .await?;
1499 let explain: String = row.get(0);
1500 Ok(serde_json::from_str(&explain).unwrap())
1501}
1502
1503pub async fn create_postgres_source_with_table<'a>(
1511 server: &TestServer,
1512 mz_client: &Client,
1513 table_name: &str,
1514 table_schema: &str,
1515 source_name: &str,
1516) -> (
1517 Client,
1518 impl FnOnce(&'a Client, &'a Client) -> LocalBoxFuture<'a, ()>,
1519) {
1520 server
1521 .enable_feature_flags(&["enable_create_table_from_source"])
1522 .await;
1523
1524 let postgres_url = env::var("POSTGRES_URL")
1525 .map_err(|_| anyhow!("POSTGRES_URL environment variable is not set"))
1526 .unwrap();
1527
1528 let (pg_client, connection) = tokio_postgres::connect(&postgres_url, postgres::NoTls)
1529 .await
1530 .unwrap();
1531
1532 let pg_config: tokio_postgres::Config = postgres_url.parse().unwrap();
1533 let user = pg_config.get_user().unwrap_or("postgres");
1534 let db_name = pg_config.get_dbname().unwrap_or(user);
1535 let ports = pg_config.get_ports();
1536 let port = if ports.is_empty() { 5432 } else { ports[0] };
1537 let hosts = pg_config.get_hosts();
1538 let host = if hosts.is_empty() {
1539 "localhost".to_string()
1540 } else {
1541 match &hosts[0] {
1542 Host::Tcp(host) => host.to_string(),
1543 Host::Unix(host) => host.to_str().unwrap().to_string(),
1544 }
1545 };
1546 let password = pg_config.get_password();
1547
1548 mz_ore::task::spawn(|| "postgres-source-connection", async move {
1549 if let Err(e) = connection.await {
1550 panic!("connection error: {}", e);
1551 }
1552 });
1553
1554 let _ = pg_execute(
1556 &pg_client,
1557 sql!("DROP TABLE IF EXISTS {};", Sql::ident(table_name)),
1558 &[],
1559 )
1560 .await
1561 .unwrap();
1562 let _ = pg_execute(
1563 &pg_client,
1564 sql!("DROP PUBLICATION IF EXISTS {};", Sql::ident(source_name)),
1565 &[],
1566 )
1567 .await
1568 .unwrap();
1569 #[allow(clippy::disallowed_methods)]
1572 let _ = pg_client
1573 .execute(
1574 format!("CREATE TABLE {} {table_schema};", Sql::ident(table_name)).as_str(),
1575 &[],
1576 )
1577 .await
1578 .unwrap();
1579 let _ = pg_execute(
1580 &pg_client,
1581 sql!(
1582 "ALTER TABLE {} REPLICA IDENTITY FULL;",
1583 Sql::ident(table_name)
1584 ),
1585 &[],
1586 )
1587 .await
1588 .unwrap();
1589 let _ = pg_execute(
1590 &pg_client,
1591 sql!(
1592 "CREATE PUBLICATION {} FOR TABLE {};",
1593 Sql::ident(source_name),
1594 Sql::ident(table_name)
1595 ),
1596 &[],
1597 )
1598 .await
1599 .unwrap();
1600
1601 let mut connection_str = format!("HOST '{host}', PORT {port}, USER {user}, DATABASE {db_name}");
1603 if let Some(password) = password {
1604 let password = std::str::from_utf8(password).unwrap();
1605 pg_batch_execute(
1606 mz_client,
1607 sql!("CREATE SECRET s AS {}", Sql::literal(password)),
1608 )
1609 .await
1610 .unwrap();
1611 connection_str = format!("{connection_str}, PASSWORD SECRET s");
1612 }
1613 #[allow(clippy::disallowed_methods)]
1616 mz_client
1617 .batch_execute(format!("CREATE CONNECTION pgconn TO POSTGRES ({connection_str})").as_str())
1618 .await
1619 .unwrap();
1620 pg_batch_execute(
1621 mz_client,
1622 sql!(
1623 "CREATE SOURCE {} \
1624 FROM POSTGRES \
1625 CONNECTION pgconn \
1626 (PUBLICATION {})",
1627 Sql::ident(source_name),
1628 Sql::literal(source_name),
1629 ),
1630 )
1631 .await
1632 .unwrap();
1633 pg_batch_execute(
1634 mz_client,
1635 sql!(
1636 "CREATE TABLE {} \
1637 FROM SOURCE {} \
1638 (REFERENCE {});",
1639 Sql::ident(table_name),
1640 Sql::ident(source_name),
1641 Sql::ident(table_name),
1642 ),
1643 )
1644 .await
1645 .unwrap();
1646
1647 let table_name = table_name.to_string();
1648 let source_name = source_name.to_string();
1649 (
1650 pg_client,
1651 move |mz_client: &'a Client, pg_client: &'a Client| {
1652 let f: Pin<Box<dyn Future<Output = ()> + 'a>> = Box::pin(async move {
1653 pg_batch_execute(
1654 mz_client,
1655 sql!("DROP SOURCE {} CASCADE;", Sql::ident(&source_name)),
1656 )
1657 .await
1658 .unwrap();
1659 pg_batch_execute(mz_client, sql!("DROP CONNECTION pgconn;"))
1660 .await
1661 .unwrap();
1662
1663 let _ = pg_execute(
1664 pg_client,
1665 sql!("DROP PUBLICATION {};", Sql::ident(&source_name)),
1666 &[],
1667 )
1668 .await
1669 .unwrap();
1670 let _ = pg_execute(
1671 pg_client,
1672 sql!("DROP TABLE {};", Sql::ident(&table_name)),
1673 &[],
1674 )
1675 .await
1676 .unwrap();
1677 });
1678 f
1679 },
1680 )
1681}
1682
1683pub async fn wait_for_pg_table_population(mz_client: &Client, view_name: &str, source_rows: i64) {
1684 let current_isolation = pg_query_one(mz_client, sql!("SHOW transaction_isolation"), &[])
1685 .await
1686 .unwrap()
1687 .get::<_, String>(0);
1688 pg_batch_execute(mz_client, sql!("SET transaction_isolation = SERIALIZABLE"))
1689 .await
1690 .unwrap();
1691 Retry::default()
1692 .retry_async(|_| async move {
1693 let rows = pg_query_one(
1694 mz_client,
1695 sql!("SELECT COUNT(*) FROM {};", Sql::ident(view_name)),
1696 &[],
1697 )
1698 .await
1699 .unwrap()
1700 .get::<_, i64>(0);
1701 if rows == source_rows {
1702 Ok(())
1703 } else {
1704 Err(format!(
1705 "Waiting for {source_rows} row to be ingested. Currently at {rows}."
1706 ))
1707 }
1708 })
1709 .await
1710 .unwrap();
1711 pg_batch_execute(
1712 mz_client,
1713 sql!(
1714 "SET transaction_isolation = {}",
1715 Sql::literal(¤t_isolation),
1716 ),
1717 )
1718 .await
1719 .unwrap();
1720}
1721
1722pub fn auth_with_ws(
1724 ws: &mut WebSocket<MaybeTlsStream<TcpStream>>,
1725 mut options: BTreeMap<String, String>,
1726) -> Result<Vec<WebSocketResponse>, anyhow::Error> {
1727 if !options.contains_key("welcome_message") {
1728 options.insert("welcome_message".into(), "off".into());
1729 }
1730 auth_with_ws_impl(
1731 ws,
1732 Message::Text(
1733 serde_json::to_string(&WebSocketAuth::Basic {
1734 user: "materialize".into(),
1735 password: "".into(),
1736 options,
1737 })
1738 .unwrap()
1739 .into(),
1740 ),
1741 )
1742}
1743
1744pub fn auth_with_ws_impl(
1745 ws: &mut WebSocket<MaybeTlsStream<TcpStream>>,
1746 auth_message: Message,
1747) -> Result<Vec<WebSocketResponse>, anyhow::Error> {
1748 ws.send(auth_message)?;
1749
1750 let mut msgs = Vec::new();
1752 loop {
1753 let resp = ws.read()?;
1754 match resp {
1755 Message::Text(msg) => {
1756 let msg: WebSocketResponse = serde_json::from_str(&msg).unwrap();
1757 match msg {
1758 WebSocketResponse::ReadyForQuery(_) => break,
1759 msg => {
1760 msgs.push(msg);
1761 }
1762 }
1763 }
1764 Message::Ping(_) => continue,
1765 Message::Close(None) => return Err(anyhow!("ws closed after auth")),
1766 Message::Close(Some(close_frame)) => {
1767 return Err(anyhow!("ws closed after auth").context(close_frame));
1768 }
1769 _ => panic!("unexpected response: {:?}", resp),
1770 }
1771 }
1772 Ok(msgs)
1773}
1774
1775pub fn make_header<H: Header>(h: H) -> HeaderMap {
1776 let mut map = HeaderMap::new();
1777 map.typed_insert(h);
1778 map
1779}
1780
1781pub fn make_pg_tls<F>(configure: F) -> MakeTlsConnector
1782where
1783 F: FnOnce(&mut SslConnectorBuilder) -> Result<(), ErrorStack>,
1784{
1785 let mut connector_builder = SslConnector::builder(SslMethod::tls()).unwrap();
1786 let options = connector_builder.options() | SslOptions::NO_TLSV1_3;
1807 connector_builder.set_options(options);
1808 configure(&mut connector_builder).unwrap();
1809 MakeTlsConnector::new(connector_builder.build())
1810}
1811
1812pub struct Ca {
1814 pub dir: TempDir,
1815 pub name: X509Name,
1816 pub cert: X509,
1817 pub pkey: PKey<Private>,
1818}
1819
1820impl Ca {
1821 fn make_ca(name: &str, parent: Option<&Ca>) -> Result<Ca, Box<dyn Error>> {
1822 let dir = tempfile::tempdir()?;
1823 let rsa = Rsa::generate(2048)?;
1824 let pkey = PKey::from_rsa(rsa)?;
1825 let name = {
1826 let mut builder = X509NameBuilder::new()?;
1827 builder.append_entry_by_nid(Nid::COMMONNAME, name)?;
1828 builder.build()
1829 };
1830 let cert = {
1831 let mut builder = X509::builder()?;
1832 builder.set_version(2)?;
1833 builder.set_pubkey(&pkey)?;
1834 builder.set_issuer_name(parent.map(|ca| &ca.name).unwrap_or(&name))?;
1835 builder.set_subject_name(&name)?;
1836 builder.set_not_before(&*Asn1Time::days_from_now(0)?)?;
1837 builder.set_not_after(&*Asn1Time::days_from_now(365)?)?;
1838 builder.append_extension(BasicConstraints::new().critical().ca().build()?)?;
1839 builder.sign(
1840 parent.map(|ca| &ca.pkey).unwrap_or(&pkey),
1841 MessageDigest::sha256(),
1842 )?;
1843 builder.build()
1844 };
1845 fs::write(dir.path().join("ca.crt"), cert.to_pem()?)?;
1846 Ok(Ca {
1847 dir,
1848 name,
1849 cert,
1850 pkey,
1851 })
1852 }
1853
1854 pub fn new_root(name: &str) -> Result<Ca, Box<dyn Error>> {
1856 Ca::make_ca(name, None)
1857 }
1858
1859 pub fn ca_cert_path(&self) -> PathBuf {
1861 self.dir.path().join("ca.crt")
1862 }
1863
1864 pub fn request_ca(&self, name: &str) -> Result<Ca, Box<dyn Error>> {
1866 Ca::make_ca(name, Some(self))
1867 }
1868
1869 pub fn request_client_cert(&self, name: &str) -> Result<(PathBuf, PathBuf), Box<dyn Error>> {
1874 self.request_cert(name, iter::empty())
1875 }
1876
1877 pub fn request_cert<I>(&self, name: &str, ips: I) -> Result<(PathBuf, PathBuf), Box<dyn Error>>
1880 where
1881 I: IntoIterator<Item = IpAddr>,
1882 {
1883 let rsa = Rsa::generate(2048)?;
1884 let pkey = PKey::from_rsa(rsa)?;
1885 let subject_name = {
1886 let mut builder = X509NameBuilder::new()?;
1887 builder.append_entry_by_nid(Nid::COMMONNAME, name)?;
1888 builder.build()
1889 };
1890 let cert = {
1891 let mut builder = X509::builder()?;
1892 builder.set_version(2)?;
1893 builder.set_pubkey(&pkey)?;
1894 builder.set_issuer_name(self.cert.subject_name())?;
1895 builder.set_subject_name(&subject_name)?;
1896 builder.set_not_before(&*Asn1Time::days_from_now(0)?)?;
1897 builder.set_not_after(&*Asn1Time::days_from_now(365)?)?;
1898 for ip in ips {
1899 builder.append_extension(
1900 SubjectAlternativeName::new()
1901 .ip(&ip.to_string())
1902 .build(&builder.x509v3_context(None, None))?,
1903 )?;
1904 }
1905 builder.sign(&self.pkey, MessageDigest::sha256())?;
1906 builder.build()
1907 };
1908 let cert_path = self.dir.path().join(Path::new(name).with_extension("crt"));
1909 let key_path = self.dir.path().join(Path::new(name).with_extension("key"));
1910 fs::write(&cert_path, cert.to_pem()?)?;
1911 fs::write(&key_path, pkey.private_key_to_pem_pkcs8()?)?;
1912 Ok((cert_path, key_path))
1913 }
1914}
1915
1916pub fn get_counter_value(registry: &MetricsRegistry, name: &str, labels: &[(&str, &str)]) -> u64 {
1921 let Some(family) = registry.gather().into_iter().find(|m| m.name() == name) else {
1922 return 0;
1923 };
1924 family
1925 .get_metric()
1926 .iter()
1927 .filter(|metric| {
1928 labels.iter().all(|(name, value)| {
1929 metric
1930 .get_label()
1931 .iter()
1932 .any(|label| label.name() == *name && label.value() == *value)
1933 })
1934 })
1935 .map(|metric| u64::cast_lossy(metric.get_counter().value()))
1936 .sum()
1937}