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