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