1#![allow(rustdoc::invalid_html_tags)]
16
17use std::collections::BTreeMap;
18use std::time::Duration;
19
20use k8s_openapi::{
21 api::core::v1::{EnvVar, Probe, ResourceRequirements},
22 apimachinery::pkg::{
23 api::resource::Quantity,
24 apis::meta::v1::{Condition, Time},
25 },
26 jiff::Timestamp,
27};
28use kube::{CustomResource, Resource, ResourceExt};
29use schemars::JsonSchema;
30use semver::Version;
31use serde::{Deserialize, Serialize};
32use sha2::{Digest, Sha256};
33use uuid::Uuid;
34
35use crate::crd::{ManagedResource, MaterializeCertSpec, new_resource_id};
36use mz_server_core::listeners::AuthenticatorKind;
37
38pub const LAST_KNOWN_ACTIVE_GENERATION_ANNOTATION: &str =
39 "materialize.cloud/last-known-active-generation";
40pub const FORCE_ROLLOUT_ANNOTATION: &str = "materialize.cloud/force-rollout";
41
42#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize, JsonSchema)]
43pub enum MaterializeRolloutStrategy {
44 #[default]
48 WaitUntilReady,
49
50 ManuallyPromote,
78
79 ImmediatelyPromoteCausingDowntime,
90}
91
92pub const DEFAULT_ROLLOUT_REQUEST_TIMEOUT: &str = "24h";
97
98#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)]
109#[serde(transparent)]
110pub struct RolloutRequestTimeout(pub String);
111
112impl Default for RolloutRequestTimeout {
113 fn default() -> Self {
114 RolloutRequestTimeout(DEFAULT_ROLLOUT_REQUEST_TIMEOUT.to_owned())
115 }
116}
117
118pub mod v1alpha1 {
119 use super::*;
120
121 #[derive(
122 CustomResource,
123 Clone,
124 Debug,
125 Default,
126 PartialEq,
127 Deserialize,
128 Serialize,
129 JsonSchema
130 )]
131 #[serde(rename_all = "camelCase")]
132 #[kube(
133 namespaced,
134 group = "materialize.cloud",
135 version = "v1alpha1",
136 kind = "Materialize",
137 singular = "materialize",
138 plural = "materializes",
139 shortname = "mzs",
140 status = "MaterializeStatus",
141 printcolumn = r#"{"name": "ImageRefRunning", "type": "string", "description": "Reference to the Docker image that is currently in use.", "jsonPath": ".status.lastCompletedRolloutEnvironmentdImageRef", "priority": 1}"#,
142 printcolumn = r#"{"name": "ImageRefToDeploy", "type": "string", "description": "Reference to the Docker image which will be deployed on the next rollout.", "jsonPath": ".spec.environmentdImageRef", "priority": 1}"#,
143 printcolumn = r#"{"name": "UpToDate", "type": "string", "description": "Whether the spec has been applied", "jsonPath": ".status.conditions[?(@.type==\"UpToDate\")].status", "priority": 1}"#
144 )]
145 pub struct MaterializeSpec {
146 pub environmentd_image_ref: String,
148 pub environmentd_extra_args: Option<Vec<String>>,
150 pub environmentd_extra_env: Option<Vec<EnvVar>>,
152 #[kube(deprecated)]
161 pub environmentd_iam_role_arn: Option<String>,
162 pub environmentd_connection_role_arn: Option<String>,
165 pub environmentd_resource_requirements: Option<ResourceRequirements>,
167 pub environmentd_scratch_volume_storage_requirement: Option<Quantity>,
169 #[serde(skip_serializing_if = "Option::is_none")]
175 pub environmentd_readiness_probe: Option<Probe>,
176 #[serde(skip_serializing_if = "Option::is_none")]
182 pub environmentd_liveness_probe: Option<Probe>,
183 #[serde(skip_serializing_if = "Option::is_none")]
192 pub environmentd_startup_probe: Option<Probe>,
193 pub balancerd_resource_requirements: Option<ResourceRequirements>,
195 #[serde(skip_serializing_if = "Option::is_none")]
200 pub balancerd_configmap_name: Option<String>,
201 pub console_resource_requirements: Option<ResourceRequirements>,
203 pub balancerd_replicas: Option<i32>,
205 #[serde(skip_serializing_if = "Option::is_none")]
209 pub balancerd_externally_scaled: Option<bool>,
210 pub console_replicas: Option<i32>,
212
213 pub service_account_name: Option<String>,
216 pub service_account_annotations: Option<BTreeMap<String, String>>,
223 pub service_account_labels: Option<BTreeMap<String, String>>,
225 pub pod_annotations: Option<BTreeMap<String, String>>,
227 pub pod_labels: Option<BTreeMap<String, String>>,
229
230 #[serde(default)]
242 pub request_rollout: Uuid,
243 #[serde(default)]
248 pub force_promote: String,
249 #[serde(default)]
256 pub force_rollout: Uuid,
257 #[kube(deprecated)]
261 #[serde(default)]
262 pub in_place_rollout: bool,
263 #[serde(default)]
265 pub rollout_strategy: MaterializeRolloutStrategy,
266 #[serde(default)]
287 pub rollout_request_timeout: RolloutRequestTimeout,
288 pub backend_secret_name: String,
293 #[serde(default)]
295 pub authenticator_kind: AuthenticatorKind,
296 #[serde(default)]
298 pub enable_rbac: bool,
299
300 #[serde(default)]
307 pub environment_id: Uuid,
308
309 pub system_parameter_configmap_name: Option<String>,
324
325 pub balancerd_external_certificate_spec: Option<MaterializeCertSpec>,
329 pub console_external_certificate_spec: Option<MaterializeCertSpec>,
334 pub internal_certificate_spec: Option<MaterializeCertSpec>,
339 }
340
341 impl Materialize {
342 pub fn backend_secret_name(&self) -> String {
343 self.spec.backend_secret_name.clone()
344 }
345
346 pub fn namespace(&self) -> String {
347 self.meta().namespace.clone().unwrap()
348 }
349
350 pub fn create_service_account(&self) -> bool {
351 self.spec.service_account_name.is_none()
352 }
353
354 pub fn service_account_name(&self) -> String {
355 self.spec
356 .service_account_name
357 .clone()
358 .unwrap_or_else(|| self.name_unchecked())
359 }
360
361 pub fn role_name(&self) -> String {
362 self.name_unchecked()
363 }
364
365 pub fn role_binding_name(&self) -> String {
366 self.name_unchecked()
367 }
368
369 pub fn environmentd_statefulset_name(&self, generation: u64) -> String {
370 self.name_prefixed(&format!("environmentd-{generation}"))
371 }
372
373 pub fn environmentd_app_name(&self) -> String {
374 "environmentd".to_owned()
375 }
376
377 pub fn environmentd_service_name(&self) -> String {
378 self.name_prefixed("environmentd")
379 }
380
381 pub fn environmentd_service_internal_fqdn(&self) -> String {
382 format!(
383 "{}.{}.svc.cluster.local",
384 self.environmentd_service_name(),
385 self.meta().namespace.as_ref().unwrap()
386 )
387 }
388
389 pub fn environmentd_generation_service_name(&self, generation: u64) -> String {
390 self.name_prefixed(&format!("environmentd-{generation}"))
391 }
392
393 pub fn balancerd_app_name(&self) -> String {
394 "balancerd".to_owned()
395 }
396
397 pub fn environmentd_certificate_name(&self) -> String {
398 self.name_prefixed("environmentd-external")
399 }
400
401 pub fn environmentd_certificate_secret_name(&self) -> String {
402 self.name_prefixed("environmentd-tls")
403 }
404
405 pub fn balancerd_deployment_name(&self) -> String {
406 self.name_prefixed("balancerd")
407 }
408
409 pub fn balancerd_service_name(&self) -> String {
410 self.name_prefixed("balancerd")
411 }
412
413 pub fn console_app_name(&self) -> String {
414 "console".to_owned()
415 }
416
417 pub fn balancerd_external_certificate_name(&self) -> String {
418 self.name_prefixed("balancerd-external")
419 }
420
421 pub fn balancerd_external_certificate_secret_name(&self) -> String {
422 self.name_prefixed("balancerd-external-tls")
423 }
424
425 pub fn balancerd_replicas(&self) -> i32 {
426 self.spec.balancerd_replicas.unwrap_or(2)
427 }
428
429 pub fn console_replicas(&self) -> i32 {
430 self.spec.console_replicas.unwrap_or(2)
431 }
432
433 pub fn console_configmap_name(&self) -> String {
434 self.name_prefixed("console")
435 }
436
437 pub fn console_deployment_name(&self) -> String {
438 self.name_prefixed("console")
439 }
440
441 pub fn console_service_name(&self) -> String {
442 self.name_prefixed("console")
443 }
444
445 pub fn console_external_certificate_name(&self) -> String {
446 self.name_prefixed("console-external")
447 }
448
449 pub fn console_external_certificate_secret_name(&self) -> String {
450 self.name_prefixed("console-external-tls")
451 }
452
453 pub fn persist_pubsub_service_name(&self, generation: u64) -> String {
454 self.name_prefixed(&format!("persist-pubsub-{generation}"))
455 }
456
457 pub fn listeners_configmap_name(&self, generation: u64) -> String {
458 self.name_prefixed(&format!("listeners-{generation}"))
459 }
460
461 pub fn name_prefixed(&self, suffix: &str) -> String {
462 format!("mz{}-{}", self.resource_id(), suffix)
463 }
464
465 pub fn resource_id(&self) -> &str {
466 &self.status.as_ref().unwrap().resource_id
467 }
468
469 pub fn system_parameter_configmap_name(&self) -> Option<String> {
470 self.spec.system_parameter_configmap_name.clone()
471 }
472
473 pub fn environmentd_scratch_volume_storage_requirement(&self) -> Quantity {
474 self.spec
475 .environmentd_scratch_volume_storage_requirement
476 .clone()
477 .unwrap_or_else(|| {
478 self.spec
479 .environmentd_resource_requirements
480 .as_ref()
481 .and_then(|requirements| {
482 requirements
483 .requests
484 .as_ref()
485 .or(requirements.limits.as_ref())
486 })
487 .and_then(|requirements| requirements.get("memory").cloned())
492 .unwrap_or_else(|| Quantity("4096Mi".to_string()))
494 })
495 }
496
497 pub fn environment_id(&self, cloud_provider: &str, region: &str) -> String {
498 format!(
499 "{}-{}-{}-0",
500 cloud_provider, region, self.spec.environment_id,
501 )
502 }
503
504 pub fn requested_reconciliation_id(&self) -> Uuid {
505 self.spec.request_rollout
506 }
507
508 pub fn force_rollout_value(&self) -> String {
520 match self
521 .meta()
522 .annotations
523 .as_ref()
524 .and_then(|annotations| annotations.get(FORCE_ROLLOUT_ANNOTATION))
525 {
526 Some(annotation) => format!("{}/{}", self.spec.force_rollout, annotation),
527 None => self.spec.force_rollout.to_string(),
528 }
529 }
530
531 pub fn rollout_requested(&self) -> bool {
532 self.requested_reconciliation_id()
533 != self
534 .status
535 .as_ref()
536 .map_or_else(Uuid::nil, |status| status.last_completed_rollout_request)
537 }
538
539 pub fn rollout_request_timeout(&self) -> Duration {
544 let timeout = &self.spec.rollout_request_timeout.0;
545 humantime::parse_duration(timeout)
546 .or_else(|e| {
547 tracing::warn!(
548 rollout_request_timeout = %timeout,
549 "failed to parse rolloutRequestTimeout, using default: {e}",
550 );
551 humantime::parse_duration(DEFAULT_ROLLOUT_REQUEST_TIMEOUT)
552 })
553 .expect("DEFAULT_ROLLOUT_REQUEST_TIMEOUT must be a valid duration")
554 }
555
556 pub fn rollout_in_progress_since(&self) -> Option<Timestamp> {
573 self.status
574 .as_ref()?
575 .conditions
576 .iter()
577 .find_map(|condition| {
578 if condition.type_ == "UpToDate"
579 && condition.status == "Unknown"
580 && condition.reason != "Promoting"
581 {
582 Some(condition.last_transition_time.0)
583 } else {
584 None
585 }
586 })
587 }
588
589 pub fn up_to_date_transition_time(&self, new_status: &str, now: Timestamp) -> Timestamp {
601 self.status
602 .as_ref()
603 .and_then(|status| {
604 status
605 .conditions
606 .iter()
607 .find(|condition| condition.type_ == "UpToDate")
608 })
609 .filter(|condition| condition.status == new_status)
610 .map_or(now, |condition| condition.last_transition_time.0)
611 }
612
613 pub fn active_environmentd_image_ref(&self) -> &str {
621 self.status
622 .as_ref()
623 .and_then(|s| s.last_completed_rollout_environmentd_image_ref.as_deref())
624 .unwrap_or(&self.spec.environmentd_image_ref)
625 }
626
627 pub fn set_force_promote(&mut self) {
628 self.spec.force_promote = self.spec.request_rollout.hyphenated().to_string();
629 }
630
631 pub fn should_force_promote(&self) -> bool {
632 self.spec.force_promote == self.spec.request_rollout.hyphenated().to_string()
633 || self.spec.force_promote
634 == super::v1::Materialize::from(self.clone()).generate_rollout_hash()
635 || self.spec.rollout_strategy
636 == MaterializeRolloutStrategy::ImmediatelyPromoteCausingDowntime
637 }
638
639 pub fn conditions_need_update(&self) -> bool {
640 let Some(status) = self.status.as_ref() else {
641 return true;
642 };
643 if status.conditions.is_empty() {
644 return true;
645 }
646 for condition in &status.conditions {
647 if condition.observed_generation != self.meta().generation {
648 return true;
649 }
650 }
651 false
652 }
653
654 pub fn is_ready_to_promote(&self, resources_hash: &str) -> bool {
655 let Some(status) = self.status.as_ref() else {
656 return false;
657 };
658 if status.conditions.is_empty() {
659 return false;
660 }
661 status
662 .conditions
663 .iter()
664 .any(|condition| condition.reason == "ReadyToPromote")
665 && &status.resources_hash == resources_hash
666 }
667
668 pub fn is_promoting(&self) -> bool {
669 let Some(status) = self.status.as_ref() else {
670 return false;
671 };
672 if status.conditions.is_empty() {
673 return false;
674 }
675 status
676 .conditions
677 .iter()
678 .any(|condition| condition.reason == "Promoting")
679 }
680
681 pub fn update_in_progress(&self) -> bool {
682 let Some(status) = self.status.as_ref() else {
683 return false;
684 };
685 if status.conditions.is_empty() {
686 return false;
687 }
688 for condition in &status.conditions {
689 if condition.type_ == "UpToDate" && condition.status == "Unknown" {
690 return true;
691 }
692 }
693 false
694 }
695
696 pub fn meets_minimum_version(&self, minimum: &Version) -> bool {
700 let version = parse_image_ref(&self.spec.environmentd_image_ref);
701 match version {
702 Some(version) => version.cmp_precedence(minimum).is_ge(),
704 None => {
710 tracing::warn!(
711 image_ref = %self.spec.environmentd_image_ref,
712 "failed to parse image ref",
713 );
714 true
715 }
716 }
717 }
718
719 pub fn is_valid_upgrade_version(active_version: &Version, next_version: &Version) -> bool {
723 if next_version.cmp_precedence(active_version) == std::cmp::Ordering::Less {
728 return false;
729 }
730
731 if active_version.major == 0 {
732 if next_version.major != active_version.major {
733 if next_version.major == 26 {
734 return (active_version.minor == 147 && active_version.patch >= 20)
737 || active_version.minor >= 164;
738 } else {
739 return false;
740 }
741 }
742 if next_version.minor == 147 && active_version.minor == 130 {
744 return true;
745 }
746 return next_version.minor <= active_version.minor + 1;
748 } else if active_version.major >= 26 {
749 return next_version.major <= active_version.major + 1;
751 }
752
753 true
754 }
755
756 pub fn within_upgrade_window(&self) -> bool {
759 let active_environmentd_version = self
760 .status
761 .as_ref()
762 .and_then(|status| {
763 status
764 .last_completed_rollout_environmentd_image_ref
765 .as_ref()
766 })
767 .and_then(|image_ref| parse_image_ref(image_ref));
768
769 if let (Some(next_environmentd_version), Some(active_environmentd_version)) = (
770 parse_image_ref(&self.spec.environmentd_image_ref),
771 active_environmentd_version,
772 ) {
773 Self::is_valid_upgrade_version(
774 &active_environmentd_version,
775 &next_environmentd_version,
776 )
777 } else {
778 true
781 }
782 }
783
784 pub fn status(&self) -> MaterializeStatus {
785 self.status.clone().unwrap_or_else(|| {
786 let mut status = MaterializeStatus::default();
787
788 status.resource_id = new_resource_id();
789
790 if let Some(last_active_generation) = self
795 .annotations()
796 .get(LAST_KNOWN_ACTIVE_GENERATION_ANNOTATION)
797 {
798 status.active_generation = last_active_generation
799 .parse()
800 .expect("valid int generation");
801 }
802
803 status.last_completed_rollout_environmentd_image_ref =
806 Some(self.spec.environmentd_image_ref.clone());
807
808 status
809 })
810 }
811 }
812
813 #[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq)]
814 #[serde(rename_all = "camelCase")]
815 pub struct MaterializeStatus {
816 pub resource_id: String,
818 pub active_generation: u64,
820 pub last_completed_rollout_request: Uuid,
822 pub last_completed_rollout_environmentd_image_ref: Option<String>,
826 pub resources_hash: String,
831 pub last_completed_rollout_hash: Option<String>,
834 pub conditions: Vec<Condition>,
835 }
836
837 impl MaterializeStatus {
838 pub fn needs_update(&self, other: &Self) -> bool {
839 let now = Timestamp::now();
840 let mut a = self.clone();
841 for condition in &mut a.conditions {
842 condition.last_transition_time = Time(now);
843 }
844 let mut b = other.clone();
845 for condition in &mut b.conditions {
846 condition.last_transition_time = Time(now);
847 }
848 a != b
849 }
850 }
851
852 impl ManagedResource for Materialize {
853 fn default_labels(&self) -> BTreeMap<String, String> {
854 BTreeMap::from_iter([
855 (
856 "materialize.cloud/organization-name".to_owned(),
857 self.name_unchecked(),
858 ),
859 (
860 "materialize.cloud/organization-namespace".to_owned(),
861 self.namespace(),
862 ),
863 (
864 "materialize.cloud/mz-resource-id".to_owned(),
865 self.resource_id().to_owned(),
866 ),
867 ])
868 }
869
870 fn app_name(&self) -> Option<&str> {
871 Some("environmentd")
872 }
873 }
874
875 impl From<v1::Materialize> for Materialize {
876 fn from(value: v1::Materialize) -> Self {
877 let rollout_hash = value.generate_rollout_hash();
878 let request_rollout = Uuid::new_v5(&Uuid::NAMESPACE_OID, rollout_hash.as_bytes());
882 Materialize {
883 metadata: value.metadata,
884 spec: MaterializeSpec {
885 environmentd_image_ref: value.spec.environmentd_image_ref,
886 environmentd_extra_args: value.spec.environmentd_extra_args,
887 environmentd_extra_env: value.spec.environmentd_extra_env,
888 environmentd_iam_role_arn: None,
889 environmentd_connection_role_arn: value.spec.environmentd_connection_role_arn,
890 environmentd_resource_requirements: value
891 .spec
892 .environmentd_resource_requirements,
893 environmentd_scratch_volume_storage_requirement: value
894 .spec
895 .environmentd_scratch_volume_storage_requirement,
896 environmentd_readiness_probe: value.spec.environmentd_readiness_probe,
897 environmentd_liveness_probe: value.spec.environmentd_liveness_probe,
898 environmentd_startup_probe: value.spec.environmentd_startup_probe,
899 balancerd_resource_requirements: value.spec.balancerd_resource_requirements,
900 balancerd_configmap_name: value.spec.balancerd_configmap_name,
901 console_resource_requirements: value.spec.console_resource_requirements,
902 balancerd_replicas: value.spec.balancerd_replicas,
903 balancerd_externally_scaled: value.spec.balancerd_externally_scaled,
904 console_replicas: value.spec.console_replicas,
905 service_account_name: value.spec.service_account_name,
906 service_account_annotations: value.spec.service_account_annotations,
907 service_account_labels: value.spec.service_account_labels,
908 pod_annotations: value.spec.pod_annotations,
909 pod_labels: value.spec.pod_labels,
910 force_promote: value.spec.force_promote.unwrap_or_default(),
911 force_rollout: value.spec.force_rollout,
912 rollout_strategy: value.spec.rollout_strategy,
913 rollout_request_timeout: value.spec.rollout_request_timeout,
914 backend_secret_name: value.spec.backend_secret_name,
915 authenticator_kind: value.spec.authenticator_kind,
916 enable_rbac: value.spec.enable_rbac,
917 environment_id: value.spec.environment_id,
918 system_parameter_configmap_name: value.spec.system_parameter_configmap_name,
919 balancerd_external_certificate_spec: value
920 .spec
921 .balancerd_external_certificate_spec,
922 console_external_certificate_spec: value.spec.console_external_certificate_spec,
923 internal_certificate_spec: value.spec.internal_certificate_spec,
924 request_rollout,
925 in_place_rollout: false,
926 },
927 status: value.status.map(|status| MaterializeStatus {
928 resource_id: status.resource_id,
929 active_generation: status.active_generation,
930 last_completed_rollout_environmentd_image_ref: status
931 .last_completed_rollout_environmentd_image_ref,
932 conditions: status.conditions,
933 last_completed_rollout_request: status
937 .last_completed_rollout_hash
938 .as_ref()
939 .map(|hash| Uuid::new_v5(&Uuid::NAMESPACE_OID, hash.as_bytes()))
940 .unwrap_or(Uuid::nil()),
941 last_completed_rollout_hash: status.last_completed_rollout_hash,
942 resources_hash: "".to_owned(),
943 }),
944 }
945 }
946 }
947
948 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
957 #[serde(rename_all = "camelCase")]
958 pub struct PartialMaterializeSpec {
959 #[serde(
960 default,
961 with = "double_option",
962 skip_serializing_if = "Option::is_none"
963 )]
964 pub environmentd_image_ref: PartialField,
965 #[serde(
966 default,
967 with = "double_option",
968 skip_serializing_if = "Option::is_none"
969 )]
970 pub environmentd_extra_args: PartialField,
971 #[serde(
972 default,
973 with = "double_option",
974 skip_serializing_if = "Option::is_none"
975 )]
976 pub environmentd_extra_env: PartialField,
977 #[serde(
978 default,
979 with = "double_option",
980 skip_serializing_if = "Option::is_none"
981 )]
982 pub environmentd_iam_role_arn: PartialField,
983 #[serde(
984 default,
985 with = "double_option",
986 skip_serializing_if = "Option::is_none"
987 )]
988 pub environmentd_connection_role_arn: PartialField,
989 #[serde(
990 default,
991 with = "double_option",
992 skip_serializing_if = "Option::is_none"
993 )]
994 pub environmentd_resource_requirements: PartialField,
995 #[serde(
996 default,
997 with = "double_option",
998 skip_serializing_if = "Option::is_none"
999 )]
1000 pub environmentd_scratch_volume_storage_requirement: PartialField,
1001 #[serde(
1002 default,
1003 with = "double_option",
1004 skip_serializing_if = "Option::is_none"
1005 )]
1006 pub environmentd_readiness_probe: PartialField,
1007 #[serde(
1008 default,
1009 with = "double_option",
1010 skip_serializing_if = "Option::is_none"
1011 )]
1012 pub environmentd_liveness_probe: PartialField,
1013 #[serde(
1014 default,
1015 with = "double_option",
1016 skip_serializing_if = "Option::is_none"
1017 )]
1018 pub environmentd_startup_probe: PartialField,
1019 #[serde(
1020 default,
1021 with = "double_option",
1022 skip_serializing_if = "Option::is_none"
1023 )]
1024 pub balancerd_resource_requirements: PartialField,
1025 #[serde(
1026 default,
1027 with = "double_option",
1028 skip_serializing_if = "Option::is_none"
1029 )]
1030 pub balancerd_configmap_name: PartialField,
1031 #[serde(
1032 default,
1033 with = "double_option",
1034 skip_serializing_if = "Option::is_none"
1035 )]
1036 pub console_resource_requirements: PartialField,
1037 #[serde(
1038 default,
1039 with = "double_option",
1040 skip_serializing_if = "Option::is_none"
1041 )]
1042 pub balancerd_replicas: PartialField,
1043 #[serde(
1044 default,
1045 with = "double_option",
1046 skip_serializing_if = "Option::is_none"
1047 )]
1048 pub balancerd_externally_scaled: PartialField,
1049 #[serde(
1050 default,
1051 with = "double_option",
1052 skip_serializing_if = "Option::is_none"
1053 )]
1054 pub console_replicas: PartialField,
1055 #[serde(
1056 default,
1057 with = "double_option",
1058 skip_serializing_if = "Option::is_none"
1059 )]
1060 pub service_account_name: PartialField,
1061 #[serde(
1062 default,
1063 with = "double_option",
1064 skip_serializing_if = "Option::is_none"
1065 )]
1066 pub service_account_annotations: PartialField,
1067 #[serde(
1068 default,
1069 with = "double_option",
1070 skip_serializing_if = "Option::is_none"
1071 )]
1072 pub service_account_labels: PartialField,
1073 #[serde(
1074 default,
1075 with = "double_option",
1076 skip_serializing_if = "Option::is_none"
1077 )]
1078 pub pod_annotations: PartialField,
1079 #[serde(
1080 default,
1081 with = "double_option",
1082 skip_serializing_if = "Option::is_none"
1083 )]
1084 pub pod_labels: PartialField,
1085 #[serde(
1086 default,
1087 with = "double_option",
1088 skip_serializing_if = "Option::is_none"
1089 )]
1090 pub request_rollout: PartialField,
1091 #[serde(
1092 default,
1093 with = "double_option",
1094 skip_serializing_if = "Option::is_none"
1095 )]
1096 pub force_promote: PartialField,
1097 #[serde(
1098 default,
1099 with = "double_option",
1100 skip_serializing_if = "Option::is_none"
1101 )]
1102 pub force_rollout: PartialField,
1103 #[serde(
1104 default,
1105 with = "double_option",
1106 skip_serializing_if = "Option::is_none"
1107 )]
1108 pub in_place_rollout: PartialField,
1109 #[serde(
1110 default,
1111 with = "double_option",
1112 skip_serializing_if = "Option::is_none"
1113 )]
1114 pub rollout_strategy: PartialField,
1115 #[serde(
1116 default,
1117 with = "double_option",
1118 skip_serializing_if = "Option::is_none"
1119 )]
1120 pub rollout_request_timeout: PartialField,
1121 #[serde(
1122 default,
1123 with = "double_option",
1124 skip_serializing_if = "Option::is_none"
1125 )]
1126 pub backend_secret_name: PartialField,
1127 #[serde(
1128 default,
1129 with = "double_option",
1130 skip_serializing_if = "Option::is_none"
1131 )]
1132 pub authenticator_kind: PartialField,
1133 #[serde(
1134 default,
1135 with = "double_option",
1136 skip_serializing_if = "Option::is_none"
1137 )]
1138 pub enable_rbac: PartialField,
1139 #[serde(
1140 default,
1141 with = "double_option",
1142 skip_serializing_if = "Option::is_none"
1143 )]
1144 pub environment_id: PartialField,
1145 #[serde(
1146 default,
1147 with = "double_option",
1148 skip_serializing_if = "Option::is_none"
1149 )]
1150 pub system_parameter_configmap_name: PartialField,
1151 #[serde(
1152 default,
1153 with = "double_option",
1154 skip_serializing_if = "Option::is_none"
1155 )]
1156 pub balancerd_external_certificate_spec: PartialField,
1157 #[serde(
1158 default,
1159 with = "double_option",
1160 skip_serializing_if = "Option::is_none"
1161 )]
1162 pub console_external_certificate_spec: PartialField,
1163 #[serde(
1164 default,
1165 with = "double_option",
1166 skip_serializing_if = "Option::is_none"
1167 )]
1168 pub internal_certificate_spec: PartialField,
1169 #[serde(flatten)]
1170 pub extra: serde_json::Map<String, serde_json::Value>,
1171 }
1172
1173 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
1175 #[serde(rename_all = "camelCase")]
1176 pub struct PartialMaterializeStatus {
1177 #[serde(
1178 default,
1179 with = "double_option",
1180 skip_serializing_if = "Option::is_none"
1181 )]
1182 pub resource_id: PartialField,
1183 #[serde(
1184 default,
1185 with = "double_option",
1186 skip_serializing_if = "Option::is_none"
1187 )]
1188 pub active_generation: PartialField,
1189 #[serde(
1190 default,
1191 with = "double_option",
1192 skip_serializing_if = "Option::is_none"
1193 )]
1194 pub last_completed_rollout_request: PartialField,
1195 #[serde(
1196 default,
1197 with = "double_option",
1198 skip_serializing_if = "Option::is_none"
1199 )]
1200 pub last_completed_rollout_environmentd_image_ref: PartialField,
1201 #[serde(
1202 default,
1203 with = "double_option",
1204 skip_serializing_if = "Option::is_none"
1205 )]
1206 pub resources_hash: PartialField,
1207 #[serde(
1208 default,
1209 with = "double_option",
1210 skip_serializing_if = "Option::is_none"
1211 )]
1212 pub last_completed_rollout_hash: PartialField,
1213 #[serde(
1214 default,
1215 with = "double_option",
1216 skip_serializing_if = "Option::is_none"
1217 )]
1218 pub conditions: PartialField,
1219 #[serde(flatten)]
1220 pub extra: serde_json::Map<String, serde_json::Value>,
1221 }
1222
1223 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
1225 #[serde(rename_all = "camelCase")]
1226 pub struct PartialMaterialize {
1227 #[serde(skip_serializing_if = "Option::is_none")]
1228 pub api_version: Option<String>,
1229 #[serde(skip_serializing_if = "Option::is_none")]
1230 pub kind: Option<serde_json::Value>,
1231 #[serde(skip_serializing_if = "Option::is_none")]
1232 pub metadata: Option<serde_json::Value>,
1233 #[serde(skip_serializing_if = "Option::is_none")]
1234 pub spec: Option<PartialMaterializeSpec>,
1235 #[serde(skip_serializing_if = "Option::is_none")]
1236 pub status: Option<PartialMaterializeStatus>,
1237 #[serde(flatten)]
1238 pub extra: serde_json::Map<String, serde_json::Value>,
1239 }
1240
1241 impl From<MaterializeSpec> for PartialMaterializeSpec {
1242 fn from(spec: MaterializeSpec) -> Self {
1243 let MaterializeSpec {
1244 environmentd_image_ref,
1245 environmentd_extra_args,
1246 environmentd_extra_env,
1247 environmentd_iam_role_arn,
1248 environmentd_connection_role_arn,
1249 environmentd_resource_requirements,
1250 environmentd_scratch_volume_storage_requirement,
1251 environmentd_readiness_probe,
1252 environmentd_liveness_probe,
1253 environmentd_startup_probe,
1254 balancerd_resource_requirements,
1255 balancerd_configmap_name,
1256 console_resource_requirements,
1257 balancerd_replicas,
1258 balancerd_externally_scaled,
1259 console_replicas,
1260 service_account_name,
1261 service_account_annotations,
1262 service_account_labels,
1263 pod_annotations,
1264 pod_labels,
1265 request_rollout,
1266 force_promote,
1267 force_rollout,
1268 in_place_rollout,
1269 rollout_strategy,
1270 rollout_request_timeout,
1271 backend_secret_name,
1272 authenticator_kind,
1273 enable_rbac,
1274 environment_id,
1275 system_parameter_configmap_name,
1276 balancerd_external_certificate_spec,
1277 console_external_certificate_spec,
1278 internal_certificate_spec,
1279 } = spec;
1280 Self {
1281 environmentd_image_ref: present(environmentd_image_ref),
1282 environmentd_extra_args: present_opt(environmentd_extra_args),
1283 environmentd_extra_env: present_opt(environmentd_extra_env),
1284 environmentd_iam_role_arn: present_opt(environmentd_iam_role_arn),
1285 environmentd_connection_role_arn: present_opt(environmentd_connection_role_arn),
1286 environmentd_resource_requirements: present_opt(environmentd_resource_requirements),
1287 environmentd_scratch_volume_storage_requirement: present_opt(
1288 environmentd_scratch_volume_storage_requirement,
1289 ),
1290 environmentd_readiness_probe: present_opt(environmentd_readiness_probe),
1291 environmentd_liveness_probe: present_opt(environmentd_liveness_probe),
1292 environmentd_startup_probe: present_opt(environmentd_startup_probe),
1293 balancerd_resource_requirements: present_opt(balancerd_resource_requirements),
1294 balancerd_configmap_name: present_opt(balancerd_configmap_name),
1295 console_resource_requirements: present_opt(console_resource_requirements),
1296 balancerd_replicas: present_opt(balancerd_replicas),
1297 balancerd_externally_scaled: present_opt(balancerd_externally_scaled),
1298 console_replicas: present_opt(console_replicas),
1299 service_account_name: present_opt(service_account_name),
1300 service_account_annotations: present_opt(service_account_annotations),
1301 service_account_labels: present_opt(service_account_labels),
1302 pod_annotations: present_opt(pod_annotations),
1303 pod_labels: present_opt(pod_labels),
1304 request_rollout: present(request_rollout),
1305 force_promote: present(force_promote),
1306 force_rollout: present(force_rollout),
1307 in_place_rollout: present(in_place_rollout),
1308 rollout_strategy: present(rollout_strategy),
1309 rollout_request_timeout: present(rollout_request_timeout),
1310 backend_secret_name: present(backend_secret_name),
1311 authenticator_kind: present(authenticator_kind),
1312 enable_rbac: present(enable_rbac),
1313 environment_id: present(environment_id),
1314 system_parameter_configmap_name: present_opt(system_parameter_configmap_name),
1315 balancerd_external_certificate_spec: present_opt(
1316 balancerd_external_certificate_spec,
1317 ),
1318 console_external_certificate_spec: present_opt(console_external_certificate_spec),
1319 internal_certificate_spec: present_opt(internal_certificate_spec),
1320 extra: serde_json::Map::new(),
1321 }
1322 }
1323 }
1324
1325 impl From<MaterializeStatus> for PartialMaterializeStatus {
1326 fn from(status: MaterializeStatus) -> Self {
1327 let MaterializeStatus {
1328 resource_id,
1329 active_generation,
1330 last_completed_rollout_request,
1331 last_completed_rollout_environmentd_image_ref,
1332 resources_hash,
1333 last_completed_rollout_hash,
1334 conditions,
1335 } = status;
1336 Self {
1337 resource_id: present(resource_id),
1338 active_generation: present(active_generation),
1339 last_completed_rollout_request: present(last_completed_rollout_request),
1340 last_completed_rollout_environmentd_image_ref: present_opt(
1341 last_completed_rollout_environmentd_image_ref,
1342 ),
1343 resources_hash: present(resources_hash),
1344 last_completed_rollout_hash: present_opt(last_completed_rollout_hash),
1345 conditions: present(conditions),
1346 extra: serde_json::Map::new(),
1347 }
1348 }
1349 }
1350
1351 impl From<Materialize> for PartialMaterialize {
1352 fn from(mz: Materialize) -> Self {
1353 let Materialize {
1354 metadata,
1355 spec,
1356 status,
1357 } = mz;
1358 Self {
1359 api_version: Some("materialize.cloud/v1alpha1".to_owned()),
1360 kind: Some("Materialize".into()),
1361 metadata: Some(
1362 serde_json::to_value(metadata).expect("ObjectMeta serializes to JSON"),
1363 ),
1364 spec: Some(spec.into()),
1365 status: status.map(Into::into),
1366 extra: serde_json::Map::new(),
1367 }
1368 }
1369 }
1370
1371 impl From<super::v1::PartialMaterializeSpec> for PartialMaterializeSpec {
1372 fn from(spec: super::v1::PartialMaterializeSpec) -> Self {
1373 let super::v1::PartialMaterializeSpec {
1374 environmentd_image_ref,
1375 environmentd_extra_args,
1376 environmentd_extra_env,
1377 environmentd_connection_role_arn,
1378 environmentd_resource_requirements,
1379 environmentd_scratch_volume_storage_requirement,
1380 environmentd_readiness_probe,
1381 environmentd_liveness_probe,
1382 environmentd_startup_probe,
1383 balancerd_resource_requirements,
1384 balancerd_configmap_name,
1385 console_resource_requirements,
1386 balancerd_replicas,
1387 balancerd_externally_scaled,
1388 console_replicas,
1389 service_account_name,
1390 service_account_annotations,
1391 service_account_labels,
1392 pod_annotations,
1393 pod_labels,
1394 force_promote,
1395 force_rollout,
1396 rollout_strategy,
1397 rollout_request_timeout,
1398 backend_secret_name,
1399 authenticator_kind,
1400 enable_rbac,
1401 environment_id,
1402 system_parameter_configmap_name,
1403 balancerd_external_certificate_spec,
1404 console_external_certificate_spec,
1405 internal_certificate_spec,
1406 extra,
1407 } = spec;
1408 Self {
1409 environmentd_image_ref,
1410 environmentd_extra_args,
1411 environmentd_extra_env,
1412 environmentd_iam_role_arn: None,
1413 environmentd_connection_role_arn,
1414 environmentd_resource_requirements,
1415 environmentd_scratch_volume_storage_requirement,
1416 environmentd_readiness_probe,
1417 environmentd_liveness_probe,
1418 environmentd_startup_probe,
1419 balancerd_resource_requirements,
1420 balancerd_configmap_name,
1421 console_resource_requirements,
1422 balancerd_replicas,
1423 balancerd_externally_scaled,
1424 console_replicas,
1425 service_account_name,
1426 service_account_annotations,
1427 service_account_labels,
1428 pod_annotations,
1429 pod_labels,
1430 request_rollout: None,
1435 force_promote,
1436 force_rollout,
1437 in_place_rollout: None,
1438 rollout_strategy,
1439 rollout_request_timeout,
1440 backend_secret_name,
1441 authenticator_kind,
1442 enable_rbac,
1443 environment_id,
1444 system_parameter_configmap_name,
1445 balancerd_external_certificate_spec,
1446 console_external_certificate_spec,
1447 internal_certificate_spec,
1448 extra,
1449 }
1450 }
1451 }
1452
1453 impl From<super::v1::PartialMaterializeStatus> for PartialMaterializeStatus {
1454 fn from(status: super::v1::PartialMaterializeStatus) -> Self {
1455 let super::v1::PartialMaterializeStatus {
1456 resource_id,
1457 active_generation,
1458 last_completed_rollout_environmentd_image_ref,
1459 last_completed_rollout_hash,
1460 requested_rollout_hash: _,
1461 conditions,
1462 extra,
1463 } = status;
1464 Self {
1465 resource_id,
1466 active_generation,
1467 last_completed_rollout_request: None,
1472 resources_hash: None,
1473 last_completed_rollout_environmentd_image_ref,
1474 last_completed_rollout_hash,
1475 conditions,
1476 extra,
1477 }
1478 }
1479 }
1480
1481 impl From<super::v1::PartialMaterialize> for PartialMaterialize {
1482 fn from(mz: super::v1::PartialMaterialize) -> Self {
1483 let super::v1::PartialMaterialize {
1484 api_version: _,
1485 kind,
1486 metadata,
1487 spec,
1488 status,
1489 extra,
1490 } = mz;
1491 Self {
1492 api_version: Some("materialize.cloud/v1alpha1".to_owned()),
1493 kind,
1494 metadata,
1495 spec: spec.map(Into::into),
1496 status: status.map(Into::into),
1497 extra,
1498 }
1499 }
1500 }
1501}
1502
1503pub mod v1 {
1504 use super::*;
1505
1506 #[derive(
1507 CustomResource,
1508 Clone,
1509 Debug,
1510 Default,
1511 PartialEq,
1512 Deserialize,
1513 Serialize,
1514 JsonSchema
1515 )]
1516 #[serde(rename_all = "camelCase")]
1517 #[kube(
1518 namespaced,
1519 group = "materialize.cloud",
1520 version = "v1",
1521 kind = "Materialize",
1522 singular = "materialize",
1523 plural = "materializes",
1524 shortname = "mzs",
1525 status = "MaterializeStatus",
1526 printcolumn = r#"{"name": "ImageRefRunning", "type": "string", "description": "Reference to the Docker image that is currently in use.", "jsonPath": ".status.lastCompletedRolloutEnvironmentdImageRef", "priority": 1}"#,
1527 printcolumn = r#"{"name": "ImageRefToDeploy", "type": "string", "description": "Reference to the Docker image which will be deployed on the next rollout.", "jsonPath": ".spec.environmentdImageRef", "priority": 1}"#,
1528 printcolumn = r#"{"name": "UpToDate", "type": "string", "description": "Whether the spec has been applied", "jsonPath": ".status.conditions[?(@.type==\"UpToDate\")].status", "priority": 1}"#
1529 )]
1530 pub struct MaterializeSpec {
1531 pub environmentd_image_ref: String,
1533 pub environmentd_extra_args: Option<Vec<String>>,
1535 pub environmentd_extra_env: Option<Vec<EnvVar>>,
1537 pub environmentd_connection_role_arn: Option<String>,
1540 pub environmentd_resource_requirements: Option<ResourceRequirements>,
1542 pub environmentd_scratch_volume_storage_requirement: Option<Quantity>,
1544 #[serde(skip_serializing_if = "Option::is_none")]
1550 pub environmentd_readiness_probe: Option<Probe>,
1551 #[serde(skip_serializing_if = "Option::is_none")]
1557 pub environmentd_liveness_probe: Option<Probe>,
1558 #[serde(skip_serializing_if = "Option::is_none")]
1567 pub environmentd_startup_probe: Option<Probe>,
1568 pub balancerd_resource_requirements: Option<ResourceRequirements>,
1572 #[serde(skip_serializing_if = "Option::is_none")]
1577 pub balancerd_configmap_name: Option<String>,
1578 pub console_resource_requirements: Option<ResourceRequirements>,
1582 pub balancerd_replicas: Option<i32>,
1586 #[serde(skip_serializing_if = "Option::is_none")]
1592 pub balancerd_externally_scaled: Option<bool>,
1593 pub console_replicas: Option<i32>,
1597
1598 pub service_account_name: Option<String>,
1601 pub service_account_annotations: Option<BTreeMap<String, String>>,
1608 pub service_account_labels: Option<BTreeMap<String, String>>,
1610 pub pod_annotations: Option<BTreeMap<String, String>>,
1612 pub pod_labels: Option<BTreeMap<String, String>>,
1614
1615 pub force_promote: Option<String>,
1622 #[serde(default)]
1626 pub force_rollout: Uuid,
1627 #[serde(default)]
1629 pub rollout_strategy: MaterializeRolloutStrategy,
1630 #[serde(default)]
1651 pub rollout_request_timeout: RolloutRequestTimeout,
1652 pub backend_secret_name: String,
1656 #[serde(default)]
1658 pub authenticator_kind: AuthenticatorKind,
1659 #[serde(default)]
1661 pub enable_rbac: bool,
1662
1663 #[serde(default)]
1670 pub environment_id: Uuid,
1671
1672 pub system_parameter_configmap_name: Option<String>,
1687
1688 pub balancerd_external_certificate_spec: Option<MaterializeCertSpec>,
1694 pub console_external_certificate_spec: Option<MaterializeCertSpec>,
1701 pub internal_certificate_spec: Option<MaterializeCertSpec>,
1706 }
1707
1708 impl Materialize {
1709 pub fn generate_rollout_hash(&self) -> String {
1710 let mut hasher = Sha256::new();
1711 let spec = MaterializeSpec {
1714 environmentd_image_ref: self.spec.environmentd_image_ref.clone(),
1715 environmentd_extra_args: self.spec.environmentd_extra_args.clone(),
1716 environmentd_extra_env: self.spec.environmentd_extra_env.clone(),
1717 environmentd_connection_role_arn: self
1718 .spec
1719 .environmentd_connection_role_arn
1720 .clone(),
1721 environmentd_resource_requirements: self
1722 .spec
1723 .environmentd_resource_requirements
1724 .clone(),
1725 environmentd_scratch_volume_storage_requirement: self
1726 .spec
1727 .environmentd_scratch_volume_storage_requirement
1728 .clone(),
1729 environmentd_readiness_probe: self.spec.environmentd_readiness_probe.clone(),
1730 environmentd_liveness_probe: self.spec.environmentd_liveness_probe.clone(),
1731 environmentd_startup_probe: self.spec.environmentd_startup_probe.clone(),
1732 balancerd_resource_requirements: None,
1733 balancerd_configmap_name: None,
1734 console_resource_requirements: None,
1735 balancerd_replicas: None,
1736 balancerd_externally_scaled: None,
1737 console_replicas: None,
1738 service_account_name: self.spec.service_account_name.clone(),
1739 service_account_annotations: self.spec.service_account_annotations.clone(),
1740 service_account_labels: self.spec.service_account_labels.clone(),
1741 pod_annotations: self.spec.pod_annotations.clone(),
1742 pod_labels: self.spec.pod_labels.clone(),
1743 force_promote: None,
1744 force_rollout: self.spec.force_rollout,
1745 rollout_strategy: self.spec.rollout_strategy.clone(),
1746 rollout_request_timeout: self.spec.rollout_request_timeout.clone(),
1747 backend_secret_name: self.spec.backend_secret_name.clone(),
1748 authenticator_kind: self.spec.authenticator_kind,
1749 enable_rbac: self.spec.enable_rbac,
1750 environment_id: self.spec.environment_id,
1751 system_parameter_configmap_name: self.spec.system_parameter_configmap_name.clone(),
1752 balancerd_external_certificate_spec: None,
1753 console_external_certificate_spec: None,
1754 internal_certificate_spec: self.spec.internal_certificate_spec.clone(),
1755 };
1756 hasher.update(&serde_json::to_vec(&spec).unwrap());
1757 if let Some(annotation) = self
1758 .metadata
1759 .annotations
1760 .as_ref()
1761 .and_then(|annotations| annotations.get(FORCE_ROLLOUT_ANNOTATION))
1762 {
1763 hasher.update(annotation);
1764 }
1765 hex::encode(hasher.finalize())
1766 }
1767
1768 pub fn backend_secret_name(&self) -> String {
1769 self.spec.backend_secret_name.clone()
1770 }
1771
1772 pub fn namespace(&self) -> String {
1773 self.meta().namespace.clone().unwrap()
1774 }
1775
1776 pub fn create_service_account(&self) -> bool {
1777 self.spec.service_account_name.is_none()
1778 }
1779
1780 pub fn service_account_name(&self) -> String {
1781 self.spec
1782 .service_account_name
1783 .clone()
1784 .unwrap_or_else(|| self.name_unchecked())
1785 }
1786
1787 pub fn role_name(&self) -> String {
1788 self.name_unchecked()
1789 }
1790
1791 pub fn role_binding_name(&self) -> String {
1792 self.name_unchecked()
1793 }
1794
1795 pub fn environmentd_statefulset_name(&self, generation: u64) -> String {
1796 self.name_prefixed(&format!("environmentd-{generation}"))
1797 }
1798
1799 pub fn environmentd_app_name(&self) -> String {
1800 "environmentd".to_owned()
1801 }
1802
1803 pub fn environmentd_service_name(&self) -> String {
1804 self.name_prefixed("environmentd")
1805 }
1806
1807 pub fn environmentd_service_internal_fqdn(&self) -> String {
1808 format!(
1809 "{}.{}.svc.cluster.local",
1810 self.environmentd_service_name(),
1811 self.meta().namespace.as_ref().unwrap()
1812 )
1813 }
1814
1815 pub fn environmentd_generation_service_name(&self, generation: u64) -> String {
1816 self.name_prefixed(&format!("environmentd-{generation}"))
1817 }
1818
1819 pub fn balancerd_app_name(&self) -> String {
1820 "balancerd".to_owned()
1821 }
1822
1823 pub fn environmentd_certificate_name(&self) -> String {
1824 self.name_prefixed("environmentd-external")
1825 }
1826
1827 pub fn environmentd_certificate_secret_name(&self) -> String {
1828 self.name_prefixed("environmentd-tls")
1829 }
1830
1831 pub fn balancerd_deployment_name(&self) -> String {
1832 self.name_prefixed("balancerd")
1833 }
1834
1835 pub fn balancerd_service_name(&self) -> String {
1836 self.name_prefixed("balancerd")
1837 }
1838
1839 pub fn console_app_name(&self) -> String {
1840 "console".to_owned()
1841 }
1842
1843 pub fn balancerd_external_certificate_name(&self) -> String {
1844 self.name_prefixed("balancerd-external")
1845 }
1846
1847 pub fn balancerd_external_certificate_secret_name(&self) -> String {
1848 self.name_prefixed("balancerd-external-tls")
1849 }
1850
1851 pub fn balancerd_replicas(&self) -> i32 {
1852 self.spec.balancerd_replicas.unwrap_or(2)
1853 }
1854
1855 pub fn console_replicas(&self) -> i32 {
1856 self.spec.console_replicas.unwrap_or(2)
1857 }
1858
1859 pub fn console_configmap_name(&self) -> String {
1860 self.name_prefixed("console")
1861 }
1862
1863 pub fn console_deployment_name(&self) -> String {
1864 self.name_prefixed("console")
1865 }
1866
1867 pub fn console_service_name(&self) -> String {
1868 self.name_prefixed("console")
1869 }
1870
1871 pub fn console_external_certificate_name(&self) -> String {
1872 self.name_prefixed("console-external")
1873 }
1874
1875 pub fn console_external_certificate_secret_name(&self) -> String {
1876 self.name_prefixed("console-external-tls")
1877 }
1878
1879 pub fn persist_pubsub_service_name(&self, generation: u64) -> String {
1880 self.name_prefixed(&format!("persist-pubsub-{generation}"))
1881 }
1882
1883 pub fn listeners_configmap_name(&self, generation: u64) -> String {
1884 self.name_prefixed(&format!("listeners-{generation}"))
1885 }
1886
1887 pub fn name_prefixed(&self, suffix: &str) -> String {
1888 format!("mz{}-{}", self.resource_id(), suffix)
1889 }
1890
1891 pub fn resource_id(&self) -> &str {
1892 &self.status.as_ref().unwrap().resource_id
1893 }
1894
1895 pub fn system_parameter_configmap_name(&self) -> Option<String> {
1896 self.spec.system_parameter_configmap_name.clone()
1897 }
1898
1899 pub fn environmentd_scratch_volume_storage_requirement(&self) -> Quantity {
1900 self.spec
1901 .environmentd_scratch_volume_storage_requirement
1902 .clone()
1903 .unwrap_or_else(|| {
1904 self.spec
1905 .environmentd_resource_requirements
1906 .as_ref()
1907 .and_then(|requirements| {
1908 requirements
1909 .requests
1910 .as_ref()
1911 .or(requirements.limits.as_ref())
1912 })
1913 .and_then(|requirements| requirements.get("memory").cloned())
1918 .unwrap_or_else(|| Quantity("4096Mi".to_string()))
1920 })
1921 }
1922
1923 pub fn environment_id(&self, cloud_provider: &str, region: &str) -> String {
1924 format!(
1925 "{}-{}-{}-0",
1926 cloud_provider, region, self.spec.environment_id,
1927 )
1928 }
1929
1930 pub fn rollout_requested(&self) -> bool {
1931 self.status
1932 .as_ref()
1933 .map(|status| status.last_completed_rollout_hash != status.requested_rollout_hash)
1934 .unwrap_or(false)
1935 }
1936
1937 pub fn set_force_promote(&mut self) {
1938 self.spec.force_promote = Some(self.generate_rollout_hash());
1939 }
1940
1941 pub fn should_force_promote(&self) -> bool {
1942 self.spec.force_promote.as_ref()
1943 == self
1944 .status
1945 .as_ref()
1946 .and_then(|status| status.requested_rollout_hash.as_ref())
1947 || self.spec.rollout_strategy
1948 == MaterializeRolloutStrategy::ImmediatelyPromoteCausingDowntime
1949 }
1950
1951 pub fn conditions_need_update(&self) -> bool {
1952 let Some(status) = self.status.as_ref() else {
1953 return true;
1954 };
1955 if status.conditions.is_empty() {
1956 return true;
1957 }
1958 for condition in &status.conditions {
1959 if condition.observed_generation != self.meta().generation {
1960 return true;
1961 }
1962 }
1963 false
1964 }
1965
1966 pub fn is_ready_to_promote(&self, rollout_hash: &str) -> bool {
1967 let Some(status) = self.status.as_ref() else {
1968 return false;
1969 };
1970 if status.conditions.is_empty() {
1971 return false;
1972 }
1973 status
1974 .conditions
1975 .iter()
1976 .any(|condition| condition.reason == "ReadyToPromote")
1977 && status.requested_rollout_hash.as_deref() == Some(rollout_hash)
1978 }
1979
1980 pub fn is_promoting(&self) -> bool {
1981 let Some(status) = self.status.as_ref() else {
1982 return false;
1983 };
1984 if status.conditions.is_empty() {
1985 return false;
1986 }
1987 status
1988 .conditions
1989 .iter()
1990 .any(|condition| condition.reason == "Promoting")
1991 }
1992
1993 pub fn update_in_progress(&self) -> bool {
1994 let Some(status) = self.status.as_ref() else {
1995 return false;
1996 };
1997 if status.conditions.is_empty() {
1998 return false;
1999 }
2000 for condition in &status.conditions {
2001 if condition.type_ == "UpToDate" && condition.status == "Unknown" {
2002 return true;
2003 }
2004 }
2005 false
2006 }
2007
2008 pub fn meets_minimum_version(&self, minimum: &Version) -> bool {
2012 let version = parse_image_ref(&self.spec.environmentd_image_ref);
2013 match version {
2014 Some(version) => version.cmp_precedence(minimum).is_ge(),
2016 None => {
2022 tracing::warn!(
2023 image_ref = %self.spec.environmentd_image_ref,
2024 "failed to parse image ref",
2025 );
2026 true
2027 }
2028 }
2029 }
2030
2031 pub fn is_valid_upgrade_version(active_version: &Version, next_version: &Version) -> bool {
2035 if next_version.cmp_precedence(active_version) == std::cmp::Ordering::Less {
2040 return false;
2041 }
2042
2043 if active_version.major == 0 {
2044 if next_version.major != active_version.major {
2045 if next_version.major == 26 {
2046 return (active_version.minor == 147 && active_version.patch >= 20)
2050 || active_version.minor >= 164;
2051 } else {
2052 return false;
2053 }
2054 }
2055 if next_version.minor == 147 && active_version.minor == 130 {
2057 return true;
2058 }
2059 return next_version.minor <= active_version.minor + 1;
2061 } else if active_version.major >= 26 {
2062 return next_version.major <= active_version.major + 1;
2064 }
2065
2066 true
2067 }
2068
2069 pub fn within_upgrade_window(&self) -> bool {
2072 let active_environmentd_version = self
2073 .status
2074 .as_ref()
2075 .and_then(|status| {
2076 status
2077 .last_completed_rollout_environmentd_image_ref
2078 .as_ref()
2079 })
2080 .and_then(|image_ref| parse_image_ref(image_ref));
2081
2082 if let (Some(next_environmentd_version), Some(active_environmentd_version)) = (
2083 parse_image_ref(&self.spec.environmentd_image_ref),
2084 active_environmentd_version,
2085 ) {
2086 Self::is_valid_upgrade_version(
2087 &active_environmentd_version,
2088 &next_environmentd_version,
2089 )
2090 } else {
2091 true
2094 }
2095 }
2096
2097 pub fn status(&self) -> MaterializeStatus {
2098 self.status.clone().unwrap_or_else(|| {
2099 let mut status = MaterializeStatus::default();
2100
2101 status.resource_id = new_resource_id();
2102
2103 if let Some(last_active_generation) = self
2108 .annotations()
2109 .get(LAST_KNOWN_ACTIVE_GENERATION_ANNOTATION)
2110 {
2111 status.active_generation = last_active_generation
2112 .parse()
2113 .expect("valid int generation");
2114 }
2115
2116 status.last_completed_rollout_environmentd_image_ref =
2119 Some(self.spec.environmentd_image_ref.clone());
2120
2121 status
2122 })
2123 }
2124 }
2125
2126 #[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq)]
2127 #[serde(rename_all = "camelCase")]
2128 pub struct MaterializeStatus {
2129 pub resource_id: String,
2131 pub active_generation: u64,
2133 pub last_completed_rollout_environmentd_image_ref: Option<String>,
2137 pub last_completed_rollout_hash: Option<String>,
2139 pub requested_rollout_hash: Option<String>,
2142 pub conditions: Vec<Condition>,
2143 }
2144
2145 impl MaterializeStatus {
2146 pub fn needs_update(&self, other: &Self) -> bool {
2147 let now = Timestamp::now();
2148 let mut a = self.clone();
2149 for condition in &mut a.conditions {
2150 condition.last_transition_time = Time(now);
2151 }
2152 let mut b = other.clone();
2153 for condition in &mut b.conditions {
2154 condition.last_transition_time = Time(now);
2155 }
2156 a != b
2157 }
2158 }
2159
2160 impl ManagedResource for Materialize {
2161 fn default_labels(&self) -> BTreeMap<String, String> {
2162 BTreeMap::from_iter([
2163 (
2164 "materialize.cloud/organization-name".to_owned(),
2165 self.name_unchecked(),
2166 ),
2167 (
2168 "materialize.cloud/organization-namespace".to_owned(),
2169 self.namespace(),
2170 ),
2171 (
2172 "materialize.cloud/mz-resource-id".to_owned(),
2173 self.resource_id().to_owned(),
2174 ),
2175 ])
2176 }
2177
2178 fn app_name(&self) -> Option<&str> {
2179 Some("environmentd")
2180 }
2181 }
2182
2183 impl From<v1alpha1::Materialize> for Materialize {
2184 fn from(value: v1alpha1::Materialize) -> Self {
2185 let is_promoting = value.is_promoting();
2186 let service_account_annotations = if let Some(environmentd_iam_role_arn) =
2187 value.spec.environmentd_iam_role_arn
2188 {
2189 let mut annotations = value.spec.service_account_annotations.unwrap_or_default();
2190 annotations
2191 .entry("eks.amazonaws.com/role-arn".to_owned())
2192 .or_insert(environmentd_iam_role_arn);
2193 Some(annotations)
2194 } else {
2195 value.spec.service_account_annotations
2196 };
2197 let mut mz = Materialize {
2198 metadata: value.metadata,
2199 spec: MaterializeSpec {
2200 environmentd_image_ref: value.spec.environmentd_image_ref,
2201 environmentd_extra_args: value.spec.environmentd_extra_args,
2202 environmentd_extra_env: value.spec.environmentd_extra_env,
2203 environmentd_connection_role_arn: value.spec.environmentd_connection_role_arn,
2204 environmentd_resource_requirements: value
2205 .spec
2206 .environmentd_resource_requirements,
2207 environmentd_scratch_volume_storage_requirement: value
2208 .spec
2209 .environmentd_scratch_volume_storage_requirement,
2210 environmentd_readiness_probe: value.spec.environmentd_readiness_probe,
2211 environmentd_liveness_probe: value.spec.environmentd_liveness_probe,
2212 environmentd_startup_probe: value.spec.environmentd_startup_probe,
2213 balancerd_resource_requirements: value.spec.balancerd_resource_requirements,
2214 balancerd_configmap_name: value.spec.balancerd_configmap_name,
2215 console_resource_requirements: value.spec.console_resource_requirements,
2216 balancerd_replicas: value.spec.balancerd_replicas,
2217 balancerd_externally_scaled: value.spec.balancerd_externally_scaled,
2218 console_replicas: value.spec.console_replicas,
2219 service_account_name: value.spec.service_account_name,
2220 service_account_annotations,
2221 service_account_labels: value.spec.service_account_labels,
2222 pod_annotations: value.spec.pod_annotations,
2223 pod_labels: value.spec.pod_labels,
2224 force_promote: if value.spec.force_promote.is_empty()
2225 || &value.spec.force_promote == "00000000-0000-0000-0000-000000000000"
2226 {
2227 None
2228 } else {
2229 Some(value.spec.force_promote.to_string())
2230 },
2231 force_rollout: value.spec.force_rollout,
2232 rollout_strategy: value.spec.rollout_strategy,
2233 rollout_request_timeout: value.spec.rollout_request_timeout,
2234 backend_secret_name: value.spec.backend_secret_name,
2235 authenticator_kind: value.spec.authenticator_kind,
2236 enable_rbac: value.spec.enable_rbac,
2237 environment_id: value.spec.environment_id,
2238 system_parameter_configmap_name: value.spec.system_parameter_configmap_name,
2239 balancerd_external_certificate_spec: value
2240 .spec
2241 .balancerd_external_certificate_spec,
2242 console_external_certificate_spec: value.spec.console_external_certificate_spec,
2243 internal_certificate_spec: value.spec.internal_certificate_spec,
2244 },
2245 status: None,
2246 };
2247 let calculated_rollout_hash = mz.generate_rollout_hash();
2248 let last_completed_rollout_hash = match value
2249 .status
2250 .as_ref()
2251 .and_then(|status| status.last_completed_rollout_hash.to_owned())
2252 {
2253 Some(last_completed_rollout_hash) => Some(last_completed_rollout_hash),
2254 None => {
2255 let currently_rolling_out = value
2256 .status
2257 .as_ref()
2258 .map(|status| {
2259 status.last_completed_rollout_request != value.spec.request_rollout
2260 || status.last_completed_rollout_request.is_nil()
2263 })
2264 .unwrap_or(true);
2265 if currently_rolling_out {
2266 None
2268 } else {
2269 Some(calculated_rollout_hash.clone())
2270 }
2271 }
2272 };
2273 let requested_rollout_hash = if is_promoting {
2274 None
2275 } else {
2276 Some(calculated_rollout_hash)
2277 };
2278 mz.status = value.status.map(|status| MaterializeStatus {
2279 resource_id: status.resource_id,
2280 active_generation: status.active_generation,
2281 last_completed_rollout_environmentd_image_ref: status
2282 .last_completed_rollout_environmentd_image_ref,
2283 last_completed_rollout_hash,
2284 requested_rollout_hash,
2285 conditions: status.conditions,
2286 });
2287 mz
2288 }
2289 }
2290
2291 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
2300 #[serde(rename_all = "camelCase")]
2301 pub struct PartialMaterializeSpec {
2302 #[serde(
2303 default,
2304 with = "double_option",
2305 skip_serializing_if = "Option::is_none"
2306 )]
2307 pub environmentd_image_ref: PartialField,
2308 #[serde(
2309 default,
2310 with = "double_option",
2311 skip_serializing_if = "Option::is_none"
2312 )]
2313 pub environmentd_extra_args: PartialField,
2314 #[serde(
2315 default,
2316 with = "double_option",
2317 skip_serializing_if = "Option::is_none"
2318 )]
2319 pub environmentd_extra_env: PartialField,
2320 #[serde(
2321 default,
2322 with = "double_option",
2323 skip_serializing_if = "Option::is_none"
2324 )]
2325 pub environmentd_connection_role_arn: PartialField,
2326 #[serde(
2327 default,
2328 with = "double_option",
2329 skip_serializing_if = "Option::is_none"
2330 )]
2331 pub environmentd_resource_requirements: PartialField,
2332 #[serde(
2333 default,
2334 with = "double_option",
2335 skip_serializing_if = "Option::is_none"
2336 )]
2337 pub environmentd_scratch_volume_storage_requirement: PartialField,
2338 #[serde(
2339 default,
2340 with = "double_option",
2341 skip_serializing_if = "Option::is_none"
2342 )]
2343 pub environmentd_readiness_probe: PartialField,
2344 #[serde(
2345 default,
2346 with = "double_option",
2347 skip_serializing_if = "Option::is_none"
2348 )]
2349 pub environmentd_liveness_probe: PartialField,
2350 #[serde(
2351 default,
2352 with = "double_option",
2353 skip_serializing_if = "Option::is_none"
2354 )]
2355 pub environmentd_startup_probe: PartialField,
2356 #[serde(
2357 default,
2358 with = "double_option",
2359 skip_serializing_if = "Option::is_none"
2360 )]
2361 pub balancerd_resource_requirements: PartialField,
2362 #[serde(
2363 default,
2364 with = "double_option",
2365 skip_serializing_if = "Option::is_none"
2366 )]
2367 pub balancerd_configmap_name: PartialField,
2368 #[serde(
2369 default,
2370 with = "double_option",
2371 skip_serializing_if = "Option::is_none"
2372 )]
2373 pub console_resource_requirements: PartialField,
2374 #[serde(
2375 default,
2376 with = "double_option",
2377 skip_serializing_if = "Option::is_none"
2378 )]
2379 pub balancerd_replicas: PartialField,
2380 #[serde(
2381 default,
2382 with = "double_option",
2383 skip_serializing_if = "Option::is_none"
2384 )]
2385 pub balancerd_externally_scaled: PartialField,
2386 #[serde(
2387 default,
2388 with = "double_option",
2389 skip_serializing_if = "Option::is_none"
2390 )]
2391 pub console_replicas: PartialField,
2392 #[serde(
2393 default,
2394 with = "double_option",
2395 skip_serializing_if = "Option::is_none"
2396 )]
2397 pub service_account_name: PartialField,
2398 #[serde(
2399 default,
2400 with = "double_option",
2401 skip_serializing_if = "Option::is_none"
2402 )]
2403 pub service_account_annotations: PartialField,
2404 #[serde(
2405 default,
2406 with = "double_option",
2407 skip_serializing_if = "Option::is_none"
2408 )]
2409 pub service_account_labels: PartialField,
2410 #[serde(
2411 default,
2412 with = "double_option",
2413 skip_serializing_if = "Option::is_none"
2414 )]
2415 pub pod_annotations: PartialField,
2416 #[serde(
2417 default,
2418 with = "double_option",
2419 skip_serializing_if = "Option::is_none"
2420 )]
2421 pub pod_labels: PartialField,
2422 #[serde(
2423 default,
2424 with = "double_option",
2425 skip_serializing_if = "Option::is_none"
2426 )]
2427 pub force_promote: PartialField,
2428 #[serde(
2429 default,
2430 with = "double_option",
2431 skip_serializing_if = "Option::is_none"
2432 )]
2433 pub force_rollout: PartialField,
2434 #[serde(
2435 default,
2436 with = "double_option",
2437 skip_serializing_if = "Option::is_none"
2438 )]
2439 pub rollout_strategy: PartialField,
2440 #[serde(
2441 default,
2442 with = "double_option",
2443 skip_serializing_if = "Option::is_none"
2444 )]
2445 pub rollout_request_timeout: PartialField,
2446 #[serde(
2447 default,
2448 with = "double_option",
2449 skip_serializing_if = "Option::is_none"
2450 )]
2451 pub backend_secret_name: PartialField,
2452 #[serde(
2453 default,
2454 with = "double_option",
2455 skip_serializing_if = "Option::is_none"
2456 )]
2457 pub authenticator_kind: PartialField,
2458 #[serde(
2459 default,
2460 with = "double_option",
2461 skip_serializing_if = "Option::is_none"
2462 )]
2463 pub enable_rbac: PartialField,
2464 #[serde(
2465 default,
2466 with = "double_option",
2467 skip_serializing_if = "Option::is_none"
2468 )]
2469 pub environment_id: PartialField,
2470 #[serde(
2471 default,
2472 with = "double_option",
2473 skip_serializing_if = "Option::is_none"
2474 )]
2475 pub system_parameter_configmap_name: PartialField,
2476 #[serde(
2477 default,
2478 with = "double_option",
2479 skip_serializing_if = "Option::is_none"
2480 )]
2481 pub balancerd_external_certificate_spec: PartialField,
2482 #[serde(
2483 default,
2484 with = "double_option",
2485 skip_serializing_if = "Option::is_none"
2486 )]
2487 pub console_external_certificate_spec: PartialField,
2488 #[serde(
2489 default,
2490 with = "double_option",
2491 skip_serializing_if = "Option::is_none"
2492 )]
2493 pub internal_certificate_spec: PartialField,
2494 #[serde(flatten)]
2495 pub extra: serde_json::Map<String, serde_json::Value>,
2496 }
2497
2498 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
2500 #[serde(rename_all = "camelCase")]
2501 pub struct PartialMaterializeStatus {
2502 #[serde(
2503 default,
2504 with = "double_option",
2505 skip_serializing_if = "Option::is_none"
2506 )]
2507 pub resource_id: PartialField,
2508 #[serde(
2509 default,
2510 with = "double_option",
2511 skip_serializing_if = "Option::is_none"
2512 )]
2513 pub active_generation: PartialField,
2514 #[serde(
2515 default,
2516 with = "double_option",
2517 skip_serializing_if = "Option::is_none"
2518 )]
2519 pub last_completed_rollout_environmentd_image_ref: PartialField,
2520 #[serde(
2521 default,
2522 with = "double_option",
2523 skip_serializing_if = "Option::is_none"
2524 )]
2525 pub last_completed_rollout_hash: PartialField,
2526 #[serde(
2527 default,
2528 with = "double_option",
2529 skip_serializing_if = "Option::is_none"
2530 )]
2531 pub requested_rollout_hash: PartialField,
2532 #[serde(
2533 default,
2534 with = "double_option",
2535 skip_serializing_if = "Option::is_none"
2536 )]
2537 pub conditions: PartialField,
2538 #[serde(flatten)]
2539 pub extra: serde_json::Map<String, serde_json::Value>,
2540 }
2541
2542 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
2544 #[serde(rename_all = "camelCase")]
2545 pub struct PartialMaterialize {
2546 #[serde(skip_serializing_if = "Option::is_none")]
2547 pub api_version: Option<String>,
2548 #[serde(skip_serializing_if = "Option::is_none")]
2549 pub kind: Option<serde_json::Value>,
2550 #[serde(skip_serializing_if = "Option::is_none")]
2551 pub metadata: Option<serde_json::Value>,
2552 #[serde(skip_serializing_if = "Option::is_none")]
2553 pub spec: Option<PartialMaterializeSpec>,
2554 #[serde(skip_serializing_if = "Option::is_none")]
2555 pub status: Option<PartialMaterializeStatus>,
2556 #[serde(flatten)]
2557 pub extra: serde_json::Map<String, serde_json::Value>,
2558 }
2559
2560 impl From<MaterializeSpec> for PartialMaterializeSpec {
2561 fn from(spec: MaterializeSpec) -> Self {
2562 let MaterializeSpec {
2563 environmentd_image_ref,
2564 environmentd_extra_args,
2565 environmentd_extra_env,
2566 environmentd_connection_role_arn,
2567 environmentd_resource_requirements,
2568 environmentd_scratch_volume_storage_requirement,
2569 environmentd_readiness_probe,
2570 environmentd_liveness_probe,
2571 environmentd_startup_probe,
2572 balancerd_resource_requirements,
2573 balancerd_configmap_name,
2574 console_resource_requirements,
2575 balancerd_replicas,
2576 balancerd_externally_scaled,
2577 console_replicas,
2578 service_account_name,
2579 service_account_annotations,
2580 service_account_labels,
2581 pod_annotations,
2582 pod_labels,
2583 force_promote,
2584 force_rollout,
2585 rollout_strategy,
2586 rollout_request_timeout,
2587 backend_secret_name,
2588 authenticator_kind,
2589 enable_rbac,
2590 environment_id,
2591 system_parameter_configmap_name,
2592 balancerd_external_certificate_spec,
2593 console_external_certificate_spec,
2594 internal_certificate_spec,
2595 } = spec;
2596 Self {
2597 environmentd_image_ref: present(environmentd_image_ref),
2598 environmentd_extra_args: present_opt(environmentd_extra_args),
2599 environmentd_extra_env: present_opt(environmentd_extra_env),
2600 environmentd_connection_role_arn: present_opt(environmentd_connection_role_arn),
2601 environmentd_resource_requirements: present_opt(environmentd_resource_requirements),
2602 environmentd_scratch_volume_storage_requirement: present_opt(
2603 environmentd_scratch_volume_storage_requirement,
2604 ),
2605 environmentd_readiness_probe: present_opt(environmentd_readiness_probe),
2606 environmentd_liveness_probe: present_opt(environmentd_liveness_probe),
2607 environmentd_startup_probe: present_opt(environmentd_startup_probe),
2608 balancerd_resource_requirements: present_opt(balancerd_resource_requirements),
2609 balancerd_configmap_name: present_opt(balancerd_configmap_name),
2610 console_resource_requirements: present_opt(console_resource_requirements),
2611 balancerd_replicas: present_opt(balancerd_replicas),
2612 balancerd_externally_scaled: present_opt(balancerd_externally_scaled),
2613 console_replicas: present_opt(console_replicas),
2614 service_account_name: present_opt(service_account_name),
2615 service_account_annotations: present_opt(service_account_annotations),
2616 service_account_labels: present_opt(service_account_labels),
2617 pod_annotations: present_opt(pod_annotations),
2618 pod_labels: present_opt(pod_labels),
2619 force_promote: present_opt(force_promote),
2620 force_rollout: present(force_rollout),
2621 rollout_strategy: present(rollout_strategy),
2622 rollout_request_timeout: present(rollout_request_timeout),
2623 backend_secret_name: present(backend_secret_name),
2624 authenticator_kind: present(authenticator_kind),
2625 enable_rbac: present(enable_rbac),
2626 environment_id: present(environment_id),
2627 system_parameter_configmap_name: present_opt(system_parameter_configmap_name),
2628 balancerd_external_certificate_spec: present_opt(
2629 balancerd_external_certificate_spec,
2630 ),
2631 console_external_certificate_spec: present_opt(console_external_certificate_spec),
2632 internal_certificate_spec: present_opt(internal_certificate_spec),
2633 extra: serde_json::Map::new(),
2634 }
2635 }
2636 }
2637
2638 impl From<MaterializeStatus> for PartialMaterializeStatus {
2639 fn from(status: MaterializeStatus) -> Self {
2640 let MaterializeStatus {
2641 resource_id,
2642 active_generation,
2643 last_completed_rollout_environmentd_image_ref,
2644 last_completed_rollout_hash,
2645 requested_rollout_hash,
2646 conditions,
2647 } = status;
2648 Self {
2649 resource_id: present(resource_id),
2650 active_generation: present(active_generation),
2651 last_completed_rollout_environmentd_image_ref: present_opt(
2652 last_completed_rollout_environmentd_image_ref,
2653 ),
2654 last_completed_rollout_hash: present_opt(last_completed_rollout_hash),
2655 requested_rollout_hash: present_opt(requested_rollout_hash),
2656 conditions: present(conditions),
2657 extra: serde_json::Map::new(),
2658 }
2659 }
2660 }
2661
2662 impl From<Materialize> for PartialMaterialize {
2663 fn from(mz: Materialize) -> Self {
2664 let Materialize {
2665 metadata,
2666 spec,
2667 status,
2668 } = mz;
2669 Self {
2670 api_version: Some("materialize.cloud/v1".to_owned()),
2671 kind: Some("Materialize".into()),
2672 metadata: Some(
2673 serde_json::to_value(metadata).expect("ObjectMeta serializes to JSON"),
2674 ),
2675 spec: Some(spec.into()),
2676 status: status.map(Into::into),
2677 extra: serde_json::Map::new(),
2678 }
2679 }
2680 }
2681
2682 impl From<super::v1alpha1::PartialMaterializeSpec> for PartialMaterializeSpec {
2683 fn from(spec: super::v1alpha1::PartialMaterializeSpec) -> Self {
2684 let super::v1alpha1::PartialMaterializeSpec {
2685 environmentd_image_ref,
2686 environmentd_extra_args,
2687 environmentd_extra_env,
2688 environmentd_iam_role_arn,
2689 environmentd_connection_role_arn,
2690 environmentd_resource_requirements,
2691 environmentd_scratch_volume_storage_requirement,
2692 environmentd_readiness_probe,
2693 environmentd_liveness_probe,
2694 environmentd_startup_probe,
2695 balancerd_resource_requirements,
2696 balancerd_configmap_name,
2697 console_resource_requirements,
2698 balancerd_replicas,
2699 balancerd_externally_scaled,
2700 console_replicas,
2701 service_account_name,
2702 service_account_annotations,
2703 service_account_labels,
2704 pod_annotations,
2705 pod_labels,
2706 request_rollout: _,
2707 force_promote,
2708 force_rollout,
2709 in_place_rollout: _,
2710 rollout_strategy,
2711 rollout_request_timeout,
2712 backend_secret_name,
2713 authenticator_kind,
2714 enable_rbac,
2715 environment_id,
2716 system_parameter_configmap_name,
2717 balancerd_external_certificate_spec,
2718 console_external_certificate_spec,
2719 internal_certificate_spec,
2720 extra,
2721 } = spec;
2722 let service_account_annotations = merge_environmentd_iam_role_arn(
2723 service_account_annotations,
2724 environmentd_iam_role_arn,
2725 );
2726 let force_promote = match force_promote {
2729 Some(Some(value)) if value == "" || value == NIL_UUID_STR => None,
2730 other => other,
2731 };
2732 Self {
2733 environmentd_image_ref,
2734 environmentd_extra_args,
2735 environmentd_extra_env,
2736 environmentd_connection_role_arn,
2737 environmentd_resource_requirements,
2738 environmentd_scratch_volume_storage_requirement,
2739 environmentd_readiness_probe,
2740 environmentd_liveness_probe,
2741 environmentd_startup_probe,
2742 balancerd_resource_requirements,
2743 balancerd_configmap_name,
2744 console_resource_requirements,
2745 balancerd_replicas,
2746 balancerd_externally_scaled,
2747 console_replicas,
2748 service_account_name,
2749 service_account_annotations,
2750 service_account_labels,
2751 pod_annotations,
2752 pod_labels,
2753 force_promote,
2754 force_rollout,
2755 rollout_strategy,
2756 rollout_request_timeout,
2757 backend_secret_name,
2758 authenticator_kind,
2759 enable_rbac,
2760 environment_id,
2761 system_parameter_configmap_name,
2762 balancerd_external_certificate_spec,
2763 console_external_certificate_spec,
2764 internal_certificate_spec,
2765 extra,
2766 }
2767 }
2768 }
2769
2770 impl From<super::v1alpha1::PartialMaterializeStatus> for PartialMaterializeStatus {
2771 fn from(status: super::v1alpha1::PartialMaterializeStatus) -> Self {
2772 let super::v1alpha1::PartialMaterializeStatus {
2773 resource_id,
2774 active_generation,
2775 last_completed_rollout_request: _,
2776 last_completed_rollout_environmentd_image_ref,
2777 resources_hash: _,
2778 last_completed_rollout_hash,
2779 conditions,
2780 extra,
2781 } = status;
2782 Self {
2783 resource_id,
2784 active_generation,
2785 last_completed_rollout_environmentd_image_ref,
2786 last_completed_rollout_hash,
2787 requested_rollout_hash: None,
2792 conditions,
2793 extra,
2794 }
2795 }
2796 }
2797
2798 impl From<super::v1alpha1::PartialMaterialize> for PartialMaterialize {
2799 fn from(mz: super::v1alpha1::PartialMaterialize) -> Self {
2800 let super::v1alpha1::PartialMaterialize {
2801 api_version: _,
2802 kind,
2803 metadata,
2804 spec,
2805 status,
2806 extra,
2807 } = mz;
2808 Self {
2809 api_version: Some("materialize.cloud/v1".to_owned()),
2810 kind,
2811 metadata,
2812 spec: spec.map(Into::into),
2813 status: status.map(Into::into),
2814 extra,
2815 }
2816 }
2817 }
2818}
2819
2820const NIL_UUID_STR: &str = "00000000-0000-0000-0000-000000000000";
2822
2823pub type PartialField = Option<Option<serde_json::Value>>;
2832
2833mod double_option {
2837 use serde::{Deserialize, Deserializer, Serialize, Serializer};
2838
2839 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
2840 where
2841 T: Deserialize<'de>,
2842 D: Deserializer<'de>,
2843 {
2844 Option::<T>::deserialize(deserializer).map(Some)
2845 }
2846
2847 pub fn serialize<T, S>(value: &Option<Option<T>>, serializer: S) -> Result<S::Ok, S::Error>
2848 where
2849 T: Serialize,
2850 S: Serializer,
2851 {
2852 match value {
2853 Some(inner) => inner.serialize(serializer),
2854 None => serializer.serialize_none(),
2856 }
2857 }
2858}
2859
2860fn present<T: Serialize>(value: T) -> PartialField {
2863 Some(Some(
2864 serde_json::to_value(value).expect("CRD field serializes to JSON"),
2865 ))
2866}
2867
2868fn present_opt<T: Serialize>(value: Option<T>) -> PartialField {
2870 value.map(|value| Some(serde_json::to_value(value).expect("CRD field serializes to JSON")))
2871}
2872
2873fn merge_environmentd_iam_role_arn(
2878 annotations: PartialField,
2879 role_arn: PartialField,
2880) -> PartialField {
2881 let Some(Some(role_arn)) = role_arn else {
2882 return annotations;
2883 };
2884 let mut map = match annotations {
2885 Some(Some(serde_json::Value::Object(map))) => map,
2886 Some(Some(other)) => return Some(Some(other)),
2887 Some(None) | None => serde_json::Map::new(),
2888 };
2889 map.entry("eks.amazonaws.com/role-arn").or_insert(role_arn);
2890 Some(Some(serde_json::Value::Object(map)))
2891}
2892
2893pub fn convert_v1alpha1_to_v1(
2912 value: serde_json::Value,
2913) -> Result<serde_json::Value, anyhow::Error> {
2914 let complete = serde_json::from_value::<v1alpha1::Materialize>(value.clone()).ok();
2915 let partial: v1alpha1::PartialMaterialize = serde_json::from_value(value)?;
2916 let mut converted = v1::PartialMaterialize::from(partial);
2917 if let Some(complete) = complete {
2918 let typed = v1::Materialize::from(complete);
2919 if let (Some(status), Some(typed_status)) = (converted.status.as_mut(), typed.status) {
2920 status.requested_rollout_hash = present_opt(typed_status.requested_rollout_hash);
2921 status.last_completed_rollout_hash =
2922 present_opt(typed_status.last_completed_rollout_hash);
2923 }
2924 }
2925 Ok(serde_json::to_value(converted)?)
2926}
2927
2928pub fn convert_v1_to_v1alpha1(
2939 value: serde_json::Value,
2940) -> Result<serde_json::Value, anyhow::Error> {
2941 let complete = serde_json::from_value::<v1::Materialize>(value.clone()).ok();
2942 let partial: v1::PartialMaterialize = serde_json::from_value(value)?;
2943 let mut converted = v1alpha1::PartialMaterialize::from(partial);
2944 if let Some(complete) = complete {
2945 let typed = v1alpha1::Materialize::from(complete);
2946 if let Some(spec) = converted.spec.as_mut() {
2947 spec.request_rollout = present(typed.spec.request_rollout);
2948 }
2949 if let (Some(status), Some(typed_status)) = (converted.status.as_mut(), typed.status) {
2950 status.last_completed_rollout_request =
2951 present(typed_status.last_completed_rollout_request);
2952 status.resources_hash = present(typed_status.resources_hash);
2953 }
2954 }
2955 Ok(serde_json::to_value(converted)?)
2956}
2957
2958fn parse_image_ref(image_ref: &str) -> Option<Version> {
2959 image_ref
2960 .rsplit_once(':')
2961 .and_then(|(_repo, tag)| tag.strip_prefix('v'))
2962 .and_then(|tag| {
2963 let tag = tag.replace("--", "+");
2968 Version::parse(&tag).ok()
2969 })
2970}
2971
2972#[cfg(test)]
2973mod tests {
2974 use std::time::Duration;
2975
2976 use k8s_openapi::apimachinery::pkg::apis::meta::v1::{Condition, Time};
2977 use k8s_openapi::jiff::Timestamp;
2978 use kube::core::ObjectMeta;
2979 use semver::Version;
2980
2981 use super::v1alpha1::{Materialize, MaterializeSpec, MaterializeStatus};
2982 use super::{DEFAULT_ROLLOUT_REQUEST_TIMEOUT, FORCE_ROLLOUT_ANNOTATION, RolloutRequestTimeout};
2983
2984 #[mz_ore::test]
2985 #[cfg_attr(miri, ignore)] fn force_rollout_annotation_forces_new_generation() {
2987 let mut mz = super::v1::Materialize {
2993 spec: super::v1::MaterializeSpec {
2994 environmentd_image_ref: "materialize/environmentd:v26.0.0".to_owned(),
2995 ..Default::default()
2996 },
2997 metadata: ObjectMeta::default(),
2998 status: None,
2999 };
3000 let hash_without_annotation = mz.generate_rollout_hash();
3001 let force_without_annotation = Materialize::from(mz.clone()).force_rollout_value();
3002
3003 mz.metadata.annotations = Some(std::collections::BTreeMap::from_iter([(
3004 FORCE_ROLLOUT_ANNOTATION.to_owned(),
3005 "a4b56cbb-a13e-4f95-8a9d-425c9ba28576".to_owned(),
3006 )]));
3007 assert_ne!(mz.generate_rollout_hash(), hash_without_annotation);
3008 assert_ne!(
3009 Materialize::from(mz.clone()).force_rollout_value(),
3010 force_without_annotation
3011 );
3012
3013 let hash = mz.generate_rollout_hash();
3015 let force = Materialize::from(mz.clone()).force_rollout_value();
3016 mz.metadata.annotations.as_mut().unwrap().insert(
3017 FORCE_ROLLOUT_ANNOTATION.to_owned(),
3018 "3f61bf8d-0714-462c-8b3b-3d9a68d0bcba".to_owned(),
3019 );
3020 assert_ne!(mz.generate_rollout_hash(), hash);
3021 assert_ne!(Materialize::from(mz.clone()).force_rollout_value(), force);
3022 }
3023
3024 #[mz_ore::test]
3025 #[cfg_attr(miri, ignore)]
3026 fn balancerd_configmap_reference_preserves_rollout_hash_and_conversion() {
3027 let mut mz = super::v1::Materialize {
3028 spec: super::v1::MaterializeSpec {
3029 environmentd_image_ref: "materialize/environmentd:v26.0.0".to_owned(),
3030 ..Default::default()
3031 },
3032 metadata: ObjectMeta::default(),
3033 status: None,
3034 };
3035 let hash = mz.generate_rollout_hash();
3036 assert!(
3038 serde_json::to_value(&mz.spec)
3039 .unwrap()
3040 .get("balancerdConfigmapName")
3041 .is_none()
3042 );
3043 mz.spec.balancerd_configmap_name = Some("balancerd-settings".to_owned());
3044 assert_eq!(mz.generate_rollout_hash(), hash);
3045 let alpha = Materialize::from(mz.clone());
3046 assert_eq!(
3047 alpha.spec.balancerd_configmap_name,
3048 mz.spec.balancerd_configmap_name
3049 );
3050 assert_eq!(
3051 super::v1::Materialize::from(alpha)
3052 .spec
3053 .balancerd_configmap_name,
3054 mz.spec.balancerd_configmap_name,
3055 );
3056 for value in [
3057 serde_json::json!("balancerd-settings"),
3058 serde_json::Value::Null,
3059 ] {
3060 let subset = serde_json::json!({
3061 "apiVersion": "materialize.cloud/v1alpha1",
3062 "kind": "Materialize",
3063 "metadata": {"name": "mz"},
3064 "spec": {"balancerdConfigmapName": value},
3065 });
3066 let v1 = super::convert_v1alpha1_to_v1(subset.clone()).unwrap();
3067 assert_eq!(v1["spec"]["balancerdConfigmapName"], value);
3068 let roundtrip = super::convert_v1_to_v1alpha1(v1).unwrap();
3069 assert_eq!(roundtrip, subset);
3070 }
3071 }
3072
3073 #[mz_ore::test]
3074 #[cfg_attr(miri, ignore)]
3075 fn balancerd_externally_scaled_preserves_rollout_hash_and_conversion() {
3076 let mut mz = super::v1::Materialize {
3077 spec: super::v1::MaterializeSpec {
3078 environmentd_image_ref: "materialize/environmentd:v26.0.0".to_owned(),
3079 ..Default::default()
3080 },
3081 metadata: ObjectMeta::default(),
3082 status: None,
3083 };
3084 let hash = mz.generate_rollout_hash();
3085 assert!(
3087 serde_json::to_value(&mz.spec)
3088 .unwrap()
3089 .get("balancerdExternallyScaled")
3090 .is_none()
3091 );
3092 mz.spec.balancerd_externally_scaled = Some(true);
3093 assert_eq!(mz.generate_rollout_hash(), hash);
3094 let alpha = Materialize::from(mz.clone());
3095 assert_eq!(alpha.spec.balancerd_externally_scaled, Some(true));
3096 assert_eq!(
3097 super::v1::Materialize::from(alpha)
3098 .spec
3099 .balancerd_externally_scaled,
3100 Some(true),
3101 );
3102 for value in [serde_json::json!(true), serde_json::Value::Null] {
3103 let subset = serde_json::json!({
3104 "apiVersion": "materialize.cloud/v1alpha1",
3105 "kind": "Materialize",
3106 "metadata": {"name": "mz"},
3107 "spec": {"balancerdExternallyScaled": value},
3108 });
3109 let v1 = super::convert_v1alpha1_to_v1(subset.clone()).unwrap();
3110 assert_eq!(v1["spec"]["balancerdExternallyScaled"], value);
3111 let roundtrip = super::convert_v1_to_v1alpha1(v1).unwrap();
3112 assert_eq!(roundtrip, subset);
3113 }
3114 }
3115
3116 #[mz_ore::test]
3117 #[cfg_attr(miri, ignore)]
3118 fn environmentd_probes_trigger_rollout_and_survive_conversion() {
3119 let mut mz = super::v1::Materialize {
3120 spec: super::v1::MaterializeSpec {
3121 environmentd_image_ref: "materialize/environmentd:v26.0.0".to_owned(),
3122 ..Default::default()
3123 },
3124 metadata: ObjectMeta::default(),
3125 status: None,
3126 };
3127 let hash = mz.generate_rollout_hash();
3128 let spec = serde_json::to_value(&mz.spec).unwrap();
3130 assert!(spec.get("environmentdReadinessProbe").is_none());
3131 assert!(spec.get("environmentdLivenessProbe").is_none());
3132 assert!(spec.get("environmentdStartupProbe").is_none());
3133
3134 let probe = k8s_openapi::api::core::v1::Probe {
3135 failure_threshold: Some(60),
3136 ..Default::default()
3137 };
3138 mz.spec.environmentd_readiness_probe = Some(probe.clone());
3139 let readiness_hash = mz.generate_rollout_hash();
3140 assert_ne!(readiness_hash, hash);
3141 mz.spec.environmentd_liveness_probe = Some(probe.clone());
3142 let liveness_hash = mz.generate_rollout_hash();
3143 assert_ne!(liveness_hash, readiness_hash);
3144 mz.spec.environmentd_startup_probe = Some(probe);
3145 assert_ne!(mz.generate_rollout_hash(), liveness_hash);
3146
3147 let alpha = Materialize::from(mz.clone());
3148 assert_eq!(
3149 alpha.spec.environmentd_readiness_probe,
3150 mz.spec.environmentd_readiness_probe
3151 );
3152 assert_eq!(
3153 alpha.spec.environmentd_liveness_probe,
3154 mz.spec.environmentd_liveness_probe
3155 );
3156 assert_eq!(
3157 alpha.spec.environmentd_startup_probe,
3158 mz.spec.environmentd_startup_probe
3159 );
3160 let v1 = super::v1::Materialize::from(alpha);
3161 assert_eq!(
3162 v1.spec.environmentd_readiness_probe,
3163 mz.spec.environmentd_readiness_probe
3164 );
3165 assert_eq!(
3166 v1.spec.environmentd_liveness_probe,
3167 mz.spec.environmentd_liveness_probe
3168 );
3169 assert_eq!(
3170 v1.spec.environmentd_startup_probe,
3171 mz.spec.environmentd_startup_probe
3172 );
3173
3174 let subset = serde_json::json!({
3175 "apiVersion": "materialize.cloud/v1alpha1",
3176 "kind": "Materialize",
3177 "metadata": {"name": "mz"},
3178 "spec": {
3179 "environmentdReadinessProbe": {"periodSeconds": 5},
3180 "environmentdLivenessProbe": null,
3181 "environmentdStartupProbe": {"failureThreshold": 360},
3182 },
3183 });
3184 let v1 = super::convert_v1alpha1_to_v1(subset.clone()).unwrap();
3185 assert_eq!(v1["spec"], subset["spec"]);
3186 assert_eq!(super::convert_v1_to_v1alpha1(v1).unwrap(), subset);
3187 }
3188
3189 #[mz_ore::test]
3190 fn meets_minimum_version() {
3191 let mut mz = Materialize {
3192 spec: MaterializeSpec {
3193 environmentd_image_ref:
3194 "materialize/environmentd:devel-47116c24b8d0df33d3f60a9ee476aa8d7bce5953"
3195 .to_owned(),
3196 ..Default::default()
3197 },
3198 metadata: ObjectMeta {
3199 ..Default::default()
3200 },
3201 status: None,
3202 };
3203
3204 assert!(mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
3206 mz.spec.environmentd_image_ref = "materialize/environmentd:v0.34.0".to_owned();
3207 assert!(mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
3208 mz.spec.environmentd_image_ref = "materialize/environmentd:v0.35.0".to_owned();
3209 assert!(mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
3210 mz.spec.environmentd_image_ref = "materialize/environmentd:v0.34.3".to_owned();
3211 assert!(mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
3212 mz.spec.environmentd_image_ref = "materialize/environmentd@41af286dc0b172ed2f1ca934fd2278de4a1192302ffa07087cea2682e7d372e3".to_owned();
3213 assert!(mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
3214 mz.spec.environmentd_image_ref = "my.private.registry:5000:v0.34.3".to_owned();
3215 assert!(mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
3216 mz.spec.environmentd_image_ref = "materialize/environmentd:v0.asdf.0".to_owned();
3217 assert!(mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
3218 mz.spec.environmentd_image_ref =
3219 "materialize/environmentd:v0.146.0-dev.0--pr.g5a05a9e4ba873be8adaa528644aaae6e4c7cd29b"
3220 .to_owned();
3221 assert!(mz.meets_minimum_version(&Version::parse("0.146.0-dev.0").unwrap()));
3222
3223 mz.spec.environmentd_image_ref = "materialize/environmentd:v0.34.0-dev".to_owned();
3225 assert!(!mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
3226 mz.spec.environmentd_image_ref = "materialize/environmentd:v0.33.0".to_owned();
3227 assert!(!mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
3228 mz.spec.environmentd_image_ref = "materialize/environmentd:v0.34.0".to_owned();
3229 assert!(!mz.meets_minimum_version(&Version::parse("1.0.0").unwrap()));
3230 mz.spec.environmentd_image_ref = "my.private.registry:5000:v0.33.3".to_owned();
3231 assert!(!mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
3232 }
3233
3234 #[mz_ore::test]
3235 fn within_upgrade_window() {
3236 let mut mz = Materialize {
3237 spec: MaterializeSpec {
3238 environmentd_image_ref: "materialize/environmentd:v26.0.0".to_owned(),
3239 ..Default::default()
3240 },
3241 metadata: ObjectMeta {
3242 ..Default::default()
3243 },
3244 status: Some(MaterializeStatus {
3245 last_completed_rollout_environmentd_image_ref: Some(
3246 "materialize/environmentd:v26.0.0".to_owned(),
3247 ),
3248 ..Default::default()
3249 }),
3250 };
3251
3252 mz.spec.environmentd_image_ref = "materialize/environmentd:v27.7.3".to_owned();
3254 assert!(mz.within_upgrade_window());
3255
3256 mz.spec.environmentd_image_ref = "materialize/environmentd:v27.7.8-dev.0".to_owned();
3258 assert!(mz.within_upgrade_window());
3259
3260 mz.spec.environmentd_image_ref = "materialize/environmentd:v28.0.1".to_owned();
3262 assert!(!mz.within_upgrade_window());
3263
3264 mz.spec.environmentd_image_ref =
3266 "materialize/environmentd:v28.0.1.not_a_valid_version".to_owned();
3267 assert!(mz.within_upgrade_window());
3268
3269 mz.status
3271 .as_mut()
3272 .unwrap()
3273 .last_completed_rollout_environmentd_image_ref =
3274 Some("materialize/environmentd:v0.147.20".to_owned());
3275 mz.spec.environmentd_image_ref = "materialize/environmentd:v26.1.0".to_owned();
3276 assert!(mz.within_upgrade_window());
3277
3278 mz.status
3280 .as_mut()
3281 .unwrap()
3282 .last_completed_rollout_environmentd_image_ref =
3283 Some("materialize/environmentd:v26.11.0-dev.0+b".to_owned());
3284 mz.spec.environmentd_image_ref = "materialize/environmentd:v26.11.0-dev.0+a".to_owned();
3285 assert!(mz.within_upgrade_window());
3286 }
3287
3288 #[mz_ore::test]
3289 fn is_valid_upgrade_version() {
3290 let success_tests = [
3291 (Version::new(0, 83, 0), Version::new(0, 83, 0)),
3292 (Version::new(0, 83, 0), Version::new(0, 84, 0)),
3293 (Version::new(0, 9, 0), Version::new(0, 10, 0)),
3294 (Version::new(0, 99, 0), Version::new(0, 100, 0)),
3295 (Version::new(0, 83, 0), Version::new(0, 83, 1)),
3296 (Version::new(0, 83, 0), Version::new(0, 83, 2)),
3297 (Version::new(0, 83, 2), Version::new(0, 83, 10)),
3298 (Version::new(0, 147, 20), Version::new(26, 0, 0)),
3300 (Version::new(0, 164, 0), Version::new(26, 0, 0)),
3301 (Version::new(26, 0, 0), Version::new(26, 1, 0)),
3302 (Version::new(26, 5, 3), Version::new(26, 10, 0)),
3303 (Version::new(0, 130, 0), Version::new(0, 147, 0)),
3304 ];
3305 for (active_version, next_version) in success_tests {
3306 assert!(
3307 Materialize::is_valid_upgrade_version(&active_version, &next_version),
3308 "v{active_version} can upgrade to v{next_version}"
3309 );
3310 }
3311
3312 let failure_tests = [
3313 (Version::new(0, 83, 0), Version::new(0, 82, 0)),
3314 (Version::new(0, 83, 3), Version::new(0, 83, 2)),
3315 (Version::new(0, 83, 3), Version::new(1, 83, 3)),
3316 (Version::new(0, 83, 0), Version::new(0, 85, 0)),
3317 (Version::new(26, 0, 0), Version::new(28, 0, 0)),
3318 (Version::new(0, 130, 0), Version::new(26, 1, 0)),
3319 (Version::new(0, 147, 1), Version::new(26, 0, 0)),
3321 (Version::new(0, 148, 0), Version::new(26, 0, 0)),
3323 ];
3324 for (active_version, next_version) in failure_tests {
3325 assert!(
3326 !Materialize::is_valid_upgrade_version(&active_version, &next_version),
3327 "v{active_version} can't upgrade to v{next_version}"
3328 );
3329 }
3330 }
3331
3332 #[mz_ore::test]
3333 fn rollout_request_timeout() {
3334 let mz_with = |timeout: &str| Materialize {
3335 spec: MaterializeSpec {
3336 rollout_request_timeout: RolloutRequestTimeout(timeout.to_owned()),
3337 ..Default::default()
3338 },
3339 metadata: ObjectMeta::default(),
3340 status: None,
3341 };
3342
3343 let default = humantime::parse_duration(DEFAULT_ROLLOUT_REQUEST_TIMEOUT).unwrap();
3345 assert_eq!(default, Duration::from_secs(24 * 60 * 60));
3346
3347 assert_eq!(
3350 RolloutRequestTimeout::default().0,
3351 DEFAULT_ROLLOUT_REQUEST_TIMEOUT
3352 );
3353 assert_eq!(
3354 Materialize {
3355 spec: MaterializeSpec::default(),
3356 metadata: ObjectMeta::default(),
3357 status: None,
3358 }
3359 .rollout_request_timeout(),
3360 default
3361 );
3362
3363 assert_eq!(
3365 mz_with("1h").rollout_request_timeout(),
3366 Duration::from_secs(60 * 60)
3367 );
3368 assert_eq!(
3369 mz_with("90m").rollout_request_timeout(),
3370 Duration::from_secs(90 * 60)
3371 );
3372 assert_eq!(
3373 mz_with("1h 30m").rollout_request_timeout(),
3374 Duration::from_secs(90 * 60)
3375 );
3376 assert_eq!(mz_with("not a duration").rollout_request_timeout(), default);
3378 }
3379
3380 #[mz_ore::test]
3381 fn rollout_request_timeout_schema_default() {
3382 let crd = serde_json::to_value(<Materialize as kube::CustomResourceExt>::crd())
3386 .expect("CRD serializes");
3387 let default = &crd["spec"]["versions"][0]["schema"]["openAPIV3Schema"]["properties"]["spec"]
3388 ["properties"]["rolloutRequestTimeout"]["default"];
3389 assert_eq!(
3390 default,
3391 &serde_json::json!(DEFAULT_ROLLOUT_REQUEST_TIMEOUT),
3392 "rolloutRequestTimeout schema default missing/wrong in generated CRD",
3393 );
3394 }
3395
3396 #[mz_ore::test]
3397 fn rollout_in_progress_since() {
3398 let now = Timestamp::now();
3399 let condition = |type_: &str, status: &str| Condition {
3400 type_: type_.to_owned(),
3401 status: status.to_owned(),
3402 last_transition_time: Time(now),
3403 message: String::new(),
3404 observed_generation: None,
3405 reason: "Test".to_owned(),
3406 };
3407 let mz_with = |conditions: Vec<Condition>| Materialize {
3408 spec: MaterializeSpec::default(),
3409 metadata: ObjectMeta::default(),
3410 status: Some(MaterializeStatus {
3411 conditions,
3412 ..Default::default()
3413 }),
3414 };
3415
3416 let mz = Materialize {
3418 spec: MaterializeSpec::default(),
3419 metadata: ObjectMeta::default(),
3420 status: None,
3421 };
3422 assert_eq!(mz.rollout_in_progress_since(), None);
3423
3424 assert_eq!(
3427 mz_with(vec![condition("UpToDate", "Unknown")]).rollout_in_progress_since(),
3428 Some(now)
3429 );
3430
3431 assert_eq!(
3435 mz_with(vec![Condition {
3436 reason: "Promoting".to_owned(),
3437 ..condition("UpToDate", "Unknown")
3438 }])
3439 .rollout_in_progress_since(),
3440 None
3441 );
3442
3443 assert_eq!(
3445 mz_with(vec![condition("UpToDate", "True")]).rollout_in_progress_since(),
3446 None
3447 );
3448 assert_eq!(
3449 mz_with(vec![condition("UpToDate", "False")]).rollout_in_progress_since(),
3450 None
3451 );
3452 }
3453
3454 #[mz_ore::test]
3455 fn up_to_date_transition_time() {
3456 let stored = Timestamp::from_second(1_000).unwrap();
3459 let now = Timestamp::from_second(2_000).unwrap();
3460
3461 let condition = |status: &str| Condition {
3462 type_: "UpToDate".to_owned(),
3463 status: status.to_owned(),
3464 last_transition_time: Time(stored),
3465 message: String::new(),
3466 observed_generation: None,
3467 reason: "Test".to_owned(),
3468 };
3469 let mz_with = |conditions: Vec<Condition>| Materialize {
3470 spec: MaterializeSpec::default(),
3471 metadata: ObjectMeta::default(),
3472 status: Some(MaterializeStatus {
3473 conditions,
3474 ..Default::default()
3475 }),
3476 };
3477
3478 let mz = Materialize {
3480 spec: MaterializeSpec::default(),
3481 metadata: ObjectMeta::default(),
3482 status: None,
3483 };
3484 assert_eq!(mz.up_to_date_transition_time("Unknown", now), now);
3485
3486 assert_eq!(
3490 mz_with(vec![condition("Unknown")]).up_to_date_transition_time("Unknown", now),
3491 stored
3492 );
3493
3494 assert_eq!(
3496 mz_with(vec![condition("Unknown")]).up_to_date_transition_time("True", now),
3497 now
3498 );
3499 }
3500
3501 #[mz_ore::test]
3502 fn active_environmentd_image_ref() {
3503 const OLD: &str = "materialize/environmentd:v26.0.0";
3504 const NEW: &str = "materialize/environmentd:v27.0.0";
3505
3506 let mz_with = |spec_image: &str, status: Option<MaterializeStatus>| Materialize {
3507 spec: MaterializeSpec {
3508 environmentd_image_ref: spec_image.to_owned(),
3509 ..Default::default()
3510 },
3511 metadata: ObjectMeta::default(),
3512 status,
3513 };
3514
3515 let mz = mz_with(NEW, None);
3517 assert_eq!(mz.active_environmentd_image_ref(), NEW);
3518
3519 let mz = mz_with(
3523 NEW,
3524 Some(MaterializeStatus {
3525 last_completed_rollout_environmentd_image_ref: None,
3526 ..Default::default()
3527 }),
3528 );
3529 assert_eq!(mz.active_environmentd_image_ref(), NEW);
3530
3531 let mz = mz_with(
3534 NEW,
3535 Some(MaterializeStatus {
3536 last_completed_rollout_environmentd_image_ref: Some(NEW.to_owned()),
3537 ..Default::default()
3538 }),
3539 );
3540 assert_eq!(mz.active_environmentd_image_ref(), NEW);
3541
3542 let mz = mz_with(
3551 NEW,
3552 Some(MaterializeStatus {
3553 last_completed_rollout_environmentd_image_ref: Some(OLD.to_owned()),
3554 ..Default::default()
3555 }),
3556 );
3557 assert_eq!(mz.active_environmentd_image_ref(), OLD);
3558 }
3559
3560 #[mz_ore::test]
3564 fn convert_partial_v1alpha1_to_v1() {
3565 let subset = serde_json::json!({
3566 "apiVersion": "materialize.cloud/v1alpha1",
3567 "kind": "Materialize",
3568 "metadata": {"name": "mz", "namespace": "materialize"},
3569 "spec": {
3570 "environmentdIamRoleArn": "arn:aws:iam::123456789012:role/mz",
3571 "requestRollout": "1e6ef7bc-bff2-4bd4-90dc-eda5697bd0e6",
3572 "inPlaceRollout": false,
3573 "forcePromote": "",
3574 "serviceAccountLabels": {"team": "data"},
3575 },
3576 "status": {
3577 "activeGeneration": 3,
3578 "lastCompletedRolloutRequest": "1e6ef7bc-bff2-4bd4-90dc-eda5697bd0e6",
3579 "resourcesHash": "abc123",
3580 },
3581 });
3582 let converted = super::convert_v1alpha1_to_v1(subset).unwrap();
3583 assert_eq!(converted["apiVersion"], "materialize.cloud/v1");
3584 assert_eq!(converted["kind"], "Materialize");
3585 assert_eq!(converted["metadata"]["name"], "mz");
3586 let spec = converted["spec"].as_object().unwrap();
3587 assert!(!spec.contains_key("requestRollout"));
3588 assert!(!spec.contains_key("inPlaceRollout"));
3589 assert!(!spec.contains_key("forcePromote"));
3590 assert!(!spec.contains_key("environmentdIamRoleArn"));
3591 assert_eq!(
3592 spec["serviceAccountAnnotations"]["eks.amazonaws.com/role-arn"],
3593 "arn:aws:iam::123456789012:role/mz"
3594 );
3595 assert_eq!(spec["serviceAccountLabels"]["team"], "data");
3596 let status = converted["status"].as_object().unwrap();
3597 assert_eq!(status["activeGeneration"], 3);
3598 assert!(!status.contains_key("lastCompletedRolloutRequest"));
3599 assert!(!status.contains_key("resourcesHash"));
3600 assert!(!status.contains_key("requestedRolloutHash"));
3601
3602 let subset = serde_json::json!({
3604 "apiVersion": "materialize.cloud/v1alpha1",
3605 "kind": "Materialize",
3606 "metadata": {"name": "mz"},
3607 "spec": {"forcePromote": "1e6ef7bc-bff2-4bd4-90dc-eda5697bd0e6"},
3608 });
3609 let converted = super::convert_v1alpha1_to_v1(subset).unwrap();
3610 assert_eq!(
3611 converted["spec"]["forcePromote"],
3612 "1e6ef7bc-bff2-4bd4-90dc-eda5697bd0e6"
3613 );
3614 }
3615
3616 #[mz_ore::test]
3617 fn convert_partial_v1_to_v1alpha1() {
3618 let subset = serde_json::json!({
3619 "apiVersion": "materialize.cloud/v1",
3620 "kind": "Materialize",
3621 "metadata": {"name": "mz"},
3622 "spec": {"environmentdImageRef": "materialize/environmentd:v26.0.0"},
3623 "status": {"requestedRolloutHash": "abc123", "activeGeneration": 1},
3624 });
3625 let converted = super::convert_v1_to_v1alpha1(subset).unwrap();
3626 assert_eq!(converted["apiVersion"], "materialize.cloud/v1alpha1");
3627 assert_eq!(
3628 converted["spec"]["environmentdImageRef"],
3629 "materialize/environmentd:v26.0.0"
3630 );
3631 let status = converted["status"].as_object().unwrap();
3632 assert_eq!(status["activeGeneration"], 1);
3633 assert!(!status.contains_key("requestedRolloutHash"));
3634 assert!(!status.contains_key("resourcesHash"));
3635 assert!(
3638 !converted["spec"]
3639 .as_object()
3640 .unwrap()
3641 .contains_key("requestRollout")
3642 );
3643 }
3644
3645 #[mz_ore::test]
3646 #[cfg_attr(miri, ignore)] fn convert_full_v1alpha1_to_v1_derives_fields() {
3648 let mz = Materialize {
3649 metadata: ObjectMeta {
3650 name: Some("mz".to_owned()),
3651 namespace: Some("materialize".to_owned()),
3652 ..Default::default()
3653 },
3654 spec: MaterializeSpec {
3655 environmentd_image_ref: "materialize/environmentd:v26.0.0".to_owned(),
3656 backend_secret_name: "mz-backend".to_owned(),
3657 ..Default::default()
3658 },
3659 status: Some(MaterializeStatus::default()),
3660 };
3661 let value = serde_json::to_value(&mz).unwrap();
3662 let converted = super::convert_v1alpha1_to_v1(value).unwrap();
3663 assert_eq!(converted["apiVersion"], "materialize.cloud/v1");
3664 assert!(converted["status"]["requestedRolloutHash"].is_string());
3667 }
3668
3669 #[mz_ore::test]
3670 fn convert_rejects_non_objects() {
3671 assert!(super::convert_v1alpha1_to_v1(serde_json::json!("not an object")).is_err());
3672 assert!(super::convert_v1_to_v1alpha1(serde_json::json!(42)).is_err());
3673 assert!(
3674 super::convert_v1alpha1_to_v1(serde_json::json!({"spec": "not an object"})).is_err()
3675 );
3676 }
3677
3678 #[mz_ore::test]
3679 fn convert_preserves_null_vs_absent() {
3680 let subset = serde_json::json!({
3681 "apiVersion": "materialize.cloud/v1alpha1",
3682 "kind": "Materialize",
3683 "metadata": {"name": "mz"},
3684 "spec": {
3685 "environmentdExtraArgs": null,
3686 "backendSecretName": "mz-backend",
3687 },
3688 });
3689 let converted = super::convert_v1alpha1_to_v1(subset).unwrap();
3690 let spec = converted["spec"].as_object().unwrap();
3691 assert!(spec.contains_key("environmentdExtraArgs"));
3692 assert!(spec["environmentdExtraArgs"].is_null());
3693 assert!(!spec.contains_key("consoleReplicas"));
3694 }
3695
3696 #[mz_ore::test]
3701 #[cfg_attr(miri, ignore)] fn convert_faithful_output_for_required_field_subsets() {
3703 let subset = serde_json::json!({
3704 "apiVersion": "materialize.cloud/v1alpha1",
3705 "kind": "Materialize",
3706 "metadata": {"name": "mz"},
3707 "spec": {
3708 "environmentdImageRef": "materialize/environmentd:v26.0.0",
3709 "backendSecretName": "mz-backend",
3710 },
3711 });
3712 let converted = super::convert_v1alpha1_to_v1(subset).unwrap();
3713 let spec = converted["spec"].as_object().unwrap();
3714 let mut keys: Vec<_> = spec.keys().cloned().collect();
3715 keys.sort();
3716 assert_eq!(keys, ["backendSecretName", "environmentdImageRef"]);
3717 }
3718
3719 #[mz_ore::test]
3720 fn convert_passes_unknown_fields_through() {
3721 let subset = serde_json::json!({
3722 "apiVersion": "materialize.cloud/v1alpha1",
3723 "kind": "Materialize",
3724 "metadata": {"name": "mz"},
3725 "spec": {"someFutureField": {"a": 1}},
3726 "someTopLevelField": true,
3727 });
3728 let converted = super::convert_v1alpha1_to_v1(subset).unwrap();
3729 assert_eq!(converted["spec"]["someFutureField"]["a"], 1);
3730 assert_eq!(converted["someTopLevelField"], true);
3731 }
3732
3733 #[mz_ore::test]
3734 #[cfg_attr(miri, ignore)] fn convert_full_v1_to_v1alpha1_derives_request_rollout() {
3736 let mz = super::v1::Materialize {
3737 metadata: ObjectMeta {
3738 name: Some("mz".to_owned()),
3739 namespace: Some("materialize".to_owned()),
3740 ..Default::default()
3741 },
3742 spec: super::v1::MaterializeSpec {
3743 environmentd_image_ref: "materialize/environmentd:v26.0.0".to_owned(),
3744 backend_secret_name: "mz-backend".to_owned(),
3745 ..Default::default()
3746 },
3747 status: Some(super::v1::MaterializeStatus::default()),
3748 };
3749 let expected = Materialize::from(mz.clone()).spec.request_rollout;
3750 let converted = super::convert_v1_to_v1alpha1(serde_json::to_value(&mz).unwrap()).unwrap();
3751 assert_eq!(converted["apiVersion"], "materialize.cloud/v1alpha1");
3752 assert_eq!(
3753 converted["spec"]["requestRollout"],
3754 expected.hyphenated().to_string()
3755 );
3756 let status = converted["status"].as_object().unwrap();
3760 assert!(status["resourcesHash"].is_string());
3761 assert!(status["lastCompletedRolloutRequest"].is_string());
3762 }
3763
3764 #[mz_ore::test]
3769 fn partial_mirror_from_typed_omits_unset_fields() {
3770 let mz = Materialize {
3771 metadata: ObjectMeta::default(),
3772 spec: MaterializeSpec {
3773 environmentd_image_ref: "materialize/environmentd:v26.0.0".to_owned(),
3774 backend_secret_name: "mz-backend".to_owned(),
3775 ..Default::default()
3776 },
3777 status: None,
3778 };
3779 let value = serde_json::to_value(super::v1alpha1::PartialMaterialize::from(mz)).unwrap();
3780 let spec = value["spec"].as_object().unwrap();
3781 assert!(!spec.contains_key("balancerdReplicas"));
3782 for (key, field_value) in spec {
3783 assert!(!field_value.is_null(), "unexpected null for {key}");
3784 }
3785 assert!(!value.as_object().unwrap().contains_key("status"));
3786 }
3787}