-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: adds null migrator #31
Merged
sam-berning
merged 2 commits into
bottlerocket-os:develop
from
sam-berning:null-migrator
Dec 12, 2023
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 9 additions & 0 deletions
9
bottlerocket-settings-sdk/src/migrate/null/extensionbuilder.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
use super::NullMigrator; | ||
use crate::extension_builder; | ||
|
||
extension_builder!( | ||
pub, | ||
NullMigratorExtensionBuilder, | ||
NullMigrator, | ||
NullMigrator | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
//! Provides a `NullMigrator` for settings that do not require migration, e.g. settings with a | ||
//! single version. | ||
use crate::migrate::{MigrationResult, ModelStore}; | ||
use crate::model::{AsTypeErasedModel, TypeErasedModel}; | ||
use crate::Migrator; | ||
use std::any::Any; | ||
|
||
mod extensionbuilder; | ||
|
||
pub use error::NullMigratorError; | ||
pub use extensionbuilder::NullMigratorExtensionBuilder; | ||
|
||
/// `NullMigrator` is to be used for settings that do not require migration, e.g. settings with a | ||
/// single version. For cases where multiple versions of a setting are required, you should use a | ||
/// different Migrator, such as `LinearMigrator`, and define migrations between each version. | ||
/// | ||
/// As `NullMigrator` takes anything that implements `TypeErasedModel`, it can be used with any | ||
/// existing `SettingsModel` without needing to implement any additional traits. | ||
#[derive(Default, Debug, Clone)] | ||
pub struct NullMigrator; | ||
|
||
impl Migrator for NullMigrator { | ||
type ErrorKind = NullMigratorError; | ||
type ModelKind = Box<dyn TypeErasedModel>; | ||
|
||
/// Asserts that the `NullMigrator` is only used with a single version of a model. For cases | ||
/// where multiple versions are required, you should use a different migrator, such as | ||
/// `LinearMigrator`. | ||
fn validate_migrations( | ||
&self, | ||
models: &dyn ModelStore<ModelKind = Self::ModelKind>, | ||
) -> Result<(), Self::ErrorKind> { | ||
snafu::ensure!(models.len() == 1, error::TooManyModelVersionsSnafu); | ||
Ok(()) | ||
} | ||
|
||
/// Always returns a `NoMigration` error. Extensions that use `NullMigrator` should never need | ||
/// to migrate. | ||
fn perform_migration( | ||
&self, | ||
_models: &dyn ModelStore<ModelKind = Self::ModelKind>, | ||
_starting_value: Box<dyn Any>, | ||
_starting_version: &str, | ||
_target_version: &str, | ||
) -> Result<serde_json::Value, Self::ErrorKind> { | ||
Err(NullMigratorError::NoMigration) | ||
} | ||
|
||
/// Always returns a `NoMigration` error. Extensions that use `NullMigrator` should never need | ||
/// to migrate. | ||
fn perform_flood_migrations( | ||
&self, | ||
_models: &dyn ModelStore<ModelKind = Self::ModelKind>, | ||
_starting_value: Box<dyn Any>, | ||
_starting_version: &str, | ||
) -> Result<Vec<MigrationResult>, Self::ErrorKind> { | ||
Err(NullMigratorError::NoMigration) | ||
} | ||
} | ||
|
||
// Needed to satisfy the type constraints of `ModelKind` in `Migrator`. Unfortunately, `Box` has no | ||
// way of providing all traits implemented by the type it points to, so we need to reimplement this | ||
// trait ourselves. | ||
impl AsTypeErasedModel for Box<dyn TypeErasedModel> { | ||
fn as_model(&self) -> &dyn TypeErasedModel { | ||
self.as_ref() | ||
} | ||
} | ||
|
||
mod error { | ||
#![allow(missing_docs)] | ||
use snafu::Snafu; | ||
|
||
/// The error type returned by `NullMigrator`. | ||
#[derive(Debug, Snafu)] | ||
#[snafu(visibility(pub))] | ||
pub enum NullMigratorError { | ||
#[snafu(display("No migration to perform"))] | ||
NoMigration, | ||
|
||
#[snafu(display("NullMigrator cannot be used with models with multiple versions"))] | ||
TooManyModelVersions, | ||
} | ||
} |
201 changes: 201 additions & 0 deletions
201
bottlerocket-settings-sdk/tests/migration_validation/linear.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
use anyhow::Result; | ||
use bottlerocket_settings_sdk::{ | ||
extension::SettingsExtensionError, BottlerocketSetting, GenerateResult, | ||
LinearMigratorExtensionBuilder, LinearlyMigrateable, NoMigration, SettingsModel, | ||
}; | ||
use serde::{Deserialize, Serialize}; | ||
|
||
use super::*; | ||
|
||
macro_rules! define_model { | ||
($name:ident, $version:expr, $forward:ident, $backward:ident) => { | ||
common::define_model!($name, $version); | ||
|
||
impl LinearlyMigrateable for $name { | ||
type ForwardMigrationTarget = $forward; | ||
type BackwardMigrationTarget = $backward; | ||
|
||
fn migrate_forward(&self) -> Result<Self::ForwardMigrationTarget> { | ||
unimplemented!() | ||
} | ||
|
||
fn migrate_backward(&self) -> Result<Self::BackwardMigrationTarget> { | ||
unimplemented!() | ||
} | ||
} | ||
}; | ||
} | ||
|
||
define_model!(DisjointA, "v1", NoMigration, NoMigration); | ||
define_model!(DisjointB, "v2", NoMigration, NoMigration); | ||
|
||
#[test] | ||
fn test_no_small_disjoint_islands() { | ||
// Given two models which do not link in a migration chain, | ||
// When an linear migrator extension is built with those models, | ||
// The extension will fail to build. | ||
|
||
assert!(matches!( | ||
LinearMigratorExtensionBuilder::with_name("disjoint-models") | ||
.with_models(vec![ | ||
BottlerocketSetting::<DisjointA>::model(), | ||
BottlerocketSetting::<DisjointB>::model(), | ||
]) | ||
.build(), | ||
Err(SettingsExtensionError::MigrationValidation { .. }) | ||
)); | ||
} | ||
|
||
// A <-> B <-> D | ||
// E <-> C <-> A | ||
define_model!(LargeDisjointA, "v1", LargeDisjointB, NoMigration); | ||
define_model!(LargeDisjointB, "v2", LargeDisjointD, LargeDisjointA); | ||
define_model!(LargeDisjointC, "v3", LargeDisjointA, LargeDisjointE); | ||
define_model!(LargeDisjointD, "v4", NoMigration, LargeDisjointB); | ||
define_model!(LargeDisjointE, "v5", NoMigration, LargeDisjointC); | ||
|
||
#[test] | ||
fn test_no_large_disjoint_islands() { | ||
assert!(matches!( | ||
LinearMigratorExtensionBuilder::with_name("disjoint-models") | ||
.with_models(vec![ | ||
BottlerocketSetting::<LargeDisjointA>::model(), | ||
BottlerocketSetting::<LargeDisjointB>::model(), | ||
BottlerocketSetting::<LargeDisjointC>::model(), | ||
BottlerocketSetting::<LargeDisjointD>::model(), | ||
BottlerocketSetting::<LargeDisjointE>::model(), | ||
]) | ||
.build(), | ||
Err(SettingsExtensionError::MigrationValidation { .. }) | ||
)); | ||
} | ||
|
||
// A <-> C <-> D | ||
// B ---^ | ||
define_model!(DoubleTailedA, "v1", DoubleTailedC, NoMigration); | ||
define_model!(DoubleTailedB, "v2", DoubleTailedC, NoMigration); | ||
define_model!(DoubleTailedC, "v3", DoubleTailedD, DoubleTailedA); | ||
define_model!(DoubleTailedD, "v4", NoMigration, DoubleTailedC); | ||
|
||
#[test] | ||
fn test_no_double_tail() { | ||
assert!(matches!( | ||
LinearMigratorExtensionBuilder::with_name("disjoint-models") | ||
.with_models(vec![ | ||
BottlerocketSetting::<DoubleTailedA>::model(), | ||
BottlerocketSetting::<DoubleTailedB>::model(), | ||
BottlerocketSetting::<DoubleTailedC>::model(), | ||
BottlerocketSetting::<DoubleTailedD>::model(), | ||
]) | ||
.build(), | ||
Err(SettingsExtensionError::MigrationValidation { .. }) | ||
)); | ||
} | ||
|
||
// C <-> A <-> B <-> C | ||
define_model!(LoopA, "v1", LoopC, LoopB); | ||
define_model!(LoopB, "v2", LoopA, LoopC); | ||
define_model!(LoopC, "v3", LoopB, LoopA); | ||
|
||
#[test] | ||
fn test_no_migration_loops_simple_circle() { | ||
// Given a simple loop of linear migrations between models, | ||
// When an linear migrator extension is built with those models, | ||
// The extension will fail to build. | ||
|
||
assert!(matches!( | ||
LinearMigratorExtensionBuilder::with_name("circular-loop") | ||
.with_models(vec![ | ||
BottlerocketSetting::<LoopA>::model(), | ||
BottlerocketSetting::<LoopB>::model(), | ||
BottlerocketSetting::<LoopC>::model(), | ||
]) | ||
.build(), | ||
Err(SettingsExtensionError::MigrationValidation { .. }) | ||
)); | ||
} | ||
|
||
// A <-> B -> C | ||
// ^----------| | ||
define_model!(BrokenBacklinkA, "v1", NoMigration, LoopB); | ||
define_model!(BrokenBacklinkB, "v2", LoopA, LoopC); | ||
// C mistakenly points back to A | ||
define_model!(BrokenBacklinkC, "v3", LoopA, NoMigration); | ||
|
||
#[test] | ||
fn test_no_migration_loops_backlink() { | ||
// Given a set of models with a backwards migration resulting in a loop, | ||
// When an linear migrator extension is built with those models, | ||
// The extension will fail to build. | ||
|
||
assert!(matches!( | ||
LinearMigratorExtensionBuilder::with_name("broken-backlink") | ||
.with_models(vec![ | ||
BottlerocketSetting::<BrokenBacklinkA>::model(), | ||
BottlerocketSetting::<BrokenBacklinkB>::model(), | ||
BottlerocketSetting::<BrokenBacklinkC>::model(), | ||
]) | ||
.build(), | ||
Err(SettingsExtensionError::MigrationValidation { .. }) | ||
)); | ||
} | ||
|
||
// A mistakenly points back to C | ||
define_model!(BackwardsCycleA, "v1", BackwardsCycleC, BackwardsCycleB); | ||
define_model!(BackwardsCycleB, "v2", BackwardsCycleA, BackwardsCycleC); | ||
define_model!(BackwardsCycleC, "v3", BackwardsCycleB, NoMigration); | ||
|
||
#[test] | ||
fn test_no_migration_loops_backcycle() { | ||
assert!(matches!( | ||
LinearMigratorExtensionBuilder::with_name("backcycle") | ||
.with_models(vec![ | ||
BottlerocketSetting::<BackwardsCycleA>::model(), | ||
BottlerocketSetting::<BackwardsCycleB>::model(), | ||
BottlerocketSetting::<BackwardsCycleC>::model(), | ||
]) | ||
.build(), | ||
Err(SettingsExtensionError::MigrationValidation { .. }) | ||
)); | ||
} | ||
|
||
define_model!(ForwardsCycleA, "v1", NoMigration, ForwardsCycleB); | ||
define_model!(ForwardsCycleB, "v2", ForwardsCycleA, ForwardsCycleC); | ||
// C mistakenly points forward to A | ||
define_model!(ForwardsCycleC, "v3", ForwardsCycleB, ForwardsCycleA); | ||
|
||
#[test] | ||
fn test_no_migration_loops_forwardcycle() { | ||
assert!(matches!( | ||
LinearMigratorExtensionBuilder::with_name("forwards-cycle") | ||
.with_models(vec![ | ||
BottlerocketSetting::<ForwardsCycleA>::model(), | ||
BottlerocketSetting::<ForwardsCycleB>::model(), | ||
BottlerocketSetting::<ForwardsCycleC>::model(), | ||
]) | ||
.build(), | ||
Err(SettingsExtensionError::MigrationValidation { .. }) | ||
)); | ||
} | ||
|
||
// A -> B -> C -> D | ||
// A <- C <- B <- D | ||
define_model!(NotReversibleA, "v1", NotReversibleB, NoMigration); | ||
define_model!(NotReversibleB, "v2", NotReversibleC, NotReversibleC); | ||
define_model!(NotReversibleC, "v3", NotReversibleD, NotReversibleA); | ||
define_model!(NotReversibleD, "v4", NoMigration, NotReversibleB); | ||
|
||
#[test] | ||
fn test_no_non_reversible() { | ||
assert!(matches!( | ||
LinearMigratorExtensionBuilder::with_name("not-reversible") | ||
.with_models(vec![ | ||
BottlerocketSetting::<NotReversibleA>::model(), | ||
BottlerocketSetting::<NotReversibleB>::model(), | ||
BottlerocketSetting::<NotReversibleC>::model(), | ||
BottlerocketSetting::<NotReversibleD>::model(), | ||
]) | ||
.build(), | ||
Err(SettingsExtensionError::MigrationValidation { .. }) | ||
)); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cbgbt We should strongly consider not using Snafu for pub errors. The long-and-short of it is that we are exposing a library and its traits in our public interface and it might be impossible to avoid a major version break if the Snafu library changes in some way. Can discuss.
For this PR it's fine, but I would recommend a backlog item to convert away from pub Snafu errors in favor of hand-rolled errors (they're simple, it's not a lot of boilerplate).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion. I've cut #32 to track this idea.