Skip to main content

mz_environmentd/environmentd/
main.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
10//! Manages a single Materialize environment.
11//!
12//! It listens for SQL connections on port 6875 (MTRL) and for HTTP connections
13//! on port 6876.
14
15use std::ffi::CStr;
16use std::fs::File;
17use std::net::{IpAddr, SocketAddr};
18use std::path::PathBuf;
19use std::sync::Arc;
20use std::sync::LazyLock;
21use std::sync::atomic::{AtomicUsize, Ordering};
22use std::time::{Duration, Instant};
23use std::{cmp, env, iter, thread};
24
25use anyhow::{Context, bail};
26use clap::{ArgAction, Parser, ValueEnum};
27use fail::FailScenario;
28use http::header::HeaderValue;
29use ipnet::IpNet;
30use itertools::Itertools;
31use mz_adapter::ResultExt;
32use mz_adapter_types::bootstrap_builtin_cluster_config::{
33    ANALYTICS_CLUSTER_DEFAULT_REPLICATION_FACTOR, BootstrapBuiltinClusterConfig,
34    CATALOG_SERVER_CLUSTER_DEFAULT_REPLICATION_FACTOR, DEFAULT_REPLICATION_FACTOR,
35    PROBE_CLUSTER_DEFAULT_REPLICATION_FACTOR, SUPPORT_CLUSTER_DEFAULT_REPLICATION_FACTOR,
36    SYSTEM_CLUSTER_DEFAULT_REPLICATION_FACTOR,
37};
38use mz_auth::password::Password;
39use mz_aws_secrets_controller::AwsSecretsController;
40use mz_build_info::BuildInfo;
41use mz_catalog::config::ClusterReplicaSizeMap;
42use mz_cloud_resources::{AwsExternalIdPrefix, CloudResourceController};
43use mz_controller::{ControllerConfig, ReplicaHttpLocator};
44use mz_frontegg_auth::{Authenticator as FronteggAuthenticator, FronteggCliArgs};
45use mz_license_keys::{ExpirationBehavior, ValidatedLicenseKey};
46use mz_orchestrator::Orchestrator;
47use mz_orchestrator_kubernetes::{
48    KubernetesImagePullPolicy, KubernetesOrchestrator, KubernetesOrchestratorConfig,
49};
50use mz_orchestrator_process::{
51    ProcessOrchestrator, ProcessOrchestratorConfig, ProcessOrchestratorTcpProxyConfig,
52};
53use mz_orchestrator_tracing::{StaticTracingConfig, TracingCliArgs, TracingOrchestrator};
54use mz_ore::cli::{self, CliConfig, KeyValueArg};
55use mz_ore::error::ErrorExt;
56use mz_ore::metric;
57use mz_ore::metrics::{MetricsRegistry, register_runtime_metrics};
58use mz_ore::now::SYSTEM_TIME;
59use mz_ore::task::RuntimeExt;
60use mz_ore::url::SensitiveUrl;
61use mz_persist_client::PersistLocation;
62use mz_persist_client::cache::PersistClientCache;
63use mz_persist_client::cfg::PersistConfig;
64use mz_persist_client::rpc::{
65    MetricsSameProcessPubSubSender, PersistGrpcPubSubServer, PubSubClientConnection, PubSubSender,
66};
67use mz_secrets::SecretsController;
68use mz_server_core::TlsCliArgs;
69use mz_service::emit_boot_diagnostics;
70use mz_service::secrets::{SecretsControllerKind, SecretsReaderCliArgs};
71use mz_sql::catalog::EnvironmentId;
72use mz_storage_types::connections::ConnectionContext;
73use opentelemetry::trace::TraceContextExt;
74use prometheus::IntGauge;
75use tracing::{Instrument, error, info, info_span, warn};
76use tracing_opentelemetry::OpenTelemetrySpanExt;
77use url::Url;
78
79use crate::environmentd::sys;
80use crate::{BUILD_INFO, CatalogConfig, ListenerConfig, Listeners, ListenersConfig};
81
82static VERSION: LazyLock<String> = LazyLock::new(|| BUILD_INFO.human_version(None));
83static LONG_VERSION: LazyLock<String> = LazyLock::new(|| {
84    iter::once(BUILD_INFO.human_version(None))
85        .chain(build_info())
86        .join("\n")
87});
88
89/// Manages a single Materialize environment.
90#[derive(Parser, Debug)]
91#[clap(
92    name = "environmentd",
93    next_line_help = true,
94    version = VERSION.as_str(),
95    long_version = LONG_VERSION.as_str(),
96)]
97pub struct Args {
98    // === Special modes. ===
99    /// Enable unsafe features. Unsafe features are those that should never run
100    /// in production but are appropriate for testing/local development.
101    #[clap(long, env = "UNSAFE_MODE")]
102    unsafe_mode: bool,
103    /// Enables all feature flags, meant only as a tool for local development;
104    /// this should never be enabled in CI.
105    #[clap(long, env = "ALL_FEATURES")]
106    all_features: bool,
107
108    // === Connection options. ===
109    /// Path to a file containing the json-formatted configuration of our
110    /// metrics, HTTP, and sql listeners.
111    #[clap(
112        long,
113        env = "LISTENERS_CONFIG_PATH",
114        value_name = "PATH",
115        action = ArgAction::Set,
116    )]
117    listeners_config_path: PathBuf,
118    /// Password for the mz_system user.
119    #[clap(
120        long,
121        env = "EXTERNAL_LOGIN_PASSWORD_MZ_SYSTEM",
122        action = ArgAction::Set,
123    )]
124    external_login_password_mz_system: Option<Password>,
125    /// The address on which to listen for Persist PubSub connections.
126    ///
127    /// Connections to this address are not subject to encryption, authentication,
128    /// or access control. Care should be taken to not expose the listen address
129    /// to the public internet or other unauthorized parties.
130    #[clap(
131        long,
132        value_name = "HOST:PORT",
133        env = "INTERNAL_PERSIST_PUBSUB_LISTEN_ADDR",
134        default_value = "127.0.0.1:6879",
135        action = ArgAction::Set,
136    )]
137    internal_persist_pubsub_listen_addr: SocketAddr,
138    /// Enable cross-origin resource sharing (CORS) for HTTP requests from the
139    /// specified origin.
140    ///
141    /// The default allows all local connections.
142    /// "*" allows all.
143    /// "*.domain.com" allows connections from any matching subdomain.
144    ///
145    /// Wildcards in other positions (e.g., "https://*.foo.com" or "https://foo.*.com") have no effect.
146    #[structopt(long, env = "CORS_ALLOWED_ORIGIN")]
147    cors_allowed_origin: Vec<HeaderValue>,
148    /// Public CIDR which the cloud environment has configured for
149    /// egress.
150    #[clap(
151        long,
152        env = "ANNOUNCE_EGRESS_ADDRESS",
153        action = ArgAction::Append,
154        use_value_delimiter = true
155    )]
156    announce_egress_address: Vec<IpNet>,
157    /// The external host name to connect to the HTTP server of this
158    /// environment.
159    ///
160    /// Used to render absolute URLs the server publishes: webhook URLs in
161    /// notices and the system catalog, and the OAuth Protected Resource
162    /// Metadata `resource`/`resource_metadata` URLs (RFC 9728). Not used to
163    /// establish connections directly. When unset, those URLs fall back to
164    /// the request's `Host` header.
165    #[clap(long, env = "HTTP_HOST_NAME")]
166    http_host_name: Option<String>,
167    /// The URL of the Materialize console to proxy from the /internal-console
168    /// endpoint on the internal HTTP server.
169    #[clap(long, env = "INTERNAL_CONSOLE_REDIRECT_URL")]
170    internal_console_redirect_url: Option<String>,
171    /// TLS arguments.
172    #[clap(flatten)]
173    tls: TlsCliArgs,
174    /// Frontegg arguments.
175    #[clap(flatten)]
176    frontegg: FronteggCliArgs,
177    // === Orchestrator options. ===
178    /// The service orchestrator implementation to use.
179    #[structopt(long, value_enum, env = "ORCHESTRATOR")]
180    orchestrator: OrchestratorKind,
181    /// Name of a non-default Kubernetes scheduler, if any.
182    #[structopt(long, env = "ORCHESTRATOR_KUBERNETES_SCHEDULER_NAME")]
183    orchestrator_kubernetes_scheduler_name: Option<String>,
184    /// Annotations to apply to all services created by the Kubernetes orchestrator
185    /// in the form `KEY=VALUE`.
186    #[structopt(long, env = "ORCHESTRATOR_KUBERNETES_SERVICE_ANNOTATION")]
187    orchestrator_kubernetes_service_annotation: Vec<KeyValueArg<String, String>>,
188    /// Labels to apply to all services created by the Kubernetes orchestrator
189    /// in the form `KEY=VALUE`.
190    #[structopt(long, env = "ORCHESTRATOR_KUBERNETES_SERVICE_LABEL")]
191    orchestrator_kubernetes_service_label: Vec<KeyValueArg<String, String>>,
192    /// Node selector to apply to all services created by the Kubernetes
193    /// orchestrator in the form `KEY=VALUE`.
194    #[structopt(long, env = "ORCHESTRATOR_KUBERNETES_SERVICE_NODE_SELECTOR")]
195    orchestrator_kubernetes_service_node_selector: Vec<KeyValueArg<String, String>>,
196    /// Affinity to apply to all services created by the Kubernetes
197    /// orchestrator as a JSON string.
198    #[structopt(long, env = "ORCHESTRATOR_KUBERNETES_SERVICE_AFFINITY")]
199    orchestrator_kubernetes_service_affinity: Option<String>,
200    /// Tolerations to apply to all services created by the Kubernetes
201    /// orchestrator as a JSON string.
202    #[structopt(long, env = "ORCHESTRATOR_KUBERNETES_SERVICE_TOLERATIONS")]
203    orchestrator_kubernetes_service_tolerations: Option<String>,
204    /// The name of a service account to apply to all services created by the
205    /// Kubernetes orchestrator.
206    #[structopt(long, env = "ORCHESTRATOR_KUBERNETES_SERVICE_ACCOUNT")]
207    orchestrator_kubernetes_service_account: Option<String>,
208    /// The Kubernetes context to use with the Kubernetes orchestrator.
209    ///
210    /// This defaults to `minikube` to prevent disaster (e.g., connecting to a
211    /// production cluster that happens to be the active Kubernetes context.)
212    #[structopt(
213        long,
214        env = "ORCHESTRATOR_KUBERNETES_CONTEXT",
215        default_value = "minikube"
216    )]
217    orchestrator_kubernetes_context: String,
218    /// The image pull policy to use for services created by the Kubernetes
219    /// orchestrator.
220    #[structopt(
221        long,
222        env = "ORCHESTRATOR_KUBERNETES_IMAGE_PULL_POLICY",
223        default_value = "always",
224        value_enum
225    )]
226    orchestrator_kubernetes_image_pull_policy: KubernetesImagePullPolicy,
227    /// The init container for services created by the Kubernetes orchestrator.
228    #[clap(long, env = "ORCHESTRATOR_KUBERNETES_INIT_CONTAINER_IMAGE")]
229    orchestrator_kubernetes_init_container_image: Option<String>,
230    /// The Kubernetes StorageClass to use for the ephemeral volume attached to
231    /// services that request disk.
232    ///
233    /// If unspecified, the Kubernetes orchestrator will refuse to create
234    /// services that request disk.
235    #[clap(long, env = "ORCHESTRATOR_KUBERNETES_EPHEMERAL_VOLUME_CLASS")]
236    orchestrator_kubernetes_ephemeral_volume_class: Option<String>,
237    /// The optional fs group for service's pods' `securityContext`.
238    #[clap(long, env = "ORCHESTRATOR_KUBERNETES_SERVICE_FS_GROUP")]
239    orchestrator_kubernetes_service_fs_group: Option<i64>,
240    /// The prefix to prepend to all kubernetes object names.
241    #[clap(long, env = "ORCHESTRATOR_KUBERNETES_NAME_PREFIX")]
242    orchestrator_kubernetes_name_prefix: Option<String>,
243    /// Whether to enable pod metrics collection.
244    ///
245    /// Required for resource usage graphs in the console.
246    /// Requires metrics-server to be installed.
247    #[clap(long, env = "ORCHESTRATOR_KUBERNETES_DISABLE_POD_METRICS_COLLECTION")]
248    orchestrator_kubernetes_disable_pod_metrics_collection: bool,
249    /// Whether to annotate pods for prometheus service discovery.
250    #[clap(
251        long,
252        env = "ORCHESTRATOR_KUBERNETES_ENABLE_PROMETHEUS_SCRAPE_ANNOTATIONS"
253    )]
254    orchestrator_kubernetes_enable_prometheus_scrape_annotations: bool,
255    #[clap(long, env = "ORCHESTRATOR_PROCESS_WRAPPER")]
256    orchestrator_process_wrapper: Option<String>,
257    /// Where the process orchestrator should store secrets.
258    #[clap(
259        long,
260        env = "ORCHESTRATOR_PROCESS_SECRETS_DIRECTORY",
261        value_name = "PATH",
262        required_if_eq("orchestrator", "process")
263    )]
264    orchestrator_process_secrets_directory: Option<PathBuf>,
265    /// Whether the process orchestrator should handle crashes in child
266    /// processes by crashing the parent process.
267    #[clap(long, env = "ORCHESTRATOR_PROCESS_PROPAGATE_CRASHES")]
268    orchestrator_process_propagate_crashes: bool,
269    /// An IP address on which the process orchestrator should bind TCP proxies
270    /// for Unix domain sockets.
271    ///
272    /// When specified, for each named port of each created service, the process
273    /// orchestrator will bind a TCP listener to the specified address that
274    /// proxies incoming connections to the underlying Unix domain socket. The
275    /// allocated TCP port will be emitted as a tracing event.
276    ///
277    /// The primary use is live debugging the running child services via tools
278    /// that do not support Unix domain sockets (e.g., Prometheus, web
279    /// browsers).
280    #[clap(long, env = "ORCHESTRATOR_PROCESS_TCP_PROXY_LISTEN_ADDR")]
281    orchestrator_process_tcp_proxy_listen_addr: Option<IpAddr>,
282    /// A directory in which the process orchestrator should write Prometheus
283    /// scrape targets, for use with Prometheus's file-based service discovery.
284    ///
285    /// Each namespaced orchestrator will maintain a single JSON file into the
286    /// directory named `NAMESPACE.json` containing the scrape targets for all
287    /// extant services. The scrape targets will use the TCP proxy address, as
288    /// Prometheus does not support scraping over Unix domain sockets.
289    ///
290    /// This option is ignored unless
291    /// `--orchestrator-process-tcp-proxy-listen-addr` is set.
292    ///
293    /// See also: <https://prometheus.io/docs/guides/file-sd/>
294    #[clap(
295        long,
296        env = "ORCHESTRATOR_PROCESS_PROMETHEUS_SERVICE_DISCOVERY_DIRECTORY"
297    )]
298    orchestrator_process_prometheus_service_discovery_directory: Option<PathBuf>,
299    /// A scratch directory that orchestrated processes can use for ephemeral storage.
300    #[clap(
301        long,
302        env = "ORCHESTRATOR_PROCESS_SCRATCH_DIRECTORY",
303        value_name = "PATH"
304    )]
305    orchestrator_process_scratch_directory: Option<PathBuf>,
306    /// Whether to use coverage build and collect coverage information. Not to be used for
307    /// production, only testing.
308    #[structopt(long, env = "ORCHESTRATOR_KUBERNETES_COVERAGE")]
309    orchestrator_kubernetes_coverage: bool,
310    /// The secrets controller implementation to use.
311    #[structopt(
312        long,
313        value_enum,
314        env = "SECRETS_CONTROLLER",
315        default_value_ifs([
316            ("orchestrator", "kubernetes", Some("kubernetes")),
317            ("orchestrator", "process", Some("local-file"))
318        ]),
319        default_value("kubernetes"), // This shouldn't be possible, but it makes clap happy.
320    )]
321    secrets_controller: SecretsControllerKind,
322    /// The list of tags to be set on AWS Secrets Manager secrets created by the
323    /// AWS secrets controller.
324    #[clap(
325        long,
326        env = "AWS_SECRETS_CONTROLLER_TAGS",
327        action = ArgAction::Append,
328        value_delimiter = ';',
329        required_if_eq("secrets_controller", "aws-secrets-manager")
330    )]
331    aws_secrets_controller_tags: Vec<KeyValueArg<String, String>>,
332    /// The clusterd image reference to use.
333    #[structopt(
334        long,
335        env = "CLUSTERD_IMAGE",
336        required_if_eq("orchestrator", "kubernetes"),
337        default_value_if("orchestrator", "process", Some("clusterd"))
338    )]
339    clusterd_image: Option<String>,
340    /// A number representing the environment's generation.
341    ///
342    /// This is incremented to request that the new process perform a graceful
343    /// transition of power from the prior generation.
344    #[clap(long, env = "DEPLOY_GENERATION", default_value = "0")]
345    deploy_generation: u64,
346
347    /// Can be provided in place of both persist_consensus_url and
348    /// timestamp_oracle_url in order to point both at the same backend
349    #[clap(
350        long,
351        env = "METADATA_BACKEND_URL",
352        conflicts_with_all = &[
353            "persist_consensus_url",
354            "timestamp_oracle_url",
355        ],
356    )]
357    metadata_backend_url: Option<SensitiveUrl>,
358
359    /// Helm chart version for self-hosted Materialize. This version is supposed to correspond to
360    /// the Materialize (core) version. This version is displayed in addition in `SELECT
361    /// mz_version()` if set and if it differs from the Materialize (core) version (which it should
362    /// not!).
363    #[clap(long, env = "HELM_CHART_VERSION")]
364    helm_chart_version: Option<String>,
365
366    // === Storage options. ===
367    /// Where the persist library should store its blob data.
368    #[clap(long, env = "PERSIST_BLOB_URL")]
369    persist_blob_url: SensitiveUrl,
370    /// Where the persist library should perform consensus.
371    #[clap(long, env = "PERSIST_CONSENSUS_URL")]
372    persist_consensus_url: Option<SensitiveUrl>,
373    /// The Persist PubSub URL.
374    ///
375    /// This URL is passed to `clusterd` for discovery of the Persist PubSub service.
376    #[clap(
377        long,
378        env = "PERSIST_PUBSUB_URL",
379        default_value = "http://localhost:6879"
380    )]
381    persist_pubsub_url: String,
382    /// The number of worker threads created for the IsolatedRuntime used for
383    /// storage related tasks. A negative value will subtract from the number
384    /// of threads returned by [`num_cpus::get`].
385    #[clap(long, env = "PERSIST_ISOLATED_RUNTIME_THREADS")]
386    persist_isolated_runtime_threads: Option<isize>,
387    /// The interval in seconds at which to collect storage usage information.
388    #[clap(
389        long,
390        env = "STORAGE_USAGE_COLLECTION_INTERVAL",
391        value_parser = humantime::parse_duration,
392        default_value = "3600s"
393    )]
394    storage_usage_collection_interval_sec: Duration,
395    /// The period for which to retain usage records. Note that the retention
396    /// period is only evaluated at server start time, so rebooting the server
397    /// is required to discard old records.
398    #[clap(long, env = "STORAGE_USAGE_RETENTION_PERIOD", value_parser = humantime::parse_duration)]
399    storage_usage_retention_period: Option<Duration>,
400
401    // === Adapter options. ===
402    /// The PostgreSQL URL for the Postgres-backed timestamp oracle.
403    #[clap(long, env = "TIMESTAMP_ORACLE_URL", value_name = "POSTGRES_URL")]
404    timestamp_oracle_url: Option<SensitiveUrl>,
405    /// Availability zones in which storage and compute resources may be
406    /// deployed.
407    #[clap(long, env = "AVAILABILITY_ZONE", use_value_delimiter = true)]
408    availability_zone: Vec<String>,
409    /// A map from size name to resource allocations for cluster replicas.
410    #[clap(
411        long,
412        env = "CLUSTER_REPLICA_SIZES",
413        requires = "bootstrap_default_cluster_replica_size"
414    )]
415    cluster_replica_sizes: String,
416    /// An API key for Segment. Enables export of audit events to Segment.
417    #[clap(long, env = "SEGMENT_API_KEY")]
418    segment_api_key: Option<String>,
419    /// Whether the Segment client is being used on the client side
420    /// (rather than the server side).
421    ///
422    /// Enabling this causes the Segment server to record the IP address from
423    /// which the event was sent.
424    #[clap(long, env = "SEGMENT_CLIENT_SIDE")]
425    segment_client_side: bool,
426    /// Only create a dummy segment client when no segment api key is provided, only to get more
427    /// testing coverage.
428    #[clap(long, env = "TEST_ONLY_DUMMY_SEGMENT_CLIENT")]
429    test_only_dummy_segment_client: bool,
430    /// An SDK key for LaunchDarkly.
431    ///
432    /// Setting this in combination with [`Self::config_sync_loop_interval`]
433    /// will enable synchronization of LaunchDarkly features with system
434    /// configuration parameters.
435    #[clap(long, env = "LAUNCHDARKLY_SDK_KEY")]
436    launchdarkly_sdk_key: Option<String>,
437    /// Overrides the LaunchDarkly streaming, polling, and events endpoints with
438    /// a single base URL, as for a relay proxy. Primarily intended for pointing
439    /// the SDK at a mock LaunchDarkly server in tests.
440    #[clap(long, env = "LAUNCHDARKLY_BASE_URI", value_name = "URL")]
441    launchdarkly_base_uri: Option<String>,
442    /// A list of PARAM_NAME=KEY_NAME pairs from system parameter names to
443    /// LaunchDarkly feature keys.
444    ///
445    /// This is used (so far only for testing purposes) when propagating values
446    /// from the latter to the former. The identity map is assumed for absent
447    /// parameter names.
448    #[clap(
449        long,
450        env = "LAUNCHDARKLY_KEY_MAP",
451        action = ArgAction::Append,
452        value_delimiter = ';'
453    )]
454    launchdarkly_key_map: Vec<KeyValueArg<String, String>>,
455    /// The duration at which the system parameter synchronization times out during startup.
456    #[clap(
457        long,
458        env = "CONFIG_SYNC_TIMEOUT",
459        value_parser = humantime::parse_duration,
460        default_value = "30s"
461    )]
462    config_sync_timeout: Duration,
463    /// The interval in seconds at which to synchronize system parameter values.
464    ///
465    /// If this is not explicitly set, the loop that synchronizes LaunchDarkly
466    /// features with system configuration parameters will not run _even if
467    /// [`Self::launchdarkly_sdk_key`] is present_.
468    #[clap(
469        long,
470        env = "CONFIG_SYNC_LOOP_INTERVAL",
471        value_parser = humantime::parse_duration,
472    )]
473    config_sync_loop_interval: Option<Duration>,
474    /// Path to a JSON file containing system parameter values.
475    /// If specified, this file will be used instead of LaunchDarkly for configuration.
476    #[clap(long, env = "CONFIG_SYNC_FILE_PATH", value_name = "PATH")]
477    config_sync_file_path: Option<PathBuf>,
478
479    // === Bootstrap options. ===
480    #[clap(
481        long,
482        env = "ENVIRONMENT_ID",
483        value_name = "<CLOUD>-<REGION>-<ORG-ID>-<ORDINAL>"
484    )]
485    environment_id: EnvironmentId,
486    /// If set, a role with the provided name will be created with `CREATEDB`
487    /// and `CREATECLUSTER` attributes. It will also have `CREATE` privileges on
488    /// the `materialize` database, `materialize.public` schema, and
489    /// `quickstart` cluster.
490    ///
491    /// This option is meant for local development and testing to simplify the
492    /// initial process of granting attributes and privileges to some default
493    /// role.
494    #[clap(long, env = "BOOTSTRAP_ROLE")]
495    bootstrap_role: Option<String>,
496    /// The size of the default cluster replica if bootstrapping.
497    #[clap(
498        long,
499        env = "BOOTSTRAP_DEFAULT_CLUSTER_REPLICA_SIZE",
500        default_value = "scale=1,workers=1"
501    )]
502    bootstrap_default_cluster_replica_size: String,
503    /// The size of the builtin system cluster replicas if bootstrapping.
504    #[clap(
505        long,
506        env = "BOOTSTRAP_BUILTIN_SYSTEM_CLUSTER_REPLICA_SIZE",
507        default_value = "scale=1,workers=1"
508    )]
509    bootstrap_builtin_system_cluster_replica_size: String,
510    /// The size of the builtin catalog server cluster replicas if bootstrapping.
511    #[clap(
512        long,
513        env = "BOOTSTRAP_BUILTIN_CATALOG_SERVER_CLUSTER_REPLICA_SIZE",
514        default_value = "scale=1,workers=1"
515    )]
516    bootstrap_builtin_catalog_server_cluster_replica_size: String,
517    /// The size of the builtin probe cluster replicas if bootstrapping.
518    #[clap(
519        long,
520        env = "BOOTSTRAP_BUILTIN_PROBE_CLUSTER_REPLICA_SIZE",
521        default_value = "scale=1,workers=1"
522    )]
523    bootstrap_builtin_probe_cluster_replica_size: String,
524    /// The size of the builtin support cluster replicas if bootstrapping.
525    #[clap(
526        long,
527        env = "BOOTSTRAP_BUILTIN_SUPPORT_CLUSTER_REPLICA_SIZE",
528        default_value = "scale=1,workers=1"
529    )]
530    bootstrap_builtin_support_cluster_replica_size: String,
531    /// The size of the builtin analytics cluster replicas if bootstrapping.
532    #[clap(
533        long,
534        env = "BOOTSTRAP_BUILTIN_ANALYTICS_CLUSTER_REPLICA_SIZE",
535        default_value = "scale=1,workers=1"
536    )]
537    bootstrap_builtin_analytics_cluster_replica_size: String,
538    #[clap(
539        long,
540        env = "BOOTSTRAP_DEFAULT_CLUSTER_REPLICATION_FACTOR",
541        default_value = DEFAULT_REPLICATION_FACTOR.to_string(),
542        value_parser = clap::value_parser!(u32).range(0..=2)
543    )]
544    bootstrap_default_cluster_replication_factor: u32,
545    /// The replication factor of the builtin system cluster replicas if bootstrapping.
546    #[clap(
547        long,
548        env = "BOOTSTRAP_BUILTIN_SYSTEM_CLUSTER_REPLICATION_FACTOR",
549        default_value = SYSTEM_CLUSTER_DEFAULT_REPLICATION_FACTOR.to_string(),
550        value_parser = clap::value_parser!(u32).range(0..=2)
551    )]
552    bootstrap_builtin_system_cluster_replication_factor: u32,
553    /// The replication factor of the builtin catalog server cluster replicas if bootstrapping.
554    #[clap(
555        long,
556        env = "BOOTSTRAP_BUILTIN_CATALOG_SERVER_CLUSTER_REPLICATION_FACTOR",
557        default_value = CATALOG_SERVER_CLUSTER_DEFAULT_REPLICATION_FACTOR.to_string(),
558        value_parser = clap::value_parser!(u32).range(0..=2)
559    )]
560    bootstrap_builtin_catalog_server_cluster_replication_factor: u32,
561    /// The replication factor of the builtin probe cluster replicas if bootstrapping.
562    #[clap(
563        long,
564        env = "BOOTSTRAP_BUILTIN_PROBE_CLUSTER_REPLICATION_FACTOR",
565        default_value = PROBE_CLUSTER_DEFAULT_REPLICATION_FACTOR.to_string(),
566        value_parser = clap::value_parser!(u32).range(0..=2)
567    )]
568    bootstrap_builtin_probe_cluster_replication_factor: u32,
569    /// The replication factor of the builtin support cluster replicas if bootstrapping.
570    #[clap(
571        long,
572        env = "BOOTSTRAP_BUILTIN_SUPPORT_CLUSTER_REPLICATION_FACTOR",
573        default_value = SUPPORT_CLUSTER_DEFAULT_REPLICATION_FACTOR.to_string(),
574        value_parser = clap::value_parser!(u32).range(0..=2)
575    )]
576    bootstrap_builtin_support_cluster_replication_factor: u32,
577    /// The replication factor of the builtin analytics cluster replicas if bootstrapping.
578    #[clap(
579        long,
580        env = "BOOTSTRAP_BUILTIN_ANALYTICS_CLUSTER_REPLICATION_FACTOR",
581        default_value = ANALYTICS_CLUSTER_DEFAULT_REPLICATION_FACTOR.to_string(),
582        value_parser = clap::value_parser!(u32).range(0..=2)
583    )]
584    bootstrap_builtin_analytics_cluster_replication_factor: u32,
585    /// An list of NAME=VALUE pairs used to override static defaults
586    /// for system parameters.
587    #[clap(
588        long,
589        env = "SYSTEM_PARAMETER_DEFAULT",
590        action = ArgAction::Append,
591        value_delimiter = ';'
592    )]
593    system_parameter_default: Vec<KeyValueArg<String, String>>,
594    /// File containing a valid Materialize license key.
595    #[clap(long, env = "LICENSE_KEY")]
596    license_key: Option<String>,
597
598    // === AWS options. ===
599    /// The AWS account ID, which will be used to generate ARNs for
600    /// Materialize-controlled AWS resources.
601    #[clap(long, env = "AWS_ACCOUNT_ID")]
602    aws_account_id: Option<String>,
603    /// The ARN for a Materialize-controlled role to assume before assuming
604    /// a customer's requested role for an AWS connection.
605    #[clap(long, env = "AWS_CONNECTION_ROLE_ARN")]
606    aws_connection_role_arn: Option<String>,
607    /// Prefix for an external ID to be supplied to all AWS AssumeRole operations.
608    ///
609    /// Details: <https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html>
610    #[clap(long, env = "AWS_EXTERNAL_ID_PREFIX", value_name = "ID", value_parser = AwsExternalIdPrefix::new_from_cli_argument_or_environment_variable)]
611    aws_external_id_prefix: Option<AwsExternalIdPrefix>,
612    /// The list of supported AWS PrivateLink availability zone ids.
613    /// Must be zone IDs, of format e.g. "use-az1".
614    #[clap(
615        long,
616        env = "AWS_PRIVATELINK_AVAILABILITY_ZONES",
617        action = ArgAction::Append,
618        use_value_delimiter = true
619    )]
620    aws_privatelink_availability_zones: Option<Vec<String>>,
621
622    // === Tracing options. ===
623    #[clap(flatten)]
624    tracing: TracingCliArgs,
625
626    // === Testing options. ===
627    /// Forces the migration of all builtin storage collections using the
628    /// specified migration mechanism (either "evolution" or "replacement").
629    ///
630    /// This argument is meant for testing only and as the names suggests
631    /// should not be set in production.
632    #[clap(long, value_enum, requires = "unsafe_mode")]
633    unsafe_force_builtin_schema_migration: Option<String>,
634}
635
636#[derive(ValueEnum, Debug, Clone)]
637enum OrchestratorKind {
638    Kubernetes,
639    Process,
640}
641
642// TODO [Alex Hunt] move this to a shared function that can be imported by the
643// region-controller.
644fn aws_secrets_controller_prefix(env_id: &EnvironmentId) -> String {
645    format!("/user-managed/{}/", env_id)
646}
647fn aws_secrets_controller_key_alias(env_id: &EnvironmentId) -> String {
648    // TODO [Alex Hunt] move this to a shared function that can be imported by the
649    // region-controller.
650    format!("alias/customer_key_{}", env_id)
651}
652
653pub fn main() {
654    let args = cli::parse_args(CliConfig {
655        env_prefix: Some("MZ_"),
656        enable_version_flag: true,
657    });
658    if let Err(err) = run(args) {
659        panic!("environmentd: fatal: {}", err.display_with_causes());
660    }
661}
662
663fn run(mut args: Args) -> Result<(), anyhow::Error> {
664    mz_ore::panic::install_enhanced_handler();
665
666    // Pin the rustls crypto provider to aws-lc-rs. The LaunchDarkly SDK uses
667    // hyper-rustls, so building its client resolves the process-default rustls
668    // provider. The workspace also links rustls' `ring` feature (pulled by
669    // other hyper-rustls chains), and with both provider features enabled
670    // rustls cannot choose a default on its own and panics. The call is
671    // idempotent, so ignore the result.
672    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
673
674    let envd_start = Instant::now();
675
676    // Configure signal handling as soon as possible. We want signals to be
677    // handled to our liking ASAP.
678    sys::enable_sigusr2_coverage_dump()?;
679    sys::enable_termination_signal_cleanup()?;
680
681    let license_key = if let Some(license_key_file) = args.license_key {
682        let license_key_text = std::fs::read_to_string(&license_key_file)
683            .context("failed to open license key file")?;
684        let license_key = mz_license_keys::validate(license_key_text.trim())
685            .context("failed to validate license key file")?;
686        if license_key.expired {
687            let message = format!(
688                "The license key provided at {license_key_file} is expired! Please contact Materialize for assistance."
689            );
690            match license_key.expiration_behavior {
691                ExpirationBehavior::Warn | ExpirationBehavior::DisableClusterCreation => {
692                    warn!("{message}");
693                }
694                ExpirationBehavior::Disable => bail!("{message}"),
695            }
696        }
697        license_key
698    } else if matches!(args.orchestrator, OrchestratorKind::Kubernetes) {
699        bail!("--license-key is required when running in Kubernetes");
700    } else {
701        // license key checks are optional for the emulator
702        ValidatedLicenseKey::disabled()
703    };
704
705    // Configure testing options.
706    let force_builtin_schema_migration = args
707        .unsafe_force_builtin_schema_migration
708        .inspect(|_mechanism| assert!(args.unsafe_mode));
709
710    // Start Tokio runtime.
711
712    let ncpus_useful = usize::max(1, cmp::min(num_cpus::get(), num_cpus::get_physical()));
713    let runtime = Arc::new(
714        tokio::runtime::Builder::new_multi_thread()
715            .worker_threads(ncpus_useful)
716            .thread_stack_size(3 * 1024 * 1024) // 3 MiB
717            // The default thread name exceeds the Linux limit on thread name
718            // length, so pick something shorter.
719            .thread_name_fn(|| {
720                static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0);
721                let id = ATOMIC_ID.fetch_add(1, Ordering::Relaxed);
722                format!("tokio:work-{}", id)
723            })
724            .enable_all()
725            .build()?,
726    );
727
728    // Configure tracing to log the service name when using the process
729    // orchestrator, which intermingles log output from multiple services. Other
730    // orchestrators separate log output from different services.
731    args.tracing.log_prefix = if matches!(args.orchestrator, OrchestratorKind::Process) {
732        Some("environmentd".to_string())
733    } else {
734        None
735    };
736
737    let metrics_registry = MetricsRegistry::new();
738    let tracing_handle = runtime.block_on(args.tracing.configure_tracing(
739        StaticTracingConfig {
740            service_name: "environmentd",
741            build_info: BUILD_INFO,
742        },
743        metrics_registry.clone(),
744    ))?;
745    register_runtime_metrics("main", runtime.metrics(), &metrics_registry);
746
747    let span = tracing::info_span!("environmentd::run").entered();
748
749    info!("startup: envd init: beginning");
750    info!("startup: envd init: preamble beginning");
751
752    let metrics = Metrics::register_into(&metrics_registry, BUILD_INFO);
753
754    runtime.block_on(mz_alloc::register_metrics_into(&metrics_registry));
755    runtime.block_on(mz_metrics::register_metrics_into(
756        &metrics_registry,
757        mz_dyncfgs::all_dyncfgs(),
758    ));
759
760    // Initialize fail crate for failpoint support
761    let _failpoint_scenario = FailScenario::setup();
762
763    // Configure connections.
764    let tls = args.tls.into_config()?;
765    let frontegg_oauth_issuer_url = args.frontegg.oauth_issuer_url().map(str::to_string);
766    let frontegg = FronteggAuthenticator::from_args(args.frontegg, &metrics_registry)?;
767    let listeners_config: ListenersConfig = {
768        let f = File::open(args.listeners_config_path)?;
769        serde_json::from_reader(f)?
770    };
771
772    for (_, listener) in &listeners_config.sql {
773        listener
774            .validate()
775            .map_err(|e| anyhow::anyhow!("invalid SQL listener: {}", e))?;
776    }
777
778    for (_, listener) in &listeners_config.http {
779        listener
780            .validate()
781            .map_err(|e| anyhow::anyhow!("invalid HTTP listener: {}", e))?;
782    }
783
784    // Configure CORS.
785    let allowed_origins = if !args.cors_allowed_origin.is_empty() {
786        args.cors_allowed_origin
787    } else {
788        let mut allowed_origins = Vec::with_capacity(listeners_config.http.len() * 6);
789        for (_, listener) in &listeners_config.http {
790            let port = listener.addr().port();
791            allowed_origins.extend([
792                HeaderValue::from_str(&format!("http://localhost:{}", port)).unwrap(),
793                HeaderValue::from_str(&format!("http://127.0.0.1:{}", port)).unwrap(),
794                HeaderValue::from_str(&format!("http://[::1]:{}", port)).unwrap(),
795                HeaderValue::from_str(&format!("https://localhost:{}", port)).unwrap(),
796                HeaderValue::from_str(&format!("https://127.0.0.1:{}", port)).unwrap(),
797                HeaderValue::from_str(&format!("https://[::1]:{}", port)).unwrap(),
798            ])
799        }
800        allowed_origins
801    };
802    let cors_allowed_origin = mz_http_util::build_cors_allowed_origin(&allowed_origins);
803    let cors_allowed_origin_list = allowed_origins.clone();
804
805    // Configure controller.
806    let entered = info_span!("environmentd::configure_controller").entered();
807    let (orchestrator, secrets_controller, cloud_resource_controller): (
808        Arc<dyn Orchestrator>,
809        Arc<dyn SecretsController>,
810        Option<Arc<dyn CloudResourceController>>,
811    ) = match args.orchestrator {
812        OrchestratorKind::Kubernetes => {
813            if args.orchestrator_process_scratch_directory.is_some() {
814                bail!(
815                    "--orchestrator-process-scratch-directory is \
816                      not currently usable with the kubernetes orchestrator"
817                );
818            }
819
820            let orchestrator = Arc::new(
821                runtime
822                    .block_on(KubernetesOrchestrator::new(KubernetesOrchestratorConfig {
823                        context: args.orchestrator_kubernetes_context.clone(),
824                        scheduler_name: args.orchestrator_kubernetes_scheduler_name,
825                        service_annotations: args
826                            .orchestrator_kubernetes_service_annotation
827                            .into_iter()
828                            .map(|l| (l.key, l.value))
829                            .collect(),
830                        service_labels: args
831                            .orchestrator_kubernetes_service_label
832                            .into_iter()
833                            .map(|l| (l.key, l.value))
834                            .collect(),
835                        service_node_selector: args
836                            .orchestrator_kubernetes_service_node_selector
837                            .into_iter()
838                            .map(|l| (l.key, l.value))
839                            .collect(),
840                        service_affinity: args.orchestrator_kubernetes_service_affinity,
841                        service_tolerations: args.orchestrator_kubernetes_service_tolerations,
842                        service_account: args.orchestrator_kubernetes_service_account,
843                        image_pull_policy: args.orchestrator_kubernetes_image_pull_policy,
844                        aws_external_id_prefix: args.aws_external_id_prefix.clone(),
845                        coverage: args.orchestrator_kubernetes_coverage,
846                        ephemeral_volume_storage_class: args
847                            .orchestrator_kubernetes_ephemeral_volume_class
848                            .clone(),
849                        service_fs_group: args.orchestrator_kubernetes_service_fs_group.clone(),
850                        name_prefix: args.orchestrator_kubernetes_name_prefix.clone(),
851                        collect_pod_metrics: !args
852                            .orchestrator_kubernetes_disable_pod_metrics_collection,
853                        enable_prometheus_scrape_annotations: args
854                            .orchestrator_kubernetes_enable_prometheus_scrape_annotations,
855                    }))
856                    .context("creating kubernetes orchestrator")?,
857            );
858            let secrets_controller: Arc<dyn SecretsController> = match args.secrets_controller {
859                SecretsControllerKind::Kubernetes => {
860                    let sc = Arc::clone(&orchestrator);
861                    let sc: Arc<dyn SecretsController> = sc;
862                    sc
863                }
864                SecretsControllerKind::AwsSecretsManager => {
865                    Arc::new(
866                        runtime.block_on(AwsSecretsController::new(
867                            // TODO [Alex Hunt] move this to a shared function that can be imported by the
868                            // region-controller.
869                            &aws_secrets_controller_prefix(&args.environment_id),
870                            &aws_secrets_controller_key_alias(&args.environment_id),
871                            args.aws_secrets_controller_tags
872                                .into_iter()
873                                .map(|tag| (tag.key, tag.value))
874                                .collect(),
875                        )),
876                    )
877                }
878                SecretsControllerKind::LocalFile => bail!(
879                    "SecretsControllerKind::LocalFile is not compatible with Orchestrator::Kubernetes."
880                ),
881            };
882            let cloud_resource_controller = Arc::clone(&orchestrator);
883            (
884                orchestrator,
885                secrets_controller,
886                Some(cloud_resource_controller),
887            )
888        }
889        OrchestratorKind::Process => {
890            if args
891                .orchestrator_kubernetes_ephemeral_volume_class
892                .is_some()
893            {
894                bail!(
895                    "--orchestrator-kubernetes-ephemeral-volume-class is \
896                      not usable with the process orchestrator"
897                );
898            }
899            let orchestrator = Arc::new(
900                runtime
901                    .block_on(ProcessOrchestrator::new(ProcessOrchestratorConfig {
902                        // Look for binaries in the same directory as the
903                        // running binary. When running via `cargo run`, this
904                        // means that debug binaries look for other debug
905                        // binaries and release binaries look for other release
906                        // binaries.
907                        image_dir: env::current_exe()?.parent().unwrap().to_path_buf(),
908                        suppress_output: false,
909                        environment_id: args.environment_id.to_string(),
910                        secrets_dir: args
911                            .orchestrator_process_secrets_directory
912                            .clone()
913                            .expect("clap enforced"),
914                        command_wrapper: args
915                            .orchestrator_process_wrapper
916                            .map_or(Ok(vec![]), |s| shell_words::split(&s))?,
917                        propagate_crashes: args.orchestrator_process_propagate_crashes,
918                        tcp_proxy: args.orchestrator_process_tcp_proxy_listen_addr.map(
919                            |listen_addr| ProcessOrchestratorTcpProxyConfig {
920                                listen_addr,
921                                prometheus_service_discovery_dir: args
922                                    .orchestrator_process_prometheus_service_discovery_directory,
923                            },
924                        ),
925                        scratch_directory: args
926                            .orchestrator_process_scratch_directory
927                            .expect("process orchestrator requires scratch directory"),
928                    }))
929                    .context("creating process orchestrator")?,
930            );
931            let secrets_controller: Arc<dyn SecretsController> = match args.secrets_controller {
932                SecretsControllerKind::Kubernetes => bail!(
933                    "SecretsControllerKind::Kubernetes is not compatible with Orchestrator::Process."
934                ),
935                SecretsControllerKind::AwsSecretsManager => Arc::new(
936                    runtime.block_on(AwsSecretsController::new(
937                        &aws_secrets_controller_prefix(&args.environment_id),
938                        &aws_secrets_controller_key_alias(&args.environment_id),
939                        args.aws_secrets_controller_tags
940                            .into_iter()
941                            .map(|tag| (tag.key, tag.value))
942                            .collect(),
943                    )),
944                ),
945                SecretsControllerKind::LocalFile => {
946                    let sc = Arc::clone(&orchestrator);
947                    let sc: Arc<dyn SecretsController> = sc;
948                    sc
949                }
950            };
951            (orchestrator, secrets_controller, None)
952        }
953    };
954    drop(entered);
955    let cloud_resource_reader = cloud_resource_controller.as_ref().map(|c| c.reader());
956    let secrets_reader = secrets_controller.reader();
957    let now = SYSTEM_TIME.clone();
958
959    let mut persist_config =
960        PersistConfig::new(&BUILD_INFO, now.clone(), mz_dyncfgs::all_dyncfgs());
961    // Start with compaction disabled, later enable it if we're not in read-only mode.
962    persist_config.disable_compaction();
963
964    let persist_pubsub_server = PersistGrpcPubSubServer::new(&persist_config, &metrics_registry);
965    let persist_pubsub_client = persist_pubsub_server.new_same_process_connection();
966
967    match args.persist_isolated_runtime_threads {
968        // Use the default.
969        None | Some(0) => (),
970        Some(x @ ..=-1) => {
971            let threads = num_cpus::get().saturating_add_signed(x).max(1);
972            persist_config.isolated_runtime_worker_threads = threads;
973        }
974        Some(x @ 1..) => {
975            let threads = usize::try_from(x).expect("pattern matched a positive value");
976            persist_config.isolated_runtime_worker_threads = threads;
977        }
978    };
979
980    let _server = runtime.spawn_named(
981        || "persist::rpc::server",
982        async move {
983            info!(
984                "listening for Persist PubSub connections on {}",
985                args.internal_persist_pubsub_listen_addr
986            );
987            // Intentionally do not bubble up errors here, we don't want to take
988            // down environmentd if there are any issues with the pubsub server.
989            let res = persist_pubsub_server
990                .serve(args.internal_persist_pubsub_listen_addr)
991                .await;
992            error!("Persist Pubsub server exited {:?}", res);
993        }
994        .instrument(tracing::info_span!("persist::rpc::server")),
995    );
996
997    let persist_clients = {
998        // PersistClientCache may spawn tasks, so run within a tokio runtime context
999        let _tokio_guard = runtime.enter();
1000        PersistClientCache::new(persist_config, &metrics_registry, |cfg, metrics| {
1001            let sender: Arc<dyn PubSubSender> = Arc::new(MetricsSameProcessPubSubSender::new(
1002                cfg,
1003                persist_pubsub_client.sender,
1004                metrics,
1005            ));
1006            PubSubClientConnection::new(sender, persist_pubsub_client.receiver)
1007        })
1008    };
1009
1010    let consensus_uri = args.persist_consensus_url.unwrap_or_else(|| {
1011        args.metadata_backend_url
1012            .as_ref()
1013            .map(|metadata_backend_url| {
1014                SensitiveUrl(
1015                    Url::parse_with_params(
1016                        metadata_backend_url.0.as_ref(),
1017                        &[("options", "--search_path=consensus")],
1018                    )
1019                    .unwrap(),
1020                )
1021            })
1022            .expect("either --persist-consensus-url or --metadata-backend-url must be provided")
1023    });
1024    let timestamp_oracle_url = args.timestamp_oracle_url.or_else(|| {
1025        args.metadata_backend_url
1026            .as_ref()
1027            .map(|metadata_backend_url| {
1028                SensitiveUrl(
1029                    Url::parse_with_params(
1030                        metadata_backend_url.0.as_ref(),
1031                        &[("options", "--search_path=tsoracle")],
1032                    )
1033                    .unwrap(),
1034                )
1035            })
1036    });
1037
1038    let persist_clients = Arc::new(persist_clients);
1039    let system_dyncfgs = Arc::clone(&persist_clients.cfg().configs);
1040    let connection_context = ConnectionContext::from_cli_args(
1041        args.environment_id.to_string(),
1042        &args.tracing.startup_log_filter,
1043        args.aws_external_id_prefix,
1044        args.aws_connection_role_arn,
1045        secrets_reader,
1046        cloud_resource_reader,
1047    );
1048    let orchestrator = Arc::new(TracingOrchestrator::new(orchestrator, args.tracing.clone()));
1049    let replica_http_locator = Arc::new(ReplicaHttpLocator::default());
1050    let controller = ControllerConfig {
1051        build_info: &BUILD_INFO,
1052        orchestrator,
1053        persist_location: PersistLocation {
1054            blob_uri: args.persist_blob_url,
1055            consensus_uri,
1056        },
1057        persist_clients: Arc::clone(&persist_clients),
1058        clusterd_image: args.clusterd_image.expect("clap enforced"),
1059        init_container_image: args.orchestrator_kubernetes_init_container_image,
1060        deploy_generation: args.deploy_generation,
1061        now: SYSTEM_TIME.clone(),
1062        metrics_registry: metrics_registry.clone(),
1063        persist_pubsub_url: args.persist_pubsub_url,
1064        connection_context,
1065        // When serialized to args in the controller, only the relevant flags will be passed
1066        // through, so we just set all of them
1067        secrets_args: SecretsReaderCliArgs {
1068            secrets_reader: args.secrets_controller,
1069            secrets_reader_local_file_dir: args.orchestrator_process_secrets_directory,
1070            secrets_reader_kubernetes_context: Some(args.orchestrator_kubernetes_context),
1071            secrets_reader_aws_prefix: Some(aws_secrets_controller_prefix(&args.environment_id)),
1072            secrets_reader_name_prefix: args.orchestrator_kubernetes_name_prefix.clone(),
1073        },
1074        replica_http_locator: Arc::clone(&replica_http_locator),
1075    };
1076
1077    let cluster_replica_sizes = ClusterReplicaSizeMap::parse_from_str(
1078        &args.cluster_replica_sizes,
1079        !license_key.allow_credit_consumption_override,
1080    )
1081    .context("parsing replica size map")?;
1082
1083    emit_boot_diagnostics!(&BUILD_INFO);
1084    sys::adjust_rlimits();
1085
1086    info!(
1087        "startup: envd init: preamble complete in {:?}",
1088        envd_start.elapsed()
1089    );
1090
1091    let serve_start = Instant::now();
1092    info!("startup: envd init: serving beginning");
1093    let server = runtime.block_on(async {
1094        let listeners = Listeners::bind(listeners_config).await?;
1095        let catalog_config = CatalogConfig {
1096            persist_clients,
1097            metrics: Arc::new(mz_catalog::durable::Metrics::new(&metrics_registry)),
1098        };
1099        let server = listeners
1100            .serve(crate::Config {
1101                // Special modes.
1102                unsafe_mode: args.unsafe_mode,
1103                all_features: args.all_features,
1104                // Connection options.
1105                tls,
1106                tls_reload_certs: mz_server_core::default_cert_reload_ticker(),
1107                external_login_password_mz_system: args.external_login_password_mz_system,
1108                frontegg,
1109                frontegg_oauth_issuer_url,
1110                cors_allowed_origin,
1111                cors_allowed_origin_list,
1112                egress_addresses: args.announce_egress_address,
1113                http_host_name: args.http_host_name,
1114                internal_console_redirect_url: args.internal_console_redirect_url,
1115                // Controller options.
1116                controller,
1117                secrets_controller,
1118                cloud_resource_controller,
1119                system_dyncfgs,
1120                // Storage options.
1121                storage_usage_collection_interval: args.storage_usage_collection_interval_sec,
1122                storage_usage_retention_period: args.storage_usage_retention_period,
1123                // Adapter options.
1124                catalog_config,
1125                availability_zones: args.availability_zone,
1126                cluster_replica_sizes,
1127                timestamp_oracle_url,
1128                segment_api_key: args.segment_api_key,
1129                segment_client_side: args.segment_client_side,
1130                test_only_dummy_segment_client: args.test_only_dummy_segment_client,
1131                launchdarkly_sdk_key: args.launchdarkly_sdk_key,
1132                launchdarkly_base_uri: args.launchdarkly_base_uri,
1133                launchdarkly_key_map: args
1134                    .launchdarkly_key_map
1135                    .into_iter()
1136                    .map(|kv| (kv.key, kv.value))
1137                    .collect(),
1138                config_sync_timeout: args.config_sync_timeout,
1139                config_sync_loop_interval: args.config_sync_loop_interval,
1140                config_sync_file_path: args.config_sync_file_path,
1141
1142                // Bootstrap options.
1143                environment_id: args.environment_id,
1144                bootstrap_role: args.bootstrap_role,
1145                bootstrap_default_cluster_replica_size: args.bootstrap_default_cluster_replica_size,
1146                bootstrap_default_cluster_replication_factor: args
1147                    .bootstrap_default_cluster_replication_factor,
1148                bootstrap_builtin_system_cluster_config: BootstrapBuiltinClusterConfig {
1149                    size: args.bootstrap_builtin_system_cluster_replica_size,
1150                    replication_factor: args.bootstrap_builtin_system_cluster_replication_factor,
1151                },
1152                bootstrap_builtin_catalog_server_cluster_config: BootstrapBuiltinClusterConfig {
1153                    size: args.bootstrap_builtin_catalog_server_cluster_replica_size,
1154                    replication_factor: args
1155                        .bootstrap_builtin_catalog_server_cluster_replication_factor,
1156                },
1157                bootstrap_builtin_probe_cluster_config: BootstrapBuiltinClusterConfig {
1158                    size: args.bootstrap_builtin_probe_cluster_replica_size,
1159                    replication_factor: args.bootstrap_builtin_probe_cluster_replication_factor,
1160                },
1161                bootstrap_builtin_support_cluster_config: BootstrapBuiltinClusterConfig {
1162                    size: args.bootstrap_builtin_support_cluster_replica_size,
1163                    replication_factor: args.bootstrap_builtin_support_cluster_replication_factor,
1164                },
1165                bootstrap_builtin_analytics_cluster_config: BootstrapBuiltinClusterConfig {
1166                    size: args.bootstrap_builtin_analytics_cluster_replica_size,
1167                    replication_factor: args.bootstrap_builtin_analytics_cluster_replication_factor,
1168                },
1169                system_parameter_defaults: args
1170                    .system_parameter_default
1171                    .into_iter()
1172                    .map(|kv| (kv.key, kv.value))
1173                    .collect(),
1174                helm_chart_version: args.helm_chart_version.clone(),
1175                license_key,
1176                // AWS options.
1177                aws_account_id: args.aws_account_id,
1178                aws_privatelink_availability_zones: args.aws_privatelink_availability_zones,
1179                // Observability options.
1180                metrics_registry,
1181                tracing_handle,
1182                // Testing options.
1183                now,
1184                force_builtin_schema_migration,
1185            })
1186            .await
1187            .maybe_terminate("booting server")?;
1188        Ok::<_, anyhow::Error>(server)
1189    })?;
1190    info!(
1191        "startup: envd init: serving complete in {:?}",
1192        serve_start.elapsed()
1193    );
1194
1195    let start_duration = envd_start.elapsed();
1196    metrics
1197        .start_time_environmentd
1198        .set(start_duration.as_millis().try_into().expect("must fit"));
1199    let span = span.exit();
1200    let id = span.context().span().span_context().trace_id();
1201    drop(span);
1202
1203    info!("startup: envd init: complete in {start_duration:?}");
1204
1205    println!(
1206        "environmentd {} listening...",
1207        BUILD_INFO.human_version(args.helm_chart_version)
1208    );
1209    for (name, handle) in &server.sql_listener_handles {
1210        println!("{} SQL address: {}", name, handle.local_addr);
1211    }
1212    for (name, handle) in &server.http_listener_handles {
1213        println!("{} HTTP address: {}", name, handle.local_addr);
1214    }
1215    // TODO move persist pubsub address like metrics address?
1216    println!(
1217        " Internal Persist PubSub address: {}",
1218        args.internal_persist_pubsub_listen_addr
1219    );
1220
1221    println!(" Root trace ID: {id}");
1222
1223    // Block forever.
1224    loop {
1225        thread::park();
1226    }
1227}
1228
1229fn build_info() -> Vec<String> {
1230    let openssl_version =
1231        unsafe { CStr::from_ptr(openssl_sys::OpenSSL_version(openssl_sys::OPENSSL_VERSION)) };
1232    let rdkafka_version = unsafe { CStr::from_ptr(rdkafka_sys::bindings::rd_kafka_version_str()) };
1233    vec![
1234        openssl_version.to_string_lossy().into_owned(),
1235        format!("librdkafka v{}", rdkafka_version.to_string_lossy()),
1236    ]
1237}
1238
1239#[derive(Debug, Clone)]
1240struct Metrics {
1241    pub start_time_environmentd: IntGauge,
1242}
1243
1244impl Metrics {
1245    pub fn register_into(registry: &MetricsRegistry, build_info: BuildInfo) -> Metrics {
1246        Metrics {
1247            start_time_environmentd: registry.register(metric!(
1248                name: "mz_start_time_environmentd",
1249                help: "Time in milliseconds from environmentd start until the adapter is ready.",
1250                const_labels: {
1251                    "version" => build_info.version,
1252                    "build_type" => if cfg!(release) { "release" } else { "debug" }
1253                },
1254            )),
1255        }
1256    }
1257}