Skip to content

Commit

Permalink
Add migrations necessary for kernel lockdown feature
Browse files Browse the repository at this point in the history
  • Loading branch information
tjkirch committed Nov 30, 2020
1 parent bfade20 commit 821342a
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 0 deletions.
1 change: 1 addition & 0 deletions Release.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ version = "1.0.4"
"(1.0.1, 1.0.2)" = ["migrate_v1.0.2_add-enable-spot-instance-draining.lz4"]
"(1.0.2, 1.0.3)" = ["migrate_v1.0.3_add-sysctl.lz4"]
"(1.0.3, 1.0.4)" = []
"(1.0.4, 1.0.5)" = ["migrate_v1.0.5_add-lockdown.lz4", "migrate_v1.0.5_sysctl-subcommand.lz4"]
14 changes: 14 additions & 0 deletions sources/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions sources/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ members = [
"api/migration/migrations/v1.0.0/ecr-helper-control",
"api/migration/migrations/v1.0.2/add-enable-spot-instance-draining",
"api/migration/migrations/v1.0.3/add-sysctl",
"api/migration/migrations/v1.0.5/add-lockdown",
"api/migration/migrations/v1.0.5/sysctl-subcommand",

"bottlerocket-release",

Expand Down
236 changes: 236 additions & 0 deletions sources/api/migration/migration-helpers/src/common_migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,242 @@ impl Migration for ReplaceStringMigration {

// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=

/// We use this migration when we replace a setting that contains a list of string values.
// String is the only type we use today, and handling multiple value types is more complicated than
// we need at the moment. Allowing &[serde_json::Value] seems nice, but it would allow arbitrary
// data transformations that the API model would then fail to load.
pub struct ReplaceListMigration {
pub setting: &'static str,
pub old_vals: &'static [&'static str],
pub new_vals: &'static [&'static str],
}

impl Migration for ReplaceListMigration {
fn forward(&mut self, mut input: MigrationData) -> Result<MigrationData> {
if let Some(data) = input.data.get_mut(self.setting) {
match data {
serde_json::Value::Array(data) => {
// We only handle string lists; convert each value to a str we can compare.
let list: Vec<&str> = data
.iter()
.map(|v| v.as_str())
.collect::<Option<Vec<&str>>>()
.with_context(|| error::ReplaceListContents {
setting: self.setting,
data: data.clone(),
})?;

if list == self.old_vals {
// Convert back to the original type so we can store it.
*data = self.new_vals.iter().map(|s| (*s).into()).collect();
println!(
"Changed value of '{}' from {:?} to {:?} on upgrade",
self.setting, self.old_vals, self.new_vals
);
} else {
println!("'{}' is not set to {:?}, leaving alone", self.setting, list);
}
}
_ => {
println!(
"'{}' is set to non-list value '{}'; ReplaceListMigration only handles lists",
self.setting, data
);
}
}
} else {
println!("Found no '{}' to change on upgrade", self.setting);
}
Ok(input)
}

fn backward(&mut self, mut input: MigrationData) -> Result<MigrationData> {
if let Some(data) = input.data.get_mut(self.setting) {
match data {
serde_json::Value::Array(data) => {
// We only handle string lists; convert each value to a str we can compare.
let list: Vec<&str> = data
.iter()
.map(|v| v.as_str())
.collect::<Option<Vec<&str>>>()
.with_context(|| error::ReplaceListContents {
setting: self.setting,
data: data.clone(),
})?;

if list == self.new_vals {
// Convert back to the original type so we can store it.
*data = self.old_vals.iter().map(|s| (*s).into()).collect();
println!(
"Changed value of '{}' from {:?} to {:?} on downgrade",
self.setting, self.new_vals, self.old_vals
);
} else {
println!("'{}' is not set to {:?}, leaving alone", self.setting, list);
}
}
_ => {
println!(
"'{}' is set to non-list value '{}'; ReplaceListMigration only handles lists",
self.setting, data
);
}
}
} else {
println!("Found no '{}' to change on downgrade", self.setting);
}
Ok(input)
}
}

#[cfg(test)]
mod test_replace_list {
use super::ReplaceListMigration;
use crate::{Migration, MigrationData};
use maplit::hashmap;
use std::collections::HashMap;

#[test]
fn single() {
let data = MigrationData {
data: hashmap! {
"hi".into() => vec!["there"].into(),
},
metadata: HashMap::new(),
};
let result = ReplaceListMigration {
setting: "hi",
old_vals: &["there"],
new_vals: &["sup"],
}
.forward(data)
.unwrap();
assert_eq!(
result.data,
hashmap! {
"hi".into() => vec!["sup"].into(),
}
);
}

#[test]
fn backward() {
let data = MigrationData {
data: hashmap! {
"hi".into() => vec!["there"].into(),
},
metadata: HashMap::new(),
};
let result = ReplaceListMigration {
setting: "hi",
old_vals: &["sup"],
new_vals: &["there"],
}
.backward(data)
.unwrap();
assert_eq!(
result.data,
hashmap! {
"hi".into() => vec!["sup"].into(),
}
);
}

#[test]
fn multiple() {
let data = MigrationData {
data: hashmap! {
"hi".into() => vec!["there", "you"].into(),
"ignored".into() => vec!["no", "change"].into(),
},
metadata: HashMap::new(),
};
let result = ReplaceListMigration {
setting: "hi",
old_vals: &["there", "you"],
new_vals: &["sup", "hey"],
}
.forward(data)
.unwrap();
assert_eq!(
result.data,
hashmap! {
"hi".into() => vec!["sup", "hey"].into(),
"ignored".into() => vec!["no", "change"].into(),
}
);
}

#[test]
fn no_match() {
let data = MigrationData {
data: hashmap! {
"hi".into() => vec!["no", "change"].into(),
"hi2".into() => vec!["no", "change"].into(),
},
metadata: HashMap::new(),
};
let result = ReplaceListMigration {
setting: "hi",
old_vals: &["there"],
new_vals: &["sup", "hey"],
}
.forward(data)
.unwrap();
// No change
assert_eq!(
result.data,
hashmap! {
"hi".into() => vec!["no", "change"].into(),
"hi2".into() => vec!["no", "change"].into(),
}
);
}

#[test]
fn not_list() {
let data = MigrationData {
data: hashmap! {
"hi".into() => "just a string, not a list".into(),
},
metadata: HashMap::new(),
};
let result = ReplaceListMigration {
setting: "hi",
old_vals: &["there"],
new_vals: &["sup", "hey"],
}
.forward(data)
.unwrap();
// No change
assert_eq!(
result.data,
hashmap! {
"hi".into() => "just a string, not a list".into(),
}
);
}

#[test]
fn not_string() {
let data = MigrationData {
data: hashmap! {
"hi".into() => vec![0].into(),
},
metadata: HashMap::new(),
};
ReplaceListMigration {
setting: "hi",
old_vals: &["there"],
new_vals: &["sup", "hey"],
}
.forward(data)
.unwrap_err();
}
}

// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=

/// We use this migration when we replace an existing template for generating some setting.
pub struct ReplaceTemplateMigration {
pub setting: &'static str,
Expand Down
6 changes: 6 additions & 0 deletions sources/api/migration/migration-helpers/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ pub enum Error {
NewKey {
source: apiserver::datastore::error::Error,
},

#[snafu(display("Setting '{}' contains non-string item: {:?}", setting, data))]
ReplaceListContents {
setting: String,
data: Vec<serde_json::Value>,
},
}

/// Result alias containing our Error type.
Expand Down
12 changes: 12 additions & 0 deletions sources/api/migration/migrations/v1.0.5/add-lockdown/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "add-lockdown"
version = "0.1.0"
authors = ["Tom Kirchner <tjk@amazon.com>"]
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" }
24 changes: 24 additions & 0 deletions sources/api/migration/migrations/v1.0.5/add-lockdown/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#![deny(rust_2018_idioms)]

use migration_helpers::common_migrations::AddPrefixesMigration;
use migration_helpers::{migrate, Result};
use std::process;

/// We added the ability to set kernel lockdown mode through a setting, so on downgrade we need to
/// remove the setting and the associated settings for the service that writes out changes.
fn run() -> Result<()> {
migrate(AddPrefixesMigration(vec![
"settings.kernel.lockdown",
"services.lockdown",
]))
}

// 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "sysctl-subcommand"
version = "0.1.0"
authors = ["Tom Kirchner <tjk@amazon.com>"]
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" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#![deny(rust_2018_idioms)]

use migration_helpers::common_migrations::ReplaceListMigration;
use migration_helpers::{migrate, Result};
use std::process;

/// We changed corndog to use subcommands so it can handle different kernel settings without having
/// to apply them all every time.
fn run() -> Result<()> {
migrate(ReplaceListMigration {
setting: "services.sysctl.restart-commands",
old_vals: &["/usr/bin/corndog"],
new_vals: &["/usr/bin/corndog sysctl"],
})
}

// 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);
}
}

0 comments on commit 821342a

Please sign in to comment.