Skip to content

Commit 2c74f73

Browse files
Shubham8287kazimuthgefjon
authored
Endpoint for pretty print migration plan (#3137)
# Description of Changes - Adds endpoint for for pretty printing migration plan. - It also changes current `publish` endpoint to optionally provide `MigrationToken` and `MigrationPolicy` to allow migration with breaking clients. # API and ABI breaking changes Backward compatible change to existing API and new Api # Expected complexity level and risk 2 # Testing - Existing smoketest should cover changes for `publish` endpoint. - For pretty print endpoint, smoketests can be written only after cli changes. --------- Signed-off-by: Shubham Mishra <[email protected]> Co-authored-by: James Gilles <[email protected]> Co-authored-by: Phoebe Goldman <[email protected]>
1 parent f394de3 commit 2c74f73

File tree

13 files changed

+414
-28
lines changed

13 files changed

+414
-28
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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,27 @@ pub enum PublishResult {
106106
PermissionDenied { name: DatabaseName },
107107
}
108108

109+
#[derive(serde::Serialize, serde::Deserialize, Debug, Default)]
110+
pub enum MigrationPolicy {
111+
#[default]
112+
Compatible,
113+
BreakClients,
114+
}
115+
116+
#[derive(serde::Serialize, serde::Deserialize, Debug, Default)]
117+
pub enum PrettyPrintStyle {
118+
#[default]
119+
AnsiColor,
120+
NoColor,
121+
}
122+
123+
#[derive(serde::Serialize, serde::Deserialize, Debug)]
124+
pub struct PrintPlanResult {
125+
pub migrate_plan: Box<str>,
126+
pub break_clients: bool,
127+
pub token: spacetimedb_lib::Hash,
128+
}
129+
109130
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
110131
pub enum DnsLookupResponse {
111132
/// 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;
@@ -146,9 +147,10 @@ impl Host {
146147
database: Database,
147148
host_type: HostType,
148149
program_bytes: Box<[u8]>,
150+
policy: MigrationPolicy,
149151
) -> anyhow::Result<UpdateDatabaseResult> {
150152
self.host_controller
151-
.update_module_host(database, host_type, self.replica_id, program_bytes)
153+
.update_module_host(database, host_type, self.replica_id, program_bytes, policy)
152154
.await
153155
}
154156
}
@@ -231,8 +233,11 @@ pub trait ControlStateWriteAccess: Send + Sync {
231233
&self,
232234
publisher: &Identity,
233235
spec: DatabaseDef,
236+
policy: MigrationPolicy,
234237
) -> anyhow::Result<Option<UpdateDatabaseResult>>;
235238

239+
async fn migrate_plan(&self, spec: DatabaseDef, style: PrettyPrintStyle) -> anyhow::Result<MigratePlanResult>;
240+
236241
async fn delete_database(&self, caller_identity: &Identity, database_identity: &Identity) -> anyhow::Result<()>;
237242

238243
// Energy
@@ -321,8 +326,13 @@ impl<T: ControlStateWriteAccess + ?Sized> ControlStateWriteAccess for Arc<T> {
321326
&self,
322327
identity: &Identity,
323328
spec: DatabaseDef,
329+
policy: MigrationPolicy,
324330
) -> anyhow::Result<Option<UpdateDatabaseResult>> {
325-
(**self).publish_database(identity, spec).await
331+
(**self).publish_database(identity, spec, policy).await
332+
}
333+
334+
async fn migrate_plan(&self, spec: DatabaseDef, style: PrettyPrintStyle) -> anyhow::Result<MigratePlanResult> {
335+
(**self).migrate_plan(spec, style).await
326336
}
327337

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

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

Lines changed: 136 additions & 5 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

@@ -474,6 +479,13 @@ pub struct PublishDatabaseQueryParams {
474479
#[serde(default)]
475480
clear: bool,
476481
num_replicas: Option<usize>,
482+
/// [`Hash`] of [`MigrationToken`]` to be checked if `MigrationPolicy::BreakClients` is set.
483+
///
484+
/// Users obtain such a hash via the `/database/:name_or_identity/pre-publish POST` route.
485+
/// This is a safeguard to require explicit approval for updates which will break clients.
486+
token: Option<spacetimedb_lib::Hash>,
487+
#[serde(default)]
488+
policy: MigrationPolicy,
477489
}
478490

479491
use std::env;
@@ -501,7 +513,12 @@ fn allow_creation(auth: &SpacetimeAuth) -> Result<(), ErrorResponse> {
501513
pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
502514
State(ctx): State<S>,
503515
Path(PublishDatabaseParams { name_or_identity }): Path<PublishDatabaseParams>,
504-
Query(PublishDatabaseQueryParams { clear, num_replicas }): Query<PublishDatabaseQueryParams>,
516+
Query(PublishDatabaseQueryParams {
517+
clear,
518+
num_replicas,
519+
token,
520+
policy,
521+
}): Query<PublishDatabaseQueryParams>,
505522
Extension(auth): Extension<SpacetimeAuth>,
506523
body: Bytes,
507524
) -> axum::response::Result<axum::Json<PublishResult>> {
@@ -551,6 +568,21 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
551568
}
552569
};
553570

571+
let policy: SchemaMigrationPolicy = match policy {
572+
MigrationPolicy::BreakClients => {
573+
if let Some(token) = token {
574+
Ok(SchemaMigrationPolicy::BreakClients(token))
575+
} else {
576+
Err((
577+
StatusCode::BAD_REQUEST,
578+
"Migration policy is set to `BreakClients`, but no migration token was provided.",
579+
))
580+
}
581+
}
582+
583+
MigrationPolicy::Compatible => Ok(SchemaMigrationPolicy::Compatible),
584+
}?;
585+
554586
log::trace!("Publishing to the identity: {}", database_identity.to_hex());
555587

556588
let op = {
@@ -592,6 +624,7 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
592624
num_replicas,
593625
host_type: HostType::Wasm,
594626
},
627+
policy,
595628
)
596629
.await
597630
.map_err(log_and_500)?;
@@ -619,6 +652,101 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
619652
}))
620653
}
621654

655+
#[derive(serde::Deserialize)]
656+
pub struct PrePublishParams {
657+
name_or_identity: NameOrIdentity,
658+
}
659+
660+
#[derive(serde::Deserialize)]
661+
pub struct PrePublishQueryParams {
662+
#[serde(default)]
663+
style: PrettyPrintStyle,
664+
}
665+
666+
pub async fn pre_publish<S: NodeDelegate + ControlStateDelegate>(
667+
State(ctx): State<S>,
668+
Path(PrePublishParams { name_or_identity }): Path<PrePublishParams>,
669+
Query(PrePublishQueryParams { style }): Query<PrePublishQueryParams>,
670+
Extension(auth): Extension<SpacetimeAuth>,
671+
body: Bytes,
672+
) -> axum::response::Result<axum::Json<PrintPlanResult>> {
673+
// User should not be able to print migration plans for a database that they do not own
674+
let database_identity = resolve_and_authenticate(&ctx, &name_or_identity, &auth).await?;
675+
let style = match style {
676+
PrettyPrintStyle::NoColor => AutoMigratePrettyPrintStyle::NoColor,
677+
PrettyPrintStyle::AnsiColor => AutoMigratePrettyPrintStyle::AnsiColor,
678+
};
679+
680+
let migrate_plan = ctx
681+
.migrate_plan(
682+
DatabaseDef {
683+
database_identity,
684+
program_bytes: body.into(),
685+
num_replicas: None,
686+
host_type: HostType::Wasm,
687+
},
688+
style,
689+
)
690+
.await
691+
.map_err(log_and_500)?;
692+
693+
match migrate_plan {
694+
MigratePlanResult::Success {
695+
old_module_hash,
696+
new_module_hash,
697+
breaks_client,
698+
plan,
699+
} => {
700+
let token = MigrationToken {
701+
database_identity,
702+
old_module_hash,
703+
new_module_hash,
704+
}
705+
.hash();
706+
707+
Ok(PrintPlanResult {
708+
token,
709+
migrate_plan: plan,
710+
break_clients: breaks_client,
711+
})
712+
}
713+
MigratePlanResult::AutoMigrationError(e) => Err((
714+
StatusCode::BAD_REQUEST,
715+
format!("Automatic migration is not possible: {e}"),
716+
)
717+
.into()),
718+
}
719+
.map(axum::Json)
720+
}
721+
722+
/// Resolves the [`NameOrIdentity`] to a database identity and checks if the
723+
/// `auth` identity owns the database.
724+
async fn resolve_and_authenticate<S: ControlStateDelegate>(
725+
ctx: &S,
726+
name_or_identity: &NameOrIdentity,
727+
auth: &SpacetimeAuth,
728+
) -> axum::response::Result<Identity> {
729+
let database_identity = name_or_identity.resolve(ctx).await?;
730+
731+
let database = worker_ctx_find_database(ctx, &database_identity)
732+
.await?
733+
.ok_or(NO_SUCH_DATABASE)?;
734+
735+
if database.owner_identity != auth.identity {
736+
return Err((
737+
StatusCode::UNAUTHORIZED,
738+
format!(
739+
"Identity does not own database, expected: {} got: {}",
740+
database.owner_identity.to_hex(),
741+
auth.identity.to_hex()
742+
),
743+
)
744+
.into());
745+
}
746+
747+
Ok(database_identity)
748+
}
749+
622750
#[derive(Deserialize)]
623751
pub struct DeleteDatabaseParams {
624752
name_or_identity: NameOrIdentity,
@@ -788,7 +916,8 @@ pub struct DatabaseRoutes<S> {
788916
pub logs_get: MethodRouter<S>,
789917
/// POST: /database/:name_or_identity/sql
790918
pub sql_post: MethodRouter<S>,
791-
919+
/// POST: /database/:name_or_identity/pre-publish
920+
pub pre_publish: MethodRouter<S>,
792921
/// GET: /database/: name_or_identity/unstable/timestamp
793922
pub timestamp_get: MethodRouter<S>,
794923
}
@@ -813,6 +942,7 @@ where
813942
schema_get: get(schema::<S>),
814943
logs_get: get(logs::<S>),
815944
sql_post: post(sql::<S>),
945+
pre_publish: post(pre_publish::<S>),
816946
timestamp_get: get(get_timestamp::<S>),
817947
}
818948
}
@@ -836,7 +966,8 @@ where
836966
.route("/schema", self.schema_get)
837967
.route("/logs", self.logs_get)
838968
.route("/sql", self.sql_post)
839-
.route("/unstable/timestamp", self.timestamp_get);
969+
.route("/unstable/timestamp", self.timestamp_get)
970+
.route("/pre-publish", self.pre_publish);
840971

841972
axum::Router::new()
842973
.route("/", self.root_post)

0 commit comments

Comments
 (0)