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