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