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