diff --git a/sources/updater/README.md b/sources/updater/README.md index cf0d421dc02..be86e99f625 100644 --- a/sources/updater/README.md +++ b/sources/updater/README.md @@ -41,8 +41,8 @@ Updog will ensure that appropriate migration files are available to safely trans ### Update wave Updates may include "wave" information which provides a way for updates to be scheduled over time for groups of Bottlerocket hosts. -Updog will find the update wave the host belongs to and jitter its update time within that range. -If the calculated time has not passed yet, Updog returns the update timestamp to the caller so it can be called again at the correct time. +Updog will find the update wave the host belongs to and calculate its time position within the wave based on its `settings.updates.seed` value. +If the calculated time has not passed, Updog will not report an update as being available. Assuming all the requirements are met, Updog requests the update images from the TUF repository and writes them to the "inactive" partition. diff --git a/sources/updater/update_metadata/src/lib.rs b/sources/updater/update_metadata/src/lib.rs index 28db2c06988..a3a4dea1c21 100644 --- a/sources/updater/update_metadata/src/lib.rs +++ b/sources/updater/update_metadata/src/lib.rs @@ -5,9 +5,8 @@ pub mod error; mod se; use crate::error::Result; -use chrono::{DateTime, Duration, Utc}; +use chrono::{DateTime, Utc}; use parse_datetime::parse_offset; -use rand::{thread_rng, Rng}; use semver::Version; use serde::{Deserialize, Serialize}; use snafu::{ensure, OptionExt, ResultExt}; @@ -23,30 +22,34 @@ pub const MAX_SEED: u32 = 2048; #[derive(Debug, PartialEq, Eq)] pub enum Wave { Initial { - end: DateTime, + end_time: DateTime, + end_seed: u32, }, General { - start: DateTime, - end: DateTime, + start_time: DateTime, + end_time: DateTime, + start_seed: u32, + end_seed: u32, }, Last { - start: DateTime, + start_time: DateTime, + start_seed: u32, }, } impl Wave { - pub fn has_started(&self) -> bool { + pub fn has_started(&self, time: DateTime) -> bool { match self { Self::Initial { .. } => true, - Self::General { start, .. } | Self::Last { start } => *start <= Utc::now(), + Self::General { start_time, .. } | Self::Last { start_time, .. } => *start_time <= time, } } - pub fn has_passed(&self) -> bool { + pub fn has_passed(&self, time: DateTime) -> bool { match self { - Self::Initial { end } => *end <= Utc::now(), - Self::General { end, .. } => *end <= Utc::now(), - Self::Last { start } => *start <= Utc::now(), + Self::Initial { end_time, .. } => *end_time <= time, + Self::General { end_time, .. } => *end_time <= time, + Self::Last { start_time, .. } => *start_time <= time, } } } @@ -295,58 +298,92 @@ impl Manifest { impl Update { /// Returns the update wave that Updog belongs to, based on the seed value. /// Depending on the waves described in the update, the possible results are - /// - Some wave described by a start and end time. - /// - The "0th" wave, which has an "end" time but no specified start time. - /// - The last wave, which has a start time but no specified end time. + /// - Some wave described by a start and end time, and the starting seed and ending seed. + /// - The "0th" wave, which has an "end" time but no specified start time, and the ending seed. + /// - The last wave, which has a start time but no specified end time, and the starting seed. /// - Nothing, if no waves are configured. + #[must_use] pub fn update_wave(&self, seed: u32) -> Option { - let start = self + let start_wave = self .waves .range((Included(0), Excluded(seed))) - .last() - .map(|(_, wave)| *wave); - let end = self + .map(|(k, v)| (*k, *v)) + .last(); + let end_wave = self .waves .range((Included(seed), Included(MAX_SEED))) - .next() - .map(|(_, wave)| *wave); - - match (start, end) { - (None, Some(end)) => Some(Wave::Initial { end }), - (Some(start), Some(end)) => Some(Wave::General { start, end }), - (Some(start), None) => Some(Wave::Last { start }), + .map(|(k, v)| (*k, *v)) + .next(); + + match (start_wave, end_wave) { + // Note that the key for each wave entry is the starting seed for that wave, the value is the DateTime + (None, Some((end_seed, end_time))) => Some(Wave::Initial { end_seed, end_time }), + (Some((start_seed, start_time)), Some((end_seed, end_time))) => Some(Wave::General { + start_time, + end_time, + start_seed, + end_seed, + }), + (Some((start_seed, start_time)), None) => Some(Wave::Last { + start_time, + start_seed, + }), _ => None, } } - pub fn update_ready(&self, seed: u32) -> bool { - // Has this client's wave started - if let Some(wave) = self.update_wave(seed) { - return wave.has_started(); - } - - // Or there are no waves - true - } - - pub fn jitter(&self, seed: u32) -> Option> { + /// Returns whether the update is available. An update is said to be 'ready/available' if the wave + /// this host belongs to has fully passed, or if the host's position in the wave has passed, or + /// if there are no waves. + /// The position of the host within the wave is determined by the seed value. + #[must_use] + pub fn update_ready(&self, seed: u32, time: DateTime) -> bool { + // If this host is part of some update wave if let Some(wave) = self.update_wave(seed) { - if wave.has_passed() { - return None; + // If the wave has passed, the update is available (this includes passing the last wave start time) + if wave.has_passed(time) { + return true; + } else if !wave.has_started(time) { + return false; } - let bounds = match self.update_wave(seed) { - Some(Wave::Initial { end }) => Some((Utc::now(), end)), - Some(Wave::General { start, end }) => Some((start, end)), - Some(Wave::Last { start: _ }) | None => None, + let bound = match wave { + // Hosts should not wind up in the special "initial" wave with no start time, but if they do, + // we consider the update as being available immediately. + Wave::Initial { .. } => None, + Wave::General { + start_time, + end_time, + start_seed, + end_seed, + } => Some((start_time, Some(end_time), start_seed, end_seed)), + // Last wave has no end time nor end seed; Let end seed be `MAX_SEED` since all the + // remaining hosts are in this last wave + Wave::Last { + start_time, + start_seed, + } => Some((start_time, None, start_seed, MAX_SEED)), }; - if let Some((start, end)) = bounds { - let mut rng = thread_rng(); - if let Some(range) = end.timestamp().checked_sub(start.timestamp()) { - return Some(start + Duration::seconds(rng.gen_range(1, range))); + if let Some((start_time, maybe_end_time, start_seed, end_seed)) = bound { + if let Some(end_time) = maybe_end_time { + // This host is not part of last wave + // Determine the duration of this host's wave + let wave_duration = end_time - start_time; + let num_seeds_allocated_to_wave = (end_seed - start_seed) as i32; + if num_seeds_allocated_to_wave == 0 { + // Empty wave, no host should have been allocated to it + return true; + } + let time_per_seed = wave_duration / num_seeds_allocated_to_wave; + // Derive the target time position within the wave given the host's seed. + let target_time = start_time + (time_per_seed * (seed as i32)); + // If the current time is past the target time position in the wave, the update is + // marked available + return time >= target_time; } } } - None + // There are no waves, so we consider the update available + true } } @@ -418,35 +455,245 @@ pub fn load_manifest(repository: &tough::Repository) -> .context(error::ManifestParse) } -#[test] -fn test_migrations_forward() { - // A manifest with four migration tuples starting at 1.0 and ending at 1.3. - // There is a shortcut from 1.1 to 1.3, skipping 1.2 - let path = "./tests/data/migrations.json"; - let manifest: Manifest = serde_json::from_reader(File::open(path).unwrap()).unwrap(); - let from = Version::parse("1.0.0").unwrap(); - let to = Version::parse("1.5.0").unwrap(); - let targets = find_migrations(&from, &to, &manifest).unwrap(); - - assert!(targets.len() == 3); - let mut i = targets.iter(); - assert!(i.next().unwrap() == "migration_1.1.0_a"); - assert!(i.next().unwrap() == "migration_1.1.0_b"); - assert!(i.next().unwrap() == "migration_1.5.0_shortcut"); -} +#[cfg(test)] +mod tests { + use super::*; + use chrono::{DateTime, Duration, NaiveDate, Utc}; + + fn test_time() -> DateTime { + // DateTime for 1/1/2000 00:00:00 + DateTime::::from_utc( + NaiveDate::from_ymd(2000, 1, 1).and_hms_milli(0, 0, 0, 0), + Utc, + ) + } + + fn test_update() -> Update { + Update { + variant: "bottlerocket".to_string(), + arch: "test".to_string(), + version: Version::parse("1.1.1").unwrap(), + max_version: Version::parse("1.1.1").unwrap(), + waves: BTreeMap::new(), + images: Images { + boot: String::from("boot"), + root: String::from("root"), + hash: String::from("hash"), + }, + } + } + + #[test] + fn test_update_ready_no_wave() { + let time = test_time(); + let seed = 100; + let update = test_update(); + assert!( + update.update_ready(seed, time), + "no waves specified, update should be ready" + ); + } + + #[test] + fn test_update_ready_single_wave() { + let time = test_time(); + let mut update = test_update(); + // One single wave (0th wave does not count) for every update that spans over 2048 millisecond, + // Each seed will be mapped to a single millisecond within this wave, + // e.g. seed 1 -> update is ready 1 millisecond past start of wave + // seed 500 -> update is ready 500 millisecond past start of wave, etc + update.waves.insert(0, time); + update + .waves + .insert(MAX_SEED, time + Duration::milliseconds(MAX_SEED as i64)); + + for seed in (100..500).step_by(100) { + assert!( + !update.update_ready(seed, time + Duration::milliseconds((seed as i64) - 1)), + "seed: {}, time: {}, wave start time: {}, wave start seed: {}, {} milliseconds hasn't passed yet", + seed, + time, + time, + 0, + seed + ); + assert!( + update.update_ready(seed, time + Duration::milliseconds(seed as i64)), + "seed: {}, time: {}, wave start time: {}, wave start seed: {}, update should be ready", + seed, + time + Duration::milliseconds(100), + time, + 0, + ); + } + } + + fn add_test_waves(update: &mut Update) { + let time = test_time(); + update.waves.insert(0, time); + // First wave ends 200 milliseconds into the update and has seeds 0 - 50 + update.waves.insert(50, time + Duration::milliseconds(200)); + // Second wave ends 1024 milliseconds into the update and has seeds 50 - 100 + update + .waves + .insert(100, time + Duration::milliseconds(1024)); + // Third wave ends 4096 milliseconds into the update and has seeds 100 - 1024 + update + .waves + .insert(1024, time + Duration::milliseconds(4096)); + } + + #[test] + fn test_update_ready_second_wave() { + let time = test_time(); + let mut update = test_update(); + add_test_waves(&mut update); + // Now we should be in the second wave + let seed = 60; + + for duration in (0..200).step_by(10) { + assert!( + !update.update_ready(seed, time + Duration::milliseconds(duration)), + "seed should not part of first wave", + ); + } -#[test] -fn test_migrations_backward() { - // The same manifest as `test_migrations_forward` but this time we will migrate backward. - let path = "./tests/data/migrations.json"; - let manifest: Manifest = serde_json::from_reader(File::open(path).unwrap()).unwrap(); - let from = Version::parse("1.5.0").unwrap(); - let to = Version::parse("1.0.0").unwrap(); - let targets = find_migrations(&from, &to, &manifest).unwrap(); - - assert!(targets.len() == 3); - let mut i = targets.iter(); - assert!(i.next().unwrap() == "migration_1.5.0_shortcut"); - assert!(i.next().unwrap() == "migration_1.1.0_b"); - assert!(i.next().unwrap() == "migration_1.1.0_a"); + let seed_time_position = (1024 - 200) / (100 - 50) * seed; + for duration in (200..seed_time_position).step_by(2) { + assert!( + !update.update_ready( + seed, time + Duration::milliseconds(duration as i64) + ), + "update should not be ready, it's the second wave but not at position within wave yet: {}", duration, + ); + } + + for duration in (seed_time_position..1024).step_by(4) { + assert!( + update.update_ready( + seed, + time + Duration::milliseconds(200) + + Duration::milliseconds(duration as i64) + ), + "update should be ready now that we're passed the allocated time position within the second wave: {}", duration, + ); + } + + for duration in (1024..4096).step_by(8) { + assert!( + update.update_ready(seed, time + Duration::milliseconds(duration as i64)), + "update should be ready after the third wave starts and onwards", + ); + } + } + + #[test] + fn test_update_ready_third_wave() { + let time = test_time(); + let mut update = test_update(); + add_test_waves(&mut update); + let seed = 148; + + for duration in (0..200).step_by(10) { + assert!( + !update.update_ready(seed, time + Duration::milliseconds(duration)), + "seed should not part of first wave", + ); + } + + for duration in (200..1024).step_by(4) { + assert!( + !update.update_ready(seed, time + Duration::milliseconds(duration)), + "seed should not part of second wave", + ); + } + + let seed_time_position = (4096 - 1024) / (1024 - 100) * seed; + for duration in (1024..seed_time_position).step_by(4) { + assert!( + !update.update_ready( + seed, + time + Duration::milliseconds(200) + + Duration::milliseconds(duration as i64) + ), + "update should not be ready, it's the third wave but not at position within wave yet: {}", duration, + ); + } + + for duration in (seed_time_position..4096).step_by(4) { + assert!( + update.update_ready( + seed, + time + Duration::milliseconds(1024 + 200) + + Duration::milliseconds(duration as i64) + ), + "update should be ready now that we're passed the allocated time position within the third wave: {}", duration, + ); + } + } + + #[test] + fn test_update_ready_final_wave() { + let mut update = Update { + variant: String::from("bottlerocket"), + arch: String::from("test"), + version: Version::parse("1.0.0").unwrap(), + max_version: Version::parse("1.1.0").unwrap(), + waves: BTreeMap::new(), + images: Images { + boot: String::from("boot"), + root: String::from("root"), + hash: String::from("hash"), + }, + }; + let seed = 1024; + // Construct a DateTime object for 1/1/2000 00:00:00 + let time = DateTime::::from_utc( + NaiveDate::from_ymd(2000, 1, 1).and_hms_milli(0, 0, 0, 0), + Utc, + ); + + update.waves.insert(0, time - Duration::hours(3)); + update.waves.insert(256, time - Duration::hours(2)); + update.waves.insert(512, time - Duration::hours(1)); + + assert!( + // Last wave should have already passed + update.update_ready(seed, time), + "update should be ready" + ); + } + + #[test] + fn test_migrations_forward() { + // A manifest with four migration tuples starting at 1.0 and ending at 1.3. + // There is a shortcut from 1.1 to 1.3, skipping 1.2 + let path = "./tests/data/migrations.json"; + let manifest: Manifest = serde_json::from_reader(File::open(path).unwrap()).unwrap(); + let from = Version::parse("1.0.0").unwrap(); + let to = Version::parse("1.5.0").unwrap(); + let targets = find_migrations(&from, &to, &manifest).unwrap(); + + assert!(targets.len() == 3); + let mut i = targets.iter(); + assert!(i.next().unwrap() == "migration_1.1.0_a"); + assert!(i.next().unwrap() == "migration_1.1.0_b"); + assert!(i.next().unwrap() == "migration_1.5.0_shortcut"); + } + + #[test] + fn test_migrations_backward() { + // The same manifest as `test_migrations_forward` but this time we will migrate backward. + let path = "./tests/data/migrations.json"; + let manifest: Manifest = serde_json::from_reader(File::open(path).unwrap()).unwrap(); + let from = Version::parse("1.5.0").unwrap(); + let to = Version::parse("1.0.0").unwrap(); + let targets = find_migrations(&from, &to, &manifest).unwrap(); + + assert!(targets.len() == 3); + let mut i = targets.iter(); + assert!(i.next().unwrap() == "migration_1.5.0_shortcut"); + assert!(i.next().unwrap() == "migration_1.1.0_b"); + assert!(i.next().unwrap() == "migration_1.1.0_a"); + } } diff --git a/sources/updater/updog/src/main.rs b/sources/updater/updog/src/main.rs index 3b77fa5a532..e6713aec502 100644 --- a/sources/updater/updog/src/main.rs +++ b/sources/updater/updog/src/main.rs @@ -7,18 +7,17 @@ mod transport; use crate::error::Result; use crate::transport::{HttpQueryRepo, HttpQueryTransport}; use bottlerocket_release::BottlerocketRelease; -use chrono::{DateTime, Utc}; +use chrono::Utc; use model::modeled_types::FriendlyVersion; use semver::Version; use serde::{Deserialize, Serialize}; use signal_hook::{iterator::Signals, SIGTERM}; use signpost::State; use simplelog::{Config as LogConfig, LevelFilter, TermLogger, TerminalMode}; -use snafu::{ensure, ErrorCompat, OptionExt, ResultExt}; +use snafu::{ErrorCompat, OptionExt, ResultExt}; use std::convert::{TryFrom, TryInto}; -use std::fs::{self, File, OpenOptions, Permissions}; +use std::fs::{self, File, OpenOptions}; use std::io; -use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::process; use std::str::FromStr; @@ -79,7 +78,7 @@ USAGE: SUBCOMMANDS: check-update Show if an update is available - [ -a | --all ] Output all applicable updates + [ -a | --all ] Output all available updates, even if they're not upgrades [ --ignore-waves ] Ignore release schedule when checking for a new update @@ -89,7 +88,6 @@ SUBCOMMANDS: [ -i | --image version ] Update to a specfic image version [ -n | --now ] Update immediately, ignoring any release schedule [ -r | --reboot ] Reboot into new update on success - [ -t | --timestamp time ] The timestamp from which to execute an update update-image Download & write an update but do not update flags [ -i | --image version ] Update to a specfic image version @@ -138,11 +136,21 @@ fn load_repository<'a>( .context(error::Metadata) } -fn applicable_updates<'a>(manifest: &'a Manifest, variant: &str) -> Vec<&'a Update> { +fn applicable_updates<'a>( + manifest: &'a Manifest, + variant: &str, + ignore_waves: bool, + seed: u32, +) -> Vec<&'a Update> { let mut updates: Vec<&Update> = manifest .updates .iter() - .filter(|u| u.variant == *variant && u.arch == TARGET_ARCH && u.version <= u.max_version) + .filter(|u| { + u.variant == *variant + && u.arch == TARGET_ARCH + && u.version <= u.max_version + && (ignore_waves || u.update_ready(seed, Utc::now())) + }) .collect(); // sort descending updates.sort_unstable_by(|a, b| b.version.cmp(&a.version)); @@ -156,29 +164,32 @@ fn applicable_updates<'a>(manifest: &'a Manifest, variant: &str) -> Vec<&'a Upda // Ignore Any Target // ... fn update_required<'a>( - config: &Config, manifest: &'a Manifest, version: &Version, variant: &str, + ignore_waves: bool, + seed: u32, + version_lock: &str, force_version: Option, ) -> Result> { - let updates = applicable_updates(manifest, variant); + let updates = applicable_updates(manifest, variant, ignore_waves, seed); if let Some(forced_version) = force_version { return Ok(updates.into_iter().find(|u| u.version == forced_version)); } - if config.version_lock != "latest" { + if version_lock != "latest" { // Make sure the version string from the config is a valid version string that might be prefixed with 'v' - let version_lock = FriendlyVersion::try_from(config.version_lock.as_str()).context( - error::BadVersionConfig { - version_str: config.version_lock.to_owned(), - }, - )?; + let friendly_version_lock = + FriendlyVersion::try_from(version_lock).context(error::BadVersionConfig { + version_str: version_lock, + })?; // Convert back to semver::Version - let semver_version_lock = version_lock.try_into().with_context(|| error::BadVersion { - version_str: config.version_lock.to_owned(), - })?; + let semver_version_lock = friendly_version_lock + .try_into() + .context(error::BadVersion { + version_str: version_lock, + })?; // If the configured version-lock matches our current version, we won't update to the same version return if semver_version_lock == *version { Ok(None) @@ -311,9 +322,15 @@ fn set_common_query_params( Ok(()) } -/// List any available update that matches the current variant, ignoring waves -fn list_updates(manifest: &Manifest, variant: &str, json: bool) -> Result<()> { - let updates = applicable_updates(manifest, variant); +/// List any available update that matches the current variant +fn list_updates( + manifest: &Manifest, + variant: &str, + json: bool, + ignore_waves: bool, + seed: u32, +) -> Result<()> { + let updates = applicable_updates(manifest, variant, ignore_waves, seed); if json { println!( "{}", @@ -336,7 +353,6 @@ struct Arguments { force_version: Option, all: bool, reboot: bool, - timestamp: Option>, variant: Option, } @@ -349,7 +365,6 @@ fn parse_args(args: std::env::Args) -> Arguments { let mut json = false; let mut all = false; let mut reboot = false; - let mut timestamp = None; let mut variant = None; let mut iter = args.skip(1); @@ -379,13 +394,6 @@ fn parse_args(args: std::env::Args) -> Arguments { "-n" | "--now" | "--ignore-waves" => { ignore_waves = true; } - "-t" | "--timestamp" => match iter.next() { - Some(t) => match DateTime::parse_from_rfc3339(&t) { - Ok(t) => timestamp = Some(DateTime::from_utc(t.naive_utc(), Utc)), - _ => usage(), - }, - _ => usage(), - }, "-j" | "--json" => { json = true; } @@ -414,7 +422,6 @@ fn parse_args(args: std::env::Args) -> Arguments { force_version: update_version, all, reboot, - timestamp, variant, } } @@ -486,90 +493,64 @@ fn main_inner() -> Result<()> { match command { Command::CheckUpdate | Command::Whats => { if arguments.all { - return list_updates(&manifest, &variant, arguments.json); + return list_updates( + &manifest, + &variant, + arguments.json, + ignore_waves, + config.seed, + ); } let update = update_required( - &config, &manifest, ¤t_release.version_id, &variant, + ignore_waves, + config.seed, + &config.version_lock, arguments.force_version, )? .context(error::UpdateNotAvailable)?; - if !ignore_waves { - ensure!( - update.update_ready(config.seed), - error::UpdateNotReady { - version: update.version.clone() - } - ); - } output(arguments.json, &update, &fmt_full_version(&update))?; } Command::Update | Command::UpdateImage => { if let Some(u) = update_required( - &config, &manifest, ¤t_release.version_id, &variant, + ignore_waves, + config.seed, + &config.version_lock, arguments.force_version, )? { - if ignore_waves || u.update_ready(config.seed) { - eprintln!("Starting update to {}", u.version); - - if ignore_waves { - eprintln!("** Updating immediately **"); - } else { - let jitter = match arguments.timestamp { - Some(t) => Some(t), - _ => u.jitter(config.seed), - }; - - if let Some(j) = jitter { - if j > Utc::now() { - // not yet! - output(arguments.json, &j, &format!("{}", j))?; - return Ok(()); - } - } + eprintln!("Starting update to {}", u.version); + + transport + .queries_get_mut() + .context(error::TransportBorrow)? + .push((String::from("target"), u.version.to_string())); + + retrieve_migrations( + &repository, + &transport, + &manifest, + u, + ¤t_release.version_id, + )?; + update_image(u, &repository)?; + if command == Command::Update { + update_flags()?; + if arguments.reboot { + initiate_reboot()?; } - - transport - .queries_get_mut() - .context(error::TransportBorrow)? - .push((String::from("target"), u.version.to_string())); - - retrieve_migrations( - &repository, - &transport, - &manifest, - u, - ¤t_release.version_id, - )?; - update_image(u, &repository)?; - if command == Command::Update { - update_flags()?; - if arguments.reboot { - initiate_reboot()?; - } - } - output( - arguments.json, - &u, - &format!("Update applied: {}", fmt_full_version(&u)), - )?; - } else if let Some(wave) = u.jitter(config.seed) { - // return the jittered time of our wave in the update - output( - arguments.json, - &wave, - &format!("Update available at {}", &wave), - )?; - } else { - eprintln!("Update available in later wave"); } + output( + arguments.json, + &u, + &format!("Update applied: {}", fmt_full_version(&u)), + )?; } else { eprintln!("No update required"); } @@ -613,7 +594,7 @@ mod tests { use super::*; use chrono::Duration as TestDuration; use std::collections::BTreeMap; - use update_metadata::{Images, Wave}; + use update_metadata::Images; #[test] fn test_manifest_json() { @@ -649,74 +630,6 @@ mod tests { assert!(manifest.updates.len() > 0); } - #[test] - fn test_update_ready() { - let mut update = Update { - variant: String::from("bottlerocket"), - arch: String::from("test"), - version: Version::parse("1.0.0").unwrap(), - max_version: Version::parse("1.1.0").unwrap(), - waves: BTreeMap::new(), - images: Images { - boot: String::from("boot"), - root: String::from("root"), - hash: String::from("hash"), - }, - }; - - let seed = 123; - assert!( - update.update_ready(seed), - "No waves specified but no update" - ); - - update - .waves - .insert(1024, Utc::now() + TestDuration::hours(1)); - - assert!(update.update_ready(seed), "0th wave not ready"); - - update - .waves - .insert(100, Utc::now() + TestDuration::minutes(30)); - - assert!(!update.update_ready(seed), "1st wave scheduled early"); - - let early_seed = 50; - update - .waves - .insert(49, Utc::now() - TestDuration::minutes(30)); - - assert!(update.update_ready(early_seed), "Update wave missed"); - } - - #[test] - fn test_final_wave() { - let mut update = Update { - variant: String::from("bottlerocket"), - arch: String::from("test"), - version: Version::parse("1.0.0").unwrap(), - max_version: Version::parse("1.1.0").unwrap(), - waves: BTreeMap::new(), - images: Images { - boot: String::from("boot"), - root: String::from("root"), - hash: String::from("hash"), - }, - }; - let seed = 1024; - - update.waves.insert(0, Utc::now() - TestDuration::hours(3)); - update - .waves - .insert(256, Utc::now() - TestDuration::hours(2)); - update - .waves - .insert(512, Utc::now() - TestDuration::hours(1)); - - assert!(update.update_ready(seed), "All waves passed but no update"); - } - #[test] fn test_versions() { // A manifest with a single update whose version exceeds the max version. @@ -736,9 +649,17 @@ mod tests { let variant = String::from("bottlerocket-aws-eks"); assert!( - update_required(&config, &manifest, &version, &variant, None) - .unwrap() - .is_none(), + update_required( + &manifest, + &version, + &variant, + config.ignore_waves, + config.seed, + &config.version_lock, + None + ) + .unwrap() + .is_none(), "Updog tried to exceed max_version" ); } @@ -758,7 +679,16 @@ mod tests { let version = Version::parse("0.1.3").unwrap(); let variant = String::from("aws-k8s-1.15"); - let update = update_required(&config, &manifest, &version, &variant, None).unwrap(); + let update = update_required( + &manifest, + &version, + &variant, + config.ignore_waves, + config.seed, + &config.version_lock, + None, + ) + .unwrap(); assert!(update.is_some(), "Updog ignored max version"); assert!( @@ -785,7 +715,16 @@ mod tests { let version = Version::parse("1.10.0").unwrap(); let variant = String::from("bottlerocket-aws-eks"); - let result = update_required(&config, &manifest, &version, &variant, None).unwrap(); + let result = update_required( + &manifest, + &version, + &variant, + config.ignore_waves, + config.seed, + &config.version_lock, + None, + ) + .unwrap(); assert!(result.is_some(), "Updog failed to find an update"); @@ -817,7 +756,16 @@ mod tests { let version = Version::parse("1.10.0").unwrap(); let forced = Version::parse("1.13.0").unwrap(); let variant = String::from("bottlerocket-aws-eks"); - let result = update_required(&config, &manifest, &version, &variant, Some(forced)).unwrap(); + let result = update_required( + &manifest, + &version, + &variant, + config.ignore_waves, + config.seed, + &config.version_lock, + Some(forced), + ) + .unwrap(); assert!(result.is_some(), "Updog failed to find an update"); @@ -858,74 +806,7 @@ mod tests { } #[test] - fn early_wave() { - let mut u = Update { - variant: String::from("bottlerocket"), - arch: String::from("test"), - version: Version::parse("1.0.0").unwrap(), - max_version: Version::parse("1.1.0").unwrap(), - waves: BTreeMap::new(), - images: Images { - boot: String::from("boot"), - root: String::from("root"), - hash: String::from("hash"), - }, - }; - - // | ---- (100, "now") --- - let first_bound = Utc::now(); - u.waves.insert(100, first_bound); - assert!( - u.update_wave(1).unwrap() == Wave::Initial { end: first_bound }, - "Expected to be 0th wave" - ); - assert!(u.jitter(1).is_none(), "Expected immediate update"); - assert!( - u.update_wave(101).unwrap() == Wave::Last { start: first_bound }, - "Expected to be final wave" - ); - assert!(u.jitter(101).is_none(), "Expected immediate update"); - - // | ---- (100, "now") ---- (200, "+1hr") --- - let second_bound = Utc::now() + TestDuration::hours(1); - u.waves.insert(200, second_bound); - assert!( - u.update_wave(1).unwrap() == Wave::Initial { end: first_bound }, - "Expected to be 0th wave" - ); - assert!(u.jitter(1).is_none(), "Expected immediate update"); - - assert!( - u.update_wave(100).unwrap() == Wave::Initial { end: first_bound }, - "Expected to be 0th wave (just!)" - ); - assert!(u.jitter(100).is_none(), "Expected immediate update"); - - assert!( - u.update_wave(150).unwrap() - == Wave::General { - start: first_bound, - end: second_bound, - }, - "Expected to be some bounded wave" - ); - assert!( - u.jitter(150).is_some(), - "Expected to have to wait for update" - ); - - assert!( - u.update_wave(201).unwrap() - == Wave::Last { - start: second_bound - }, - "Expected to be final wave" - ); - assert!(u.jitter(201).is_none(), "Expected immediate update"); - } - - #[test] - /// Make sure that update_ready() doesn't return true unless the client's + /// Make sure that update_required() doesn't return true unless the client's /// wave is also ready. fn check_update_waves() { let mut manifest = Manifest::default(); @@ -944,31 +825,49 @@ mod tests { let current_version = Version::parse("1.0.0").unwrap(); let variant = String::from("aws-k8s-1.15"); + let first_wave_seed = 0; let config = Config { metadata_base_url: String::from("foo"), targets_base_url: String::from("bar"), - seed: 512, + seed: first_wave_seed, version_lock: "latest".to_string(), ignore_waves: false, }; - // Two waves; the 0th wave, and the final wave which starts in one hour - update - .waves - .insert(1024, Utc::now() + TestDuration::hours(1)); + // Two waves; the 1st wave that starts immediately, and the final wave which starts in one hour + let time = Utc::now(); + update.waves.insert(0, time); + update.waves.insert(1024, time + TestDuration::hours(1)); + update.waves.insert(2048, time + TestDuration::hours(1)); manifest.updates.push(update); - let potential_update = - update_required(&config, &manifest, ¤t_version, &variant, None) - .unwrap() - .unwrap(); - assert!( - potential_update.update_ready(512), - "0th wave doesn't appear ready" + update_required( + &manifest, + ¤t_version, + &variant, + config.ignore_waves, + config.seed, + &config.version_lock, + None, + ) + .unwrap() + .is_some(), + "1st wave doesn't appear ready" ); + assert!( - !potential_update.update_ready(2000), + update_required( + &manifest, + ¤t_version, + &variant, + config.ignore_waves, + 2000, + &config.version_lock, + None, + ) + .unwrap() + .is_none(), "Later wave incorrectly sees update" ); } diff --git a/sources/updater/waves/accelerated-waves.toml b/sources/updater/waves/accelerated-waves.toml index e3c00b5b35b..c98c672ba2a 100644 --- a/sources/updater/waves/accelerated-waves.toml +++ b/sources/updater/waves/accelerated-waves.toml @@ -11,8 +11,14 @@ fleet_percentage = 12 [[waves]] start_after = '8 hours' -fleet_percentage = 50 +fleet_percentage = 40 +[[waves]] +start_after = '16 hours' +fleet_percentage = 80 + +# Last 20 percent of the hosts will update immediately after 24 hours since the start of +# deployment. Unlike the other waves, there will be no velocity control. [[waves]] start_after = '1 day' fleet_percentage = 100 diff --git a/sources/updater/waves/default-waves.toml b/sources/updater/waves/default-waves.toml index 06e02a124f4..36ce3b9e4c6 100644 --- a/sources/updater/waves/default-waves.toml +++ b/sources/updater/waves/default-waves.toml @@ -11,12 +11,22 @@ fleet_percentage = 5 [[waves]] start_after = '1 day' -fleet_percentage = 10 +fleet_percentage = 15 [[waves]] -start_after = '3 days' -fleet_percentage = 25 +start_after = '2 days' +fleet_percentage = 40 +[[waves]] +start_after = '4 days' +fleet_percentage = 60 + +[[waves]] +start_after = '5 days' +fleet_percentage = 90 + +# Last 10 percent of the hosts will update immediately after 6 days since the start of +# deployment. Unlike the other waves, there will be no velocity control. [[waves]] start_after = '6 days' fleet_percentage = 100 diff --git a/sources/updater/waves/ohno.toml b/sources/updater/waves/ohno.toml index 7ffdc5c9612..e34ba9b98d5 100644 --- a/sources/updater/waves/ohno.toml +++ b/sources/updater/waves/ohno.toml @@ -1,14 +1,23 @@ # The following represents an "emergency" set of update waves for a rapid -# deployment. The deployment lasts for 3 hours, with a small initial wave, -# and then all nodes will be updated after the first hour. +# deployment. The deployment lasts for 3 hours, with a small initial wave. [[waves]] -start_after = '1 hour' +start_after = '30 minutes' fleet_percentage = 5 [[waves]] -start_after = '2 hours' +start_after = '60 minutes' fleet_percentage = 25 [[waves]] -start_after = '3 hours' +start_after = '120 minutes' +fleet_percentage = 50 + +[[waves]] +start_after = '150 minutes' +fleet_percentage = 90 + +# Last 10 percent of the hosts will update immediately after 3 hours since the start of +# deployment. Unlike the other waves, there will be no velocity control. +[[waves]] +start_after = '180 minutes' fleet_percentage = 100