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