Skip to content

Commit 68d0045

Browse files
committed
print-plan endpoint & accordingly change publish endpoint
1 parent 0e73fef commit 68d0045

File tree

14 files changed

+403
-33
lines changed

14 files changed

+403
-33
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/client-api-messages/src/name.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,25 @@ pub enum PublishResult {
106106
PermissionDenied { name: DatabaseName },
107107
}
108108

109+
#[derive(serde::Serialize, serde::Deserialize, Debug)]
110+
pub enum MigrationPolicy {
111+
Compatible,
112+
BreakClients,
113+
}
114+
115+
#[derive(serde::Serialize, serde::Deserialize, Debug)]
116+
pub enum PrettyPrintStyle {
117+
AnsiColor,
118+
NoColor,
119+
}
120+
121+
#[derive(serde::Serialize, serde::Deserialize, Debug)]
122+
pub struct PrintPlanResult {
123+
pub migrate_plan: Box<str>,
124+
pub break_clients: bool,
125+
pub token: spacetimedb_lib::Hash,
126+
}
127+
109128
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
110129
pub enum DnsLookupResponse {
111130
/// The lookup was successful and the domain and identity are returned.

crates/client-api/src/lib.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ use http::StatusCode;
77

88
use spacetimedb::client::ClientActorIndex;
99
use spacetimedb::energy::{EnergyBalance, EnergyQuanta};
10-
use spacetimedb::host::{HostController, ModuleHost, NoSuchModule, UpdateDatabaseResult};
10+
use spacetimedb::host::{HostController, MigratePlanResult, ModuleHost, NoSuchModule, UpdateDatabaseResult};
1111
use spacetimedb::identity::{AuthCtx, Identity};
1212
use spacetimedb::messages::control_db::{Database, HostType, Node, Replica};
1313
use spacetimedb::sql;
1414
use spacetimedb_client_api_messages::http::{SqlStmtResult, SqlStmtStats};
1515
use spacetimedb_client_api_messages::name::{DomainName, InsertDomainResult, RegisterTldResult, SetDomainsResult, Tld};
1616
use spacetimedb_lib::{ProductTypeElement, ProductValue};
1717
use spacetimedb_paths::server::ModuleLogsDir;
18+
use spacetimedb_schema::auto_migrate::{MigrationPolicy, PrettyPrintStyle};
1819
use tokio::sync::watch;
1920

2021
pub mod auth;
@@ -134,9 +135,10 @@ impl Host {
134135
database: Database,
135136
host_type: HostType,
136137
program_bytes: Box<[u8]>,
138+
policy: MigrationPolicy,
137139
) -> anyhow::Result<UpdateDatabaseResult> {
138140
self.host_controller
139-
.update_module_host(database, host_type, self.replica_id, program_bytes)
141+
.update_module_host(database, host_type, self.replica_id, program_bytes, policy)
140142
.await
141143
}
142144
}
@@ -219,8 +221,11 @@ pub trait ControlStateWriteAccess: Send + Sync {
219221
&self,
220222
publisher: &Identity,
221223
spec: DatabaseDef,
224+
policy: MigrationPolicy,
222225
) -> anyhow::Result<Option<UpdateDatabaseResult>>;
223226

227+
async fn migrate_plan(&self, spec: DatabaseDef, style: PrettyPrintStyle) -> anyhow::Result<MigratePlanResult>;
228+
224229
async fn delete_database(&self, caller_identity: &Identity, database_identity: &Identity) -> anyhow::Result<()>;
225230

226231
// Energy
@@ -309,8 +314,13 @@ impl<T: ControlStateWriteAccess + ?Sized> ControlStateWriteAccess for Arc<T> {
309314
&self,
310315
identity: &Identity,
311316
spec: DatabaseDef,
317+
policy: MigrationPolicy,
312318
) -> anyhow::Result<Option<UpdateDatabaseResult>> {
313-
(**self).publish_database(identity, spec).await
319+
(**self).publish_database(identity, spec, policy).await
320+
}
321+
322+
async fn migrate_plan(&self, spec: DatabaseDef, style: PrettyPrintStyle) -> anyhow::Result<MigratePlanResult> {
323+
(**self).migrate_plan(spec, style).await
314324
}
315325

316326
async fn delete_database(&self, caller_identity: &Identity, database_identity: &Identity) -> anyhow::Result<()> {

crates/client-api/src/routes/database.rs

Lines changed: 132 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,21 @@ use http::StatusCode;
2020
use serde::Deserialize;
2121
use spacetimedb::database_logger::DatabaseLogger;
2222
use spacetimedb::host::module_host::ClientConnectedError;
23-
use spacetimedb::host::ReducerArgs;
2423
use spacetimedb::host::ReducerCallError;
2524
use spacetimedb::host::ReducerOutcome;
2625
use spacetimedb::host::UpdateDatabaseResult;
26+
use spacetimedb::host::{MigratePlanResult, ReducerArgs};
2727
use spacetimedb::identity::Identity;
2828
use spacetimedb::messages::control_db::{Database, HostType};
29-
use spacetimedb_client_api_messages::name::{self, DatabaseName, DomainName, PublishOp, PublishResult};
29+
use spacetimedb_client_api_messages::name::{
30+
self, DatabaseName, DomainName, MigrationPolicy, PrettyPrintStyle, PrintPlanResult, PublishOp, PublishResult,
31+
};
3032
use spacetimedb_lib::db::raw_def::v9::RawModuleDefV9;
3133
use spacetimedb_lib::identity::AuthCtx;
3234
use spacetimedb_lib::{sats, Timestamp};
35+
use spacetimedb_schema::auto_migrate::{
36+
MigrationPolicy as SchemaMigrationPolicy, MigrationToken, PrettyPrintStyle as AutoMigratePrettyPrintStyle,
37+
};
3338

3439
use super::subscribe::{handle_websocket, HasWebSocketOptions};
3540

@@ -469,6 +474,9 @@ pub struct PublishDatabaseQueryParams {
469474
#[serde(default)]
470475
clear: bool,
471476
num_replicas: Option<usize>,
477+
// `Hash` of `MigrationToken` to be checked if `MigrationPolicy::BreakClients` is set.
478+
token: Option<spacetimedb_lib::Hash>,
479+
policy: Option<MigrationPolicy>,
472480
}
473481

474482
use std::env;
@@ -496,7 +504,12 @@ fn allow_creation(auth: &SpacetimeAuth) -> Result<(), ErrorResponse> {
496504
pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
497505
State(ctx): State<S>,
498506
Path(PublishDatabaseParams { name_or_identity }): Path<PublishDatabaseParams>,
499-
Query(PublishDatabaseQueryParams { clear, num_replicas }): Query<PublishDatabaseQueryParams>,
507+
Query(PublishDatabaseQueryParams {
508+
clear,
509+
num_replicas,
510+
token,
511+
policy,
512+
}): Query<PublishDatabaseQueryParams>,
500513
Extension(auth): Extension<SpacetimeAuth>,
501514
body: Bytes,
502515
) -> axum::response::Result<axum::Json<PublishResult>> {
@@ -546,6 +559,21 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
546559
}
547560
};
548561

562+
let policy: SchemaMigrationPolicy = match policy.unwrap_or(MigrationPolicy::Compatible) {
563+
MigrationPolicy::BreakClients => {
564+
if let Some(token) = token {
565+
Ok(SchemaMigrationPolicy::BreakClients(token))
566+
} else {
567+
Err((
568+
StatusCode::BAD_REQUEST,
569+
"Migration policy is set to `BreakClients`, but no migration token was provided.",
570+
))
571+
}
572+
}
573+
574+
MigrationPolicy::Compatible => Ok(SchemaMigrationPolicy::Compatible),
575+
}?;
576+
549577
log::trace!("Publishing to the identity: {}", database_identity.to_hex());
550578

551579
let op = {
@@ -587,6 +615,7 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
587615
num_replicas,
588616
host_type: HostType::Wasm,
589617
},
618+
policy,
590619
)
591620
.await
592621
.map_err(log_and_500)?;
@@ -614,6 +643,102 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
614643
}))
615644
}
616645

646+
#[derive(serde::Deserialize)]
647+
pub struct PrintPlanParams {
648+
name_or_identity: NameOrIdentity,
649+
}
650+
651+
#[derive(serde::Deserialize)]
652+
pub struct PrintPlanQueryParams {
653+
style: Option<PrettyPrintStyle>,
654+
}
655+
656+
pub async fn print_migration_plan<S: NodeDelegate + ControlStateDelegate>(
657+
State(ctx): State<S>,
658+
Path(PrintPlanParams { name_or_identity }): Path<PrintPlanParams>,
659+
Query(PrintPlanQueryParams { style }): Query<PrintPlanQueryParams>,
660+
Extension(auth): Extension<SpacetimeAuth>,
661+
body: Bytes,
662+
) -> axum::response::Result<axum::Json<PrintPlanResult>> {
663+
// User should not be able to print migration plans for a database that they do not own
664+
let database_identity = resolve_and_authenticate(&ctx, &name_or_identity, &auth).await?;
665+
let style = style
666+
.map(|s| match s {
667+
PrettyPrintStyle::NoColor => AutoMigratePrettyPrintStyle::NoColor,
668+
PrettyPrintStyle::AnsiColor => AutoMigratePrettyPrintStyle::AnsiColor,
669+
})
670+
.unwrap_or_default();
671+
672+
let migrate_plan = ctx
673+
.migrate_plan(
674+
DatabaseDef {
675+
database_identity,
676+
program_bytes: body.into(),
677+
num_replicas: None,
678+
host_type: HostType::Wasm,
679+
},
680+
style,
681+
)
682+
.await
683+
.map_err(log_and_500)?;
684+
685+
match migrate_plan {
686+
MigratePlanResult::Success {
687+
old_module_hash,
688+
new_module_hash,
689+
breaks_client,
690+
plan,
691+
} => {
692+
let token = MigrationToken {
693+
database_identity,
694+
old_module_hash,
695+
new_module_hash,
696+
}
697+
.hash();
698+
699+
Ok(PrintPlanResult {
700+
token,
701+
migrate_plan: plan,
702+
break_clients: breaks_client,
703+
})
704+
}
705+
MigratePlanResult::AutoMigrationError(e) => Err((
706+
StatusCode::BAD_REQUEST,
707+
format!("Automatic migration is not possible: {e}"),
708+
)
709+
.into()),
710+
}
711+
.map(axum::Json)
712+
}
713+
714+
/// Resolves the `NameOrIdentity` to a database identity and checks if the
715+
/// `auth` identity owns the database.
716+
async fn resolve_and_authenticate<S: ControlStateDelegate>(
717+
ctx: &S,
718+
name_or_identity: &NameOrIdentity,
719+
auth: &SpacetimeAuth,
720+
) -> axum::response::Result<Identity> {
721+
let database_identity = name_or_identity.resolve(ctx).await?;
722+
723+
let database = worker_ctx_find_database(ctx, &database_identity)
724+
.await?
725+
.ok_or(NO_SUCH_DATABASE)?;
726+
727+
if database.owner_identity != auth.identity {
728+
return Err((
729+
StatusCode::BAD_REQUEST,
730+
format!(
731+
"Identity does not own database, expected: {} got: {}",
732+
database.owner_identity.to_hex(),
733+
auth.identity.to_hex()
734+
),
735+
)
736+
.into());
737+
}
738+
739+
Ok(database_identity)
740+
}
741+
617742
#[derive(Deserialize)]
618743
pub struct DeleteDatabaseParams {
619744
name_or_identity: NameOrIdentity,
@@ -783,7 +908,8 @@ pub struct DatabaseRoutes<S> {
783908
pub logs_get: MethodRouter<S>,
784909
/// POST: /database/:name_or_identity/sql
785910
pub sql_post: MethodRouter<S>,
786-
911+
/// POST: /database/print-plan/:name_or_identity/sql
912+
pub print_migration_plan: MethodRouter<S>,
787913
/// GET: /database/: name_or_identity/unstable/timestamp
788914
pub timestamp_get: MethodRouter<S>,
789915
}
@@ -808,6 +934,7 @@ where
808934
schema_get: get(schema::<S>),
809935
logs_get: get(logs::<S>),
810936
sql_post: post(sql::<S>),
937+
print_migration_plan: post(print_migration_plan::<S>),
811938
timestamp_get: get(get_timestamp::<S>),
812939
}
813940
}
@@ -835,6 +962,7 @@ where
835962

836963
axum::Router::new()
837964
.route("/", self.root_post)
965+
.route("/print-plan/:name_or_identity", self.print_migration_plan)
838966
.nest("/:name_or_identity", db_router)
839967
.route_layer(axum::middleware::from_fn_with_state(ctx, anon_auth_middleware::<S>))
840968
}

0 commit comments

Comments
 (0)