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_50_0: LazyLock<Version> = LazyLock::new(|| Version {
92 major: 26,
93 minor: 50,
94 patch: 0,
95 pre: Prerelease::new("dev.0").expect("dev.0 is valid prerelease"),
96 build: BuildMetadata::new("").expect("empty string is valid buildmetadata"),
97});
98
99static V26_1_0: LazyLock<Version> = LazyLock::new(|| Version {
100 major: 26,
101 minor: 1,
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
107static PER_ROUTE_GROUP_ROLES_VERSION: LazyLock<Version> = LazyLock::new(|| Version {
111 major: 26,
112 minor: 32,
113 patch: 0,
114 pre: Prerelease::new("dev.0").expect("dev.0 is valid prerelease"),
115 build: BuildMetadata::new("").expect("empty string is valid buildmetadata"),
116});
117
118#[derive(
123 Debug,
124 Serialize,
125 Deserialize,
126 Clone,
127 Copy,
128 PartialEq,
129 Eq,
130 PartialOrd,
131 Ord
132)]
133pub enum DeploymentStatus {
134 Initializing,
137 ReadyToPromote,
139 Promoting,
141 IsLeader,
143}
144
145#[derive(
146 Debug,
147 Serialize,
148 Deserialize,
149 Clone,
150 Copy,
151 PartialEq,
152 Eq,
153 PartialOrd,
154 Ord
155)]
156pub struct GetLeaderStatusResponse {
157 status: DeploymentStatus,
158}
159
160#[derive(Deserialize, Serialize)]
161pub struct LoginCredentials {
162 username: String,
163 password: String,
165}
166
167#[derive(Debug, Serialize)]
168pub struct ConnectionInfo {
169 pub environmentd_url: String,
170 pub mz_system_secret_name: Option<String>,
171 pub listeners_configmap: ConfigMap,
172}
173
174#[derive(Debug, Serialize)]
175pub struct Resources {
176 pub generation: u64,
177 pub public_service: Box<Service>,
178 pub generation_service: Box<Service>,
179 pub persist_pubsub_service: Box<Service>,
180 pub environmentd_statefulset: Box<StatefulSet>,
181 pub connection_info: Box<ConnectionInfo>,
182}
183
184impl Resources {
185 pub fn new(config: &super::Config, mz: &Materialize, generation: u64) -> Self {
186 let public_service = Box::new(create_public_service_object(config, mz, generation));
187 let generation_service = Box::new(create_generation_service_object(config, mz, generation));
188 let persist_pubsub_service =
189 Box::new(create_persist_pubsub_service(config, mz, generation));
190 let environmentd_statefulset = Box::new(create_environmentd_statefulset_object(
191 config, mz, generation,
192 ));
193 let connection_info = Box::new(create_connection_info(config, mz, generation));
194
195 Self {
196 generation,
197 public_service,
198 generation_service,
199 persist_pubsub_service,
200 environmentd_statefulset,
201 connection_info,
202 }
203 }
204
205 #[instrument]
206 pub async fn apply(
207 &self,
208 client: &Client,
209 force_promote: bool,
210 namespace: &str,
211 ) -> Result<Option<Action>, Error> {
212 let service_api: Api<Service> = Api::namespaced(client.clone(), namespace);
213 let statefulset_api: Api<StatefulSet> = Api::namespaced(client.clone(), namespace);
214 let configmap_api: Api<ConfigMap> = Api::namespaced(client.clone(), namespace);
215
216 trace!("applying environmentd per-generation service");
217 apply_resource(&service_api, &*self.generation_service).await?;
218
219 trace!("creating persist pubsub service");
220 apply_resource(&service_api, &*self.persist_pubsub_service).await?;
221
222 trace!("applying listeners configmap");
223 apply_resource(&configmap_api, &self.connection_info.listeners_configmap).await?;
224
225 trace!("creating new environmentd statefulset");
226 apply_resource(&statefulset_api, &*self.environmentd_statefulset).await?;
227
228 let retry_action = Action::requeue(Duration::from_secs(rand::random_range(5..10)));
229
230 let statefulset = get_resource(
231 &statefulset_api,
232 &self.environmentd_statefulset.name_unchecked(),
233 )
234 .await?;
235 if statefulset
236 .and_then(|statefulset| statefulset.status)
237 .and_then(|status| status.ready_replicas)
238 .unwrap_or(0)
239 == 0
240 {
241 trace!("environmentd statefulset is not ready yet...");
242 return Ok(Some(retry_action));
243 }
244
245 let Some(http_client) = self.get_http_client(client.clone(), namespace).await else {
246 return Ok(Some(retry_action));
247 };
248 let status_url = reqwest::Url::parse(&format!(
249 "{}/api/leader/status",
250 self.connection_info.environmentd_url,
251 ))
252 .unwrap();
253
254 match http_client.get(status_url.clone()).send().await {
255 Ok(response) => {
256 let response: GetLeaderStatusResponse = match response.error_for_status() {
257 Ok(response) => response.json().await?,
258 Err(e) => {
259 trace!("failed to get status of environmentd, retrying... ({e})");
260 return Ok(Some(retry_action));
261 }
262 };
263 if force_promote {
264 trace!("skipping cluster catchup");
265 let skip_catchup_url = reqwest::Url::parse(&format!(
266 "{}/api/leader/skip-catchup",
267 self.connection_info.environmentd_url,
268 ))
269 .unwrap();
270 let response = http_client.post(skip_catchup_url).send().await?;
271 if response.status() == StatusCode::BAD_REQUEST {
272 let err: SkipCatchupError = response.json().await?;
273 return Err(
274 anyhow::anyhow!("failed to skip catchup: {}", err.message).into()
275 );
276 }
277 } else {
278 match response.status {
279 DeploymentStatus::Initializing => {
280 trace!("environmentd is still initializing, retrying...");
281 return Ok(Some(retry_action));
282 }
283 DeploymentStatus::ReadyToPromote
284 | DeploymentStatus::Promoting
285 | DeploymentStatus::IsLeader => trace!("environmentd is ready"),
286 }
287 }
288 }
289 Err(e) => {
290 trace!("failed to connect to environmentd, retrying... ({e})");
291 return Ok(Some(retry_action));
292 }
293 }
294
295 Ok(None)
296 }
297
298 #[instrument]
299 async fn get_http_client(&self, client: Client, namespace: &str) -> Option<HttpClient> {
300 let secret_api: Api<Secret> = Api::namespaced(client.clone(), namespace);
301 Some(match &self.connection_info.mz_system_secret_name {
302 Some(mz_system_secret_name) => {
303 let http_client = reqwest::Client::builder()
304 .timeout(std::time::Duration::from_secs(10))
305 .cookie_store(true)
306 .danger_accept_invalid_certs(true)
308 .build()
309 .unwrap();
310
311 let secret = secret_api
312 .get(mz_system_secret_name)
313 .await
314 .map_err(|e| {
315 error!("Failed to get backend secret: {:?}", e);
316 e
317 })
318 .ok()?;
319 if let Some(data) = secret.data {
320 if let Some(password) = data.get("external_login_password_mz_system").cloned() {
321 let password = String::from_utf8_lossy(&password.0).to_string();
322 let login_url = reqwest::Url::parse(&format!(
323 "{}/api/login",
324 self.connection_info.environmentd_url,
325 ))
326 .unwrap();
327 match http_client
328 .post(login_url)
329 .body(
330 serde_json::to_string(&LoginCredentials {
331 username: "mz_system".to_owned(),
332 password,
333 })
334 .expect(
335 "Serializing a simple struct with utf8 strings doesn't fail.",
336 ),
337 )
338 .header("Content-Type", "application/json")
339 .send()
340 .await
341 {
342 Ok(response) => {
343 if let Err(e) = response.error_for_status() {
344 trace!("failed to login to environmentd, retrying... ({e})");
345 return None;
346 }
347 }
348 Err(e) => {
349 trace!("failed to connect to environmentd, retrying... ({e})");
350 return None;
351 }
352 };
353 }
354 };
355 http_client
356 }
357 None => reqwest::Client::builder()
358 .timeout(std::time::Duration::from_secs(10))
359 .build()
360 .unwrap(),
361 })
362 }
363
364 #[instrument]
365 pub async fn promote_services(
366 &self,
367 client: &Client,
368 namespace: &str,
369 ) -> Result<Option<Action>, Error> {
370 let service_api: Api<Service> = Api::namespaced(client.clone(), namespace);
371 let retry_action = Action::requeue(Duration::from_secs(rand::random_range(5..10)));
372
373 let promote_url = reqwest::Url::parse(&format!(
374 "{}/api/leader/promote",
375 self.connection_info.environmentd_url,
376 ))
377 .unwrap();
378
379 let Some(http_client) = self.get_http_client(client.clone(), namespace).await else {
380 return Ok(Some(retry_action));
381 };
382
383 trace!("promoting new environmentd to leader");
384 let response = http_client.post(promote_url).send().await?;
385 let response: BecomeLeaderResponse = response.error_for_status()?.json().await?;
386 if let BecomeLeaderResult::Failure { message } = response.result {
387 return Err(Error::Anyhow(anyhow::anyhow!(
388 "failed to promote new environmentd: {message}"
389 )));
390 }
391
392 let status_url = reqwest::Url::parse(&format!(
404 "{}/api/leader/status",
405 self.connection_info.environmentd_url,
406 ))
407 .unwrap();
408 match http_client.get(status_url.clone()).send().await {
409 Ok(response) => {
410 let response: GetLeaderStatusResponse = response.json().await?;
411 if response.status != DeploymentStatus::IsLeader {
412 trace!(
413 "environmentd is still promoting (status: {:?}), retrying...",
414 response.status
415 );
416 return Ok(Some(retry_action));
417 } else {
418 trace!("environmentd is ready");
419 }
420 }
421 Err(e) => {
422 trace!("failed to connect to environmentd, retrying... ({e})");
423 return Ok(Some(retry_action));
424 }
425 }
426
427 trace!("applying environmentd public service");
428 apply_resource(&service_api, &*self.public_service).await?;
429
430 Ok(None)
431 }
432
433 #[instrument]
434 pub async fn teardown_generation(
435 &self,
436 client: &Client,
437 mz: &Materialize,
438 generation: u64,
439 ) -> Result<(), anyhow::Error> {
440 let configmap_api: Api<ConfigMap> = Api::namespaced(client.clone(), &mz.namespace());
441 let service_api: Api<Service> = Api::namespaced(client.clone(), &mz.namespace());
442 let statefulset_api: Api<StatefulSet> = Api::namespaced(client.clone(), &mz.namespace());
443
444 trace!("deleting environmentd statefulset for generation {generation}");
445 delete_resource(
446 &statefulset_api,
447 &mz.environmentd_statefulset_name(generation),
448 )
449 .await?;
450
451 trace!("deleting persist pubsub service for generation {generation}");
452 delete_resource(&service_api, &mz.persist_pubsub_service_name(generation)).await?;
453
454 trace!("deleting environmentd per-generation service for generation {generation}");
455 delete_resource(
456 &service_api,
457 &mz.environmentd_generation_service_name(generation),
458 )
459 .await?;
460
461 trace!("deleting listeners configmap for generation {generation}");
462 delete_resource(&configmap_api, &mz.listeners_configmap_name(generation)).await?;
463
464 Ok(())
465 }
466
467 pub fn generate_hash(&self) -> String {
470 let mut hasher = Sha256::new();
471 hasher.update(&serde_json::to_string(self).unwrap());
472 hex::encode(hasher.finalize())
473 }
474}
475
476fn create_public_service_object(
477 config: &super::Config,
478 mz: &Materialize,
479 generation: u64,
480) -> Service {
481 let mut service = create_base_service_object(
482 config,
483 mz,
484 generation,
485 &mz.environmentd_service_name(),
486 true,
487 );
488 super::teleport::apply_teleport_registration(config, mz, &mut service);
492 service
493}
494
495fn create_generation_service_object(
496 config: &super::Config,
497 mz: &Materialize,
498 generation: u64,
499) -> Service {
500 create_base_service_object(
501 config,
502 mz,
503 generation,
504 &mz.environmentd_generation_service_name(generation),
505 false,
506 )
507}
508
509fn create_base_service_object(
510 config: &super::Config,
511 mz: &Materialize,
512 generation: u64,
513 service_name: &str,
514 headless: bool,
515) -> Service {
516 let ports = vec![
517 ServicePort {
518 port: config.environmentd_sql_port.into(),
519 protocol: Some("TCP".to_string()),
520 name: Some("sql".to_string()),
521 ..Default::default()
522 },
523 ServicePort {
524 port: config.environmentd_http_port.into(),
525 protocol: Some("TCP".to_string()),
526 name: Some("https".to_string()),
527 ..Default::default()
528 },
529 ServicePort {
530 port: config.environmentd_internal_sql_port.into(),
531 protocol: Some("TCP".to_string()),
532 name: Some("internal-sql".to_string()),
533 ..Default::default()
534 },
535 ServicePort {
536 port: config.environmentd_internal_http_port.into(),
537 protocol: Some("TCP".to_string()),
538 name: Some("internal-http".to_string()),
539 ..Default::default()
540 },
541 ];
542
543 let selector = btreemap! {"materialize.cloud/name".to_string() => mz.environmentd_statefulset_name(generation)};
544
545 let spec = ServiceSpec {
546 type_: Some("ClusterIP".to_string()),
547 cluster_ip: if headless {
548 Some("None".to_string())
549 } else {
550 None
551 },
552 selector: Some(selector),
553 ports: Some(ports),
554 ..Default::default()
555 };
556
557 Service {
558 metadata: mz.managed_resource_meta(service_name.to_string()),
559 spec: Some(spec),
560 status: None,
561 }
562}
563
564fn create_persist_pubsub_service(
565 config: &super::Config,
566 mz: &Materialize,
567 generation: u64,
568) -> Service {
569 Service {
570 metadata: mz.managed_resource_meta(mz.persist_pubsub_service_name(generation)),
571 spec: Some(ServiceSpec {
572 type_: Some("ClusterIP".to_string()),
573 cluster_ip: Some("None".to_string()),
574 selector: Some(btreemap! {
575 "materialize.cloud/name".to_string() => mz.environmentd_statefulset_name(generation),
576 }),
577 ports: Some(vec![ServicePort {
578 name: Some("grpc".to_string()),
579 protocol: Some("TCP".to_string()),
580 port: config.environmentd_internal_persist_pubsub_port.into(),
581 ..Default::default()
582 }]),
583 ..Default::default()
584 }),
585 status: None,
586 }
587}
588
589fn create_environmentd_statefulset_object(
590 config: &super::Config,
591 mz: &Materialize,
592 generation: u64,
593) -> StatefulSet {
594 let mut env = vec![
604 EnvVar {
605 name: "MZ_METADATA_BACKEND_URL".to_string(),
606 value_from: Some(EnvVarSource {
607 secret_key_ref: Some(SecretKeySelector {
608 name: mz.backend_secret_name(),
609 key: "metadata_backend_url".to_string(),
610 optional: Some(false),
611 }),
612 ..Default::default()
613 }),
614 ..Default::default()
615 },
616 EnvVar {
617 name: "MZ_PERSIST_BLOB_URL".to_string(),
618 value_from: Some(EnvVarSource {
619 secret_key_ref: Some(SecretKeySelector {
620 name: mz.backend_secret_name(),
621 key: "persist_backend_url".to_string(),
622 optional: Some(false),
623 }),
624 ..Default::default()
625 }),
626 ..Default::default()
627 },
628 ];
629
630 env.push(EnvVar {
631 name: "AWS_REGION".to_string(),
632 value: Some(config.region.clone()),
633 ..Default::default()
634 });
635
636 env.extend(mz.spec.environmentd_extra_env.iter().flatten().cloned());
637
638 let mut args = vec![];
639
640 if let Some(helm_chart_version) = &config.helm_chart_version {
641 args.push(format!("--helm-chart-version={helm_chart_version}"));
642 }
643
644 args.push(format!(
646 "--environment-id={}",
647 mz.environment_id(&config.cloud_provider.to_string(), &config.region)
648 ));
649
650 args.push(format!(
652 "--clusterd-image={}",
653 matching_image_from_environmentd_image_ref(
654 &mz.spec.environmentd_image_ref,
655 "clusterd",
656 None
657 )
658 ));
659
660 args.extend(
662 [
663 config
664 .environmentd_cluster_replica_sizes
665 .as_ref()
666 .map(|sizes| format!("--cluster-replica-sizes={sizes}")),
667 config
668 .bootstrap_default_cluster_replica_size
669 .as_ref()
670 .map(|size| format!("--bootstrap-default-cluster-replica-size={size}")),
671 config
672 .bootstrap_builtin_system_cluster_replica_size
673 .as_ref()
674 .map(|size| format!("--bootstrap-builtin-system-cluster-replica-size={size}")),
675 config
676 .bootstrap_builtin_probe_cluster_replica_size
677 .as_ref()
678 .map(|size| format!("--bootstrap-builtin-probe-cluster-replica-size={size}")),
679 config
680 .bootstrap_builtin_support_cluster_replica_size
681 .as_ref()
682 .map(|size| format!("--bootstrap-builtin-support-cluster-replica-size={size}")),
683 config
684 .bootstrap_builtin_catalog_server_cluster_replica_size
685 .as_ref()
686 .map(|size| format!("--bootstrap-builtin-catalog-server-cluster-replica-size={size}")),
687 config
688 .bootstrap_builtin_analytics_cluster_replica_size
689 .as_ref()
690 .map(|size| format!("--bootstrap-builtin-analytics-cluster-replica-size={size}")),
691 config
692 .bootstrap_builtin_system_cluster_replication_factor
693 .as_ref()
694 .map(|replication_factor| {
695 format!("--bootstrap-builtin-system-cluster-replication-factor={replication_factor}")
696 }),
697 config
698 .bootstrap_builtin_probe_cluster_replication_factor
699 .as_ref()
700 .map(|replication_factor| format!("--bootstrap-builtin-probe-cluster-replication-factor={replication_factor}")),
701 config
702 .bootstrap_builtin_support_cluster_replication_factor
703 .as_ref()
704 .map(|replication_factor| format!("--bootstrap-builtin-support-cluster-replication-factor={replication_factor}")),
705 config
706 .bootstrap_builtin_analytics_cluster_replication_factor
707 .as_ref()
708 .map(|replication_factor| format!("--bootstrap-builtin-analytics-cluster-replication-factor={replication_factor}")),
709 ]
710 .into_iter()
711 .flatten(),
712 );
713
714 args.extend(
715 config
716 .environmentd_allowed_origins
717 .iter()
718 .map(|origin| format!("--cors-allowed-origin={}", origin.to_str().unwrap())),
719 );
720
721 args.push(format!(
722 "--secrets-controller={}",
723 config.secrets_controller
724 ));
725
726 if let Some(cluster_replica_sizes) = &config.environmentd_cluster_replica_sizes {
727 if let Ok(cluster_replica_sizes) =
728 serde_json::from_str::<BTreeMap<String, serde_json::Value>>(cluster_replica_sizes)
729 {
730 let cluster_replica_sizes: Vec<_> =
731 cluster_replica_sizes.keys().map(|s| s.as_str()).collect();
732 args.push(format!(
733 "--system-parameter-default=allowed_cluster_replica_sizes='{}'",
734 cluster_replica_sizes.join("', '")
735 ));
736 }
737 }
738 if !config.cloud_provider.is_cloud() {
739 args.push("--system-parameter-default=cluster_enable_topology_spread=false".into());
740 }
741
742 if config.enable_internal_statement_logging {
743 args.push("--system-parameter-default=enable_internal_statement_logging=true".into());
744 }
745
746 if let Some(rate) = config.statement_logging_max_sample_rate {
747 args.push(format!(
748 "--system-parameter-default=statement_logging_max_sample_rate={rate}"
749 ));
750 }
751
752 if let Some(rate) = config.statement_logging_target_data_rate {
753 args.push(format!(
754 "--system-parameter-default=statement_logging_target_data_rate={rate}"
755 ));
756 }
757
758 if !mz.spec.enable_rbac {
759 args.push("--system-parameter-default=enable_rbac_checks=false".into());
760 }
761
762 if config.disable_database_network_policies {
763 args.push("--system-parameter-default=enable_network_policies=false".into());
764 }
765
766 args.push("--persist-isolated-runtime-threads=-1".to_string());
770
771 if config.cloud_provider == CloudProvider::Aws {
773 if let Some(azs) = config.environmentd_availability_zones.as_ref() {
774 for az in azs {
775 args.push(format!("--availability-zone={az}"));
776 }
777 }
778
779 if let Some(environmentd_connection_role_arn) = mz
780 .spec
781 .environmentd_connection_role_arn
782 .as_deref()
783 .or(config.environmentd_connection_role_arn.as_deref())
784 {
785 args.push(format!(
786 "--aws-connection-role-arn={}",
787 environmentd_connection_role_arn
788 ));
789 }
790 if let Some(account_id) = &config.aws_account_id {
791 args.push(format!("--aws-account-id={account_id}"));
792 }
793
794 args.extend([format!(
795 "--aws-secrets-controller-tags=Environment={}",
796 mz.name_unchecked()
797 )]);
798 args.extend_from_slice(&config.aws_secrets_controller_tags);
799 }
800
801 args.extend([
803 "--orchestrator=kubernetes".into(),
804 format!(
805 "--orchestrator-kubernetes-service-account={}",
806 mz.service_account_name()
807 ),
808 format!(
809 "--orchestrator-kubernetes-image-pull-policy={}",
810 config.image_pull_policy.as_kebab_case_str(),
811 ),
812 ]);
813 for selector in &config.clusterd_node_selector {
814 args.push(format!(
815 "--orchestrator-kubernetes-service-node-selector={}={}",
816 selector.key, selector.value,
817 ));
818 }
819 if mz.meets_minimum_version(&V144) {
820 if let Some(affinity) = &config.clusterd_affinity {
821 let affinity = serde_json::to_string(affinity).unwrap();
822 args.push(format!(
823 "--orchestrator-kubernetes-service-affinity={affinity}"
824 ))
825 }
826 if let Some(tolerations) = &config.clusterd_tolerations {
827 let tolerations = serde_json::to_string(tolerations).unwrap();
828 args.push(format!(
829 "--orchestrator-kubernetes-service-tolerations={tolerations}"
830 ))
831 }
832 }
833 if let Some(scheduler_name) = &config.scheduler_name {
834 args.push(format!(
835 "--orchestrator-kubernetes-scheduler-name={}",
836 scheduler_name
837 ));
838 }
839 if mz.meets_minimum_version(&V26_42_0) {
840 if let Some(priority_class_name) = &config.clusterd_priority_class_name {
841 args.push(format!(
842 "--orchestrator-kubernetes-priority-class-name={priority_class_name}"
843 ));
844 }
845 }
846 if mz.meets_minimum_version(&V154_DEV0) {
847 args.extend(
848 mz.spec
849 .pod_annotations
850 .as_ref()
851 .map(|annotations| annotations.iter())
852 .unwrap_or_default()
853 .map(|(key, val)| {
854 format!("--orchestrator-kubernetes-service-annotation={key}={val}")
855 }),
856 );
857 }
858 args.extend(
859 mz.default_labels()
860 .iter()
861 .chain(
862 mz.spec
863 .pod_labels
864 .as_ref()
865 .map(|labels| labels.iter())
866 .unwrap_or_default(),
867 )
868 .map(|(key, val)| format!("--orchestrator-kubernetes-service-label={key}={val}")),
869 );
870 if let Some(status) = &mz.status {
871 args.push(format!(
872 "--orchestrator-kubernetes-name-prefix=mz{}-",
873 status.resource_id
874 ));
875 }
876
877 args.extend(["--log-format=json".into()]);
879 if let Some(endpoint) = &config.tracing.opentelemetry_endpoint {
880 args.push(format!("--opentelemetry-endpoint={}", endpoint));
881 }
882 args.extend([
884 format!(
885 "--opentelemetry-resource=organization_id={}",
886 mz.spec.environment_id
887 ),
888 format!(
889 "--opentelemetry-resource=environment_name={}",
890 mz.name_unchecked()
891 ),
892 ]);
893
894 if let Some(segment_api_key) = &config.segment_api_key {
895 args.push(format!("--segment-api-key={}", segment_api_key));
896 if config.segment_client_side {
897 args.push("--segment-client-side".into());
898 }
899 }
900
901 let mut volumes = Vec::new();
902 let mut volume_mounts = Vec::new();
903 if issuer_ref_defined(
904 &config.default_certificate_specs.internal,
905 &mz.spec.internal_certificate_spec,
906 ) {
907 volumes.push(Volume {
908 name: "certificate".to_owned(),
909 secret: Some(SecretVolumeSource {
910 default_mode: Some(0o400),
911 secret_name: Some(mz.environmentd_certificate_secret_name()),
912 items: None,
913 optional: Some(false),
914 }),
915 ..Default::default()
916 });
917 volume_mounts.push(VolumeMount {
918 name: "certificate".to_owned(),
919 mount_path: "/etc/materialized".to_owned(),
920 read_only: Some(true),
921 ..Default::default()
922 });
923 args.extend([
924 "--tls-mode=require".into(),
925 "--tls-cert=/etc/materialized/tls.crt".into(),
926 "--tls-key=/etc/materialized/tls.key".into(),
927 ]);
928 } else {
929 args.push("--tls-mode=disable".to_string());
930 }
931 if let Some(ephemeral_volume_class) = &config.ephemeral_volume_class {
932 args.push(format!(
933 "--orchestrator-kubernetes-ephemeral-volume-class={}",
934 ephemeral_volume_class
935 ));
936 }
937 args.push("--orchestrator-kubernetes-service-fs-group=999".to_string());
939
940 if mz.meets_minimum_version(&V26_1_0) {
944 if let Some(ref name) = mz.spec.system_parameter_configmap_name {
945 volumes.push(Volume {
946 name: "system-params".to_string(),
947 config_map: Some(ConfigMapVolumeSource {
948 default_mode: Some(0o400),
949 name: name.to_owned(),
950 items: None,
951 optional: Some(true),
952 }),
953 ..Default::default()
954 });
955 volume_mounts.push(VolumeMount {
956 name: "system-params".to_string(),
957 mount_path: "/system-params".to_owned(),
959 read_only: Some(true),
960 ..Default::default()
961 });
962 args.push("--config-sync-file-path=/system-params/system-params.json".to_string());
963 args.push("--config-sync-loop-interval=1s".to_string());
964 }
965 }
966
967 if let Some(sentry_dsn) = &config.tracing.sentry_dsn {
969 args.push(format!("--sentry-dsn={}", sentry_dsn));
970 if let Some(sentry_environment) = &config.tracing.sentry_environment {
971 args.push(format!("--sentry-environment={}", sentry_environment));
972 }
973 args.push(format!("--sentry-tag=region={}", config.region));
974 }
975
976 args.push(format!(
978 "--persist-pubsub-url=http://{}:{}",
979 mz.persist_pubsub_service_name(generation),
980 config.environmentd_internal_persist_pubsub_port,
981 ));
982 args.push(format!(
983 "--internal-persist-pubsub-listen-addr=0.0.0.0:{}",
984 config.environmentd_internal_persist_pubsub_port
985 ));
986
987 args.push(format!("--deploy-generation={}", generation));
988
989 args.push(format!(
991 "--internal-console-redirect-url={}",
992 config.internal_console_proxy_url,
993 ));
994
995 if !config.collect_pod_metrics {
996 args.push("--orchestrator-kubernetes-disable-pod-metrics-collection".into());
997 }
998 if config.enable_prometheus_scrape_annotations {
999 args.push("--orchestrator-kubernetes-enable-prometheus-scrape-annotations".into());
1000 }
1001 if config.enable_user_namespaces && mz.meets_minimum_version(&V26_50_0) {
1004 args.push("--orchestrator-kubernetes-enable-user-namespaces".into());
1005 }
1006
1007 if config.disable_license_key_checks {
1010 if mz.meets_minimum_version(&V143) && !mz.meets_minimum_version(&V153) {
1011 args.push("--disable-license-key-checks".into());
1012 }
1013 }
1014
1015 if (mz.meets_minimum_version(&V140_DEV0) && !config.disable_license_key_checks)
1018 || mz.meets_minimum_version(&V153)
1019 {
1020 volume_mounts.push(VolumeMount {
1021 name: "license-key".to_string(),
1022 mount_path: "/license_key".to_string(),
1023 ..Default::default()
1024 });
1025 volumes.push(Volume {
1026 name: "license-key".to_string(),
1027 secret: Some(SecretVolumeSource {
1028 default_mode: Some(256),
1029 optional: Some(false),
1030 secret_name: Some(mz.backend_secret_name()),
1031 items: Some(vec![KeyToPath {
1032 key: "license_key".to_string(),
1033 path: "license_key".to_string(),
1034 ..Default::default()
1035 }]),
1036 ..Default::default()
1037 }),
1038 ..Default::default()
1039 });
1040 env.push(EnvVar {
1041 name: "MZ_LICENSE_KEY".to_string(),
1042 value: Some("/license_key/license_key".to_string()),
1043 ..Default::default()
1044 });
1045 }
1046
1047 if let Some(extra_args) = &mz.spec.environmentd_extra_args {
1049 args.extend(extra_args.iter().cloned());
1050 }
1051
1052 let listeners_config = create_v0_147_0_listeners_config(config, mz);
1061 let probe_port = listeners_config
1062 .sql
1063 .get("internal")
1064 .or_else(|| listeners_config.sql.get("external"))
1065 .expect("external SQL listener is always configured")
1066 .addr
1067 .port();
1068 let base_probe = Probe {
1069 initial_delay_seconds: Some(1),
1070 failure_threshold: Some(12),
1071 tcp_socket: Some(TCPSocketAction {
1072 host: None,
1073 port: IntOrString::Int(probe_port.into()),
1074 }),
1075 ..Default::default()
1076 };
1077 let readiness_probe = environmentd_probe(
1078 base_probe.clone(),
1079 config.environmentd_default_readiness_probe.as_ref(),
1080 mz.spec.environmentd_readiness_probe.as_ref(),
1081 );
1082 let liveness_probe = environmentd_probe(
1083 base_probe.clone(),
1084 config.environmentd_default_liveness_probe.as_ref(),
1085 mz.spec.environmentd_liveness_probe.as_ref(),
1086 );
1087 let startup_probe = (config.environmentd_default_startup_probe.is_some()
1090 || mz.spec.environmentd_startup_probe.is_some())
1091 .then(|| {
1092 environmentd_probe(
1093 base_probe,
1094 config.environmentd_default_startup_probe.as_ref(),
1095 mz.spec.environmentd_startup_probe.as_ref(),
1096 )
1097 });
1098
1099 let security_context = if config.enable_security_context {
1100 Some(SecurityContext {
1104 run_as_non_root: Some(true),
1105 capabilities: Some(Capabilities {
1106 drop: Some(vec!["ALL".to_string()]),
1107 ..Default::default()
1108 }),
1109 seccomp_profile: Some(SeccompProfile {
1110 type_: "RuntimeDefault".to_string(),
1111 ..Default::default()
1112 }),
1113 allow_privilege_escalation: Some(false),
1114 ..Default::default()
1115 })
1116 } else {
1117 None
1118 };
1119
1120 let ports = vec![
1121 ContainerPort {
1122 container_port: config.environmentd_sql_port.into(),
1123 name: Some("sql".to_owned()),
1124 ..Default::default()
1125 },
1126 ContainerPort {
1127 container_port: config.environmentd_internal_sql_port.into(),
1128 name: Some("internal-sql".to_owned()),
1129 ..Default::default()
1130 },
1131 ContainerPort {
1132 container_port: config.environmentd_http_port.into(),
1133 name: Some("http".to_owned()),
1134 ..Default::default()
1135 },
1136 ContainerPort {
1137 container_port: config.environmentd_internal_http_port.into(),
1138 name: Some("internal-http".to_owned()),
1139 ..Default::default()
1140 },
1141 ContainerPort {
1142 container_port: config.environmentd_internal_persist_pubsub_port.into(),
1143 name: Some("persist-pubsub".to_owned()),
1144 ..Default::default()
1145 },
1146 ];
1147
1148 if mz.meets_minimum_version(&V147_DEV0) {
1150 volume_mounts.push(VolumeMount {
1151 name: "listeners-configmap".to_string(),
1152 mount_path: "/listeners".to_string(),
1153 ..Default::default()
1154 });
1155 volumes.push(Volume {
1156 name: "listeners-configmap".to_string(),
1157 config_map: Some(ConfigMapVolumeSource {
1158 name: mz.listeners_configmap_name(generation),
1159 default_mode: Some(256),
1160 optional: Some(false),
1161 items: Some(vec![KeyToPath {
1162 key: "listeners.json".to_string(),
1163 path: "listeners.json".to_string(),
1164 ..Default::default()
1165 }]),
1166 }),
1167 ..Default::default()
1168 });
1169 args.push("--listeners-config-path=/listeners/listeners.json".to_owned());
1170 if matches!(
1171 mz.spec.authenticator_kind,
1172 AuthenticatorKind::Password | AuthenticatorKind::Sasl | AuthenticatorKind::Oidc
1173 ) {
1174 args.push("--system-parameter-default=enable_password_auth=true".into());
1175 env.push(EnvVar {
1176 name: "MZ_EXTERNAL_LOGIN_PASSWORD_MZ_SYSTEM".to_string(),
1177 value_from: Some(EnvVarSource {
1178 secret_key_ref: Some(SecretKeySelector {
1179 name: mz.backend_secret_name(),
1180 key: "external_login_password_mz_system".to_string(),
1181 optional: Some(false),
1182 }),
1183 ..Default::default()
1184 }),
1185 ..Default::default()
1186 })
1187 }
1188 } else {
1189 args.extend([
1190 format!("--sql-listen-addr=0.0.0.0:{}", config.environmentd_sql_port),
1191 format!(
1192 "--http-listen-addr=0.0.0.0:{}",
1193 config.environmentd_http_port
1194 ),
1195 format!(
1196 "--internal-sql-listen-addr=0.0.0.0:{}",
1197 config.environmentd_internal_sql_port
1198 ),
1199 format!(
1200 "--internal-http-listen-addr=0.0.0.0:{}",
1201 config.environmentd_internal_http_port
1202 ),
1203 ]);
1204 }
1205
1206 let container = Container {
1207 name: "environmentd".to_owned(),
1208 image: Some(mz.spec.environmentd_image_ref.to_owned()),
1209 image_pull_policy: Some(config.image_pull_policy.to_string()),
1210 ports: Some(ports),
1211 args: Some(args),
1212 env: Some(env),
1213 volume_mounts: Some(volume_mounts),
1214 startup_probe,
1215 liveness_probe: Some(liveness_probe),
1216 readiness_probe: Some(readiness_probe),
1217 resources: mz
1218 .spec
1219 .environmentd_resource_requirements
1220 .clone()
1221 .or_else(|| config.environmentd_default_resources.clone()),
1222 security_context: security_context.clone(),
1223 ..Default::default()
1224 };
1225
1226 let mut pod_template_labels = mz.default_labels();
1227 pod_template_labels.insert(
1228 "materialize.cloud/name".to_owned(),
1229 mz.environmentd_statefulset_name(generation),
1230 );
1231 pod_template_labels.insert(
1232 "materialize.cloud/app".to_owned(),
1233 mz.environmentd_app_name(),
1234 );
1235 pod_template_labels.extend(recommended_k8s_labels("environmentd".into()));
1236 pod_template_labels.extend(
1237 mz.spec
1238 .pod_labels
1239 .as_ref()
1240 .map(|labels| labels.iter())
1241 .unwrap_or_default()
1242 .map(|(key, value)| (key.clone(), value.clone())),
1243 );
1244
1245 let mut pod_template_annotations = btreemap! {
1246 "cluster-autoscaler.kubernetes.io/safe-to-evict".to_owned() => "false".to_string(),
1248
1249 "karpenter.sh/do-not-evict".to_owned() => "true".to_string(),
1251 "karpenter.sh/do-not-disrupt".to_owned() => "true".to_string(),
1252 "materialize.cloud/generation".to_owned() => generation.to_string(),
1253 };
1254 if config.enable_prometheus_scrape_annotations {
1255 pod_template_annotations.insert("prometheus.io/scrape".to_owned(), "true".to_string());
1256 pod_template_annotations.insert(
1257 "prometheus.io/port".to_owned(),
1258 config.environmentd_internal_http_port.to_string(),
1259 );
1260 pod_template_annotations.insert("prometheus.io/path".to_owned(), "/metrics".to_string());
1261 pod_template_annotations.insert("prometheus.io/scheme".to_owned(), "http".to_string());
1262 pod_template_annotations.insert(
1263 "materialize.prometheus.io/mz_usage_path".to_owned(),
1264 "/metrics/mz_usage".to_string(),
1265 );
1266 pod_template_annotations.insert(
1267 "materialize.prometheus.io/mz_frontier_path".to_owned(),
1268 "/metrics/mz_frontier".to_string(),
1269 );
1270 pod_template_annotations.insert(
1271 "materialize.prometheus.io/mz_compute_path".to_owned(),
1272 "/metrics/mz_compute".to_string(),
1273 );
1274 pod_template_annotations.insert(
1275 "materialize.prometheus.io/mz_storage_path".to_owned(),
1276 "/metrics/mz_storage".to_string(),
1277 );
1278 }
1279 pod_template_annotations.extend(
1280 mz.spec
1281 .pod_annotations
1282 .as_ref()
1283 .map(|annotations| annotations.iter())
1284 .unwrap_or_default()
1285 .map(|(key, value)| (key.clone(), value.clone())),
1286 );
1287
1288 let mut tolerations = vec![
1289 Toleration {
1293 effect: Some("NoExecute".into()),
1294 key: Some("node.kubernetes.io/not-ready".into()),
1295 operator: Some("Exists".into()),
1296 toleration_seconds: Some(30),
1297 value: None,
1298 },
1299 Toleration {
1300 effect: Some("NoExecute".into()),
1301 key: Some("node.kubernetes.io/unreachable".into()),
1302 operator: Some("Exists".into()),
1303 toleration_seconds: Some(30),
1304 value: None,
1305 },
1306 ];
1307 if let Some(user_tolerations) = &config.environmentd_tolerations {
1308 tolerations.extend(user_tolerations.iter().cloned());
1309 }
1310 let tolerations = Some(tolerations);
1311
1312 let pod_template_spec = PodTemplateSpec {
1313 metadata: Some(ObjectMeta {
1316 labels: Some(pod_template_labels),
1317 annotations: Some(pod_template_annotations), ..Default::default()
1319 }),
1320 spec: Some(PodSpec {
1321 containers: vec![container],
1322 node_selector: Some(
1323 config
1324 .environmentd_node_selector
1325 .iter()
1326 .map(|selector| (selector.key.clone(), selector.value.clone()))
1327 .collect(),
1328 ),
1329 affinity: config.environmentd_affinity.clone(),
1330 scheduler_name: config.scheduler_name.clone(),
1331 priority_class_name: config.environmentd_priority_class_name.clone(),
1332 service_account_name: Some(mz.service_account_name()),
1333 volumes: Some(volumes),
1334 security_context: Some(PodSecurityContext {
1335 fs_group: Some(999),
1336 run_as_user: Some(999),
1337 run_as_group: Some(999),
1338 ..Default::default()
1339 }),
1340 host_users: config.enable_user_namespaces.then_some(false),
1344 tolerations,
1345 termination_grace_period_seconds: Some(0),
1366 ..Default::default()
1367 }),
1368 };
1369
1370 let mut match_labels = BTreeMap::new();
1371 match_labels.insert(
1372 "materialize.cloud/name".to_owned(),
1373 mz.environmentd_statefulset_name(generation),
1374 );
1375
1376 let statefulset_spec = StatefulSetSpec {
1377 replicas: Some(1),
1378 template: pod_template_spec,
1379 service_name: Some(mz.environmentd_service_name()),
1380 selector: LabelSelector {
1381 match_expressions: None,
1382 match_labels: Some(match_labels),
1383 },
1384 ..Default::default()
1385 };
1386
1387 StatefulSet {
1388 metadata: ObjectMeta {
1389 annotations: Some(btreemap! {
1390 "materialize.cloud/generation".to_owned() => generation.to_string(),
1391 "materialize.cloud/force".to_owned() => mz.force_rollout_value(),
1392 }),
1393 ..mz.managed_resource_meta(mz.environmentd_statefulset_name(generation))
1394 },
1395 spec: Some(statefulset_spec),
1396 status: None,
1397 }
1398}
1399
1400pub fn validate_probe_handlers(probe: &Probe) -> Result<(), anyhow::Error> {
1406 let handlers: Vec<_> = [
1407 ("exec", probe.exec.is_some()),
1408 ("grpc", probe.grpc.is_some()),
1409 ("httpGet", probe.http_get.is_some()),
1410 ("tcpSocket", probe.tcp_socket.is_some()),
1411 ]
1412 .into_iter()
1413 .filter_map(|(name, set)| set.then_some(name))
1414 .collect();
1415 if handlers.len() > 1 {
1416 anyhow::bail!(
1417 "probe sets more than one handler ({}), but at most one is allowed",
1418 handlers.join(", ")
1419 );
1420 }
1421 Ok(())
1422}
1423
1424fn environmentd_probe(
1431 base: Probe,
1432 operator_default: Option<&Probe>,
1433 environment_override: Option<&Probe>,
1434) -> Probe {
1435 [operator_default, environment_override]
1436 .into_iter()
1437 .flatten()
1438 .fold(base, overlay_probe)
1439}
1440
1441fn overlay_probe(base: Probe, overlay: &Probe) -> Probe {
1442 let Probe {
1445 exec,
1446 failure_threshold,
1447 grpc,
1448 http_get,
1449 initial_delay_seconds,
1450 period_seconds,
1451 success_threshold,
1452 tcp_socket,
1453 termination_grace_period_seconds,
1454 timeout_seconds,
1455 } = overlay.clone();
1456 let has_handler =
1457 exec.is_some() || grpc.is_some() || http_get.is_some() || tcp_socket.is_some();
1458 let (exec, grpc, http_get, tcp_socket) = if has_handler {
1459 (exec, grpc, http_get, tcp_socket)
1460 } else {
1461 (base.exec, base.grpc, base.http_get, base.tcp_socket)
1462 };
1463 Probe {
1464 exec,
1465 failure_threshold: failure_threshold.or(base.failure_threshold),
1466 grpc,
1467 http_get,
1468 initial_delay_seconds: initial_delay_seconds.or(base.initial_delay_seconds),
1469 period_seconds: period_seconds.or(base.period_seconds),
1470 success_threshold: success_threshold.or(base.success_threshold),
1471 tcp_socket,
1472 termination_grace_period_seconds: termination_grace_period_seconds
1473 .or(base.termination_grace_period_seconds),
1474 timeout_seconds: timeout_seconds.or(base.timeout_seconds),
1475 }
1476}
1477
1478fn create_v0_147_0_listeners_config(
1479 config: &super::Config,
1480 mz: &Materialize,
1481) -> v0_147_0::ListenersConfig {
1482 let external_enable_tls = issuer_ref_defined(
1483 &config.default_certificate_specs.internal,
1484 &mz.spec.internal_certificate_spec,
1485 );
1486 let authenticator_kind = mz.spec.authenticator_kind;
1487
1488 let mut listeners_config = v0_147_0::ListenersConfig {
1489 sql: btreemap! {
1490 "external".to_owned() => SqlListenerConfig{
1491 addr: SocketAddr::new(
1492 IpAddr::V4(Ipv4Addr::new(0,0,0,0)),
1493 config.environmentd_sql_port,
1494 ),
1495 authenticator_kind,
1496 allowed_roles: AllowedRoles::Normal,
1497 enable_tls: external_enable_tls,
1498 },
1499 "internal".to_owned() => SqlListenerConfig{
1500 addr: SocketAddr::new(
1501 IpAddr::V4(Ipv4Addr::new(0,0,0,0)),
1502 config.environmentd_internal_sql_port,
1503 ),
1504 authenticator_kind: AuthenticatorKind::None,
1505 allowed_roles: AllowedRoles::NormalAndInternal,
1507 enable_tls: false,
1508 },
1509 },
1510 http: btreemap! {
1511 "external".to_owned() => v0_147_0::HttpListenerConfig{
1512 base: BaseListenerConfig {
1513 addr: SocketAddr::new(
1514 IpAddr::V4(Ipv4Addr::new(0,0,0,0)),
1515 config.environmentd_http_port,
1516 ),
1517 authenticator_kind: if authenticator_kind == AuthenticatorKind::Sasl {
1521 AuthenticatorKind::Password
1522 } else {
1523 authenticator_kind
1524 },
1525 allowed_roles: AllowedRoles::Normal,
1526 enable_tls: external_enable_tls,
1527 },
1528 routes: v0_147_0::HttpRoutes{
1529 base: true,
1530 webhook: true,
1531 internal: false,
1532 metrics: false,
1533 profiling: false,
1534 mcp_agent: true,
1535 mcp_developer: true,
1536 console_config: true,
1537 },
1538 },
1539 "internal".to_owned() => v0_147_0::HttpListenerConfig{
1540 base: BaseListenerConfig {
1541 addr: SocketAddr::new(
1542 IpAddr::V4(Ipv4Addr::new(0,0,0,0)),
1543 config.environmentd_internal_http_port,
1544 ),
1545 authenticator_kind: AuthenticatorKind::None,
1546 allowed_roles: AllowedRoles::NormalAndInternal,
1548 enable_tls: false,
1549 },
1550 routes: v0_147_0::HttpRoutes{
1551 base: true,
1552 webhook: true,
1553 internal: true,
1554 metrics: true,
1555 profiling: true,
1556 mcp_agent: true,
1557 mcp_developer: true,
1558 console_config: false,
1559 },
1560 },
1561 },
1562 };
1563
1564 if matches!(
1567 authenticator_kind,
1568 AuthenticatorKind::Password | AuthenticatorKind::Sasl | AuthenticatorKind::Oidc
1569 ) {
1570 listeners_config.sql.remove("internal");
1571 listeners_config.http.remove("internal");
1572
1573 listeners_config.sql.get_mut("external").map(|listener| {
1574 listener.allowed_roles = AllowedRoles::NormalAndInternal;
1575 listener
1576 });
1577 listeners_config.http.get_mut("external").map(|listener| {
1578 listener.base.allowed_roles = AllowedRoles::NormalAndInternal;
1579 listener.routes.internal = true;
1580 listener.routes.profiling = true;
1581 listener
1582 });
1583
1584 listeners_config.http.insert(
1585 "metrics".to_owned(),
1586 v0_147_0::HttpListenerConfig {
1587 base: BaseListenerConfig {
1588 addr: SocketAddr::new(
1589 IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
1590 config.environmentd_internal_http_port,
1591 ),
1592 authenticator_kind: AuthenticatorKind::None,
1593 allowed_roles: AllowedRoles::NormalAndInternal,
1594 enable_tls: false,
1595 },
1596 routes: v0_147_0::HttpRoutes {
1597 base: false,
1598 webhook: false,
1599 internal: false,
1600 metrics: true,
1601 profiling: false,
1602 mcp_agent: false,
1603 mcp_developer: false,
1604 console_config: false,
1605 },
1606 },
1607 );
1608 };
1609 listeners_config
1610}
1611
1612fn create_connection_info(
1613 config: &super::Config,
1614 mz: &Materialize,
1615 generation: u64,
1616) -> ConnectionInfo {
1617 let external_enable_tls = issuer_ref_defined(
1618 &config.default_certificate_specs.internal,
1619 &mz.spec.internal_certificate_spec,
1620 );
1621 let authenticator_kind = mz.spec.authenticator_kind;
1622
1623 let listeners_config = create_v0_147_0_listeners_config(config, mz);
1624
1625 let listeners_json = if mz.meets_minimum_version(&PER_ROUTE_GROUP_ROLES_VERSION) {
1629 let mut listeners_config: v26_32_0::ListenersConfig = listeners_config.into();
1634 if matches!(
1641 authenticator_kind,
1642 AuthenticatorKind::Password | AuthenticatorKind::Sasl | AuthenticatorKind::Oidc
1643 ) {
1644 listeners_config.http.get_mut("external").map(|listener| {
1645 listener.routes.internal = RouteGroup::Enabled(AllowedRoles::Internal);
1646 listener.routes.profiling = RouteGroup::Enabled(AllowedRoles::Internal);
1647 listener
1648 });
1649 }
1650
1651 serde_json::to_string(&VersionedListenersConfig::V2(listeners_config)).expect("known valid")
1652 } else {
1653 serde_json::to_string(&VersionedListenersConfig::V1(listeners_config)).expect("known valid")
1654 };
1655 let listeners_configmap = ConfigMap {
1656 binary_data: None,
1657 data: Some(btreemap! {
1658 "listeners.json".to_owned() => listeners_json,
1659 }),
1660 immutable: None,
1661 metadata: ObjectMeta {
1662 annotations: Some(btreemap! {
1663 "materialize.cloud/generation".to_owned() => generation.to_string(),
1664 }),
1665 ..mz.managed_resource_meta(mz.listeners_configmap_name(generation))
1666 },
1667 };
1668
1669 let (scheme, leader_api_port, mz_system_secret_name) = match authenticator_kind {
1670 AuthenticatorKind::Password | AuthenticatorKind::Sasl | AuthenticatorKind::Oidc => {
1671 let scheme = if external_enable_tls { "https" } else { "http" };
1672 (
1673 scheme,
1674 config.environmentd_http_port,
1675 Some(mz.spec.backend_secret_name.clone()),
1676 )
1677 }
1678 _ => ("http", config.environmentd_internal_http_port, None),
1679 };
1680 let environmentd_url = format!(
1681 "{}://{}.{}.svc.cluster.local:{}",
1682 scheme,
1683 mz.environmentd_generation_service_name(generation),
1684 mz.namespace(),
1685 leader_api_port,
1686 );
1687 ConnectionInfo {
1688 environmentd_url,
1689 listeners_configmap,
1690 mz_system_secret_name,
1691 }
1692}
1693
1694#[derive(Debug, Deserialize, PartialEq, Eq)]
1696struct BecomeLeaderResponse {
1697 result: BecomeLeaderResult,
1698}
1699
1700#[derive(Debug, Deserialize, PartialEq, Eq)]
1701enum BecomeLeaderResult {
1702 Success,
1703 Failure { message: String },
1704}
1705
1706#[derive(Debug, Deserialize, PartialEq, Eq)]
1707struct SkipCatchupError {
1708 message: String,
1709}
1710
1711#[cfg(test)]
1712mod tests {
1713 use k8s_openapi::api::core::v1::{HTTPGetAction, Probe, TCPSocketAction};
1714 use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString;
1715
1716 use super::{environmentd_probe, validate_probe_handlers};
1717
1718 fn base() -> Probe {
1719 Probe {
1720 initial_delay_seconds: Some(1),
1721 failure_threshold: Some(12),
1722 tcp_socket: Some(TCPSocketAction {
1723 host: None,
1724 port: IntOrString::Int(6877),
1725 }),
1726 ..Default::default()
1727 }
1728 }
1729
1730 #[mz_ore::test]
1731 fn environmentd_probe_without_overrides_is_base() {
1732 assert_eq!(environmentd_probe(base(), None, None), base());
1733 }
1734
1735 #[mz_ore::test]
1736 fn environmentd_probe_layers_fields() {
1737 let operator_default = Probe {
1738 failure_threshold: Some(30),
1739 period_seconds: Some(20),
1740 ..Default::default()
1741 };
1742 let environment_override = Probe {
1743 period_seconds: Some(5),
1744 ..Default::default()
1745 };
1746 assert_eq!(
1747 environmentd_probe(base(), Some(&operator_default), Some(&environment_override)),
1748 Probe {
1749 failure_threshold: Some(30),
1750 period_seconds: Some(5),
1751 ..base()
1752 }
1753 );
1754 }
1755
1756 #[mz_ore::test]
1757 fn environmentd_probe_replaces_handler_as_unit() {
1758 let http_get = HTTPGetAction {
1759 path: Some("/api/readyz".to_owned()),
1760 port: IntOrString::Int(6878),
1761 ..Default::default()
1762 };
1763 let environment_override = Probe {
1764 http_get: Some(http_get.clone()),
1765 ..Default::default()
1766 };
1767 assert_eq!(
1768 environmentd_probe(base(), None, Some(&environment_override)),
1769 Probe {
1770 tcp_socket: None,
1771 http_get: Some(http_get),
1772 ..base()
1773 }
1774 );
1775 }
1776
1777 #[mz_ore::test]
1778 fn validate_probe_handlers_rejects_multiple_handlers() {
1779 assert!(validate_probe_handlers(&Probe::default()).is_ok());
1780 assert!(validate_probe_handlers(&base()).is_ok());
1781 let both = Probe {
1782 http_get: Some(HTTPGetAction {
1783 port: IntOrString::Int(6878),
1784 ..Default::default()
1785 }),
1786 ..base()
1787 };
1788 let err = validate_probe_handlers(&both).unwrap_err();
1789 assert!(
1790 err.to_string().contains("httpGet, tcpSocket"),
1791 "unexpected error: {err}"
1792 );
1793 }
1794}