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 })
992 .await?;
993
994 Ok(TestServer {
995 inner,
996 metrics_registry,
997 _temp_dir: temp_dir,
998 _scratch_dir: scratch_dir,
999 })
1000 }
1001}
1002
1003pub struct TestServer {
1005 pub inner: crate::Server,
1006 pub metrics_registry: MetricsRegistry,
1007 _temp_dir: Option<TempDir>,
1009 _scratch_dir: TempDir,
1010}
1011
1012impl TestServer {
1013 pub fn connect(&self) -> ConnectBuilder<'_, postgres::NoTls, NoHandle> {
1014 ConnectBuilder::new(self).no_tls()
1015 }
1016
1017 pub async fn enable_feature_flags(&self, flags: &[&'static str]) {
1018 let internal_client = self.connect().internal().await.unwrap();
1019
1020 for flag in flags {
1021 let query = sql!("ALTER SYSTEM SET {} = true;", Sql::ident(flag));
1022 pg_batch_execute(&internal_client, query).await.unwrap();
1023 }
1024 }
1025
1026 pub async fn disable_feature_flags(&self, flags: &[&'static str]) {
1027 let internal_client = self.connect().internal().await.unwrap();
1028
1029 for flag in flags {
1030 let query = sql!("ALTER SYSTEM SET {} = false;", Sql::ident(flag));
1031 pg_batch_execute(&internal_client, query).await.unwrap();
1032 }
1033 }
1034
1035 pub fn ws_addr(&self) -> Uri {
1036 format!(
1037 "ws://{}/api/experimental/sql",
1038 self.inner.http_listener_handles["external"].local_addr
1039 )
1040 .parse()
1041 .unwrap()
1042 }
1043
1044 pub fn internal_ws_addr(&self) -> Uri {
1045 format!(
1046 "ws://{}/api/experimental/sql",
1047 self.inner.http_listener_handles["internal"].local_addr
1048 )
1049 .parse()
1050 .unwrap()
1051 }
1052
1053 pub fn http_local_addr(&self) -> SocketAddr {
1054 self.inner.http_listener_handles["external"].local_addr
1055 }
1056
1057 pub fn internal_http_local_addr(&self) -> SocketAddr {
1058 self.inner.http_listener_handles["internal"].local_addr
1059 }
1060
1061 pub fn sql_local_addr(&self) -> SocketAddr {
1062 self.inner.sql_listener_handles["external"].local_addr
1063 }
1064
1065 pub fn internal_sql_local_addr(&self) -> SocketAddr {
1066 self.inner.sql_listener_handles["internal"].local_addr
1067 }
1068}
1069
1070pub struct ConnectBuilder<'s, T, H> {
1074 server: &'s TestServer,
1076
1077 pg_config: tokio_postgres::Config,
1079 port: u16,
1081 tls: T,
1083
1084 notice_callback: Option<Box<dyn FnMut(tokio_postgres::error::DbError) + Send + 'static>>,
1086
1087 _with_handle: H,
1089}
1090
1091impl<'s> ConnectBuilder<'s, (), NoHandle> {
1092 fn new(server: &'s TestServer) -> Self {
1093 let mut pg_config = tokio_postgres::Config::new();
1094 pg_config
1095 .host(&Ipv4Addr::LOCALHOST.to_string())
1096 .user("materialize")
1097 .options("--welcome_message=off")
1098 .application_name("environmentd_test_framework");
1099
1100 ConnectBuilder {
1101 server,
1102 pg_config,
1103 port: server.sql_local_addr().port(),
1104 tls: (),
1105 notice_callback: None,
1106 _with_handle: NoHandle,
1107 }
1108 }
1109}
1110
1111impl<'s, T, H> ConnectBuilder<'s, T, H> {
1112 pub fn no_tls(self) -> ConnectBuilder<'s, postgres::NoTls, H> {
1116 ConnectBuilder {
1117 server: self.server,
1118 pg_config: self.pg_config,
1119 port: self.port,
1120 tls: postgres::NoTls,
1121 notice_callback: self.notice_callback,
1122 _with_handle: self._with_handle,
1123 }
1124 }
1125
1126 pub fn with_tls<Tls>(self, tls: Tls) -> ConnectBuilder<'s, Tls, H>
1128 where
1129 Tls: MakeTlsConnect<Socket> + Send + 'static,
1130 Tls::TlsConnect: Send,
1131 Tls::Stream: Send,
1132 <Tls::TlsConnect as TlsConnect<Socket>>::Future: Send,
1133 {
1134 ConnectBuilder {
1135 server: self.server,
1136 pg_config: self.pg_config,
1137 port: self.port,
1138 tls,
1139 notice_callback: self.notice_callback,
1140 _with_handle: self._with_handle,
1141 }
1142 }
1143
1144 pub fn with_config(mut self, pg_config: tokio_postgres::Config) -> Self {
1146 self.pg_config = pg_config;
1147 self
1148 }
1149
1150 pub fn ssl_mode(mut self, mode: SslMode) -> Self {
1152 self.pg_config.ssl_mode(mode);
1153 self
1154 }
1155
1156 pub fn user(mut self, user: &str) -> Self {
1158 self.pg_config.user(user);
1159 self
1160 }
1161
1162 pub fn password(mut self, password: &str) -> Self {
1164 self.pg_config.password(password);
1165 self
1166 }
1167
1168 pub fn application_name(mut self, application_name: &str) -> Self {
1170 self.pg_config.application_name(application_name);
1171 self
1172 }
1173
1174 pub fn dbname(mut self, dbname: &str) -> Self {
1176 self.pg_config.dbname(dbname);
1177 self
1178 }
1179
1180 pub fn options(mut self, options: &str) -> Self {
1182 self.pg_config.options(options);
1183 self
1184 }
1185
1186 pub fn internal(mut self) -> Self {
1191 self.port = self.server.internal_sql_local_addr().port();
1192 self.pg_config.user(mz_sql::session::user::SYSTEM_USER_NAME);
1193 self
1194 }
1195
1196 pub fn notice_callback(self, callback: impl FnMut(DbError) + Send + 'static) -> Self {
1198 ConnectBuilder {
1199 notice_callback: Some(Box::new(callback)),
1200 ..self
1201 }
1202 }
1203
1204 pub fn with_handle(self) -> ConnectBuilder<'s, T, WithHandle> {
1207 ConnectBuilder {
1208 server: self.server,
1209 pg_config: self.pg_config,
1210 port: self.port,
1211 tls: self.tls,
1212 notice_callback: self.notice_callback,
1213 _with_handle: WithHandle,
1214 }
1215 }
1216
1217 pub fn as_pg_config(&self) -> &tokio_postgres::Config {
1219 &self.pg_config
1220 }
1221}
1222
1223pub trait IncludeHandle: Send {
1226 type Output;
1227 fn transform_result(
1228 client: tokio_postgres::Client,
1229 handle: mz_ore::task::JoinHandle<()>,
1230 ) -> Self::Output;
1231}
1232
1233pub struct NoHandle;
1236impl IncludeHandle for NoHandle {
1237 type Output = tokio_postgres::Client;
1238 fn transform_result(
1239 client: tokio_postgres::Client,
1240 _handle: mz_ore::task::JoinHandle<()>,
1241 ) -> Self::Output {
1242 client
1243 }
1244}
1245
1246pub struct WithHandle;
1249impl IncludeHandle for WithHandle {
1250 type Output = (tokio_postgres::Client, mz_ore::task::JoinHandle<()>);
1251 fn transform_result(
1252 client: tokio_postgres::Client,
1253 handle: mz_ore::task::JoinHandle<()>,
1254 ) -> Self::Output {
1255 (client, handle)
1256 }
1257}
1258
1259impl<'s, T, H> IntoFuture for ConnectBuilder<'s, T, H>
1260where
1261 T: MakeTlsConnect<Socket> + Send + 'static,
1262 T::TlsConnect: Send,
1263 T::Stream: Send,
1264 <T::TlsConnect as TlsConnect<Socket>>::Future: Send,
1265 H: IncludeHandle,
1266{
1267 type Output = Result<H::Output, postgres::Error>;
1268 type IntoFuture = BoxFuture<'static, Self::Output>;
1269
1270 fn into_future(mut self) -> Self::IntoFuture {
1271 Box::pin(async move {
1272 assert!(
1273 self.pg_config.get_ports().is_empty(),
1274 "specifying multiple ports is not supported"
1275 );
1276 self.pg_config.port(self.port);
1277
1278 let (client, mut conn) = self.pg_config.connect(self.tls).await?;
1279 let mut notice_callback = self.notice_callback.take();
1280
1281 let handle = task::spawn(|| "connect", async move {
1282 while let Some(msg) = std::future::poll_fn(|cx| conn.poll_message(cx)).await {
1283 match msg {
1284 Ok(AsyncMessage::Notice(notice)) => {
1285 if let Some(callback) = notice_callback.as_mut() {
1286 callback(notice);
1287 }
1288 }
1289 Ok(msg) => {
1290 tracing::debug!(?msg, "Dropping message from database");
1291 }
1292 Err(e) => {
1293 tracing::info!("connection error: {e}");
1298 break;
1299 }
1300 }
1301 }
1302 tracing::info!("connection closed");
1303 });
1304
1305 let output = H::transform_result(client, handle);
1306 Ok(output)
1307 })
1308 }
1309}
1310
1311pub struct TestServerWithRuntime {
1316 server: TestServer,
1317 runtime: Arc<Runtime>,
1318}
1319
1320impl TestServerWithRuntime {
1321 pub fn runtime(&self) -> &Arc<Runtime> {
1325 &self.runtime
1326 }
1327
1328 pub fn inner(&self) -> &crate::Server {
1330 &self.server.inner
1331 }
1332
1333 pub fn connect<T>(&self, tls: T) -> Result<postgres::Client, postgres::Error>
1335 where
1336 T: MakeTlsConnect<Socket> + Send + 'static,
1337 T::TlsConnect: Send,
1338 T::Stream: Send,
1339 <T::TlsConnect as TlsConnect<Socket>>::Future: Send,
1340 {
1341 self.pg_config().connect(tls)
1342 }
1343
1344 pub fn connect_internal<T>(&self, tls: T) -> Result<postgres::Client, anyhow::Error>
1346 where
1347 T: MakeTlsConnect<Socket> + Send + 'static,
1348 T::TlsConnect: Send,
1349 T::Stream: Send,
1350 <T::TlsConnect as TlsConnect<Socket>>::Future: Send,
1351 {
1352 Ok(self.pg_config_internal().connect(tls)?)
1353 }
1354
1355 pub fn enable_feature_flags(&self, flags: &[&'static str]) {
1357 let mut internal_client = self.connect_internal(postgres::NoTls).unwrap();
1358
1359 for flag in flags {
1360 let query = sql!("ALTER SYSTEM SET {} = true;", Sql::ident(flag));
1361 #[allow(clippy::disallowed_methods)]
1364 internal_client.batch_execute(query.as_str()).unwrap();
1365 }
1366 }
1367
1368 pub fn disable_feature_flags(&self, flags: &[&'static str]) {
1370 let mut internal_client = self.connect_internal(postgres::NoTls).unwrap();
1371
1372 for flag in flags {
1373 let query = sql!("ALTER SYSTEM SET {} = false;", Sql::ident(flag));
1374 #[allow(clippy::disallowed_methods)]
1377 internal_client.batch_execute(query.as_str()).unwrap();
1378 }
1379 }
1380
1381 pub fn pg_config(&self) -> postgres::Config {
1384 let local_addr = self.server.sql_local_addr();
1385 let mut config = postgres::Config::new();
1386 config
1387 .host(&Ipv4Addr::LOCALHOST.to_string())
1388 .port(local_addr.port())
1389 .user("materialize")
1390 .options("--welcome_message=off");
1391 config
1392 }
1393
1394 pub fn pg_config_internal(&self) -> postgres::Config {
1397 let local_addr = self.server.internal_sql_local_addr();
1398 let mut config = postgres::Config::new();
1399 config
1400 .host(&Ipv4Addr::LOCALHOST.to_string())
1401 .port(local_addr.port())
1402 .user("mz_system")
1403 .options("--welcome_message=off");
1404 config
1405 }
1406
1407 pub fn ws_addr(&self) -> Uri {
1408 self.server.ws_addr()
1409 }
1410
1411 pub fn internal_ws_addr(&self) -> Uri {
1412 self.server.internal_ws_addr()
1413 }
1414
1415 pub fn http_local_addr(&self) -> SocketAddr {
1416 self.server.http_local_addr()
1417 }
1418
1419 pub fn internal_http_local_addr(&self) -> SocketAddr {
1420 self.server.internal_http_local_addr()
1421 }
1422
1423 pub fn sql_local_addr(&self) -> SocketAddr {
1424 self.server.sql_local_addr()
1425 }
1426
1427 pub fn internal_sql_local_addr(&self) -> SocketAddr {
1428 self.server.internal_sql_local_addr()
1429 }
1430
1431 pub fn metrics_registry(&self) -> &MetricsRegistry {
1433 &self.server.metrics_registry
1434 }
1435}
1436
1437#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
1438pub struct MzTimestamp(pub u64);
1439
1440impl<'a> FromSql<'a> for MzTimestamp {
1441 fn from_sql(ty: &Type, raw: &'a [u8]) -> Result<MzTimestamp, Box<dyn Error + Sync + Send>> {
1442 let n = mz_pgrepr::Numeric::from_sql(ty, raw)?;
1443 Ok(MzTimestamp(u64::try_from(n.0.0)?))
1444 }
1445
1446 fn accepts(ty: &Type) -> bool {
1447 mz_pgrepr::Numeric::accepts(ty)
1448 }
1449}
1450
1451pub trait PostgresErrorExt {
1452 fn unwrap_db_error(self) -> DbError;
1453}
1454
1455impl PostgresErrorExt for postgres::Error {
1456 fn unwrap_db_error(self) -> DbError {
1457 match self.source().and_then(|e| e.downcast_ref::<DbError>()) {
1458 Some(e) => e.clone(),
1459 None => panic!("expected DbError, but got: {:?}", self),
1460 }
1461 }
1462}
1463
1464impl<T, E> PostgresErrorExt for Result<T, E>
1465where
1466 E: PostgresErrorExt,
1467{
1468 fn unwrap_db_error(self) -> DbError {
1469 match self {
1470 Ok(_) => panic!("expected Err(DbError), but got Ok(_)"),
1471 Err(e) => e.unwrap_db_error(),
1472 }
1473 }
1474}
1475
1476pub async fn insert_with_deterministic_timestamps(
1480 table: &'static str,
1481 values: &'static str,
1482 server: &TestServer,
1483 now: Arc<std::sync::Mutex<EpochMillis>>,
1484) -> Result<(), Box<dyn Error>> {
1485 let client_write = server.connect().await?;
1486 let client_read = server.connect().await?;
1487
1488 let mut current_ts = get_explain_timestamp(table, &client_read).await;
1489
1490 let insert_query = format!("INSERT INTO {} VALUES {values}", Sql::ident(table));
1491
1492 #[allow(clippy::disallowed_methods)]
1495 let write_future = client_write.execute(&insert_query, &[]);
1496 let timestamp_interval = tokio::time::interval(Duration::from_millis(1));
1497
1498 let mut write_future = std::pin::pin!(write_future);
1499 let mut timestamp_interval = std::pin::pin!(timestamp_interval);
1500
1501 loop {
1504 tokio::select! {
1505 _ = (&mut write_future) => return Ok(()),
1506 _ = timestamp_interval.tick() => {
1507 current_ts += 1;
1508 *now.lock().expect("lock poisoned") = current_ts;
1509 }
1510 };
1511 }
1512}
1513
1514pub async fn get_explain_timestamp(from_suffix: &str, client: &Client) -> EpochMillis {
1515 try_get_explain_timestamp(from_suffix, client)
1516 .await
1517 .unwrap()
1518}
1519
1520pub async fn try_get_explain_timestamp(
1521 from_suffix: &str,
1522 client: &Client,
1523) -> Result<EpochMillis, anyhow::Error> {
1524 let det = get_explain_timestamp_determination(from_suffix, client).await?;
1525 let ts = det.determination.timestamp_context.timestamp_or_default();
1526 Ok(ts.into())
1527}
1528
1529pub async fn get_explain_timestamp_determination(
1530 from_suffix: &str,
1531 client: &Client,
1532) -> Result<TimestampExplanation, anyhow::Error> {
1533 #[allow(clippy::disallowed_methods)]
1536 let row = client
1537 .query_one(
1538 &format!("EXPLAIN TIMESTAMP AS JSON FOR SELECT * FROM {from_suffix}"),
1539 &[],
1540 )
1541 .await?;
1542 let explain: String = row.get(0);
1543 Ok(serde_json::from_str(&explain).unwrap())
1544}
1545
1546pub async fn create_postgres_source_with_table<'a>(
1554 server: &TestServer,
1555 mz_client: &Client,
1556 table_name: &str,
1557 table_schema: &str,
1558 source_name: &str,
1559) -> (
1560 Client,
1561 impl FnOnce(&'a Client, &'a Client) -> LocalBoxFuture<'a, ()>,
1562) {
1563 server
1564 .enable_feature_flags(&["enable_create_table_from_source"])
1565 .await;
1566
1567 let postgres_url = env::var("POSTGRES_URL")
1568 .map_err(|_| anyhow!("POSTGRES_URL environment variable is not set"))
1569 .unwrap();
1570
1571 let (pg_client, connection) = tokio_postgres::connect(&postgres_url, postgres::NoTls)
1572 .await
1573 .unwrap();
1574
1575 let pg_config: tokio_postgres::Config = postgres_url.parse().unwrap();
1576 let user = pg_config.get_user().unwrap_or("postgres");
1577 let db_name = pg_config.get_dbname().unwrap_or(user);
1578 let ports = pg_config.get_ports();
1579 let port = if ports.is_empty() { 5432 } else { ports[0] };
1580 let hosts = pg_config.get_hosts();
1581 let host = if hosts.is_empty() {
1582 "localhost".to_string()
1583 } else {
1584 match &hosts[0] {
1585 Host::Tcp(host) => host.to_string(),
1586 Host::Unix(host) => host.to_str().unwrap().to_string(),
1587 }
1588 };
1589 let password = pg_config.get_password();
1590
1591 mz_ore::task::spawn(|| "postgres-source-connection", async move {
1592 if let Err(e) = connection.await {
1593 panic!("connection error: {}", e);
1594 }
1595 });
1596
1597 let _ = pg_execute(
1599 &pg_client,
1600 sql!("DROP TABLE IF EXISTS {};", Sql::ident(table_name)),
1601 &[],
1602 )
1603 .await
1604 .unwrap();
1605 let _ = pg_execute(
1606 &pg_client,
1607 sql!("DROP PUBLICATION IF EXISTS {};", Sql::ident(source_name)),
1608 &[],
1609 )
1610 .await
1611 .unwrap();
1612 #[allow(clippy::disallowed_methods)]
1615 let _ = pg_client
1616 .execute(
1617 format!("CREATE TABLE {} {table_schema};", Sql::ident(table_name)).as_str(),
1618 &[],
1619 )
1620 .await
1621 .unwrap();
1622 let _ = pg_execute(
1623 &pg_client,
1624 sql!(
1625 "ALTER TABLE {} REPLICA IDENTITY FULL;",
1626 Sql::ident(table_name)
1627 ),
1628 &[],
1629 )
1630 .await
1631 .unwrap();
1632 let _ = pg_execute(
1633 &pg_client,
1634 sql!(
1635 "CREATE PUBLICATION {} FOR TABLE {};",
1636 Sql::ident(source_name),
1637 Sql::ident(table_name)
1638 ),
1639 &[],
1640 )
1641 .await
1642 .unwrap();
1643
1644 let mut connection_str = format!("HOST '{host}', PORT {port}, USER {user}, DATABASE {db_name}");
1646 if let Some(password) = password {
1647 let password = std::str::from_utf8(password).unwrap();
1648 pg_batch_execute(
1649 mz_client,
1650 sql!("CREATE SECRET s AS {}", Sql::literal(password)),
1651 )
1652 .await
1653 .unwrap();
1654 connection_str = format!("{connection_str}, PASSWORD SECRET s");
1655 }
1656 #[allow(clippy::disallowed_methods)]
1659 mz_client
1660 .batch_execute(format!("CREATE CONNECTION pgconn TO POSTGRES ({connection_str})").as_str())
1661 .await
1662 .unwrap();
1663 pg_batch_execute(
1664 mz_client,
1665 sql!(
1666 "CREATE SOURCE {} \
1667 FROM POSTGRES \
1668 CONNECTION pgconn \
1669 (PUBLICATION {})",
1670 Sql::ident(source_name),
1671 Sql::literal(source_name),
1672 ),
1673 )
1674 .await
1675 .unwrap();
1676 pg_batch_execute(
1677 mz_client,
1678 sql!(
1679 "CREATE TABLE {} \
1680 FROM SOURCE {} \
1681 (REFERENCE {});",
1682 Sql::ident(table_name),
1683 Sql::ident(source_name),
1684 Sql::ident(table_name),
1685 ),
1686 )
1687 .await
1688 .unwrap();
1689
1690 let table_name = table_name.to_string();
1691 let source_name = source_name.to_string();
1692 (
1693 pg_client,
1694 move |mz_client: &'a Client, pg_client: &'a Client| {
1695 let f: Pin<Box<dyn Future<Output = ()> + 'a>> = Box::pin(async move {
1696 pg_batch_execute(
1697 mz_client,
1698 sql!("DROP SOURCE {} CASCADE;", Sql::ident(&source_name)),
1699 )
1700 .await
1701 .unwrap();
1702 pg_batch_execute(mz_client, sql!("DROP CONNECTION pgconn;"))
1703 .await
1704 .unwrap();
1705
1706 let _ = pg_execute(
1707 pg_client,
1708 sql!("DROP PUBLICATION {};", Sql::ident(&source_name)),
1709 &[],
1710 )
1711 .await
1712 .unwrap();
1713 let _ = pg_execute(
1714 pg_client,
1715 sql!("DROP TABLE {};", Sql::ident(&table_name)),
1716 &[],
1717 )
1718 .await
1719 .unwrap();
1720 });
1721 f
1722 },
1723 )
1724}
1725
1726pub async fn wait_for_pg_table_population(mz_client: &Client, view_name: &str, source_rows: i64) {
1727 let current_isolation = pg_query_one(mz_client, sql!("SHOW transaction_isolation"), &[])
1728 .await
1729 .unwrap()
1730 .get::<_, String>(0);
1731 pg_batch_execute(mz_client, sql!("SET transaction_isolation = SERIALIZABLE"))
1732 .await
1733 .unwrap();
1734 Retry::default()
1735 .retry_async(|_| async move {
1736 let rows = pg_query_one(
1737 mz_client,
1738 sql!("SELECT COUNT(*) FROM {};", Sql::ident(view_name)),
1739 &[],
1740 )
1741 .await
1742 .unwrap()
1743 .get::<_, i64>(0);
1744 if rows == source_rows {
1745 Ok(())
1746 } else {
1747 Err(format!(
1748 "Waiting for {source_rows} row to be ingested. Currently at {rows}."
1749 ))
1750 }
1751 })
1752 .await
1753 .unwrap();
1754 pg_batch_execute(
1755 mz_client,
1756 sql!(
1757 "SET transaction_isolation = {}",
1758 Sql::literal(¤t_isolation),
1759 ),
1760 )
1761 .await
1762 .unwrap();
1763}
1764
1765pub fn auth_with_ws(
1767 ws: &mut WebSocket<MaybeTlsStream<TcpStream>>,
1768 mut options: BTreeMap<String, String>,
1769) -> Result<Vec<WebSocketResponse>, anyhow::Error> {
1770 if !options.contains_key("welcome_message") {
1771 options.insert("welcome_message".into(), "off".into());
1772 }
1773 auth_with_ws_impl(
1774 ws,
1775 Message::Text(
1776 serde_json::to_string(&WebSocketAuth::Basic {
1777 user: "materialize".into(),
1778 password: "".into(),
1779 options,
1780 })
1781 .unwrap()
1782 .into(),
1783 ),
1784 )
1785}
1786
1787pub fn auth_with_ws_impl(
1788 ws: &mut WebSocket<MaybeTlsStream<TcpStream>>,
1789 auth_message: Message,
1790) -> Result<Vec<WebSocketResponse>, anyhow::Error> {
1791 ws.send(auth_message)?;
1792
1793 let mut msgs = Vec::new();
1795 loop {
1796 let resp = ws.read()?;
1797 match resp {
1798 Message::Text(msg) => {
1799 let msg: WebSocketResponse = serde_json::from_str(&msg).unwrap();
1800 match msg {
1801 WebSocketResponse::ReadyForQuery(_) => break,
1802 msg => {
1803 msgs.push(msg);
1804 }
1805 }
1806 }
1807 Message::Ping(_) => continue,
1808 Message::Close(None) => return Err(anyhow!("ws closed after auth")),
1809 Message::Close(Some(close_frame)) => {
1810 return Err(anyhow!("ws closed after auth").context(close_frame));
1811 }
1812 _ => panic!("unexpected response: {:?}", resp),
1813 }
1814 }
1815 Ok(msgs)
1816}
1817
1818pub fn make_header<H: Header>(h: H) -> HeaderMap {
1819 let mut map = HeaderMap::new();
1820 map.typed_insert(h);
1821 map
1822}
1823
1824pub fn make_pg_tls<F>(configure: F) -> MakeTlsConnector
1825where
1826 F: FnOnce(&mut SslConnectorBuilder) -> Result<(), ErrorStack>,
1827{
1828 let mut connector_builder = SslConnector::builder(SslMethod::tls()).unwrap();
1829 let options = connector_builder.options() | SslOptions::NO_TLSV1_3;
1850 connector_builder.set_options(options);
1851 configure(&mut connector_builder).unwrap();
1852 MakeTlsConnector::new(connector_builder.build())
1853}
1854
1855pub struct Ca {
1857 pub dir: TempDir,
1858 pub name: X509Name,
1859 pub cert: X509,
1860 pub pkey: PKey<Private>,
1861}
1862
1863impl Ca {
1864 fn make_ca(name: &str, parent: Option<&Ca>) -> Result<Ca, Box<dyn Error>> {
1865 let dir = tempfile::tempdir()?;
1866 let rsa = Rsa::generate(2048)?;
1867 let pkey = PKey::from_rsa(rsa)?;
1868 let name = {
1869 let mut builder = X509NameBuilder::new()?;
1870 builder.append_entry_by_nid(Nid::COMMONNAME, name)?;
1871 builder.build()
1872 };
1873 let cert = {
1874 let mut builder = X509::builder()?;
1875 builder.set_version(2)?;
1876 builder.set_pubkey(&pkey)?;
1877 builder.set_issuer_name(parent.map(|ca| &ca.name).unwrap_or(&name))?;
1878 builder.set_subject_name(&name)?;
1879 builder.set_not_before(&*Asn1Time::days_from_now(0)?)?;
1880 builder.set_not_after(&*Asn1Time::days_from_now(365)?)?;
1881 builder.append_extension(BasicConstraints::new().critical().ca().build()?)?;
1882 builder.sign(
1883 parent.map(|ca| &ca.pkey).unwrap_or(&pkey),
1884 MessageDigest::sha256(),
1885 )?;
1886 builder.build()
1887 };
1888 fs::write(dir.path().join("ca.crt"), cert.to_pem()?)?;
1889 Ok(Ca {
1890 dir,
1891 name,
1892 cert,
1893 pkey,
1894 })
1895 }
1896
1897 pub fn new_root(name: &str) -> Result<Ca, Box<dyn Error>> {
1899 Ca::make_ca(name, None)
1900 }
1901
1902 pub fn ca_cert_path(&self) -> PathBuf {
1904 self.dir.path().join("ca.crt")
1905 }
1906
1907 pub fn request_ca(&self, name: &str) -> Result<Ca, Box<dyn Error>> {
1909 Ca::make_ca(name, Some(self))
1910 }
1911
1912 pub fn request_client_cert(&self, name: &str) -> Result<(PathBuf, PathBuf), Box<dyn Error>> {
1917 self.request_cert(name, iter::empty())
1918 }
1919
1920 pub fn request_cert<I>(&self, name: &str, ips: I) -> Result<(PathBuf, PathBuf), Box<dyn Error>>
1923 where
1924 I: IntoIterator<Item = IpAddr>,
1925 {
1926 let rsa = Rsa::generate(2048)?;
1927 let pkey = PKey::from_rsa(rsa)?;
1928 let subject_name = {
1929 let mut builder = X509NameBuilder::new()?;
1930 builder.append_entry_by_nid(Nid::COMMONNAME, name)?;
1931 builder.build()
1932 };
1933 let cert = {
1934 let mut builder = X509::builder()?;
1935 builder.set_version(2)?;
1936 builder.set_pubkey(&pkey)?;
1937 builder.set_issuer_name(self.cert.subject_name())?;
1938 builder.set_subject_name(&subject_name)?;
1939 builder.set_not_before(&*Asn1Time::days_from_now(0)?)?;
1940 builder.set_not_after(&*Asn1Time::days_from_now(365)?)?;
1941 for ip in ips {
1942 builder.append_extension(
1943 SubjectAlternativeName::new()
1944 .ip(&ip.to_string())
1945 .build(&builder.x509v3_context(None, None))?,
1946 )?;
1947 }
1948 builder.sign(&self.pkey, MessageDigest::sha256())?;
1949 builder.build()
1950 };
1951 let cert_path = self.dir.path().join(Path::new(name).with_extension("crt"));
1952 let key_path = self.dir.path().join(Path::new(name).with_extension("key"));
1953 fs::write(&cert_path, cert.to_pem()?)?;
1954 fs::write(&key_path, pkey.private_key_to_pem_pkcs8()?)?;
1955 Ok((cert_path, key_path))
1956 }
1957}
1958
1959pub fn get_counter_value(registry: &MetricsRegistry, name: &str, labels: &[(&str, &str)]) -> u64 {
1964 let Some(family) = registry.gather().into_iter().find(|m| m.name() == name) else {
1965 return 0;
1966 };
1967 family
1968 .get_metric()
1969 .iter()
1970 .filter(|metric| {
1971 labels.iter().all(|(name, value)| {
1972 metric
1973 .get_label()
1974 .iter()
1975 .any(|label| label.name() == *name && label.value() == *value)
1976 })
1977 })
1978 .map(|metric| u64::cast_lossy(metric.get_counter().value()))
1979 .sum()
1980}