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, 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_priority_class_name: Option<String>,
102 pub clusterd_node_selector: Vec<KeyValueArg<String, String>>,
103 pub clusterd_affinity: Option<Affinity>,
104 pub clusterd_tolerations: Option<Vec<Toleration>>,
105 pub clusterd_priority_class_name: Option<String>,
107 pub image_pull_policy: KubernetesImagePullPolicy,
108 pub network_policies_internal_enabled: bool,
109 pub network_policies_ingress_enabled: bool,
110 pub network_policies_ingress_cidrs: Vec<String>,
111 pub network_policies_egress_enabled: bool,
112 pub network_policies_egress_cidrs: Vec<String>,
113
114 pub environmentd_cluster_replica_sizes: Option<String>,
115 pub bootstrap_default_cluster_replica_size: Option<String>,
116 pub bootstrap_builtin_system_cluster_replica_size: Option<String>,
117 pub bootstrap_builtin_probe_cluster_replica_size: Option<String>,
118 pub bootstrap_builtin_support_cluster_replica_size: Option<String>,
119 pub bootstrap_builtin_catalog_server_cluster_replica_size: Option<String>,
120 pub bootstrap_builtin_analytics_cluster_replica_size: Option<String>,
121 pub bootstrap_builtin_system_cluster_replication_factor: Option<u32>,
122 pub bootstrap_builtin_probe_cluster_replication_factor: Option<u32>,
123 pub bootstrap_builtin_support_cluster_replication_factor: Option<u32>,
124 pub bootstrap_builtin_analytics_cluster_replication_factor: Option<u32>,
125
126 pub environmentd_allowed_origins: Vec<HeaderValue>,
127 pub internal_console_proxy_url: String,
128
129 pub environmentd_sql_port: u16,
130 pub environmentd_http_port: u16,
131 pub environmentd_internal_sql_port: u16,
132 pub environmentd_internal_http_port: u16,
133 pub environmentd_internal_persist_pubsub_port: u16,
134
135 pub default_certificate_specs: DefaultCertificateSpecs,
136
137 pub disable_license_key_checks: bool,
138 pub disable_database_network_policies: bool,
139
140 pub tracing: TracingCliArgs,
141 pub orchestratord_namespace: String,
142
143 pub teleport: Option<teleport::TeleportConfig>,
144}
145
146pub struct Context {
147 config: Config,
148 metrics: Arc<Metrics>,
149 needs_update: Arc<Mutex<BTreeSet<String>>>,
150}
151
152impl Context {
153 pub fn new(config: Config, metrics: Arc<Metrics>) -> Self {
154 if config.cloud_provider == CloudProvider::Aws {
155 assert!(
156 config.aws_account_id.is_some(),
157 "--aws-account-id is required when using --cloud-provider=aws"
158 );
159 }
160
161 Self {
162 config,
163 metrics,
164 needs_update: Default::default(),
165 }
166 }
167
168 fn set_needs_update(&self, mz: &Materialize, needs_update: bool) {
169 let mut needs_update_set = self.needs_update.lock().unwrap();
170 if needs_update {
171 needs_update_set.insert(mz.name_unchecked());
172 } else {
173 needs_update_set.remove(&mz.name_unchecked());
174 }
175 self.metrics
176 .environmentd_needs_update
177 .set(u64::cast_from(needs_update_set.len()));
178 }
179
180 async fn update_status(
184 &self,
185 metadata: &TraceMetadata,
186 mz_api: &Api<Materialize>,
187 mz: &Materialize,
188 status: MaterializeStatus,
189 needs_update: bool,
190 ) -> Result<Materialize, kube::Error> {
191 self.set_needs_update(mz, needs_update);
192
193 let mut new_mz = mz.clone();
194 if !mz
195 .status
196 .as_ref()
197 .map_or(true, |mz_status| mz_status.needs_update(&status))
198 {
199 return Ok(new_mz);
200 }
201
202 let condition = status.conditions.first().cloned();
203 new_mz.status = Some(status);
204 let new_mz = mz_api
205 .replace_status(&mz.name_unchecked(), &PostParams::default(), &new_mz)
206 .await?;
207
208 if let Some(condition) = condition {
209 metadata
210 .publish_event(Event {
211 type_: transition_event_type(&condition),
212 reason: condition.reason,
213 action: "Reconcile".into(),
214 note: Some(condition.message),
215 related: None,
216 })
217 .await;
218 }
219
220 Ok(new_mz)
221 }
222
223 async fn teardown_generation(
226 &self,
227 metadata: &TraceMetadata,
228 client: &Client,
229 mz: &Materialize,
230 resources: &generation::Resources,
231 generation: u64,
232 ) -> Result<(), Error> {
233 let step = metadata.step("teardown_generation");
234 resources
235 .teardown_generation(client, mz, generation)
236 .await?;
237 step.finish(Outcome::Completed);
238 Ok(())
239 }
240
241 async fn promote(
242 &self,
243 metadata: &TraceMetadata,
244 client: &Client,
245 mz: &Materialize,
246 resources: generation::Resources,
247 active_generation: u64,
248 desired_generation: u64,
249 resources_hash: String,
250 ) -> Result<Option<Action>, Error> {
251 let step = metadata.step("promote");
252 if let Some(action) = resources.promote_services(client, &mz.namespace()).await? {
253 step.finish(Outcome::Waiting);
254 return Ok(Some(action));
255 }
256 step.finish(Outcome::Completed);
257
258 self.teardown_generation(metadata, client, mz, &resources, active_generation)
259 .await?;
260 let mz_api: Api<Materialize> = Api::namespaced(client.clone(), &mz.namespace());
261 self.update_status(
262 metadata,
263 &mz_api,
264 mz,
265 MaterializeStatus {
266 active_generation: desired_generation,
267 last_completed_rollout_request: mz.requested_reconciliation_id(),
268 last_completed_rollout_environmentd_image_ref: Some(
269 mz.spec.environmentd_image_ref.clone(),
270 ),
271 resource_id: mz.status().resource_id,
272 resources_hash,
273 last_completed_rollout_hash: None,
274 conditions: vec![Condition {
275 type_: "UpToDate".into(),
276 status: "True".into(),
277 last_transition_time: Time(Timestamp::now()),
278 message: format!(
279 "Successfully applied changes for generation {desired_generation}"
280 ),
281 observed_generation: mz.meta().generation,
282 reason: "Applied".into(),
283 }],
284 },
285 false,
286 )
287 .await?;
288 Ok(None)
289 }
290
291 async fn check_environment_id_conflicts(
292 &self,
293 client: &Client,
294 mz: &Materialize,
295 ) -> Result<(), Error> {
296 if mz.spec.environment_id.is_nil() {
297 return Err(Error::Anyhow(anyhow::anyhow!(
301 "trying to reconcile a materialize resource with no environment id - this is a bug!"
302 )));
303 }
304
305 let mz_api: Api<Materialize> = Api::all(client.clone());
306 let all_mz = mz_api.list(&ListParams::default()).await?;
307 for existing_mz in &all_mz.items {
308 if existing_mz.spec.environment_id == mz.spec.environment_id
309 && existing_mz.metadata.uid != mz.metadata.uid
310 {
311 return Err(Error::Anyhow(anyhow::anyhow!(
312 "Materialize resources {}/{} and {}/{} have the environmentId field set to the same value. This field must be unique across environments.",
313 mz.namespace(),
314 mz.name_unchecked(),
315 existing_mz.namespace(),
316 existing_mz.name_unchecked(),
317 )));
318 }
319 }
320
321 Ok(())
322 }
323}
324
325#[async_trait::async_trait]
326impl k8s_controller::Context for Context {
327 type Resource = Materialize;
328 type Error = Error;
329
330 const FINALIZER_NAME: Option<&'static str> =
331 Some("orchestratord.materialize.cloud/materialize");
332
333 #[instrument(fields(organization_name=mz.name_unchecked()))]
334 async fn apply(
335 &self,
336 client: Client,
337 mz: &Self::Resource,
338 metadata: &mut TraceMetadata,
339 ) -> Result<Option<Action>, Self::Error> {
340 let metadata = &*metadata;
341 let mz_api: Api<Materialize> = Api::namespaced(client.clone(), &mz.namespace());
342 let balancer_api: Api<Balancer> = Api::namespaced(client.clone(), &mz.namespace());
343 let console_api: Api<Console> = Api::namespaced(client.clone(), &mz.namespace());
344 let secret_api: Api<Secret> = Api::namespaced(client.clone(), &mz.namespace());
345
346 let status = mz.status();
347 if mz.status.is_none() {
348 let step = metadata.step("initialize_status");
349 self.update_status(metadata, &mz_api, mz, status, true)
350 .await?;
351 step.finish(Outcome::Completed);
352 return Ok(None);
355 }
356
357 let step = metadata.step("resolve_environment_id");
358 let backend_secret = secret_api.get(&mz.spec.backend_secret_name).await?;
359 let license_key_environment_id: Option<Uuid> = if let Some(license_key) = backend_secret
360 .data
361 .as_ref()
362 .and_then(|data| data.get("license_key"))
363 {
364 let license_key = validate(
365 str::from_utf8(&license_key.0)
366 .context("invalid utf8")?
367 .trim(),
368 )?;
369 let environment_id = license_key
370 .environment_id
371 .parse()
372 .context("invalid environment id in license key")?;
373 Some(environment_id)
374 } else {
375 if mz.meets_minimum_version(&V161) {
376 return Err(Error::Anyhow(anyhow::anyhow!(
377 "license_key is required when running in kubernetes",
378 )));
379 } else {
380 None
381 }
382 };
383
384 if mz.spec.request_rollout.is_nil() || mz.spec.environment_id.is_nil() {
385 let mut mz = mz.clone();
386 if mz.spec.request_rollout.is_nil() {
387 mz.spec.request_rollout = Uuid::new_v4();
388 }
389 if mz.spec.environment_id.is_nil() {
390 if let Some(environment_id) = license_key_environment_id {
391 if environment_id.is_nil() {
392 mz.spec.environment_id = Uuid::new_v4();
395 } else {
396 mz.spec.environment_id = environment_id;
397 }
398 } else {
399 if mz.meets_minimum_version(&V161) {
400 return Err(Error::Anyhow(anyhow::anyhow!(
401 "environmentId is not set in materialize resource {}/{} but no license key was given",
402 mz.namespace(),
403 mz.name_unchecked()
404 )));
405 } else {
406 mz.spec.environment_id = Uuid::new_v4();
407 }
408 }
409 }
410 mz_api
411 .replace(&mz.name_unchecked(), &PostParams::default(), &mz)
412 .await?;
413 step.finish(Outcome::Completed);
414 return Ok(None);
418 }
419
420 if let Some(environment_id) = license_key_environment_id {
421 if !environment_id.is_nil() && mz.spec.environment_id != environment_id {
424 return Err(Error::Anyhow(anyhow::anyhow!(
425 "environment_id is set in materialize resource {}/{} but does not match the environment_id set in the associated license key {}",
426 mz.namespace(),
427 mz.name_unchecked(),
428 environment_id,
429 )));
430 }
431 }
432
433 self.check_environment_id_conflicts(&client, mz).await?;
434 step.finish(Outcome::Completed);
435
436 let step = metadata.step("global_resources");
437 global::Resources::new(&self.config, mz)?
438 .apply(&client, &mz.namespace())
439 .await?;
440 step.finish(Outcome::Completed);
441
442 let active_resources =
448 generation::Resources::new(&self.config, mz, status.active_generation);
449 let has_current_changes = status.resources_hash != active_resources.generate_hash();
450 let active_generation = status.active_generation;
451 let next_generation = active_generation + 1;
452 let desired_generation = if has_current_changes {
453 next_generation
454 } else {
455 active_generation
456 };
457
458 let resources = generation::Resources::new(&self.config, mz, desired_generation);
461 let resources_hash = resources.generate_hash();
462
463 let mut result = match (
464 mz.is_promoting(),
465 has_current_changes,
466 mz.rollout_requested(),
467 ) {
468 (true, _, _) => {
471 self.promote(
472 metadata,
473 &client,
474 mz,
475 resources,
476 active_generation,
477 desired_generation,
478 resources_hash,
479 )
480 .await
481 }
482 (false, true, true) => {
484 if !mz.should_force_promote() {
496 if let Some(started) = mz.rollout_in_progress_since() {
497 let timeout = mz.rollout_request_timeout();
498 let elapsed = Timestamp::now().duration_since(started);
499 let timed_out = SignedDuration::try_from(timeout)
500 .is_ok_and(|timeout| elapsed >= timeout);
501 if timed_out {
502 warn!(
503 "rollout to generation {desired_generation} exceeded timeout, cancelling"
504 );
505 self.teardown_generation(
508 metadata,
509 &client,
510 mz,
511 &resources,
512 next_generation,
513 )
514 .await?;
515 self.update_status(
516 metadata,
517 &mz_api,
518 mz,
519 MaterializeStatus {
520 active_generation,
521 last_completed_rollout_request: mz
526 .requested_reconciliation_id(),
527 last_completed_rollout_environmentd_image_ref: status
528 .last_completed_rollout_environmentd_image_ref
529 .clone(),
530 resource_id: status.resource_id.clone(),
531 resources_hash: status.resources_hash.clone(),
532 last_completed_rollout_hash: None,
533 conditions: vec![Condition {
534 type_: "UpToDate".into(),
535 status: "False".into(),
536 last_transition_time: Time(Timestamp::now()),
537 message: format!(
538 "Cancelled rollout to generation \
539 {desired_generation} after it \
540 exceeded the rollout timeout of {}",
541 humantime::format_duration(timeout),
542 ),
543 observed_generation: mz.meta().generation,
544 reason: "RolloutTimeout".into(),
545 }],
546 },
547 active_generation != desired_generation,
548 )
549 .await?;
550 return Ok(None);
551 }
552 }
553 }
554
555 if !mz.within_upgrade_window() {
556 let last_completed_rollout_environmentd_image_ref =
557 status.last_completed_rollout_environmentd_image_ref;
558
559 self.update_status(
560 metadata,
561 &mz_api,
562 mz,
563 MaterializeStatus {
564 active_generation,
565 last_completed_rollout_request: status.last_completed_rollout_request,
566 last_completed_rollout_environmentd_image_ref:
567 last_completed_rollout_environmentd_image_ref.clone(),
568 resource_id: status.resource_id,
569 resources_hash: status.resources_hash,
570 last_completed_rollout_hash: None,
571 conditions: vec![Condition {
572 type_: "UpToDate".into(),
573 status: "False".into(),
574 last_transition_time: Time(Timestamp::now()),
575 message: format!(
576 "Refusing to upgrade from {} to {}. \
577 More than one major version from \
578 last successful rollout. If coming \
579 from Self Managed 25.2, upgrade to \
580 materialize/environmentd:v0.147.20 \
581 first.",
582 last_completed_rollout_environmentd_image_ref
583 .expect("should be set if upgrade window check fails"),
584 mz.spec.environmentd_image_ref,
585 ),
586 observed_generation: mz.meta().generation,
587 reason: "FailedDeploy".into(),
588 }],
589 },
590 active_generation != desired_generation,
591 )
592 .await?;
593 return Ok(None);
594 }
595
596 let mz = if mz.is_ready_to_promote(&resources_hash) {
608 mz
609 } else {
610 &self
611 .update_status(
612 metadata,
613 &mz_api,
614 mz,
615 MaterializeStatus {
616 active_generation,
617 last_completed_rollout_request: status
622 .last_completed_rollout_request,
623 last_completed_rollout_environmentd_image_ref: status
624 .last_completed_rollout_environmentd_image_ref,
625 resource_id: status.resource_id.clone(),
626 resources_hash: String::new(),
627 last_completed_rollout_hash: None,
628 conditions: vec![Condition {
629 type_: "UpToDate".into(),
630 status: "Unknown".into(),
631 last_transition_time: Time(Timestamp::now()),
632 message: format!(
633 "Applying changes for generation {desired_generation}"
634 ),
635 observed_generation: mz.meta().generation,
636 reason: "Applying".into(),
637 }],
638 },
639 active_generation != desired_generation,
640 )
641 .await?
642 };
643 let status = mz.status();
644
645 if mz.spec.rollout_strategy
646 == MaterializeRolloutStrategy::ImmediatelyPromoteCausingDowntime
647 {
648 self.teardown_generation(metadata, &client, mz, &resources, active_generation)
652 .await?;
653 }
654
655 trace!("applying environment resources");
656 let step = metadata.step("generation_resources");
657 let applied = resources
658 .apply(&client, mz.should_force_promote(), &mz.namespace())
659 .await;
660 step.finish_with(&applied);
661 match applied {
662 Ok(Some(action)) => {
663 trace!("new environment is not yet ready");
664 Ok(Some(action))
665 }
666 Ok(None) => {
667 if mz.spec.rollout_strategy == MaterializeRolloutStrategy::ManuallyPromote
668 && !mz.should_force_promote()
669 {
670 trace!(
671 "Ready to promote, but not promoting because the instance is configured with ManuallyPromote rollout strategy."
672 );
673 self.update_status(
674 metadata,
675 &mz_api,
676 mz,
677 MaterializeStatus {
678 active_generation,
679 last_completed_rollout_request: status
680 .last_completed_rollout_request,
681 last_completed_rollout_environmentd_image_ref: status
682 .last_completed_rollout_environmentd_image_ref,
683 resource_id: status.resource_id,
684 resources_hash,
685 last_completed_rollout_hash: None,
686 conditions: vec![Condition {
687 type_: "UpToDate".into(),
688 status: "Unknown".into(),
689 last_transition_time: Time(mz.up_to_date_transition_time(
695 "Unknown",
696 Timestamp::now(),
697 )),
698 message: format!(
699 "Ready to promote generation {desired_generation}"
700 ),
701 observed_generation: mz.meta().generation,
702 reason: "ReadyToPromote".into(),
703 }],
704 },
705 active_generation != desired_generation,
706 )
707 .await?;
708 return Ok(None);
709 }
710 self.update_status(
718 metadata,
719 &mz_api,
720 mz,
721 MaterializeStatus {
722 active_generation,
723 last_completed_rollout_request: status
728 .last_completed_rollout_request,
729 last_completed_rollout_environmentd_image_ref: status
730 .last_completed_rollout_environmentd_image_ref,
731 resource_id: status.resource_id,
732 resources_hash: resources_hash.clone(),
733 last_completed_rollout_hash: None,
734 conditions: vec![Condition {
735 type_: "UpToDate".into(),
736 status: "Unknown".into(),
737 last_transition_time: Time(Timestamp::now()),
738 message: format!(
739 "Attempting to promote generation {desired_generation}"
740 ),
741 observed_generation: mz.meta().generation,
742 reason: "Promoting".into(),
743 }],
744 },
745 active_generation != desired_generation,
746 )
747 .await?;
748 self.promote(
749 metadata,
750 &client,
751 mz,
752 resources,
753 active_generation,
754 desired_generation,
755 resources_hash,
756 )
757 .await
758 }
759 Err(e) => {
760 self.update_status(
761 metadata,
762 &mz_api,
763 mz,
764 MaterializeStatus {
765 active_generation,
766 last_completed_rollout_request: status
771 .last_completed_rollout_request,
772 last_completed_rollout_environmentd_image_ref: status
773 .last_completed_rollout_environmentd_image_ref,
774 resource_id: status.resource_id,
775 resources_hash: status.resources_hash,
776 last_completed_rollout_hash: None,
777 conditions: vec![Condition {
778 type_: "UpToDate".into(),
779 status: "False".into(),
780 last_transition_time: Time(Timestamp::now()),
781 message: format!(
782 "Failed to apply changes for \
783 generation {desired_generation}: {e}"
784 ),
785 observed_generation: mz.meta().generation,
786 reason: "FailedDeploy".into(),
787 }],
788 },
789 active_generation != desired_generation,
790 )
791 .await?;
792 Err(e)
793 }
794 }
795 }
796 (false, true, false) => {
798 let mut needs_update = mz.conditions_need_update();
799 if mz.update_in_progress() {
800 self.teardown_generation(metadata, &client, mz, &resources, next_generation)
801 .await?;
802 needs_update = true;
803 }
804 if needs_update {
805 self.update_status(metadata, &mz_api,
806 mz,
807 MaterializeStatus {
808 active_generation,
809 last_completed_rollout_request: mz.requested_reconciliation_id(),
810 last_completed_rollout_environmentd_image_ref: status
811 .last_completed_rollout_environmentd_image_ref,
812 resource_id: status.resource_id.clone(),
813 resources_hash: status.resources_hash,
814 last_completed_rollout_hash: None,
815 conditions: vec![Condition {
816 type_: "UpToDate".into(),
817 status: "False".into(),
818 last_transition_time: Time(Timestamp::now()),
819 message: format!(
820 "Changes detected, waiting for approval for generation {desired_generation}"
821 ),
822 observed_generation: mz.meta().generation,
823 reason: "WaitingForApproval".into(),
824 }],
825 },
826 active_generation != desired_generation,
827 )
828 .await?;
829 }
830 debug!("changes detected, waiting for approval");
831 Ok(None)
832 }
833 (false, false, _) => {
835 let mut needs_update = mz.conditions_need_update() || mz.rollout_requested();
840 if mz.update_in_progress() {
841 self.teardown_generation(metadata, &client, mz, &resources, next_generation)
842 .await?;
843 needs_update = true;
844 }
845 if needs_update {
846 self.update_status(
847 metadata,
848 &mz_api,
849 mz,
850 MaterializeStatus {
851 active_generation,
852 last_completed_rollout_request: mz.requested_reconciliation_id(),
853 last_completed_rollout_environmentd_image_ref: status
854 .last_completed_rollout_environmentd_image_ref,
855 resource_id: status.resource_id.clone(),
856 resources_hash: status.resources_hash,
857 last_completed_rollout_hash: None,
858 conditions: vec![Condition {
859 type_: "UpToDate".into(),
860 status: "True".into(),
861 last_transition_time: Time(Timestamp::now()),
862 message: format!(
863 "No changes found from generation {active_generation}"
864 ),
865 observed_generation: mz.meta().generation,
866 reason: "Applied".into(),
867 }],
868 },
869 active_generation != desired_generation,
870 )
871 .await?;
872 }
873 debug!("no changes");
874 Ok(None)
875 }
876 }?;
877
878 if let Some(action) = result {
879 return Ok(Some(action));
880 }
881
882 let step = metadata.step("balancer");
887 if self.config.create_balancers {
888 let balancer = Balancer {
889 metadata: mz.managed_resource_meta(mz.name_unchecked()),
890 spec: BalancerSpec {
891 balancerd_image_ref: matching_image_from_environmentd_image_ref(
892 mz.active_environmentd_image_ref(),
893 "balancerd",
894 None,
895 ),
896 resource_requirements: mz.spec.balancerd_resource_requirements.clone(),
897 configmap_name: mz.spec.balancerd_configmap_name.clone(),
898 replicas: Some(mz.balancerd_replicas()),
899 external_certificate_spec: mz.spec.balancerd_external_certificate_spec.clone(),
900 internal_certificate_spec: mz.spec.internal_certificate_spec.clone(),
901 pod_annotations: mz.spec.pod_annotations.clone(),
902 pod_labels: mz.spec.pod_labels.clone(),
903 static_routing: Some(
904 mz_cloud_resources::crd::balancer::v1alpha1::StaticRoutingConfig {
905 environmentd_namespace: mz.namespace(),
906 environmentd_service_name: mz.environmentd_service_name(),
907 },
908 ),
909 frontegg_routing: None,
910 resource_id: Some(status.resource_id.clone()),
911 },
912 status: None,
913 };
914 let balancer = apply_resource(&balancer_api, &balancer).await?;
915 result = wait_for_balancer(&balancer)?;
916 step.finish(match result {
917 Some(_) => Outcome::Waiting,
918 None => Outcome::Completed,
919 });
920 } else {
921 delete_resource(&balancer_api, &mz.name_unchecked()).await?;
922 step.finish(Outcome::Skipped);
923 }
924
925 if let Some(action) = result {
926 return Ok(Some(action));
927 }
928
929 let step = metadata.step("console");
933 if self.config.create_console {
934 let active_environmentd_image_ref = mz.active_environmentd_image_ref();
935 let environmentd_image_tag =
936 parse_image_tag(active_environmentd_image_ref).unwrap_or("latest");
937 let console_image_tag = self
938 .config
939 .console_image_tag_map
940 .iter()
941 .find(|kv| kv.key == environmentd_image_tag)
942 .map(|kv| kv.value.clone())
943 .unwrap_or_else(|| self.config.console_image_tag_default.clone());
944 let console = Console {
945 metadata: mz.managed_resource_meta(mz.name_unchecked()),
946 spec: ConsoleSpec {
947 console_image_ref: matching_image_from_environmentd_image_ref(
948 active_environmentd_image_ref,
949 "console",
950 Some(&console_image_tag),
951 ),
952 resource_requirements: mz.spec.console_resource_requirements.clone(),
953 replicas: Some(mz.console_replicas()),
954 external_certificate_spec: mz.spec.console_external_certificate_spec.clone(),
955 pod_annotations: mz.spec.pod_annotations.clone(),
956 pod_labels: mz.spec.pod_labels.clone(),
957 balancerd: BalancerdRef {
958 service_name: mz.balancerd_service_name(),
959 namespace: mz.namespace(),
960 scheme: if issuer_ref_defined(
961 &self.config.default_certificate_specs.balancerd_external,
962 &mz.spec.balancerd_external_certificate_spec,
963 ) {
964 HttpConnectionScheme::Https
965 } else {
966 HttpConnectionScheme::Http
967 },
968 dns_names: resolved_dns_names(
969 &self.config.default_certificate_specs.balancerd_external,
970 &mz.spec.balancerd_external_certificate_spec,
971 ),
972 },
973 authenticator_kind: mz.spec.authenticator_kind,
974 resource_id: Some(status.resource_id),
975 },
976 status: None,
977 };
978 apply_resource(&console_api, &console).await?;
979 step.finish(Outcome::Completed);
980 } else {
981 delete_resource(&console_api, &mz.name_unchecked()).await?;
982 step.finish(Outcome::Skipped);
983 }
984
985 Ok(result)
986 }
987
988 #[instrument(fields(organization_name=mz.name_unchecked()))]
989 async fn cleanup(
990 &self,
991 _client: Client,
992 mz: &Self::Resource,
993 _metadata: &mut TraceMetadata,
994 ) -> Result<Option<Action>, Self::Error> {
995 self.set_needs_update(mz, false);
996
997 Ok(None)
998 }
999}
1000
1001fn transition_event_type(condition: &Condition) -> EventType {
1006 match condition.reason.as_str() {
1007 "WaitingForApproval" => EventType::Normal,
1008 _ if condition.status == "False" => EventType::Warning,
1009 _ => EventType::Normal,
1010 }
1011}
1012
1013fn wait_for_balancer(balancer: &Balancer) -> Result<Option<Action>, Error> {
1014 if let Some(conditions) = balancer
1015 .status
1016 .as_ref()
1017 .map(|status| status.conditions.as_slice())
1018 {
1019 if conditions
1020 .iter()
1021 .any(|condition| condition.type_ == "Ready" && condition.status == "True")
1022 {
1023 return Ok(None);
1024 }
1025 }
1026
1027 Ok(Some(Action::requeue(Duration::from_secs(1))))
1028}
1029
1030#[cfg(test)]
1031mod tests {
1032 use super::*;
1033
1034 fn condition(status: &str, reason: &str) -> Condition {
1035 Condition {
1036 type_: "UpToDate".into(),
1037 status: status.into(),
1038 reason: reason.into(),
1039 message: String::new(),
1040 last_transition_time: Time(Timestamp::now()),
1041 observed_generation: None,
1042 }
1043 }
1044
1045 #[mz_ore::test]
1046 fn test_transition_event_type() {
1047 for (status, reason, expected) in [
1048 ("True", "Applied", EventType::Normal),
1049 ("Unknown", "Applying", EventType::Normal),
1050 ("Unknown", "ReadyToPromote", EventType::Normal),
1051 ("Unknown", "Promoting", EventType::Normal),
1052 ("False", "WaitingForApproval", EventType::Normal),
1053 ("False", "FailedDeploy", EventType::Warning),
1054 ("False", "RolloutTimeout", EventType::Warning),
1055 ("False", "SomeFutureFailure", EventType::Warning),
1056 ("Unknown", "SomeFuturePhase", EventType::Normal),
1057 ] {
1058 assert_eq!(
1059 transition_event_type(&condition(status, reason)),
1060 expected,
1061 "status={status} reason={reason}",
1062 );
1063 }
1064 }
1065}