1use std::{
11 collections::BTreeMap,
12 net::{IpAddr, Ipv4Addr, SocketAddr},
13 sync::LazyLock,
14 time::Duration,
15};
16
17use k8s_openapi::{
18 api::{
19 apps::v1::{StatefulSet, StatefulSetSpec},
20 core::v1::{
21 Capabilities, ConfigMap, ConfigMapVolumeSource, Container, ContainerPort, EnvVar,
22 EnvVarSource, KeyToPath, PodSecurityContext, PodSpec, PodTemplateSpec, Probe,
23 SeccompProfile, Secret, SecretKeySelector, SecretVolumeSource, SecurityContext,
24 Service, ServicePort, ServiceSpec, TCPSocketAction, Toleration, Volume, VolumeMount,
25 },
26 },
27 apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString},
28};
29use kube::{Api, Client, ResourceExt, api::ObjectMeta, runtime::controller::Action};
30use maplit::btreemap;
31use mz_server_core::listeners::{
32 AllowedRoles, AuthenticatorKind, BaseListenerConfig, RouteGroup, SqlListenerConfig,
33 VersionedListenersConfig, v0_147_0, v26_32_0,
34};
35use reqwest::{Client as HttpClient, StatusCode};
36use semver::{BuildMetadata, Prerelease, Version};
37use serde::{Deserialize, Serialize};
38use sha2::{Digest, Sha256};
39use tracing::{error, trace};
40
41use super::Error;
42use super::matching_image_from_environmentd_image_ref;
43use crate::k8s::{apply_resource, delete_resource, get_resource};
44use crate::tls::issuer_ref_defined;
45use mz_cloud_provider::CloudProvider;
46use mz_cloud_resources::crd::materialize::v1alpha1::Materialize;
47use mz_cloud_resources::crd::{ManagedResource, recommended_k8s_labels};
48use mz_ore::instrument;
49
50static V140_DEV0: LazyLock<Version> = LazyLock::new(|| Version {
51 major: 0,
52 minor: 140,
53 patch: 0,
54 pre: Prerelease::new("dev.0").expect("dev.0 is valid prerelease"),
55 build: BuildMetadata::new("").expect("empty string is valid buildmetadata"),
56});
57const V143: Version = Version::new(0, 143, 0);
58const V144: Version = Version::new(0, 144, 0);
59static V147_DEV0: LazyLock<Version> = LazyLock::new(|| Version {
60 major: 0,
61 minor: 147,
62 patch: 0,
63 pre: Prerelease::new("dev.0").expect("dev.0 is valid prerelease"),
64 build: BuildMetadata::new("").expect("empty string is valid buildmetadata"),
65});
66const V153: Version = Version::new(0, 153, 0);
67static V154_DEV0: LazyLock<Version> = LazyLock::new(|| Version {
68 major: 0,
69 minor: 154,
70 patch: 0,
71 pre: Prerelease::new("").expect("dev.0 is valid prerelease"),
72 build: BuildMetadata::new("").expect("empty string is valid buildmetadata"),
73});
74pub const V161: Version = Version::new(0, 161, 0);
75
76static V26_42_0: LazyLock<Version> = LazyLock::new(|| Version {
81 major: 26,
82 minor: 42,
83 patch: 0,
84 pre: Prerelease::new("dev.0").expect("dev.0 is valid prerelease"),
85 build: BuildMetadata::new("").expect("empty string is valid buildmetadata"),
86});
87
88static V26_1_0: LazyLock<Version> = LazyLock::new(|| Version {
89 major: 26,
90 minor: 1,
91 patch: 0,
92 pre: Prerelease::new("dev.0").expect("dev.0 is valid prerelease"),
93 build: BuildMetadata::new("").expect("empty string is valid buildmetadata"),
94});
95
96static PER_ROUTE_GROUP_ROLES_VERSION: LazyLock<Version> = LazyLock::new(|| Version {
100 major: 26,
101 minor: 32,
102 patch: 0,
103 pre: Prerelease::new("dev.0").expect("dev.0 is valid prerelease"),
104 build: BuildMetadata::new("").expect("empty string is valid buildmetadata"),
105});
106
107#[derive(
112 Debug,
113 Serialize,
114 Deserialize,
115 Clone,
116 Copy,
117 PartialEq,
118 Eq,
119 PartialOrd,
120 Ord
121)]
122pub enum DeploymentStatus {
123 Initializing,
126 ReadyToPromote,
128 Promoting,
130 IsLeader,
132}
133
134#[derive(
135 Debug,
136 Serialize,
137 Deserialize,
138 Clone,
139 Copy,
140 PartialEq,
141 Eq,
142 PartialOrd,
143 Ord
144)]
145pub struct GetLeaderStatusResponse {
146 status: DeploymentStatus,
147}
148
149#[derive(Deserialize, Serialize)]
150pub struct LoginCredentials {
151 username: String,
152 password: String,
154}
155
156#[derive(Debug, Serialize)]
157pub struct ConnectionInfo {
158 pub environmentd_url: String,
159 pub mz_system_secret_name: Option<String>,
160 pub listeners_configmap: ConfigMap,
161}
162
163#[derive(Debug, Serialize)]
164pub struct Resources {
165 pub generation: u64,
166 pub public_service: Box<Service>,
167 pub generation_service: Box<Service>,
168 pub persist_pubsub_service: Box<Service>,
169 pub environmentd_statefulset: Box<StatefulSet>,
170 pub connection_info: Box<ConnectionInfo>,
171}
172
173impl Resources {
174 pub fn new(config: &super::Config, mz: &Materialize, generation: u64) -> Self {
175 let public_service = Box::new(create_public_service_object(config, mz, generation));
176 let generation_service = Box::new(create_generation_service_object(config, mz, generation));
177 let persist_pubsub_service =
178 Box::new(create_persist_pubsub_service(config, mz, generation));
179 let environmentd_statefulset = Box::new(create_environmentd_statefulset_object(
180 config, mz, generation,
181 ));
182 let connection_info = Box::new(create_connection_info(config, mz, generation));
183
184 Self {
185 generation,
186 public_service,
187 generation_service,
188 persist_pubsub_service,
189 environmentd_statefulset,
190 connection_info,
191 }
192 }
193
194 #[instrument]
195 pub async fn apply(
196 &self,
197 client: &Client,
198 force_promote: bool,
199 namespace: &str,
200 ) -> Result<Option<Action>, Error> {
201 let service_api: Api<Service> = Api::namespaced(client.clone(), namespace);
202 let statefulset_api: Api<StatefulSet> = Api::namespaced(client.clone(), namespace);
203 let configmap_api: Api<ConfigMap> = Api::namespaced(client.clone(), namespace);
204
205 trace!("applying environmentd per-generation service");
206 apply_resource(&service_api, &*self.generation_service).await?;
207
208 trace!("creating persist pubsub service");
209 apply_resource(&service_api, &*self.persist_pubsub_service).await?;
210
211 trace!("applying listeners configmap");
212 apply_resource(&configmap_api, &self.connection_info.listeners_configmap).await?;
213
214 trace!("creating new environmentd statefulset");
215 apply_resource(&statefulset_api, &*self.environmentd_statefulset).await?;
216
217 let retry_action = Action::requeue(Duration::from_secs(rand::random_range(5..10)));
218
219 let statefulset = get_resource(
220 &statefulset_api,
221 &self.environmentd_statefulset.name_unchecked(),
222 )
223 .await?;
224 if statefulset
225 .and_then(|statefulset| statefulset.status)
226 .and_then(|status| status.ready_replicas)
227 .unwrap_or(0)
228 == 0
229 {
230 trace!("environmentd statefulset is not ready yet...");
231 return Ok(Some(retry_action));
232 }
233
234 let Some(http_client) = self.get_http_client(client.clone(), namespace).await else {
235 return Ok(Some(retry_action));
236 };
237 let status_url = reqwest::Url::parse(&format!(
238 "{}/api/leader/status",
239 self.connection_info.environmentd_url,
240 ))
241 .unwrap();
242
243 match http_client.get(status_url.clone()).send().await {
244 Ok(response) => {
245 let response: GetLeaderStatusResponse = match response.error_for_status() {
246 Ok(response) => response.json().await?,
247 Err(e) => {
248 trace!("failed to get status of environmentd, retrying... ({e})");
249 return Ok(Some(retry_action));
250 }
251 };
252 if force_promote {
253 trace!("skipping cluster catchup");
254 let skip_catchup_url = reqwest::Url::parse(&format!(
255 "{}/api/leader/skip-catchup",
256 self.connection_info.environmentd_url,
257 ))
258 .unwrap();
259 let response = http_client.post(skip_catchup_url).send().await?;
260 if response.status() == StatusCode::BAD_REQUEST {
261 let err: SkipCatchupError = response.json().await?;
262 return Err(
263 anyhow::anyhow!("failed to skip catchup: {}", err.message).into()
264 );
265 }
266 } else {
267 match response.status {
268 DeploymentStatus::Initializing => {
269 trace!("environmentd is still initializing, retrying...");
270 return Ok(Some(retry_action));
271 }
272 DeploymentStatus::ReadyToPromote
273 | DeploymentStatus::Promoting
274 | DeploymentStatus::IsLeader => trace!("environmentd is ready"),
275 }
276 }
277 }
278 Err(e) => {
279 trace!("failed to connect to environmentd, retrying... ({e})");
280 return Ok(Some(retry_action));
281 }
282 }
283
284 Ok(None)
285 }
286
287 #[instrument]
288 async fn get_http_client(&self, client: Client, namespace: &str) -> Option<HttpClient> {
289 let secret_api: Api<Secret> = Api::namespaced(client.clone(), namespace);
290 Some(match &self.connection_info.mz_system_secret_name {
291 Some(mz_system_secret_name) => {
292 let http_client = reqwest::Client::builder()
293 .timeout(std::time::Duration::from_secs(10))
294 .cookie_store(true)
295 .danger_accept_invalid_certs(true)
297 .build()
298 .unwrap();
299
300 let secret = secret_api
301 .get(mz_system_secret_name)
302 .await
303 .map_err(|e| {
304 error!("Failed to get backend secret: {:?}", e);
305 e
306 })
307 .ok()?;
308 if let Some(data) = secret.data {
309 if let Some(password) = data.get("external_login_password_mz_system").cloned() {
310 let password = String::from_utf8_lossy(&password.0).to_string();
311 let login_url = reqwest::Url::parse(&format!(
312 "{}/api/login",
313 self.connection_info.environmentd_url,
314 ))
315 .unwrap();
316 match http_client
317 .post(login_url)
318 .body(
319 serde_json::to_string(&LoginCredentials {
320 username: "mz_system".to_owned(),
321 password,
322 })
323 .expect(
324 "Serializing a simple struct with utf8 strings doesn't fail.",
325 ),
326 )
327 .header("Content-Type", "application/json")
328 .send()
329 .await
330 {
331 Ok(response) => {
332 if let Err(e) = response.error_for_status() {
333 trace!("failed to login to environmentd, retrying... ({e})");
334 return None;
335 }
336 }
337 Err(e) => {
338 trace!("failed to connect to environmentd, retrying... ({e})");
339 return None;
340 }
341 };
342 }
343 };
344 http_client
345 }
346 None => reqwest::Client::builder()
347 .timeout(std::time::Duration::from_secs(10))
348 .build()
349 .unwrap(),
350 })
351 }
352
353 #[instrument]
354 pub async fn promote_services(
355 &self,
356 client: &Client,
357 namespace: &str,
358 ) -> Result<Option<Action>, Error> {
359 let service_api: Api<Service> = Api::namespaced(client.clone(), namespace);
360 let retry_action = Action::requeue(Duration::from_secs(rand::random_range(5..10)));
361
362 let promote_url = reqwest::Url::parse(&format!(
363 "{}/api/leader/promote",
364 self.connection_info.environmentd_url,
365 ))
366 .unwrap();
367
368 let Some(http_client) = self.get_http_client(client.clone(), namespace).await else {
369 return Ok(Some(retry_action));
370 };
371
372 trace!("promoting new environmentd to leader");
373 let response = http_client.post(promote_url).send().await?;
374 let response: BecomeLeaderResponse = response.error_for_status()?.json().await?;
375 if let BecomeLeaderResult::Failure { message } = response.result {
376 return Err(Error::Anyhow(anyhow::anyhow!(
377 "failed to promote new environmentd: {message}"
378 )));
379 }
380
381 let status_url = reqwest::Url::parse(&format!(
393 "{}/api/leader/status",
394 self.connection_info.environmentd_url,
395 ))
396 .unwrap();
397 match http_client.get(status_url.clone()).send().await {
398 Ok(response) => {
399 let response: GetLeaderStatusResponse = response.json().await?;
400 if response.status != DeploymentStatus::IsLeader {
401 trace!(
402 "environmentd is still promoting (status: {:?}), retrying...",
403 response.status
404 );
405 return Ok(Some(retry_action));
406 } else {
407 trace!("environmentd is ready");
408 }
409 }
410 Err(e) => {
411 trace!("failed to connect to environmentd, retrying... ({e})");
412 return Ok(Some(retry_action));
413 }
414 }
415
416 trace!("applying environmentd public service");
417 apply_resource(&service_api, &*self.public_service).await?;
418
419 Ok(None)
420 }
421
422 #[instrument]
423 pub async fn teardown_generation(
424 &self,
425 client: &Client,
426 mz: &Materialize,
427 generation: u64,
428 ) -> Result<(), anyhow::Error> {
429 let configmap_api: Api<ConfigMap> = Api::namespaced(client.clone(), &mz.namespace());
430 let service_api: Api<Service> = Api::namespaced(client.clone(), &mz.namespace());
431 let statefulset_api: Api<StatefulSet> = Api::namespaced(client.clone(), &mz.namespace());
432
433 trace!("deleting environmentd statefulset for generation {generation}");
434 delete_resource(
435 &statefulset_api,
436 &mz.environmentd_statefulset_name(generation),
437 )
438 .await?;
439
440 trace!("deleting persist pubsub service for generation {generation}");
441 delete_resource(&service_api, &mz.persist_pubsub_service_name(generation)).await?;
442
443 trace!("deleting environmentd per-generation service for generation {generation}");
444 delete_resource(
445 &service_api,
446 &mz.environmentd_generation_service_name(generation),
447 )
448 .await?;
449
450 trace!("deleting listeners configmap for generation {generation}");
451 delete_resource(&configmap_api, &mz.listeners_configmap_name(generation)).await?;
452
453 Ok(())
454 }
455
456 pub fn generate_hash(&self) -> String {
459 let mut hasher = Sha256::new();
460 hasher.update(&serde_json::to_string(self).unwrap());
461 hex::encode(hasher.finalize())
462 }
463}
464
465fn create_public_service_object(
466 config: &super::Config,
467 mz: &Materialize,
468 generation: u64,
469) -> Service {
470 let mut service = create_base_service_object(
471 config,
472 mz,
473 generation,
474 &mz.environmentd_service_name(),
475 true,
476 );
477 super::teleport::apply_teleport_registration(config, mz, &mut service);
481 service
482}
483
484fn create_generation_service_object(
485 config: &super::Config,
486 mz: &Materialize,
487 generation: u64,
488) -> Service {
489 create_base_service_object(
490 config,
491 mz,
492 generation,
493 &mz.environmentd_generation_service_name(generation),
494 false,
495 )
496}
497
498fn create_base_service_object(
499 config: &super::Config,
500 mz: &Materialize,
501 generation: u64,
502 service_name: &str,
503 headless: bool,
504) -> Service {
505 let ports = vec![
506 ServicePort {
507 port: config.environmentd_sql_port.into(),
508 protocol: Some("TCP".to_string()),
509 name: Some("sql".to_string()),
510 ..Default::default()
511 },
512 ServicePort {
513 port: config.environmentd_http_port.into(),
514 protocol: Some("TCP".to_string()),
515 name: Some("https".to_string()),
516 ..Default::default()
517 },
518 ServicePort {
519 port: config.environmentd_internal_sql_port.into(),
520 protocol: Some("TCP".to_string()),
521 name: Some("internal-sql".to_string()),
522 ..Default::default()
523 },
524 ServicePort {
525 port: config.environmentd_internal_http_port.into(),
526 protocol: Some("TCP".to_string()),
527 name: Some("internal-http".to_string()),
528 ..Default::default()
529 },
530 ];
531
532 let selector = btreemap! {"materialize.cloud/name".to_string() => mz.environmentd_statefulset_name(generation)};
533
534 let spec = ServiceSpec {
535 type_: Some("ClusterIP".to_string()),
536 cluster_ip: if headless {
537 Some("None".to_string())
538 } else {
539 None
540 },
541 selector: Some(selector),
542 ports: Some(ports),
543 ..Default::default()
544 };
545
546 Service {
547 metadata: mz.managed_resource_meta(service_name.to_string()),
548 spec: Some(spec),
549 status: None,
550 }
551}
552
553fn create_persist_pubsub_service(
554 config: &super::Config,
555 mz: &Materialize,
556 generation: u64,
557) -> Service {
558 Service {
559 metadata: mz.managed_resource_meta(mz.persist_pubsub_service_name(generation)),
560 spec: Some(ServiceSpec {
561 type_: Some("ClusterIP".to_string()),
562 cluster_ip: Some("None".to_string()),
563 selector: Some(btreemap! {
564 "materialize.cloud/name".to_string() => mz.environmentd_statefulset_name(generation),
565 }),
566 ports: Some(vec![ServicePort {
567 name: Some("grpc".to_string()),
568 protocol: Some("TCP".to_string()),
569 port: config.environmentd_internal_persist_pubsub_port.into(),
570 ..Default::default()
571 }]),
572 ..Default::default()
573 }),
574 status: None,
575 }
576}
577
578fn create_environmentd_statefulset_object(
579 config: &super::Config,
580 mz: &Materialize,
581 generation: u64,
582) -> StatefulSet {
583 let mut env = vec![
593 EnvVar {
594 name: "MZ_METADATA_BACKEND_URL".to_string(),
595 value_from: Some(EnvVarSource {
596 secret_key_ref: Some(SecretKeySelector {
597 name: mz.backend_secret_name(),
598 key: "metadata_backend_url".to_string(),
599 optional: Some(false),
600 }),
601 ..Default::default()
602 }),
603 ..Default::default()
604 },
605 EnvVar {
606 name: "MZ_PERSIST_BLOB_URL".to_string(),
607 value_from: Some(EnvVarSource {
608 secret_key_ref: Some(SecretKeySelector {
609 name: mz.backend_secret_name(),
610 key: "persist_backend_url".to_string(),
611 optional: Some(false),
612 }),
613 ..Default::default()
614 }),
615 ..Default::default()
616 },
617 ];
618
619 env.push(EnvVar {
620 name: "AWS_REGION".to_string(),
621 value: Some(config.region.clone()),
622 ..Default::default()
623 });
624
625 env.extend(mz.spec.environmentd_extra_env.iter().flatten().cloned());
626
627 let mut args = vec![];
628
629 if let Some(helm_chart_version) = &config.helm_chart_version {
630 args.push(format!("--helm-chart-version={helm_chart_version}"));
631 }
632
633 args.push(format!(
635 "--environment-id={}",
636 mz.environment_id(&config.cloud_provider.to_string(), &config.region)
637 ));
638
639 args.push(format!(
641 "--clusterd-image={}",
642 matching_image_from_environmentd_image_ref(
643 &mz.spec.environmentd_image_ref,
644 "clusterd",
645 None
646 )
647 ));
648
649 args.extend(
651 [
652 config
653 .environmentd_cluster_replica_sizes
654 .as_ref()
655 .map(|sizes| format!("--cluster-replica-sizes={sizes}")),
656 config
657 .bootstrap_default_cluster_replica_size
658 .as_ref()
659 .map(|size| format!("--bootstrap-default-cluster-replica-size={size}")),
660 config
661 .bootstrap_builtin_system_cluster_replica_size
662 .as_ref()
663 .map(|size| format!("--bootstrap-builtin-system-cluster-replica-size={size}")),
664 config
665 .bootstrap_builtin_probe_cluster_replica_size
666 .as_ref()
667 .map(|size| format!("--bootstrap-builtin-probe-cluster-replica-size={size}")),
668 config
669 .bootstrap_builtin_support_cluster_replica_size
670 .as_ref()
671 .map(|size| format!("--bootstrap-builtin-support-cluster-replica-size={size}")),
672 config
673 .bootstrap_builtin_catalog_server_cluster_replica_size
674 .as_ref()
675 .map(|size| format!("--bootstrap-builtin-catalog-server-cluster-replica-size={size}")),
676 config
677 .bootstrap_builtin_analytics_cluster_replica_size
678 .as_ref()
679 .map(|size| format!("--bootstrap-builtin-analytics-cluster-replica-size={size}")),
680 config
681 .bootstrap_builtin_system_cluster_replication_factor
682 .as_ref()
683 .map(|replication_factor| {
684 format!("--bootstrap-builtin-system-cluster-replication-factor={replication_factor}")
685 }),
686 config
687 .bootstrap_builtin_probe_cluster_replication_factor
688 .as_ref()
689 .map(|replication_factor| format!("--bootstrap-builtin-probe-cluster-replication-factor={replication_factor}")),
690 config
691 .bootstrap_builtin_support_cluster_replication_factor
692 .as_ref()
693 .map(|replication_factor| format!("--bootstrap-builtin-support-cluster-replication-factor={replication_factor}")),
694 config
695 .bootstrap_builtin_analytics_cluster_replication_factor
696 .as_ref()
697 .map(|replication_factor| format!("--bootstrap-builtin-analytics-cluster-replication-factor={replication_factor}")),
698 ]
699 .into_iter()
700 .flatten(),
701 );
702
703 args.extend(
704 config
705 .environmentd_allowed_origins
706 .iter()
707 .map(|origin| format!("--cors-allowed-origin={}", origin.to_str().unwrap())),
708 );
709
710 args.push(format!(
711 "--secrets-controller={}",
712 config.secrets_controller
713 ));
714
715 if let Some(cluster_replica_sizes) = &config.environmentd_cluster_replica_sizes {
716 if let Ok(cluster_replica_sizes) =
717 serde_json::from_str::<BTreeMap<String, serde_json::Value>>(cluster_replica_sizes)
718 {
719 let cluster_replica_sizes: Vec<_> =
720 cluster_replica_sizes.keys().map(|s| s.as_str()).collect();
721 args.push(format!(
722 "--system-parameter-default=allowed_cluster_replica_sizes='{}'",
723 cluster_replica_sizes.join("', '")
724 ));
725 }
726 }
727 if !config.cloud_provider.is_cloud() {
728 args.push("--system-parameter-default=cluster_enable_topology_spread=false".into());
729 }
730
731 if config.enable_internal_statement_logging {
732 args.push("--system-parameter-default=enable_internal_statement_logging=true".into());
733 }
734
735 if let Some(rate) = config.statement_logging_max_sample_rate {
736 args.push(format!(
737 "--system-parameter-default=statement_logging_max_sample_rate={rate}"
738 ));
739 }
740
741 if let Some(rate) = config.statement_logging_target_data_rate {
742 args.push(format!(
743 "--system-parameter-default=statement_logging_target_data_rate={rate}"
744 ));
745 }
746
747 if !mz.spec.enable_rbac {
748 args.push("--system-parameter-default=enable_rbac_checks=false".into());
749 }
750
751 if config.disable_database_network_policies {
752 args.push("--system-parameter-default=enable_network_policies=false".into());
753 }
754
755 args.push("--persist-isolated-runtime-threads=-1".to_string());
759
760 if config.cloud_provider == CloudProvider::Aws {
762 if let Some(azs) = config.environmentd_availability_zones.as_ref() {
763 for az in azs {
764 args.push(format!("--availability-zone={az}"));
765 }
766 }
767
768 if let Some(environmentd_connection_role_arn) = mz
769 .spec
770 .environmentd_connection_role_arn
771 .as_deref()
772 .or(config.environmentd_connection_role_arn.as_deref())
773 {
774 args.push(format!(
775 "--aws-connection-role-arn={}",
776 environmentd_connection_role_arn
777 ));
778 }
779 if let Some(account_id) = &config.aws_account_id {
780 args.push(format!("--aws-account-id={account_id}"));
781 }
782
783 args.extend([format!(
784 "--aws-secrets-controller-tags=Environment={}",
785 mz.name_unchecked()
786 )]);
787 args.extend_from_slice(&config.aws_secrets_controller_tags);
788 }
789
790 args.extend([
792 "--orchestrator=kubernetes".into(),
793 format!(
794 "--orchestrator-kubernetes-service-account={}",
795 mz.service_account_name()
796 ),
797 format!(
798 "--orchestrator-kubernetes-image-pull-policy={}",
799 config.image_pull_policy.as_kebab_case_str(),
800 ),
801 ]);
802 for selector in &config.clusterd_node_selector {
803 args.push(format!(
804 "--orchestrator-kubernetes-service-node-selector={}={}",
805 selector.key, selector.value,
806 ));
807 }
808 if mz.meets_minimum_version(&V144) {
809 if let Some(affinity) = &config.clusterd_affinity {
810 let affinity = serde_json::to_string(affinity).unwrap();
811 args.push(format!(
812 "--orchestrator-kubernetes-service-affinity={affinity}"
813 ))
814 }
815 if let Some(tolerations) = &config.clusterd_tolerations {
816 let tolerations = serde_json::to_string(tolerations).unwrap();
817 args.push(format!(
818 "--orchestrator-kubernetes-service-tolerations={tolerations}"
819 ))
820 }
821 }
822 if let Some(scheduler_name) = &config.scheduler_name {
823 args.push(format!(
824 "--orchestrator-kubernetes-scheduler-name={}",
825 scheduler_name
826 ));
827 }
828 if mz.meets_minimum_version(&V26_42_0) {
829 if let Some(priority_class_name) = &config.clusterd_priority_class_name {
830 args.push(format!(
831 "--orchestrator-kubernetes-priority-class-name={priority_class_name}"
832 ));
833 }
834 }
835 if mz.meets_minimum_version(&V154_DEV0) {
836 args.extend(
837 mz.spec
838 .pod_annotations
839 .as_ref()
840 .map(|annotations| annotations.iter())
841 .unwrap_or_default()
842 .map(|(key, val)| {
843 format!("--orchestrator-kubernetes-service-annotation={key}={val}")
844 }),
845 );
846 }
847 args.extend(
848 mz.default_labels()
849 .iter()
850 .chain(
851 mz.spec
852 .pod_labels
853 .as_ref()
854 .map(|labels| labels.iter())
855 .unwrap_or_default(),
856 )
857 .map(|(key, val)| format!("--orchestrator-kubernetes-service-label={key}={val}")),
858 );
859 if let Some(status) = &mz.status {
860 args.push(format!(
861 "--orchestrator-kubernetes-name-prefix=mz{}-",
862 status.resource_id
863 ));
864 }
865
866 args.extend(["--log-format=json".into()]);
868 if let Some(endpoint) = &config.tracing.opentelemetry_endpoint {
869 args.push(format!("--opentelemetry-endpoint={}", endpoint));
870 }
871 args.extend([
873 format!(
874 "--opentelemetry-resource=organization_id={}",
875 mz.spec.environment_id
876 ),
877 format!(
878 "--opentelemetry-resource=environment_name={}",
879 mz.name_unchecked()
880 ),
881 ]);
882
883 if let Some(segment_api_key) = &config.segment_api_key {
884 args.push(format!("--segment-api-key={}", segment_api_key));
885 if config.segment_client_side {
886 args.push("--segment-client-side".into());
887 }
888 }
889
890 let mut volumes = Vec::new();
891 let mut volume_mounts = Vec::new();
892 if issuer_ref_defined(
893 &config.default_certificate_specs.internal,
894 &mz.spec.internal_certificate_spec,
895 ) {
896 volumes.push(Volume {
897 name: "certificate".to_owned(),
898 secret: Some(SecretVolumeSource {
899 default_mode: Some(0o400),
900 secret_name: Some(mz.environmentd_certificate_secret_name()),
901 items: None,
902 optional: Some(false),
903 }),
904 ..Default::default()
905 });
906 volume_mounts.push(VolumeMount {
907 name: "certificate".to_owned(),
908 mount_path: "/etc/materialized".to_owned(),
909 read_only: Some(true),
910 ..Default::default()
911 });
912 args.extend([
913 "--tls-mode=require".into(),
914 "--tls-cert=/etc/materialized/tls.crt".into(),
915 "--tls-key=/etc/materialized/tls.key".into(),
916 ]);
917 } else {
918 args.push("--tls-mode=disable".to_string());
919 }
920 if let Some(ephemeral_volume_class) = &config.ephemeral_volume_class {
921 args.push(format!(
922 "--orchestrator-kubernetes-ephemeral-volume-class={}",
923 ephemeral_volume_class
924 ));
925 }
926 args.push("--orchestrator-kubernetes-service-fs-group=999".to_string());
928
929 if mz.meets_minimum_version(&V26_1_0) {
933 if let Some(ref name) = mz.spec.system_parameter_configmap_name {
934 volumes.push(Volume {
935 name: "system-params".to_string(),
936 config_map: Some(ConfigMapVolumeSource {
937 default_mode: Some(0o400),
938 name: name.to_owned(),
939 items: None,
940 optional: Some(true),
941 }),
942 ..Default::default()
943 });
944 volume_mounts.push(VolumeMount {
945 name: "system-params".to_string(),
946 mount_path: "/system-params".to_owned(),
948 read_only: Some(true),
949 ..Default::default()
950 });
951 args.push("--config-sync-file-path=/system-params/system-params.json".to_string());
952 args.push("--config-sync-loop-interval=1s".to_string());
953 }
954 }
955
956 if let Some(sentry_dsn) = &config.tracing.sentry_dsn {
958 args.push(format!("--sentry-dsn={}", sentry_dsn));
959 if let Some(sentry_environment) = &config.tracing.sentry_environment {
960 args.push(format!("--sentry-environment={}", sentry_environment));
961 }
962 args.push(format!("--sentry-tag=region={}", config.region));
963 }
964
965 args.push(format!(
967 "--persist-pubsub-url=http://{}:{}",
968 mz.persist_pubsub_service_name(generation),
969 config.environmentd_internal_persist_pubsub_port,
970 ));
971 args.push(format!(
972 "--internal-persist-pubsub-listen-addr=0.0.0.0:{}",
973 config.environmentd_internal_persist_pubsub_port
974 ));
975
976 args.push(format!("--deploy-generation={}", generation));
977
978 args.push(format!(
980 "--internal-console-redirect-url={}",
981 config.internal_console_proxy_url,
982 ));
983
984 if !config.collect_pod_metrics {
985 args.push("--orchestrator-kubernetes-disable-pod-metrics-collection".into());
986 }
987 if config.enable_prometheus_scrape_annotations {
988 args.push("--orchestrator-kubernetes-enable-prometheus-scrape-annotations".into());
989 }
990
991 if config.disable_license_key_checks {
994 if mz.meets_minimum_version(&V143) && !mz.meets_minimum_version(&V153) {
995 args.push("--disable-license-key-checks".into());
996 }
997 }
998
999 if (mz.meets_minimum_version(&V140_DEV0) && !config.disable_license_key_checks)
1002 || mz.meets_minimum_version(&V153)
1003 {
1004 volume_mounts.push(VolumeMount {
1005 name: "license-key".to_string(),
1006 mount_path: "/license_key".to_string(),
1007 ..Default::default()
1008 });
1009 volumes.push(Volume {
1010 name: "license-key".to_string(),
1011 secret: Some(SecretVolumeSource {
1012 default_mode: Some(256),
1013 optional: Some(false),
1014 secret_name: Some(mz.backend_secret_name()),
1015 items: Some(vec![KeyToPath {
1016 key: "license_key".to_string(),
1017 path: "license_key".to_string(),
1018 ..Default::default()
1019 }]),
1020 ..Default::default()
1021 }),
1022 ..Default::default()
1023 });
1024 env.push(EnvVar {
1025 name: "MZ_LICENSE_KEY".to_string(),
1026 value: Some("/license_key/license_key".to_string()),
1027 ..Default::default()
1028 });
1029 }
1030
1031 if let Some(extra_args) = &mz.spec.environmentd_extra_args {
1033 args.extend(extra_args.iter().cloned());
1034 }
1035
1036 let listeners_config = create_v0_147_0_listeners_config(config, mz);
1045 let probe_port = listeners_config
1046 .sql
1047 .get("internal")
1048 .or_else(|| listeners_config.sql.get("external"))
1049 .expect("external SQL listener is always configured")
1050 .addr
1051 .port();
1052 let probe = Probe {
1053 initial_delay_seconds: Some(1),
1054 failure_threshold: Some(12),
1055 tcp_socket: Some(TCPSocketAction {
1056 host: None,
1057 port: IntOrString::Int(probe_port.into()),
1058 }),
1059 ..Default::default()
1060 };
1061
1062 let security_context = if config.enable_security_context {
1063 Some(SecurityContext {
1067 run_as_non_root: Some(true),
1068 capabilities: Some(Capabilities {
1069 drop: Some(vec!["ALL".to_string()]),
1070 ..Default::default()
1071 }),
1072 seccomp_profile: Some(SeccompProfile {
1073 type_: "RuntimeDefault".to_string(),
1074 ..Default::default()
1075 }),
1076 allow_privilege_escalation: Some(false),
1077 ..Default::default()
1078 })
1079 } else {
1080 None
1081 };
1082
1083 let ports = vec![
1084 ContainerPort {
1085 container_port: config.environmentd_sql_port.into(),
1086 name: Some("sql".to_owned()),
1087 ..Default::default()
1088 },
1089 ContainerPort {
1090 container_port: config.environmentd_internal_sql_port.into(),
1091 name: Some("internal-sql".to_owned()),
1092 ..Default::default()
1093 },
1094 ContainerPort {
1095 container_port: config.environmentd_http_port.into(),
1096 name: Some("http".to_owned()),
1097 ..Default::default()
1098 },
1099 ContainerPort {
1100 container_port: config.environmentd_internal_http_port.into(),
1101 name: Some("internal-http".to_owned()),
1102 ..Default::default()
1103 },
1104 ContainerPort {
1105 container_port: config.environmentd_internal_persist_pubsub_port.into(),
1106 name: Some("persist-pubsub".to_owned()),
1107 ..Default::default()
1108 },
1109 ];
1110
1111 if mz.meets_minimum_version(&V147_DEV0) {
1113 volume_mounts.push(VolumeMount {
1114 name: "listeners-configmap".to_string(),
1115 mount_path: "/listeners".to_string(),
1116 ..Default::default()
1117 });
1118 volumes.push(Volume {
1119 name: "listeners-configmap".to_string(),
1120 config_map: Some(ConfigMapVolumeSource {
1121 name: mz.listeners_configmap_name(generation),
1122 default_mode: Some(256),
1123 optional: Some(false),
1124 items: Some(vec![KeyToPath {
1125 key: "listeners.json".to_string(),
1126 path: "listeners.json".to_string(),
1127 ..Default::default()
1128 }]),
1129 }),
1130 ..Default::default()
1131 });
1132 args.push("--listeners-config-path=/listeners/listeners.json".to_owned());
1133 if matches!(
1134 mz.spec.authenticator_kind,
1135 AuthenticatorKind::Password | AuthenticatorKind::Sasl | AuthenticatorKind::Oidc
1136 ) {
1137 args.push("--system-parameter-default=enable_password_auth=true".into());
1138 env.push(EnvVar {
1139 name: "MZ_EXTERNAL_LOGIN_PASSWORD_MZ_SYSTEM".to_string(),
1140 value_from: Some(EnvVarSource {
1141 secret_key_ref: Some(SecretKeySelector {
1142 name: mz.backend_secret_name(),
1143 key: "external_login_password_mz_system".to_string(),
1144 optional: Some(false),
1145 }),
1146 ..Default::default()
1147 }),
1148 ..Default::default()
1149 })
1150 }
1151 } else {
1152 args.extend([
1153 format!("--sql-listen-addr=0.0.0.0:{}", config.environmentd_sql_port),
1154 format!(
1155 "--http-listen-addr=0.0.0.0:{}",
1156 config.environmentd_http_port
1157 ),
1158 format!(
1159 "--internal-sql-listen-addr=0.0.0.0:{}",
1160 config.environmentd_internal_sql_port
1161 ),
1162 format!(
1163 "--internal-http-listen-addr=0.0.0.0:{}",
1164 config.environmentd_internal_http_port
1165 ),
1166 ]);
1167 }
1168
1169 let container = Container {
1170 name: "environmentd".to_owned(),
1171 image: Some(mz.spec.environmentd_image_ref.to_owned()),
1172 image_pull_policy: Some(config.image_pull_policy.to_string()),
1173 ports: Some(ports),
1174 args: Some(args),
1175 env: Some(env),
1176 volume_mounts: Some(volume_mounts),
1177 liveness_probe: Some(probe.clone()),
1178 readiness_probe: Some(probe),
1179 resources: mz
1180 .spec
1181 .environmentd_resource_requirements
1182 .clone()
1183 .or_else(|| config.environmentd_default_resources.clone()),
1184 security_context: security_context.clone(),
1185 ..Default::default()
1186 };
1187
1188 let mut pod_template_labels = mz.default_labels();
1189 pod_template_labels.insert(
1190 "materialize.cloud/name".to_owned(),
1191 mz.environmentd_statefulset_name(generation),
1192 );
1193 pod_template_labels.insert(
1194 "materialize.cloud/app".to_owned(),
1195 mz.environmentd_app_name(),
1196 );
1197 pod_template_labels.extend(recommended_k8s_labels("environmentd".into()));
1198 pod_template_labels.extend(
1199 mz.spec
1200 .pod_labels
1201 .as_ref()
1202 .map(|labels| labels.iter())
1203 .unwrap_or_default()
1204 .map(|(key, value)| (key.clone(), value.clone())),
1205 );
1206
1207 let mut pod_template_annotations = btreemap! {
1208 "cluster-autoscaler.kubernetes.io/safe-to-evict".to_owned() => "false".to_string(),
1210
1211 "karpenter.sh/do-not-evict".to_owned() => "true".to_string(),
1213 "karpenter.sh/do-not-disrupt".to_owned() => "true".to_string(),
1214 "materialize.cloud/generation".to_owned() => generation.to_string(),
1215 };
1216 if config.enable_prometheus_scrape_annotations {
1217 pod_template_annotations.insert("prometheus.io/scrape".to_owned(), "true".to_string());
1218 pod_template_annotations.insert(
1219 "prometheus.io/port".to_owned(),
1220 config.environmentd_internal_http_port.to_string(),
1221 );
1222 pod_template_annotations.insert("prometheus.io/path".to_owned(), "/metrics".to_string());
1223 pod_template_annotations.insert("prometheus.io/scheme".to_owned(), "http".to_string());
1224 pod_template_annotations.insert(
1225 "materialize.prometheus.io/mz_usage_path".to_owned(),
1226 "/metrics/mz_usage".to_string(),
1227 );
1228 pod_template_annotations.insert(
1229 "materialize.prometheus.io/mz_frontier_path".to_owned(),
1230 "/metrics/mz_frontier".to_string(),
1231 );
1232 pod_template_annotations.insert(
1233 "materialize.prometheus.io/mz_compute_path".to_owned(),
1234 "/metrics/mz_compute".to_string(),
1235 );
1236 pod_template_annotations.insert(
1237 "materialize.prometheus.io/mz_storage_path".to_owned(),
1238 "/metrics/mz_storage".to_string(),
1239 );
1240 }
1241 pod_template_annotations.extend(
1242 mz.spec
1243 .pod_annotations
1244 .as_ref()
1245 .map(|annotations| annotations.iter())
1246 .unwrap_or_default()
1247 .map(|(key, value)| (key.clone(), value.clone())),
1248 );
1249
1250 let mut tolerations = vec![
1251 Toleration {
1255 effect: Some("NoExecute".into()),
1256 key: Some("node.kubernetes.io/not-ready".into()),
1257 operator: Some("Exists".into()),
1258 toleration_seconds: Some(30),
1259 value: None,
1260 },
1261 Toleration {
1262 effect: Some("NoExecute".into()),
1263 key: Some("node.kubernetes.io/unreachable".into()),
1264 operator: Some("Exists".into()),
1265 toleration_seconds: Some(30),
1266 value: None,
1267 },
1268 ];
1269 if let Some(user_tolerations) = &config.environmentd_tolerations {
1270 tolerations.extend(user_tolerations.iter().cloned());
1271 }
1272 let tolerations = Some(tolerations);
1273
1274 let pod_template_spec = PodTemplateSpec {
1275 metadata: Some(ObjectMeta {
1278 labels: Some(pod_template_labels),
1279 annotations: Some(pod_template_annotations), ..Default::default()
1281 }),
1282 spec: Some(PodSpec {
1283 containers: vec![container],
1284 node_selector: Some(
1285 config
1286 .environmentd_node_selector
1287 .iter()
1288 .map(|selector| (selector.key.clone(), selector.value.clone()))
1289 .collect(),
1290 ),
1291 affinity: config.environmentd_affinity.clone(),
1292 scheduler_name: config.scheduler_name.clone(),
1293 priority_class_name: config.environmentd_priority_class_name.clone(),
1294 service_account_name: Some(mz.service_account_name()),
1295 volumes: Some(volumes),
1296 security_context: Some(PodSecurityContext {
1297 fs_group: Some(999),
1298 run_as_user: Some(999),
1299 run_as_group: Some(999),
1300 ..Default::default()
1301 }),
1302 tolerations,
1303 termination_grace_period_seconds: Some(0),
1324 ..Default::default()
1325 }),
1326 };
1327
1328 let mut match_labels = BTreeMap::new();
1329 match_labels.insert(
1330 "materialize.cloud/name".to_owned(),
1331 mz.environmentd_statefulset_name(generation),
1332 );
1333
1334 let statefulset_spec = StatefulSetSpec {
1335 replicas: Some(1),
1336 template: pod_template_spec,
1337 service_name: Some(mz.environmentd_service_name()),
1338 selector: LabelSelector {
1339 match_expressions: None,
1340 match_labels: Some(match_labels),
1341 },
1342 ..Default::default()
1343 };
1344
1345 StatefulSet {
1346 metadata: ObjectMeta {
1347 annotations: Some(btreemap! {
1348 "materialize.cloud/generation".to_owned() => generation.to_string(),
1349 "materialize.cloud/force".to_owned() => mz.force_rollout_value(),
1350 }),
1351 ..mz.managed_resource_meta(mz.environmentd_statefulset_name(generation))
1352 },
1353 spec: Some(statefulset_spec),
1354 status: None,
1355 }
1356}
1357
1358fn create_v0_147_0_listeners_config(
1359 config: &super::Config,
1360 mz: &Materialize,
1361) -> v0_147_0::ListenersConfig {
1362 let external_enable_tls = issuer_ref_defined(
1363 &config.default_certificate_specs.internal,
1364 &mz.spec.internal_certificate_spec,
1365 );
1366 let authenticator_kind = mz.spec.authenticator_kind;
1367
1368 let mut listeners_config = v0_147_0::ListenersConfig {
1369 sql: btreemap! {
1370 "external".to_owned() => SqlListenerConfig{
1371 addr: SocketAddr::new(
1372 IpAddr::V4(Ipv4Addr::new(0,0,0,0)),
1373 config.environmentd_sql_port,
1374 ),
1375 authenticator_kind,
1376 allowed_roles: AllowedRoles::Normal,
1377 enable_tls: external_enable_tls,
1378 },
1379 "internal".to_owned() => SqlListenerConfig{
1380 addr: SocketAddr::new(
1381 IpAddr::V4(Ipv4Addr::new(0,0,0,0)),
1382 config.environmentd_internal_sql_port,
1383 ),
1384 authenticator_kind: AuthenticatorKind::None,
1385 allowed_roles: AllowedRoles::NormalAndInternal,
1387 enable_tls: false,
1388 },
1389 },
1390 http: btreemap! {
1391 "external".to_owned() => v0_147_0::HttpListenerConfig{
1392 base: BaseListenerConfig {
1393 addr: SocketAddr::new(
1394 IpAddr::V4(Ipv4Addr::new(0,0,0,0)),
1395 config.environmentd_http_port,
1396 ),
1397 authenticator_kind: if authenticator_kind == AuthenticatorKind::Sasl {
1401 AuthenticatorKind::Password
1402 } else {
1403 authenticator_kind
1404 },
1405 allowed_roles: AllowedRoles::Normal,
1406 enable_tls: external_enable_tls,
1407 },
1408 routes: v0_147_0::HttpRoutes{
1409 base: true,
1410 webhook: true,
1411 internal: false,
1412 metrics: false,
1413 profiling: false,
1414 mcp_agent: true,
1415 mcp_developer: true,
1416 console_config: true,
1417 },
1418 },
1419 "internal".to_owned() => v0_147_0::HttpListenerConfig{
1420 base: BaseListenerConfig {
1421 addr: SocketAddr::new(
1422 IpAddr::V4(Ipv4Addr::new(0,0,0,0)),
1423 config.environmentd_internal_http_port,
1424 ),
1425 authenticator_kind: AuthenticatorKind::None,
1426 allowed_roles: AllowedRoles::NormalAndInternal,
1428 enable_tls: false,
1429 },
1430 routes: v0_147_0::HttpRoutes{
1431 base: true,
1432 webhook: true,
1433 internal: true,
1434 metrics: true,
1435 profiling: true,
1436 mcp_agent: true,
1437 mcp_developer: true,
1438 console_config: false,
1439 },
1440 },
1441 },
1442 };
1443
1444 if matches!(
1447 authenticator_kind,
1448 AuthenticatorKind::Password | AuthenticatorKind::Sasl | AuthenticatorKind::Oidc
1449 ) {
1450 listeners_config.sql.remove("internal");
1451 listeners_config.http.remove("internal");
1452
1453 listeners_config.sql.get_mut("external").map(|listener| {
1454 listener.allowed_roles = AllowedRoles::NormalAndInternal;
1455 listener
1456 });
1457 listeners_config.http.get_mut("external").map(|listener| {
1458 listener.base.allowed_roles = AllowedRoles::NormalAndInternal;
1459 listener.routes.internal = true;
1460 listener.routes.profiling = true;
1461 listener
1462 });
1463
1464 listeners_config.http.insert(
1465 "metrics".to_owned(),
1466 v0_147_0::HttpListenerConfig {
1467 base: BaseListenerConfig {
1468 addr: SocketAddr::new(
1469 IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
1470 config.environmentd_internal_http_port,
1471 ),
1472 authenticator_kind: AuthenticatorKind::None,
1473 allowed_roles: AllowedRoles::NormalAndInternal,
1474 enable_tls: false,
1475 },
1476 routes: v0_147_0::HttpRoutes {
1477 base: false,
1478 webhook: false,
1479 internal: false,
1480 metrics: true,
1481 profiling: false,
1482 mcp_agent: false,
1483 mcp_developer: false,
1484 console_config: false,
1485 },
1486 },
1487 );
1488 };
1489 listeners_config
1490}
1491
1492fn create_connection_info(
1493 config: &super::Config,
1494 mz: &Materialize,
1495 generation: u64,
1496) -> ConnectionInfo {
1497 let external_enable_tls = issuer_ref_defined(
1498 &config.default_certificate_specs.internal,
1499 &mz.spec.internal_certificate_spec,
1500 );
1501 let authenticator_kind = mz.spec.authenticator_kind;
1502
1503 let listeners_config = create_v0_147_0_listeners_config(config, mz);
1504
1505 let listeners_json = if mz.meets_minimum_version(&PER_ROUTE_GROUP_ROLES_VERSION) {
1509 let mut listeners_config: v26_32_0::ListenersConfig = listeners_config.into();
1514 if matches!(
1521 authenticator_kind,
1522 AuthenticatorKind::Password | AuthenticatorKind::Sasl | AuthenticatorKind::Oidc
1523 ) {
1524 listeners_config.http.get_mut("external").map(|listener| {
1525 listener.routes.internal = RouteGroup::Enabled(AllowedRoles::Internal);
1526 listener.routes.profiling = RouteGroup::Enabled(AllowedRoles::Internal);
1527 listener
1528 });
1529 }
1530
1531 serde_json::to_string(&VersionedListenersConfig::V2(listeners_config)).expect("known valid")
1532 } else {
1533 serde_json::to_string(&VersionedListenersConfig::V1(listeners_config)).expect("known valid")
1534 };
1535 let listeners_configmap = ConfigMap {
1536 binary_data: None,
1537 data: Some(btreemap! {
1538 "listeners.json".to_owned() => listeners_json,
1539 }),
1540 immutable: None,
1541 metadata: ObjectMeta {
1542 annotations: Some(btreemap! {
1543 "materialize.cloud/generation".to_owned() => generation.to_string(),
1544 }),
1545 ..mz.managed_resource_meta(mz.listeners_configmap_name(generation))
1546 },
1547 };
1548
1549 let (scheme, leader_api_port, mz_system_secret_name) = match authenticator_kind {
1550 AuthenticatorKind::Password | AuthenticatorKind::Sasl | AuthenticatorKind::Oidc => {
1551 let scheme = if external_enable_tls { "https" } else { "http" };
1552 (
1553 scheme,
1554 config.environmentd_http_port,
1555 Some(mz.spec.backend_secret_name.clone()),
1556 )
1557 }
1558 _ => ("http", config.environmentd_internal_http_port, None),
1559 };
1560 let environmentd_url = format!(
1561 "{}://{}.{}.svc.cluster.local:{}",
1562 scheme,
1563 mz.environmentd_generation_service_name(generation),
1564 mz.namespace(),
1565 leader_api_port,
1566 );
1567 ConnectionInfo {
1568 environmentd_url,
1569 listeners_configmap,
1570 mz_system_secret_name,
1571 }
1572}
1573
1574#[derive(Debug, Deserialize, PartialEq, Eq)]
1576struct BecomeLeaderResponse {
1577 result: BecomeLeaderResult,
1578}
1579
1580#[derive(Debug, Deserialize, PartialEq, Eq)]
1581enum BecomeLeaderResult {
1582 Success,
1583 Failure { message: String },
1584}
1585
1586#[derive(Debug, Deserialize, PartialEq, Eq)]
1587struct SkipCatchupError {
1588 message: String,
1589}