Skip to content

Commit 13a8710

Browse files
committed
feat: add upgrade window check for environmentd image ref
This commit introduces a new method to check if the current environmentd image reference is within the upgrade window of the last successful rollout. We default the image ref to the current version if status is not set.
1 parent 2dc5d08 commit 13a8710

File tree

2 files changed

+136
-1
lines changed

2 files changed

+136
-1
lines changed

src/cloud-resources/src/crd/materialize.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ use crate::crd::generated::cert_manager::certificates::{
3030
CertificateIssuerRef, CertificateSecretTemplate,
3131
};
3232

33+
const V26_0_0: Version = Version::new(26, 0, 0);
34+
3335
pub const LAST_KNOWN_ACTIVE_GENERATION_ANNOTATION: &str =
3436
"materialize.cloud/last-known-active-generation";
3537

@@ -479,6 +481,38 @@ pub mod v1alpha1 {
479481
}
480482
}
481483

484+
/// Checks if the current environmentd image ref is within the upgrade window of the last
485+
/// successful rollout.
486+
///
487+
/// This check isn't strictly required since environmentd will still be able to determine
488+
/// if the upgrade is allowed or not. However, doing this check allows us to provide
489+
/// the error as soon as possible and in a more user friendly way.
490+
pub fn within_upgrade_window(&self) -> bool {
491+
let current_environmentd_version = self
492+
.status
493+
.as_ref()
494+
.and_then(|status| {
495+
status
496+
.last_completed_rollout_environmentd_image_ref
497+
.as_ref()
498+
})
499+
.and_then(|image_ref| parse_image_ref(image_ref));
500+
501+
if let (Some(new_environmentd_version), Some(current_environmentd_version)) = (
502+
parse_image_ref(&self.spec.environmentd_image_ref),
503+
current_environmentd_version,
504+
) {
505+
if current_environmentd_version >= V26_0_0 {
506+
// We deny upgrades past 1 major version of the last successful rollout
507+
return new_environmentd_version.major
508+
<= current_environmentd_version.major + 1;
509+
}
510+
}
511+
// If we fail any of the preconditions for the check (e.g. we couldn't parse either version),
512+
// we still allow the upgrade since environmentd will still error if the upgrade is not allowed.
513+
true
514+
}
515+
482516
pub fn managed_resource_meta(&self, name: String) -> ObjectMeta {
483517
ObjectMeta {
484518
namespace: Some(self.namespace()),
@@ -516,6 +550,11 @@ pub mod v1alpha1 {
516550
.expect("valid int generation");
517551
}
518552

553+
// Initialize the last completed rollout environmentd image ref to
554+
// the current image ref if not already set.
555+
status.last_completed_rollout_environmentd_image_ref =
556+
Some(self.spec.environmentd_image_ref.clone());
557+
519558
status
520559
})
521560
}
@@ -530,6 +569,10 @@ pub mod v1alpha1 {
530569
pub active_generation: u64,
531570
/// The UUID of the last successfully completed rollout.
532571
pub last_completed_rollout_request: Uuid,
572+
/// The image ref of the environmentd image that was last successfully rolled out.
573+
/// Used to deny upgrades past 1 major version from the last successful rollout.
574+
/// When None, we upgrade anyways.
575+
pub last_completed_rollout_environmentd_image_ref: Option<String>,
533576
/// A hash calculated from the spec of resources to be created based on this Materialize
534577
/// spec. This is used for detecting when the existing resources are up to date.
535578
/// If you want to trigger a rollout without making other changes that would cause this
@@ -630,4 +673,51 @@ mod tests {
630673
mz.spec.environmentd_image_ref = "my.private.registry:5000:v0.33.3".to_owned();
631674
assert!(!mz.meets_minimum_version(&Version::parse("0.34.0").unwrap()));
632675
}
676+
677+
#[mz_ore::test]
678+
fn within_upgrade_window() {
679+
use super::v1alpha1::MaterializeStatus;
680+
681+
let mut mz = Materialize {
682+
spec: MaterializeSpec {
683+
environmentd_image_ref: "materialize/environmentd:v26.0.0".to_owned(),
684+
..Default::default()
685+
},
686+
metadata: ObjectMeta {
687+
..Default::default()
688+
},
689+
status: Some(MaterializeStatus {
690+
last_completed_rollout_environmentd_image_ref: Some(
691+
"materialize/environmentd:v26.0.0".to_owned(),
692+
),
693+
..Default::default()
694+
}),
695+
};
696+
697+
// Pass: upgrading from 26.0.0 to 27.7.3 (within 1 major version)
698+
mz.spec.environmentd_image_ref = "materialize/environmentd:v27.7.3".to_owned();
699+
assert!(mz.within_upgrade_window());
700+
701+
// Pass: upgrading from 26.0.0 to 27.7.8-dev.0 (within 1 major version, pre-release)
702+
mz.spec.environmentd_image_ref = "materialize/environmentd:v27.7.8-dev.0".to_owned();
703+
assert!(mz.within_upgrade_window());
704+
705+
// Fail: upgrading from 26.0.0 to 28.0.1 (more than 1 major version)
706+
mz.spec.environmentd_image_ref = "materialize/environmentd:v28.0.1".to_owned();
707+
assert!(!mz.within_upgrade_window());
708+
709+
// Pass: upgrading from 26.0.0 to 28.0.1.not_a_valid_version (invalid version, defaults to true)
710+
mz.spec.environmentd_image_ref =
711+
"materialize/environmentd:v28.0.1.not_a_valid_version".to_owned();
712+
assert!(mz.within_upgrade_window());
713+
714+
// Pass: upgrading from 0.147.5 to 26.1.0 (any version before 26.0.0 passes)
715+
mz.status
716+
.as_mut()
717+
.unwrap()
718+
.last_completed_rollout_environmentd_image_ref =
719+
Some("materialize/environmentd:v0.147.5".to_owned());
720+
mz.spec.environmentd_image_ref = "materialize/environmentd:v26.1.0".to_owned();
721+
assert!(mz.within_upgrade_window());
722+
}
633723
}

src/orchestratord/src/controller/materialize.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,9 @@ impl Context {
360360
MaterializeStatus {
361361
active_generation: desired_generation,
362362
last_completed_rollout_request: mz.requested_reconciliation_id(),
363+
last_completed_rollout_environmentd_image_ref: Some(
364+
mz.spec.environmentd_image_ref.clone(),
365+
),
363366
resource_id: mz.status().resource_id,
364367
resources_hash,
365368
conditions: vec![Condition {
@@ -591,7 +594,7 @@ impl k8s_controller::Context for Context {
591594
)
592595
.await
593596
}
594-
// There are changes pending, and we want to appy them.
597+
// There are changes pending, and we want to apply them.
595598
(false, true, true) => {
596599
// we remove the environment resources hash annotation here
597600
// because if we fail halfway through applying the resources,
@@ -615,6 +618,8 @@ impl k8s_controller::Context for Context {
615618
// we fail later on, we want to ensure that the
616619
// rollout gets retried.
617620
last_completed_rollout_request: status.last_completed_rollout_request,
621+
last_completed_rollout_environmentd_image_ref: status
622+
.last_completed_rollout_environmentd_image_ref,
618623
resource_id: status.resource_id,
619624
resources_hash: String::new(),
620625
conditions: vec![Condition {
@@ -634,6 +639,38 @@ impl k8s_controller::Context for Context {
634639
let mz = &mz;
635640
let status = mz.status();
636641

642+
if !mz.within_upgrade_window() {
643+
let last_completed_rollout_environmentd_image_ref =
644+
status.last_completed_rollout_environmentd_image_ref;
645+
646+
self.update_status(
647+
&mz_api,
648+
mz,
649+
MaterializeStatus {
650+
active_generation,
651+
last_completed_rollout_request: status.last_completed_rollout_request,
652+
last_completed_rollout_environmentd_image_ref: last_completed_rollout_environmentd_image_ref.clone(),
653+
resource_id: status.resource_id,
654+
resources_hash: status.resources_hash,
655+
conditions: vec![Condition {
656+
type_: "UpToDate".into(),
657+
status: "False".into(),
658+
last_transition_time: Time(chrono::offset::Utc::now()),
659+
message: format!(
660+
"Refusing to upgrade from {} to {}. More than one major version from last successful rollout.",
661+
last_completed_rollout_environmentd_image_ref.expect("should be set if upgrade window check fails"),
662+
&mz.spec.environmentd_image_ref,
663+
),
664+
observed_generation: mz.meta().generation,
665+
reason: "FailedDeploy".into(),
666+
}],
667+
},
668+
active_generation != desired_generation,
669+
)
670+
.await?;
671+
return Ok(None);
672+
}
673+
637674
if mz.spec.rollout_strategy
638675
== MaterializeRolloutStrategy::ImmediatelyPromoteCausingDowntime
639676
{
@@ -673,6 +710,8 @@ impl k8s_controller::Context for Context {
673710
// rollout gets retried.
674711
last_completed_rollout_request: status
675712
.last_completed_rollout_request,
713+
last_completed_rollout_environmentd_image_ref: status
714+
.last_completed_rollout_environmentd_image_ref,
676715
resource_id: status.resource_id,
677716
resources_hash: resources_hash.clone(),
678717
conditions: vec![Condition {
@@ -710,6 +749,8 @@ impl k8s_controller::Context for Context {
710749
// the rollout and we want to ensure it gets
711750
// retried.
712751
last_completed_rollout_request: status.last_completed_rollout_request,
752+
last_completed_rollout_environmentd_image_ref: status
753+
.last_completed_rollout_environmentd_image_ref,
713754
resource_id: status.resource_id,
714755
resources_hash: status.resources_hash,
715756
conditions: vec![Condition {
@@ -746,6 +787,8 @@ impl k8s_controller::Context for Context {
746787
MaterializeStatus {
747788
active_generation,
748789
last_completed_rollout_request: mz.requested_reconciliation_id(),
790+
last_completed_rollout_environmentd_image_ref: status
791+
.last_completed_rollout_environmentd_image_ref,
749792
resource_id: status.resource_id,
750793
resources_hash: status.resources_hash,
751794
conditions: vec![Condition {
@@ -786,6 +829,8 @@ impl k8s_controller::Context for Context {
786829
MaterializeStatus {
787830
active_generation,
788831
last_completed_rollout_request: mz.requested_reconciliation_id(),
832+
last_completed_rollout_environmentd_image_ref: status
833+
.last_completed_rollout_environmentd_image_ref,
789834
resource_id: status.resource_id,
790835
resources_hash: status.resources_hash,
791836
conditions: vec![Condition {

0 commit comments

Comments
 (0)