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