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