Skip to content

Commit 66c4a65

Browse files
committed
feat: adds null migrator
Signed-off-by: Sam Berning <[email protected]>
1 parent 6a7e291 commit 66c4a65

File tree

8 files changed

+365
-223
lines changed

8 files changed

+365
-223
lines changed

bottlerocket-settings-sdk/src/extension/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ where
6262
Ok(extension)
6363
}
6464

65-
/// Converts a list of models into a map of Version => Model while checing for uniqueness.
65+
/// Converts a list of models into a map of Version => Model while checking for uniqueness.
6666
fn build_model_map(
6767
models: Vec<Mo>,
6868
) -> Result<HashMap<Version, Mo>, SettingsExtensionError<Mi::ErrorKind>> {

bottlerocket-settings-sdk/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub use helper::{template_helper, HelperDef, HelperError};
3333
#[cfg(feature = "extension")]
3434
pub use migrate::{
3535
LinearMigrator, LinearMigratorExtensionBuilder, LinearMigratorModel, LinearlyMigrateable,
36-
Migrator, NoMigration,
36+
Migrator, NoMigration, NullMigrator, NullMigratorExtensionBuilder,
3737
};
3838

3939
pub use model::{BottlerocketSetting, GenerateResult, SettingsModel};

bottlerocket-settings-sdk/src/migrate/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ pub use linear::{
1616
LinearMigrator, LinearMigratorExtensionBuilder, LinearMigratorModel, LinearlyMigrateable,
1717
};
1818

19+
pub mod null;
20+
pub use null::{NullMigrator, NullMigratorExtensionBuilder};
21+
1922
/// Implementors of the `Migrator` trait inform a [`SettingsExtension`](crate::SettingsExtension)
2023
/// how to migrate settings values between different versions.
2124
pub trait Migrator: Debug {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use super::NullMigrator;
2+
use crate::extension_builder;
3+
4+
extension_builder!(
5+
pub,
6+
NullMigratorExtensionBuilder,
7+
NullMigrator,
8+
NullMigrator
9+
);
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//! Provides a `NullMigrator` for settings that do not require migration, e.g. settings with a
2+
//! single version.
3+
use crate::migrate::{MigrationResult, ModelStore};
4+
use crate::model::{AsTypeErasedModel, TypeErasedModel};
5+
use crate::Migrator;
6+
use std::any::Any;
7+
8+
mod extensionbuilder;
9+
10+
pub use error::NullMigratorError;
11+
pub use extensionbuilder::NullMigratorExtensionBuilder;
12+
13+
/// `NullMigrator` is to be used for settings that do not require migration, e.g. settings with a
14+
/// single version. For cases where multiple versions of a setting are required, you should use a
15+
/// different Migrator, such as `LinearMigrator`, and define migrations between each version.
16+
///
17+
/// As `NullMigrator` takes anything that implements `TypeErasedModel`, it can be used with any
18+
/// existing `SettingsModel` without needing to implement any additional traits.
19+
#[derive(Default, Debug, Clone)]
20+
pub struct NullMigrator;
21+
22+
impl Migrator for NullMigrator {
23+
type ErrorKind = NullMigratorError;
24+
type ModelKind = Box<dyn TypeErasedModel>;
25+
26+
/// Asserts that the `NullMigrator` is only used with a single version of a model. For cases
27+
/// where multiple versions are required, you should use a different migrator, such as
28+
/// `LinearMigrator`.
29+
fn validate_migrations(
30+
&self,
31+
models: &dyn ModelStore<ModelKind = Self::ModelKind>,
32+
) -> Result<(), Self::ErrorKind> {
33+
snafu::ensure!(models.len() == 1, error::TooManyModelVersionsSnafu);
34+
}
35+
36+
/// Always returns a `NoMigration` error. Extensions that use `NullMigrator` should never need
37+
/// to migrate.
38+
fn perform_migration(
39+
&self,
40+
_models: &dyn ModelStore<ModelKind = Self::ModelKind>,
41+
_starting_value: Box<dyn Any>,
42+
_starting_version: &str,
43+
_target_version: &str,
44+
) -> Result<serde_json::Value, Self::ErrorKind> {
45+
Err(NullMigratorError::NoMigration)
46+
}
47+
48+
/// Always returns a `NoMigration` error. Extensions that use `NullMigrator` should never need
49+
/// to migrate.
50+
fn perform_flood_migrations(
51+
&self,
52+
_models: &dyn ModelStore<ModelKind = Self::ModelKind>,
53+
_starting_value: Box<dyn Any>,
54+
_starting_version: &str,
55+
) -> Result<Vec<MigrationResult>, Self::ErrorKind> {
56+
Err(NullMigratorError::NoMigration)
57+
}
58+
}
59+
60+
// Needed to satisfy the type constraints of `ModelKind` in `Migrator`. Unfortunately, `Box` has no
61+
// way of providing all traits implemented by the type it points to, so we need to reimplement this
62+
// trait ourselves.
63+
impl AsTypeErasedModel for Box<dyn TypeErasedModel> {
64+
fn as_model(&self) -> &dyn TypeErasedModel {
65+
self.as_ref()
66+
}
67+
}
68+
69+
mod error {
70+
#![allow(missing_docs)]
71+
use snafu::Snafu;
72+
73+
/// The error type returned by `NullMigrator`.
74+
#[derive(Debug, Snafu)]
75+
#[snafu(visibility(pub))]
76+
pub enum NullMigratorError {
77+
#[snafu(display("No migration to perform"))]
78+
NoMigration,
79+
80+
#[snafu(display("NullMigrator cannot be used with models with multiple versions"))]
81+
TooManyModelVersions,
82+
}
83+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
use anyhow::Result;
2+
use bottlerocket_settings_sdk::{
3+
extension::SettingsExtensionError, BottlerocketSetting, GenerateResult,
4+
LinearMigratorExtensionBuilder, LinearlyMigrateable, NoMigration, SettingsModel,
5+
};
6+
use serde::{Deserialize, Serialize};
7+
8+
use super::*;
9+
10+
macro_rules! define_model {
11+
($name:ident, $version:expr, $forward:ident, $backward:ident) => {
12+
common::define_model!($name, $version);
13+
14+
impl LinearlyMigrateable for $name {
15+
type ForwardMigrationTarget = $forward;
16+
type BackwardMigrationTarget = $backward;
17+
18+
fn migrate_forward(&self) -> Result<Self::ForwardMigrationTarget> {
19+
unimplemented!()
20+
}
21+
22+
fn migrate_backward(&self) -> Result<Self::BackwardMigrationTarget> {
23+
unimplemented!()
24+
}
25+
}
26+
};
27+
}
28+
29+
define_model!(DisjointA, "v1", NoMigration, NoMigration);
30+
define_model!(DisjointB, "v2", NoMigration, NoMigration);
31+
32+
#[test]
33+
fn test_no_small_disjoint_islands() {
34+
// Given two models which do not link in a migration chain,
35+
// When an linear migrator extension is built with those models,
36+
// The extension will fail to build.
37+
38+
assert!(matches!(
39+
LinearMigratorExtensionBuilder::with_name("disjoint-models")
40+
.with_models(vec![
41+
BottlerocketSetting::<DisjointA>::model(),
42+
BottlerocketSetting::<DisjointB>::model(),
43+
])
44+
.build(),
45+
Err(SettingsExtensionError::MigrationValidation { .. })
46+
));
47+
}
48+
49+
// A <-> B <-> D
50+
// E <-> C <-> A
51+
define_model!(LargeDisjointA, "v1", LargeDisjointB, NoMigration);
52+
define_model!(LargeDisjointB, "v2", LargeDisjointD, LargeDisjointA);
53+
define_model!(LargeDisjointC, "v3", LargeDisjointA, LargeDisjointE);
54+
define_model!(LargeDisjointD, "v4", NoMigration, LargeDisjointB);
55+
define_model!(LargeDisjointE, "v5", NoMigration, LargeDisjointC);
56+
57+
#[test]
58+
fn test_no_large_disjoint_islands() {
59+
assert!(matches!(
60+
LinearMigratorExtensionBuilder::with_name("disjoint-models")
61+
.with_models(vec![
62+
BottlerocketSetting::<LargeDisjointA>::model(),
63+
BottlerocketSetting::<LargeDisjointB>::model(),
64+
BottlerocketSetting::<LargeDisjointC>::model(),
65+
BottlerocketSetting::<LargeDisjointD>::model(),
66+
BottlerocketSetting::<LargeDisjointE>::model(),
67+
])
68+
.build(),
69+
Err(SettingsExtensionError::MigrationValidation { .. })
70+
));
71+
}
72+
73+
// A <-> C <-> D
74+
// B ---^
75+
define_model!(DoubleTailedA, "v1", DoubleTailedC, NoMigration);
76+
define_model!(DoubleTailedB, "v2", DoubleTailedC, NoMigration);
77+
define_model!(DoubleTailedC, "v3", DoubleTailedD, DoubleTailedA);
78+
define_model!(DoubleTailedD, "v4", NoMigration, DoubleTailedC);
79+
80+
#[test]
81+
fn test_no_double_tail() {
82+
assert!(matches!(
83+
LinearMigratorExtensionBuilder::with_name("disjoint-models")
84+
.with_models(vec![
85+
BottlerocketSetting::<DoubleTailedA>::model(),
86+
BottlerocketSetting::<DoubleTailedB>::model(),
87+
BottlerocketSetting::<DoubleTailedC>::model(),
88+
BottlerocketSetting::<DoubleTailedD>::model(),
89+
])
90+
.build(),
91+
Err(SettingsExtensionError::MigrationValidation { .. })
92+
));
93+
}
94+
95+
// C <-> A <-> B <-> C
96+
define_model!(LoopA, "v1", LoopC, LoopB);
97+
define_model!(LoopB, "v2", LoopA, LoopC);
98+
define_model!(LoopC, "v3", LoopB, LoopA);
99+
100+
#[test]
101+
fn test_no_migration_loops_simple_circle() {
102+
// Given a simple loop of linear migrations between models,
103+
// When an linear migrator extension is built with those models,
104+
// The extension will fail to build.
105+
106+
assert!(matches!(
107+
LinearMigratorExtensionBuilder::with_name("circular-loop")
108+
.with_models(vec![
109+
BottlerocketSetting::<LoopA>::model(),
110+
BottlerocketSetting::<LoopB>::model(),
111+
BottlerocketSetting::<LoopC>::model(),
112+
])
113+
.build(),
114+
Err(SettingsExtensionError::MigrationValidation { .. })
115+
));
116+
}
117+
118+
// A <-> B -> C
119+
// ^----------|
120+
define_model!(BrokenBacklinkA, "v1", NoMigration, LoopB);
121+
define_model!(BrokenBacklinkB, "v2", LoopA, LoopC);
122+
// C mistakenly points back to A
123+
define_model!(BrokenBacklinkC, "v3", LoopA, NoMigration);
124+
125+
#[test]
126+
fn test_no_migration_loops_backlink() {
127+
// Given a set of models with a backwards migration resulting in a loop,
128+
// When an linear migrator extension is built with those models,
129+
// The extension will fail to build.
130+
131+
assert!(matches!(
132+
LinearMigratorExtensionBuilder::with_name("broken-backlink")
133+
.with_models(vec![
134+
BottlerocketSetting::<BrokenBacklinkA>::model(),
135+
BottlerocketSetting::<BrokenBacklinkB>::model(),
136+
BottlerocketSetting::<BrokenBacklinkC>::model(),
137+
])
138+
.build(),
139+
Err(SettingsExtensionError::MigrationValidation { .. })
140+
));
141+
}
142+
143+
// A mistakenly points back to C
144+
define_model!(BackwardsCycleA, "v1", BackwardsCycleC, BackwardsCycleB);
145+
define_model!(BackwardsCycleB, "v2", BackwardsCycleA, BackwardsCycleC);
146+
define_model!(BackwardsCycleC, "v3", BackwardsCycleB, NoMigration);
147+
148+
#[test]
149+
fn test_no_migration_loops_backcycle() {
150+
assert!(matches!(
151+
LinearMigratorExtensionBuilder::with_name("backcycle")
152+
.with_models(vec![
153+
BottlerocketSetting::<BackwardsCycleA>::model(),
154+
BottlerocketSetting::<BackwardsCycleB>::model(),
155+
BottlerocketSetting::<BackwardsCycleC>::model(),
156+
])
157+
.build(),
158+
Err(SettingsExtensionError::MigrationValidation { .. })
159+
));
160+
}
161+
162+
define_model!(ForwardsCycleA, "v1", NoMigration, ForwardsCycleB);
163+
define_model!(ForwardsCycleB, "v2", ForwardsCycleA, ForwardsCycleC);
164+
// C mistakenly points forward to A
165+
define_model!(ForwardsCycleC, "v3", ForwardsCycleB, ForwardsCycleA);
166+
167+
#[test]
168+
fn test_no_migration_loops_forwardcycle() {
169+
assert!(matches!(
170+
LinearMigratorExtensionBuilder::with_name("forwards-cycle")
171+
.with_models(vec![
172+
BottlerocketSetting::<ForwardsCycleA>::model(),
173+
BottlerocketSetting::<ForwardsCycleB>::model(),
174+
BottlerocketSetting::<ForwardsCycleC>::model(),
175+
])
176+
.build(),
177+
Err(SettingsExtensionError::MigrationValidation { .. })
178+
));
179+
}
180+
181+
// A -> B -> C -> D
182+
// A <- C <- B <- D
183+
define_model!(NotReversibleA, "v1", NotReversibleB, NoMigration);
184+
define_model!(NotReversibleB, "v2", NotReversibleC, NotReversibleC);
185+
define_model!(NotReversibleC, "v3", NotReversibleD, NotReversibleA);
186+
define_model!(NotReversibleD, "v4", NoMigration, NotReversibleB);
187+
188+
#[test]
189+
fn test_no_non_reversible() {
190+
assert!(matches!(
191+
LinearMigratorExtensionBuilder::with_name("not-reversible")
192+
.with_models(vec![
193+
BottlerocketSetting::<NotReversibleA>::model(),
194+
BottlerocketSetting::<NotReversibleB>::model(),
195+
BottlerocketSetting::<NotReversibleC>::model(),
196+
BottlerocketSetting::<NotReversibleD>::model(),
197+
])
198+
.build(),
199+
Err(SettingsExtensionError::MigrationValidation { .. })
200+
));
201+
}

0 commit comments

Comments
 (0)