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