1#![allow(rustdoc::invalid_html_tags)]
16
17use std::collections::BTreeMap;
18use std::time::Duration;
19
20use k8s_openapi::{
21 api::core::v1::{EnvVar, 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 pub balancerd_resource_requirements: Option<ResourceRequirements>,
171 pub console_resource_requirements: Option<ResourceRequirements>,
173 pub balancerd_replicas: Option<i32>,
175 pub console_replicas: Option<i32>,
177
178 pub service_account_name: Option<String>,
181 pub service_account_annotations: Option<BTreeMap<String, String>>,
188 pub service_account_labels: Option<BTreeMap<String, String>>,
190 pub pod_annotations: Option<BTreeMap<String, String>>,
192 pub pod_labels: Option<BTreeMap<String, String>>,
194
195 #[serde(default)]
207 pub request_rollout: Uuid,
208 #[serde(default)]
213 pub force_promote: String,
214 #[serde(default)]
221 pub force_rollout: Uuid,
222 #[kube(deprecated)]
226 #[serde(default)]
227 pub in_place_rollout: bool,
228 #[serde(default)]
230 pub rollout_strategy: MaterializeRolloutStrategy,
231 #[serde(default)]
252 pub rollout_request_timeout: RolloutRequestTimeout,
253 pub backend_secret_name: String,
258 #[serde(default)]
260 pub authenticator_kind: AuthenticatorKind,
261 #[serde(default)]
263 pub enable_rbac: bool,
264
265 #[serde(default)]
272 pub environment_id: Uuid,
273
274 pub system_parameter_configmap_name: Option<String>,
289
290 pub balancerd_external_certificate_spec: Option<MaterializeCertSpec>,
294 pub console_external_certificate_spec: Option<MaterializeCertSpec>,
299 pub internal_certificate_spec: Option<MaterializeCertSpec>,
304 }
305
306 impl Materialize {
307 pub fn backend_secret_name(&self) -> String {
308 self.spec.backend_secret_name.clone()
309 }
310
311 pub fn namespace(&self) -> String {
312 self.meta().namespace.clone().unwrap()
313 }
314
315 pub fn create_service_account(&self) -> bool {
316 self.spec.service_account_name.is_none()
317 }
318
319 pub fn service_account_name(&self) -> String {
320 self.spec
321 .service_account_name
322 .clone()
323 .unwrap_or_else(|| self.name_unchecked())
324 }
325
326 pub fn role_name(&self) -> String {
327 self.name_unchecked()
328 }
329
330 pub fn role_binding_name(&self) -> String {
331 self.name_unchecked()
332 }
333
334 pub fn environmentd_statefulset_name(&self, generation: u64) -> String {
335 self.name_prefixed(&format!("environmentd-{generation}"))
336 }
337
338 pub fn environmentd_app_name(&self) -> String {
339 "environmentd".to_owned()
340 }
341
342 pub fn environmentd_service_name(&self) -> String {
343 self.name_prefixed("environmentd")
344 }
345
346 pub fn environmentd_service_internal_fqdn(&self) -> String {
347 format!(
348 "{}.{}.svc.cluster.local",
349 self.environmentd_service_name(),
350 self.meta().namespace.as_ref().unwrap()
351 )
352 }
353
354 pub fn environmentd_generation_service_name(&self, generation: u64) -> String {
355 self.name_prefixed(&format!("environmentd-{generation}"))
356 }
357
358 pub fn balancerd_app_name(&self) -> String {
359 "balancerd".to_owned()
360 }
361
362 pub fn environmentd_certificate_name(&self) -> String {
363 self.name_prefixed("environmentd-external")
364 }
365
366 pub fn environmentd_certificate_secret_name(&self) -> String {
367 self.name_prefixed("environmentd-tls")
368 }
369
370 pub fn balancerd_deployment_name(&self) -> String {
371 self.name_prefixed("balancerd")
372 }
373
374 pub fn balancerd_service_name(&self) -> String {
375 self.name_prefixed("balancerd")
376 }
377
378 pub fn console_app_name(&self) -> String {
379 "console".to_owned()
380 }
381
382 pub fn balancerd_external_certificate_name(&self) -> String {
383 self.name_prefixed("balancerd-external")
384 }
385
386 pub fn balancerd_external_certificate_secret_name(&self) -> String {
387 self.name_prefixed("balancerd-external-tls")
388 }
389
390 pub fn balancerd_replicas(&self) -> i32 {
391 self.spec.balancerd_replicas.unwrap_or(2)
392 }
393
394 pub fn console_replicas(&self) -> i32 {
395 self.spec.console_replicas.unwrap_or(2)
396 }
397
398 pub fn console_configmap_name(&self) -> String {
399 self.name_prefixed("console")
400 }
401
402 pub fn console_deployment_name(&self) -> String {
403 self.name_prefixed("console")
404 }
405
406 pub fn console_service_name(&self) -> String {
407 self.name_prefixed("console")
408 }
409
410 pub fn console_external_certificate_name(&self) -> String {
411 self.name_prefixed("console-external")
412 }
413
414 pub fn console_external_certificate_secret_name(&self) -> String {
415 self.name_prefixed("console-external-tls")
416 }
417
418 pub fn persist_pubsub_service_name(&self, generation: u64) -> String {
419 self.name_prefixed(&format!("persist-pubsub-{generation}"))
420 }
421
422 pub fn listeners_configmap_name(&self, generation: u64) -> String {
423 self.name_prefixed(&format!("listeners-{generation}"))
424 }
425
426 pub fn name_prefixed(&self, suffix: &str) -> String {
427 format!("mz{}-{}", self.resource_id(), suffix)
428 }
429
430 pub fn resource_id(&self) -> &str {
431 &self.status.as_ref().unwrap().resource_id
432 }
433
434 pub fn system_parameter_configmap_name(&self) -> Option<String> {
435 self.spec.system_parameter_configmap_name.clone()
436 }
437
438 pub fn environmentd_scratch_volume_storage_requirement(&self) -> Quantity {
439 self.spec
440 .environmentd_scratch_volume_storage_requirement
441 .clone()
442 .unwrap_or_else(|| {
443 self.spec
444 .environmentd_resource_requirements
445 .as_ref()
446 .and_then(|requirements| {
447 requirements
448 .requests
449 .as_ref()
450 .or(requirements.limits.as_ref())
451 })
452 .and_then(|requirements| requirements.get("memory").cloned())
457 .unwrap_or_else(|| Quantity("4096Mi".to_string()))
459 })
460 }
461
462 pub fn environment_id(&self, cloud_provider: &str, region: &str) -> String {
463 format!(
464 "{}-{}-{}-0",
465 cloud_provider, region, self.spec.environment_id,
466 )
467 }
468
469 pub fn requested_reconciliation_id(&self) -> Uuid {
470 self.spec.request_rollout
471 }
472
473 pub fn force_rollout_value(&self) -> String {
485 match self
486 .meta()
487 .annotations
488 .as_ref()
489 .and_then(|annotations| annotations.get(FORCE_ROLLOUT_ANNOTATION))
490 {
491 Some(annotation) => format!("{}/{}", self.spec.force_rollout, annotation),
492 None => self.spec.force_rollout.to_string(),
493 }
494 }
495
496 pub fn rollout_requested(&self) -> bool {
497 self.requested_reconciliation_id()
498 != self
499 .status
500 .as_ref()
501 .map_or_else(Uuid::nil, |status| status.last_completed_rollout_request)
502 }
503
504 pub fn rollout_request_timeout(&self) -> Duration {
509 let timeout = &self.spec.rollout_request_timeout.0;
510 humantime::parse_duration(timeout)
511 .or_else(|e| {
512 tracing::warn!(
513 rollout_request_timeout = %timeout,
514 "failed to parse rolloutRequestTimeout, using default: {e}",
515 );
516 humantime::parse_duration(DEFAULT_ROLLOUT_REQUEST_TIMEOUT)
517 })
518 .expect("DEFAULT_ROLLOUT_REQUEST_TIMEOUT must be a valid duration")
519 }
520
521 pub fn rollout_in_progress_since(&self) -> Option<Timestamp> {
538 self.status
539 .as_ref()?
540 .conditions
541 .iter()
542 .find_map(|condition| {
543 if condition.type_ == "UpToDate"
544 && condition.status == "Unknown"
545 && condition.reason != "Promoting"
546 {
547 Some(condition.last_transition_time.0)
548 } else {
549 None
550 }
551 })
552 }
553
554 pub fn up_to_date_transition_time(&self, new_status: &str, now: Timestamp) -> Timestamp {
566 self.status
567 .as_ref()
568 .and_then(|status| {
569 status
570 .conditions
571 .iter()
572 .find(|condition| condition.type_ == "UpToDate")
573 })
574 .filter(|condition| condition.status == new_status)
575 .map_or(now, |condition| condition.last_transition_time.0)
576 }
577
578 pub fn active_environmentd_image_ref(&self) -> &str {
586 self.status
587 .as_ref()
588 .and_then(|s| s.last_completed_rollout_environmentd_image_ref.as_deref())
589 .unwrap_or(&self.spec.environmentd_image_ref)
590 }
591
592 pub fn set_force_promote(&mut self) {
593 self.spec.force_promote = self.spec.request_rollout.hyphenated().to_string();
594 }
595
596 pub fn should_force_promote(&self) -> bool {
597 self.spec.force_promote == self.spec.request_rollout.hyphenated().to_string()
598 || self.spec.force_promote
599 == super::v1::Materialize::from(self.clone()).generate_rollout_hash()
600 || self.spec.rollout_strategy
601 == MaterializeRolloutStrategy::ImmediatelyPromoteCausingDowntime
602 }
603
604 pub fn conditions_need_update(&self) -> bool {
605 let Some(status) = self.status.as_ref() else {
606 return true;
607 };
608 if status.conditions.is_empty() {
609 return true;
610 }
611 for condition in &status.conditions {
612 if condition.observed_generation != self.meta().generation {
613 return true;
614 }
615 }
616 false
617 }
618
619 pub fn is_ready_to_promote(&self, resources_hash: &str) -> bool {
620 let Some(status) = self.status.as_ref() else {
621 return false;
622 };
623 if status.conditions.is_empty() {
624 return false;
625 }
626 status
627 .conditions
628 .iter()
629 .any(|condition| condition.reason == "ReadyToPromote")
630 && &status.resources_hash == resources_hash
631 }
632
633 pub fn is_promoting(&self) -> bool {
634 let Some(status) = self.status.as_ref() else {
635 return false;
636 };
637 if status.conditions.is_empty() {
638 return false;
639 }
640 status
641 .conditions
642 .iter()
643 .any(|condition| condition.reason == "Promoting")
644 }
645
646 pub fn update_in_progress(&self) -> bool {
647 let Some(status) = self.status.as_ref() else {
648 return false;
649 };
650 if status.conditions.is_empty() {
651 return false;
652 }
653 for condition in &status.conditions {
654 if condition.type_ == "UpToDate" && condition.status == "Unknown" {
655 return true;
656 }
657 }
658 false
659 }
660
661 pub fn meets_minimum_version(&self, minimum: &Version) -> bool {
665 let version = parse_image_ref(&self.spec.environmentd_image_ref);
666 match version {
667 Some(version) => version.cmp_precedence(minimum).is_ge(),
669 None => {
675 tracing::warn!(
676 image_ref = %self.spec.environmentd_image_ref,
677 "failed to parse image ref",
678 );
679 true
680 }
681 }
682 }
683
684 pub fn is_valid_upgrade_version(active_version: &Version, next_version: &Version) -> bool {
688 if next_version.cmp_precedence(active_version) == std::cmp::Ordering::Less {
693 return false;
694 }
695
696 if active_version.major == 0 {
697 if next_version.major != active_version.major {
698 if next_version.major == 26 {
699 return (active_version.minor == 147 && active_version.patch >= 20)
702 || active_version.minor >= 164;
703 } else {
704 return false;
705 }
706 }
707 if next_version.minor == 147 && active_version.minor == 130 {
709 return true;
710 }
711 return next_version.minor <= active_version.minor + 1;
713 } else if active_version.major >= 26 {
714 return next_version.major <= active_version.major + 1;
716 }
717
718 true
719 }
720
721 pub fn within_upgrade_window(&self) -> bool {
724 let active_environmentd_version = self
725 .status
726 .as_ref()
727 .and_then(|status| {
728 status
729 .last_completed_rollout_environmentd_image_ref
730 .as_ref()
731 })
732 .and_then(|image_ref| parse_image_ref(image_ref));
733
734 if let (Some(next_environmentd_version), Some(active_environmentd_version)) = (
735 parse_image_ref(&self.spec.environmentd_image_ref),
736 active_environmentd_version,
737 ) {
738 Self::is_valid_upgrade_version(
739 &active_environmentd_version,
740 &next_environmentd_version,
741 )
742 } else {
743 true
746 }
747 }
748
749 pub fn status(&self) -> MaterializeStatus {
750 self.status.clone().unwrap_or_else(|| {
751 let mut status = MaterializeStatus::default();
752
753 status.resource_id = new_resource_id();
754
755 if let Some(last_active_generation) = self
760 .annotations()
761 .get(LAST_KNOWN_ACTIVE_GENERATION_ANNOTATION)
762 {
763 status.active_generation = last_active_generation
764 .parse()
765 .expect("valid int generation");
766 }
767
768 status.last_completed_rollout_environmentd_image_ref =
771 Some(self.spec.environmentd_image_ref.clone());
772
773 status
774 })
775 }
776 }
777
778 #[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq)]
779 #[serde(rename_all = "camelCase")]
780 pub struct MaterializeStatus {
781 pub resource_id: String,
783 pub active_generation: u64,
785 pub last_completed_rollout_request: Uuid,
787 pub last_completed_rollout_environmentd_image_ref: Option<String>,
791 pub resources_hash: String,
796 pub last_completed_rollout_hash: Option<String>,
799 pub conditions: Vec<Condition>,
800 }
801
802 impl MaterializeStatus {
803 pub fn needs_update(&self, other: &Self) -> bool {
804 let now = Timestamp::now();
805 let mut a = self.clone();
806 for condition in &mut a.conditions {
807 condition.last_transition_time = Time(now);
808 }
809 let mut b = other.clone();
810 for condition in &mut b.conditions {
811 condition.last_transition_time = Time(now);
812 }
813 a != b
814 }
815 }
816
817 impl ManagedResource for Materialize {
818 fn default_labels(&self) -> BTreeMap<String, String> {
819 BTreeMap::from_iter([
820 (
821 "materialize.cloud/organization-name".to_owned(),
822 self.name_unchecked(),
823 ),
824 (
825 "materialize.cloud/organization-namespace".to_owned(),
826 self.namespace(),
827 ),
828 (
829 "materialize.cloud/mz-resource-id".to_owned(),
830 self.resource_id().to_owned(),
831 ),
832 ])
833 }
834
835 fn app_name(&self) -> Option<&str> {
836 Some("environmentd")
837 }
838 }
839
840 impl From<v1::Materialize> for Materialize {
841 fn from(value: v1::Materialize) -> Self {
842 let rollout_hash = value.generate_rollout_hash();
843 let request_rollout = Uuid::new_v5(&Uuid::NAMESPACE_OID, rollout_hash.as_bytes());
847 Materialize {
848 metadata: value.metadata,
849 spec: MaterializeSpec {
850 environmentd_image_ref: value.spec.environmentd_image_ref,
851 environmentd_extra_args: value.spec.environmentd_extra_args,
852 environmentd_extra_env: value.spec.environmentd_extra_env,
853 environmentd_iam_role_arn: None,
854 environmentd_connection_role_arn: value.spec.environmentd_connection_role_arn,
855 environmentd_resource_requirements: value
856 .spec
857 .environmentd_resource_requirements,
858 environmentd_scratch_volume_storage_requirement: value
859 .spec
860 .environmentd_scratch_volume_storage_requirement,
861 balancerd_resource_requirements: value.spec.balancerd_resource_requirements,
862 console_resource_requirements: value.spec.console_resource_requirements,
863 balancerd_replicas: value.spec.balancerd_replicas,
864 console_replicas: value.spec.console_replicas,
865 service_account_name: value.spec.service_account_name,
866 service_account_annotations: value.spec.service_account_annotations,
867 service_account_labels: value.spec.service_account_labels,
868 pod_annotations: value.spec.pod_annotations,
869 pod_labels: value.spec.pod_labels,
870 force_promote: value.spec.force_promote.unwrap_or_default(),
871 force_rollout: value.spec.force_rollout,
872 rollout_strategy: value.spec.rollout_strategy,
873 rollout_request_timeout: value.spec.rollout_request_timeout,
874 backend_secret_name: value.spec.backend_secret_name,
875 authenticator_kind: value.spec.authenticator_kind,
876 enable_rbac: value.spec.enable_rbac,
877 environment_id: value.spec.environment_id,
878 system_parameter_configmap_name: value.spec.system_parameter_configmap_name,
879 balancerd_external_certificate_spec: value
880 .spec
881 .balancerd_external_certificate_spec,
882 console_external_certificate_spec: value.spec.console_external_certificate_spec,
883 internal_certificate_spec: value.spec.internal_certificate_spec,
884 request_rollout,
885 in_place_rollout: false,
886 },
887 status: value.status.map(|status| MaterializeStatus {
888 resource_id: status.resource_id,
889 active_generation: status.active_generation,
890 last_completed_rollout_environmentd_image_ref: status
891 .last_completed_rollout_environmentd_image_ref,
892 conditions: status.conditions,
893 last_completed_rollout_request: status
897 .last_completed_rollout_hash
898 .as_ref()
899 .map(|hash| Uuid::new_v5(&Uuid::NAMESPACE_OID, hash.as_bytes()))
900 .unwrap_or(Uuid::nil()),
901 last_completed_rollout_hash: status.last_completed_rollout_hash,
902 resources_hash: "".to_owned(),
903 }),
904 }
905 }
906 }
907
908 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
917 #[serde(rename_all = "camelCase")]
918 pub struct PartialMaterializeSpec {
919 #[serde(
920 default,
921 with = "double_option",
922 skip_serializing_if = "Option::is_none"
923 )]
924 pub environmentd_image_ref: PartialField,
925 #[serde(
926 default,
927 with = "double_option",
928 skip_serializing_if = "Option::is_none"
929 )]
930 pub environmentd_extra_args: PartialField,
931 #[serde(
932 default,
933 with = "double_option",
934 skip_serializing_if = "Option::is_none"
935 )]
936 pub environmentd_extra_env: PartialField,
937 #[serde(
938 default,
939 with = "double_option",
940 skip_serializing_if = "Option::is_none"
941 )]
942 pub environmentd_iam_role_arn: PartialField,
943 #[serde(
944 default,
945 with = "double_option",
946 skip_serializing_if = "Option::is_none"
947 )]
948 pub environmentd_connection_role_arn: PartialField,
949 #[serde(
950 default,
951 with = "double_option",
952 skip_serializing_if = "Option::is_none"
953 )]
954 pub environmentd_resource_requirements: PartialField,
955 #[serde(
956 default,
957 with = "double_option",
958 skip_serializing_if = "Option::is_none"
959 )]
960 pub environmentd_scratch_volume_storage_requirement: PartialField,
961 #[serde(
962 default,
963 with = "double_option",
964 skip_serializing_if = "Option::is_none"
965 )]
966 pub balancerd_resource_requirements: PartialField,
967 #[serde(
968 default,
969 with = "double_option",
970 skip_serializing_if = "Option::is_none"
971 )]
972 pub console_resource_requirements: PartialField,
973 #[serde(
974 default,
975 with = "double_option",
976 skip_serializing_if = "Option::is_none"
977 )]
978 pub balancerd_replicas: PartialField,
979 #[serde(
980 default,
981 with = "double_option",
982 skip_serializing_if = "Option::is_none"
983 )]
984 pub console_replicas: PartialField,
985 #[serde(
986 default,
987 with = "double_option",
988 skip_serializing_if = "Option::is_none"
989 )]
990 pub service_account_name: PartialField,
991 #[serde(
992 default,
993 with = "double_option",
994 skip_serializing_if = "Option::is_none"
995 )]
996 pub service_account_annotations: PartialField,
997 #[serde(
998 default,
999 with = "double_option",
1000 skip_serializing_if = "Option::is_none"
1001 )]
1002 pub service_account_labels: PartialField,
1003 #[serde(
1004 default,
1005 with = "double_option",
1006 skip_serializing_if = "Option::is_none"
1007 )]
1008 pub pod_annotations: PartialField,
1009 #[serde(
1010 default,
1011 with = "double_option",
1012 skip_serializing_if = "Option::is_none"
1013 )]
1014 pub pod_labels: PartialField,
1015 #[serde(
1016 default,
1017 with = "double_option",
1018 skip_serializing_if = "Option::is_none"
1019 )]
1020 pub request_rollout: PartialField,
1021 #[serde(
1022 default,
1023 with = "double_option",
1024 skip_serializing_if = "Option::is_none"
1025 )]
1026 pub force_promote: PartialField,
1027 #[serde(
1028 default,
1029 with = "double_option",
1030 skip_serializing_if = "Option::is_none"
1031 )]
1032 pub force_rollout: PartialField,
1033 #[serde(
1034 default,
1035 with = "double_option",
1036 skip_serializing_if = "Option::is_none"
1037 )]
1038 pub in_place_rollout: PartialField,
1039 #[serde(
1040 default,
1041 with = "double_option",
1042 skip_serializing_if = "Option::is_none"
1043 )]
1044 pub rollout_strategy: PartialField,
1045 #[serde(
1046 default,
1047 with = "double_option",
1048 skip_serializing_if = "Option::is_none"
1049 )]
1050 pub rollout_request_timeout: PartialField,
1051 #[serde(
1052 default,
1053 with = "double_option",
1054 skip_serializing_if = "Option::is_none"
1055 )]
1056 pub backend_secret_name: PartialField,
1057 #[serde(
1058 default,
1059 with = "double_option",
1060 skip_serializing_if = "Option::is_none"
1061 )]
1062 pub authenticator_kind: PartialField,
1063 #[serde(
1064 default,
1065 with = "double_option",
1066 skip_serializing_if = "Option::is_none"
1067 )]
1068 pub enable_rbac: PartialField,
1069 #[serde(
1070 default,
1071 with = "double_option",
1072 skip_serializing_if = "Option::is_none"
1073 )]
1074 pub environment_id: PartialField,
1075 #[serde(
1076 default,
1077 with = "double_option",
1078 skip_serializing_if = "Option::is_none"
1079 )]
1080 pub system_parameter_configmap_name: PartialField,
1081 #[serde(
1082 default,
1083 with = "double_option",
1084 skip_serializing_if = "Option::is_none"
1085 )]
1086 pub balancerd_external_certificate_spec: PartialField,
1087 #[serde(
1088 default,
1089 with = "double_option",
1090 skip_serializing_if = "Option::is_none"
1091 )]
1092 pub console_external_certificate_spec: PartialField,
1093 #[serde(
1094 default,
1095 with = "double_option",
1096 skip_serializing_if = "Option::is_none"
1097 )]
1098 pub internal_certificate_spec: PartialField,
1099 #[serde(flatten)]
1100 pub extra: serde_json::Map<String, serde_json::Value>,
1101 }
1102
1103 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
1105 #[serde(rename_all = "camelCase")]
1106 pub struct PartialMaterializeStatus {
1107 #[serde(
1108 default,
1109 with = "double_option",
1110 skip_serializing_if = "Option::is_none"
1111 )]
1112 pub resource_id: PartialField,
1113 #[serde(
1114 default,
1115 with = "double_option",
1116 skip_serializing_if = "Option::is_none"
1117 )]
1118 pub active_generation: PartialField,
1119 #[serde(
1120 default,
1121 with = "double_option",
1122 skip_serializing_if = "Option::is_none"
1123 )]
1124 pub last_completed_rollout_request: PartialField,
1125 #[serde(
1126 default,
1127 with = "double_option",
1128 skip_serializing_if = "Option::is_none"
1129 )]
1130 pub last_completed_rollout_environmentd_image_ref: PartialField,
1131 #[serde(
1132 default,
1133 with = "double_option",
1134 skip_serializing_if = "Option::is_none"
1135 )]
1136 pub resources_hash: PartialField,
1137 #[serde(
1138 default,
1139 with = "double_option",
1140 skip_serializing_if = "Option::is_none"
1141 )]
1142 pub last_completed_rollout_hash: PartialField,
1143 #[serde(
1144 default,
1145 with = "double_option",
1146 skip_serializing_if = "Option::is_none"
1147 )]
1148 pub conditions: PartialField,
1149 #[serde(flatten)]
1150 pub extra: serde_json::Map<String, serde_json::Value>,
1151 }
1152
1153 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
1155 #[serde(rename_all = "camelCase")]
1156 pub struct PartialMaterialize {
1157 #[serde(default, skip_serializing_if = "Option::is_none")]
1158 pub api_version: Option<String>,
1159 #[serde(default, skip_serializing_if = "Option::is_none")]
1160 pub kind: Option<serde_json::Value>,
1161 #[serde(default, skip_serializing_if = "Option::is_none")]
1162 pub metadata: Option<serde_json::Value>,
1163 #[serde(default, skip_serializing_if = "Option::is_none")]
1164 pub spec: Option<PartialMaterializeSpec>,
1165 #[serde(default, skip_serializing_if = "Option::is_none")]
1166 pub status: Option<PartialMaterializeStatus>,
1167 #[serde(flatten)]
1168 pub extra: serde_json::Map<String, serde_json::Value>,
1169 }
1170
1171 impl From<MaterializeSpec> for PartialMaterializeSpec {
1172 fn from(spec: MaterializeSpec) -> Self {
1173 let MaterializeSpec {
1174 environmentd_image_ref,
1175 environmentd_extra_args,
1176 environmentd_extra_env,
1177 environmentd_iam_role_arn,
1178 environmentd_connection_role_arn,
1179 environmentd_resource_requirements,
1180 environmentd_scratch_volume_storage_requirement,
1181 balancerd_resource_requirements,
1182 console_resource_requirements,
1183 balancerd_replicas,
1184 console_replicas,
1185 service_account_name,
1186 service_account_annotations,
1187 service_account_labels,
1188 pod_annotations,
1189 pod_labels,
1190 request_rollout,
1191 force_promote,
1192 force_rollout,
1193 in_place_rollout,
1194 rollout_strategy,
1195 rollout_request_timeout,
1196 backend_secret_name,
1197 authenticator_kind,
1198 enable_rbac,
1199 environment_id,
1200 system_parameter_configmap_name,
1201 balancerd_external_certificate_spec,
1202 console_external_certificate_spec,
1203 internal_certificate_spec,
1204 } = spec;
1205 Self {
1206 environmentd_image_ref: present(environmentd_image_ref),
1207 environmentd_extra_args: present_opt(environmentd_extra_args),
1208 environmentd_extra_env: present_opt(environmentd_extra_env),
1209 environmentd_iam_role_arn: present_opt(environmentd_iam_role_arn),
1210 environmentd_connection_role_arn: present_opt(environmentd_connection_role_arn),
1211 environmentd_resource_requirements: present_opt(environmentd_resource_requirements),
1212 environmentd_scratch_volume_storage_requirement: present_opt(
1213 environmentd_scratch_volume_storage_requirement,
1214 ),
1215 balancerd_resource_requirements: present_opt(balancerd_resource_requirements),
1216 console_resource_requirements: present_opt(console_resource_requirements),
1217 balancerd_replicas: present_opt(balancerd_replicas),
1218 console_replicas: present_opt(console_replicas),
1219 service_account_name: present_opt(service_account_name),
1220 service_account_annotations: present_opt(service_account_annotations),
1221 service_account_labels: present_opt(service_account_labels),
1222 pod_annotations: present_opt(pod_annotations),
1223 pod_labels: present_opt(pod_labels),
1224 request_rollout: present(request_rollout),
1225 force_promote: present(force_promote),
1226 force_rollout: present(force_rollout),
1227 in_place_rollout: present(in_place_rollout),
1228 rollout_strategy: present(rollout_strategy),
1229 rollout_request_timeout: present(rollout_request_timeout),
1230 backend_secret_name: present(backend_secret_name),
1231 authenticator_kind: present(authenticator_kind),
1232 enable_rbac: present(enable_rbac),
1233 environment_id: present(environment_id),
1234 system_parameter_configmap_name: present_opt(system_parameter_configmap_name),
1235 balancerd_external_certificate_spec: present_opt(
1236 balancerd_external_certificate_spec,
1237 ),
1238 console_external_certificate_spec: present_opt(console_external_certificate_spec),
1239 internal_certificate_spec: present_opt(internal_certificate_spec),
1240 extra: serde_json::Map::new(),
1241 }
1242 }
1243 }
1244
1245 impl From<MaterializeStatus> for PartialMaterializeStatus {
1246 fn from(status: MaterializeStatus) -> Self {
1247 let MaterializeStatus {
1248 resource_id,
1249 active_generation,
1250 last_completed_rollout_request,
1251 last_completed_rollout_environmentd_image_ref,
1252 resources_hash,
1253 last_completed_rollout_hash,
1254 conditions,
1255 } = status;
1256 Self {
1257 resource_id: present(resource_id),
1258 active_generation: present(active_generation),
1259 last_completed_rollout_request: present(last_completed_rollout_request),
1260 last_completed_rollout_environmentd_image_ref: present_opt(
1261 last_completed_rollout_environmentd_image_ref,
1262 ),
1263 resources_hash: present(resources_hash),
1264 last_completed_rollout_hash: present_opt(last_completed_rollout_hash),
1265 conditions: present(conditions),
1266 extra: serde_json::Map::new(),
1267 }
1268 }
1269 }
1270
1271 impl From<Materialize> for PartialMaterialize {
1272 fn from(mz: Materialize) -> Self {
1273 let Materialize {
1274 metadata,
1275 spec,
1276 status,
1277 } = mz;
1278 Self {
1279 api_version: Some("materialize.cloud/v1alpha1".to_owned()),
1280 kind: Some("Materialize".into()),
1281 metadata: Some(
1282 serde_json::to_value(metadata).expect("ObjectMeta serializes to JSON"),
1283 ),
1284 spec: Some(spec.into()),
1285 status: status.map(Into::into),
1286 extra: serde_json::Map::new(),
1287 }
1288 }
1289 }
1290
1291 impl From<super::v1::PartialMaterializeSpec> for PartialMaterializeSpec {
1292 fn from(spec: super::v1::PartialMaterializeSpec) -> Self {
1293 let super::v1::PartialMaterializeSpec {
1294 environmentd_image_ref,
1295 environmentd_extra_args,
1296 environmentd_extra_env,
1297 environmentd_connection_role_arn,
1298 environmentd_resource_requirements,
1299 environmentd_scratch_volume_storage_requirement,
1300 balancerd_resource_requirements,
1301 console_resource_requirements,
1302 balancerd_replicas,
1303 console_replicas,
1304 service_account_name,
1305 service_account_annotations,
1306 service_account_labels,
1307 pod_annotations,
1308 pod_labels,
1309 force_promote,
1310 force_rollout,
1311 rollout_strategy,
1312 rollout_request_timeout,
1313 backend_secret_name,
1314 authenticator_kind,
1315 enable_rbac,
1316 environment_id,
1317 system_parameter_configmap_name,
1318 balancerd_external_certificate_spec,
1319 console_external_certificate_spec,
1320 internal_certificate_spec,
1321 extra,
1322 } = spec;
1323 Self {
1324 environmentd_image_ref,
1325 environmentd_extra_args,
1326 environmentd_extra_env,
1327 environmentd_iam_role_arn: None,
1328 environmentd_connection_role_arn,
1329 environmentd_resource_requirements,
1330 environmentd_scratch_volume_storage_requirement,
1331 balancerd_resource_requirements,
1332 console_resource_requirements,
1333 balancerd_replicas,
1334 console_replicas,
1335 service_account_name,
1336 service_account_annotations,
1337 service_account_labels,
1338 pod_annotations,
1339 pod_labels,
1340 request_rollout: None,
1345 force_promote,
1346 force_rollout,
1347 in_place_rollout: None,
1348 rollout_strategy,
1349 rollout_request_timeout,
1350 backend_secret_name,
1351 authenticator_kind,
1352 enable_rbac,
1353 environment_id,
1354 system_parameter_configmap_name,
1355 balancerd_external_certificate_spec,
1356 console_external_certificate_spec,
1357 internal_certificate_spec,
1358 extra,
1359 }
1360 }
1361 }
1362
1363 impl From<super::v1::PartialMaterializeStatus> for PartialMaterializeStatus {
1364 fn from(status: super::v1::PartialMaterializeStatus) -> Self {
1365 let super::v1::PartialMaterializeStatus {
1366 resource_id,
1367 active_generation,
1368 last_completed_rollout_environmentd_image_ref,
1369 last_completed_rollout_hash,
1370 requested_rollout_hash: _,
1371 conditions,
1372 extra,
1373 } = status;
1374 Self {
1375 resource_id,
1376 active_generation,
1377 last_completed_rollout_request: None,
1382 resources_hash: None,
1383 last_completed_rollout_environmentd_image_ref,
1384 last_completed_rollout_hash,
1385 conditions,
1386 extra,
1387 }
1388 }
1389 }
1390
1391 impl From<super::v1::PartialMaterialize> for PartialMaterialize {
1392 fn from(mz: super::v1::PartialMaterialize) -> Self {
1393 let super::v1::PartialMaterialize {
1394 api_version: _,
1395 kind,
1396 metadata,
1397 spec,
1398 status,
1399 extra,
1400 } = mz;
1401 Self {
1402 api_version: Some("materialize.cloud/v1alpha1".to_owned()),
1403 kind,
1404 metadata,
1405 spec: spec.map(Into::into),
1406 status: status.map(Into::into),
1407 extra,
1408 }
1409 }
1410 }
1411}
1412
1413pub mod v1 {
1414 use super::*;
1415
1416 #[derive(
1417 CustomResource,
1418 Clone,
1419 Debug,
1420 Default,
1421 PartialEq,
1422 Deserialize,
1423 Serialize,
1424 JsonSchema
1425 )]
1426 #[serde(rename_all = "camelCase")]
1427 #[kube(
1428 namespaced,
1429 group = "materialize.cloud",
1430 version = "v1",
1431 kind = "Materialize",
1432 singular = "materialize",
1433 plural = "materializes",
1434 shortname = "mzs",
1435 status = "MaterializeStatus",
1436 printcolumn = r#"{"name": "ImageRefRunning", "type": "string", "description": "Reference to the Docker image that is currently in use.", "jsonPath": ".status.lastCompletedRolloutEnvironmentdImageRef", "priority": 1}"#,
1437 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}"#,
1438 printcolumn = r#"{"name": "UpToDate", "type": "string", "description": "Whether the spec has been applied", "jsonPath": ".status.conditions[?(@.type==\"UpToDate\")].status", "priority": 1}"#
1439 )]
1440 pub struct MaterializeSpec {
1441 pub environmentd_image_ref: String,
1443 pub environmentd_extra_args: Option<Vec<String>>,
1445 pub environmentd_extra_env: Option<Vec<EnvVar>>,
1447 pub environmentd_connection_role_arn: Option<String>,
1450 pub environmentd_resource_requirements: Option<ResourceRequirements>,
1452 pub environmentd_scratch_volume_storage_requirement: Option<Quantity>,
1454 pub balancerd_resource_requirements: Option<ResourceRequirements>,
1458 pub console_resource_requirements: Option<ResourceRequirements>,
1462 pub balancerd_replicas: Option<i32>,
1466 pub console_replicas: Option<i32>,
1470
1471 pub service_account_name: Option<String>,
1474 pub service_account_annotations: Option<BTreeMap<String, String>>,
1481 pub service_account_labels: Option<BTreeMap<String, String>>,
1483 pub pod_annotations: Option<BTreeMap<String, String>>,
1485 pub pod_labels: Option<BTreeMap<String, String>>,
1487
1488 pub force_promote: Option<String>,
1495 #[serde(default)]
1499 pub force_rollout: Uuid,
1500 #[serde(default)]
1502 pub rollout_strategy: MaterializeRolloutStrategy,
1503 #[serde(default)]
1524 pub rollout_request_timeout: RolloutRequestTimeout,
1525 pub backend_secret_name: String,
1529 #[serde(default)]
1531 pub authenticator_kind: AuthenticatorKind,
1532 #[serde(default)]
1534 pub enable_rbac: bool,
1535
1536 #[serde(default)]
1543 pub environment_id: Uuid,
1544
1545 pub system_parameter_configmap_name: Option<String>,
1560
1561 pub balancerd_external_certificate_spec: Option<MaterializeCertSpec>,
1567 pub console_external_certificate_spec: Option<MaterializeCertSpec>,
1574 pub internal_certificate_spec: Option<MaterializeCertSpec>,
1579 }
1580
1581 impl Materialize {
1582 pub fn generate_rollout_hash(&self) -> String {
1583 let mut hasher = Sha256::new();
1584 let spec = MaterializeSpec {
1587 environmentd_image_ref: self.spec.environmentd_image_ref.clone(),
1588 environmentd_extra_args: self.spec.environmentd_extra_args.clone(),
1589 environmentd_extra_env: self.spec.environmentd_extra_env.clone(),
1590 environmentd_connection_role_arn: self
1591 .spec
1592 .environmentd_connection_role_arn
1593 .clone(),
1594 environmentd_resource_requirements: self
1595 .spec
1596 .environmentd_resource_requirements
1597 .clone(),
1598 environmentd_scratch_volume_storage_requirement: self
1599 .spec
1600 .environmentd_scratch_volume_storage_requirement
1601 .clone(),
1602 balancerd_resource_requirements: None,
1603 console_resource_requirements: None,
1604 balancerd_replicas: None,
1605 console_replicas: None,
1606 service_account_name: self.spec.service_account_name.clone(),
1607 service_account_annotations: self.spec.service_account_annotations.clone(),
1608 service_account_labels: self.spec.service_account_labels.clone(),
1609 pod_annotations: self.spec.pod_annotations.clone(),
1610 pod_labels: self.spec.pod_labels.clone(),
1611 force_promote: None,
1612 force_rollout: self.spec.force_rollout,
1613 rollout_strategy: self.spec.rollout_strategy.clone(),
1614 rollout_request_timeout: self.spec.rollout_request_timeout.clone(),
1615 backend_secret_name: self.spec.backend_secret_name.clone(),
1616 authenticator_kind: self.spec.authenticator_kind,
1617 enable_rbac: self.spec.enable_rbac,
1618 environment_id: self.spec.environment_id,
1619 system_parameter_configmap_name: self.spec.system_parameter_configmap_name.clone(),
1620 balancerd_external_certificate_spec: None,
1621 console_external_certificate_spec: None,
1622 internal_certificate_spec: self.spec.internal_certificate_spec.clone(),
1623 };
1624 hasher.update(&serde_json::to_vec(&spec).unwrap());
1625 if let Some(annotation) = self
1626 .metadata
1627 .annotations
1628 .as_ref()
1629 .and_then(|annotations| annotations.get(FORCE_ROLLOUT_ANNOTATION))
1630 {
1631 hasher.update(annotation);
1632 }
1633 format!("{:x}", hasher.finalize())
1634 }
1635
1636 pub fn backend_secret_name(&self) -> String {
1637 self.spec.backend_secret_name.clone()
1638 }
1639
1640 pub fn namespace(&self) -> String {
1641 self.meta().namespace.clone().unwrap()
1642 }
1643
1644 pub fn create_service_account(&self) -> bool {
1645 self.spec.service_account_name.is_none()
1646 }
1647
1648 pub fn service_account_name(&self) -> String {
1649 self.spec
1650 .service_account_name
1651 .clone()
1652 .unwrap_or_else(|| self.name_unchecked())
1653 }
1654
1655 pub fn role_name(&self) -> String {
1656 self.name_unchecked()
1657 }
1658
1659 pub fn role_binding_name(&self) -> String {
1660 self.name_unchecked()
1661 }
1662
1663 pub fn environmentd_statefulset_name(&self, generation: u64) -> String {
1664 self.name_prefixed(&format!("environmentd-{generation}"))
1665 }
1666
1667 pub fn environmentd_app_name(&self) -> String {
1668 "environmentd".to_owned()
1669 }
1670
1671 pub fn environmentd_service_name(&self) -> String {
1672 self.name_prefixed("environmentd")
1673 }
1674
1675 pub fn environmentd_service_internal_fqdn(&self) -> String {
1676 format!(
1677 "{}.{}.svc.cluster.local",
1678 self.environmentd_service_name(),
1679 self.meta().namespace.as_ref().unwrap()
1680 )
1681 }
1682
1683 pub fn environmentd_generation_service_name(&self, generation: u64) -> String {
1684 self.name_prefixed(&format!("environmentd-{generation}"))
1685 }
1686
1687 pub fn balancerd_app_name(&self) -> String {
1688 "balancerd".to_owned()
1689 }
1690
1691 pub fn environmentd_certificate_name(&self) -> String {
1692 self.name_prefixed("environmentd-external")
1693 }
1694
1695 pub fn environmentd_certificate_secret_name(&self) -> String {
1696 self.name_prefixed("environmentd-tls")
1697 }
1698
1699 pub fn balancerd_deployment_name(&self) -> String {
1700 self.name_prefixed("balancerd")
1701 }
1702
1703 pub fn balancerd_service_name(&self) -> String {
1704 self.name_prefixed("balancerd")
1705 }
1706
1707 pub fn console_app_name(&self) -> String {
1708 "console".to_owned()
1709 }
1710
1711 pub fn balancerd_external_certificate_name(&self) -> String {
1712 self.name_prefixed("balancerd-external")
1713 }
1714
1715 pub fn balancerd_external_certificate_secret_name(&self) -> String {
1716 self.name_prefixed("balancerd-external-tls")
1717 }
1718
1719 pub fn balancerd_replicas(&self) -> i32 {
1720 self.spec.balancerd_replicas.unwrap_or(2)
1721 }
1722
1723 pub fn console_replicas(&self) -> i32 {
1724 self.spec.console_replicas.unwrap_or(2)
1725 }
1726
1727 pub fn console_configmap_name(&self) -> String {
1728 self.name_prefixed("console")
1729 }
1730
1731 pub fn console_deployment_name(&self) -> String {
1732 self.name_prefixed("console")
1733 }
1734
1735 pub fn console_service_name(&self) -> String {
1736 self.name_prefixed("console")
1737 }
1738
1739 pub fn console_external_certificate_name(&self) -> String {
1740 self.name_prefixed("console-external")
1741 }
1742
1743 pub fn console_external_certificate_secret_name(&self) -> String {
1744 self.name_prefixed("console-external-tls")
1745 }
1746
1747 pub fn persist_pubsub_service_name(&self, generation: u64) -> String {
1748 self.name_prefixed(&format!("persist-pubsub-{generation}"))
1749 }
1750
1751 pub fn listeners_configmap_name(&self, generation: u64) -> String {
1752 self.name_prefixed(&format!("listeners-{generation}"))
1753 }
1754
1755 pub fn name_prefixed(&self, suffix: &str) -> String {
1756 format!("mz{}-{}", self.resource_id(), suffix)
1757 }
1758
1759 pub fn resource_id(&self) -> &str {
1760 &self.status.as_ref().unwrap().resource_id
1761 }
1762
1763 pub fn system_parameter_configmap_name(&self) -> Option<String> {
1764 self.spec.system_parameter_configmap_name.clone()
1765 }
1766
1767 pub fn environmentd_scratch_volume_storage_requirement(&self) -> Quantity {
1768 self.spec
1769 .environmentd_scratch_volume_storage_requirement
1770 .clone()
1771 .unwrap_or_else(|| {
1772 self.spec
1773 .environmentd_resource_requirements
1774 .as_ref()
1775 .and_then(|requirements| {
1776 requirements
1777 .requests
1778 .as_ref()
1779 .or(requirements.limits.as_ref())
1780 })
1781 .and_then(|requirements| requirements.get("memory").cloned())
1786 .unwrap_or_else(|| Quantity("4096Mi".to_string()))
1788 })
1789 }
1790
1791 pub fn environment_id(&self, cloud_provider: &str, region: &str) -> String {
1792 format!(
1793 "{}-{}-{}-0",
1794 cloud_provider, region, self.spec.environment_id,
1795 )
1796 }
1797
1798 pub fn rollout_requested(&self) -> bool {
1799 self.status
1800 .as_ref()
1801 .map(|status| status.last_completed_rollout_hash != status.requested_rollout_hash)
1802 .unwrap_or(false)
1803 }
1804
1805 pub fn set_force_promote(&mut self) {
1806 self.spec.force_promote = Some(self.generate_rollout_hash());
1807 }
1808
1809 pub fn should_force_promote(&self) -> bool {
1810 self.spec.force_promote.as_ref()
1811 == self
1812 .status
1813 .as_ref()
1814 .and_then(|status| status.requested_rollout_hash.as_ref())
1815 || self.spec.rollout_strategy
1816 == MaterializeRolloutStrategy::ImmediatelyPromoteCausingDowntime
1817 }
1818
1819 pub fn conditions_need_update(&self) -> bool {
1820 let Some(status) = self.status.as_ref() else {
1821 return true;
1822 };
1823 if status.conditions.is_empty() {
1824 return true;
1825 }
1826 for condition in &status.conditions {
1827 if condition.observed_generation != self.meta().generation {
1828 return true;
1829 }
1830 }
1831 false
1832 }
1833
1834 pub fn is_ready_to_promote(&self, rollout_hash: &str) -> bool {
1835 let Some(status) = self.status.as_ref() else {
1836 return false;
1837 };
1838 if status.conditions.is_empty() {
1839 return false;
1840 }
1841 status
1842 .conditions
1843 .iter()
1844 .any(|condition| condition.reason == "ReadyToPromote")
1845 && status.requested_rollout_hash.as_deref() == Some(rollout_hash)
1846 }
1847
1848 pub fn is_promoting(&self) -> bool {
1849 let Some(status) = self.status.as_ref() else {
1850 return false;
1851 };
1852 if status.conditions.is_empty() {
1853 return false;
1854 }
1855 status
1856 .conditions
1857 .iter()
1858 .any(|condition| condition.reason == "Promoting")
1859 }
1860
1861 pub fn update_in_progress(&self) -> bool {
1862 let Some(status) = self.status.as_ref() else {
1863 return false;
1864 };
1865 if status.conditions.is_empty() {
1866 return false;
1867 }
1868 for condition in &status.conditions {
1869 if condition.type_ == "UpToDate" && condition.status == "Unknown" {
1870 return true;
1871 }
1872 }
1873 false
1874 }
1875
1876 pub fn meets_minimum_version(&self, minimum: &Version) -> bool {
1880 let version = parse_image_ref(&self.spec.environmentd_image_ref);
1881 match version {
1882 Some(version) => version.cmp_precedence(minimum).is_ge(),
1884 None => {
1890 tracing::warn!(
1891 image_ref = %self.spec.environmentd_image_ref,
1892 "failed to parse image ref",
1893 );
1894 true
1895 }
1896 }
1897 }
1898
1899 pub fn is_valid_upgrade_version(active_version: &Version, next_version: &Version) -> bool {
1903 if next_version.cmp_precedence(active_version) == std::cmp::Ordering::Less {
1908 return false;
1909 }
1910
1911 if active_version.major == 0 {
1912 if next_version.major != active_version.major {
1913 if next_version.major == 26 {
1914 return (active_version.minor == 147 && active_version.patch >= 20)
1918 || active_version.minor >= 164;
1919 } else {
1920 return false;
1921 }
1922 }
1923 if next_version.minor == 147 && active_version.minor == 130 {
1925 return true;
1926 }
1927 return next_version.minor <= active_version.minor + 1;
1929 } else if active_version.major >= 26 {
1930 return next_version.major <= active_version.major + 1;
1932 }
1933
1934 true
1935 }
1936
1937 pub fn within_upgrade_window(&self) -> bool {
1940 let active_environmentd_version = self
1941 .status
1942 .as_ref()
1943 .and_then(|status| {
1944 status
1945 .last_completed_rollout_environmentd_image_ref
1946 .as_ref()
1947 })
1948 .and_then(|image_ref| parse_image_ref(image_ref));
1949
1950 if let (Some(next_environmentd_version), Some(active_environmentd_version)) = (
1951 parse_image_ref(&self.spec.environmentd_image_ref),
1952 active_environmentd_version,
1953 ) {
1954 Self::is_valid_upgrade_version(
1955 &active_environmentd_version,
1956 &next_environmentd_version,
1957 )
1958 } else {
1959 true
1962 }
1963 }
1964
1965 pub fn status(&self) -> MaterializeStatus {
1966 self.status.clone().unwrap_or_else(|| {
1967 let mut status = MaterializeStatus::default();
1968
1969 status.resource_id = new_resource_id();
1970
1971 if let Some(last_active_generation) = self
1976 .annotations()
1977 .get(LAST_KNOWN_ACTIVE_GENERATION_ANNOTATION)
1978 {
1979 status.active_generation = last_active_generation
1980 .parse()
1981 .expect("valid int generation");
1982 }
1983
1984 status.last_completed_rollout_environmentd_image_ref =
1987 Some(self.spec.environmentd_image_ref.clone());
1988
1989 status
1990 })
1991 }
1992 }
1993
1994 #[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq)]
1995 #[serde(rename_all = "camelCase")]
1996 pub struct MaterializeStatus {
1997 pub resource_id: String,
1999 pub active_generation: u64,
2001 pub last_completed_rollout_environmentd_image_ref: Option<String>,
2005 pub last_completed_rollout_hash: Option<String>,
2007 pub requested_rollout_hash: Option<String>,
2010 pub conditions: Vec<Condition>,
2011 }
2012
2013 impl MaterializeStatus {
2014 pub fn needs_update(&self, other: &Self) -> bool {
2015 let now = Timestamp::now();
2016 let mut a = self.clone();
2017 for condition in &mut a.conditions {
2018 condition.last_transition_time = Time(now);
2019 }
2020 let mut b = other.clone();
2021 for condition in &mut b.conditions {
2022 condition.last_transition_time = Time(now);
2023 }
2024 a != b
2025 }
2026 }
2027
2028 impl ManagedResource for Materialize {
2029 fn default_labels(&self) -> BTreeMap<String, String> {
2030 BTreeMap::from_iter([
2031 (
2032 "materialize.cloud/organization-name".to_owned(),
2033 self.name_unchecked(),
2034 ),
2035 (
2036 "materialize.cloud/organization-namespace".to_owned(),
2037 self.namespace(),
2038 ),
2039 (
2040 "materialize.cloud/mz-resource-id".to_owned(),
2041 self.resource_id().to_owned(),
2042 ),
2043 ])
2044 }
2045
2046 fn app_name(&self) -> Option<&str> {
2047 Some("environmentd")
2048 }
2049 }
2050
2051 impl From<v1alpha1::Materialize> for Materialize {
2052 fn from(value: v1alpha1::Materialize) -> Self {
2053 let is_promoting = value.is_promoting();
2054 let service_account_annotations = if let Some(environmentd_iam_role_arn) =
2055 value.spec.environmentd_iam_role_arn
2056 {
2057 let mut annotations = value.spec.service_account_annotations.unwrap_or_default();
2058 annotations
2059 .entry("eks.amazonaws.com/role-arn".to_owned())
2060 .or_insert(environmentd_iam_role_arn);
2061 Some(annotations)
2062 } else {
2063 value.spec.service_account_annotations
2064 };
2065 let mut mz = Materialize {
2066 metadata: value.metadata,
2067 spec: MaterializeSpec {
2068 environmentd_image_ref: value.spec.environmentd_image_ref,
2069 environmentd_extra_args: value.spec.environmentd_extra_args,
2070 environmentd_extra_env: value.spec.environmentd_extra_env,
2071 environmentd_connection_role_arn: value.spec.environmentd_connection_role_arn,
2072 environmentd_resource_requirements: value
2073 .spec
2074 .environmentd_resource_requirements,
2075 environmentd_scratch_volume_storage_requirement: value
2076 .spec
2077 .environmentd_scratch_volume_storage_requirement,
2078 balancerd_resource_requirements: value.spec.balancerd_resource_requirements,
2079 console_resource_requirements: value.spec.console_resource_requirements,
2080 balancerd_replicas: value.spec.balancerd_replicas,
2081 console_replicas: value.spec.console_replicas,
2082 service_account_name: value.spec.service_account_name,
2083 service_account_annotations,
2084 service_account_labels: value.spec.service_account_labels,
2085 pod_annotations: value.spec.pod_annotations,
2086 pod_labels: value.spec.pod_labels,
2087 force_promote: if value.spec.force_promote.is_empty()
2088 || &value.spec.force_promote == "00000000-0000-0000-0000-000000000000"
2089 {
2090 None
2091 } else {
2092 Some(value.spec.force_promote.to_string())
2093 },
2094 force_rollout: value.spec.force_rollout,
2095 rollout_strategy: value.spec.rollout_strategy,
2096 rollout_request_timeout: value.spec.rollout_request_timeout,
2097 backend_secret_name: value.spec.backend_secret_name,
2098 authenticator_kind: value.spec.authenticator_kind,
2099 enable_rbac: value.spec.enable_rbac,
2100 environment_id: value.spec.environment_id,
2101 system_parameter_configmap_name: value.spec.system_parameter_configmap_name,
2102 balancerd_external_certificate_spec: value
2103 .spec
2104 .balancerd_external_certificate_spec,
2105 console_external_certificate_spec: value.spec.console_external_certificate_spec,
2106 internal_certificate_spec: value.spec.internal_certificate_spec,
2107 },
2108 status: None,
2109 };
2110 let calculated_rollout_hash = mz.generate_rollout_hash();
2111 let last_completed_rollout_hash = match value
2112 .status
2113 .as_ref()
2114 .and_then(|status| status.last_completed_rollout_hash.to_owned())
2115 {
2116 Some(last_completed_rollout_hash) => Some(last_completed_rollout_hash),
2117 None => {
2118 let currently_rolling_out = value
2119 .status
2120 .as_ref()
2121 .map(|status| {
2122 status.last_completed_rollout_request != value.spec.request_rollout
2123 || status.last_completed_rollout_request.is_nil()
2126 })
2127 .unwrap_or(true);
2128 if currently_rolling_out {
2129 None
2131 } else {
2132 Some(calculated_rollout_hash.clone())
2133 }
2134 }
2135 };
2136 let requested_rollout_hash = if is_promoting {
2137 None
2138 } else {
2139 Some(calculated_rollout_hash)
2140 };
2141 mz.status = value.status.map(|status| MaterializeStatus {
2142 resource_id: status.resource_id,
2143 active_generation: status.active_generation,
2144 last_completed_rollout_environmentd_image_ref: status
2145 .last_completed_rollout_environmentd_image_ref,
2146 last_completed_rollout_hash,
2147 requested_rollout_hash,
2148 conditions: status.conditions,
2149 });
2150 mz
2151 }
2152 }
2153
2154 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
2163 #[serde(rename_all = "camelCase")]
2164 pub struct PartialMaterializeSpec {
2165 #[serde(
2166 default,
2167 with = "double_option",
2168 skip_serializing_if = "Option::is_none"
2169 )]
2170 pub environmentd_image_ref: PartialField,
2171 #[serde(
2172 default,
2173 with = "double_option",
2174 skip_serializing_if = "Option::is_none"
2175 )]
2176 pub environmentd_extra_args: PartialField,
2177 #[serde(
2178 default,
2179 with = "double_option",
2180 skip_serializing_if = "Option::is_none"
2181 )]
2182 pub environmentd_extra_env: PartialField,
2183 #[serde(
2184 default,
2185 with = "double_option",
2186 skip_serializing_if = "Option::is_none"
2187 )]
2188 pub environmentd_connection_role_arn: PartialField,
2189 #[serde(
2190 default,
2191 with = "double_option",
2192 skip_serializing_if = "Option::is_none"
2193 )]
2194 pub environmentd_resource_requirements: PartialField,
2195 #[serde(
2196 default,
2197 with = "double_option",
2198 skip_serializing_if = "Option::is_none"
2199 )]
2200 pub environmentd_scratch_volume_storage_requirement: PartialField,
2201 #[serde(
2202 default,
2203 with = "double_option",
2204 skip_serializing_if = "Option::is_none"
2205 )]
2206 pub balancerd_resource_requirements: PartialField,
2207 #[serde(
2208 default,
2209 with = "double_option",
2210 skip_serializing_if = "Option::is_none"
2211 )]
2212 pub console_resource_requirements: PartialField,
2213 #[serde(
2214 default,
2215 with = "double_option",
2216 skip_serializing_if = "Option::is_none"
2217 )]
2218 pub balancerd_replicas: PartialField,
2219 #[serde(
2220 default,
2221 with = "double_option",
2222 skip_serializing_if = "Option::is_none"
2223 )]
2224 pub console_replicas: PartialField,
2225 #[serde(
2226 default,
2227 with = "double_option",
2228 skip_serializing_if = "Option::is_none"
2229 )]
2230 pub service_account_name: PartialField,
2231 #[serde(
2232 default,
2233 with = "double_option",
2234 skip_serializing_if = "Option::is_none"
2235 )]
2236 pub service_account_annotations: PartialField,
2237 #[serde(
2238 default,
2239 with = "double_option",
2240 skip_serializing_if = "Option::is_none"
2241 )]
2242 pub service_account_labels: PartialField,
2243 #[serde(
2244 default,
2245 with = "double_option",
2246 skip_serializing_if = "Option::is_none"
2247 )]
2248 pub pod_annotations: PartialField,
2249 #[serde(
2250 default,
2251 with = "double_option",
2252 skip_serializing_if = "Option::is_none"
2253 )]
2254 pub pod_labels: PartialField,
2255 #[serde(
2256 default,
2257 with = "double_option",
2258 skip_serializing_if = "Option::is_none"
2259 )]
2260 pub force_promote: PartialField,
2261 #[serde(
2262 default,
2263 with = "double_option",
2264 skip_serializing_if = "Option::is_none"
2265 )]
2266 pub force_rollout: PartialField,
2267 #[serde(
2268 default,
2269 with = "double_option",
2270 skip_serializing_if = "Option::is_none"
2271 )]
2272 pub rollout_strategy: PartialField,
2273 #[serde(
2274 default,
2275 with = "double_option",
2276 skip_serializing_if = "Option::is_none"
2277 )]
2278 pub rollout_request_timeout: PartialField,
2279 #[serde(
2280 default,
2281 with = "double_option",
2282 skip_serializing_if = "Option::is_none"
2283 )]
2284 pub backend_secret_name: PartialField,
2285 #[serde(
2286 default,
2287 with = "double_option",
2288 skip_serializing_if = "Option::is_none"
2289 )]
2290 pub authenticator_kind: PartialField,
2291 #[serde(
2292 default,
2293 with = "double_option",
2294 skip_serializing_if = "Option::is_none"
2295 )]
2296 pub enable_rbac: PartialField,
2297 #[serde(
2298 default,
2299 with = "double_option",
2300 skip_serializing_if = "Option::is_none"
2301 )]
2302 pub environment_id: PartialField,
2303 #[serde(
2304 default,
2305 with = "double_option",
2306 skip_serializing_if = "Option::is_none"
2307 )]
2308 pub system_parameter_configmap_name: PartialField,
2309 #[serde(
2310 default,
2311 with = "double_option",
2312 skip_serializing_if = "Option::is_none"
2313 )]
2314 pub balancerd_external_certificate_spec: PartialField,
2315 #[serde(
2316 default,
2317 with = "double_option",
2318 skip_serializing_if = "Option::is_none"
2319 )]
2320 pub console_external_certificate_spec: PartialField,
2321 #[serde(
2322 default,
2323 with = "double_option",
2324 skip_serializing_if = "Option::is_none"
2325 )]
2326 pub internal_certificate_spec: PartialField,
2327 #[serde(flatten)]
2328 pub extra: serde_json::Map<String, serde_json::Value>,
2329 }
2330
2331 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
2333 #[serde(rename_all = "camelCase")]
2334 pub struct PartialMaterializeStatus {
2335 #[serde(
2336 default,
2337 with = "double_option",
2338 skip_serializing_if = "Option::is_none"
2339 )]
2340 pub resource_id: PartialField,
2341 #[serde(
2342 default,
2343 with = "double_option",
2344 skip_serializing_if = "Option::is_none"
2345 )]
2346 pub active_generation: PartialField,
2347 #[serde(
2348 default,
2349 with = "double_option",
2350 skip_serializing_if = "Option::is_none"
2351 )]
2352 pub last_completed_rollout_environmentd_image_ref: PartialField,
2353 #[serde(
2354 default,
2355 with = "double_option",
2356 skip_serializing_if = "Option::is_none"
2357 )]
2358 pub last_completed_rollout_hash: PartialField,
2359 #[serde(
2360 default,
2361 with = "double_option",
2362 skip_serializing_if = "Option::is_none"
2363 )]
2364 pub requested_rollout_hash: PartialField,
2365 #[serde(
2366 default,
2367 with = "double_option",
2368 skip_serializing_if = "Option::is_none"
2369 )]
2370 pub conditions: PartialField,
2371 #[serde(flatten)]
2372 pub extra: serde_json::Map<String, serde_json::Value>,
2373 }
2374
2375 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
2377 #[serde(rename_all = "camelCase")]
2378 pub struct PartialMaterialize {
2379 #[serde(default, skip_serializing_if = "Option::is_none")]
2380 pub api_version: Option<String>,
2381 #[serde(default, skip_serializing_if = "Option::is_none")]
2382 pub kind: Option<serde_json::Value>,
2383 #[serde(default, skip_serializing_if = "Option::is_none")]
2384 pub metadata: Option<serde_json::Value>,
2385 #[serde(default, skip_serializing_if = "Option::is_none")]
2386 pub spec: Option<PartialMaterializeSpec>,
2387 #[serde(default, skip_serializing_if = "Option::is_none")]
2388 pub status: Option<PartialMaterializeStatus>,
2389 #[serde(flatten)]
2390 pub extra: serde_json::Map<String, serde_json::Value>,
2391 }
2392
2393 impl From<MaterializeSpec> for PartialMaterializeSpec {
2394 fn from(spec: MaterializeSpec) -> Self {
2395 let MaterializeSpec {
2396 environmentd_image_ref,
2397 environmentd_extra_args,
2398 environmentd_extra_env,
2399 environmentd_connection_role_arn,
2400 environmentd_resource_requirements,
2401 environmentd_scratch_volume_storage_requirement,
2402 balancerd_resource_requirements,
2403 console_resource_requirements,
2404 balancerd_replicas,
2405 console_replicas,
2406 service_account_name,
2407 service_account_annotations,
2408 service_account_labels,
2409 pod_annotations,
2410 pod_labels,
2411 force_promote,
2412 force_rollout,
2413 rollout_strategy,
2414 rollout_request_timeout,
2415 backend_secret_name,
2416 authenticator_kind,
2417 enable_rbac,
2418 environment_id,
2419 system_parameter_configmap_name,
2420 balancerd_external_certificate_spec,
2421 console_external_certificate_spec,
2422 internal_certificate_spec,
2423 } = spec;
2424 Self {
2425 environmentd_image_ref: present(environmentd_image_ref),
2426 environmentd_extra_args: present_opt(environmentd_extra_args),
2427 environmentd_extra_env: present_opt(environmentd_extra_env),
2428 environmentd_connection_role_arn: present_opt(environmentd_connection_role_arn),
2429 environmentd_resource_requirements: present_opt(environmentd_resource_requirements),
2430 environmentd_scratch_volume_storage_requirement: present_opt(
2431 environmentd_scratch_volume_storage_requirement,
2432 ),
2433 balancerd_resource_requirements: present_opt(balancerd_resource_requirements),
2434 console_resource_requirements: present_opt(console_resource_requirements),
2435 balancerd_replicas: present_opt(balancerd_replicas),
2436 console_replicas: present_opt(console_replicas),
2437 service_account_name: present_opt(service_account_name),
2438 service_account_annotations: present_opt(service_account_annotations),
2439 service_account_labels: present_opt(service_account_labels),
2440 pod_annotations: present_opt(pod_annotations),
2441 pod_labels: present_opt(pod_labels),
2442 force_promote: present_opt(force_promote),
2443 force_rollout: present(force_rollout),
2444 rollout_strategy: present(rollout_strategy),
2445 rollout_request_timeout: present(rollout_request_timeout),
2446 backend_secret_name: present(backend_secret_name),
2447 authenticator_kind: present(authenticator_kind),
2448 enable_rbac: present(enable_rbac),
2449 environment_id: present(environment_id),
2450 system_parameter_configmap_name: present_opt(system_parameter_configmap_name),
2451 balancerd_external_certificate_spec: present_opt(
2452 balancerd_external_certificate_spec,
2453 ),
2454 console_external_certificate_spec: present_opt(console_external_certificate_spec),
2455 internal_certificate_spec: present_opt(internal_certificate_spec),
2456 extra: serde_json::Map::new(),
2457 }
2458 }
2459 }
2460
2461 impl From<MaterializeStatus> for PartialMaterializeStatus {
2462 fn from(status: MaterializeStatus) -> Self {
2463 let MaterializeStatus {
2464 resource_id,
2465 active_generation,
2466 last_completed_rollout_environmentd_image_ref,
2467 last_completed_rollout_hash,
2468 requested_rollout_hash,
2469 conditions,
2470 } = status;
2471 Self {
2472 resource_id: present(resource_id),
2473 active_generation: present(active_generation),
2474 last_completed_rollout_environmentd_image_ref: present_opt(
2475 last_completed_rollout_environmentd_image_ref,
2476 ),
2477 last_completed_rollout_hash: present_opt(last_completed_rollout_hash),
2478 requested_rollout_hash: present_opt(requested_rollout_hash),
2479 conditions: present(conditions),
2480 extra: serde_json::Map::new(),
2481 }
2482 }
2483 }
2484
2485 impl From<Materialize> for PartialMaterialize {
2486 fn from(mz: Materialize) -> Self {
2487 let Materialize {
2488 metadata,
2489 spec,
2490 status,
2491 } = mz;
2492 Self {
2493 api_version: Some("materialize.cloud/v1".to_owned()),
2494 kind: Some("Materialize".into()),
2495 metadata: Some(
2496 serde_json::to_value(metadata).expect("ObjectMeta serializes to JSON"),
2497 ),
2498 spec: Some(spec.into()),
2499 status: status.map(Into::into),
2500 extra: serde_json::Map::new(),
2501 }
2502 }
2503 }
2504
2505 impl From<super::v1alpha1::PartialMaterializeSpec> for PartialMaterializeSpec {
2506 fn from(spec: super::v1alpha1::PartialMaterializeSpec) -> Self {
2507 let super::v1alpha1::PartialMaterializeSpec {
2508 environmentd_image_ref,
2509 environmentd_extra_args,
2510 environmentd_extra_env,
2511 environmentd_iam_role_arn,
2512 environmentd_connection_role_arn,
2513 environmentd_resource_requirements,
2514 environmentd_scratch_volume_storage_requirement,
2515 balancerd_resource_requirements,
2516 console_resource_requirements,
2517 balancerd_replicas,
2518 console_replicas,
2519 service_account_name,
2520 service_account_annotations,
2521 service_account_labels,
2522 pod_annotations,
2523 pod_labels,
2524 request_rollout: _,
2525 force_promote,
2526 force_rollout,
2527 in_place_rollout: _,
2528 rollout_strategy,
2529 rollout_request_timeout,
2530 backend_secret_name,
2531 authenticator_kind,
2532 enable_rbac,
2533 environment_id,
2534 system_parameter_configmap_name,
2535 balancerd_external_certificate_spec,
2536 console_external_certificate_spec,
2537 internal_certificate_spec,
2538 extra,
2539 } = spec;
2540 let service_account_annotations = merge_environmentd_iam_role_arn(
2541 service_account_annotations,
2542 environmentd_iam_role_arn,
2543 );
2544 let force_promote = match force_promote {
2547 Some(Some(value)) if value == "" || value == NIL_UUID_STR => None,
2548 other => other,
2549 };
2550 Self {
2551 environmentd_image_ref,
2552 environmentd_extra_args,
2553 environmentd_extra_env,
2554 environmentd_connection_role_arn,
2555 environmentd_resource_requirements,
2556 environmentd_scratch_volume_storage_requirement,
2557 balancerd_resource_requirements,
2558 console_resource_requirements,
2559 balancerd_replicas,
2560 console_replicas,
2561 service_account_name,
2562 service_account_annotations,
2563 service_account_labels,
2564 pod_annotations,
2565 pod_labels,
2566 force_promote,
2567 force_rollout,
2568 rollout_strategy,
2569 rollout_request_timeout,
2570 backend_secret_name,
2571 authenticator_kind,
2572 enable_rbac,
2573 environment_id,
2574 system_parameter_configmap_name,
2575 balancerd_external_certificate_spec,
2576 console_external_certificate_spec,
2577 internal_certificate_spec,
2578 extra,
2579 }
2580 }
2581 }
2582
2583 impl From<super::v1alpha1::PartialMaterializeStatus> for PartialMaterializeStatus {
2584 fn from(status: super::v1alpha1::PartialMaterializeStatus) -> Self {
2585 let super::v1alpha1::PartialMaterializeStatus {
2586 resource_id,
2587 active_generation,
2588 last_completed_rollout_request: _,
2589 last_completed_rollout_environmentd_image_ref,
2590 resources_hash: _,
2591 last_completed_rollout_hash,
2592 conditions,
2593 extra,
2594 } = status;
2595 Self {
2596 resource_id,
2597 active_generation,
2598 last_completed_rollout_environmentd_image_ref,
2599 last_completed_rollout_hash,
2600 requested_rollout_hash: None,
2605 conditions,
2606 extra,
2607 }
2608 }
2609 }
2610
2611 impl From<super::v1alpha1::PartialMaterialize> for PartialMaterialize {
2612 fn from(mz: super::v1alpha1::PartialMaterialize) -> Self {
2613 let super::v1alpha1::PartialMaterialize {
2614 api_version: _,
2615 kind,
2616 metadata,
2617 spec,
2618 status,
2619 extra,
2620 } = mz;
2621 Self {
2622 api_version: Some("materialize.cloud/v1".to_owned()),
2623 kind,
2624 metadata,
2625 spec: spec.map(Into::into),
2626 status: status.map(Into::into),
2627 extra,
2628 }
2629 }
2630 }
2631}
2632
2633const NIL_UUID_STR: &str = "00000000-0000-0000-0000-000000000000";
2635
2636pub type PartialField = Option<Option<serde_json::Value>>;
2645
2646mod double_option {
2650 use serde::{Deserialize, Deserializer, Serialize, Serializer};
2651
2652 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
2653 where
2654 T: Deserialize<'de>,
2655 D: Deserializer<'de>,
2656 {
2657 Option::<T>::deserialize(deserializer).map(Some)
2658 }
2659
2660 pub fn serialize<T, S>(value: &Option<Option<T>>, serializer: S) -> Result<S::Ok, S::Error>
2661 where
2662 T: Serialize,
2663 S: Serializer,
2664 {
2665 match value {
2666 Some(inner) => inner.serialize(serializer),
2667 None => serializer.serialize_none(),
2669 }
2670 }
2671}
2672
2673fn present<T: Serialize>(value: T) -> PartialField {
2676 Some(Some(
2677 serde_json::to_value(value).expect("CRD field serializes to JSON"),
2678 ))
2679}
2680
2681fn present_opt<T: Serialize>(value: Option<T>) -> PartialField {
2683 value.map(|value| Some(serde_json::to_value(value).expect("CRD field serializes to JSON")))
2684}
2685
2686fn merge_environmentd_iam_role_arn(
2691 annotations: PartialField,
2692 role_arn: PartialField,
2693) -> PartialField {
2694 let Some(Some(role_arn)) = role_arn else {
2695 return annotations;
2696 };
2697 let mut map = match annotations {
2698 Some(Some(serde_json::Value::Object(map))) => map,
2699 Some(Some(other)) => return Some(Some(other)),
2700 Some(None) | None => serde_json::Map::new(),
2701 };
2702 map.entry("eks.amazonaws.com/role-arn").or_insert(role_arn);
2703 Some(Some(serde_json::Value::Object(map)))
2704}
2705
2706pub fn convert_v1alpha1_to_v1(
2725 value: serde_json::Value,
2726) -> Result<serde_json::Value, anyhow::Error> {
2727 let complete = serde_json::from_value::<v1alpha1::Materialize>(value.clone()).ok();
2728 let partial: v1alpha1::PartialMaterialize = serde_json::from_value(value)?;
2729 let mut converted = v1::PartialMaterialize::from(partial);
2730 if let Some(complete) = complete {
2731 let typed = v1::Materialize::from(complete);
2732 if let (Some(status), Some(typed_status)) = (converted.status.as_mut(), typed.status) {
2733 status.requested_rollout_hash = present_opt(typed_status.requested_rollout_hash);
2734 status.last_completed_rollout_hash =
2735 present_opt(typed_status.last_completed_rollout_hash);
2736 }
2737 }
2738 Ok(serde_json::to_value(converted)?)
2739}
2740
2741pub fn convert_v1_to_v1alpha1(
2752 value: serde_json::Value,
2753) -> Result<serde_json::Value, anyhow::Error> {
2754 let complete = serde_json::from_value::<v1::Materialize>(value.clone()).ok();
2755 let partial: v1::PartialMaterialize = serde_json::from_value(value)?;
2756 let mut converted = v1alpha1::PartialMaterialize::from(partial);
2757 if let Some(complete) = complete {
2758 let typed = v1alpha1::Materialize::from(complete);
2759 if let Some(spec) = converted.spec.as_mut() {
2760 spec.request_rollout = present(typed.spec.request_rollout);
2761 }
2762 if let (Some(status), Some(typed_status)) = (converted.status.as_mut(), typed.status) {
2763 status.last_completed_rollout_request =
2764 present(typed_status.last_completed_rollout_request);
2765 status.resources_hash = present(typed_status.resources_hash);
2766 }
2767 }
2768 Ok(serde_json::to_value(converted)?)
2769}
2770
2771fn parse_image_ref(image_ref: &str) -> Option<Version> {
2772 image_ref
2773 .rsplit_once(':')
2774 .and_then(|(_repo, tag)| tag.strip_prefix('v'))
2775 .and_then(|tag| {
2776 let tag = tag.replace("--", "+");
2781 Version::parse(&tag).ok()
2782 })
2783}
2784
2785#[cfg(test)]
2786mod tests {
2787 use std::time::Duration;
2788
2789 use k8s_openapi::apimachinery::pkg::apis::meta::v1::{Condition, Time};
2790 use k8s_openapi::jiff::Timestamp;
2791 use kube::core::ObjectMeta;
2792 use semver::Version;
2793
2794 use super::v1alpha1::{Materialize, MaterializeSpec, MaterializeStatus};
2795 use super::{DEFAULT_ROLLOUT_REQUEST_TIMEOUT, FORCE_ROLLOUT_ANNOTATION, RolloutRequestTimeout};
2796
2797 #[mz_ore::test]
2798 #[cfg_attr(miri, ignore)] fn force_rollout_annotation_forces_new_generation() {
2800 let mut mz = super::v1::Materialize {
2806 spec: super::v1::MaterializeSpec {
2807 environmentd_image_ref: "materialize/environmentd:v26.0.0".to_owned(),
2808 ..Default::default()
2809 },
2810 metadata: ObjectMeta::default(),
2811 status: None,
2812 };
2813 let hash_without_annotation = mz.generate_rollout_hash();
2814 let force_without_annotation = Materialize::from(mz.clone()).force_rollout_value();
2815
2816 mz.metadata.annotations = Some(std::collections::BTreeMap::from_iter([(
2817 FORCE_ROLLOUT_ANNOTATION.to_owned(),
2818 "a4b56cbb-a13e-4f95-8a9d-425c9ba28576".to_owned(),
2819 )]));
2820 assert_ne!(mz.generate_rollout_hash(), hash_without_annotation);
2821 assert_ne!(
2822 Materialize::from(mz.clone()).force_rollout_value(),
2823 force_without_annotation
2824 );
2825
2826 let hash = mz.generate_rollout_hash();
2828 let force = Materialize::from(mz.clone()).force_rollout_value();
2829 mz.metadata.annotations.as_mut().unwrap().insert(
2830 FORCE_ROLLOUT_ANNOTATION.to_owned(),
2831 "3f61bf8d-0714-462c-8b3b-3d9a68d0bcba".to_owned(),
2832 );
2833 assert_ne!(mz.generate_rollout_hash(), hash);
2834 assert_ne!(Materialize::from(mz.clone()).force_rollout_value(), force);
2835 }
2836
2837 #[mz_ore::test]
2838 fn meets_minimum_version() {
2839 let mut mz = Materialize {
2840 spec: MaterializeSpec {
2841 environmentd_image_ref:
2842 "materialize/environmentd:devel-47116c24b8d0df33d3f60a9ee476aa8d7bce5953"
2843 .to_owned(),
2844 ..Default::default()
2845 },
2846 metadata: ObjectMeta {
2847 ..Default::default()
2848 },
2849 status: None,
2850 };
2851
2852 assert!(mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
2854 mz.spec.environmentd_image_ref = "materialize/environmentd:v0.34.0".to_owned();
2855 assert!(mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
2856 mz.spec.environmentd_image_ref = "materialize/environmentd:v0.35.0".to_owned();
2857 assert!(mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
2858 mz.spec.environmentd_image_ref = "materialize/environmentd:v0.34.3".to_owned();
2859 assert!(mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
2860 mz.spec.environmentd_image_ref = "materialize/environmentd@41af286dc0b172ed2f1ca934fd2278de4a1192302ffa07087cea2682e7d372e3".to_owned();
2861 assert!(mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
2862 mz.spec.environmentd_image_ref = "my.private.registry:5000:v0.34.3".to_owned();
2863 assert!(mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
2864 mz.spec.environmentd_image_ref = "materialize/environmentd:v0.asdf.0".to_owned();
2865 assert!(mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
2866 mz.spec.environmentd_image_ref =
2867 "materialize/environmentd:v0.146.0-dev.0--pr.g5a05a9e4ba873be8adaa528644aaae6e4c7cd29b"
2868 .to_owned();
2869 assert!(mz.meets_minimum_version(&Version::parse("0.146.0-dev.0").unwrap()));
2870
2871 mz.spec.environmentd_image_ref = "materialize/environmentd:v0.34.0-dev".to_owned();
2873 assert!(!mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
2874 mz.spec.environmentd_image_ref = "materialize/environmentd:v0.33.0".to_owned();
2875 assert!(!mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
2876 mz.spec.environmentd_image_ref = "materialize/environmentd:v0.34.0".to_owned();
2877 assert!(!mz.meets_minimum_version(&Version::parse("1.0.0").unwrap()));
2878 mz.spec.environmentd_image_ref = "my.private.registry:5000:v0.33.3".to_owned();
2879 assert!(!mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
2880 }
2881
2882 #[mz_ore::test]
2883 fn within_upgrade_window() {
2884 let mut mz = Materialize {
2885 spec: MaterializeSpec {
2886 environmentd_image_ref: "materialize/environmentd:v26.0.0".to_owned(),
2887 ..Default::default()
2888 },
2889 metadata: ObjectMeta {
2890 ..Default::default()
2891 },
2892 status: Some(MaterializeStatus {
2893 last_completed_rollout_environmentd_image_ref: Some(
2894 "materialize/environmentd:v26.0.0".to_owned(),
2895 ),
2896 ..Default::default()
2897 }),
2898 };
2899
2900 mz.spec.environmentd_image_ref = "materialize/environmentd:v27.7.3".to_owned();
2902 assert!(mz.within_upgrade_window());
2903
2904 mz.spec.environmentd_image_ref = "materialize/environmentd:v27.7.8-dev.0".to_owned();
2906 assert!(mz.within_upgrade_window());
2907
2908 mz.spec.environmentd_image_ref = "materialize/environmentd:v28.0.1".to_owned();
2910 assert!(!mz.within_upgrade_window());
2911
2912 mz.spec.environmentd_image_ref =
2914 "materialize/environmentd:v28.0.1.not_a_valid_version".to_owned();
2915 assert!(mz.within_upgrade_window());
2916
2917 mz.status
2919 .as_mut()
2920 .unwrap()
2921 .last_completed_rollout_environmentd_image_ref =
2922 Some("materialize/environmentd:v0.147.20".to_owned());
2923 mz.spec.environmentd_image_ref = "materialize/environmentd:v26.1.0".to_owned();
2924 assert!(mz.within_upgrade_window());
2925
2926 mz.status
2928 .as_mut()
2929 .unwrap()
2930 .last_completed_rollout_environmentd_image_ref =
2931 Some("materialize/environmentd:v26.11.0-dev.0+b".to_owned());
2932 mz.spec.environmentd_image_ref = "materialize/environmentd:v26.11.0-dev.0+a".to_owned();
2933 assert!(mz.within_upgrade_window());
2934 }
2935
2936 #[mz_ore::test]
2937 fn is_valid_upgrade_version() {
2938 let success_tests = [
2939 (Version::new(0, 83, 0), Version::new(0, 83, 0)),
2940 (Version::new(0, 83, 0), Version::new(0, 84, 0)),
2941 (Version::new(0, 9, 0), Version::new(0, 10, 0)),
2942 (Version::new(0, 99, 0), Version::new(0, 100, 0)),
2943 (Version::new(0, 83, 0), Version::new(0, 83, 1)),
2944 (Version::new(0, 83, 0), Version::new(0, 83, 2)),
2945 (Version::new(0, 83, 2), Version::new(0, 83, 10)),
2946 (Version::new(0, 147, 20), Version::new(26, 0, 0)),
2948 (Version::new(0, 164, 0), Version::new(26, 0, 0)),
2949 (Version::new(26, 0, 0), Version::new(26, 1, 0)),
2950 (Version::new(26, 5, 3), Version::new(26, 10, 0)),
2951 (Version::new(0, 130, 0), Version::new(0, 147, 0)),
2952 ];
2953 for (active_version, next_version) in success_tests {
2954 assert!(
2955 Materialize::is_valid_upgrade_version(&active_version, &next_version),
2956 "v{active_version} can upgrade to v{next_version}"
2957 );
2958 }
2959
2960 let failure_tests = [
2961 (Version::new(0, 83, 0), Version::new(0, 82, 0)),
2962 (Version::new(0, 83, 3), Version::new(0, 83, 2)),
2963 (Version::new(0, 83, 3), Version::new(1, 83, 3)),
2964 (Version::new(0, 83, 0), Version::new(0, 85, 0)),
2965 (Version::new(26, 0, 0), Version::new(28, 0, 0)),
2966 (Version::new(0, 130, 0), Version::new(26, 1, 0)),
2967 (Version::new(0, 147, 1), Version::new(26, 0, 0)),
2969 (Version::new(0, 148, 0), Version::new(26, 0, 0)),
2971 ];
2972 for (active_version, next_version) in failure_tests {
2973 assert!(
2974 !Materialize::is_valid_upgrade_version(&active_version, &next_version),
2975 "v{active_version} can't upgrade to v{next_version}"
2976 );
2977 }
2978 }
2979
2980 #[mz_ore::test]
2981 fn rollout_request_timeout() {
2982 let mz_with = |timeout: &str| Materialize {
2983 spec: MaterializeSpec {
2984 rollout_request_timeout: RolloutRequestTimeout(timeout.to_owned()),
2985 ..Default::default()
2986 },
2987 metadata: ObjectMeta::default(),
2988 status: None,
2989 };
2990
2991 let default = humantime::parse_duration(DEFAULT_ROLLOUT_REQUEST_TIMEOUT).unwrap();
2993 assert_eq!(default, Duration::from_secs(24 * 60 * 60));
2994
2995 assert_eq!(
2998 RolloutRequestTimeout::default().0,
2999 DEFAULT_ROLLOUT_REQUEST_TIMEOUT
3000 );
3001 assert_eq!(
3002 Materialize {
3003 spec: MaterializeSpec::default(),
3004 metadata: ObjectMeta::default(),
3005 status: None,
3006 }
3007 .rollout_request_timeout(),
3008 default
3009 );
3010
3011 assert_eq!(
3013 mz_with("1h").rollout_request_timeout(),
3014 Duration::from_secs(60 * 60)
3015 );
3016 assert_eq!(
3017 mz_with("90m").rollout_request_timeout(),
3018 Duration::from_secs(90 * 60)
3019 );
3020 assert_eq!(
3021 mz_with("1h 30m").rollout_request_timeout(),
3022 Duration::from_secs(90 * 60)
3023 );
3024 assert_eq!(mz_with("not a duration").rollout_request_timeout(), default);
3026 }
3027
3028 #[mz_ore::test]
3029 fn rollout_request_timeout_schema_default() {
3030 let crd = serde_json::to_value(<Materialize as kube::CustomResourceExt>::crd())
3034 .expect("CRD serializes");
3035 let default = &crd["spec"]["versions"][0]["schema"]["openAPIV3Schema"]["properties"]["spec"]
3036 ["properties"]["rolloutRequestTimeout"]["default"];
3037 assert_eq!(
3038 default,
3039 &serde_json::json!(DEFAULT_ROLLOUT_REQUEST_TIMEOUT),
3040 "rolloutRequestTimeout schema default missing/wrong in generated CRD",
3041 );
3042 }
3043
3044 #[mz_ore::test]
3045 fn rollout_in_progress_since() {
3046 let now = Timestamp::now();
3047 let condition = |type_: &str, status: &str| Condition {
3048 type_: type_.to_owned(),
3049 status: status.to_owned(),
3050 last_transition_time: Time(now),
3051 message: String::new(),
3052 observed_generation: None,
3053 reason: "Test".to_owned(),
3054 };
3055 let mz_with = |conditions: Vec<Condition>| Materialize {
3056 spec: MaterializeSpec::default(),
3057 metadata: ObjectMeta::default(),
3058 status: Some(MaterializeStatus {
3059 conditions,
3060 ..Default::default()
3061 }),
3062 };
3063
3064 let mz = Materialize {
3066 spec: MaterializeSpec::default(),
3067 metadata: ObjectMeta::default(),
3068 status: None,
3069 };
3070 assert_eq!(mz.rollout_in_progress_since(), None);
3071
3072 assert_eq!(
3075 mz_with(vec![condition("UpToDate", "Unknown")]).rollout_in_progress_since(),
3076 Some(now)
3077 );
3078
3079 assert_eq!(
3083 mz_with(vec![Condition {
3084 reason: "Promoting".to_owned(),
3085 ..condition("UpToDate", "Unknown")
3086 }])
3087 .rollout_in_progress_since(),
3088 None
3089 );
3090
3091 assert_eq!(
3093 mz_with(vec![condition("UpToDate", "True")]).rollout_in_progress_since(),
3094 None
3095 );
3096 assert_eq!(
3097 mz_with(vec![condition("UpToDate", "False")]).rollout_in_progress_since(),
3098 None
3099 );
3100 }
3101
3102 #[mz_ore::test]
3103 fn up_to_date_transition_time() {
3104 let stored = Timestamp::from_second(1_000).unwrap();
3107 let now = Timestamp::from_second(2_000).unwrap();
3108
3109 let condition = |status: &str| Condition {
3110 type_: "UpToDate".to_owned(),
3111 status: status.to_owned(),
3112 last_transition_time: Time(stored),
3113 message: String::new(),
3114 observed_generation: None,
3115 reason: "Test".to_owned(),
3116 };
3117 let mz_with = |conditions: Vec<Condition>| Materialize {
3118 spec: MaterializeSpec::default(),
3119 metadata: ObjectMeta::default(),
3120 status: Some(MaterializeStatus {
3121 conditions,
3122 ..Default::default()
3123 }),
3124 };
3125
3126 let mz = Materialize {
3128 spec: MaterializeSpec::default(),
3129 metadata: ObjectMeta::default(),
3130 status: None,
3131 };
3132 assert_eq!(mz.up_to_date_transition_time("Unknown", now), now);
3133
3134 assert_eq!(
3138 mz_with(vec![condition("Unknown")]).up_to_date_transition_time("Unknown", now),
3139 stored
3140 );
3141
3142 assert_eq!(
3144 mz_with(vec![condition("Unknown")]).up_to_date_transition_time("True", now),
3145 now
3146 );
3147 }
3148
3149 #[mz_ore::test]
3150 fn active_environmentd_image_ref() {
3151 const OLD: &str = "materialize/environmentd:v26.0.0";
3152 const NEW: &str = "materialize/environmentd:v27.0.0";
3153
3154 let mz_with = |spec_image: &str, status: Option<MaterializeStatus>| Materialize {
3155 spec: MaterializeSpec {
3156 environmentd_image_ref: spec_image.to_owned(),
3157 ..Default::default()
3158 },
3159 metadata: ObjectMeta::default(),
3160 status,
3161 };
3162
3163 let mz = mz_with(NEW, None);
3165 assert_eq!(mz.active_environmentd_image_ref(), NEW);
3166
3167 let mz = mz_with(
3171 NEW,
3172 Some(MaterializeStatus {
3173 last_completed_rollout_environmentd_image_ref: None,
3174 ..Default::default()
3175 }),
3176 );
3177 assert_eq!(mz.active_environmentd_image_ref(), NEW);
3178
3179 let mz = mz_with(
3182 NEW,
3183 Some(MaterializeStatus {
3184 last_completed_rollout_environmentd_image_ref: Some(NEW.to_owned()),
3185 ..Default::default()
3186 }),
3187 );
3188 assert_eq!(mz.active_environmentd_image_ref(), NEW);
3189
3190 let mz = mz_with(
3199 NEW,
3200 Some(MaterializeStatus {
3201 last_completed_rollout_environmentd_image_ref: Some(OLD.to_owned()),
3202 ..Default::default()
3203 }),
3204 );
3205 assert_eq!(mz.active_environmentd_image_ref(), OLD);
3206 }
3207
3208 #[mz_ore::test]
3212 fn convert_partial_v1alpha1_to_v1() {
3213 let subset = serde_json::json!({
3214 "apiVersion": "materialize.cloud/v1alpha1",
3215 "kind": "Materialize",
3216 "metadata": {"name": "mz", "namespace": "materialize"},
3217 "spec": {
3218 "environmentdIamRoleArn": "arn:aws:iam::123456789012:role/mz",
3219 "requestRollout": "1e6ef7bc-bff2-4bd4-90dc-eda5697bd0e6",
3220 "inPlaceRollout": false,
3221 "forcePromote": "",
3222 "serviceAccountLabels": {"team": "data"},
3223 },
3224 "status": {
3225 "activeGeneration": 3,
3226 "lastCompletedRolloutRequest": "1e6ef7bc-bff2-4bd4-90dc-eda5697bd0e6",
3227 "resourcesHash": "abc123",
3228 },
3229 });
3230 let converted = super::convert_v1alpha1_to_v1(subset).unwrap();
3231 assert_eq!(converted["apiVersion"], "materialize.cloud/v1");
3232 assert_eq!(converted["kind"], "Materialize");
3233 assert_eq!(converted["metadata"]["name"], "mz");
3234 let spec = converted["spec"].as_object().unwrap();
3235 assert!(!spec.contains_key("requestRollout"));
3236 assert!(!spec.contains_key("inPlaceRollout"));
3237 assert!(!spec.contains_key("forcePromote"));
3238 assert!(!spec.contains_key("environmentdIamRoleArn"));
3239 assert_eq!(
3240 spec["serviceAccountAnnotations"]["eks.amazonaws.com/role-arn"],
3241 "arn:aws:iam::123456789012:role/mz"
3242 );
3243 assert_eq!(spec["serviceAccountLabels"]["team"], "data");
3244 let status = converted["status"].as_object().unwrap();
3245 assert_eq!(status["activeGeneration"], 3);
3246 assert!(!status.contains_key("lastCompletedRolloutRequest"));
3247 assert!(!status.contains_key("resourcesHash"));
3248 assert!(!status.contains_key("requestedRolloutHash"));
3249
3250 let subset = serde_json::json!({
3252 "apiVersion": "materialize.cloud/v1alpha1",
3253 "kind": "Materialize",
3254 "metadata": {"name": "mz"},
3255 "spec": {"forcePromote": "1e6ef7bc-bff2-4bd4-90dc-eda5697bd0e6"},
3256 });
3257 let converted = super::convert_v1alpha1_to_v1(subset).unwrap();
3258 assert_eq!(
3259 converted["spec"]["forcePromote"],
3260 "1e6ef7bc-bff2-4bd4-90dc-eda5697bd0e6"
3261 );
3262 }
3263
3264 #[mz_ore::test]
3265 fn convert_partial_v1_to_v1alpha1() {
3266 let subset = serde_json::json!({
3267 "apiVersion": "materialize.cloud/v1",
3268 "kind": "Materialize",
3269 "metadata": {"name": "mz"},
3270 "spec": {"environmentdImageRef": "materialize/environmentd:v26.0.0"},
3271 "status": {"requestedRolloutHash": "abc123", "activeGeneration": 1},
3272 });
3273 let converted = super::convert_v1_to_v1alpha1(subset).unwrap();
3274 assert_eq!(converted["apiVersion"], "materialize.cloud/v1alpha1");
3275 assert_eq!(
3276 converted["spec"]["environmentdImageRef"],
3277 "materialize/environmentd:v26.0.0"
3278 );
3279 let status = converted["status"].as_object().unwrap();
3280 assert_eq!(status["activeGeneration"], 1);
3281 assert!(!status.contains_key("requestedRolloutHash"));
3282 assert!(!status.contains_key("resourcesHash"));
3283 assert!(
3286 !converted["spec"]
3287 .as_object()
3288 .unwrap()
3289 .contains_key("requestRollout")
3290 );
3291 }
3292
3293 #[mz_ore::test]
3294 #[cfg_attr(miri, ignore)] fn convert_full_v1alpha1_to_v1_derives_fields() {
3296 let mz = Materialize {
3297 metadata: ObjectMeta {
3298 name: Some("mz".to_owned()),
3299 namespace: Some("materialize".to_owned()),
3300 ..Default::default()
3301 },
3302 spec: MaterializeSpec {
3303 environmentd_image_ref: "materialize/environmentd:v26.0.0".to_owned(),
3304 backend_secret_name: "mz-backend".to_owned(),
3305 ..Default::default()
3306 },
3307 status: Some(MaterializeStatus::default()),
3308 };
3309 let value = serde_json::to_value(&mz).unwrap();
3310 let converted = super::convert_v1alpha1_to_v1(value).unwrap();
3311 assert_eq!(converted["apiVersion"], "materialize.cloud/v1");
3312 assert!(converted["status"]["requestedRolloutHash"].is_string());
3315 }
3316
3317 #[mz_ore::test]
3318 fn convert_rejects_non_objects() {
3319 assert!(super::convert_v1alpha1_to_v1(serde_json::json!("not an object")).is_err());
3320 assert!(super::convert_v1_to_v1alpha1(serde_json::json!(42)).is_err());
3321 assert!(
3322 super::convert_v1alpha1_to_v1(serde_json::json!({"spec": "not an object"})).is_err()
3323 );
3324 }
3325
3326 #[mz_ore::test]
3327 fn convert_preserves_null_vs_absent() {
3328 let subset = serde_json::json!({
3329 "apiVersion": "materialize.cloud/v1alpha1",
3330 "kind": "Materialize",
3331 "metadata": {"name": "mz"},
3332 "spec": {
3333 "environmentdExtraArgs": null,
3334 "backendSecretName": "mz-backend",
3335 },
3336 });
3337 let converted = super::convert_v1alpha1_to_v1(subset).unwrap();
3338 let spec = converted["spec"].as_object().unwrap();
3339 assert!(spec.contains_key("environmentdExtraArgs"));
3340 assert!(spec["environmentdExtraArgs"].is_null());
3341 assert!(!spec.contains_key("consoleReplicas"));
3342 }
3343
3344 #[mz_ore::test]
3349 #[cfg_attr(miri, ignore)] fn convert_faithful_output_for_required_field_subsets() {
3351 let subset = serde_json::json!({
3352 "apiVersion": "materialize.cloud/v1alpha1",
3353 "kind": "Materialize",
3354 "metadata": {"name": "mz"},
3355 "spec": {
3356 "environmentdImageRef": "materialize/environmentd:v26.0.0",
3357 "backendSecretName": "mz-backend",
3358 },
3359 });
3360 let converted = super::convert_v1alpha1_to_v1(subset).unwrap();
3361 let spec = converted["spec"].as_object().unwrap();
3362 let mut keys: Vec<_> = spec.keys().cloned().collect();
3363 keys.sort();
3364 assert_eq!(keys, ["backendSecretName", "environmentdImageRef"]);
3365 }
3366
3367 #[mz_ore::test]
3368 fn convert_passes_unknown_fields_through() {
3369 let subset = serde_json::json!({
3370 "apiVersion": "materialize.cloud/v1alpha1",
3371 "kind": "Materialize",
3372 "metadata": {"name": "mz"},
3373 "spec": {"someFutureField": {"a": 1}},
3374 "someTopLevelField": true,
3375 });
3376 let converted = super::convert_v1alpha1_to_v1(subset).unwrap();
3377 assert_eq!(converted["spec"]["someFutureField"]["a"], 1);
3378 assert_eq!(converted["someTopLevelField"], true);
3379 }
3380
3381 #[mz_ore::test]
3382 #[cfg_attr(miri, ignore)] fn convert_full_v1_to_v1alpha1_derives_request_rollout() {
3384 let mz = super::v1::Materialize {
3385 metadata: ObjectMeta {
3386 name: Some("mz".to_owned()),
3387 namespace: Some("materialize".to_owned()),
3388 ..Default::default()
3389 },
3390 spec: super::v1::MaterializeSpec {
3391 environmentd_image_ref: "materialize/environmentd:v26.0.0".to_owned(),
3392 backend_secret_name: "mz-backend".to_owned(),
3393 ..Default::default()
3394 },
3395 status: Some(super::v1::MaterializeStatus::default()),
3396 };
3397 let expected = Materialize::from(mz.clone()).spec.request_rollout;
3398 let converted = super::convert_v1_to_v1alpha1(serde_json::to_value(&mz).unwrap()).unwrap();
3399 assert_eq!(converted["apiVersion"], "materialize.cloud/v1alpha1");
3400 assert_eq!(
3401 converted["spec"]["requestRollout"],
3402 expected.hyphenated().to_string()
3403 );
3404 let status = converted["status"].as_object().unwrap();
3408 assert!(status["resourcesHash"].is_string());
3409 assert!(status["lastCompletedRolloutRequest"].is_string());
3410 }
3411
3412 #[mz_ore::test]
3417 fn partial_mirror_from_typed_omits_unset_fields() {
3418 let mz = Materialize {
3419 metadata: ObjectMeta::default(),
3420 spec: MaterializeSpec {
3421 environmentd_image_ref: "materialize/environmentd:v26.0.0".to_owned(),
3422 backend_secret_name: "mz-backend".to_owned(),
3423 ..Default::default()
3424 },
3425 status: None,
3426 };
3427 let value = serde_json::to_value(super::v1alpha1::PartialMaterialize::from(mz)).unwrap();
3428 let spec = value["spec"].as_object().unwrap();
3429 assert!(!spec.contains_key("balancerdReplicas"));
3430 for (key, field_value) in spec {
3431 assert!(!field_value.is_null(), "unexpected null for {key}");
3432 }
3433 assert!(!value.as_object().unwrap().contains_key("status"));
3434 }
3435}