feat: Implement database migration framework with initial migrations for organizations, workspaces, agents, virtual hosts, upstreams, certificates, and users

This commit is contained in:
GW_MC
2026-03-03 04:31:06 +00:00
parent 2e9ad4fc21
commit 7d9285ba44
15 changed files with 785 additions and 23 deletions

19
migration/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "migration"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "migration"
path = "src/lib.rs"
[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }
[dependencies.sea-orm-migration]
version = "2.0.0-rc"
features = [
"runtime-tokio-rustls",
"sqlx-postgres",
]

41
migration/README.md Normal file
View File

@@ -0,0 +1,41 @@
# Running Migrator CLI
- Generate a new migration file
```sh
cargo run -- generate MIGRATION_NAME
```
- Apply all pending migrations
```sh
cargo run
```
```sh
cargo run -- up
```
- Apply first 10 pending migrations
```sh
cargo run -- up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- down
```
- Rollback last 10 applied migrations
```sh
cargo run -- down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- refresh
```
- Rollback all applied migrations
```sh
cargo run -- reset
```
- Check the status of all migrations
```sh
cargo run -- status
```

26
migration/src/lib.rs Normal file
View File

@@ -0,0 +1,26 @@
pub use sea_orm_migration::prelude::*;
mod m20240301_000001_create_organizations;
mod m20240301_000002_create_workspaces;
mod m20240301_000003_create_agents;
mod m20240301_000004_create_virtual_hosts;
mod m20240301_000005_create_upstreams;
mod m20240301_000006_create_certificates;
mod m20240301_000007_create_users;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20240301_000001_create_organizations::Migration),
Box::new(m20240301_000002_create_workspaces::Migration),
Box::new(m20240301_000003_create_agents::Migration),
Box::new(m20240301_000004_create_virtual_hosts::Migration),
Box::new(m20240301_000005_create_upstreams::Migration),
Box::new(m20240301_000006_create_certificates::Migration),
Box::new(m20240301_000007_create_users::Migration),
]
}
}

View File

@@ -0,0 +1,59 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Organizations::Table)
.if_not_exists()
.col(
ColumnDef::new(Organizations::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Organizations::Name).string().not_null())
.col(
ColumnDef::new(Organizations::Slug)
.string()
.not_null()
.unique_key(),
)
.col(
ColumnDef::new(Organizations::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Organizations::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(ColumnDef::new(Organizations::Settings).json())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Organizations::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Organizations {
Table,
Id,
Name,
Slug,
CreatedAt,
UpdatedAt,
Settings,
}

View File

@@ -0,0 +1,88 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Workspaces::Table)
.if_not_exists()
.col(
ColumnDef::new(Workspaces::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(Workspaces::OrganizationId)
.uuid()
.not_null(),
)
.col(ColumnDef::new(Workspaces::Name).string().not_null())
.col(
ColumnDef::new(Workspaces::Slug)
.string()
.not_null(),
)
.col(
ColumnDef::new(Workspaces::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Workspaces::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.name("fk_workspace_organization")
.from(Workspaces::Table, Workspaces::OrganizationId)
.to(Organizations::Table, Organizations::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
// Create unique index on organization_id + slug
manager
.create_index(
Index::create()
.unique()
.name("idx_workspaces_org_slug")
.table(Workspaces::Table)
.col(Workspaces::OrganizationId)
.col(Workspaces::Slug)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Workspaces::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Workspaces {
Table,
Id,
OrganizationId,
Name,
Slug,
CreatedAt,
UpdatedAt,
}
#[derive(DeriveIden)]
enum Organizations {
Table,
Id,
}

View File

@@ -0,0 +1,90 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Agents::Table)
.if_not_exists()
.col(
ColumnDef::new(Agents::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(Agents::WorkspaceId)
.uuid()
.not_null(),
)
.col(ColumnDef::new(Agents::Name).string().not_null())
.col(ColumnDef::new(Agents::Hostname).string().not_null())
.col(ColumnDef::new(Agents::IpAddress).string())
.col(ColumnDef::new(Agents::Version).string())
.col(ColumnDef::new(Agents::State).string().not_null())
.col(ColumnDef::new(Agents::DeploymentMode).string())
.col(
ColumnDef::new(Agents::LastSeenAt)
.timestamp_with_time_zone(),
)
.col(ColumnDef::new(Agents::Capabilities).json())
.col(ColumnDef::new(Agents::Labels).json())
.col(ColumnDef::new(Agents::TokenHash).string())
.col(
ColumnDef::new(Agents::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Agents::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.name("fk_agent_workspace")
.from(Agents::Table, Agents::WorkspaceId)
.to(Workspaces::Table, Workspaces::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Agents::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Agents {
Table,
Id,
WorkspaceId,
Name,
Hostname,
IpAddress,
Version,
State,
DeploymentMode,
LastSeenAt,
Capabilities,
Labels,
TokenHash,
CreatedAt,
UpdatedAt,
}
#[derive(DeriveIden)]
enum Workspaces {
Table,
Id,
}

View File

@@ -0,0 +1,115 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(VirtualHosts::Table)
.if_not_exists()
.col(
ColumnDef::new(VirtualHosts::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(VirtualHosts::WorkspaceId)
.uuid()
.not_null(),
)
.col(ColumnDef::new(VirtualHosts::Name).string().not_null())
.col(
ColumnDef::new(VirtualHosts::ServerName)
.string()
.not_null(),
)
.col(
ColumnDef::new(VirtualHosts::ListenPort)
.integer()
.not_null(),
)
.col(
ColumnDef::new(VirtualHosts::SslEnabled)
.boolean()
.not_null()
.default(false),
)
.col(ColumnDef::new(VirtualHosts::SslCertificateId).uuid())
.col(ColumnDef::new(VirtualHosts::Locations).json())
.col(
ColumnDef::new(VirtualHosts::Http2Enabled)
.boolean()
.not_null()
.default(false),
)
.col(
ColumnDef::new(VirtualHosts::Http3Enabled)
.boolean()
.not_null()
.default(false),
)
.col(
ColumnDef::new(VirtualHosts::GzipEnabled)
.boolean()
.not_null()
.default(true),
)
.col(ColumnDef::new(VirtualHosts::TargetAgents).json())
.col(
ColumnDef::new(VirtualHosts::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(VirtualHosts::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.name("fk_vh_workspace")
.from(VirtualHosts::Table, VirtualHosts::WorkspaceId)
.to(Workspaces::Table, Workspaces::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(VirtualHosts::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum VirtualHosts {
Table,
Id,
WorkspaceId,
Name,
ServerName,
ListenPort,
SslEnabled,
SslCertificateId,
Locations,
Http2Enabled,
Http3Enabled,
GzipEnabled,
TargetAgents,
CreatedAt,
UpdatedAt,
}
#[derive(DeriveIden)]
enum Workspaces {
Table,
Id,
}

View File

@@ -0,0 +1,83 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Upstreams::Table)
.if_not_exists()
.col(
ColumnDef::new(Upstreams::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(Upstreams::WorkspaceId)
.uuid()
.not_null(),
)
.col(ColumnDef::new(Upstreams::Name).string().not_null())
.col(
ColumnDef::new(Upstreams::Algorithm)
.string()
.not_null(),
)
.col(ColumnDef::new(Upstreams::Servers).json())
.col(ColumnDef::new(Upstreams::HealthCheck).json())
.col(ColumnDef::new(Upstreams::KeepaliveConnections).integer())
.col(ColumnDef::new(Upstreams::KeepaliveTimeout).integer())
.col(
ColumnDef::new(Upstreams::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Upstreams::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.name("fk_upstream_workspace")
.from(Upstreams::Table, Upstreams::WorkspaceId)
.to(Workspaces::Table, Workspaces::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Upstreams::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Upstreams {
Table,
Id,
WorkspaceId,
Name,
Algorithm,
Servers,
HealthCheck,
KeepaliveConnections,
KeepaliveTimeout,
CreatedAt,
UpdatedAt,
}
#[derive(DeriveIden)]
enum Workspaces {
Table,
Id,
}

View File

@@ -0,0 +1,99 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Certificates::Table)
.if_not_exists()
.col(
ColumnDef::new(Certificates::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(Certificates::WorkspaceId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(Certificates::Domain)
.string()
.not_null(),
)
.col(
ColumnDef::new(Certificates::IsWildcard)
.boolean()
.not_null()
.default(false),
)
.col(ColumnDef::new(Certificates::Provider).string())
.col(ColumnDef::new(Certificates::Status).string())
.col(ColumnDef::new(Certificates::IssuedAt).timestamp_with_time_zone())
.col(ColumnDef::new(Certificates::ExpiresAt).timestamp_with_time_zone())
.col(
ColumnDef::new(Certificates::AutoRenew)
.boolean()
.not_null()
.default(true),
)
.col(ColumnDef::new(Certificates::CertificatePem).text())
.col(ColumnDef::new(Certificates::PrivateKeyPem).text())
.col(
ColumnDef::new(Certificates::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Certificates::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.name("fk_certificate_workspace")
.from(Certificates::Table, Certificates::WorkspaceId)
.to(Workspaces::Table, Workspaces::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Certificates::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Certificates {
Table,
Id,
WorkspaceId,
Domain,
IsWildcard,
Provider,
Status,
IssuedAt,
ExpiresAt,
AutoRenew,
CertificatePem,
PrivateKeyPem,
CreatedAt,
UpdatedAt,
}
#[derive(DeriveIden)]
enum Workspaces {
Table,
Id,
}

View File

@@ -0,0 +1,74 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Users::Table)
.if_not_exists()
.col(
ColumnDef::new(Users::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Users::Email).string().not_null().unique_key())
.col(ColumnDef::new(Users::PasswordHash).string().not_null())
.col(ColumnDef::new(Users::Name).string())
.col(ColumnDef::new(Users::Role).string().not_null())
.col(
ColumnDef::new(Users::OrganizationId)
.uuid(),
)
.col(
ColumnDef::new(Users::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Users::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.name("fk_user_organization")
.from(Users::Table, Users::OrganizationId)
.to(Organizations::Table, Organizations::Id)
.on_delete(ForeignKeyAction::SetNull),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Users::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Users {
Table,
Id,
Email,
PasswordHash,
Name,
Role,
OrganizationId,
CreatedAt,
UpdatedAt,
}
#[derive(DeriveIden)]
enum Organizations {
Table,
Id,
}

6
migration/src/main.rs Normal file
View File

@@ -0,0 +1,6 @@
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(migration::Migrator).await;
}