Skip to main content

mz_environmentd/
test_util.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use 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/// Entry point for creating and configuring an `environmentd` test harness.
105#[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    /// Whether the connection context carries the AWS external ID prefix and
114    /// connection role ARN. Default true, matching a cloud deployment. Set false
115    /// via [`TestHarness::without_aws_connection_context`] to model a deployment
116    /// that never configured the AWS context, where the context functions fold
117    /// to NULL.
118    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    // This is currently unrelated to enable_tracing, and is used only to disable orchestrator
135    // tracing.
136    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            // This and startup_log_filter below are both (?) needed to suppress clusterd messages.
240            // If we need those in the future, we might need to change both.
241            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    /// Starts a test [`TestServer`], panicking if the server could not be started.
273    ///
274    /// For cases when startup might fail, see [`TestHarness::try_start`].
275    pub async fn start(self) -> TestServer {
276        self.try_start().await.expect("Failed to start test Server")
277    }
278
279    /// Like [`TestHarness::start`] but can specify a cert reload trigger.
280    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    /// Starts a test [`TestServer`], returning an error if the server could not be started.
287    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    /// Like [`TestHarness::try_start`] but can specify a cert reload trigger.
293    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    /// Starts a runtime and returns a [`TestServerWithRuntime`].
302    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    /// Models a deployment that never configured the AWS context, so the AWS
338    /// external ID prefix and connection role ARN are absent and the plan-time
339    /// AWS context functions fold to NULL.
340    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            // Match the MCP routes to the listener's `base` policy (falling back
691            // to `NormalAndInternal` if `base` is disabled, e.g. a metrics-only
692            // listener), then enable/disable them.
693            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    /// Forces every builtin storage collection through the given migration mechanism,
730    /// `"evolution"` or `"replacement"`.
731    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                // If no data directory is provided, we create a temporary
765                // directory. The temporary directory is cleaned up when the
766                // `TempDir` is dropped, so we keep it alive until the `Server` is
767                // dropped.
768                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        // Messing with the clock causes persist to expire leases, causing hangs and
824        // panics. Is it possible/desirable to put this back somehow?
825        let persist_now = SYSTEM_TIME.clone();
826        let dyncfgs = mz_dyncfgs::all_dyncfgs();
827
828        let mut updates = ConfigUpdates::default();
829        // Tune down the number of connections to make this all work a little easier
830        // with local postgres.
831        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        // Stress persist more by writing rollups frequently
837        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        // Spawn the persist pub-sub server.
851        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
1004/// A running instance of `environmentd`.
1005pub struct TestServer {
1006    pub inner: crate::Server,
1007    pub metrics_registry: MetricsRegistry,
1008    /// The `TempDir`s are saved to prevent them from being dropped, and thus cleaned up too early.
1009    _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
1071/// A builder struct to configure a pgwire connection to a running [`TestServer`].
1072///
1073/// You can create this struct, and thus open a pgwire connection, using [`TestServer::connect`].
1074pub struct ConnectBuilder<'s, T, H> {
1075    /// A running `environmentd` test server.
1076    server: &'s TestServer,
1077
1078    /// Postgres configuration for connecting to the test server.
1079    pg_config: tokio_postgres::Config,
1080    /// Port to use when connecting to the test server.
1081    port: u16,
1082    /// Tls settings to use.
1083    tls: T,
1084
1085    /// Callback that gets invoked for every notice we receive.
1086    notice_callback: Option<Box<dyn FnMut(tokio_postgres::error::DbError) + Send + 'static>>,
1087
1088    /// Type variable for whether or not we include the handle for the spawned [`tokio::task`].
1089    _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    /// Create a pgwire connection without using TLS.
1114    ///
1115    /// Note: this is the default for all connections.
1116    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    /// Create a pgwire connection with TLS.
1128    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    /// Create a [`ConnectBuilder`] using the provided [`tokio_postgres::Config`].
1146    pub fn with_config(mut self, pg_config: tokio_postgres::Config) -> Self {
1147        self.pg_config = pg_config;
1148        self
1149    }
1150
1151    /// Set the [`SslMode`] to be used with the resulting connection.
1152    pub fn ssl_mode(mut self, mode: SslMode) -> Self {
1153        self.pg_config.ssl_mode(mode);
1154        self
1155    }
1156
1157    /// Set the user for the pgwire connection.
1158    pub fn user(mut self, user: &str) -> Self {
1159        self.pg_config.user(user);
1160        self
1161    }
1162
1163    /// Set the password for the pgwire connection.
1164    pub fn password(mut self, password: &str) -> Self {
1165        self.pg_config.password(password);
1166        self
1167    }
1168
1169    /// Set the application name for the pgwire connection.
1170    pub fn application_name(mut self, application_name: &str) -> Self {
1171        self.pg_config.application_name(application_name);
1172        self
1173    }
1174
1175    /// Set the database name for the pgwire connection.
1176    pub fn dbname(mut self, dbname: &str) -> Self {
1177        self.pg_config.dbname(dbname);
1178        self
1179    }
1180
1181    /// Set the options for the pgwire connection.
1182    pub fn options(mut self, options: &str) -> Self {
1183        self.pg_config.options(options);
1184        self
1185    }
1186
1187    /// Configures this [`ConnectBuilder`] to connect to the __internal__ SQL port of the running
1188    /// [`TestServer`].
1189    ///
1190    /// For example, this will change the port we connect to, and the user we connect as.
1191    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    /// Sets a callback for any database notices that are received from the [`TestServer`].
1198    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    /// Configures this [`ConnectBuilder`] to return the [`mz_ore::task::JoinHandle`] that is
1206    /// polling the underlying postgres connection, associated with the returned client.
1207    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    /// Returns the [`tokio_postgres::Config`] that will be used to connect.
1219    pub fn as_pg_config(&self) -> &tokio_postgres::Config {
1220        &self.pg_config
1221    }
1222}
1223
1224/// This trait enables us to either include or omit the [`mz_ore::task::JoinHandle`] in the result
1225/// of a client connection.
1226pub 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
1234/// Type parameter that denotes we __will not__ return the [`mz_ore::task::JoinHandle`] in the
1235/// result of a [`ConnectBuilder`].
1236pub 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
1247/// Type parameter that denotes we __will__ return the [`mz_ore::task::JoinHandle`] in the result of
1248/// a [`ConnectBuilder`].
1249pub 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                            // tokio_postgres::Connection docs say:
1295                            // > Return values of None or Some(Err(_)) are “terminal”; callers
1296                            // > should not invoke this method again after receiving one of those
1297                            // > values.
1298                            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
1312/// A running instance of `environmentd`, that exposes blocking/synchronous test helpers.
1313///
1314/// Note: Ideally you should use a [`TestServer`] which relies on an external runtime, e.g. the
1315/// [`tokio::test`] macro. This struct exists so we can incrementally migrate our existing tests.
1316pub struct TestServerWithRuntime {
1317    server: TestServer,
1318    runtime: Arc<Runtime>,
1319}
1320
1321impl TestServerWithRuntime {
1322    /// Returns the [`Runtime`] owned by this [`TestServerWithRuntime`].
1323    ///
1324    /// Can be used to spawn async tasks.
1325    pub fn runtime(&self) -> &Arc<Runtime> {
1326        &self.runtime
1327    }
1328
1329    /// Returns a referece to the inner running `environmentd` [`crate::Server`]`.
1330    pub fn inner(&self) -> &crate::Server {
1331        &self.server.inner
1332    }
1333
1334    /// Connect to the __public__ SQL port of the running `environmentd` server.
1335    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    /// Connect to the __internal__ SQL port of the running `environmentd` server.
1346    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    /// Enable LaunchDarkly feature flags.
1357    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            // This uses the synchronous `postgres::Client`; wrappers are async
1363            // and currently only defined for tokio-postgres clients.
1364            #[allow(clippy::disallowed_methods)]
1365            internal_client.batch_execute(query.as_str()).unwrap();
1366        }
1367    }
1368
1369    /// Disable LaunchDarkly feature flags.
1370    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            // This uses the synchronous `postgres::Client`; wrappers are async
1376            // and currently only defined for tokio-postgres clients.
1377            #[allow(clippy::disallowed_methods)]
1378            internal_client.batch_execute(query.as_str()).unwrap();
1379        }
1380    }
1381
1382    /// Return a [`postgres::Config`] for connecting to the __public__ SQL port of the running
1383    /// `environmentd` server.
1384    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    /// Return a [`postgres::Config`] for connecting to the __internal__ SQL port of the running
1396    /// `environmentd` server.
1397    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    /// Returns the metrics registry for the test server.
1433    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
1477/// Group commit will block writes until the current time has advanced. This can make
1478/// performing inserts while using deterministic time difficult. This is a helper
1479/// method to perform writes and advance the current time.
1480pub 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    // The `values` fragment is raw SQL text in test code and cannot currently
1494    // be represented as a composable `Sql` fragment.
1495    #[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    // Keep increasing `now` until the write has executed succeed. Table advancements may
1503    // have increased the global timestamp by an unknown amount.
1504    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    // `from_suffix` is a raw SQL suffix used by this test helper and cannot
1535    // currently be represented as a composable `Sql` fragment.
1536    #[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
1547/// Helper function to create a Postgres source.
1548///
1549/// IMPORTANT: Make sure to call closure that is returned at the end of the test to clean up
1550/// Postgres state.
1551///
1552/// WARNING: If multiple tests use this, and the tests are run in parallel, then make sure the test
1553/// use different postgres tables.
1554pub 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    // Create table in Postgres with publication.
1599    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    // `table_schema` is a raw schema fragment in this test helper and cannot
1614    // currently be represented as a composable `Sql` fragment.
1615    #[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    // Create postgres source in Materialize.
1646    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    // `connection_str` is a raw connection-option fragment generated for tests
1658    // and cannot currently be represented as a composable `Sql` fragment.
1659    #[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(&current_isolation),
1760        ),
1761    )
1762    .await
1763    .unwrap();
1764}
1765
1766// Initializes a websocket connection. Returns the init messages before the initial ReadyForQuery.
1767pub 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    // Wait for initial ready response.
1795    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    // Disable TLS v1.3 because `postgres` and `hyper` produce stabler error
1831    // messages with TLS v1.2.
1832    //
1833    // Briefly, in TLS v1.3, failing to present a client certificate does not
1834    // error during the TLS handshake, as it does in TLS v1.2, but on the first
1835    // attempt to read from the stream. But both `postgres` and `hyper` write a
1836    // bunch of data before attempting to read from the stream. With a failed
1837    // TLS v1.3 connection, sometimes `postgres` and `hyper` succeed in writing
1838    // out this data, and then return a nice error message on the call to read.
1839    // But sometimes the connection is closed before they write out the data,
1840    // and so they report "connection closed" before they ever call read, never
1841    // noticing the underlying SSL error.
1842    //
1843    // It's unclear who's bug this is. Is it on `hyper`/`postgres` to call read
1844    // if writing to the stream fails to see if a TLS error occured? Is it on
1845    // OpenSSL to provide a better API [1]? Is it a protocol issue that ought to
1846    // be corrected in TLS v1.4? We don't want to answer these questions, so we
1847    // just avoid TLS v1.3 for now.
1848    //
1849    // [1]: https://github.com/openssl/openssl/issues/11118
1850    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
1856/// A certificate authority for use in tests.
1857pub 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    /// Creates a new root certificate authority.
1899    pub fn new_root(name: &str) -> Result<Ca, Box<dyn Error>> {
1900        Ca::make_ca(name, None)
1901    }
1902
1903    /// Returns the path to the CA's certificate.
1904    pub fn ca_cert_path(&self) -> PathBuf {
1905        self.dir.path().join("ca.crt")
1906    }
1907
1908    /// Requests a new intermediate certificate authority.
1909    pub fn request_ca(&self, name: &str) -> Result<Ca, Box<dyn Error>> {
1910        Ca::make_ca(name, Some(self))
1911    }
1912
1913    /// Generates a certificate with the specified Common Name (CN) that is
1914    /// signed by the CA.
1915    ///
1916    /// Returns the paths to the certificate and key.
1917    pub fn request_client_cert(&self, name: &str) -> Result<(PathBuf, PathBuf), Box<dyn Error>> {
1918        self.request_cert(name, iter::empty())
1919    }
1920
1921    /// Like `request_client_cert`, but permits specifying additional IP
1922    /// addresses to attach as Subject Alternate Names.
1923    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
1960/// Sums the counter series named `name` whose labels include all of `labels`.
1961///
1962/// Returns 0 when no series matches, which is how a labelled counter reads
1963/// before its first increment.
1964pub 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}