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 base_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 let readiness_probe = environmentd_probe(
1062 base_probe.clone(),
1063 config.environmentd_default_readiness_probe.as_ref(),
1064 mz.spec.environmentd_readiness_probe.as_ref(),
1065 );
1066 let liveness_probe = environmentd_probe(
1067 base_probe.clone(),
1068 config.environmentd_default_liveness_probe.as_ref(),
1069 mz.spec.environmentd_liveness_probe.as_ref(),
1070 );
1071 let startup_probe = (config.environmentd_default_startup_probe.is_some()
1074 || mz.spec.environmentd_startup_probe.is_some())
1075 .then(|| {
1076 environmentd_probe(
1077 base_probe,
1078 config.environmentd_default_startup_probe.as_ref(),
1079 mz.spec.environmentd_startup_probe.as_ref(),
1080 )
1081 });
1082
1083 let security_context = if config.enable_security_context {
1084 Some(SecurityContext {
1088 run_as_non_root: Some(true),
1089 capabilities: Some(Capabilities {
1090 drop: Some(vec!["ALL".to_string()]),
1091 ..Default::default()
1092 }),
1093 seccomp_profile: Some(SeccompProfile {
1094 type_: "RuntimeDefault".to_string(),
1095 ..Default::default()
1096 }),
1097 allow_privilege_escalation: Some(false),
1098 ..Default::default()
1099 })
1100 } else {
1101 None
1102 };
1103
1104 let ports = vec![
1105 ContainerPort {
1106 container_port: config.environmentd_sql_port.into(),
1107 name: Some("sql".to_owned()),
1108 ..Default::default()
1109 },
1110 ContainerPort {
1111 container_port: config.environmentd_internal_sql_port.into(),
1112 name: Some("internal-sql".to_owned()),
1113 ..Default::default()
1114 },
1115 ContainerPort {
1116 container_port: config.environmentd_http_port.into(),
1117 name: Some("http".to_owned()),
1118 ..Default::default()
1119 },
1120 ContainerPort {
1121 container_port: config.environmentd_internal_http_port.into(),
1122 name: Some("internal-http".to_owned()),
1123 ..Default::default()
1124 },
1125 ContainerPort {
1126 container_port: config.environmentd_internal_persist_pubsub_port.into(),
1127 name: Some("persist-pubsub".to_owned()),
1128 ..Default::default()
1129 },
1130 ];
1131
1132 if mz.meets_minimum_version(&V147_DEV0) {
1134 volume_mounts.push(VolumeMount {
1135 name: "listeners-configmap".to_string(),
1136 mount_path: "/listeners".to_string(),
1137 ..Default::default()
1138 });
1139 volumes.push(Volume {
1140 name: "listeners-configmap".to_string(),
1141 config_map: Some(ConfigMapVolumeSource {
1142 name: mz.listeners_configmap_name(generation),
1143 default_mode: Some(256),
1144 optional: Some(false),
1145 items: Some(vec![KeyToPath {
1146 key: "listeners.json".to_string(),
1147 path: "listeners.json".to_string(),
1148 ..Default::default()
1149 }]),
1150 }),
1151 ..Default::default()
1152 });
1153 args.push("--listeners-config-path=/listeners/listeners.json".to_owned());
1154 if matches!(
1155 mz.spec.authenticator_kind,
1156 AuthenticatorKind::Password | AuthenticatorKind::Sasl | AuthenticatorKind::Oidc
1157 ) {
1158 args.push("--system-parameter-default=enable_password_auth=true".into());
1159 env.push(EnvVar {
1160 name: "MZ_EXTERNAL_LOGIN_PASSWORD_MZ_SYSTEM".to_string(),
1161 value_from: Some(EnvVarSource {
1162 secret_key_ref: Some(SecretKeySelector {
1163 name: mz.backend_secret_name(),
1164 key: "external_login_password_mz_system".to_string(),
1165 optional: Some(false),
1166 }),
1167 ..Default::default()
1168 }),
1169 ..Default::default()
1170 })
1171 }
1172 } else {
1173 args.extend([
1174 format!("--sql-listen-addr=0.0.0.0:{}", config.environmentd_sql_port),
1175 format!(
1176 "--http-listen-addr=0.0.0.0:{}",
1177 config.environmentd_http_port
1178 ),
1179 format!(
1180 "--internal-sql-listen-addr=0.0.0.0:{}",
1181 config.environmentd_internal_sql_port
1182 ),
1183 format!(
1184 "--internal-http-listen-addr=0.0.0.0:{}",
1185 config.environmentd_internal_http_port
1186 ),
1187 ]);
1188 }
1189
1190 let container = Container {
1191 name: "environmentd".to_owned(),
1192 image: Some(mz.spec.environmentd_image_ref.to_owned()),
1193 image_pull_policy: Some(config.image_pull_policy.to_string()),
1194 ports: Some(ports),
1195 args: Some(args),
1196 env: Some(env),
1197 volume_mounts: Some(volume_mounts),
1198 startup_probe,
1199 liveness_probe: Some(liveness_probe),
1200 readiness_probe: Some(readiness_probe),
1201 resources: mz
1202 .spec
1203 .environmentd_resource_requirements
1204 .clone()
1205 .or_else(|| config.environmentd_default_resources.clone()),
1206 security_context: security_context.clone(),
1207 ..Default::default()
1208 };
1209
1210 let mut pod_template_labels = mz.default_labels();
1211 pod_template_labels.insert(
1212 "materialize.cloud/name".to_owned(),
1213 mz.environmentd_statefulset_name(generation),
1214 );
1215 pod_template_labels.insert(
1216 "materialize.cloud/app".to_owned(),
1217 mz.environmentd_app_name(),
1218 );
1219 pod_template_labels.extend(recommended_k8s_labels("environmentd".into()));
1220 pod_template_labels.extend(
1221 mz.spec
1222 .pod_labels
1223 .as_ref()
1224 .map(|labels| labels.iter())
1225 .unwrap_or_default()
1226 .map(|(key, value)| (key.clone(), value.clone())),
1227 );
1228
1229 let mut pod_template_annotations = btreemap! {
1230 "cluster-autoscaler.kubernetes.io/safe-to-evict".to_owned() => "false".to_string(),
1232
1233 "karpenter.sh/do-not-evict".to_owned() => "true".to_string(),
1235 "karpenter.sh/do-not-disrupt".to_owned() => "true".to_string(),
1236 "materialize.cloud/generation".to_owned() => generation.to_string(),
1237 };
1238 if config.enable_prometheus_scrape_annotations {
1239 pod_template_annotations.insert("prometheus.io/scrape".to_owned(), "true".to_string());
1240 pod_template_annotations.insert(
1241 "prometheus.io/port".to_owned(),
1242 config.environmentd_internal_http_port.to_string(),
1243 );
1244 pod_template_annotations.insert("prometheus.io/path".to_owned(), "/metrics".to_string());
1245 pod_template_annotations.insert("prometheus.io/scheme".to_owned(), "http".to_string());
1246 pod_template_annotations.insert(
1247 "materialize.prometheus.io/mz_usage_path".to_owned(),
1248 "/metrics/mz_usage".to_string(),
1249 );
1250 pod_template_annotations.insert(
1251 "materialize.prometheus.io/mz_frontier_path".to_owned(),
1252 "/metrics/mz_frontier".to_string(),
1253 );
1254 pod_template_annotations.insert(
1255 "materialize.prometheus.io/mz_compute_path".to_owned(),
1256 "/metrics/mz_compute".to_string(),
1257 );
1258 pod_template_annotations.insert(
1259 "materialize.prometheus.io/mz_storage_path".to_owned(),
1260 "/metrics/mz_storage".to_string(),
1261 );
1262 }
1263 pod_template_annotations.extend(
1264 mz.spec
1265 .pod_annotations
1266 .as_ref()
1267 .map(|annotations| annotations.iter())
1268 .unwrap_or_default()
1269 .map(|(key, value)| (key.clone(), value.clone())),
1270 );
1271
1272 let mut tolerations = vec![
1273 Toleration {
1277 effect: Some("NoExecute".into()),
1278 key: Some("node.kubernetes.io/not-ready".into()),
1279 operator: Some("Exists".into()),
1280 toleration_seconds: Some(30),
1281 value: None,
1282 },
1283 Toleration {
1284 effect: Some("NoExecute".into()),
1285 key: Some("node.kubernetes.io/unreachable".into()),
1286 operator: Some("Exists".into()),
1287 toleration_seconds: Some(30),
1288 value: None,
1289 },
1290 ];
1291 if let Some(user_tolerations) = &config.environmentd_tolerations {
1292 tolerations.extend(user_tolerations.iter().cloned());
1293 }
1294 let tolerations = Some(tolerations);
1295
1296 let pod_template_spec = PodTemplateSpec {
1297 metadata: Some(ObjectMeta {
1300 labels: Some(pod_template_labels),
1301 annotations: Some(pod_template_annotations), ..Default::default()
1303 }),
1304 spec: Some(PodSpec {
1305 containers: vec![container],
1306 node_selector: Some(
1307 config
1308 .environmentd_node_selector
1309 .iter()
1310 .map(|selector| (selector.key.clone(), selector.value.clone()))
1311 .collect(),
1312 ),
1313 affinity: config.environmentd_affinity.clone(),
1314 scheduler_name: config.scheduler_name.clone(),
1315 priority_class_name: config.environmentd_priority_class_name.clone(),
1316 service_account_name: Some(mz.service_account_name()),
1317 volumes: Some(volumes),
1318 security_context: Some(PodSecurityContext {
1319 fs_group: Some(999),
1320 run_as_user: Some(999),
1321 run_as_group: Some(999),
1322 ..Default::default()
1323 }),
1324 tolerations,
1325 termination_grace_period_seconds: Some(0),
1346 ..Default::default()
1347 }),
1348 };
1349
1350 let mut match_labels = BTreeMap::new();
1351 match_labels.insert(
1352 "materialize.cloud/name".to_owned(),
1353 mz.environmentd_statefulset_name(generation),
1354 );
1355
1356 let statefulset_spec = StatefulSetSpec {
1357 replicas: Some(1),
1358 template: pod_template_spec,
1359 service_name: Some(mz.environmentd_service_name()),
1360 selector: LabelSelector {
1361 match_expressions: None,
1362 match_labels: Some(match_labels),
1363 },
1364 ..Default::default()
1365 };
1366
1367 StatefulSet {
1368 metadata: ObjectMeta {
1369 annotations: Some(btreemap! {
1370 "materialize.cloud/generation".to_owned() => generation.to_string(),
1371 "materialize.cloud/force".to_owned() => mz.force_rollout_value(),
1372 }),
1373 ..mz.managed_resource_meta(mz.environmentd_statefulset_name(generation))
1374 },
1375 spec: Some(statefulset_spec),
1376 status: None,
1377 }
1378}
1379
1380pub fn validate_probe_handlers(probe: &Probe) -> Result<(), anyhow::Error> {
1386 let handlers: Vec<_> = [
1387 ("exec", probe.exec.is_some()),
1388 ("grpc", probe.grpc.is_some()),
1389 ("httpGet", probe.http_get.is_some()),
1390 ("tcpSocket", probe.tcp_socket.is_some()),
1391 ]
1392 .into_iter()
1393 .filter_map(|(name, set)| set.then_some(name))
1394 .collect();
1395 if handlers.len() > 1 {
1396 anyhow::bail!(
1397 "probe sets more than one handler ({}), but at most one is allowed",
1398 handlers.join(", ")
1399 );
1400 }
1401 Ok(())
1402}
1403
1404fn environmentd_probe(
1411 base: Probe,
1412 operator_default: Option<&Probe>,
1413 environment_override: Option<&Probe>,
1414) -> Probe {
1415 [operator_default, environment_override]
1416 .into_iter()
1417 .flatten()
1418 .fold(base, overlay_probe)
1419}
1420
1421fn overlay_probe(base: Probe, overlay: &Probe) -> Probe {
1422 let Probe {
1425 exec,
1426 failure_threshold,
1427 grpc,
1428 http_get,
1429 initial_delay_seconds,
1430 period_seconds,
1431 success_threshold,
1432 tcp_socket,
1433 termination_grace_period_seconds,
1434 timeout_seconds,
1435 } = overlay.clone();
1436 let has_handler =
1437 exec.is_some() || grpc.is_some() || http_get.is_some() || tcp_socket.is_some();
1438 let (exec, grpc, http_get, tcp_socket) = if has_handler {
1439 (exec, grpc, http_get, tcp_socket)
1440 } else {
1441 (base.exec, base.grpc, base.http_get, base.tcp_socket)
1442 };
1443 Probe {
1444 exec,
1445 failure_threshold: failure_threshold.or(base.failure_threshold),
1446 grpc,
1447 http_get,
1448 initial_delay_seconds: initial_delay_seconds.or(base.initial_delay_seconds),
1449 period_seconds: period_seconds.or(base.period_seconds),
1450 success_threshold: success_threshold.or(base.success_threshold),
1451 tcp_socket,
1452 termination_grace_period_seconds: termination_grace_period_seconds
1453 .or(base.termination_grace_period_seconds),
1454 timeout_seconds: timeout_seconds.or(base.timeout_seconds),
1455 }
1456}
1457
1458fn create_v0_147_0_listeners_config(
1459 config: &super::Config,
1460 mz: &Materialize,
1461) -> v0_147_0::ListenersConfig {
1462 let external_enable_tls = issuer_ref_defined(
1463 &config.default_certificate_specs.internal,
1464 &mz.spec.internal_certificate_spec,
1465 );
1466 let authenticator_kind = mz.spec.authenticator_kind;
1467
1468 let mut listeners_config = v0_147_0::ListenersConfig {
1469 sql: btreemap! {
1470 "external".to_owned() => SqlListenerConfig{
1471 addr: SocketAddr::new(
1472 IpAddr::V4(Ipv4Addr::new(0,0,0,0)),
1473 config.environmentd_sql_port,
1474 ),
1475 authenticator_kind,
1476 allowed_roles: AllowedRoles::Normal,
1477 enable_tls: external_enable_tls,
1478 },
1479 "internal".to_owned() => SqlListenerConfig{
1480 addr: SocketAddr::new(
1481 IpAddr::V4(Ipv4Addr::new(0,0,0,0)),
1482 config.environmentd_internal_sql_port,
1483 ),
1484 authenticator_kind: AuthenticatorKind::None,
1485 allowed_roles: AllowedRoles::NormalAndInternal,
1487 enable_tls: false,
1488 },
1489 },
1490 http: btreemap! {
1491 "external".to_owned() => v0_147_0::HttpListenerConfig{
1492 base: BaseListenerConfig {
1493 addr: SocketAddr::new(
1494 IpAddr::V4(Ipv4Addr::new(0,0,0,0)),
1495 config.environmentd_http_port,
1496 ),
1497 authenticator_kind: if authenticator_kind == AuthenticatorKind::Sasl {
1501 AuthenticatorKind::Password
1502 } else {
1503 authenticator_kind
1504 },
1505 allowed_roles: AllowedRoles::Normal,
1506 enable_tls: external_enable_tls,
1507 },
1508 routes: v0_147_0::HttpRoutes{
1509 base: true,
1510 webhook: true,
1511 internal: false,
1512 metrics: false,
1513 profiling: false,
1514 mcp_agent: true,
1515 mcp_developer: true,
1516 console_config: true,
1517 },
1518 },
1519 "internal".to_owned() => v0_147_0::HttpListenerConfig{
1520 base: BaseListenerConfig {
1521 addr: SocketAddr::new(
1522 IpAddr::V4(Ipv4Addr::new(0,0,0,0)),
1523 config.environmentd_internal_http_port,
1524 ),
1525 authenticator_kind: AuthenticatorKind::None,
1526 allowed_roles: AllowedRoles::NormalAndInternal,
1528 enable_tls: false,
1529 },
1530 routes: v0_147_0::HttpRoutes{
1531 base: true,
1532 webhook: true,
1533 internal: true,
1534 metrics: true,
1535 profiling: true,
1536 mcp_agent: true,
1537 mcp_developer: true,
1538 console_config: false,
1539 },
1540 },
1541 },
1542 };
1543
1544 if matches!(
1547 authenticator_kind,
1548 AuthenticatorKind::Password | AuthenticatorKind::Sasl | AuthenticatorKind::Oidc
1549 ) {
1550 listeners_config.sql.remove("internal");
1551 listeners_config.http.remove("internal");
1552
1553 listeners_config.sql.get_mut("external").map(|listener| {
1554 listener.allowed_roles = AllowedRoles::NormalAndInternal;
1555 listener
1556 });
1557 listeners_config.http.get_mut("external").map(|listener| {
1558 listener.base.allowed_roles = AllowedRoles::NormalAndInternal;
1559 listener.routes.internal = true;
1560 listener.routes.profiling = true;
1561 listener
1562 });
1563
1564 listeners_config.http.insert(
1565 "metrics".to_owned(),
1566 v0_147_0::HttpListenerConfig {
1567 base: BaseListenerConfig {
1568 addr: SocketAddr::new(
1569 IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
1570 config.environmentd_internal_http_port,
1571 ),
1572 authenticator_kind: AuthenticatorKind::None,
1573 allowed_roles: AllowedRoles::NormalAndInternal,
1574 enable_tls: false,
1575 },
1576 routes: v0_147_0::HttpRoutes {
1577 base: false,
1578 webhook: false,
1579 internal: false,
1580 metrics: true,
1581 profiling: false,
1582 mcp_agent: false,
1583 mcp_developer: false,
1584 console_config: false,
1585 },
1586 },
1587 );
1588 };
1589 listeners_config
1590}
1591
1592fn create_connection_info(
1593 config: &super::Config,
1594 mz: &Materialize,
1595 generation: u64,
1596) -> ConnectionInfo {
1597 let external_enable_tls = issuer_ref_defined(
1598 &config.default_certificate_specs.internal,
1599 &mz.spec.internal_certificate_spec,
1600 );
1601 let authenticator_kind = mz.spec.authenticator_kind;
1602
1603 let listeners_config = create_v0_147_0_listeners_config(config, mz);
1604
1605 let listeners_json = if mz.meets_minimum_version(&PER_ROUTE_GROUP_ROLES_VERSION) {
1609 let mut listeners_config: v26_32_0::ListenersConfig = listeners_config.into();
1614 if matches!(
1621 authenticator_kind,
1622 AuthenticatorKind::Password | AuthenticatorKind::Sasl | AuthenticatorKind::Oidc
1623 ) {
1624 listeners_config.http.get_mut("external").map(|listener| {
1625 listener.routes.internal = RouteGroup::Enabled(AllowedRoles::Internal);
1626 listener.routes.profiling = RouteGroup::Enabled(AllowedRoles::Internal);
1627 listener
1628 });
1629 }
1630
1631 serde_json::to_string(&VersionedListenersConfig::V2(listeners_config)).expect("known valid")
1632 } else {
1633 serde_json::to_string(&VersionedListenersConfig::V1(listeners_config)).expect("known valid")
1634 };
1635 let listeners_configmap = ConfigMap {
1636 binary_data: None,
1637 data: Some(btreemap! {
1638 "listeners.json".to_owned() => listeners_json,
1639 }),
1640 immutable: None,
1641 metadata: ObjectMeta {
1642 annotations: Some(btreemap! {
1643 "materialize.cloud/generation".to_owned() => generation.to_string(),
1644 }),
1645 ..mz.managed_resource_meta(mz.listeners_configmap_name(generation))
1646 },
1647 };
1648
1649 let (scheme, leader_api_port, mz_system_secret_name) = match authenticator_kind {
1650 AuthenticatorKind::Password | AuthenticatorKind::Sasl | AuthenticatorKind::Oidc => {
1651 let scheme = if external_enable_tls { "https" } else { "http" };
1652 (
1653 scheme,
1654 config.environmentd_http_port,
1655 Some(mz.spec.backend_secret_name.clone()),
1656 )
1657 }
1658 _ => ("http", config.environmentd_internal_http_port, None),
1659 };
1660 let environmentd_url = format!(
1661 "{}://{}.{}.svc.cluster.local:{}",
1662 scheme,
1663 mz.environmentd_generation_service_name(generation),
1664 mz.namespace(),
1665 leader_api_port,
1666 );
1667 ConnectionInfo {
1668 environmentd_url,
1669 listeners_configmap,
1670 mz_system_secret_name,
1671 }
1672}
1673
1674#[derive(Debug, Deserialize, PartialEq, Eq)]
1676struct BecomeLeaderResponse {
1677 result: BecomeLeaderResult,
1678}
1679
1680#[derive(Debug, Deserialize, PartialEq, Eq)]
1681enum BecomeLeaderResult {
1682 Success,
1683 Failure { message: String },
1684}
1685
1686#[derive(Debug, Deserialize, PartialEq, Eq)]
1687struct SkipCatchupError {
1688 message: String,
1689}
1690
1691#[cfg(test)]
1692mod tests {
1693 use k8s_openapi::api::core::v1::{HTTPGetAction, Probe, TCPSocketAction};
1694 use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString;
1695
1696 use super::{environmentd_probe, validate_probe_handlers};
1697
1698 fn base() -> Probe {
1699 Probe {
1700 initial_delay_seconds: Some(1),
1701 failure_threshold: Some(12),
1702 tcp_socket: Some(TCPSocketAction {
1703 host: None,
1704 port: IntOrString::Int(6877),
1705 }),
1706 ..Default::default()
1707 }
1708 }
1709
1710 #[mz_ore::test]
1711 fn environmentd_probe_without_overrides_is_base() {
1712 assert_eq!(environmentd_probe(base(), None, None), base());
1713 }
1714
1715 #[mz_ore::test]
1716 fn environmentd_probe_layers_fields() {
1717 let operator_default = Probe {
1718 failure_threshold: Some(30),
1719 period_seconds: Some(20),
1720 ..Default::default()
1721 };
1722 let environment_override = Probe {
1723 period_seconds: Some(5),
1724 ..Default::default()
1725 };
1726 assert_eq!(
1727 environmentd_probe(base(), Some(&operator_default), Some(&environment_override)),
1728 Probe {
1729 failure_threshold: Some(30),
1730 period_seconds: Some(5),
1731 ..base()
1732 }
1733 );
1734 }
1735
1736 #[mz_ore::test]
1737 fn environmentd_probe_replaces_handler_as_unit() {
1738 let http_get = HTTPGetAction {
1739 path: Some("/api/readyz".to_owned()),
1740 port: IntOrString::Int(6878),
1741 ..Default::default()
1742 };
1743 let environment_override = Probe {
1744 http_get: Some(http_get.clone()),
1745 ..Default::default()
1746 };
1747 assert_eq!(
1748 environmentd_probe(base(), None, Some(&environment_override)),
1749 Probe {
1750 tcp_socket: None,
1751 http_get: Some(http_get),
1752 ..base()
1753 }
1754 );
1755 }
1756
1757 #[mz_ore::test]
1758 fn validate_probe_handlers_rejects_multiple_handlers() {
1759 assert!(validate_probe_handlers(&Probe::default()).is_ok());
1760 assert!(validate_probe_handlers(&base()).is_ok());
1761 let both = Probe {
1762 http_get: Some(HTTPGetAction {
1763 port: IntOrString::Int(6878),
1764 ..Default::default()
1765 }),
1766 ..base()
1767 };
1768 let err = validate_probe_handlers(&both).unwrap_err();
1769 assert!(
1770 err.to_string().contains("httpGet, tcpSocket"),
1771 "unexpected error: {err}"
1772 );
1773 }
1774}