diff --git a/Release.toml b/Release.toml index 85d4359c85c..70de562b048 100644 --- a/Release.toml +++ b/Release.toml @@ -74,3 +74,6 @@ version = "1.3.0" "migrate_v1.3.0_hostname-affects-etc-hosts.lz4", "migrate_v1.3.0_control-container-v0-5-2.lz4", ] +"(1.3.0, 1.4.0)" = [ + "migrate_v1.4.0_registry-mirror-representation.lz4", +] diff --git a/sources/Cargo.lock b/sources/Cargo.lock index 8b659496afd..836d2d9e3c6 100644 --- a/sources/Cargo.lock +++ b/sources/Cargo.lock @@ -2477,6 +2477,14 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "registry-mirror-representation" +version = "0.1.0" +dependencies = [ + "migration-helpers", + "serde_json", +] + [[package]] name = "remove_dir_all" version = "0.5.3" diff --git a/sources/Cargo.toml b/sources/Cargo.toml index 2bd5c9c1e58..ef7398e58f0 100644 --- a/sources/Cargo.toml +++ b/sources/Cargo.toml @@ -28,6 +28,7 @@ members = [ "api/migration/migrations/v1.3.0/etc-hosts-service", "api/migration/migrations/v1.3.0/hostname-affects-etc-hosts", "api/migration/migrations/v1.3.0/control-container-v0-5-2", + "api/migration/migrations/v1.4.0/registry-mirror-representation", "bottlerocket-release", diff --git a/sources/api/migration/migrations/v1.4.0/registry-mirror-representation/Cargo.toml b/sources/api/migration/migrations/v1.4.0/registry-mirror-representation/Cargo.toml new file mode 100644 index 00000000000..3774b434f49 --- /dev/null +++ b/sources/api/migration/migrations/v1.4.0/registry-mirror-representation/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "registry-mirror-representation" +version = "0.1.0" +authors = ["Erikson Tung "] +license = "Apache-2.0 OR MIT" +edition = "2018" +publish = false +# Don't rebuild crate just because of changes to README. +exclude = ["README.md"] + +[dependencies] +migration-helpers = { path = "../../../migration-helpers", version = "0.1.0"} +serde_json = "1.0" diff --git a/sources/api/migration/migrations/v1.4.0/registry-mirror-representation/src/main.rs b/sources/api/migration/migrations/v1.4.0/registry-mirror-representation/src/main.rs new file mode 100644 index 00000000000..e9cb54ad814 --- /dev/null +++ b/sources/api/migration/migrations/v1.4.0/registry-mirror-representation/src/main.rs @@ -0,0 +1,133 @@ +#![deny(rust_2018_idioms)] + +use migration_helpers::{migrate, Migration, MigrationData, Result}; +use serde_json::{Map, Value}; +use std::collections::HashMap; +use std::process; + +const MIRRORS_SETTING_NAME: &'static str = "settings.container-registry.mirrors"; +const DATASTORE_KEY_SEPARATOR: char = '.'; + +/// This migration changes the model type of `settings.container-registry.mirrors` from `HashMap>` +/// to `Vec` on upgrade and vice-versa on downgrades. +pub struct ChangeRegistryMirrorsType; + +// Snapshot of the `datastore::Key::valid_character` method in Bottlerocket version 1.3.0 +// +// Determines whether a character is acceptable within a segment of a key name. This is +// separate from quoting; if a character isn't valid, it isn't valid quoted, either. +fn valid_character(c: char) -> bool { + match c { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '/' => true, + _ => false, + } +} + +impl Migration for ChangeRegistryMirrorsType { + /// Newer versions store `settings.container-registry.mirrors` as `Vec`. + /// Need to convert from `HashMap>`. + fn forward(&mut self, mut input: MigrationData) -> Result { + let mirrors: HashMap<_, _> = input + .data + .iter() + .filter(|&(k, _)| k.starts_with(format!("{}.", MIRRORS_SETTING_NAME).as_str())) + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .collect(); + let mut new_mirrors = Vec::new(); + for (setting, endpoint) in mirrors { + // Get the registry name from the settings name. Trim any quotes the settings name might have. + let registry = setting + .strip_prefix(&format!("{}.", MIRRORS_SETTING_NAME)) + .unwrap_or_default() + .trim_matches('"'); + let mut registry_mirrors = Map::new(); + registry_mirrors.insert("registry".to_string(), Value::String(registry.to_string())); + registry_mirrors.insert("endpoint".to_string(), endpoint.to_owned()); + new_mirrors.push(Value::Object(registry_mirrors)); + if let Some(data) = input.data.remove(&setting) { + println!("Removed setting '{}', which was set to '{}'", setting, data); + } + } + let data = Value::Array(new_mirrors); + println!( + "Creating new setting '{}', which is set to '{}'", + MIRRORS_SETTING_NAME, &data + ); + input.data.insert(MIRRORS_SETTING_NAME.to_string(), data); + Ok(input) + } + + /// Older versions store `settings.container-registry.mirrors` as `HashMap>`. + /// Need to convert from `Vec`. + fn backward(&mut self, mut input: MigrationData) -> Result { + if let Some(data) = input.data.get_mut(MIRRORS_SETTING_NAME).cloned() { + match data { + Value::Array(arr) => { + if let Some(data) = input.data.remove(MIRRORS_SETTING_NAME) { + println!( + "Removed setting '{}', which was set to '{}'", + MIRRORS_SETTING_NAME, data + ); + } + for obj in arr { + if let Some(obj) = obj.as_object() { + if let (Some(registry), Some(endpoint)) = ( + obj.get("registry").and_then(|s| s.as_str()), + obj.get("endpoint"), + ) { + // Ensure the registry contains valid datastore key characters. + // If we encounter any invalid key characters, we skip writing out + // the setting key to prevent breakage of the datastore. + if registry + .chars() + .all(|c| valid_character(c) || c == DATASTORE_KEY_SEPARATOR) + { + let setting_name = + format!(r#"{}."{}""#, MIRRORS_SETTING_NAME, registry); + println!( + "Creating new setting '{}', which is set to '{}'", + setting_name, &endpoint + ); + input.data.insert(setting_name, endpoint.to_owned()); + } else { + eprintln!( + "Container registry '{}' contains invalid datastore key character(s). Skipping to prevent datastore breakage...", + registry + ); + } + } + } else { + println!( + "'{}' contains non-JSON Object value: '{}'.", + MIRRORS_SETTING_NAME, obj + ); + } + } + } + _ => { + println!( + "'{}' is not a JSON Array value: '{}'.", + MIRRORS_SETTING_NAME, data + ); + } + } + } else { + println!("Didn't find setting '{}'", MIRRORS_SETTING_NAME); + } + Ok(input) + } +} + +fn run() -> Result<()> { + migrate(ChangeRegistryMirrorsType) +} + +// Returning a Result from main makes it print a Debug representation of the error, but with Snafu +// we have nice Display representations of the error, so we wrap "main" (run) and print any error. +// https://github.com/shepmaster/snafu/issues/110 +fn main() { + if let Err(e) = run() { + eprintln!("{}", e); + process::exit(1); + } +}