use serde::{Deserialize, Serialize};
use crate::infra::execution::placement::ProjectRuntimeProfile;
pub const PROJECT_MIGRATION_PLAN_SCHEMA_VERSION: u32 = 1;
pub const PROJECT_MIGRATION_EXECUTION_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum MigrationStrategy {
CloneFirst,
LiveHandoff,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DataMigrationStrategy {
RepoOnly,
RepoAndRuntimeSnapshot,
FullProject,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SecretsMigrationStrategy {
RemapManually,
ReuseByName,
ReferencesOnly,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum MigrationCutoverStrategy {
Manual,
Immediate,
Planned,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum MigrationEndpointKind {
LocalNode,
Worker,
Cluster,
DedicatedRuntime,
StandaloneInstall,
IndependentPlatform,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct MigrationEndpoint {
pub kind: MigrationEndpointKind,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub platform_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub runner_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub address: Option<String>,
}
impl Default for MigrationStrategy {
fn default() -> Self {
Self::CloneFirst
}
}
impl Default for DataMigrationStrategy {
fn default() -> Self {
Self::RepoAndRuntimeSnapshot
}
}
impl Default for SecretsMigrationStrategy {
fn default() -> Self {
Self::RemapManually
}
}
impl Default for MigrationCutoverStrategy {
fn default() -> Self {
Self::Manual
}
}
impl Default for MigrationEndpointKind {
fn default() -> Self {
Self::LocalNode
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProjectMigrationPlan {
#[serde(default = "default_project_migration_plan_schema_version")]
pub schema_version: u32,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub migration_id: String,
#[serde(default)]
pub strategy: MigrationStrategy,
#[serde(default)]
pub source: MigrationEndpoint,
#[serde(default)]
pub target: MigrationEndpoint,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub bundle_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub runtime_snapshot_id: Option<String>,
#[serde(default)]
pub desired_runtime: ProjectRuntimeProfile,
#[serde(default)]
pub data_strategy: DataMigrationStrategy,
#[serde(default)]
pub secrets_strategy: SecretsMigrationStrategy,
#[serde(default)]
pub cutover_strategy: MigrationCutoverStrategy,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rollback_window_secs: Option<u64>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub notes: String,
}
const fn default_project_migration_plan_schema_version() -> u32 {
PROJECT_MIGRATION_PLAN_SCHEMA_VERSION
}
impl Default for ProjectMigrationPlan {
fn default() -> Self {
Self {
schema_version: PROJECT_MIGRATION_PLAN_SCHEMA_VERSION,
migration_id: String::new(),
strategy: MigrationStrategy::default(),
source: MigrationEndpoint::default(),
target: MigrationEndpoint::default(),
bundle_id: String::new(),
runtime_snapshot_id: None,
desired_runtime: ProjectRuntimeProfile::default(),
data_strategy: DataMigrationStrategy::default(),
secrets_strategy: SecretsMigrationStrategy::default(),
cutover_strategy: MigrationCutoverStrategy::default(),
rollback_window_secs: None,
notes: String::new(),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum MigrationStatus {
Draft,
Planned,
Syncing,
AwaitingSecrets,
Warming,
ReadyForCutover,
CuttingOver,
RunningRollbackWindow,
Completed,
RolledBack,
Failed,
}
impl Default for MigrationStatus {
fn default() -> Self {
Self::Draft
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MigrationExecution {
#[serde(default = "default_project_migration_execution_schema_version")]
pub schema_version: u32,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub migration_id: String,
#[serde(default)]
pub status: MigrationStatus,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub current_step: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub started_at: Option<i64>,
#[serde(default)]
pub updated_at: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cutover_at: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finished_at: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_error: Option<String>,
}
const fn default_project_migration_execution_schema_version() -> u32 {
PROJECT_MIGRATION_EXECUTION_SCHEMA_VERSION
}
impl Default for MigrationExecution {
fn default() -> Self {
Self {
schema_version: PROJECT_MIGRATION_EXECUTION_SCHEMA_VERSION,
migration_id: String::new(),
status: MigrationStatus::Draft,
current_step: String::new(),
started_at: None,
updated_at: 0,
cutover_at: None,
finished_at: None,
last_error: None,
}
}
}
#[cfg(test)]
mod tests {
use super::{
MigrationStatus, MigrationStrategy, PROJECT_MIGRATION_EXECUTION_SCHEMA_VERSION,
PROJECT_MIGRATION_PLAN_SCHEMA_VERSION, ProjectMigrationPlan,
};
#[test]
fn migration_defaults_are_clone_first_and_versioned() {
let plan = ProjectMigrationPlan::default();
assert_eq!(plan.schema_version, PROJECT_MIGRATION_PLAN_SCHEMA_VERSION);
assert_eq!(plan.strategy, MigrationStrategy::CloneFirst);
let execution = super::MigrationExecution::default();
assert_eq!(
execution.schema_version,
PROJECT_MIGRATION_EXECUTION_SCHEMA_VERSION
);
assert_eq!(execution.status, MigrationStatus::Draft);
}
}