1use std::{
11 collections::BTreeSet,
12 sync::{Arc, Mutex},
13 time::Duration,
14};
15
16use anyhow::Context as _;
17use http::HeaderValue;
18use k8s_controller::{
19 Outcome, TraceMetadata,
20 events::{Event, EventType},
21};
22use k8s_openapi::{
23 api::core::v1::{Affinity, Probe, ResourceRequirements, Secret, Toleration},
24 apimachinery::pkg::apis::meta::v1::{Condition, Time},
25 jiff::{SignedDuration, Timestamp},
26};
27use kube::{
28 Api, Client, Resource, ResourceExt,
29 api::{ListParams, PostParams},
30 runtime::controller::Action,
31};
32use tracing::{debug, trace, warn};
33use uuid::Uuid;
34
35use crate::{
36 Error,
37 controller::materialize::generation::V161,
38 k8s::{apply_resource, delete_resource},
39 matching_image_from_environmentd_image_ref,
40 metrics::Metrics,
41 parse_image_tag,
42 tls::{DefaultCertificateSpecs, issuer_ref_defined, resolved_dns_names},
43};
44use mz_cloud_provider::CloudProvider;
45use mz_cloud_resources::crd::{
46 ManagedResource,
47 balancer::v1alpha1::{Balancer, BalancerSpec},
48 console::v1alpha1::{BalancerdRef, Console, ConsoleSpec, HttpConnectionScheme},
49 materialize::MaterializeRolloutStrategy,
50 materialize::v1alpha1::{Materialize, MaterializeStatus},
51};
52use mz_license_keys::validate;
53use mz_orchestrator_kubernetes::KubernetesImagePullPolicy;
54use mz_orchestrator_tracing::TracingCliArgs;
55use mz_ore::{cast::CastFrom, cli::KeyValueArg, instrument};
56
57pub mod generation;
58pub mod global;
59pub mod teleport;
60
61pub const CONTROLLER_NAME: &str = "materialize";
64
65#[derive(Clone)]
66pub struct Config {
67 pub cloud_provider: CloudProvider,
68 pub region: String,
69 pub create_balancers: bool,
70 pub create_console: bool,
71 pub helm_chart_version: Option<String>,
72 pub secrets_controller: String,
73 pub collect_pod_metrics: bool,
74 pub enable_prometheus_scrape_annotations: bool,
75
76 pub segment_api_key: Option<String>,
77 pub segment_client_side: bool,
78
79 pub console_image_tag_default: String,
80 pub console_image_tag_map: Vec<KeyValueArg<String, String>>,
81
82 pub aws_account_id: Option<String>,
83 pub environmentd_iam_role_arn: Option<String>,
84 pub environmentd_connection_role_arn: Option<String>,
85 pub aws_secrets_controller_tags: Vec<String>,
86 pub environmentd_availability_zones: Option<Vec<String>>,
87
88 pub ephemeral_volume_class: Option<String>,
89 pub scheduler_name: Option<String>,
90 pub enable_security_context: bool,
91 pub enable_internal_statement_logging: bool,
92 pub statement_logging_max_sample_rate: Option<f64>,
93 pub statement_logging_target_data_rate: Option<usize>,
94
95 pub orchestratord_pod_selector_labels: Vec<KeyValueArg<String, String>>,
96 pub environmentd_node_selector: Vec<KeyValueArg<String, String>>,
97 pub environmentd_affinity: Option<Affinity>,
98 pub environmentd_tolerations: Option<Vec<Toleration>>,
99 pub environmentd_default_resources: Option<ResourceRequirements>,
100 pub environmentd_default_readiness_probe: Option<Probe>,
103 pub environmentd_default_liveness_probe: Option<Probe>,
106 pub environmentd_default_startup_probe: Option<Probe>,
109 pub environmentd_priority_class_name: Option<String>,
111 pub clusterd_node_selector: Vec<KeyValueArg<String, String>>,
112 pub clusterd_affinity: Option<Affinity>,
113 pub clusterd_tolerations: Option<Vec<Toleration>>,
114 pub clusterd_priority_class_name: Option<String>,
116 pub image_pull_policy: KubernetesImagePullPolicy,
117 pub network_policies_internal_enabled: bool,
118 pub network_policies_ingress_enabled: bool,
119 pub network_policies_ingress_cidrs: Vec<String>,
120 pub network_policies_egress_enabled: bool,
121 pub network_policies_egress_cidrs: Vec<String>,
122
123 pub environmentd_cluster_replica_sizes: Option<String>,
124 pub bootstrap_default_cluster_replica_size: Option<String>,
125 pub bootstrap_builtin_system_cluster_replica_size: Option<String>,
126 pub bootstrap_builtin_probe_cluster_replica_size: Option<String>,
127 pub bootstrap_builtin_support_cluster_replica_size: Option<String>,
128 pub bootstrap_builtin_catalog_server_cluster_replica_size: Option<String>,
129 pub bootstrap_builtin_analytics_cluster_replica_size: Option<String>,
130 pub bootstrap_builtin_system_cluster_replication_factor: Option<u32>,
131 pub bootstrap_builtin_probe_cluster_replication_factor: Option<u32>,
132 pub bootstrap_builtin_support_cluster_replication_factor: Option<u32>,
133 pub bootstrap_builtin_analytics_cluster_replication_factor: Option<u32>,
134
135 pub environmentd_allowed_origins: Vec<HeaderValue>,
136 pub internal_console_proxy_url: String,
137
138 pub environmentd_sql_port: u16,
139 pub environmentd_http_port: u16,
140 pub environmentd_internal_sql_port: u16,
141 pub environmentd_internal_http_port: u16,
142 pub environmentd_internal_persist_pubsub_port: u16,
143
144 pub default_certificate_specs: DefaultCertificateSpecs,
145
146 pub disable_license_key_checks: bool,
147 pub disable_database_network_policies: bool,
148
149 pub tracing: TracingCliArgs,
150 pub orchestratord_namespace: String,
151
152 pub teleport: Option<teleport::TeleportConfig>,
153}
154
155pub struct Context {
156 config: Config,
157 metrics: Arc<Metrics>,
158 needs_update: Arc<Mutex<BTreeSet<String>>>,
159}
160
161impl Context {
162 pub fn new(config: Config, metrics: Arc<Metrics>) -> Self {
163 if config.cloud_provider == CloudProvider::Aws {
164 assert!(
165 config.aws_account_id.is_some(),
166 "--aws-account-id is required when using --cloud-provider=aws"
167 );
168 }
169
170 Self {
171 config,
172 metrics,
173 needs_update: Default::default(),
174 }
175 }
176
177 fn set_needs_update(&self, mz: &Materialize, needs_update: bool) {
178 let mut needs_update_set = self.needs_update.lock().unwrap();
179 if needs_update {
180 needs_update_set.insert(mz.name_unchecked());
181 } else {
182 needs_update_set.remove(&mz.name_unchecked());
183 }
184 self.metrics
185 .environmentd_needs_update
186 .set(u64::cast_from(needs_update_set.len()));
187 }
188
189 async fn update_status(
193 &self,
194 metadata: &TraceMetadata,
195 mz_api: &Api<Materialize>,
196 mz: &Materialize,
197 status: MaterializeStatus,
198 needs_update: bool,
199 ) -> Result<Materialize, kube::Error> {
200 self.set_needs_update(mz, needs_update);
201
202 let mut new_mz = mz.clone();
203 if !mz
204 .status
205 .as_ref()
206 .map_or(true, |mz_status| mz_status.needs_update(&status))
207 {
208 return Ok(new_mz);
209 }
210
211 let condition = status.conditions.first().cloned();
212 new_mz.status = Some(status);
213 let new_mz = mz_api
214 .replace_status(&mz.name_unchecked(), &PostParams::default(), &new_mz)
215 .await?;
216
217 if let Some(condition) = condition {
218 metadata
219 .publish_event(Event {
220 type_: transition_event_type(&condition),
221 reason: condition.reason,
222 action: "Reconcile".into(),
223 note: Some(condition.message),
224 related: None,
225 })
226 .await;
227 }
228
229 Ok(new_mz)
230 }
231
232 async fn teardown_generation(
235 &self,
236 metadata: &TraceMetadata,
237 client: &Client,
238 mz: &Materialize,
239 resources: &generation::Resources,
240 generation: u64,
241 ) -> Result<(), Error> {
242 let step = metadata.step("teardown_generation");
243 resources
244 .teardown_generation(client, mz, generation)
245 .await?;
246 step.finish(Outcome::Completed);
247 Ok(())
248 }
249
250 async fn promote(
251 &self,
252 metadata: &TraceMetadata,
253 client: &Client,
254 mz: &Materialize,
255 resources: generation::Resources,
256 active_generation: u64,
257 desired_generation: u64,
258 resources_hash: String,
259 ) -> Result<Option<Action>, Error> {
260 let step = metadata.step("promote");
261 if let Some(action) = resources.promote_services(client, &mz.namespace()).await? {
262 step.finish(Outcome::Waiting);
263 return Ok(Some(action));
264 }
265 step.finish(Outcome::Completed);
266
267 self.teardown_generation(metadata, client, mz, &resources, active_generation)
268 .await?;
269 let mz_api: Api<Materialize> = Api::namespaced(client.clone(), &mz.namespace());
270 self.update_status(
271 metadata,
272 &mz_api,
273 mz,
274 MaterializeStatus {
275 active_generation: desired_generation,
276 last_completed_rollout_request: mz.requested_reconciliation_id(),
277 last_completed_rollout_environmentd_image_ref: Some(
278 mz.spec.environmentd_image_ref.clone(),
279 ),
280 resource_id: mz.status().resource_id,
281 resources_hash,
282 last_completed_rollout_hash: None,
283 conditions: vec![Condition {
284 type_: "UpToDate".into(),
285 status: "True".into(),
286 last_transition_time: Time(Timestamp::now()),
287 message: format!(
288 "Successfully applied changes for generation {desired_generation}"
289 ),
290 observed_generation: mz.meta().generation,
291 reason: "Applied".into(),
292 }],
293 },
294 false,
295 )
296 .await?;
297 Ok(None)
298 }
299
300 async fn check_environment_id_conflicts(
301 &self,
302 client: &Client,
303 mz: &Materialize,
304 ) -> Result<(), Error> {
305 if mz.spec.environment_id.is_nil() {
306 return Err(Error::Anyhow(anyhow::anyhow!(
310 "trying to reconcile a materialize resource with no environment id - this is a bug!"
311 )));
312 }
313
314 let mz_api: Api<Materialize> = Api::all(client.clone());
315 let all_mz = mz_api.list(&ListParams::default()).await?;
316 for existing_mz in &all_mz.items {
317 if existing_mz.spec.environment_id == mz.spec.environment_id
318 && existing_mz.metadata.uid != mz.metadata.uid
319 {
320 return Err(Error::Anyhow(anyhow::anyhow!(
321 "Materialize resources {}/{} and {}/{} have the environmentId field set to the same value. This field must be unique across environments.",
322 mz.namespace(),
323 mz.name_unchecked(),
324 existing_mz.namespace(),
325 existing_mz.name_unchecked(),
326 )));
327 }
328 }
329
330 Ok(())
331 }
332}
333
334#[async_trait::async_trait]
335impl k8s_controller::Context for Context {
336 type Resource = Materialize;
337 type Error = Error;
338
339 const FINALIZER_NAME: Option<&'static str> =
340 Some("orchestratord.materialize.cloud/materialize");
341
342 #[instrument(fields(organization_name=mz.name_unchecked()))]
343 async fn apply(
344 &self,
345 client: Client,
346 mz: &Self::Resource,
347 metadata: &mut TraceMetadata,
348 ) -> Result<Option<Action>, Self::Error> {
349 let metadata = &*metadata;
350 let mz_api: Api<Materialize> = Api::namespaced(client.clone(), &mz.namespace());
351 let balancer_api: Api<Balancer> = Api::namespaced(client.clone(), &mz.namespace());
352 let console_api: Api<Console> = Api::namespaced(client.clone(), &mz.namespace());
353 let secret_api: Api<Secret> = Api::namespaced(client.clone(), &mz.namespace());
354
355 let status = mz.status();
356 if mz.status.is_none() {
357 let step = metadata.step("initialize_status");
358 self.update_status(metadata, &mz_api, mz, status, true)
359 .await?;
360 step.finish(Outcome::Completed);
361 return Ok(None);
364 }
365
366 let step = metadata.step("resolve_environment_id");
367 let backend_secret = secret_api.get(&mz.spec.backend_secret_name).await?;
368 let license_key_environment_id: Option<Uuid> = if let Some(license_key) = backend_secret
369 .data
370 .as_ref()
371 .and_then(|data| data.get("license_key"))
372 {
373 let license_key = validate(
374 str::from_utf8(&license_key.0)
375 .context("invalid utf8")?
376 .trim(),
377 )?;
378 let environment_id = license_key
379 .environment_id
380 .parse()
381 .context("invalid environment id in license key")?;
382 Some(environment_id)
383 } else {
384 if mz.meets_minimum_version(&V161) {
385 return Err(Error::Anyhow(anyhow::anyhow!(
386 "license_key is required when running in kubernetes",
387 )));
388 } else {
389 None
390 }
391 };
392
393 if mz.spec.request_rollout.is_nil() || mz.spec.environment_id.is_nil() {
394 let mut mz = mz.clone();
395 if mz.spec.request_rollout.is_nil() {
396 mz.spec.request_rollout = Uuid::new_v4();
397 }
398 if mz.spec.environment_id.is_nil() {
399 if let Some(environment_id) = license_key_environment_id {
400 if environment_id.is_nil() {
401 mz.spec.environment_id = Uuid::new_v4();
404 } else {
405 mz.spec.environment_id = environment_id;
406 }
407 } else {
408 if mz.meets_minimum_version(&V161) {
409 return Err(Error::Anyhow(anyhow::anyhow!(
410 "environmentId is not set in materialize resource {}/{} but no license key was given",
411 mz.namespace(),
412 mz.name_unchecked()
413 )));
414 } else {
415 mz.spec.environment_id = Uuid::new_v4();
416 }
417 }
418 }
419 mz_api
420 .replace(&mz.name_unchecked(), &PostParams::default(), &mz)
421 .await?;
422 step.finish(Outcome::Completed);
423 return Ok(None);
427 }
428
429 if let Some(environment_id) = license_key_environment_id {
430 if !environment_id.is_nil() && mz.spec.environment_id != environment_id {
433 return Err(Error::Anyhow(anyhow::anyhow!(
434 "environment_id is set in materialize resource {}/{} but does not match the environment_id set in the associated license key {}",
435 mz.namespace(),
436 mz.name_unchecked(),
437 environment_id,
438 )));
439 }
440 }
441
442 for (field, probe) in [
443 (
444 "environmentdReadinessProbe",
445 &mz.spec.environmentd_readiness_probe,
446 ),
447 (
448 "environmentdLivenessProbe",
449 &mz.spec.environmentd_liveness_probe,
450 ),
451 (
452 "environmentdStartupProbe",
453 &mz.spec.environmentd_startup_probe,
454 ),
455 ] {
456 if let Some(probe) = probe {
457 generation::validate_probe_handlers(probe).map_err(|e| {
458 Error::Anyhow(anyhow::anyhow!(
459 "invalid {field} in materialize resource {}/{}: {e}",
460 mz.namespace(),
461 mz.name_unchecked(),
462 ))
463 })?;
464 }
465 }
466
467 self.check_environment_id_conflicts(&client, mz).await?;
468 step.finish(Outcome::Completed);
469
470 let step = metadata.step("global_resources");
471 global::Resources::new(&self.config, mz)?
472 .apply(&client, &mz.namespace())
473 .await?;
474 step.finish(Outcome::Completed);
475
476 let active_resources =
482 generation::Resources::new(&self.config, mz, status.active_generation);
483 let has_current_changes = status.resources_hash != active_resources.generate_hash();
484 let active_generation = status.active_generation;
485 let next_generation = active_generation + 1;
486 let desired_generation = if has_current_changes {
487 next_generation
488 } else {
489 active_generation
490 };
491
492 let resources = generation::Resources::new(&self.config, mz, desired_generation);
495 let resources_hash = resources.generate_hash();
496
497 let mut result = match (
498 mz.is_promoting(),
499 has_current_changes,
500 mz.rollout_requested(),
501 ) {
502 (true, _, _) => {
505 self.promote(
506 metadata,
507 &client,
508 mz,
509 resources,
510 active_generation,
511 desired_generation,
512 resources_hash,
513 )
514 .await
515 }
516 (false, true, true) => {
518 if !mz.should_force_promote() {
530 if let Some(started) = mz.rollout_in_progress_since() {
531 let timeout = mz.rollout_request_timeout();
532 let elapsed = Timestamp::now().duration_since(started);
533 let timed_out = SignedDuration::try_from(timeout)
534 .is_ok_and(|timeout| elapsed >= timeout);
535 if timed_out {
536 warn!(
537 "rollout to generation {desired_generation} exceeded timeout, cancelling"
538 );
539 self.teardown_generation(
542 metadata,
543 &client,
544 mz,
545 &resources,
546 next_generation,
547 )
548 .await?;
549 self.update_status(
550 metadata,
551 &mz_api,
552 mz,
553 MaterializeStatus {
554 active_generation,
555 last_completed_rollout_request: mz
560 .requested_reconciliation_id(),
561 last_completed_rollout_environmentd_image_ref: status
562 .last_completed_rollout_environmentd_image_ref
563 .clone(),
564 resource_id: status.resource_id.clone(),
565 resources_hash: status.resources_hash.clone(),
566 last_completed_rollout_hash: None,
567 conditions: vec![Condition {
568 type_: "UpToDate".into(),
569 status: "False".into(),
570 last_transition_time: Time(Timestamp::now()),
571 message: format!(
572 "Cancelled rollout to generation \
573 {desired_generation} after it \
574 exceeded the rollout timeout of {}",
575 humantime::format_duration(timeout),
576 ),
577 observed_generation: mz.meta().generation,
578 reason: "RolloutTimeout".into(),
579 }],
580 },
581 active_generation != desired_generation,
582 )
583 .await?;
584 return Ok(None);
585 }
586 }
587 }
588
589 if !mz.within_upgrade_window() {
590 let last_completed_rollout_environmentd_image_ref =
591 status.last_completed_rollout_environmentd_image_ref;
592
593 self.update_status(
594 metadata,
595 &mz_api,
596 mz,
597 MaterializeStatus {
598 active_generation,
599 last_completed_rollout_request: status.last_completed_rollout_request,
600 last_completed_rollout_environmentd_image_ref:
601 last_completed_rollout_environmentd_image_ref.clone(),
602 resource_id: status.resource_id,
603 resources_hash: status.resources_hash,
604 last_completed_rollout_hash: None,
605 conditions: vec![Condition {
606 type_: "UpToDate".into(),
607 status: "False".into(),
608 last_transition_time: Time(Timestamp::now()),
609 message: format!(
610 "Refusing to upgrade from {} to {}. \
611 More than one major version from \
612 last successful rollout. If coming \
613 from Self Managed 25.2, upgrade to \
614 materialize/environmentd:v0.147.20 \
615 first.",
616 last_completed_rollout_environmentd_image_ref
617 .expect("should be set if upgrade window check fails"),
618 mz.spec.environmentd_image_ref,
619 ),
620 observed_generation: mz.meta().generation,
621 reason: "FailedDeploy".into(),
622 }],
623 },
624 active_generation != desired_generation,
625 )
626 .await?;
627 return Ok(None);
628 }
629
630 let mz = if mz.is_ready_to_promote(&resources_hash) {
642 mz
643 } else {
644 &self
645 .update_status(
646 metadata,
647 &mz_api,
648 mz,
649 MaterializeStatus {
650 active_generation,
651 last_completed_rollout_request: status
656 .last_completed_rollout_request,
657 last_completed_rollout_environmentd_image_ref: status
658 .last_completed_rollout_environmentd_image_ref,
659 resource_id: status.resource_id.clone(),
660 resources_hash: String::new(),
661 last_completed_rollout_hash: None,
662 conditions: vec![Condition {
663 type_: "UpToDate".into(),
664 status: "Unknown".into(),
665 last_transition_time: Time(Timestamp::now()),
666 message: format!(
667 "Applying changes for generation {desired_generation}"
668 ),
669 observed_generation: mz.meta().generation,
670 reason: "Applying".into(),
671 }],
672 },
673 active_generation != desired_generation,
674 )
675 .await?
676 };
677 let status = mz.status();
678
679 if mz.spec.rollout_strategy
680 == MaterializeRolloutStrategy::ImmediatelyPromoteCausingDowntime
681 {
682 self.teardown_generation(metadata, &client, mz, &resources, active_generation)
686 .await?;
687 }
688
689 trace!("applying environment resources");
690 let step = metadata.step("generation_resources");
691 let applied = resources
692 .apply(&client, mz.should_force_promote(), &mz.namespace())
693 .await;
694 step.finish_with(&applied);
695 match applied {
696 Ok(Some(action)) => {
697 trace!("new environment is not yet ready");
698 Ok(Some(action))
699 }
700 Ok(None) => {
701 if mz.spec.rollout_strategy == MaterializeRolloutStrategy::ManuallyPromote
702 && !mz.should_force_promote()
703 {
704 trace!(
705 "Ready to promote, but not promoting because the instance is configured with ManuallyPromote rollout strategy."
706 );
707 self.update_status(
708 metadata,
709 &mz_api,
710 mz,
711 MaterializeStatus {
712 active_generation,
713 last_completed_rollout_request: status
714 .last_completed_rollout_request,
715 last_completed_rollout_environmentd_image_ref: status
716 .last_completed_rollout_environmentd_image_ref,
717 resource_id: status.resource_id,
718 resources_hash,
719 last_completed_rollout_hash: None,
720 conditions: vec![Condition {
721 type_: "UpToDate".into(),
722 status: "Unknown".into(),
723 last_transition_time: Time(mz.up_to_date_transition_time(
729 "Unknown",
730 Timestamp::now(),
731 )),
732 message: format!(
733 "Ready to promote generation {desired_generation}"
734 ),
735 observed_generation: mz.meta().generation,
736 reason: "ReadyToPromote".into(),
737 }],
738 },
739 active_generation != desired_generation,
740 )
741 .await?;
742 return Ok(None);
743 }
744 self.update_status(
752 metadata,
753 &mz_api,
754 mz,
755 MaterializeStatus {
756 active_generation,
757 last_completed_rollout_request: status
762 .last_completed_rollout_request,
763 last_completed_rollout_environmentd_image_ref: status
764 .last_completed_rollout_environmentd_image_ref,
765 resource_id: status.resource_id,
766 resources_hash: resources_hash.clone(),
767 last_completed_rollout_hash: None,
768 conditions: vec![Condition {
769 type_: "UpToDate".into(),
770 status: "Unknown".into(),
771 last_transition_time: Time(Timestamp::now()),
772 message: format!(
773 "Attempting to promote generation {desired_generation}"
774 ),
775 observed_generation: mz.meta().generation,
776 reason: "Promoting".into(),
777 }],
778 },
779 active_generation != desired_generation,
780 )
781 .await?;
782 self.promote(
783 metadata,
784 &client,
785 mz,
786 resources,
787 active_generation,
788 desired_generation,
789 resources_hash,
790 )
791 .await
792 }
793 Err(e) => {
794 self.update_status(
795 metadata,
796 &mz_api,
797 mz,
798 MaterializeStatus {
799 active_generation,
800 last_completed_rollout_request: status
805 .last_completed_rollout_request,
806 last_completed_rollout_environmentd_image_ref: status
807 .last_completed_rollout_environmentd_image_ref,
808 resource_id: status.resource_id,
809 resources_hash: status.resources_hash,
810 last_completed_rollout_hash: None,
811 conditions: vec![Condition {
812 type_: "UpToDate".into(),
813 status: "False".into(),
814 last_transition_time: Time(Timestamp::now()),
815 message: format!(
816 "Failed to apply changes for \
817 generation {desired_generation}: {e}"
818 ),
819 observed_generation: mz.meta().generation,
820 reason: "FailedDeploy".into(),
821 }],
822 },
823 active_generation != desired_generation,
824 )
825 .await?;
826 Err(e)
827 }
828 }
829 }
830 (false, true, false) => {
832 let mut needs_update = mz.conditions_need_update();
833 if mz.update_in_progress() {
834 self.teardown_generation(metadata, &client, mz, &resources, next_generation)
835 .await?;
836 needs_update = true;
837 }
838 if needs_update {
839 self.update_status(metadata, &mz_api,
840 mz,
841 MaterializeStatus {
842 active_generation,
843 last_completed_rollout_request: mz.requested_reconciliation_id(),
844 last_completed_rollout_environmentd_image_ref: status
845 .last_completed_rollout_environmentd_image_ref,
846 resource_id: status.resource_id.clone(),
847 resources_hash: status.resources_hash,
848 last_completed_rollout_hash: None,
849 conditions: vec![Condition {
850 type_: "UpToDate".into(),
851 status: "False".into(),
852 last_transition_time: Time(Timestamp::now()),
853 message: format!(
854 "Changes detected, waiting for approval for generation {desired_generation}"
855 ),
856 observed_generation: mz.meta().generation,
857 reason: "WaitingForApproval".into(),
858 }],
859 },
860 active_generation != desired_generation,
861 )
862 .await?;
863 }
864 debug!("changes detected, waiting for approval");
865 Ok(None)
866 }
867 (false, false, _) => {
869 let mut needs_update = mz.conditions_need_update() || mz.rollout_requested();
874 if mz.update_in_progress() {
875 self.teardown_generation(metadata, &client, mz, &resources, next_generation)
876 .await?;
877 needs_update = true;
878 }
879 if needs_update {
880 self.update_status(
881 metadata,
882 &mz_api,
883 mz,
884 MaterializeStatus {
885 active_generation,
886 last_completed_rollout_request: mz.requested_reconciliation_id(),
887 last_completed_rollout_environmentd_image_ref: status
888 .last_completed_rollout_environmentd_image_ref,
889 resource_id: status.resource_id.clone(),
890 resources_hash: status.resources_hash,
891 last_completed_rollout_hash: None,
892 conditions: vec![Condition {
893 type_: "UpToDate".into(),
894 status: "True".into(),
895 last_transition_time: Time(Timestamp::now()),
896 message: format!(
897 "No changes found from generation {active_generation}"
898 ),
899 observed_generation: mz.meta().generation,
900 reason: "Applied".into(),
901 }],
902 },
903 active_generation != desired_generation,
904 )
905 .await?;
906 }
907 debug!("no changes");
908 Ok(None)
909 }
910 }?;
911
912 if let Some(action) = result {
913 return Ok(Some(action));
914 }
915
916 let step = metadata.step("balancer");
921 if self.config.create_balancers {
922 let balancer = Balancer {
923 metadata: mz.managed_resource_meta(mz.name_unchecked()),
924 spec: BalancerSpec {
925 balancerd_image_ref: matching_image_from_environmentd_image_ref(
926 mz.active_environmentd_image_ref(),
927 "balancerd",
928 None,
929 ),
930 resource_requirements: mz.spec.balancerd_resource_requirements.clone(),
931 configmap_name: mz.spec.balancerd_configmap_name.clone(),
932 replicas: Some(mz.balancerd_replicas()),
933 externally_scaled: mz.spec.balancerd_externally_scaled,
934 external_certificate_spec: mz.spec.balancerd_external_certificate_spec.clone(),
935 internal_certificate_spec: mz.spec.internal_certificate_spec.clone(),
936 pod_annotations: mz.spec.pod_annotations.clone(),
937 pod_labels: mz.spec.pod_labels.clone(),
938 static_routing: Some(
939 mz_cloud_resources::crd::balancer::v1alpha1::StaticRoutingConfig {
940 environmentd_namespace: mz.namespace(),
941 environmentd_service_name: mz.environmentd_service_name(),
942 },
943 ),
944 frontegg_routing: None,
945 resource_id: Some(status.resource_id.clone()),
946 },
947 status: None,
948 };
949 let balancer = apply_resource(&balancer_api, &balancer).await?;
950 result = wait_for_balancer(&balancer)?;
951 step.finish(match result {
952 Some(_) => Outcome::Waiting,
953 None => Outcome::Completed,
954 });
955 } else {
956 delete_resource(&balancer_api, &mz.name_unchecked()).await?;
957 step.finish(Outcome::Skipped);
958 }
959
960 if let Some(action) = result {
961 return Ok(Some(action));
962 }
963
964 let step = metadata.step("console");
968 if self.config.create_console {
969 let active_environmentd_image_ref = mz.active_environmentd_image_ref();
970 let environmentd_image_tag =
971 parse_image_tag(active_environmentd_image_ref).unwrap_or("latest");
972 let console_image_tag = self
973 .config
974 .console_image_tag_map
975 .iter()
976 .find(|kv| kv.key == environmentd_image_tag)
977 .map(|kv| kv.value.clone())
978 .unwrap_or_else(|| self.config.console_image_tag_default.clone());
979 let console = Console {
980 metadata: mz.managed_resource_meta(mz.name_unchecked()),
981 spec: ConsoleSpec {
982 console_image_ref: matching_image_from_environmentd_image_ref(
983 active_environmentd_image_ref,
984 "console",
985 Some(&console_image_tag),
986 ),
987 resource_requirements: mz.spec.console_resource_requirements.clone(),
988 replicas: Some(mz.console_replicas()),
989 external_certificate_spec: mz.spec.console_external_certificate_spec.clone(),
990 pod_annotations: mz.spec.pod_annotations.clone(),
991 pod_labels: mz.spec.pod_labels.clone(),
992 balancerd: BalancerdRef {
993 service_name: mz.balancerd_service_name(),
994 namespace: mz.namespace(),
995 scheme: if issuer_ref_defined(
996 &self.config.default_certificate_specs.balancerd_external,
997 &mz.spec.balancerd_external_certificate_spec,
998 ) {
999 HttpConnectionScheme::Https
1000 } else {
1001 HttpConnectionScheme::Http
1002 },
1003 dns_names: resolved_dns_names(
1004 &self.config.default_certificate_specs.balancerd_external,
1005 &mz.spec.balancerd_external_certificate_spec,
1006 ),
1007 },
1008 authenticator_kind: mz.spec.authenticator_kind,
1009 resource_id: Some(status.resource_id),
1010 },
1011 status: None,
1012 };
1013 apply_resource(&console_api, &console).await?;
1014 step.finish(Outcome::Completed);
1015 } else {
1016 delete_resource(&console_api, &mz.name_unchecked()).await?;
1017 step.finish(Outcome::Skipped);
1018 }
1019
1020 Ok(result)
1021 }
1022
1023 #[instrument(fields(organization_name=mz.name_unchecked()))]
1024 async fn cleanup(
1025 &self,
1026 _client: Client,
1027 mz: &Self::Resource,
1028 _metadata: &mut TraceMetadata,
1029 ) -> Result<Option<Action>, Self::Error> {
1030 self.set_needs_update(mz, false);
1031
1032 Ok(None)
1033 }
1034}
1035
1036fn transition_event_type(condition: &Condition) -> EventType {
1041 match condition.reason.as_str() {
1042 "WaitingForApproval" => EventType::Normal,
1043 _ if condition.status == "False" => EventType::Warning,
1044 _ => EventType::Normal,
1045 }
1046}
1047
1048fn wait_for_balancer(balancer: &Balancer) -> Result<Option<Action>, Error> {
1049 if let Some(conditions) = balancer
1050 .status
1051 .as_ref()
1052 .map(|status| status.conditions.as_slice())
1053 {
1054 if conditions
1055 .iter()
1056 .any(|condition| condition.type_ == "Ready" && condition.status == "True")
1057 {
1058 return Ok(None);
1059 }
1060 }
1061
1062 Ok(Some(Action::requeue(Duration::from_secs(1))))
1063}
1064
1065#[cfg(test)]
1066mod tests {
1067 use super::*;
1068
1069 fn condition(status: &str, reason: &str) -> Condition {
1070 Condition {
1071 type_: "UpToDate".into(),
1072 status: status.into(),
1073 reason: reason.into(),
1074 message: String::new(),
1075 last_transition_time: Time(Timestamp::now()),
1076 observed_generation: None,
1077 }
1078 }
1079
1080 #[mz_ore::test]
1081 fn test_transition_event_type() {
1082 for (status, reason, expected) in [
1083 ("True", "Applied", EventType::Normal),
1084 ("Unknown", "Applying", EventType::Normal),
1085 ("Unknown", "ReadyToPromote", EventType::Normal),
1086 ("Unknown", "Promoting", EventType::Normal),
1087 ("False", "WaitingForApproval", EventType::Normal),
1088 ("False", "FailedDeploy", EventType::Warning),
1089 ("False", "RolloutTimeout", EventType::Warning),
1090 ("False", "SomeFutureFailure", EventType::Warning),
1091 ("Unknown", "SomeFuturePhase", EventType::Normal),
1092 ] {
1093 assert_eq!(
1094 transition_event_type(&condition(status, reason)),
1095 expected,
1096 "status={status} reason={reason}",
1097 );
1098 }
1099 }
1100}