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