Skip to content

Commit 9fcfb47

Browse files
committed
migrator: end-to-end test
1 parent d4ae0a6 commit 9fcfb47

File tree

9 files changed

+462
-89
lines changed

9 files changed

+462
-89
lines changed

sources/Cargo.lock

Lines changed: 5 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sources/api/migration/migrator/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ lazy_static = "1.2"
1313
log = "0.4"
1414
lz4 = "1.23.1"
1515
nix = "0.17"
16-
pentacle = "0.1.1"
16+
pentacle = "0.2.0"
1717
rand = { version = "0.7", default-features = false, features = ["std"] }
1818
regex = "1.1"
1919
semver = "0.9"
@@ -27,6 +27,10 @@ url = "2.1.1"
2727
[build-dependencies]
2828
cargo-readme = "3.1"
2929

30+
[dev-dependencies]
31+
chrono = "0.4.11"
32+
storewolf = { path = "../../storewolf" }
33+
3034
[[bin]]
3135
name = "migrator"
3236
path = "src/main.rs"

sources/api/migration/migrator/src/run.rs renamed to sources/api/migration/migrator/src/main.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ use update_metadata::{load_manifest, MIGRATION_FILENAME_RE};
4646
mod args;
4747
mod direction;
4848
mod error;
49+
#[cfg(test)]
50+
mod test;
4951

5052
lazy_static! {
5153
/// This is the last version of Bottlerocket that supports *only* unsigned migrations.
@@ -131,7 +133,7 @@ where
131133
Version::parse(version_str).context(error::InvalidDataStoreVersion { path: &patch })
132134
}
133135

134-
fn run(args: &Args) -> Result<()> {
136+
pub(crate) fn run(args: &Args) -> Result<()> {
135137
// Get the directory we're working in.
136138
let datastore_dir = args
137139
.datastore_path
@@ -782,7 +784,7 @@ where
782784
// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
783785

784786
#[cfg(test)]
785-
mod test {
787+
mod main_test {
786788
use super::*;
787789

788790
#[test]
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
//! Provides an end-to-end test of `migrator` via the `run` function. This module is conditionally
2+
//! compiled for cfg(test) only.
3+
use crate::args::Args;
4+
use crate::run;
5+
use chrono::{DateTime, Utc};
6+
use semver::Version;
7+
use std::fs;
8+
use std::fs::File;
9+
use std::io::Write;
10+
use std::path::{Path, PathBuf};
11+
use tempfile::TempDir;
12+
13+
/// Provides the path to a folder where test data files reside.
14+
fn test_data() -> PathBuf {
15+
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
16+
p.pop();
17+
p.join("migrator").join("tests").join("data")
18+
}
19+
20+
/// Returns the filepath to a `root.json` file stored in tree for testing. This file declares
21+
/// an expiration date of `1970-01-01` to ensure success with an expired TUF repository.
22+
fn root() -> PathBuf {
23+
test_data()
24+
.join("expired-root.json")
25+
.canonicalize()
26+
.unwrap()
27+
}
28+
29+
/// Returns the filepath to a private key, stored in tree and used only for testing.
30+
fn pem() -> PathBuf {
31+
test_data().join("snakeoil.pem").canonicalize().unwrap()
32+
}
33+
34+
/// The name of a test migration. The prefix `b-` ensures we are not alphabetically sorting.
35+
const FIRST_MIGRATION: &str = "b-first-migration";
36+
37+
/// The name of a test migration. The prefix `a-` ensures we are not alphabetically sorting.
38+
const SECOND_MIGRATION: &str = "a-second-migration";
39+
40+
/// Creates a script that will serve as a migration during testing. The script writes its migrations
41+
/// name to a file named `result.txt` in the parent directory of the datastore. `pentacle` does not
42+
/// retain the name of the executing binary or script, so we take the `migration_name` as input,
43+
/// and 'hardcode' it into the script.
44+
fn create_test_migration<S: AsRef<str>>(migration_name: S) -> String {
45+
format!(
46+
r#"#!/usr/bin/env bash
47+
set -eo pipefail
48+
migration_name="{}"
49+
datastore_parent_dir="$(dirname "${{3}}")"
50+
outfile="${{datastore_parent_dir}}/result.txt"
51+
echo "${{migration_name}}:" "${{@}}" >> "${{outfile}}"
52+
"#,
53+
migration_name.as_ref()
54+
)
55+
}
56+
57+
/// Holds the lifetime of a `TempDir` inside which a datastore directory and links are held for
58+
/// testing.
59+
struct TestDatastore {
60+
tmp: TempDir,
61+
datastore: PathBuf,
62+
}
63+
64+
impl TestDatastore {
65+
/// Creates a `TempDir`, sets up the datastore links needed to represent the `from_version`
66+
/// and returns a `TestDatastore` populated with this information.
67+
fn new(from_version: Version) -> Self {
68+
let tmp = TempDir::new().unwrap();
69+
let datastore = storewolf::create_new_datastore(tmp.path(), Some(from_version)).unwrap();
70+
TestDatastore { tmp, datastore }
71+
}
72+
}
73+
74+
/// Represents a TUF repository, which is held in a tempdir.
75+
struct TestRepo {
76+
/// This field preserves the lifetime of the TempDir even though we never read it. When
77+
/// `TestRepo` goes out of scope, `TempDir` will remove the temporary directory.
78+
_tuf_dir: TempDir,
79+
metadata_path: PathBuf,
80+
targets_path: PathBuf,
81+
}
82+
83+
/// LZ4 compresses `source` bytes to a new file at `destination`.
84+
fn compress(source: &[u8], destination: &Path) {
85+
let output_file = File::create(destination).unwrap();
86+
let mut encoder = lz4::EncoderBuilder::new()
87+
.level(4)
88+
.build(output_file)
89+
.unwrap();
90+
encoder.write_all(source).unwrap();
91+
let (_output, result) = encoder.finish();
92+
result.unwrap()
93+
}
94+
95+
/// Creates a test repository with a couple of versions defined in the manifest and a couple of
96+
/// migrations. See the test description for for more info.
97+
fn create_test_repo() -> TestRepo {
98+
// This is where the signed TUF repo will exist when we are done. It is the
99+
// root directory of the `TestRepo` we will return when we are done.
100+
let test_repo_dir = TempDir::new().unwrap();
101+
let metadata_path = test_repo_dir.path().join("metadata");
102+
let targets_path = test_repo_dir.path().join("targets");
103+
104+
// This is where we will stage the TUF repository targets prior to signing them. We are using
105+
// symlinks from `tuf_indir` to `tuf_outdir/targets` so we keep both in the same `TempDir`.
106+
let tuf_indir = test_repo_dir.path();
107+
108+
// Create a Manifest and save it to the tuftool_indir for signing.
109+
let mut manifest = update_metadata::Manifest::default();
110+
// insert the following migrations to the manifest. note that the first migration would sort
111+
// later than the second migration alphabetically. this is to help ensure that migrations
112+
// are running in their listed order (rather than sorted order as in previous
113+
// implementations).
114+
manifest.migrations.insert(
115+
(Version::new(0, 99, 0), Version::new(0, 99, 1)),
116+
vec![FIRST_MIGRATION.into(), SECOND_MIGRATION.into()],
117+
);
118+
update_metadata::write_file(tuf_indir.join("manifest.json").as_path(), &manifest).unwrap();
119+
120+
// Create an script that we can use as the 'migration' that migrator will run. This script will
121+
// write its name and arguments to a file named result.txt in the directory that is the parent
122+
// of --source-datastore. result.txt can then be used to see what migrations ran, and in what
123+
// order. Note that tests are sensitive to the order and number of arguments passed. If
124+
// --source-datastore is given at a different position then the tests will fail and the script
125+
// will need to be updated.
126+
let migration_a = create_test_migration(FIRST_MIGRATION);
127+
let migration_b = create_test_migration(SECOND_MIGRATION);
128+
129+
// Save lz4 compressed copies of the migration script into the tuftool_indir.
130+
compress(migration_a.as_bytes(), &tuf_indir.join(FIRST_MIGRATION));
131+
compress(migration_b.as_bytes(), &tuf_indir.join(SECOND_MIGRATION));
132+
133+
// Create and sign the TUF repository.
134+
let mut editor = tough::editor::RepositoryEditor::new(root()).unwrap();
135+
let long_ago: DateTime<Utc> = DateTime::parse_from_rfc3339("1970-01-01T00:00:00Z")
136+
.unwrap()
137+
.into();
138+
let one = std::num::NonZeroU64::new(1).unwrap();
139+
editor
140+
.targets_version(one)
141+
.targets_expires(long_ago)
142+
.snapshot_version(one)
143+
.snapshot_expires(long_ago)
144+
.timestamp_version(one)
145+
.timestamp_expires(long_ago);
146+
147+
fs::read_dir(tuf_indir)
148+
.unwrap()
149+
.filter(|dir_entry_result| {
150+
if let Ok(dir_entry) = dir_entry_result {
151+
return dir_entry.path().is_file();
152+
}
153+
false
154+
})
155+
.for_each(|dir_entry_result| {
156+
let dir_entry = dir_entry_result.unwrap();
157+
editor.add_target(
158+
dir_entry.file_name().to_str().unwrap().into(),
159+
tough::schema::Target::from_path(dir_entry.path()).unwrap(),
160+
);
161+
});
162+
let signed_repo = editor
163+
.sign(&[Box::new(tough::key_source::LocalKeySource { path: pem() })])
164+
.unwrap();
165+
signed_repo.link_targets(tuf_indir, &targets_path).unwrap();
166+
signed_repo.write(&metadata_path).unwrap();
167+
168+
TestRepo {
169+
_tuf_dir: test_repo_dir,
170+
metadata_path,
171+
targets_path,
172+
}
173+
}
174+
175+
/// Tests the migrator program end-to-end using the `run` function. Creates a TUF repo in a
176+
/// tempdir which includes a `manifest.json` with a couple of migrations:
177+
/// ```
178+
/// "(0.99.0, 0.99.1)": [
179+
/// "b-first-migration",
180+
/// "a-second-migration"
181+
/// ]
182+
/// ```
183+
///
184+
/// The two 'migrations' are instances of the same bash script (see `create_test_repo`) which
185+
/// writes its name (i.e. the migration name) and its arguments to a file at `./result.txt`
186+
/// (i.e. since migrations run in the context of the datastore directory, `result.txt` is
187+
/// written one directory above the datastore.) We can then inspect the contents of `result.txt`
188+
/// to see that the expected migrations ran in the correct order.
189+
#[test]
190+
fn migrate_forward() {
191+
let from_version = Version::parse("0.99.0").unwrap();
192+
let to_version = Version::parse("0.99.1").unwrap();
193+
let test_datastore = TestDatastore::new(from_version);
194+
let test_repo = create_test_repo();
195+
let args = Args {
196+
datastore_path: test_datastore.datastore.clone(),
197+
log_level: log::LevelFilter::Info,
198+
migration_directory: test_repo.targets_path.clone(),
199+
migrate_to_version: to_version,
200+
root_path: root(),
201+
metadata_directory: test_repo.metadata_path.clone(),
202+
};
203+
run(&args).unwrap();
204+
// the migrations should write to a file named result.txt.
205+
let output_file = test_datastore.tmp.path().join("result.txt");
206+
let contents = std::fs::read_to_string(&output_file).unwrap();
207+
let lines: Vec<&str> = contents.split('\n').collect();
208+
assert_eq!(lines.len(), 3);
209+
let first_line = *lines.get(0).unwrap();
210+
let want = format!("{}: --forward", FIRST_MIGRATION);
211+
let got: String = first_line.chars().take(want.len()).collect();
212+
assert_eq!(got, want);
213+
let second_line = *lines.get(1).unwrap();
214+
let want = format!("{}: --forward", SECOND_MIGRATION);
215+
let got: String = second_line.chars().take(want.len()).collect();
216+
assert_eq!(got, want);
217+
}
218+
219+
/// This test ensures that migrations run when migrating from a newer to an older version.
220+
/// See `migrate_forward` for a description of how these tests work.
221+
#[test]
222+
fn migrate_backward() {
223+
let from_version = Version::parse("0.99.1").unwrap();
224+
let to_version = Version::parse("0.99.0").unwrap();
225+
let test_datastore = TestDatastore::new(from_version);
226+
let test_repo = create_test_repo();
227+
let args = Args {
228+
datastore_path: test_datastore.datastore.clone(),
229+
log_level: log::LevelFilter::Info,
230+
migration_directory: test_repo.targets_path.clone(),
231+
migrate_to_version: to_version,
232+
root_path: root(),
233+
metadata_directory: test_repo.metadata_path.clone(),
234+
};
235+
run(&args).unwrap();
236+
let output_file = test_datastore.tmp.path().join("result.txt");
237+
let contents = std::fs::read_to_string(&output_file).unwrap();
238+
let lines: Vec<&str> = contents.split('\n').collect();
239+
assert_eq!(lines.len(), 3);
240+
let first_line = *lines.get(0).unwrap();
241+
let want = format!("{}: --backward", SECOND_MIGRATION);
242+
let got: String = first_line.chars().take(want.len()).collect();
243+
assert_eq!(got, want);
244+
let second_line = *lines.get(1).unwrap();
245+
let want = format!("{}: --backward", FIRST_MIGRATION);
246+
let got: String = second_line.chars().take(want.len()).collect();
247+
assert_eq!(got, want);
248+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"signed": {
3+
"_type": "root",
4+
"spec_version": "1.0.0",
5+
"consistent_snapshot": true,
6+
"version": 1,
7+
"expires": "1970-01-01T00:00:00Z",
8+
"keys": {
9+
"febb06e5853878c3b2447c5100d327ebcf0807832c942f5e93ab28e0e4644684": {
10+
"keytype": "rsa",
11+
"keyval": {
12+
"public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7LU2gyhDDc7jglt2h3+q\n3+pHUprpe5hX2W4yE8NlM3U7EQRjiyd9doyXGAanBMd8IyqS3Q2ehuo2TZ5aVUFh\n+s/ZboEj+VMNPwPYhRv4QnNT79/kFsA5z0jMDFxCr3+IT2NFJv9GV+83PFVrvTZX\nNeqIZiAT/EJDENn7wS8p8G+eC/XkUcyA5kWxHXDdBgs+Xd+nXkh2v/8/lFKDJ+A4\nZlF9cIuAiWB7vNRMg29bhsLreD3F73O7iJCaFfg3I9EpofVUWWNZg4VM6Mmjksav\nFneTgjXTN9wPnNTjCBrUGwChLklBtInm+9C5iIfEoysqKZSwjeF9gchOANBlu7PD\nxwIDAQAB\n-----END PUBLIC KEY-----"
13+
},
14+
"scheme": "rsassa-pss-sha256"
15+
}
16+
},
17+
"roles": {
18+
"targets": {
19+
"keyids": [
20+
"febb06e5853878c3b2447c5100d327ebcf0807832c942f5e93ab28e0e4644684"
21+
],
22+
"threshold": 1
23+
},
24+
"snapshot": {
25+
"keyids": [
26+
"febb06e5853878c3b2447c5100d327ebcf0807832c942f5e93ab28e0e4644684"
27+
],
28+
"threshold": 1
29+
},
30+
"timestamp": {
31+
"keyids": [
32+
"febb06e5853878c3b2447c5100d327ebcf0807832c942f5e93ab28e0e4644684"
33+
],
34+
"threshold": 1
35+
},
36+
"root": {
37+
"keyids": [
38+
"febb06e5853878c3b2447c5100d327ebcf0807832c942f5e93ab28e0e4644684"
39+
],
40+
"threshold": 1
41+
}
42+
}
43+
},
44+
"signatures": [
45+
{
46+
"keyid": "febb06e5853878c3b2447c5100d327ebcf0807832c942f5e93ab28e0e4644684",
47+
"sig": "4ed06d6bd1b8cc145c2a872e6705f37038f52534c01d4f52c7bd0e520aa46bfb83ee1987fdbbeb415b3d42ed6f85abed640d9cb4e403a20f56a3d6661b00a174411b927cb064e214632f0bb5d7b1b2319d8064cedb58ae1abc68908ad8e6ce2c451b0d3aafbff3700d6cd74517ccf10f5f00ee0eb16eb4272afc3a9021ff9be8b4e00a69b24607039a8230803eb537293ce6b244d77cd58db512af7ee0a976612a7498f1b31c7e5918925ca3846e5d7f419e9d5825af16290a36eb1b8465de73b8bc1bbaf2e1ae0f7eeb6999fa06f09bee19cd30d8c6848c08d33970e66d8f49704e41c4f2c933be3a77a8a949309cdcdbd7c2262ca89243aff0b5e450e45d64"
48+
}
49+
]
50+
}

0 commit comments

Comments
 (0)